diff --git a/.gitignore b/.gitignore
index 1823f2e5796b4df5d7c5694ff922d2e906b415a8..3641ea8f464ca6631e49512bb28279697cecc4e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@
 .\#*
 /node_modules
 /var
+/rpmbuild
+/npm-debug.log
+
diff --git a/ChangeLog b/ChangeLog
index f4aa9ed6447be55b03b5f0570df1925454fbcaeb..dad86b034f61d8860a027d8004537bb96f9a8f02 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+train-2011.10.27:
+  * link fixing ('need help?' to point to SUMO): #378
+  * unit tests repaired: #469 (broken in fix to #82)
+  * improve handling of network errors: #448
+  * improve styling and language of email confirmation page: #349
+  * logging improvements: #455
+  * RPM generation script created (for installation of browserid on redhat [moz prod] boxes): #478
+  * SCHEMA CHANGES to improve database performance and scalability: #480
+  * change the health check call from '/ping.txt' to '/__heartbeat__': #481
+  * remove application level network timeouts (let the network stack do its job, the user can cancel if they get sick of it): #485
+  * improve messaging for unsupported browsers: #273, #484
+  * developer documentation improvements: #496
+
 train-2011.10.20:
   * android < 3.0 now supported: #461
   * properly set assertion expiration time to when they expire, not when they're issued: #433, #457, #458
diff --git a/README.md b/README.md
index 9ecb6efd18c83e9d3d9e9af4ded21a727c745e29..908b6ea4a28cb2eed5b084f9ed015dac2e7339c1 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,12 @@ or changes will be made.
 
 2. Boot up the VM:
 
-    $ cd browserid
-    $ vagrant up
-    $ vagrant ssh
-    vagrant@lucid32:browserid$ node ./run.js
+```
+cd browserid
+vagrant up
+vagrant ssh vagrant@lucid32:browserid
+node ./run.js
+```
 
 `vagrant up` will take a while. Go get a cup of coffee. This is because it downloads the 500MB VM.
 
diff --git a/bin/browserid b/bin/browserid
new file mode 100755
index 0000000000000000000000000000000000000000..d69fe6767558d96e9b15a8cbfa9360c435873269
--- /dev/null
+++ b/bin/browserid
@@ -0,0 +1,371 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const
+fs = require('fs'),
+path = require('path'),
+url = require('url'),
+http = require('http');
+sessions = require('connect-cookie-session');
+
+// add lib/ to the require path
+require.paths.unshift(path.join(__dirname, '..', 'lib'));
+
+const
+wsapi = require('browserid/wsapi.js'),
+ca = require('browserid/ca.js'),
+httputils = require('httputils.js'),
+express = require('express'),
+secrets = require('secrets.js'),
+db = require('db.js'),
+config = require('configuration.js'),
+heartbeat = require('heartbeat.js'),
+metrics = require("metrics.js"),
+logger = require("logging.js").logger,
+forward = require('browserid/http_forward'),
+urlparse = require('urlparse');
+
+var app = undefined;
+
+app = express.createServer();
+
+logger.info("browserid server starting up");
+
+const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
+const COOKIE_KEY = 'browserid_state';
+
+// verify that we have a keysigner configured
+if (!config.get('keysigner_url')) {
+  logger.error('missing required configuration - url for the keysigner (KEYSIGNER_URL in env)');
+  process.exit(1);
+}
+
+function internal_redirector(new_url, suppress_noframes) {
+  return function(req, resp, next) {
+    if (suppress_noframes)
+      resp.removeHeader('x-frame-options');
+    req.url = new_url;
+    return next();
+  };
+}
+
+function router(app) {
+  app.set("views", path.join(__dirname, "..", "resources", "views"));
+
+  app.set('view options', {
+    production: config.get('use_minified_resources')
+  });
+
+  // this should probably be an internal redirect
+  // as soon as relative paths are figured out.
+  app.get('/sign_in', function(req, res, next ) {
+    metrics.userEntry(req);
+    res.render('dialog.ejs', {
+      title: 'A Better Way to Sign In',
+      layout: 'dialog_layout.ejs',
+      useJavascript: true,
+      production: config.get('use_minified_resources')
+    });
+  });
+
+  app.get("/unsupported_dialog", function(req,res) {
+    res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+  });
+
+  // simple redirects (internal for now)
+  app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true));
+
+  // Used for a relay page for communication.
+  app.get("/relay", function(req,res, next) {
+    // Allow the relay to be run within a frame
+    res.removeHeader('x-frame-options');
+    res.render('relay.ejs', {
+      layout: false,
+      production: config.get('use_minified_resources')
+    });
+  });
+
+
+  app.get('/', function(req,res) {
+    res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
+  });
+
+  app.get("/signup", function(req, res) {
+    res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
+  });
+
+  app.get("/forgot", function(req, res) {
+    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
+  });
+
+  app.get("/signin", function(req, res) {
+    res.render('signin.ejs', {title: 'Sign In', fullpage: false});
+  });
+
+  app.get("/about", function(req, res) {
+    res.render('about.ejs', {title: 'About', fullpage: false});
+  });
+
+  app.get("/tos", function(req, res) {
+    res.render('tos.ejs', {title: 'Terms of Service', fullpage: false});
+  });
+
+  app.get("/privacy", function(req, res) {
+    res.render('privacy.ejs', {title: 'Privacy Policy', fullpage: false});
+  });
+
+  app.get("/verify_email_address", function(req, res) {
+    res.render('verifyuser.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
+  });
+
+  app.get("/add_email_address", function(req,res) {
+    res.render('verifyemail.ejs', {title: 'Verify Email Address', fullpage: false});
+  });
+
+  // REDIRECTS
+  REDIRECTS = {
+    "/manage": "/",
+    "/users": "/",
+    "/users/": "/",
+    "/primaries" : "/developers",
+    "/primaries/" : "/developers",
+    "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
+  };
+
+  // set up all the redirects
+  // oh my watch out for scope issues on var url - closure time
+  for (var url in REDIRECTS) {
+    (function(from,to) {
+      app.get(from, function(req, res) {
+        res.redirect(to);
+      });
+    })(url, REDIRECTS[url]);
+  }
+
+  // register all the WSAPI handlers
+  wsapi.setup(app);
+
+  // setup health check / heartbeat
+  heartbeat.setup(app, function(cb) {
+    // let's check stuff!  first the heartbeat of our keysigner
+    heartbeat.check(config.get('keysigner_url'), cb);
+  });
+
+  // the public key
+  app.get("/pk", function(req, res) {
+    res.json(ca.PUBLIC_KEY.toSimpleObject());
+  });
+
+  // vep bundle of JavaScript
+  app.get("/vepbundle", function(req, res) {
+    fs.readFile(__dirname + "/../node_modules/jwcrypto/vepbundle.js", function(error, content) {
+      if (error) {
+        res.writeHead(500);
+        res.end("oops");
+        console.log(error);
+      } else {
+        res.writeHead(200, {'Content-Type': 'text/javascript'});
+        res.write(content);
+        res.end();
+      }
+    });
+  });
+
+  app.get('/code_update', function(req, resp, next) {
+    logger.warn("code updated.  shutting down.");
+    process.exit();
+  });
+};
+
+// request to logger, dev formatted which omits personal data in the requests
+app.use(express.logger({
+  format: 'dev',
+  stream: {
+    write: function(x) {
+      logger.info(typeof x === 'string' ? x.trim() : x);
+    }
+  }
+}));
+
+// if these are verify requests, we'll redirect them off
+// to the verifier
+if (config.get('verifier_url')) {
+  app.use(function(req, res, next) {
+    if (/^\/verify$/.test(req.url)) {
+      forward(
+        config.get('verifier_url'), req, res,
+        function(err) {
+          if (err) {
+            logger.error("error forwarding request:", err);
+          }
+        });
+    } else {
+      return next();
+    }
+  });
+}
+
+// over SSL?
+var overSSL = (config.get('scheme') == 'https');
+
+app.use(express.cookieParser());
+
+var cookieSessionMiddleware = sessions({
+  secret: COOKIE_SECRET,
+  key: COOKIE_KEY,
+  cookie: {
+    path: '/wsapi',
+    httpOnly: true,
+    // IMPORTANT: we allow users to go 1 weeks on the same device
+    // without entering their password again
+    maxAge: config.get('authentication_duration_ms'),
+    secure: overSSL
+  }
+});
+
+// cookie sessions && cache control
+app.use(function(req, resp, next) {
+  // cookie sessions are only applied to calls to /wsapi
+  // as all other resources can be aggressively cached
+  // by layers higher up based on cache control headers.
+  // the fallout is that all code that interacts with sessions
+  // should be under /wsapi
+  if (/^\/wsapi/.test(req.url)) {
+    // explicitly disallow caching on all /wsapi calls (issue #294)
+    resp.setHeader('Cache-Control', 'no-cache, max-age=0');
+
+    // we set this parameter so the connect-cookie-session
+    // sends the cookie even though the local connection is HTTP
+    // (the load balancer does SSL)
+    if (overSSL)
+      req.connection.proxySecure = true;
+
+    return cookieSessionMiddleware(req, resp, next);
+
+  } else {
+    return next();
+  }
+});
+
+config.performSubstitution(app);
+
+// verify all JSON responses are objects - prevents regression on issue #217
+app.use(function(req, resp, next) {
+  var realRespJSON = resp.json;
+  resp.json = function(obj) {
+    if (!obj || typeof obj !== 'object') {
+      logger.error("INTERNAL ERROR!  *all* json responses must be objects");
+      throw "internal error";
+    }
+    realRespJSON.call(resp, obj);
+  };
+  return next();
+});
+
+app.use(express.bodyParser());
+
+// Check CSRF token early.  POST requests are only allowed to
+// /wsapi and they always must have a valid csrf token
+app.use(function(req, resp, next) {
+  // only on POSTs
+  if (req.method == "POST") {
+    var denied = false;
+    if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi
+      denied = true;
+      logger.warn("CSRF validation failure: POST only allowed to /wsapi urls.  not '" + req.url + "'");
+    }
+
+    else if (req.session === undefined) { // there must be a session
+      denied = true;
+      logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
+    }
+
+    // the session must have a csrf token
+    else if (typeof req.session.csrf !== 'string') {
+      denied = true;
+      logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
+    }
+
+    // and the token must match what is sent in the post body
+    else if (req.body.csrf != req.session.csrf) {
+      denied = true;
+      // if any of these things are false, then we'll block the request
+      logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf);
+    }
+
+    if (denied) return httputils.badRequest(resp, "CSRF violation");
+
+  }
+  return next();
+});
+
+// a tweak to get the content type of host-meta correct
+app.use(function(req, resp, next) {
+  if (req.url === '/.well-known/host-meta') {
+    resp.setHeader('content-type', 'text/xml');
+  }
+  next();
+});
+
+// Strict Transport Security
+app.use(function(req, resp, next) {
+  if (overSSL) {
+    // expires in 30 days, include subdomains like www
+    resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
+  }
+  next();
+});
+
+// prevent framing
+app.use(function(req, resp, next) {
+  resp.setHeader('x-frame-options', 'DENY');
+  next();
+});
+
+// add the actual URL handlers other than static
+router(app);
+
+// use the express 'static' middleware for serving of static files (cache headers, HTTP range, etc)
+app.use(express.static(path.join(__dirname, "..", "resources", "static")));
+
+// open the databse
+db.open(config.get('database'), function () {
+  var bindTo = config.get('bind_to');
+  app.listen(bindTo.port, bindTo.host, function() {
+    logger.info("running on http://" + app.address().address + ":" + app.address().port);
+  });
+});
\ No newline at end of file
diff --git a/bin/keysigner b/bin/keysigner
new file mode 100755
index 0000000000000000000000000000000000000000..ac5fa7f249bec8a734f218e5c2597338e239e538
--- /dev/null
+++ b/bin/keysigner
@@ -0,0 +1,96 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// I sign keys.  That's what I do.
+
+const
+path = require('path'),
+express = require('express');
+
+// add lib/ to the require path for our own libs
+require.paths.unshift(path.join(__dirname, '..', 'lib'));
+
+const
+config = require('configuration.js'),
+validate = require('validate.js'),
+metrics = require("metrics.js"),
+logger = require("logging.js").logger,
+ca = require('keysigner/ca.js'),
+heartbeat = require('heartbeat');
+
+// create an express server
+var app = express.createServer();
+
+// our server will log
+app.use(express.logger({
+  format: 'dev',
+  stream: {
+    write: function(x) {
+      logger.info(typeof x === 'string' ? x.trim() : x);
+    }
+  }
+}));
+
+app.use(function(req, resp, next) {
+  next();
+});
+
+// parse POST bodies
+app.use(express.bodyParser());
+
+heartbeat.setup(app);
+
+// and our single function
+app.post('/wsapi/cert_key', validate(["email", "pubkey"]), function(req, resp) {
+  // parse the pubkey
+  var pk = ca.parsePublicKey(req.body.pubkey);
+
+  // same account, we certify the key
+  // we certify it for a day for now
+  var expiration = new Date();
+  expiration.setTime(new Date().valueOf() + config.get('certificate_validity_ms'));
+  var cert = ca.certify(req.body.email, pk, expiration);
+
+  resp.writeHead(200, {'Content-Type': 'text/plain'});
+  resp.write(cert);
+  resp.end();
+});
+
+var bindTo = config.get('bind_to');
+app.listen(bindTo.port, bindTo.host, function() {
+  logger.info("running on http://" + app.address().address + ":" + app.address().port);
+});
diff --git a/performance/run.js b/bin/load_gen
similarity index 97%
rename from performance/run.js
rename to bin/load_gen
index fea7840d007065bffe5d4f0721b24240b8b4a916..b3cd137a90a817b068aad1648ade916f8544f962 100755
--- a/performance/run.js
+++ b/bin/load_gen
@@ -20,7 +20,7 @@
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
- *   Lloyd Hilaiel <lloyd@hilaiel.com> 
+ *   Lloyd Hilaiel <lloyd@hilaiel.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
@@ -40,8 +40,7 @@
  * tool, which is capable of analysing the maximum active users that
  * a browserid deployment can support */
 
-
-// option processing with optimist 
+// option processing with optimist
 var argv = require('optimist')
 .usage('Apply load to a BrowserID server.\nUsage: $0', [ "foo" ])
 .alias('h', 'help')
@@ -92,14 +91,14 @@ var completed = {
 const activitiesPerUserPerSecond = (40.0 / ( 24 * 60 * 60 )); 
 
 // activities
-var activity = { 
+var activity = {
   "signup": {
     // a %20 montly growth rate means there's a 20% probability of
     // the monthly activity generated by an active user being a
     // new user signup
     probability: (1.0 / (40 * 28 * .2))
   },
-  "reset_pass": { 
+  "reset_pass": {
     // users forget their password once every 4 weeks
     probability: (1.0 / (40 * 28.0))
   },
@@ -129,7 +128,7 @@ var activity = {
 // now attach "start functions" to the activity map by including
 // the implementation of each activity
 Object.keys(activity).forEach(function(k) {
-  activity[k].startFunc = require("./lib/" + k).startFunc;
+  activity[k].startFunc = require("../lib/performance/" + k).startFunc;
 });
 
 // probs is a 2d array mapping normalized probabilities from 0-1 to
@@ -237,12 +236,12 @@ function poll() {
 
   // how many active users would we like to simulate
   var targetActive = args.m;
-  
+
   // if we're not throttled, then we'll trying 150% as many as
   // we're simulating right now.  If we're not simulating at least
   // 10000 active users, that shall be our lower bound
   if (!targetActive) {
-    if (averages[0] > 10000) targetActive = averages[0] * 1.5; 
+    if (averages[0] > 10000) targetActive = averages[0] * 1.5;
     else targetActive = 10000;
   }
 
diff --git a/verifier/app.js b/bin/verifier
old mode 100644
new mode 100755
similarity index 63%
rename from verifier/app.js
rename to bin/verifier
index 8a7d95e62ca09e286dfe23b7f68b130079193f05..8da38a12f39c7d57a035635a3e1271360376af46
--- a/verifier/app.js
+++ b/bin/verifier
@@ -1,3 +1,5 @@
+#!/usr/bin/env node
+
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -34,25 +36,49 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-const   path = require('path'),
-         url = require('url'),
-          fs = require('fs'),
-certassertion = require('./lib/certassertion.js'),
-     express = require('express'),
-     metrics = require('../libs/metrics.js'),
-     logger = require('../libs/logging.js').logger;
+const
+sys = require("sys"),
+path = require('path'),
+url = require('url'),
+fs = require('fs'),
+express = require('express'),
+certassertion = require('../lib/verifier/certassertion.js'),
+metrics = require('../lib/metrics'),
+heartbeat = require('../lib/heartbeat'),
+logger = require('../lib/logging').logger,
+config = require('../lib/configuration');
 
 logger.info("verifier server starting up");
 
-// updating this call for certs now (Ben - 2011-09-06)
-// assertion is the single assertion of email
-// audience is the intended audience
-// certificates is the list of chained certificates, CSV-style
-function doVerify(req, resp, next) {
+var app = express.createServer();
+
+// request to logger, dev formatted which omits personal data in the requests
+app.use(express.logger({
+  stream: {
+    write: function(x) {
+      logger.info(typeof x === 'string' ? x.trim() : x);
+    }
+  }
+}));
+
+app.use(express.bodyParser());
+
+// code_update is an internal api that causes the node server to
+// shut down.  This should never be externally accessible and
+// is used during the dead simple deployment procedure.
+app.get("/code_update", function (req, resp) {
+  logger.warn("code updated.  shutting down.");
+  process.exit();
+});
+
+// setup health check / heartbeat
+heartbeat.setup(app);
+
+app.post('/verify', function(req, resp, next) {
   req.body = req.body || {}
 
-  var assertion = (req.query && req.query.assertion) ? req.query.assertion : req.body.assertion;
-  var audience = (req.query && req.query.audience) ? req.query.audience : req.body.audience;
+  var assertion = req.body.assertion;
+  var audience = req.body.audience;
 
   if (!(assertion && audience))
     return resp.json({ status: "failure", reason: "need assertion and audience" });
@@ -86,41 +112,13 @@ function doVerify(req, resp, next) {
       resp.json({"status":"failure", reason: (error ? error.toString() : "unknown")});
       metrics.report('verify', {
         result: 'failure',
-        rp: audienceFromAssertion
+        rp: audience
       });
     });
 
-}
-
-exports.setup = function(app) {
-  // request to logger, dev formatted which omits personal data in the requests
-
-  app.use(express.logger({
-    format: 'dev',
-    stream: {
-      write: function(x) {
-        logger.info(typeof x === 'string' ? x.trim() : x);
-      }
-    }
-  }));
-
-  app.use(express.bodyParser());
-
-  // code_update is an internal api that causes the node server to
-  // shut down.  This should never be externally accessible and
-  // is used during the dead simple deployment procedure.
-  app.get("/code_update", function (req, resp) {
-    logger.warn("code updated.  shutting down.");
-    process.exit();
-  });
-
-  // A simple ping hook for monitoring.
-  app.get("/ping.txt", function(req ,resp) {
-    resp.writeHead(200, {"Content-Type": "text/plain"})
-    resp.write("k.");
-    resp.end();
-  });
+});
 
-  app.post('/', doVerify);
-  app.post('/verify', doVerify);
-};
+var bindTo = config.get('bind_to');
+app.listen(bindTo.port, bindTo.host, function(conn) {
+  logger.info("running on http://" + app.address().address + ":" + app.address().port);
+});
diff --git a/browserid/app.js b/browserid/app.js
deleted file mode 100644
index a0f26efc28471de7427facc9fd6f4e6ed105a007..0000000000000000000000000000000000000000
--- a/browserid/app.js
+++ /dev/null
@@ -1,324 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla BrowserID.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const
-fs = require('fs'),
-path = require('path'),
-url = require('url'),
-wsapi = require('./lib/wsapi.js'),
-ca = require('./lib/ca.js'),
-httputils = require('./lib/httputils.js'),
-sessions = require('connect-cookie-session'),
-express = require('express'),
-secrets = require('../libs/secrets.js'),
-db = require('./lib/db.js'),
-configuration = require('../libs/configuration.js'),
-substitution = require('../libs/substitute.js');
-metrics = require("../libs/metrics.js"),
-logger = require("../libs/logging.js").logger;
-
-logger.info("browserid server starting up");
-
-// open the databse
-db.open(configuration.get('database'));
-
-const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', configuration.get('var_path'));
-const COOKIE_KEY = 'browserid_state';
-
-function internal_redirector(new_url, suppress_noframes) {
-  return function(req, resp, next) {
-    if (suppress_noframes)
-      resp.removeHeader('x-frame-options');
-    req.url = new_url;
-    return next();
-  };
-}
-
-function router(app) {
-  app.set("views", __dirname + '/views');
-
-  app.set('view options', {
-    production: configuration.get('use_minified_resources')
-  });
-
-  // this should probably be an internal redirect
-  // as soon as relative paths are figured out.
-  app.get('/sign_in', function(req, res, next ) {
-    metrics.userEntry(req);
-    res.render('dialog.ejs', {
-      title: 'A Better Way to Sign In',
-      layout: false,
-      production: configuration.get('use_minified_resources')
-    });
-  });
-
-  // simple redirects (internal for now)
-  app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true));
-
-  // Used for a relay page for communication.
-  app.get("/relay", function(req,res, next) {
-    // Allow the relay to be run within a frame
-    res.removeHeader('x-frame-options');
-    res.render('relay.ejs', {
-      layout: false,
-      production: configuration.get('use_minified_resources')
-    });
-  });
-
-
-  app.get('/', function(req,res) {
-    res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
-  });
-
-  // BA removed .html URLs. If we have 404s,
-  // we should set up some redirects
-  
-  app.get("/signup", function(req, res) {
-    res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
-  });
-
-  app.get("/forgot", function(req, res) {
-    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
-  });
-
-  app.get("/signin", function(req, res) {
-    res.render('signin.ejs', {title: 'Sign In', fullpage: false});
-  });
-
-  app.get("/about", function(req, res) {
-    res.render('about.ejs', {title: 'About', fullpage: false});
-  });
-
-  app.get("/tos", function(req, res) {
-    res.render('tos.ejs', {title: 'Terms of Service', fullpage: false});
-  });
-
-  app.get("/privacy", function(req, res) {
-    res.render('privacy.ejs', {title: 'Privacy Policy', fullpage: false});
-  });
-
-  app.get("/verify_email_address", function(req, res) {
-    res.render('verifyuser.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
-  });
-
-  app.get("/add_email_address", function(req,res) {
-    res.render('verifyemail.ejs', {title: 'Verify Email Address', fullpage: false});
-  });
-
-  // REDIRECTS
-  REDIRECTS = {
-    "/manage": "/",
-    "/users": "/",
-    "/users/": "/",    
-    "/primaries" : "/developers",
-    "/primaries/" : "/developers",
-    "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
-  };
-
-  // set up all the redirects
-  // oh my watch out for scope issues on var url - closure time
-  for (var url in REDIRECTS) {
-    (function(from,to) {
-      app.get(from, function(req, res) {
-        res.redirect(to);
-      });
-    })(url, REDIRECTS[url]);
-  }
-
-  // register all the WSAPI handlers
-  wsapi.setup(app);
-
-  // the public key
-  app.get("/pk", function(req, res) {
-    res.json(ca.PUBLIC_KEY.toSimpleObject());
-  });
-
-  // vep bundle of JavaScript
-  app.get("/vepbundle", function(req, res) {
-    fs.readFile(__dirname + "/../node_modules/jwcrypto/vepbundle.js", function(error, content) {
-      if (error) {
-        res.writeHead(500);
-        res.end("oops");
-        console.log(error);
-      } else {
-        res.writeHead(200, {'Content-Type': 'text/javascript'});
-        res.write(content);
-        res.end();
-      }
-    });
-  });
-
-  app.get('/code_update', function(req, resp, next) {
-    logger.warn("code updated.  shutting down.");
-    process.exit();
-  });
-};
-
-exports.setup = function(server) {
-  // request to logger, dev formatted which omits personal data in the requests
-  server.use(express.logger({
-    format: 'dev',
-    stream: {
-      write: function(x) {
-        logger.info(typeof x === 'string' ? x.trim() : x);
-      }
-    }
-  }));
-
-  // over SSL?
-  var overSSL = (configuration.get('scheme') == 'https');
-
-  server.use(express.cookieParser());
-
-  var cookieSessionMiddleware = sessions({
-    secret: COOKIE_SECRET,
-    key: COOKIE_KEY,
-    cookie: {
-      path: '/wsapi',
-      httpOnly: true,
-      // IMPORTANT: we allow users to go 1 weeks on the same device
-      // without entering their password again
-      maxAge: configuration.get('authentication_duration_ms'),
-      secure: overSSL
-    }
-  });
-
-  // cookie sessions && cache control
-  server.use(function(req, resp, next) {
-    // cookie sessions are only applied to calls to /wsapi
-    // as all other resources can be aggressively cached
-    // by layers higher up based on cache control headers.
-    // the fallout is that all code that interacts with sessions
-    // should be under /wsapi
-    if (/^\/wsapi/.test(req.url)) {
-      // explicitly disallow caching on all /wsapi calls (issue #294)
-      resp.setHeader('Cache-Control', 'no-cache, max-age=0');
-
-      // we set this parameter so the connect-cookie-session
-      // sends the cookie even though the local connection is HTTP
-      // (the load balancer does SSL)
-      if (overSSL)
-        req.connection.proxySecure = true;
-
-      return cookieSessionMiddleware(req, resp, next);
-
-    } else {
-      return next();
-    }
-  });
-
-  // verify all JSON responses are objects - prevents regression on issue #217
-  server.use(function(req, resp, next) {
-    var realRespJSON = resp.json;
-    resp.json = function(obj) {
-      if (!obj || typeof obj !== 'object') {
-        logger.error("INTERNAL ERROR!  *all* json responses must be objects");
-        throw "internal error";
-      }
-      realRespJSON.call(resp, obj);
-    };
-    return next();
-  });
-
-  server.use(express.bodyParser());
-
-  // Check CSRF token early.  POST requests are only allowed to
-  // /wsapi and they always must have a valid csrf token
-  server.use(function(req, resp, next) {
-    // only on POSTs
-    if (req.method == "POST") {
-      var denied = false;
-      if (!/^\/wsapi/.test(req.url)) { // post requests only allowed to /wsapi
-        denied = true;
-        logger.warn("CSRF validation failure: POST only allowed to /wsapi urls.  not '" + req.url + "'");
-      }
-
-      if (req.session === undefined) { // there must be a session
-        denied = true;
-        logger.warn("CSRF validation failure: POST calls to /wsapi require an active session");
-      }
-
-      // the session must have a csrf token
-      if (typeof req.session.csrf !== 'string') {
-        denied = true;
-        logger.warn("CSRF validation failure: POST calls to /wsapi require an csrf token to be set");
-      }
-
-      // and the token must match what is sent in the post body
-      if (req.body.csrf != req.session.csrf) {
-        denied = true;
-        // if any of these things are false, then we'll block the request
-        logger.warn("CSRF validation failure, token mismatch. got:" + req.body.csrf + " want:" + req.session.csrf);
-      }
-
-      if (denied) return httputils.badRequest(resp, "CSRF violation");
-
-    }
-    return next();
-  });
-
-  // a tweak to get the content type of host-meta correct
-  server.use(function(req, resp, next) {
-    if (req.url === '/.well-known/host-meta') {
-      resp.setHeader('content-type', 'text/xml');
-    }
-    next();
-  });
-
-  // Strict Transport Security
-  server.use(function(req, resp, next) {
-      if (overSSL) {
-        // expires in 30 days, include subdomains like www
-        resp.setHeader("Strict-Transport-Security", "max-age=2592000; includeSubdomains");
-      }
-      next();
-    });
-
-  // prevent framing
-  server.use(function(req, resp, next) {
-    resp.setHeader('x-frame-options', 'DENY');
-    next();
-  });
-
-  // add middleware to re-write urls if needed
-  configuration.performSubstitution(server);
-
-  // add the actual URL handlers other than static
-  router(server);
-}
-
-exports.shutdown = function() {
-  db.close();
-};
diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html
deleted file mode 100644
index 716ec5323d770965ab866d14c51cb36583920fce..0000000000000000000000000000000000000000
--- a/browserid/static/dialog/qunit.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<html>
-	<head>
-		<link rel="stylesheet" type="text/css" href="/funcunit/qunit/qunit.css" />
-		<title>dialog QUnit Test</title>
-		<script type='text/javascript' src='/vepbundle'></script>
-		<script type='text/javascript' src='/steal/steal.js?/dialog/test/qunit'></script>
-	</head>
-	<body>
-
-		<h1 id="qunit-header">dialog Test Suite</h1>
-		<h2 id="qunit-banner"></h2>
-		<div id="qunit-testrunner-toolbar"></div>
-		<h2 id="qunit-userAgent"></h2>
-		<div id="test-content">
-      <div id="page_controller">
-
-        <div id="formWrap">
-            <div class="contents"></div>
-        </div>
-
-        <div id="wait">
-            <div class="contents"></div>
-        </div>
-
-        <div id="error">
-            <div class="contents"></div>
-        </div>
-
-      </div>
-      <span id="email"></span>
-      <span id="cannotconfirm" class="error">Cannot confirm</span>
-      <span id="cannotcommunicate" class="error">Cannot communicate</span>
-      <span id="siteinfo" class="error"><span class="website"></span></span>
-      <span class=".hint">Hint</span>
-    </div>
-		<ol id="qunit-tests"></ol>
-		<div id="qunit-test-area"></div>
-	</body>
-</html>
diff --git a/browserid/static/dialog/resources/error-messages.js b/browserid/static/dialog/resources/error-messages.js
deleted file mode 100644
index 7064d82d9f4a1fdaaccd01341763679ab4a412e0..0000000000000000000000000000000000000000
--- a/browserid/static/dialog/resources/error-messages.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla BrowserID.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-BrowserID.Errors = (function(){
-  "use strict";
-
-  var Errors = {
-    authenticate: {
-      type: "serverError",
-      title: "Error Authenticating",
-      message: "There was a technical problem while trying to log you in.  Yucky!"
-    },
-
-    addEmail: {
-      type: "serverError",
-      title: "Error Adding Address",
-      message: "There was a technical problem while trying to add this email to your account.  Yucky!"
-    },
-
-    checkAuthentication: {
-      type: "serverError",
-      title: "Error Checking Authentication",
-      message: "There was a technical problem while trying to log you in.  Yucky!"
-    },
-
-    createUser: {
-      type: "serverError",
-      title: "Error Creating Account",
-      message: "There was a technical problem while trying to create your account.  Yucky!"
-    },
-
-    getAssertion: {
-      type: "serverError",
-      title: "Error Getting Assertion",
-      message: "There was a technical problem while trying to authenticate you.  Yucky!"
-    },
-
-    isEmailRegistered: {
-      type: "serverError",
-      title: "Error Checking Email Address",
-      message: "There was a technical problem while trying to check that email address.  Yucky!"
-    },
-
-    logoutUser: {
-      type: "serverError",
-      title: "Logout Failed",
-      message: "An error was encountered while signing you out.  Yucky!"
-    },
-
-    offline: {
-      type: "networkError",
-      title: "You are offline!",
-      message: "Unfortunately, BrowserID cannot communicate while offline!"
-    },
-
-    registration: {
-      type: "serverError",
-      title: "Registration Failed",
-      message: "An error was encountered and the signup cannot be completed.  Yucky!"
-    },
-
-    requestPasswordReset: {
-      type: "serverError",
-      title: "Error Resetting Password",
-      message: "There was a technical problem while trying to reset your password."
-    },
-
-    signIn: {
-      type: "serverError",
-      title: "Signin Failed",
-      message: "There was an error signing in. Yucky!"
-    },
-
-    syncAddress: {
-      type: "serverError",
-      title: "Error Syncing Address",
-      message: "There was a technical problem while trying to synchronize your account.  Yucky!"
-    }
-
-  };
-
-
-  return Errors;
-}());
-
-
diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js
deleted file mode 100644
index 1733ac5548a0e89a31aa0f86527d287d7fc4d2ca..0000000000000000000000000000000000000000
--- a/browserid/static/dialog/test/qunit/qunit.js
+++ /dev/null
@@ -1,21 +0,0 @@
-steal("/dialog/resources/browserid.js",
-      "/dialog/resources/storage.js",
-      "/dialog/resources/tooltip.js",
-      "/dialog/resources/validation.js",
-      "/dialog/resources/underscore-min.js")
-  .plugins(
-    "jquery", 
-    "jquery/controller",
-    "jquery/controller/subscribe",
-    "jquery/controller/view",
-    "jquery/view/ejs",
-    "funcunit/qunit")
-	.views('testBodyTemplate.ejs')
-	.views('wait.ejs')
-  .then("browserid_unit_test")
-  .then("pages/add_email_address_test")
-  .then("controllers/page_controller_unit_test")
-  .then("resources/validation_unit_test")
-  .then("resources/storage_unit_test")
-  .then("resources/network_unit_test")
-  .then("resources/user_unit_test")
diff --git a/browserid/static/dialog/views/authenticate.ejs b/browserid/static/dialog/views/authenticate.ejs
deleted file mode 100644
index 3341bac640d68f72ff23d609be20cdc9eb3e736f..0000000000000000000000000000000000000000
--- a/browserid/static/dialog/views/authenticate.ejs
+++ /dev/null
@@ -1,54 +0,0 @@
-  <strong>Sign in using</strong>
-  <ul class="inputs">
-
-      <li>
-          <label for="email" class="serif">Email</label>
-          <input id="email" class="sans" type="email" autocapitalize="off" autocorrect="off" value="<%= email %>" />
-
-          <div id="email_format" class="tooltip" for="email">
-            This field must be an email address.
-          </div>
-
-          <div id="email_required" class="tooltip" for="email">
-            The email field is required.
-          </div>
-      </li>
-
-      <li id="hint_section" class="start">
-          <p>Enter your email address to sign in to <strong><%= sitename %></strong></p>
-      </li>
-
-      <li id="create_text_section" class="newuser">
-          <p><strong>Welcome to BrowserID!</strong></p>
-          <p>This email looks new, so let's get you set up.</p>
-      </li>
-
-      <li id="password_section" class="returning">
-
-          <label for="password" class="half serif">Password</label>
-          <div class="half right">
-              <a id="forgotPassword" href="#">forgot your password?</a>
-          </div>
-          <input id="password" class="sans" type="password" maxlength="80">
-
-
-          <div id="password_required" class="tooltip" for="password">
-            The password field is required.
-          </div>
-
-          <div id="cannot_authenticate" class="tooltip" for="password">
-            The account cannot be logged in with this username and password.
-          </div>
-      </li>
-  
-  </ul>
-
-  <div class="submit cf">
-      <button class="start">next</button>
-      <button class="newuser">Verify Email</button>
-
-      <button class="returning">sign in</button>
-
-      <button class="forgot">Reset Password</button>
-      <button id="cancel_forgot_password" class="forgot">Cancel</button>
-  </div>
diff --git a/browserid/static/ping.txt b/browserid/static/ping.txt
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/browserid/tests/lib/start-stop.js b/browserid/tests/lib/start-stop.js
deleted file mode 100644
index f9e69b8a53d1c2021ff42fb2e01da6ee62c92311..0000000000000000000000000000000000000000
--- a/browserid/tests/lib/start-stop.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla BrowserID.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const assert = require('assert'),
-      fs = require('fs'),
-      path = require('path'),
-      wsapi = require('./wsapi.js');
-
-const varPath = path.join(path.dirname(path.dirname(__dirname)), "var");
-
-function removeVarDir() {
-  try {
-    fs.readdirSync(varPath).forEach(function(f) {
-        fs.unlinkSync(path.join(varPath, f));
-    });
-    fs.rmdirSync(varPath);
-  } catch(e) {}
-}
-
-exports.addStartupBatches = function(suite) {
-  suite.addBatch({
-    "remove the user database": {
-      topic: function() {
-        removeVarDir();
-        fs.mkdirSync(varPath, 0755);
-        return true;
-      },
-      "directory should exist": function(x) {
-        assert.ok(fs.statSync(varPath).isDirectory());
-      }
-    }
-  });
-
-  suite.addBatch({
-    "run the server": {
-      topic: function() {
-        const server = require("../../run.js");
-        server.runServer();
-        return true;
-      },
-      "server should be running": {
-        topic: wsapi.get('/ping.txt'),
-        "server is running": function (r, err) {
-          assert.equal(r.code, 200);
-        }
-      }
-    }
-  });
-
-  suite.addBatch({
-    "wait for readiness": {
-      topic: function() {
-        var cb = this.callback;
-        require("../../lib/db.js").onReady(function() { cb(true) });
-      },
-      "readiness has arrived": function(v) {
-        assert.ok(v);
-      }
-    }
-  });
-};
-
-exports.addShutdownBatches = function(suite) {
-  // stop the server
-  suite.addBatch({
-    "stop the server": {
-      topic: function() {
-        const server = require("../../run.js");
-        var cb = this.callback;
-        server.stopServer(function() { cb(true); });
-      },
-      "stopped": function(x) {
-        assert.strictEqual(x, true);
-      }
-    }
-  });
-
-  // stop the database
-  suite.addBatch({
-    "stop the database": {
-      topic: function() {
-        require("../../lib/db.js").close(this.callback);
-      },
-      "stopped": function(x) {
-        assert.isUndefined(x);
-      }
-    }
-  });
-
-  // clean up
-  suite.addBatch({
-    "clean up": {
-      topic: function() {
-        removeVarDir();
-        return true;
-      },
-      "directory should not exist": function(x) {
-        assert.throws(function(){ fs.statSync(varPath) });
-      }
-    }
-  });
-}
\ No newline at end of file
diff --git a/browserid/views/index.ejs b/browserid/views/index.ejs
deleted file mode 100644
index 5725e35fd9d833061f1e36a5304cad662d90ef23..0000000000000000000000000000000000000000
--- a/browserid/views/index.ejs
+++ /dev/null
@@ -1,40 +0,0 @@
-    <div id="content" style="display:none;">
-        <div id="manage">
-            <h1 class="serif">Account Manager</h1>
-            <div class="edit cf">
-                <strong>Your Email Addresses</strong>
-
-                <a id="manageAccounts" href="#">edit</a>
-                <a id="cancelManage" href="#">done</a>
-            </div>
-            <ul id="emailList">
-            </ul>
-            <div id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></div>
-        </div>
-    </div>
-
-    <div id="vAlign" style="display:none;">
-        <div id="signUp">
-            <div id="card"><img src="/i/slit.png"></div>
-            <div id="hint"></div>
-            <div id="status"></div>
-
-            <p>Connect with <em>BrowserID</em>, the safest &amp; easiest way to sign in.</p>
-            <p>
-              <a class="granted info" href="/about">Take the tour</a> or 
-              <a href="/signup" class="button granted create">sign up</a>
-            </p>
-        </div>
-    </div>
-
-<script type="text/html" id="templateUser">
-  <li class="identity cf">
-    <div class="email">{{ email }}</div>
-    <div class="activity cf">
-      <button class="delete">remove</button>
-      <!-- removed registration info. We want to replace this with Last Used At ... -->
-      <!-- <abbr title="Registered: {{ created }}" class="status">Registered {{ relative }}.</abbr>-->
-    </div>
-  </li>
-</script>
-
diff --git a/DEPLOYMENT.md b/docs/DEPLOYMENT.md
similarity index 98%
rename from DEPLOYMENT.md
rename to docs/DEPLOYMENT.md
index 07fe7cb6e67c0bed051f18f273711ae7a05ba5dd..1e8b7b606fff18ce905173a46c624df1fbc287e8 100644
--- a/DEPLOYMENT.md
+++ b/docs/DEPLOYMENT.md
@@ -156,16 +156,16 @@ post update hook, annotated to help you follow along:
 ### 5. get node servers running
 
 At this point, pushing code to gitolite will cause /home/browserid/code to be updated.  Now
-we need to get the servers running!  Manually we can verify that the servers will run. 
+we need to get the servers running!  Manually we can verify that the servers will run.
 For the browser id server:
 
-    cd /home/browserid/code/browserid && sudo -u www-data ./run.js  
+    cd /home/browserid/code/browserid && sudo -u www-data ./run.js
 
 And for the verifier:
 
-    cd /home/browserid/code/verifier && sudo -u www-data ./run.js  
+    cd /home/browserid/code/verifier && sudo -u www-data ./run.js
 
-Now let's set up [monit] to restart the node.js servers:  
+Now let's set up [monit] to restart the node.js servers:
 
   1. install monit: `sudo apt-get install monit`
   2. enable monit by editing `/etc/default/monit`
@@ -181,7 +181,7 @@ include /etc/monit.d/*
 
 <pre>
 #!/bin/bash
-/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &    
+/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &
 </pre>
 
   5. create a file to run the verifier at `/etc/monit.d/verifier`:
@@ -192,7 +192,7 @@ check host verifier with address 127.0.0.1
         as uid "www-data" and gid "www-data"
     stop program  = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/verifier/run.js'"
     if failed port 62800 protocol HTTP
-        request /ping.txt
+        request /__heartbeat__
         with timeout 10 seconds
         then restart
 </pre>
@@ -205,7 +205,7 @@ check host browserid.org with address 127.0.0.1
         as uid "www-data" and gid "www-data"
     stop program  = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/browserid/run.js'"
     if failed port 62700 protocol HTTP
-        request /ping.txt
+        request /__heartbeat__
         with timeout 10 seconds
         then restart
 </pre>
diff --git a/performance/README.md b/docs/LOAD_GENERATION.md
similarity index 100%
rename from performance/README.md
rename to docs/LOAD_GENERATION.md
diff --git a/ORGANIZATION.md b/docs/ORGANIZATION.md
similarity index 100%
rename from ORGANIZATION.md
rename to docs/ORGANIZATION.md
diff --git a/rp/index.html b/example/index.html
similarity index 100%
rename from rp/index.html
rename to example/index.html
diff --git a/rp/jquery-min.js b/example/jquery-min.js
similarity index 100%
rename from rp/jquery-min.js
rename to example/jquery-min.js
diff --git a/browserid/lib/ca.js b/lib/browserid/ca.js
similarity index 92%
rename from browserid/lib/ca.js
rename to lib/browserid/ca.js
index 2c4cee8d34d0a911708026cea5f65d21eeb72fe0..ab07a592b45e1ef6b205dd49cc694d67845ad85f 100644
--- a/browserid/lib/ca.js
+++ b/lib/browserid/ca.js
@@ -39,8 +39,7 @@
 var jwcert = require('jwcrypto/jwcert'),
     jwk = require('jwcrypto/jwk'),
     jws = require('jwcrypto/jws'),
-    configuration = require('../../libs/configuration'),
-    secrets = require('../../libs/secrets'),
+    configuration = require('configuration'),
     path = require("path"),
     fs = require("fs");
 
@@ -59,7 +58,7 @@ function parseCert(serializedCert) {
 function certify(email, publicKey, expiration) {
   if (expiration == null)
     throw "expiration cannot be null";
-  return new jwcert.JWCert(HOSTNAME, expiration, publicKey, {email: email}).sign(secrets.SECRET_KEY);
+  return new jwcert.JWCert(HOSTNAME, expiration, publicKey, {email: email}).sign(configuration.get('secret_key'));
 }
 
 function verifyChain(certChain, cb) {
@@ -70,8 +69,8 @@ function verifyChain(certChain, cb) {
       // for now we only do browserid.org issued keys
       if (issuer != HOSTNAME)
         return next(null);
-      
-      next(secrets.PUBLIC_KEY);
+
+      next(exports.PUBLIC_KEY);
     }, cb);
 }
 
@@ -80,4 +79,4 @@ exports.certify = certify;
 exports.verifyChain = verifyChain;
 exports.parsePublicKey = parsePublicKey;
 exports.parseCert = parseCert;
-exports.PUBLIC_KEY = secrets.PUBLIC_KEY;
\ No newline at end of file
+exports.PUBLIC_KEY = configuration.get('public_key');
diff --git a/browserid/lib/email.js b/lib/browserid/email.js
similarity index 96%
rename from browserid/lib/email.js
rename to lib/browserid/email.js
index 7d8db0dfdc68d24aec062a39940e4b05313afe19..0a171152984cc10902f429022b1d08e79030c71b 100644
--- a/browserid/lib/email.js
+++ b/lib/browserid/email.js
@@ -34,13 +34,13 @@
  * ***** END LICENSE BLOCK ***** */
 
 const
-db = require('./db'),
+db = require('db.js'),
 emailer = require('nodemailer'),
 fs = require('fs'),
 path = require('path'),
 mustache = require('mustache'),
-config = require('../../libs/configuration.js'),
-logger = require('../../libs/logging.js').logger;
+config = require('configuration.js'),
+logger = require('logging.js').logger;
 
 /* if smtp parameters are configured, use them */
 var smtp_params = config.get('smtp');
diff --git a/browserid/lib/fake_verification.js b/lib/browserid/fake_verification.js
similarity index 100%
rename from browserid/lib/fake_verification.js
rename to lib/browserid/fake_verification.js
diff --git a/lib/browserid/http_forward.js b/lib/browserid/http_forward.js
new file mode 100644
index 0000000000000000000000000000000000000000..61f431f5d40fd9f7f0d4e833894278d9ab2c7621
--- /dev/null
+++ b/lib/browserid/http_forward.js
@@ -0,0 +1,49 @@
+const
+url = require('url'),
+http = require('http'),
+https = require('https'),
+logger = require('logging.js').logger,
+querystring = require('querystring');
+
+module.exports = function(dest, req, res, cb) {
+  var u = url.parse(dest.toString());
+
+  var m = u.protocol === 'http:' ? http : https;
+
+  var preq = m.request({
+    host: u.hostname,
+    port: u.port,
+    path: u.pathname,
+    method: req.method
+  }, function(pres) {
+    res.writeHead(
+      pres.statusCode,
+      pres.headers
+    );
+    pres.on('data', function (chunk) {
+      res.write(chunk);
+    }).on('end', function() {
+      res.end();
+      cb();
+    });
+  }).on('error', function(e) {
+    res.end();
+    cb(e);
+  });
+
+  if (req.headers['content-type']) {
+    preq.setHeader('content-type', req.headers['content-type']);
+  }
+
+  // if the body has already been parsed, we'll write it
+  if (req.body) {
+    var data = querystring.stringify(req.body);
+    preq.setHeader('content-length', data.length);
+    preq.write(data);
+    preq.end();
+  } else {
+    req.on('data', function(chunk) { preq.write(chunk) })
+      .on('end', function() { preq.end() });
+  }
+  logger.info("forwarding request: " + req.url + " -> " + dest);
+};
diff --git a/browserid/lib/prove_template.txt b/lib/browserid/prove_template.txt
similarity index 100%
rename from browserid/lib/prove_template.txt
rename to lib/browserid/prove_template.txt
diff --git a/browserid/lib/wsapi.js b/lib/browserid/wsapi.js
similarity index 75%
rename from browserid/lib/wsapi.js
rename to lib/browserid/wsapi.js
index 9c75fdbfc731bbbc1769f39ecb25efac83b9938b..e6be9ea80e1bf2c0a1aad28daa6b4518d496378c 100644
--- a/browserid/lib/wsapi.js
+++ b/lib/browserid/wsapi.js
@@ -39,38 +39,17 @@
 // with HTTP methods and the like, apply middleware, etc.
 
 const
-db = require('./db.js'),
+db = require('db.js'),
 url = require('url'),
-httputils = require('./httputils.js'),
+httputils = require('httputils.js'),
 email = require('./email.js'),
 bcrypt = require('bcrypt'),
 crypto = require('crypto'),
-logger = require('../../libs/logging.js').logger,
+logger = require('logging.js').logger,
 ca = require('./ca.js'),
-configuration = require('../../libs/configuration.js');
-
-function checkParams(params) {
-  return function(req, resp, next) {
-    var params_in_request=null;
-    if (req.method === "POST") {
-      params_in_request = req.body;
-    } else {
-      params_in_request = req.query;
-    }
-
-    try {
-      params.forEach(function(k) {
-        if (!params_in_request.hasOwnProperty(k) || typeof params_in_request[k] !== 'string') {
-          throw k;
-        }
-      });
-    } catch(e) {
-      logger.error(e.toString());
-      return httputils.badRequest(resp, "missing '" + e + "' argument");
-    }
-    next();
-  };
-}
+config = require('configuration.js'),
+validate = require('validate'),
+forward = require('browserid/http_forward');
 
 // log a user out, clearing everything from their session except the csrf token
 function clearAuthenticatedUser(session) {
@@ -79,7 +58,6 @@ function clearAuthenticatedUser(session) {
   });
 }
 
-
 function setAuthenticatedUser(session, email) {
   session.authenticatedUser = email;
   session.authenticatedAt = new Date();
@@ -91,7 +69,7 @@ function isAuthed(req) {
     if (req.session.authenticatedUser) {
       if (!Date.parse(req.session.authenticatedAt) > 0) throw "bad timestamp";
       if (new Date() - new Date(req.session.authenticatedAt) >
-          configuration.get('authentication_duration_ms'))
+          config.get('authentication_duration_ms'))
       {
         throw "expired";
       }
@@ -173,31 +151,39 @@ function setup(app) {
    * user via their claimed email address.  Upon timeout expiry OR clickthrough
    * the staged user account transitions to a valid user account
    */
-  app.post('/wsapi/stage_user', checkParams([ "email", "site" ]), function(req, resp) {
+  app.post('/wsapi/stage_user', validate([ "email", "site" ]), function(req, resp) {
     // staging a user logs you out.
     clearAuthenticatedUser(req.session);
 
-    try {
-      // upon success, stage_user returns a secret (that'll get baked into a url
-      // and given to the user), on failure it throws
-      db.stageUser(req.body.email, function(secret) {
-        // store the email being registered in the session data
-        if (!req.session) req.session = {};
+    db.lastStaged(req.body.email, function (last) {
+      if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
+        logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+                    ((new Date() - last) / 1000.0) + "s elapsed");
+        return httputils.forbidden(resp, "throttling.  try again later.");
+      }
 
-        // store the secret we're sending via email in the users session, as checking
-        // that it still exists in the database is the surest way to determine the
-        // status of the email verification.
-        req.session.pendingCreation = secret;
+      try {
+        // upon success, stage_user returns a secret (that'll get baked into a url
+        // and given to the user), on failure it throws
+        db.stageUser(req.body.email, function(secret) {
+          // store the email being registered in the session data
+          if (!req.session) req.session = {};
 
-        resp.json({ success: true });
+          // store the secret we're sending via email in the users session, as checking
+          // that it still exists in the database is the surest way to determine the
+          // status of the email verification.
+          req.session.pendingCreation = secret;
 
-        // let's now kick out a verification email!
-        email.sendNewUserEmail(req.body.email, req.body.site, secret);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(resp, e.toString());
-    }
+          resp.json({ success: true });
+
+          // let's now kick out a verification email!
+          email.sendNewUserEmail(req.body.email, req.body.site, secret);
+        });
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
+      }
+    });
   });
 
   app.get('/wsapi/user_creation_status', function(req, resp) {
@@ -233,7 +219,7 @@ function setup(app) {
   });
 
   function bcrypt_password(password, cb) {
-    var bcryptWorkFactor = configuration.get('bcrypt_work_factor');
+    var bcryptWorkFactor = config.get('bcrypt_work_factor');
 
     bcrypt.gen_salt(bcryptWorkFactor, function (err, salt) {
       if (err) {
@@ -252,7 +238,7 @@ function setup(app) {
     });
   };
 
-  app.post('/wsapi/complete_user_creation', checkParams(["token", "pass"]), function(req, resp) {
+  app.post('/wsapi/complete_user_creation', validate(["token", "pass"]), function(req, resp) {
     // issue #155, valid password length is between 8 and 80 chars.
     if (req.body.pass.length < 8 || req.body.pass.length > 80) {
       httputils.badRequest(resp, "valid passwords are between 8 and 80 chars");
@@ -292,26 +278,34 @@ function setup(app) {
     });
   });
 
-  app.post('/wsapi/stage_email', checkAuthed, checkParams(["email", "site"]), function (req, resp) {
-    try {
-      // on failure stageEmail may throw
-      db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
+  app.post('/wsapi/stage_email', checkAuthed, validate(["email", "site"]), function (req, resp) {
+    db.lastStaged(req.body.email, function (last) {
+      if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
+        logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+                    ((new Date() - last) / 1000.0) + "s elapsed");
+        return httputils.forbidden(resp, "throttling.  try again later.");
+      }
 
-        // store the email being added in session data
-        req.session.pendingAddition = secret;
+      try {
+        // on failure stageEmail may throw
+        db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
 
-        resp.json({ success: true });
+          // store the email being added in session data
+          req.session.pendingAddition = secret;
 
-        // let's now kick out a verification email!
-        email.sendAddAddressEmail(req.body.email, req.body.site, secret);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(resp, e.toString());
-    }
+          resp.json({ success: true });
+
+          // let's now kick out a verification email!
+          email.sendAddAddressEmail(req.body.email, req.body.site, secret);
+        });
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
+      }
+    });
   });
 
-  app.get('/wsapi/email_for_token', checkParams(["token"]), function(req,resp) {
+  app.get('/wsapi/email_for_token', validate(["token"]), function(req,resp) {
     db.emailForVerificationSecret(req.query.token, function(email) {
       resp.json({ email: email });
     });
@@ -357,7 +351,7 @@ function setup(app) {
       });
   });
 
-  app.post('/wsapi/complete_email_addition', checkParams(["token"]), function(req, resp) {
+  app.post('/wsapi/complete_email_addition', validate(["token"]), function(req, resp) {
     db.gotVerificationSecret(req.body.token, undefined, function(e) {
       if (e) {
         logger.warn("couldn't complete email verification: " + e);
@@ -368,7 +362,7 @@ function setup(app) {
     });
   });
 
-  app.post('/wsapi/authenticate_user', checkParams(["email", "pass"]), function(req, resp) {
+  app.post('/wsapi/authenticate_user', validate(["email", "pass"]), function(req, resp) {
     db.checkAuth(req.body.email, function(hash) {
       if (typeof hash !== 'string' ||
           typeof req.body.pass !== 'string')
@@ -387,7 +381,7 @@ function setup(app) {
 
           // if the work factor has changed, update the hash here.  issue #204
           // NOTE: this runs asynchronously and will not delay the response
-          if (configuration.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
+          if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
             logger.info("updating bcrypted password for email " + req.body.email);
             bcrypt_password(req.body.pass, function(err, hash) {
               db.updatePassword(req.body.email, hash, function(err) {
@@ -403,7 +397,7 @@ function setup(app) {
     });
   });
 
-  app.post('/wsapi/remove_email', checkAuthed, checkParams(["email"]), function(req, resp) {
+  app.post('/wsapi/remove_email', checkAuthed, validate(["email"]), function(req, resp) {
     var email = req.body.email;
 
     db.removeEmail(req.session.authenticatedUser, email, function(error) {
@@ -425,10 +419,10 @@ function setup(app) {
       }});
   });
 
-  app.post('/wsapi/cert_key', checkAuthed, checkParams(["email", "pubkey"]), function(req, resp) {
+  app.post('/wsapi/cert_key', checkAuthed, validate(["email", "pubkey"]), function(req, res) {
     db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
       // not same account? big fat error
-      if (!sameAccount) return httputils.badRequest(resp, "that email does not belong to you");
+      if (!sameAccount) return httputils.badRequest(res, "that email does not belong to you");
 
       // parse the pubkey
       var pk = ca.parsePublicKey(req.body.pubkey);
@@ -436,13 +430,30 @@ function setup(app) {
       // same account, we certify the key
       // we certify it for a day for now
       var expiration = new Date();
-      expiration.setTime(new Date().valueOf() + configuration.get('certificate_validity_ms'));
+      expiration.setTime(new Date().valueOf() + config.get('certificate_validity_ms'));
       var cert = ca.certify(req.body.email, pk, expiration);
 
-      resp.writeHead(200, {'Content-Type': 'text/plain'});
-      resp.write(cert);
-      resp.end();
+      res.writeHead(200, {'Content-Type': 'text/plain'});
+      res.write(cert);
+      res.end();
+    });
+
+/* code to bounce the cert off of a proper keysigner (issue #460)
+
+    db.emailsBelongToSameAccount(req.session.authenticatedUser, req.body.email, function(sameAccount) {
+      // not same account? big fat error
+      if (!sameAccount) return httputils.badRequest(res, "that email does not belong to you");
+
+      // forward to the keysigner!
+      var keysigner = config.get('keysigner_url');
+      keysigner.path = '/wsapi/cert_key';
+      forward(keysigner, req, res, function(err) {
+        if (err) {
+          logger.error("error forwarding request:", err);
+        }
+      });
     });
+*/
   });
 
   app.post('/wsapi/logout', function(req, resp) {
@@ -450,8 +461,7 @@ function setup(app) {
     resp.json({ success: true });
   });
 
-  // in the cert world, syncing is not necessary,
-  // just get a list of emails.
+  // returns a list of emails owned by the user
   // returns:
   // {
   //   "foo@foo.com" : {..properties..}
diff --git a/libs/configuration.js b/lib/configuration.js
similarity index 57%
rename from libs/configuration.js
rename to lib/configuration.js
index ab43bbcb58f104b752c1abffca5215b5b0407ebe..a71fc10d6312e51971130f5955f23badc99591a4 100644
--- a/libs/configuration.js
+++ b/lib/configuration.js
@@ -43,8 +43,11 @@
  */
 
 const
-substitution = require('./substitute.js'),
-path = require('path');
+postprocess = require('postprocess'),
+path = require('path'),
+urlparse = require('urlparse'),
+secrets = require('./secrets'),
+temp = require('temp');
 
 var g_config = {
 };
@@ -71,60 +74,54 @@ const g_configs = { };
 // production is the configuration that runs on our
 // public service (browserid.org)
 g_configs.production = {
-  hostname: 'browserid.org',
-  port: '443',
-  scheme: 'https',
+  URL: 'https://browserid.org',
   use_minified_resources: true,
   var_path: '/home/browserid/var/',
   database: {
     driver: "mysql",
-    user: 'browserid'
+    user: 'browserid',
+    create_schema: true
   },
   bcrypt_work_factor: 12,
   authentication_duration_ms: (7 * 24 * 60 * 60 * 1000),
-  certificate_validity_ms: (24 * 60 * 60 * 1000)
+  certificate_validity_ms: (24 * 60 * 60 * 1000),
+  min_time_between_emails_ms: (60 * 1000)
 };
 
-// beta (diresworb.org) the only difference from production 
-// is the hostname
-g_configs.beta = JSON.parse(JSON.stringify(g_configs.production));
-g_configs.beta.hostname = 'diresworb.org';
-
-// development (dev.diresworb.org) the only difference from production 
-// is, again, the hostname
-g_configs.development = JSON.parse(JSON.stringify(g_configs.production));
-g_configs.development.hostname = 'dev.diresworb.org';
 
 // local development configuration
 g_configs.local =  {
-  hostname: '127.0.0.1',
-  port: '10002',
-  scheme: 'http',
+  URL: 'http://127.0.0.1:10002',
   email_to_console: true, // don't send email, just dump verification URLs to console.
   use_minified_resources: false,
   var_path: path.join(__dirname, "..", "var"),
   database: { driver: "json" },
   bcrypt_work_factor: g_configs.production.bcrypt_work_factor,
   authentication_duration_ms: g_configs.production.authentication_duration_ms,
-  certificate_validity_ms: g_configs.production.certificate_validity_ms
+  certificate_validity_ms: g_configs.production.certificate_validity_ms,
+  min_time_between_emails_ms: g_configs.production.min_time_between_emails_ms
 };
 
-Object.keys(g_configs).forEach(function(config) {
-  if (!g_configs[config].smtp) {
-    g_configs[config].smtp = {
-      host: process.env['SMTP_HOST'],
-      user: process.env['SMTP_USER'],
-      pass: process.env['SMTP_PASS']
-    };
-  }
-});
+if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
+  var fs = require('fs');
+  eval(fs.readFileSync(process.env['NODE_EXTRA_CONFIG']) + '');
+}
 
 // test environments are variations on local
 g_configs.test_json = JSON.parse(JSON.stringify(g_configs.local));
-g_configs.test_json.database = { driver: "json", unit_test: true }; 
+g_configs.test_json.database = {
+  driver: "json",
+  // use a temporary path for testing
+  path: temp.path({suffix: '.db'})
+};
 
 g_configs.test_mysql = JSON.parse(JSON.stringify(g_configs.local));
-g_configs.test_mysql.database = { driver: "mysql", user: "test", unit_test: true }; 
+g_configs.test_mysql.database = {
+  driver: "mysql",
+  user: "test",
+  database: "browserid_" + secrets.generate(6),
+  create_schema: true
+};
 
 // default deployment is local
 if (undefined === process.env['NODE_ENV']) {
@@ -135,43 +132,83 @@ g_config = g_configs[process.env['NODE_ENV']];
 
 if (g_config === undefined) throw "unknown environment: " + exports.get('env');
 
-function getPortForURL() {
-  if (g_config['scheme'] === 'https' && g_config['port'] === '443') return "";
-  if (g_config['scheme'] === 'http' && g_config['port'] === '80') return "";
-  return ":" + g_config['port'];
+// what url are we running under?
+{
+  var ourURL = process.env['BROWSERID_URL'] || g_config['URL'];
+  var purl = urlparse(ourURL).validate().normalize().originOnly();
+  g_config.URL = purl.toString();
+  g_config.hostname = purl.host;
+  g_config.scheme = purl.scheme;
+  g_config.port = purl.port || (purl.scheme == 'https' ? 443 : 80);
+}
+
+if (process.env['VERIFIER_URL']) {
+  var url = urlparse(process.env['VERIFIER_URL']).validate().normalize();
+  if (!url.port) url.port = (url.scheme === 'http') ? 80 : 443;
+  g_config.verifier_url = url;
+}
+
+if (process.env['KEYSIGNER_URL']) {
+  var url = urlparse(process.env['KEYSIGNER_URL']).validate().normalize();
+  if (!url.port) url.port = (url.scheme === 'http') ? 80 : 443;
+  g_config.keysigner_url = url;
+}
+
+// extract smtp params from the environment
+if (!g_config.smtp) {
+  g_config.smtp = {
+    host: process.env['SMTP_HOST'],
+    user: process.env['SMTP_USER'],
+    pass: process.env['SMTP_PASS']
+  };
+}
+
+// now handle ephemeral database configuration.  Used in testing.
+if (g_config.database.driver === 'mysql') {
+  if (process.env['MYSQL_DATABASE_NAME']) {
+    g_config.database.database = process.env['MYSQL_DATABASE_NAME'];
+  }
+} else if (g_config.database.driver === 'json') {
+  if (process.env['JSON_DATABASE_PATH']) {
+    g_config.database.path = process.env['JSON_DATABASE_PATH'];
+  }
+}
+
+// allow work factor to be twaddled from the environment
+if (process.env['BCRYPT_WORK_FACTOR']) {
+  g_config.bcrypt_work_factor = parseInt(process.env['BCRYPT_WORK_FACTOR']);
 }
 
-g_config['URL'] = g_config['scheme'] + '://' + g_config['hostname'] + getPortForURL();
+// what host/port shall we bind to?
+g_config.bind_to = {
+  host: process.env['IP_ADDRESS'] || process.env['HOST'] || "127.0.0.1",
+  port: process.env['PORT'] || 0
+};
 
 /*
  * Install middleware that will perform textual replacement on served output
  * to re-write urls as needed for this particular environment.
  *
  * Note, for a 'local' environment, no re-write is needed because this is
- * handled at a higher level.  For a 'production' env no rewrite is necc cause
- * all source files are written for that environment.
+ * handled at a higher level.  For other environments, only perform re-writing
+ * if the host, port, or scheme are different than https://browserid.org:443
+ * (all source files always should have the production hostname written into them)
  */
 exports.performSubstitution = function(app) {
-  if (process.env['NODE_ENV'] !== 'production' &&
-      process.env['NODE_ENV'] !== 'local') {
-    app.use(substitution.substitute({
-      'https://browserid.org': g_config['URL'],
-      'browserid.org:443': g_config['hostname'] + ':' + g_config['port'],
-      'browserid.org': g_config['hostname']
+  if (g_config['URL'] != 'https://browserid.org') {
+    app.use(postprocess.middleware(function(req, buffer) {
+      return buffer.toString().replace(new RegExp('https://browserid.org', 'g'), g_config['URL']);
     }));
   }
 };
 
 // At the time this file is required, we'll determine the "process name" for this proc
 // if we can determine what type of process it is (browserid or verifier) based
-// on the path, we'll use that, otherwise we'll name it 'ephemeral'.  
-if (process.argv[1] == path.join(__dirname, "..", "browserid", "run.js")) {
-  g_config['process_type'] = 'browserid';
-} else if (process.argv[1] == path.join(__dirname, "..", "verifier", "run.js")) {
-  g_config['process_type'] = 'verifier';
-} else {
-  g_config['process_type'] = 'ephemeral';
-}
+// on the path, we'll use that, otherwise we'll name it 'ephemeral'.
+g_config['process_type'] = path.basename(process.argv[1], ".js");
+
+g_config['secret_key'] = secrets.loadSecretKey('root', exports.get('var_path'));
+g_config['public_key'] = secrets.loadPublicKey('root', exports.get('var_path'));
 
 // log the process_type
 setTimeout(function() {
diff --git a/browserid/lib/db.js b/lib/db.js
similarity index 96%
rename from browserid/lib/db.js
rename to lib/db.js
index 4545f2f71ea37ce55692d7ffee2892a45fb0a40b..e3a4092039378bd409c8f9a0f42a50d1e092c3f9 100644
--- a/browserid/lib/db.js
+++ b/lib/db.js
@@ -33,7 +33,7 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-var logger = require('../../libs/logging.js').logger;
+var logger = require('./logging.js').logger;
 
 var driver;
 
@@ -51,7 +51,7 @@ exports.open = function(cfg, cb) {
   var driverName = "json";
   if (cfg && cfg.driver) driverName = cfg.driver;
   try {
-    driver = require('./db_' + driverName + '.js');
+    driver = require('./db/' + driverName + '.js');
   } catch(e) {
     var msg = "FATAL: couldn't find database driver: " + driverName;
     console.log(msg);
@@ -104,7 +104,8 @@ exports.onReady = function(f) {
   'listEmails',
   'removeEmail',
   'cancelAccount',
-  'updatePassword'
+  'updatePassword',
+  'lastStaged'
 ].forEach(function(fn) {
   exports[fn] = function() {
     checkReady();
diff --git a/browserid/lib/db_json.js b/lib/db/json.js
similarity index 76%
rename from browserid/lib/db_json.js
rename to lib/db/json.js
index 961761a9a5302344f967c672fc5d6433da560921..9b5b68095b810031fbc27e0921c49fab390af76f 100644
--- a/browserid/lib/db_json.js
+++ b/lib/db/json.js
@@ -41,10 +41,10 @@
 const
 path = require('path'),
 fs = require('fs'),
-secrets = require('../../libs/secrets'),
+secrets = require('../secrets.js'),
 jsel = require('JSONSelect'),
-logger = require('../../libs/logging.js').logger,
-configuration = require('../../libs/configuration.js'),
+logger = require('../logging.js').logger,
+configuration = require('../configuration.js'),
 temp = require('temp');
 
 // a little alias for stringify
@@ -52,6 +52,8 @@ const ESC = JSON.stringify;
 
 var dbPath = path.join(configuration.get('var_path'), "authdb.json");
 
+var drop_on_close = undefined;
+
 /* The JSON database. The structure is thus:
  *  [
  *    {
@@ -63,9 +65,11 @@ var dbPath = path.join(configuration.get('var_path'), "authdb.json");
  *  ]
  */
 
-var db = [];
-var stagedEmails = { };
-var staged = { };
+var db = {
+  users: [ ],
+  stagedEmails: { },
+  staged: { }
+};
 
 function flush() {
   try {
@@ -75,54 +79,69 @@ function flush() {
   }
 }
 
-// when unit_test is set in configuration, database should be
-// ephemeral.  which simply means we use a temp file and delete
-// on close;
-var delete_on_close = false;
+function sync() {
+  try {
+    db = JSON.parse(fs.readFileSync(dbPath));
+  } catch(e) {
+    logger.error("Cannot read database from " + dbPath);
+  }
+}
 
 exports.open = function(cfg, cb) {
-  delete_on_close = false;
-
-  if (cfg) {
-    if (cfg.unit_test) {
-      dbPath = temp.path({suffix: '.db'});
-      delete_on_close = true;
-    } else if (cfg.path) {
-      dbPath = cfg.path;
-    }
+  if (cfg && cfg.path) {
+    dbPath = cfg.path;
   }
+  logger.debug("opening JSON database: " + dbPath);
 
-  try {
-    db = JSON.parse(fs.readFileSync(dbPath));
-  } catch(e) {
+  if (cfg && cfg.drop_on_close) {
+    logger.debug("will remove database upon close");
+    drop_on_close = true;
   }
 
+  sync();
+
   setTimeout(cb, 0);
 };
 
 exports.close = function(cb) {
   flush();
-  setTimeout(cb, 0);
-  if (delete_on_close) {
-    delete_on_close = false;
-    fs.unlink(dbPath, function(err) { });
-  };
+
+  if (drop_on_close) {
+    drop_on_close = undefined;
+    fs.unlink(dbPath, function(err) { cb(err === null ? undefined : err); });
+  } else {
+    setTimeout(cb, 0);
+  }
 };
 
 exports.emailKnown = function(email, cb) {
-  var m = jsel.match(".emails :val(" + ESC(email) + ")", db);
+  sync();
+  var m = jsel.match(".emails :val(" + ESC(email) + ")", db.users);
   setTimeout(function() { cb(m.length > 0) }, 0);
 };
 
 exports.isStaged = function(email, cb) {
   if (cb) {
     setTimeout(function() {
-      cb(stagedEmails.hasOwnProperty(email));
+      sync();
+      cb(db.stagedEmails.hasOwnProperty(email));
     }, 0);
   }
 };
 
+exports.lastStaged = function(email, cb) {
+  if (cb) {
+    sync();
+    var d;
+    if (db.stagedEmails.hasOwnProperty(email)) {
+      d = new Date(db.staged[db.stagedEmails[email]].when);
+    }
+    setTimeout(function() { cb(d); }, 0);
+  }
+};
+
 exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
+  sync();
   emailToUserID(lhs, function(lhs_uid) {
     emailToUserID(rhs, function(rhs_uid) {
       cb(lhs_uid === rhs_uid);
@@ -139,7 +158,7 @@ function addEmailToAccount(existing_email, email, cb) {
     if (userID == undefined) {
       cb("no such email: " + existing_email, undefined);
     } else {
-      db[userID].emails.push(email);
+      db.users[userID].emails.push(email);
       flush();
       cb();
     }
@@ -150,45 +169,54 @@ exports.stageUser = function(email, cb) {
   var secret = secrets.generate(48);
 
   // overwrite previously staged users
-  staged[secret] = {
+  sync();
+  db.staged[secret] = {
     type: "add_account",
-    email: email
+    email: email,
+    when: (new Date()).getTime()
   };
-
-  stagedEmails[email] = secret;
+  db.stagedEmails[email] = secret;
+  flush();
   setTimeout(function() { cb(secret); }, 0);
 };
 
 exports.stageEmail = function(existing_email, new_email, cb) {
   var secret = secrets.generate(48);
+
   // overwrite previously staged users
-  staged[secret] = {
+  sync();
+  db.staged[secret] = {
     type: "add_email",
     existing_email: existing_email,
-    email: new_email
+    email: new_email,
+    when: (new Date()).getTime()
   };
-  stagedEmails[new_email] = secret;
+  db.stagedEmails[new_email] = secret;
+  flush();
+
   setTimeout(function() { cb(secret); }, 0);
 };
 
 
 exports.emailForVerificationSecret = function(secret, cb) {
   setTimeout(function() {
-    cb(staged[secret]? staged[secret].email:undefined);
+    cb(db.staged[secret] ? db.staged[secret].email : undefined);
   }, 0);
 };
 
 exports.gotVerificationSecret = function(secret, hash, cb) {
-  if (!staged.hasOwnProperty(secret)) return cb("unknown secret");
+  sync();
+  if (!db.staged.hasOwnProperty(secret)) return cb("unknown secret");
 
   // simply move from staged over to the emails "database"
-  var o = staged[secret];
-  delete staged[secret];
-  delete stagedEmails[o.email];
+  var o = db.staged[secret];
+  delete db.staged[secret];
+  delete db.stagedEmails[o.email];
+  flush();
   if (o.type === 'add_account') {
     exports.emailKnown(o.email, function(known) {
       function createAccount() {
-        db.push({
+        db.users.push({
           password: hash,
           emails: [ o.email ]
         });
@@ -232,25 +260,29 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
 };
 
 exports.checkAuth = function(email, cb) {
-  var m = jsel.match(":root > object:has(.emails > :val(" + ESC(email) + ")) > .password", db);
+  sync();
+  var m = jsel.match(":root > object:has(.emails > :val(" + ESC(email) + ")) > .password", db.users);
   if (m.length === 0) m = undefined;
   else m = m[0];
   setTimeout(function() { cb(m) }, 0);
 };
 
 exports.updatePassword = function(email, hash, cb) {
-  var m = jsel.match(":root > object:has(.emails > :val(" + ESC(email) + "))", db);
+  sync();
+  var m = jsel.match(":root > object:has(.emails > :val(" + ESC(email) + "))", db.users);
   var err = undefined;
   if (m.length === 0) err = "no such email address";
   else m[0].password = hash;
+  flush();
   setTimeout(function() { cb(err) }, 0);
 };
 
 function emailToUserID(email, cb) {
+  sync();
   var id = undefined;
 
-  for (var i = 0; i < db.length; i++) {
-    if (jsel.match(":val(" + JSON.stringify(email) + ")", db[i]).length) {
+  for (var i = 0; i < db.users.length; i++) {
+    if (jsel.match(":val(" + JSON.stringify(email) + ")", db.users[i]).length) {
       id = i;
       break;
     }
@@ -261,13 +293,14 @@ function emailToUserID(email, cb) {
 }
 
 exports.listEmails = function(email, cb) {
+  sync();
   // get the user id associated with this account
   emailToUserID(email, function(userID) {
     if (userID === undefined) {
       cb("no such email: " + email);
       return;
     }
-    var email_list = jsel.match(".emails string", db[userID]);
+    var email_list = jsel.match(".emails string", db.users[userID]);
     var emails = {};
     for (var i=0; i < email_list.length; i++)
       emails[email_list[i]] = {};
@@ -277,24 +310,25 @@ exports.listEmails = function(email, cb) {
 };
 
 exports.removeEmail = function(authenticated_email, email, cb) {
-  var m = jsel.match(".emails:has(:val("+ESC(authenticated_email)+")):has(:val("+ESC(email)+"))", db);
+  sync();
+  var m = jsel.match(".emails:has(:val("+ESC(authenticated_email)+")):has(:val("+ESC(email)+"))", db.users);
 
   if (m.length) {
     var emails = m[0];
     for (var i = 0; i < emails.length; i++) {
       if (emails[i] === email) {
         emails.splice(i, 1);
+        flush();
         break;
       }
     }
   }
-
   setTimeout(function() { cb(); }, 0);
 };
 
 exports.cancelAccount = function(authenticated_email, cb) {
   emailToUserID(authenticated_email, function(user_id) {
-    db.splice(user_id, 1);
+    db.users.splice(user_id, 1);
     flush();
     cb();
   });
diff --git a/browserid/lib/db_mysql.js b/lib/db/mysql.js
similarity index 84%
rename from browserid/lib/db_mysql.js
rename to lib/db/mysql.js
index 06bd219bf904cd91e58c2398abb282bc108de24c..3657ff36e8f97bb9032d3516324f313b69523565 100644
--- a/browserid/lib/db_mysql.js
+++ b/lib/db/mysql.js
@@ -49,6 +49,7 @@
  *
  *
  *    +------ staged ----------+
+ *    |*int id                 |
  *    |*string secret          |
  *    | bool new_acct          |
  *    | string existing        |
@@ -59,18 +60,36 @@
 
 const
 mysql = require('mysql'),
-secrets = require('../../libs/secrets'),
-logger = require('../../libs/logging.js').logger;
+secrets = require('../secrets.js'),
+logger = require('../logging.js').logger;
 
 var client = undefined;
 
 // may get defined at open() time causing a database to be dropped upon connection closing.
 var drop_on_close = undefined;
 
+// If you change these schemas, please notify <services-ops@mozilla.com>
 const schemas = [
-  "CREATE TABLE IF NOT EXISTS user   ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );",
-  "CREATE TABLE IF NOT EXISTS email  ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, INDEX(user), address VARCHAR(255) UNIQUE, INDEX(address) );",
-  "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
+  "CREATE TABLE IF NOT EXISTS user (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "passwd CHAR(64) NOT NULL" +
+    ") ENGINE=InnoDB;",
+
+  "CREATE TABLE IF NOT EXISTS email (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "user BIGINT NOT NULL," +
+    "address VARCHAR(255) UNIQUE NOT NULL," +
+    "FOREIGN KEY user_fkey (user) REFERENCES user(id)" +
+    ") ENGINE=InnoDB;",
+
+  "CREATE TABLE IF NOT EXISTS staged (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "secret CHAR(48) UNIQUE NOT NULL," +
+    "new_acct BOOL NOT NULL," +
+    "existing VARCHAR(255)," +
+    "email VARCHAR(255) UNIQUE NOT NULL," +
+    "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" +
+    ") ENGINE=InnoDB;",
 ];
 
 // log an unexpected database error
@@ -105,43 +124,45 @@ exports.open = function(cfg, cb) {
   // let's figure out the database name
   var database = cfg.database;
   if (!database) database = "browserid";
-  if (cfg.unit_test) {
-    database += "_" + secrets.generate(8);
-    drop_on_close = database;
-  }
+
+  // if the client specifies a name other than 'browserid', and specifies
+  // that we should drop the database on close, do it
+  if (database !== 'browserid' && cfg.drop_on_close) drop_on_close = database;
 
   // now create the databse
-  client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
-    if (err) {
-      logUnexpectedError(err);
-      cb(err);
-      return;
-    }
-    client.useDatabase(database, function(err) {
+  if (cfg.create_schema) {
+    client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
       if (err) {
         logUnexpectedError(err);
         cb(err);
         return;
       }
+      client.useDatabase(database, function(err) {
+        if (err) {
+          logUnexpectedError(err);
+          cb(err);
+          return;
+        }
 
-      // now create tables
-      function createNextTable(i) {
-        if (i < schemas.length) {
-          client.query(schemas[i], function(err) {
-            if (err) {
-              logUnexpectedError(err);
-              cb(err);
-            } else {
-              createNextTable(i+1);
-            }
-          });
-        } else {
-          cb();
+        // now create tables
+        function createNextTable(i) {
+          if (i < schemas.length) {
+            client.query(schemas[i], function(err) {
+              if (err) {
+                logUnexpectedError(err);
+                cb(err);
+              } else {
+                createNextTable(i+1);
+              }
+            });
+          } else {
+            cb();
+          }
         }
-      }
-      createNextTable(0);
+        createNextTable(0);
+      });
     });
-  });
+  };
 };
 
 exports.close = function(cb) {
@@ -183,11 +204,22 @@ exports.isStaged = function(email, cb) {
   );
 }
 
+exports.lastStaged = function(email, cb) {
+  client.query(
+    "SELECT UNIX_TIMESTAMP(ts) as ts FROM staged WHERE email = ?", [ email ],
+    function(err, rows) {
+      if (err) logUnexpectedError(err);
+      if (!rows || rows.length === 0) cb();
+      else cb(new Date(rows[0].ts * 1000));
+    }
+  );
+}
+
 exports.stageUser = function(email, cb) {
   var secret = secrets.generate(48);
   // overwrite previously staged users
   client.query('INSERT INTO staged (secret, new_acct, email) VALUES(?,TRUE,?) ' +
-               'ON DUPLICATE KEY UPDATE secret=?, existing="", new_acct=TRUE',
+               'ON DUPLICATE KEY UPDATE secret=?, existing="", new_acct=TRUE, ts=NOW()',
                [ secret, email, secret],
                function(err) {
                  if (err) {
@@ -285,7 +317,7 @@ exports.stageEmail = function(existing_email, new_email, cb) {
   var secret = secrets.generate(48);
   // overwrite previously staged users
   client.query('INSERT INTO staged (secret, new_acct, existing, email) VALUES(?,FALSE,?,?) ' +
-               'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE',
+               'ON DUPLICATE KEY UPDATE secret=?, existing=?, new_acct=FALSE, ts=NOW()',
                [ secret, existing_email, new_email, secret, existing_email],
                function(err) {
                  if (err) {
diff --git a/lib/heartbeat.js b/lib/heartbeat.js
new file mode 100644
index 0000000000000000000000000000000000000000..faa697c013c543b9a61338f76a589feb2d9b4902
--- /dev/null
+++ b/lib/heartbeat.js
@@ -0,0 +1,38 @@
+const urlparse = require('urlparse');
+
+// the path that heartbeats live at
+exports.path = '/__heartbeat__';
+
+// a helper function to set up a heartbeat check
+exports.setup = function(app, cb) {
+  app.get(exports.path, function(req, res) {
+    function ok(yeah) {
+      res.writeHead(yeah ? 200 : 500);
+      res.write(yeah ? 'ok' : 'not ok');
+      res.end();
+    }
+    if (cb) cb(ok);
+    else ok(true);
+  });
+};
+
+// a function to check the heartbeat of a remote server
+exports.check = function(url, cb) {
+  if (typeof url === 'string') url = urlparse(url).normalize().validate();
+  else if (typeof url !== 'object') throw "url string or object required as argumnet to heartbeat.check";
+  if (!url.port) url.port = (url.scheme === 'http') ? 80 : 443;
+
+  var shortname = url.host + ':' + url.port;
+
+  require(url.scheme).get({
+    host: url.host,
+    port: url.port,
+    path: exports.path
+  }, function (res) {
+    if (res.statusCode === 200) cb(true);
+    else logger.error("non-200 response from " + shortname + ".  fatal! (" + res.statusCode + ")");
+  }, function (e) {
+    logger.error("can't communicate with " + shortname + ".  fatal: " + e);
+    cb(false);
+  });
+};
\ No newline at end of file
diff --git a/browserid/lib/httputils.js b/lib/httputils.js
similarity index 93%
rename from browserid/lib/httputils.js
rename to lib/httputils.js
index f88539a9b06dfc40c448fdef9e5cac1c5032bc2e..6b389251d2d807da6b86106a53b0e33b45df4810 100644
--- a/browserid/lib/httputils.js
+++ b/lib/httputils.js
@@ -63,6 +63,16 @@ exports.badRequest = function(resp, reason)
   resp.end();
 };
 
+exports.forbidden = function(resp, reason)
+{
+  resp.writeHead(403, {"Content-Type": "text/plain"});
+  resp.write("Forbidden");
+  if (reason) {
+    resp.write(": " + reason);
+  }
+  resp.end();
+};
+
 exports.jsonResponse = function(resp, obj)
 {
   resp.writeHead(200, {"Content-Type": "application/json"});
diff --git a/lib/keysigner/ca.js b/lib/keysigner/ca.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab07a592b45e1ef6b205dd49cc694d67845ad85f
--- /dev/null
+++ b/lib/keysigner/ca.js
@@ -0,0 +1,82 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *     Ben Adida <benadida@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// certificate authority
+
+var jwcert = require('jwcrypto/jwcert'),
+    jwk = require('jwcrypto/jwk'),
+    jws = require('jwcrypto/jws'),
+    configuration = require('configuration'),
+    path = require("path"),
+    fs = require("fs");
+
+var HOSTNAME = configuration.get('hostname');
+
+function parsePublicKey(serializedPK) {
+  return jwk.PublicKey.deserialize(serializedPK);
+}
+
+function parseCert(serializedCert) {
+  var cert = new jwcert.JWCert();
+  cert.parse(serializedCert);
+  return cert;
+}
+
+function certify(email, publicKey, expiration) {
+  if (expiration == null)
+    throw "expiration cannot be null";
+  return new jwcert.JWCert(HOSTNAME, expiration, publicKey, {email: email}).sign(configuration.get('secret_key'));
+}
+
+function verifyChain(certChain, cb) {
+  // raw certs
+  return jwcert.JWCert.verifyChain(
+    certChain, new Date(),
+    function(issuer, next) {
+      // for now we only do browserid.org issued keys
+      if (issuer != HOSTNAME)
+        return next(null);
+
+      next(exports.PUBLIC_KEY);
+    }, cb);
+}
+
+// exports, not the key stuff
+exports.certify = certify;
+exports.verifyChain = verifyChain;
+exports.parsePublicKey = parsePublicKey;
+exports.parseCert = parseCert;
+exports.PUBLIC_KEY = configuration.get('public_key');
diff --git a/performance/lib/add_email.js b/lib/load_gen/add_email.js
similarity index 100%
rename from performance/lib/add_email.js
rename to lib/load_gen/add_email.js
diff --git a/performance/lib/include_only.js b/lib/load_gen/include_only.js
similarity index 100%
rename from performance/lib/include_only.js
rename to lib/load_gen/include_only.js
diff --git a/performance/lib/reauth.js b/lib/load_gen/reauth.js
similarity index 100%
rename from performance/lib/reauth.js
rename to lib/load_gen/reauth.js
diff --git a/performance/lib/reset_pass.js b/lib/load_gen/reset_pass.js
similarity index 100%
rename from performance/lib/reset_pass.js
rename to lib/load_gen/reset_pass.js
diff --git a/performance/lib/signin.js b/lib/load_gen/signin.js
similarity index 100%
rename from performance/lib/signin.js
rename to lib/load_gen/signin.js
diff --git a/performance/lib/signup.js b/lib/load_gen/signup.js
similarity index 100%
rename from performance/lib/signup.js
rename to lib/load_gen/signup.js
diff --git a/performance/lib/test.js b/lib/load_gen/test.js
similarity index 100%
rename from performance/lib/test.js
rename to lib/load_gen/test.js
diff --git a/performance/lib/user_db.js b/lib/load_gen/user_db.js
similarity index 100%
rename from performance/lib/user_db.js
rename to lib/load_gen/user_db.js
diff --git a/libs/logging.js b/lib/logging.js
similarity index 97%
rename from libs/logging.js
rename to lib/logging.js
index b167c6eb0abdabc0c4be66bd0838849f85161720..3e7feb5a1a7d7e972f572b5c4923aaf205c4471f 100644
--- a/libs/logging.js
+++ b/lib/logging.js
@@ -78,4 +78,6 @@ exports.logger.emitErrs = false;
 
 exports.enableConsoleLogging = function() {
   exports.logger.add(winston.transports.Console, { colorize: true });
-};
\ No newline at end of file
+};
+
+if (process.env['LOG_TO_CONSOLE']) exports.enableConsoleLogging();
diff --git a/libs/metrics.js b/lib/metrics.js
similarity index 100%
rename from libs/metrics.js
rename to lib/metrics.js
diff --git a/libs/secrets.js b/lib/secrets.js
similarity index 93%
rename from libs/secrets.js
rename to lib/secrets.js
index 46ea829f270a236580a16743e92b8217c8bb3ea3..6382d5e4220b2883973d61afd6f68a849c7c8a84 100644
--- a/libs/secrets.js
+++ b/lib/secrets.js
@@ -57,12 +57,14 @@ exports.hydrateSecret = function(name, dir) {
 
   if (secret === undefined) {
     secret = exports.generate(128);
+    fs.writeFileSync(p, '');
+    fs.chmodSync(p, 0600);
     fs.writeFileSync(p, secret);
   }
   return secret;
 };
 
-function loadSecretKey(name, dir) {
+exports.loadSecretKey = function(name, dir) {
   var p = path.join(dir, name + ".secretkey");
   var fileExists = false;
   var secret = undefined;
@@ -77,7 +79,7 @@ function loadSecretKey(name, dir) {
   return jwk.SecretKey.deserialize(secret);
 }
 
-function loadPublicKey(name, dir) {
+exports.loadPublicKey = function(name, dir) {
   var p = path.join(dir, name + ".publickey");
   var fileExists = false;
   var secret = undefined;
@@ -93,6 +95,3 @@ function loadPublicKey(name, dir) {
   // {alg: <ALG>, value: <SERIALIZED_KEY>}
   return jwk.PublicKey.deserialize(secret);
 }
-
-exports.SECRET_KEY = loadSecretKey('root', configuration.get('var_path'));
-exports.PUBLIC_KEY = loadPublicKey('root', configuration.get('var_path'));
diff --git a/browserid/static/js/pages/forgot.js b/lib/validate.js
similarity index 64%
rename from browserid/static/js/pages/forgot.js
rename to lib/validate.js
index 1ab1bb6f0d1b4a9732829c8b8f1ac8df2d8dbe01..56161cea85c97b3a0f805bec2b8577eed8885349 100644
--- a/browserid/static/js/pages/forgot.js
+++ b/lib/validate.js
@@ -1,4 +1,3 @@
-/*globals BrowserID: true, $:true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -19,6 +18,7 @@
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
+ *   Lloyd Hilaiel <lloyd@hilaiel.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
@@ -34,36 +34,39 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-BrowserID.forgot = (function() {
-  "use strict";
+// a teensy tinsy module to do parameter validation.  A good candiate for future
+// librification.
+//
+// usage:
+//
+//   const validate = require('validate.js');
+//
+//   app.post('/wsapi/foo', validate([ "email", "site" ]), function(req, resp) {
+//   });
 
-  return function() {
-    $("form input[autofocus]").focus();
+const
+logger = require('./logging.js').logger,
+httputils = require('httputils.js');
 
-    $("#signUpForm").bind("submit", function(event) {
-      event.preventDefault();
-      $(".notifications .notification").hide();
+module.exports = function (params) {
+  return function(req, resp, next) {
+    var params_in_request=null;
+    if (req.method === "POST") {
+      params_in_request = req.body;
+    } else {
+      params_in_request = req.query;
+    }
 
-      var email = $("#email").val(),
-          password = $("#password").val(),
-          vpassword = $("#vpassword").val();
-
-      if (password != vpassword) {
-        $(".notifications .notification.mismatchpassword").fadeIn();
-        return false;
-      }
-
-      BrowserID.User.createUser(email, function onSuccess(keypair) {
-        $('#sent_to_email').html(email);
-        $('#forminputs').fadeOut();
-        $(".notifications .notification.emailsent").fadeIn();
-      }, function onFailure() {
-        // bad authentication
-        $(".notifications .notification.doh").fadeIn();
+    try {
+      params.forEach(function(k) {
+        if (!params_in_request || !params_in_request.hasOwnProperty(k) || typeof params_in_request[k] !== 'string') {
+          throw k;
+        }
       });
-    });
+      next();
+    } catch(e) {
+      logger.error(e.toString());
+      return httputils.badRequest(resp, "missing '" + e + "' argument");
+    }
   };
-
-
-}());
-
+};
diff --git a/verifier/lib/certassertion.js b/lib/verifier/certassertion.js
similarity index 93%
rename from verifier/lib/certassertion.js
rename to lib/verifier/certassertion.js
index e372a0b7515c762c058c0b51a8dfaf2e480b7d5a..618867087547fb0c89ebf9bb498a9292815116d6 100644
--- a/verifier/lib/certassertion.js
+++ b/lib/verifier/certassertion.js
@@ -45,21 +45,17 @@ jwk = require("jwcrypto/jwk"),
 jwt = require("jwcrypto/jwt"),
 jwcert = require("jwcrypto/jwcert"),
 vep = require("jwcrypto/vep"),
-configuration = require('../../libs/configuration'),
-secrets = require('../../libs/secrets'),
-logger = require("../../libs/logging.js").logger;
-
-// configuration information to check the issuer
-const config = require("../../libs/configuration.js");
+config = require("../../lib/configuration.js"),
+logger = require("../../lib/logging.js").logger;
 
 const HOSTMETA_URL = "/.well-known/host-meta";
 
 var publicKeys = {};
 
 // set up some default public keys
-publicKeys[configuration.get('hostname')] = secrets.PUBLIC_KEY;
+publicKeys[config.get('hostname')] = config.get('public_key');
 logger.debug("pre-seeded public key cache with key for " +
-             configuration.get('hostname'));
+             config.get('hostname'));
 
 function https_complete_get(host, url, successCB, errorCB) {
   https.get({host: host,path: url}, function(res) {
@@ -71,7 +67,7 @@ function https_complete_get(host, url, successCB, errorCB) {
     res.on('end', function() {
       successCB(allData);
     });
-    
+
   }).on('error', function(e) {
     console.log(e.toString());
     errorCB(e);
@@ -142,6 +138,8 @@ function retrieveHostPublicKey(host, successCB, errorCB) {
 //   it might be strangely formed.
 function compareAudiences(want, got) {
   try {
+    var checkHostOnly = false;
+
     // issue #82 - for a limited time, let's allow got to be sloppy and omit scheme
     // in which case we guess a scheme based on port
     if (!/^https?:\/\//.test(got)) {
@@ -149,6 +147,7 @@ function compareAudiences(want, got) {
       var scheme = "http";
       if (x.length === 2 && x[1] === '443') scheme = "https";
       got = scheme + "://" + got;
+      checkHostOnly = true;
     }
 
     // now parse and compare
@@ -161,9 +160,11 @@ function compareAudiences(want, got) {
 
     got = normalizeParsedURL(url.parse(got));
 
+    if (checkHostOnly) return want.hostname === got.hostname;
+
     return (want.protocol === got.protocol &&
             want.hostname === got.hostname &&
-            want.port === got.port);
+            want.port == got.port);
   } catch(e) {
     return false;
   }
@@ -192,7 +193,7 @@ function verify(assertion, audience, successCB, errorCB, pkRetriever) {
         retrieveHostPublicKey(issuer, next, function(err) {next(null);});
     }, function(pk, principal) {
       // primary?
-      if (theIssuer != configuration.get('hostname')) {
+      if (theIssuer != config.get('hostname')) {
         // then the email better match the issuer
         console.log(principal);
         if (!principal.email.match("@" + theIssuer + "$"))
diff --git a/libs/wsapi_client.js b/lib/wsapi_client.js
similarity index 99%
rename from libs/wsapi_client.js
rename to lib/wsapi_client.js
index 292a9772c08933b6af4a4d341bdeac27b7257ef7..29eeabec6db9d852dcba220ead0e3990a06f8833 100644
--- a/libs/wsapi_client.js
+++ b/lib/wsapi_client.js
@@ -54,7 +54,7 @@ function injectCookies(ctx, headers) {
       headers['Cookie'] += k + "=" + ctx.cookieJar[k];
     }
   }
-} 
+}
 
 function extractCookies(ctx, res) {
   if (ctx.cookieJar === undefined) ctx.cookieJar = {};
@@ -87,7 +87,7 @@ exports.get = function(cfg, path, context, getArgs, cb) {
     cb(false);
     return;
   }
-  
+
   var headers = { };
   injectCookies(context, headers);
 
diff --git a/libs/substitute.js b/libs/substitute.js
deleted file mode 100644
index 2c4c526aedbbaac25231c855ebddd27fcd26287b..0000000000000000000000000000000000000000
--- a/libs/substitute.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla BrowserID.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-// return a function that is substitution middleware, capable
-// of being installed to perform textual replacement on
-// all server output
-exports.substitute = function(subs) {
-  // given a buffer, find and replace all subs
-  function subHostNames(data) {
-    for (var i in subs) {
-      data = data.toString().replace(new RegExp(i, 'g'), subs[i]);
-    }
-
-    return data;
-  }
-
-  return function(req, resp, next) {
-    // cache the *real* functions
-    var realWrite = resp.write;
-    var realEnd = resp.end;
-    var realWriteHead = resp.writeHead;
-    var realSend = resp.send;
-
-    var buf = undefined;
-    var enc = undefined;
-    var contentType = undefined;
-
-    resp.writeHead = function (sc, reason, hdrs) {
-      var h = undefined;
-      if (typeof hdrs === 'object') h = hdrs;
-      else if (typeof reason === 'object') h = reason; 
-      for (var k in h) {
-        if (k.toLowerCase() === 'content-type') {
-          contentType = h[k];
-          break;
-        }
-      }
-      if (!contentType) contentType = resp.getHeader('content-type');
-      if (!contentType) contentType = "application/unknown";
-      realWriteHead.call(resp, sc, reason, hdrs);
-    };
-
-    resp.write = function (chunk, encoding) {
-      if (buf) buf += chunk;
-      else buf = chunk;
-      enc = encoding;
-    };
-
-    resp.send = function(stuff) {
-      buf = stuff;
-      realSend.call(resp,stuff);
-    };
-
-    resp.end = function() {
-      if (!contentType) contentType = resp.getHeader('content-type');
-      if (contentType && (contentType === "application/javascript" ||
-                          contentType.substr(0,4) === 'text'))
-      {
-        if (buf) {
-          if (Buffer.isBuffer(buf)) buf = buf.toString('utf8');
-          var l = Buffer.byteLength(buf);
-          buf = subHostNames(buf);
-          if (l != Buffer.byteLength(buf)) resp.setHeader('Content-Length', Buffer.byteLength(buf));
-        }
-      }
-      if (buf && buf.length) {
-        realWrite.call(resp, buf, enc);
-      }
-      realEnd.call(resp);
-    }
-
-    next();
-  };
-};
diff --git a/package.json b/package.json
index 4c650ea6142b5d9a6464a3a5e713b9506d49a89a..b56ddad2a88986421673c40b17bb08fd0fbcda8c 100644
--- a/package.json
+++ b/package.json
@@ -26,10 +26,12 @@
     , "sax" : "0.2.3"
     , "mimelib-noiconv" : "0.1.3"
     , "jwcrypto": "https://github.com/mozilla/jwcrypto/tarball/3a21befabfff"
+    , "postprocess": "0.0.3"
+    , "urlparse": "0.0.1"
   }
   , "scripts": {
     "postinstall": "./scripts/generate_ephemeral_keys.sh",
     "test": "./scripts/run_all_tests.sh",
-    "start": "node ./run.js"
+    "start": "./scripts/run_locally.js"
   }
 }
diff --git a/browserid/.gitignore b/resources/.gitignore
similarity index 50%
rename from browserid/.gitignore
rename to resources/.gitignore
index 87ff02149168e118b7f8f73ee8850cab949e80f6..ad546db10e4922983382c9235cb61b007e8d2475 100644
--- a/browserid/.gitignore
+++ b/resources/.gitignore
@@ -1,7 +1,9 @@
-/static/dialog/production.js
-/static/dialog/production.css
 /static/css/browserid.css
 /static/css/browserid.min.css
-/static/js/lib.min.js
+/static/dialog/css/production.css
+/static/dialog/css/production.min.css
+/static/dialog/production.js
+/static/include.orig.js
 /static/js/lib.js
-/var
+/static/js/lib.min.js
+/static/relay/production.js
diff --git a/browserid/assets/account-buttons.png b/resources/assets/account-buttons.png
similarity index 100%
rename from browserid/assets/account-buttons.png
rename to resources/assets/account-buttons.png
diff --git a/browserid/assets/browserID-135x35.png b/resources/assets/browserID-135x35.png
similarity index 100%
rename from browserid/assets/browserID-135x35.png
rename to resources/assets/browserID-135x35.png
diff --git a/browserid/assets/browserID-366x72.png b/resources/assets/browserID-366x72.png
similarity index 100%
rename from browserid/assets/browserID-366x72.png
rename to resources/assets/browserID-366x72.png
diff --git a/browserid/assets/browserID-80x20.png b/resources/assets/browserID-80x20.png
similarity index 100%
rename from browserid/assets/browserID-80x20.png
rename to resources/assets/browserID-80x20.png
diff --git a/browserid/assets/browserID-buttons.psd b/resources/assets/browserID-buttons.psd
similarity index 100%
rename from browserid/assets/browserID-buttons.psd
rename to resources/assets/browserID-buttons.psd
diff --git a/browserid/assets/browserID-logo.eps b/resources/assets/browserID-logo.eps
similarity index 100%
rename from browserid/assets/browserID-logo.eps
rename to resources/assets/browserID-logo.eps
diff --git a/browserid/static/.well-known/host-meta b/resources/static/.well-known/host-meta
similarity index 100%
rename from browserid/static/.well-known/host-meta
rename to resources/static/.well-known/host-meta
diff --git a/browserid/static/css/m.css b/resources/static/css/m.css
similarity index 100%
rename from browserid/static/css/m.css
rename to resources/static/css/m.css
diff --git a/browserid/static/css/sil.ttf b/resources/static/css/sil.ttf
similarity index 100%
rename from browserid/static/css/sil.ttf
rename to resources/static/css/sil.ttf
diff --git a/browserid/static/css/style.css b/resources/static/css/style.css
similarity index 98%
rename from browserid/static/css/style.css
rename to resources/static/css/style.css
index 7114748d0e4083e36fd1853a151ce6efa082dca2..64e8c5f177abf5758169124608e60b5de50d8b5f 100644
--- a/browserid/static/css/style.css
+++ b/resources/static/css/style.css
@@ -21,6 +21,18 @@ body {
   overflow-y: scroll;
 }
 
+noscript {
+  position: fixed;
+  display: block;
+  background-color: #ef1010;
+  top: 0;
+  left: 0;
+  padding: 1px;
+  width: 100%;
+  color: #fff;
+  text-align: center;
+}
+
 /* for floats */
 .cf:after {
   content: ".";
@@ -106,7 +118,7 @@ hr {
 
 #content {
   padding: 84px 0;
-  display: none;
+ /* display: none;*/
 }
 
 #about {
@@ -724,7 +736,7 @@ h1 {
 }
 
 
-#signUpForm #siteinfo, #congrats {
+#congrats #siteinfo, #congrats {
   display: none;
 }
 
diff --git a/browserid/static/css/ts.ttf b/resources/static/css/ts.ttf
similarity index 100%
rename from browserid/static/css/ts.ttf
rename to resources/static/css/ts.ttf
diff --git a/browserid/static/dialog/controllers/authenticate_controller.js b/resources/static/dialog/controllers/authenticate_controller.js
similarity index 90%
rename from browserid/static/dialog/controllers/authenticate_controller.js
rename to resources/static/dialog/controllers/authenticate_controller.js
index 84632f5f3008fe152f92d6c27a29a3b22899e047..21354b2b3ed27b4c5abe357b22da1eb0e1bff1e6 100644
--- a/browserid/static/dialog/controllers/authenticate_controller.js
+++ b/resources/static/dialog/controllers/authenticate_controller.js
@@ -42,6 +42,7 @@
       user = bid.User,
       errors = bid.Errors,
       validation = bid.Validation,
+      tooltip = bid.Tooltip,
       lastEmail = "";
 
   function checkEmail(el, event) {
@@ -50,9 +51,7 @@
 
     cancelEvent(event);
 
-    if (!validation.email(email)) {
-      return;
-    }
+    if (!validation.email(email)) return;
 
     user.isEmailRegistered(email, function onComplete(registered) {
       if (registered) {
@@ -70,19 +69,16 @@
 
     cancelEvent(event);
 
-    if (!validation.email(email)) {
-      return;
-    }
+    if (!validation.email(email)) return;
 
-    user.createUser(email, function(keypair) {
-      if (keypair) {
+    user.createUser(email, function(staged) {
+      if (staged) {
         self.close("user_staged", {
-          email: email,
-          keypair: keypair
+          email: email
         });
       }
       else {
-        // XXX can't register this email address.
+        tooltip.showTooltip("#could_not_add");
       }
     }, self.getErrorDialog(errors.createUser));
   }
@@ -94,9 +90,7 @@
 
     cancelEvent(event);
 
-    if (!validation.emailAndPassword(email, pass)) {
-      return;
-    }
+    if (!validation.emailAndPassword(email, pass)) return;
 
     user.authenticate(email, pass, 
       function onComplete(authenticated) {
@@ -131,16 +125,12 @@
   }
 
   function cancelEvent(event) {
-    if (event) {
-      event.preventDefault();
-    }
+    if (event) event.preventDefault();
   }
 
   function enterEmailState(el, event) {
-    if (event && event.which === 13) {
-      // Enter key, do nothing
-      return;
-    }
+    // Enter key, do nothing
+    if (event && event.which === 13) return;
 
     if (!el.is(":disabled")) {
       this.submit = checkEmail;
@@ -185,6 +175,10 @@
     init: function(el, options) {
       options = options || {};
 
+      if (options.user) {
+        user = options.user;
+      }
+
       this._super(el, {
         bodyTemplate: "authenticate.ejs",
         bodyVars: {
@@ -196,9 +190,7 @@
       this.submit = checkEmail;
       // If we already have an email address, check if it is valid, if so, show 
       // password.
-      if (options.email) {
-        this.submit();
-      }
+      if (options.email) this.submit();
     },
 
     "#email keyup": function(el, event) {
diff --git a/browserid/static/dialog/controllers/checkregistration_controller.js b/resources/static/dialog/controllers/checkregistration_controller.js
similarity index 100%
rename from browserid/static/dialog/controllers/checkregistration_controller.js
rename to resources/static/dialog/controllers/checkregistration_controller.js
diff --git a/browserid/static/dialog/controllers/dialog_controller.js b/resources/static/dialog/controllers/dialog_controller.js
similarity index 86%
rename from browserid/static/dialog/controllers/dialog_controller.js
rename to resources/static/dialog/controllers/dialog_controller.js
index f25b2612410bb50b136106b6be49d5a6e5f52796..88fede3a584dc58aa186569188c3b234d146880e 100644
--- a/browserid/static/dialog/controllers/dialog_controller.js
+++ b/resources/static/dialog/controllers/dialog_controller.js
@@ -45,23 +45,38 @@
   var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
-      offline = false;
+      offline = false,
+      win = window;
+      
 
   PageController.extend("Dialog", {}, {
-      init: function(el) {
+      init: function(el, options) {
+        offline = false;
+
+        options = options || {};
+
+        if(options.window) {
+          win = options.window;
+        }
+
         var self=this;
-        //this.element.show();
 
         // keep track of where we are and what we do on success and error
         self.onsuccess = null;
         self.onerror = null;
-        setupChannel(self);
-        self.stateMachine();
+
+        try {
+          win.setupChannel(self);
+          self.stateMachine();
+        } catch (e) {
+          self.renderError("error.ejs", {
+            action: errors.relaySetup
+          });
+        }
       },
         
       getVerifiedEmail: function(origin_url, onsuccess, onerror) {
         var self=this;
-
         self.onsuccess = onsuccess;
         self.onerror = onerror;
 
@@ -71,13 +86,12 @@
         }
 
         user.setOrigin(origin_url);
-        
-        // get the cleaned origin.
         $("#sitename").text(user.getHostname());
 
         self.doCheckAuth();
 
-        $(window).bind("unload", function() {
+        $(win).bind("unload", function() {
+          bid.Storage.setStagedOnBehalfOf("");
           self.doCancel();
         });
       },
@@ -87,7 +101,6 @@
         var self=this, 
             hub = OpenAjax.hub, 
             el = this.element;
-       
 
         hub.subscribe("offline", function(msg, info) {
           self.doOffline();
@@ -118,7 +131,7 @@
         });
 
         hub.subscribe("assertion_generated", function(msg, info) {
-          if(info.assertion !== null) {
+          if (info.assertion !== null) {
             self.doAssertionGenerated(info.assertion);
           }
           else {
@@ -157,12 +170,16 @@
       },
 
       doOffline: function() {
-        this.renderError(errors.offline);
+        this.renderError("offline.ejs", {});
         offline = true;
       },
 
       doXHRError: function(info) {
-        if (!offline) this.renderError(errors.offline);  
+        if (!offline) {
+          this.renderError("error.ejs", $.extend({
+            action: errors.xhrError
+          }, info));
+        }
       },
 
       doConfirmUser: function(email) {
@@ -177,13 +194,18 @@
 
       doCancel: function() {
         var self=this;
-        if(self.onsuccess) {
+        if (self.onsuccess) {
           self.onsuccess(null);
         }
       },
 
       doPickEmail: function() {
-        this.element.pickemail();
+        this.element.pickemail({
+          // XXX ideal is to get rid of this and have a User function 
+          // that takes care of getting email addresses AND the last used email 
+          // for this site.
+          origin: user.getHostname()
+        });
       },
 
       doAuthenticate: function(info) {
diff --git a/browserid/static/dialog/controllers/page_controller.js b/resources/static/dialog/controllers/page_controller.js
similarity index 83%
rename from browserid/static/dialog/controllers/page_controller.js
rename to resources/static/dialog/controllers/page_controller.js
index ba80f9bbff4ee3569571b92843c422b76aa2c0e7..70113e8ad0d3165f32734415893fd530125303a6 100644
--- a/browserid/static/dialog/controllers/page_controller.js
+++ b/resources/static/dialog/controllers/page_controller.js
@@ -48,6 +48,8 @@
       var me=this,
           bodyTemplate = options.bodyTemplate,
           bodyVars = options.bodyVars,
+          errorTemplate = options.errorTemplate,
+          errorVars = options.errorVars,
           waitTemplate = options.waitTemplate,
           waitVars = options.waitVars;
 
@@ -60,6 +62,10 @@
         me.renderWait(waitTemplate, waitVars);
       }
 
+      if(errorTemplate) {
+        me.renderError(errorTemplate, errorVars);
+      }
+
       // XXX move all of these, bleck.
       $("form").bind("submit", me.onSubmit.bind(me));
       $("#cancel").click(me.onCancel.bind(me));
@@ -99,10 +105,20 @@
       $("#wait").stop().hide().fadeIn(ANIMATION_TIME);
     },
 
-    renderError: function(error_vars) {
-      this.renderTemplates("#error", "wait.ejs", error_vars);
-      $("body").removeClass("waiting").removeClass("form").addClass("error").css('opacity', 1);
-      $("#error").stop().hide().fadeIn(ANIMATION_TIME);
+    renderError: function(body, body_vars) {
+      this.renderTemplates("#error", body, body_vars);
+      $("body").removeClass("waiting").removeClass("form").addClass("error");
+      $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME);
+
+      /**
+       * What a big steaming pile, use CSS animations for this!
+       */
+      $("#openMoreInfo").click(function(event) {
+        event.preventDefault();
+
+        $("#moreInfo").slideDown();
+        $("#openMoreInfo").css({visibility: "hidden"});
+      });
     },
 
     onSubmit: function(event) {
@@ -139,12 +155,16 @@
     /**
      * Get a curried function to an error dialog.
      * @method getErrorDialog
-     * @method {object} info - info to use for the error dialog.  Should have 
+     * @method {object} action - info to use for the error dialog.  Should have 
      * two fields, message, description.
      */
-    getErrorDialog: function(info) {
+    getErrorDialog: function(action) {
       var self=this;
-      return self.renderError.bind(self, info);
+      return function(lowLevelInfo) {
+        self.renderError("error.ejs", $.extend({
+          action: action
+        }, lowLevelInfo));
+      }
     },
 
     onCancel: function(event) {
diff --git a/browserid/static/dialog/controllers/pickemail_controller.js b/resources/static/dialog/controllers/pickemail_controller.js
similarity index 92%
rename from browserid/static/dialog/controllers/pickemail_controller.js
rename to resources/static/dialog/controllers/pickemail_controller.js
index 1b3679c132953e12afd296df960419aa5a3cb945..a9197a4c60c03029d16fe39de02c24c67f4c0815 100644
--- a/browserid/static/dialog/controllers/pickemail_controller.js
+++ b/resources/static/dialog/controllers/pickemail_controller.js
@@ -41,6 +41,8 @@
       bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
+      storage = bid.Storage,
+      origin,
       body = $("body"),
       animationComplete = body.innerWidth() < 640,
       assertion;
@@ -109,6 +111,9 @@
     // the animation, hopefully this minimizes the delay the user notices.
     var self=this;
     user.getAssertion(email, function(assert) {
+      // XXX make a user api call that gets the assertion and sets the site 
+      // email as well.
+      storage.setSiteEmail(origin, email);
       assertion = assert || null;
       startAnimation.call(self);
     }, self.getErrorDialog(errors.getAssertion));
@@ -176,10 +181,16 @@
 
   PageController.extend("Pickemail", {}, {
     init: function(el, options) {
+      origin = options.origin;
+
       this._super(el, {
         bodyTemplate: "pickemail.ejs",
         bodyVars: {
-          identities: user.getStoredEmailKeypairs()
+          identities: user.getStoredEmailKeypairs(),
+          // XXX ideal is to get rid of this and have a User function 
+          // that takes care of getting email addresses AND the last used email 
+          // for this site.
+          siteemail: storage.getSiteEmail(options.origin)
         }
       });
 
diff --git a/browserid/static/dialog/css/m.css b/resources/static/dialog/css/m.css
similarity index 80%
rename from browserid/static/dialog/css/m.css
rename to resources/static/dialog/css/m.css
index 49f4a561375d7648adab479f5d104dab69b0c9bb..303557e63d05233aeb9c500c04c35878a6b64f2e 100644
--- a/browserid/static/dialog/css/m.css
+++ b/resources/static/dialog/css/m.css
@@ -78,7 +78,7 @@
       background-color: transparent;
   }
 
-  .error #formWrap, .waiting #formWrap {
+  .error #formWrap, .error #wait, .waiting #formWrap {
       display: none;
   }
 
@@ -102,15 +102,29 @@
     font-size: 14px;
   }
 
-  #content, .pickemail .form_section, .pickemail .inputs, .vertical {
+  #content, .form_section, .pickemail .inputs, .vertical {
     height: auto;
     overflow: visible;
   }
 
-  #wait .vertical {
+  #error .vertical, #wait .vertical {
       height: 250px;
   }
 
-}
+  #error .vertical {
+    width: auto;
+  }
+
+  #error .vertical > div {
+    display: block;
+    height: auto;
+    padding: 10px;
+  }
+
+  #error #borderbox {
+    border-left: none;
+    padding: 0;
+  }
+
 
 
diff --git a/browserid/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css
similarity index 93%
rename from browserid/static/dialog/css/popup.css
rename to resources/static/dialog/css/popup.css
index b1b54243f2d023bc6e2a46f27d7a621ad3b1ba01..fb43054b98e7ff5cbcac496726a0ffd0f5378cc1 100644
--- a/browserid/static/dialog/css/popup.css
+++ b/resources/static/dialog/css/popup.css
@@ -118,16 +118,21 @@ section > .contents {
 
 #wait, #error {
     text-align: center;
-    background-image: url("/i/bg.png");
 }
 
 #wait {
     z-index: 1;
+    background-image: url("/i/bg.png");
 }
 
 #error {
     display: none;
     z-index: 2;
+    background-color: #fff;
+}
+
+#error ul, #error li {
+    list-style-type: none; 
 }
 
 #wait strong, #error strong {
@@ -135,10 +140,44 @@ section > .contents {
     font-weight: bold;
 }
 
-#error {
-    z-index: 2;
+
+#error.unsupported .vertical {
+    width: 630px;
+    margin: 0 auto;
+    display: block;
+}
+
+
+#error.unsupported .vertical > div {
+    display: table-cell;
+    vertical-align: middle;
+    padding: 0 10px;
+    height: 250px;
+}
+
+#error #moreInfo {
+    display: none;
 }
 
+#error a {
+    color: #549FDC;
+    text-decoration: underline;
+}
+
+#error #borderbox {
+    border-left: 1px solid #777;
+    padding: 20px 0;
+}
+
+#error #borderbox img {
+    border: none;
+}
+
+#error #alternative .lighter {
+    color: #777;
+}
+
+
 #formWrap {
     background-color: #fff;
     background-image: none;
@@ -269,6 +308,10 @@ label {
     color: #333;
 }
 
+.inputs > li > label.preselected {
+    font-weight: bold;
+}
+
 .inputs > li:only-child > label {
     cursor: default;
 }
@@ -521,7 +564,7 @@ html[xmlns] .cf {
     overflow-y: auto;
 }
 
-.pickemail .form_section {
+.form_section {
     height: 176px;
 }
 
diff --git a/browserid/static/dialog/dialog.js b/resources/static/dialog/dialog.js
similarity index 97%
rename from browserid/static/dialog/dialog.js
rename to resources/static/dialog/dialog.js
index d3070184d61ce0bc8855bb5e552a55f8dbeae6f9..3c70f70d2a0ed2e7fc737f91fa9cd3fe3769c026 100644
--- a/browserid/static/dialog/dialog.js
+++ b/resources/static/dialog/dialog.js
@@ -68,7 +68,9 @@ steal.plugins(
 	.views('authenticate.ejs',
            'confirmemail.ejs',
            'pickemail.ejs',
-           'wait.ejs'
+           'wait.ejs',
+           'error.ejs',
+           'offline.ejs'
           ).
 
           then(function() {
diff --git a/browserid/static/dialog/funcunit.html b/resources/static/dialog/funcunit.html
similarity index 100%
rename from browserid/static/dialog/funcunit.html
rename to resources/static/dialog/funcunit.html
diff --git a/browserid/static/dialog/mozilla.png b/resources/static/dialog/mozilla.png
similarity index 100%
rename from browserid/static/dialog/mozilla.png
rename to resources/static/dialog/mozilla.png
diff --git a/resources/static/dialog/qunit.html b/resources/static/dialog/qunit.html
new file mode 100644
index 0000000000000000000000000000000000000000..0236807217744451277741940817a7f083dcc48c
--- /dev/null
+++ b/resources/static/dialog/qunit.html
@@ -0,0 +1,63 @@
+<html>
+	<head>
+		<link rel="stylesheet" type="text/css" href="/funcunit/qunit/qunit.css" />
+		<title>dialog QUnit Test</title>
+		<script type='text/javascript' src='/vepbundle'></script>
+		<script type='text/javascript' src='/steal/steal.js?/dialog/test/qunit'></script>
+	</head>
+	<body>
+
+		<h1 id="qunit-header">dialog Test Suite</h1>
+		<h2 id="qunit-banner"></h2>
+		<div id="qunit-testrunner-toolbar"></div>
+		<h2 id="qunit-userAgent"></h2>
+		<div id="test-content">
+    </div>
+		<ol id="qunit-tests"></ol>
+		<div id="qunit-test-area"></div>
+
+    <h3>Content below here is test content that can be ignored</h3>
+
+    <div id="controller_head">
+
+      <div id="formWrap">
+          <div class="contents"></div>
+      </div>
+
+      <div id="wait">
+          <div class="contents"></div>
+      </div>
+
+      <div id="error">
+          <div class="contents"></div>
+      </div>
+
+      <input id="email" />
+      <span id="cannotconfirm" class="error">Cannot confirm</span>
+      <span id="cannotcommunicate" class="error">Cannot communicate</span>
+      <span id="siteinfo" class="error"><span class="website"></span></span>
+      <span class=".hint">Hint</span>
+    </div>
+
+    <div id="needsTooltip">Tooltip Anchor</div>
+
+    <div id="shortTooltip" class="tooltip" for="needsTooltip">
+      short tooltip
+    </div>
+
+    <div id="longTooltip" class="tooltip" for="needsTooltip">
+      This is a long tooltip.  This should remain on the screen for about 5 seconds.
+    </div>
+
+    <ul class="notifications">
+      <li class="notification emailsent">Email Sent</li> 
+      <li class="notification doh">doh</li> 
+    </ul>
+
+    <script type="text/html" id="templateTooltip">
+      <div class="tooltip">
+        {{ contents }}
+      </div>
+    </script>
+	</body>
+</html>
diff --git a/browserid/static/dialog/register_iframe.html b/resources/static/dialog/register_iframe.html
similarity index 100%
rename from browserid/static/dialog/register_iframe.html
rename to resources/static/dialog/register_iframe.html
diff --git a/browserid/static/dialog/register_iframe.js b/resources/static/dialog/register_iframe.js
similarity index 100%
rename from browserid/static/dialog/register_iframe.js
rename to resources/static/dialog/register_iframe.js
diff --git a/browserid/static/dialog/resources/base64.js b/resources/static/dialog/resources/base64.js
similarity index 100%
rename from browserid/static/dialog/resources/base64.js
rename to resources/static/dialog/resources/base64.js
diff --git a/resources/static/dialog/resources/browser-support.js b/resources/static/dialog/resources/browser-support.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c2e34818d0e67ec6b20ed76ce0f03ce3404593a
--- /dev/null
+++ b/resources/static/dialog/resources/browser-support.js
@@ -0,0 +1,118 @@
+/*globals BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.BrowserSupport = (function() {
+  var bid = BrowserID,
+      win = window,
+      nav = navigator,
+      reason;
+
+  // For unit testing
+  function setTestEnv(newNav, newWindow) {
+    nav = newNav;
+    win = newWindow;
+  }
+
+  function getInternetExplorerVersion() {
+    var rv = -1; // Return value assumes failure.
+    if (nav.appName == 'Microsoft Internet Explorer') {
+      var ua = nav.userAgent;
+      var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+      if (re.exec(ua) != null)
+        rv = parseFloat(RegExp.$1);
+    }
+
+    return rv;
+  }
+
+  function checkIE() {
+    var ieVersion = getInternetExplorerVersion(),
+        ieNosupport = ieVersion > -1 && ieVersion < 9;
+
+    if(ieNosupport) {
+      return "IE_VERSION";
+    }
+  }
+
+  function explicitNosupport() {
+    return checkIE();
+  }
+
+  function checkLocalStorage() {
+    var localStorage = 'localStorage' in win && win['localStorage'] !== null;
+    if(!localStorage) {
+      return "LOCALSTORAGE";
+    }
+  }
+
+  function checkPostMessage() {
+    if(!win.postMessage) {
+      return "POSTMESSAGE";
+    }
+  }
+
+  function isSupported() {
+    reason = checkLocalStorage() || checkPostMessage() || explicitNosupport();
+
+    return !reason;
+  }
+
+  function getNoSupportReason() {
+    return reason;
+  }
+
+  return {
+    /**
+     * Set the test environment.
+     * @method setTestEnv
+     */
+    setTestEnv: setTestEnv,
+    /**
+     * Check whether the current browser is supported
+     * @method isSupported
+     * @returns {boolean}
+     */
+    isSupported: isSupported,
+    /**
+     * Called after isSupported, if isSupported returns false.  Gets the reason 
+     * why browser is not supported.
+     * @method getNoSupportReason
+     * @returns {string}
+     */
+    getNoSupportReason: getNoSupportReason
+  };
+  
+}());
+
diff --git a/browserid/static/dialog/resources/browserid-extensions.js b/resources/static/dialog/resources/browserid-extensions.js
similarity index 100%
rename from browserid/static/dialog/resources/browserid-extensions.js
rename to resources/static/dialog/resources/browserid-extensions.js
diff --git a/browserid/static/dialog/resources/browserid.js b/resources/static/dialog/resources/browserid.js
similarity index 100%
rename from browserid/static/dialog/resources/browserid.js
rename to resources/static/dialog/resources/browserid.js
diff --git a/browserid/static/dialog/resources/channel.js b/resources/static/dialog/resources/channel.js
similarity index 61%
rename from browserid/static/dialog/resources/channel.js
rename to resources/static/dialog/resources/channel.js
index 0d52976bb22f8dd74c7599d74dff39609716189f..859d641b8b266a4bb6a9c5719c1cc3af5b893030 100644
--- a/browserid/static/dialog/resources/channel.js
+++ b/resources/static/dialog/resources/channel.js
@@ -1,5 +1,5 @@
 /*jshint browsers:true, forin: true, laxbreak: true */
-/*global alert:true, setupNativeChannel:true, setupIFrameChannel:true*/
+/*global BrowserID: true*/
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -52,93 +52,95 @@
 
 
 (function() {
-  function getRelayID() {
-    return window.location.href.slice(window.location.href.indexOf('#') + 1);
-  }
+  var win = window,
+      nav = navigator,
+      onCompleteCallback;
 
   function getRelayName() {
-    return "browserid_relay_" + getRelayID();
+    var relayID = win.location.href.slice(win.location.href.indexOf('#') + 1);
+    return "browserid_relay_" + relayID;
   }
 
   function getRelayWindow() {
-    var frameWindow = window.opener.frames[getRelayName()];
+    var frameWindow = win.opener.frames[getRelayName()];
     return frameWindow;
   }
 
-  function registerWithRelayFrame(callback) {
-    var frameWindow = getRelayWindow();
-    if (frameWindow) {
-      frameWindow['register_dialog'](callback);
-    }
+  function setupNativeChannel(controller) {
+    nav.id.channel.registerController(controller);
   }
 
-  function getRPRelay() {
-    var frameWindow = getRelayWindow();
-    return frameWindow && frameWindow['browserid_relay'];
-  }
+  function setupIFrameChannel(controller) {
+    // TODO - Add a check for whether the dialog was opened by another window
+    // (has window.opener) as well as whether the relay function exists.
+    // If these conditions are not met, then print an appropriate message.
 
+    function onsuccess(rv) {
+      onCompleteCallback(rv, null);
+    }
 
-  function errorOut(trans, code) {
-    function getVerboseMessage(code) {
-      var msgs = {
-        "canceled": "user canceled selection",
-        "notImplemented": "the user tried to invoke behavior that's not yet implemented",
-        "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers."
-      };
-      var msg = msgs[code];
-      if (!msg) {
-        alert("need verbose message for " + code);
-        msg = "unknown error";
-          }
-      return msg;
+    function onerror(error) {
+      onCompleteCallback(null, error);
     }
-    trans.error(code, getVerboseMessage(code));
-    window.self.close();
-  }
 
+    // The relay frame will give us the origin and a function to call when 
+    // dialog processing is complete.
+    var frameWindow = getRelayWindow();
+    if (frameWindow) {
+      frameWindow.BrowserID.Relay.registerClient(function(origin, onComplete) {
+        onCompleteCallback = onComplete;
+        controller.getVerifiedEmail(origin, onsuccess, onerror);
+      });
+      win.location.hash = '';
+    }
+    else {
+      throw "relay frame not found";
+    }
+  }
 
-  window.setupChannel = function(controller) {
-    if (navigator.id && navigator.id.channel)
+  function open(controller) {
+    if (nav.id && nav.id.channel)
       setupNativeChannel(controller);
     else
       setupIFrameChannel(controller);
-  };
+  }
 
-  var setupNativeChannel = function(controller) {
-    navigator.id.channel.registerController(controller);
-  };
 
-  var setupIFrameChannel = function(controller) {
-    // TODO - Add a check for whether the dialog was opened by another window
-    // (has window.opener) as well as whether the relay function exists.
-    // If these conditions are not met, then print an appropriate message.
+  function init(options) {
+    onCompleteCallback = undefined;
 
-    // get the relay here at the time the channel is setup before any navigation has
-    // occured.  if we wait the window hash might change as a side effect to user
-    // navigation, which would cause us to not find our parent window.
-    // issue #295
-    var relay = getRPRelay();
-    
-    function onsuccess(rv) {
-      // Get the relay here so that we ensure that the calling window is still
-      // open and we aren't causing a problem.
-      if (relay) {
-        relay(rv, null);
-      }
+    if(options.navigator) {
+      nav = navigator;
     }
 
-    function onerror(error) {
-      if (relay) {
-        relay(null, error);
-      }
+    if(options.window) {
+      win = options.window;
     }
+  }
 
-    // The relay frame will give us the origin.
-    registerWithRelayFrame(function(origin) {
-      controller.getVerifiedEmail(origin, onsuccess, onerror);
-    });
 
-    window.location.hash = '';
-  };
+  if(window.BrowserID) {
+    BrowserID.Channel = {
+      /**
+       * Used to intialize the channel, mostly for unit testing to override 
+       * window and navigator.
+       * @method init
+       */
+      init: init,
+
+      /**
+       * Open the channel.
+       * @method open 
+       * @param {object} options - contains:
+       * *   options.getVerifiedEmail {function} - function to /get
+       */
+      open: open
+    };
+  }
 
+  /**
+   * This is here as a legacy API for addons/etc that are depending on 
+   * window.setupChannel;
+   */
+  window.setupChannel = open;
 }());
diff --git a/browserid/static/relay/relay.js b/resources/static/dialog/resources/error-messages.js
similarity index 61%
rename from browserid/static/relay/relay.js
rename to resources/static/dialog/resources/error-messages.js
index 46212cc2479faef04dc09da5e978b792bbc7c67a..3f6aeefc37aaea3b8209523edef1df81bd182377 100644
--- a/browserid/static/relay/relay.js
+++ b/resources/static/dialog/resources/error-messages.js
@@ -1,5 +1,4 @@
-/*global Channel: true, errorOut: true */
-
+/*global BrowserID: true*/
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -34,50 +33,72 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+BrowserID.Errors = (function(){
+  "use strict";
 
+  var Errors = {
+    authenticate: {
+      title: "Authenticating User"
+    },
 
-(function() {
-  "use strict";
+    addEmail: {
+      title: "Adding Address"
+    },
 
-  window.console = window.console || {
-    log: function() {}
-  };
+    checkAuthentication: {
+      title: "Checking Authentication"
+    },
 
-  var ipServer = "https://browserid.org",
-      transaction,
-      origin,
-      chan = Channel.build( {
-        window: window.parent,
-        origin: "*",
-        scope: "mozid"
-      } );
+    createUser: {
+      title: "Creating Account"
+    },
 
+    getAssertion: {
+      title: "Getting Assertion"
+    },
 
-  window.register_dialog = function(callback) {
-    // register the dialog, tell the dialog what the origin is.  
-    // Get the origin from the channel binding.
-    callback(origin);
-  };
+    isEmailRegistered: {
+      title: "Checking Email Address"
+    },
 
-  window.browserid_relay = function(status, error) {
-      if(error) {
-        errorOut(transaction, error);
-      }
-      else {
-        try {
-          transaction.complete(status);
-        } catch(e) {
-          // The relay function is called a second time after the 
-          // initial success, when the window is closing.
-        }
-      }
-  };
+    logoutUser: {
+      title: "Logout Failed"
+    },
+
+    offline: {
+      title: "You are offline!",
+      message: "Unfortunately, BrowserID cannot communicate while offline!"
+    },
+
+    registration: {
+      title: "Registration Failed"
+    },
 
-  chan.bind("getVerifiedEmail", function(trans, s) {
-    origin = trans.origin;
-    trans.delayReturn(true);
+    relaySetup: {
+      title: "Establishing Relay",
+      message: "Relay frame could not be found"
+    },
 
-    transaction = trans;
-  });
+    requestPasswordReset: {
+      title: "Resetting Password"
+    },
 
+    signIn: {
+      title: "Signin Failed"
+    },
+
+    syncAddress: {
+      title: "Syncing Address"
+    },
+
+    xhrError: {
+      title: "Communication Error"
+    }
+
+  };
+
+
+  return Errors;
 }());
+
+
diff --git a/browserid/static/dialog/resources/jschannel.js b/resources/static/dialog/resources/jschannel.js
similarity index 100%
rename from browserid/static/dialog/resources/jschannel.js
rename to resources/static/dialog/resources/jschannel.js
diff --git a/browserid/static/dialog/resources/network.js b/resources/static/dialog/resources/network.js
similarity index 91%
rename from browserid/static/dialog/resources/network.js
rename to resources/static/dialog/resources/network.js
index b447aa2f08353db38d5f928a4f9b66154e25591b..ea1f53b312e0e9c1c9c4a3a7c6351386996c71c1 100644
--- a/browserid/static/dialog/resources/network.js
+++ b/resources/static/dialog/resources/network.js
@@ -37,8 +37,7 @@
 BrowserID.Network = (function() {
   "use strict";
 
-  var XHR_TIMEOUT = 10000,
-      csrf_token,
+  var csrf_token,
       xhr = $,
       server_time,
       auth_status,
@@ -55,10 +54,17 @@ BrowserID.Network = (function() {
     }
   }
 
-  function xhrError(cb, errorMessage) {
-    return function() {
-      if (cb) cb();
-      hub && hub.publish("xhrError", errorMessage);
+  function xhrError(cb, info) {
+    return function(jqXHR, textStatus, errorThrown) {
+      info = info || {};
+      var network = info.network = info.network || {};
+
+      network.status = jqXHR && jqXHR.status;
+      network.textStatus = textStatus;
+      network.errorThrown = errorThrown;
+
+      if (cb) cb(info);
+      hub && hub.publish("xhrError", info);
     };
   }
 
@@ -70,9 +76,13 @@ BrowserID.Network = (function() {
       // that are thrown in the response handlers and it becomes very difficult 
       // to debug.
       success: deferResponse(options.success),
-      error: deferResponse(xhrError(options.error, options.errorMessage)),
-      dataType: "json",
-      timeout: XHR_TIMEOUT
+      error: deferResponse(xhrError(options.error, {
+        network: {
+          type: "GET",
+          url: options.url
+        }
+      })),
+      dataType: "json"
     });
   }
 
@@ -92,8 +102,13 @@ BrowserID.Network = (function() {
         // that are thrown in the response handlers and it becomes very difficult 
         // to debug.
         success: deferResponse(options.success),
-        error: deferResponse(xhrError(options.error, options.errorMessage)),
-        timeout: XHR_TIMEOUT
+        error: deferResponse(xhrError(options.error, {
+          network: {
+            type: "POST",
+            url: options.url,
+            data: options.data
+          }
+        }))
       });
     }, options.error);
   }
@@ -101,6 +116,7 @@ BrowserID.Network = (function() {
   function withContext(cb, onFailure) {
     if (typeof auth_status === 'boolean' && typeof csrf_token !== 'undefined') cb();
     else {
+      var url = "/wsapi/session_context";
       xhr.ajax({
         url: "/wsapi/session_context",
         success: function(result) {
@@ -112,8 +128,12 @@ BrowserID.Network = (function() {
           auth_status = result.authenticated;
           cb();
         },
-        error: deferResponse(xhrError(onFailure)),
-        timeout: XHR_TIMEOUT
+        error: deferResponse(xhrError(onFailure, {
+          network: {
+            type: "GET",
+            url: url
+          }
+        }))
       });
     }
   }
@@ -233,7 +253,13 @@ BrowserID.Network = (function() {
         success: function(status) {
           if (onSuccess) onSuccess(status.success);
         },
-        error: onFailure
+        error: function(info) {
+          // 403 is throttling.
+          if(info.network.status === 403) {
+            if (onSuccess) onSuccess(false); 
+          }
+          else if (onFailure) onFailure(info);
+        }
       });
     },
 
@@ -391,7 +417,13 @@ BrowserID.Network = (function() {
         success: function(status) {
           if (onSuccess) onSuccess(status.success);
         },
-        error: onFailure
+        error: function(info) {
+          // 403 is throttling.
+          if(info.network.status === 403) {
+            if (onSuccess) onSuccess(false); 
+          }
+          else if (onFailure) onFailure(info);
+        }
       });
     },
 
diff --git a/browserid/static/dialog/resources/storage.js b/resources/static/dialog/resources/storage.js
similarity index 100%
rename from browserid/static/dialog/resources/storage.js
rename to resources/static/dialog/resources/storage.js
diff --git a/browserid/static/dialog/resources/tooltip.js b/resources/static/dialog/resources/tooltip.js
similarity index 81%
rename from browserid/static/dialog/resources/tooltip.js
rename to resources/static/dialog/resources/tooltip.js
index 8625595e44fb8dd760e8e63ce44d9dc671e53118..281e664db3d1c0d4884b71e0614c6d178e53cd13 100644
--- a/browserid/static/dialog/resources/tooltip.js
+++ b/resources/static/dialog/resources/tooltip.js
@@ -39,7 +39,8 @@ BrowserID.Tooltip = (function() {
   "use strict";
 
   var ANIMATION_TIME = 250,
-      TOOLTIP_DISPLAY = 2000;
+      TOOLTIP_DISPLAY = 2000,
+      READ_WPM = 200;
 
   function createTooltip(el) {
       var contents = el.html();
@@ -63,14 +64,21 @@ BrowserID.Tooltip = (function() {
   }
 
   function animateTooltip(el, complete) {
+    var contents = el.text().replace(/\s+/, ' ').replace(/^\s+/, '').replace(/\s+$/, '');
+    var words = contents.split(' ').length;
+
+    // The average person can read ± 250 wpm.
+    var wordTimeMS = (words / READ_WPM) * 60 * 1000;
+    var displayTimeMS = Math.max(wordTimeMS, TOOLTIP_DISPLAY);
+
     el.fadeIn(ANIMATION_TIME, function() {
       setTimeout(function() {
         el.fadeOut(ANIMATION_TIME, complete);
-      }, TOOLTIP_DISPLAY);
+      }, displayTimeMS);
     });
   }
 
-  function createAndShowRelatedTooltip(el, relatedTo) {
+  function createAndShowRelatedTooltip(el, relatedTo, complete) {
       // This means create a copy of the tooltip element and position it in 
       // relation to an element.  Right now we are putting the tooltip directly 
       // above the element.  Once the tooltip is no longer needed, remove it 
@@ -81,22 +89,26 @@ BrowserID.Tooltip = (function() {
       positionTooltip(tooltip, target);
 
       animateTooltip(tooltip, function() {
-        tooltip.remove();
-        tooltip = null;
+        console.log("close tooltip");
+        if (tooltip) {
+          tooltip.remove();
+          tooltip = null;
+        }
+        if (complete) complete();
       });
   }
 
-  function showTooltip(el) {
+  function showTooltip(el, complete) {
     el = $(el);
     var messageFor = el.attr("for");
 
     // First, see if we are "for" another element, if we are, create a copy of 
     // the tooltip to attach to the element.
     if(messageFor) {
-      createAndShowRelatedTooltip(el, messageFor);
+      createAndShowRelatedTooltip(el, messageFor, complete);
     }
     else {
-      animateTooltip(el);
+      animateTooltip(el, complete);
     }
   }
 
diff --git a/browserid/static/dialog/resources/underscore-min.js b/resources/static/dialog/resources/underscore-min.js
similarity index 100%
rename from browserid/static/dialog/resources/underscore-min.js
rename to resources/static/dialog/resources/underscore-min.js
diff --git a/browserid/static/dialog/resources/user.js b/resources/static/dialog/resources/user.js
similarity index 88%
rename from browserid/static/dialog/resources/user.js
rename to resources/static/dialog/resources/user.js
index fb6419addb05d1cd477065d8d22895ab15fc468d..a1541ffbdc0f35ba165c6cb15603066f6a3cb152 100644
--- a/browserid/static/dialog/resources/user.js
+++ b/resources/static/dialog/resources/user.js
@@ -108,6 +108,10 @@ BrowserID.User = (function() {
         //   'mustAuth' - user must authenticate
         //   'noRegistration' - no registration is in progress
         if (status === "complete" || status === "mustAuth") {
+          // As soon as the registration comes back as complete, we should 
+          // ensure that the stagedOnBehalfOf is cleared so there is no stale 
+          // data.
+          storage.setStagedOnBehalfOf("");
           if (onSuccess) {
             onSuccess(status);
           }
@@ -232,11 +236,7 @@ BrowserID.User = (function() {
       // remember this for later
       storage.setStagedOnBehalfOf(self.getHostname());
       
-      network.createUser(email, origin, function(created) {
-        if (onSuccess) {
-          onSuccess(created);
-        }
-      }, onFailure);
+      network.createUser(email, origin, onSuccess, onFailure);
     },
 
     /**
@@ -250,40 +250,87 @@ BrowserID.User = (function() {
       registrationPoll(network.checkUserRegistration, email, onSuccess, onFailure);
     },
 
+    /**
+     * Verify a user
+     * @method verifyUser
+     * @param {string} token - token to verify.
+     * @param {string} password - password to set for account.
+     * @param {function} [onSuccess] - Called to give status updates.
+     * @param {function} [onFailure] - Called on error.
+     */
+    verifyUser: function(token, password, onSuccess, onFailure) {
+      network.emailForVerificationToken(token, function (email) {
+        var invalidInfo = { valid: false };
+        if (email) {
+          network.completeUserRegistration(token, password, function (valid) {
+            var info = valid ? {
+              valid: valid,
+              email: email,
+              origin: storage.getStagedOnBehalfOf()
+            } : invalidInfo;
+
+            storage.setStagedOnBehalfOf("");
+
+            if (onSuccess) onSuccess(info);
+          }, onFailure);
+        } else if(onSuccess) {
+          onSuccess(invalidInfo);
+        }
+      }, onFailure);
+    },
+
     /**
      * Set the password of the current user.
      * @method setPassword
      * @param {string} password - password to set
-     * @param {function} [onSuccess] - Called on successful completion. 
+     * @param {function} [onComplete] - Called on successful completion. 
      * @param {function} [onFailure] - Called on error.
      */
-    setPassword: function(password, onSuccess, onFailure) {
-      network.setPassword(password, onSuccess, onFailure);
+    setPassword: function(password, onComplete, onFailure) {
+      network.setPassword(password, onComplete, onFailure);
     },
 
     /**
      * Request a password reset for the given email address.
      * @method requestPasswordReset
      * @param {string} email - email address to reset password for.
-     * @param {function} [onSuccess] - Callback to call when complete.
+     * @param {function} [onComplete] - Callback to call when complete, called 
+     * with a single object, info.
+     *    info.status {boolean} - true or false whether request was successful.
+     *    info.reason {string} - if status false, reason of failure.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    requestPasswordReset: function(email, onSuccess, onFailure) {
-      network.requestPasswordReset(email, origin, onSuccess, onFailure);
+    requestPasswordReset: function(email, onComplete, onFailure) {
+      this.isEmailRegistered(email, function(registered) {
+        if (registered) {
+          network.requestPasswordReset(email, origin, function(reset) {
+            var status = {
+              success: reset
+            };
+
+            if(!reset) status.reason = "throttle";
+
+            if (onComplete) onComplete(status);
+          }, onFailure);
+        }
+        else if (onComplete) {
+          onComplete({ success: false, reason: "invalid_user" });
+        }
+      }, onFailure);
     },
 
     /**
      * Cancel the current user's account.  Remove last traces of their 
      * identity.
      * @method cancelUser
-     * @param {function} [onSuccess] - Called whenever complete.
+     * @param {function} [onComplete] - Called whenever complete.
      * @param {function} [onFailure] - called on error.
      */
-    cancelUser: function(onSuccess, onFailure) {
+    cancelUser: function(onComplete, onFailure) {
       network.cancelUser(function() {
         setAuthenticationStatus(false);
-        if (onSuccess) {
-          onSuccess();
+        if (onComplete) {
+          onComplete();
         }
       }, onFailure);
 
@@ -451,13 +498,10 @@ BrowserID.User = (function() {
     addEmail: function(email, onSuccess, onFailure) {
       var self = this;
       network.addEmail(email, origin, function(added) {
-        if (added) {
-          storage.setStagedOnBehalfOf(self.getHostname());
-          // we no longer send the keypair, since we will certify it later.
-          if (onSuccess) {
-            onSuccess(added);
-          }
-        }
+        if (added) storage.setStagedOnBehalfOf(self.getHostname());
+
+        // we no longer send the keypair, since we will certify it later.
+        if (onSuccess) onSuccess(added);
       }, onFailure);
     },
 
diff --git a/browserid/static/dialog/resources/validation.js b/resources/static/dialog/resources/validation.js
similarity index 90%
rename from browserid/static/dialog/resources/validation.js
rename to resources/static/dialog/resources/validation.js
index aa8b5603bb06f4bad93b3022175752a03333944a..15b98c1b7a6bf3b1837f97f8cec72ec541bb4dea 100644
--- a/browserid/static/dialog/resources/validation.js
+++ b/resources/static/dialog/resources/validation.js
@@ -41,7 +41,15 @@ BrowserID.Validation = (function() {
     // gotten from http://blog.gerv.net/2011/05/html5_email_address_regexp/
     // changed the requirement that there must be a ldh-str because BrowserID 
     // is only used on internet based networks.
-    return /^[\w.!#$%&'*+\-/=?\^`{|}~]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(address);
+    var parts = address.split("@");
+
+    return /^[\w.!#$%&'*+\-/=?\^`{|}~]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(address)
+           // total address allwed to be 254 bytes long
+           && address.length <= 254
+           // local side only allowed to be 64 bytes long
+           && parts[0] && parts[0].length <= 64
+           // domain side allowed to be up to 253 bytes long
+           && parts[1] && parts[1].length <= 253;
   };
 
 
diff --git a/browserid/static/dialog/resources/wait-messages.js b/resources/static/dialog/resources/wait-messages.js
similarity index 100%
rename from browserid/static/dialog/resources/wait-messages.js
rename to resources/static/dialog/resources/wait-messages.js
diff --git a/browserid/static/dialog/scripts/build.html b/resources/static/dialog/scripts/build.html
similarity index 100%
rename from browserid/static/dialog/scripts/build.html
rename to resources/static/dialog/scripts/build.html
diff --git a/browserid/static/dialog/scripts/build.js b/resources/static/dialog/scripts/build.js
similarity index 100%
rename from browserid/static/dialog/scripts/build.js
rename to resources/static/dialog/scripts/build.js
diff --git a/browserid/static/dialog/scripts/clean.js b/resources/static/dialog/scripts/clean.js
similarity index 100%
rename from browserid/static/dialog/scripts/clean.js
rename to resources/static/dialog/scripts/clean.js
diff --git a/browserid/static/dialog/scripts/docs.js b/resources/static/dialog/scripts/docs.js
similarity index 100%
rename from browserid/static/dialog/scripts/docs.js
rename to resources/static/dialog/scripts/docs.js
diff --git a/browserid/static/dialog/test/funcunit/dialog_test.js b/resources/static/dialog/test/funcunit/dialog_test.js
similarity index 100%
rename from browserid/static/dialog/test/funcunit/dialog_test.js
rename to resources/static/dialog/test/funcunit/dialog_test.js
diff --git a/browserid/static/dialog/test/funcunit/funcunit.js b/resources/static/dialog/test/funcunit/funcunit.js
similarity index 100%
rename from browserid/static/dialog/test/funcunit/funcunit.js
rename to resources/static/dialog/test/funcunit/funcunit.js
diff --git a/verifier/test/certassertion-test.js b/resources/static/dialog/test/qunit/controllers/authenticate_controller_unit_test.js
similarity index 50%
rename from verifier/test/certassertion-test.js
rename to resources/static/dialog/test/qunit/controllers/authenticate_controller_unit_test.js
index 24edbead4e003185e41a33d3f8b354f6d85eb992..0a40cf088dd586bcd81dfdbcf7ec65c98a7da1c2 100644
--- a/verifier/test/certassertion-test.js
+++ b/resources/static/dialog/test/qunit/controllers/authenticate_controller_unit_test.js
@@ -1,3 +1,5 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -18,7 +20,6 @@
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
- *     Ben Adida <benadida@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
@@ -33,44 +34,61 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/controllers/authenticate_controller", function() {
+  "use strict";
 
-var vows = require("vows"),
-    assert = require("assert"),
-    certassertion = require("../lib/certassertion"),
-    jwk = require("jwcrypto/jwk"),
-    jwt = require("jwcrypto/jwt"),
-    jwcert = require("jwcrypto/jwcert"),
-    vep = require("jwcrypto/vep"),
-    events = require("events");
+  var controller, 
+      el = $("body"),
+      storage = BrowserID.Storage,
+      emailRegistered = false,
+      userCreated = true;
 
-vows.describe('certassertion').addBatch({
-  "generate and certify key + assertion" : {
-    topic: function() {
-      // generate a key
-      var root_kp = jwk.KeyPair.generate("RS", 64);
-      var user_kp = jwk.KeyPair.generate("RS", 64);
-      var cert = new jwcert.JWCert("fakeroot.com", new Date(), user_kp.publicKey, {email:"user@fakeroot.com"}).sign(root_kp.secretKey);
-      var assertion = new jwt.JWT(null, new Date(), "rp.com").sign(user_kp.secretKey);
-
-      var self = this;
-      var bundle = vep.bundleCertsAndAssertion([cert],assertion);
-      
-      // verify it
-      certassertion.verify(
-        bundle, "rp.com",
-        function(email, audience, expires) {
-          self.callback({email:email, audience: audience, expires:expires});
-        },
-        function(msg) {},
-        function(issuer, next) {
-          if (issuer == "fakeroot.com")
-            next(root_kp.publicKey);
-          else
-            next(null);
-        });
+  var userMock = {
+    getHostname: function() { return "host"; },
+    isEmailRegistered: function(email, onSuccess, onFailure) {
+      onSuccess(emailRegistered);
     },
-    "is successful": function(res, err) {
-      assert.notEqual(res.email, null);
+
+    createUser: function(email, onSuccess, onFailure) {
+      onSuccess(userCreated);
     }
+  };
+
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+
+    emailRegistered = false;
+    userCreated = true;
+
+    OpenAjax.hub.unsubscribe("user_staged");
   }
-}).export(module);
\ No newline at end of file
+
+  module("controllers/authenticate_controller", {
+    setup: function() {
+      reset();
+      storage.clear();
+      controller = el.authenticate({ user: userMock }).controller();
+    },
+
+    teardown: function() {
+      if (controller) {
+        controller.destroy();
+      }    
+      reset();
+      storage.clear();
+    } 
+  });
+
+  test("setting email address prefills address field", function() {
+      controller.destroy();
+      $("#email").val("");
+      controller = el.authenticate({ user: userMock, email: "testuser@testuser.com" }).controller();
+      equal($("#email").val(), "testuser@testuser.com", "email prefilled");
+  });
+
+
+});
+
diff --git a/resources/static/dialog/test/qunit/controllers/dialog_controller_unit_test.js b/resources/static/dialog/test/qunit/controllers/dialog_controller_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..47be43214a20d1386b239295b1d27d35b7305928
--- /dev/null
+++ b/resources/static/dialog/test/qunit/controllers/dialog_controller_unit_test.js
@@ -0,0 +1,122 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/controllers/dialog_controller", function() {
+  "use strict";
+
+  var controller,
+      el,
+      channelError = false;
+
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+
+    channelError = false;
+  }
+
+  function initController() {
+    controller = el.dialog({
+      window: {
+        setupChannel: function() { 
+          if (channelError) throw "Channel error";  
+        }
+      }
+    }).controller();
+  }
+
+  module("controllers/dialog_controller", {
+    setup: function() {
+      reset();
+      initController();
+    },
+
+    teardown: function() {
+      controller.destroy();
+      reset();
+    } 
+  });
+
+  test("initialization with channel error", function() {
+    controller.destroy();
+    reset();
+    channelError = true;
+
+    initController();
+
+    ok($("#error .contents").text().length, "contents have been written");
+  });
+
+  test("doOffline", function() {
+    controller.doOffline();
+    ok($("#error .contents").text().length, "contents have been written");
+    ok($("#error #offline").text().length, "offline error message has been written");
+  });
+
+  test("doXHRError while online, no network info given", function() {
+    controller.doXHRError();
+    ok($("#error .contents").text().length, "contents have been written");
+    ok($("#error #action").text().length, "action contents have been written");
+    equal($("#error #network").text().length, 0, "no network contents to be written");
+  });
+
+  test("doXHRError while online, network info given", function() {
+    controller.doXHRError({
+      network: {
+        type: "POST",
+        url: "browserid.org/verify"
+      }
+    });
+    ok($("#error .contents").text().length, "contents have been written");
+    ok($("#error #action").text().length, "action contents have been written");
+    ok($("#error #network").text().length, "network contents have been written");
+  });
+
+  test("doXHRError while offline does not update contents", function() {
+    controller.doOffline();
+    $("#error #action").remove();
+
+    controller.doXHRError();
+    ok(!$("#error #action").text().length, "XHR error is not reported if the user is offline.");
+  });
+
+  test("window.unload causes setStagedOnBehalfOf data to be cleared", function() {
+  });
+
+});
+
diff --git a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js b/resources/static/dialog/test/qunit/controllers/page_controller_unit_test.js
similarity index 73%
rename from browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js
rename to resources/static/dialog/test/qunit/controllers/page_controller_unit_test.js
index fca829d5c917981be1272ff7ee2148d1f089fa5c..299b8ed000444c38d3d8e3cf13ad80ad89f6b6a4 100644
--- a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js
+++ b/resources/static/dialog/test/qunit/controllers/page_controller_unit_test.js
@@ -41,16 +41,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
       bodyTemplate = "testBodyTemplate.ejs",
       waitTemplate = "wait.ejs";
 
-  module("PageController", {
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+  }
+
+  module("/controllers/page_controller", {
     setup: function() {
-      el = $("#page_controller");
+      reset();
     },
 
     teardown: function() {
-      el.find("#formWrap .contents").html("");
-      el.find("#wait .contents").html("");
-      el.find("#error .contents").html("");
       controller.destroy();
+      reset();
     } 
   });
 
@@ -76,10 +81,11 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     var html = el.find("#formWrap .contents").html();
     ok(html.length, "with template specified, form text is loaded");
 
+/*
 
     var input = el.find("input").eq(0);
     ok(input.is(":focus"), "make sure the first input is focused");
-
+*/
     html = el.find("#wait .contents").html();
     equal(html, "", "with body template specified, wait text is not loaded");
   });
@@ -100,6 +106,22 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     ok(html.length, "with wait template specified, wait text is loaded");
   });
 
+  test("page controller with error template renders in #error .contents", function() {
+    controller = el.page({
+      errorTemplate: waitTemplate,
+      errorVars: {
+        title: "Test title",
+        message: "Test message"
+      }
+    }).controller();
+
+    var html = el.find("#formWrap .contents").html();
+    equal(html, "", "with error template specified, form is ignored");
+
+    html = el.find("#error .contents").html();
+    ok(html.length, "with error template specified, error text is loaded");
+  });
+
   test("renderError renders an error message", function() {
     controller = el.page({
       waitTemplate: waitTemplate,
@@ -109,7 +131,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
       }
     }).controller();
    
-    controller.renderError({
+    controller.renderError("wait.ejs", {
       title: "error title",
       message: "error message"
     });
@@ -119,6 +141,37 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     ok(html.length, "with error template specified, error text is loaded");
   });
 
+  test("renderError allows us to open expanded error info", function() {
+    controller = el.page({
+      waitTemplate: waitTemplate,
+      waitVars: {
+        title: "Test title",
+        message: "Test message"
+      }
+    }).controller();
+   
+    controller.renderError("error.ejs", {
+      action: {
+        title: "expanded action info",
+        message: "expanded message"
+      }
+    });
+
+    var html = el.find("#error .contents").html();
+
+    $("#moreInfo").hide();
+
+    $("#openMoreInfo").click();
+
+    setTimeout(function() {
+      equal($("#showMoreInfo").is(":visible"), false, "button is not visible after clicking expanded info");
+      equal($("#moreInfo").is(":visible"), true, "expanded error info is visible after clicking expanded info");
+      start();
+    }, 500);
+
+    stop();
+  });
+
   test("getErrorDialog gets a function that can be used to render an error message", function() {
     controller = el.page({
       waitTemplate: waitTemplate,
@@ -128,9 +181,10 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
       }
     }).controller();
    
+    // This is the medium level info.
     var func = controller.getErrorDialog({
-      title: "error title",
-      message: "error message"
+      title: "medium level info error title",
+      message: "medium level info error message"
     });
 
     equal(typeof func, "function", "a function was returned from getErrorDialog");
diff --git a/resources/static/dialog/test/qunit/controllers/pickemail_controller_unit_test.js b/resources/static/dialog/test/qunit/controllers/pickemail_controller_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..25730ca41e0bbbd536078a531591c22e5d843992
--- /dev/null
+++ b/resources/static/dialog/test/qunit/controllers/pickemail_controller_unit_test.js
@@ -0,0 +1,96 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/controllers/pickemail_controller", function() {
+  "use strict";
+
+  var controller, 
+      el = $("body"),
+      storage = BrowserID.Storage;
+
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+  }
+
+  module("controllers/pickemail_controller", {
+    setup: function() {
+      reset();
+      storage.clear();
+    },
+
+    teardown: function() {
+      if (controller) {
+        controller.destroy();
+      }    
+      reset();
+      storage.clear();
+    } 
+  });
+
+
+  test("pickemail controller with email associated with site", function() {
+    storage.addEmail("testuser@testuser.com", {priv: "priv", pub: "pub"});
+    storage.addEmail("testuser2@testuser.com", {priv: "priv", pub: "pub"});
+    storage.setSiteEmail("browserid.org", "testuser2@testuser.com");
+
+    controller = el.pickemail({origin: "browserid.org"}).controller();
+    ok(controller, "controller created");
+
+    var radioButton = $("input[type=radio]").eq(1);
+    ok(radioButton.is(":checked"), "the email address we specified is checked");
+
+    var label = radioButton.parent();;
+    ok(label.hasClass("preselected"), "the label has the preselected class");
+  });
+
+  test("pickemail controller without email associated with site", function() {
+    storage.addEmail("testuser@testuser.com", {priv: "priv", pub: "pub"});
+
+    controller = el.pickemail({origin: "browserid.org"}).controller();
+    ok(controller, "controller created");
+
+    var radioButton = $("input[type=radio]").eq(0);
+    equal(radioButton.is(":checked"), false, "The email address is not checked");
+
+    var label = radioButton.parent();
+    equal(label.hasClass("preselected"), false, "the label has no class");
+  });
+
+});
+
diff --git a/browserid/static/dialog/test/qunit/dialog_test.js b/resources/static/dialog/test/qunit/dialog_test.js
similarity index 100%
rename from browserid/static/dialog/test/qunit/dialog_test.js
rename to resources/static/dialog/test/qunit/dialog_test.js
diff --git a/verifier/lib/httputils.js b/resources/static/dialog/test/qunit/include_unit_test.js
similarity index 72%
rename from verifier/lib/httputils.js
rename to resources/static/dialog/test/qunit/include_unit_test.js
index f062b02f9ed12fc519e18f3a41037e47eca2b408..815ecf82138ce58855bffdae94d8639568269a97 100644
--- a/verifier/lib/httputils.js
+++ b/resources/static/dialog/test/qunit/include_unit_test.js
@@ -1,3 +1,5 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -32,34 +34,19 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then("/include.js", function() {
+  "use strict";
 
-// various little utilities to make crafting boilerplate responses
-// simple
+  module("include.js");
+  
+  test("navigator.id is available", function() {
+    equal(typeof navigator.id, "object", "navigator.id namespace is available");
+  });
 
-exports.fourOhFour = function(resp, reason)
-{
-  resp.writeHead(404, {"Content-Type": "text/plain"});
-  resp.write("Not Found");
-  if (reason) {
-    resp.write(": " + reason);
-  }
-  resp.end();
-};
+  test("navigator.id.getVerifiedEmail is available", function() {
+    equal(typeof navigator.id.getVerifiedEmail, "function", "navigator.id.getVerifiedEmail is available");
+  });
 
-exports.serverError = function(resp, reason)
-{
-  resp.writeHead(500, {"Content-Type": "text/plain"});
-  if (reason) resp.write(reason);
-  resp.end();
-};
 
-exports.badRequest = function(resp, reason)
-{
-  resp.writeHead(400, {"Content-Type": "text/plain"});
-  resp.write("Bad Request");
-  if (reason) {
-    resp.write(": " + reason);
-  }
-  resp.end();
-};
+});
 
diff --git a/browserid/static/dialog/test/qunit/browserid_unit_test.js b/resources/static/dialog/test/qunit/js/browserid_unit_test.js
similarity index 93%
rename from browserid/static/dialog/test/qunit/browserid_unit_test.js
rename to resources/static/dialog/test/qunit/js/browserid_unit_test.js
index 5755fabc56122270993a990e6bea620d0c424eb5..840039df59b43dd36e016f6c23636bddcc784d55 100644
--- a/browserid/static/dialog/test/qunit/browserid_unit_test.js
+++ b/resources/static/dialog/test/qunit/js/browserid_unit_test.js
@@ -34,10 +34,10 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", function() {
+steal.plugins("jquery", "funcunit/qunit").then("/js/page_helpers", "/js/browserid", function() {
   "use strict";
 
-  module("browserid-unit");
+  module("/js/browserid");
   
 
 });
diff --git a/resources/static/dialog/test/qunit/js/page_helpers_unit_test.js b/resources/static/dialog/test/qunit/js/page_helpers_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..9f076596ea5b4691651f1e9fa7705923b556fc02
--- /dev/null
+++ b/resources/static/dialog/test/qunit/js/page_helpers_unit_test.js
@@ -0,0 +1,81 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", function() {
+  "use strict";
+
+  var pageHelpers = BrowserID.PageHelpers;
+
+  module("/js/page_helpers");
+  
+
+  test("setStoredEmail/getStoredEmail/setupEmail prefills the email address", function() {
+    $("#email").val("");
+
+    pageHelpers.setStoredEmail("testuser@testuser.com");
+    pageHelpers.setupEmail();
+
+    equal($("#email").val(), "testuser@testuser.com", "email was set on setupEmail");
+    equal(pageHelpers.getStoredEmail(), "testuser@testuser.com", "getStoredEmail works correctly");
+  });
+
+  test("a key press in the email address field saves it", function() {
+    $("#email").val("");
+
+    pageHelpers.setStoredEmail("testuser@testuser.co");
+    pageHelpers.setupEmail();
+
+    // The fake jQuery event does not actually cause the letter to be added, we 
+    // have to do that manually.
+    $("#email").val("testuser@testuser.com");
+
+    var e = jQuery.Event("keyup");
+    e.which = 77; //choose the one you want
+    e.keyCode = 77;
+    $("#email").trigger(e);
+
+    equal(pageHelpers.getStoredEmail(), "testuser@testuser.com", "hitting a key updates the stored email");
+  });
+
+  test("clearStoredEmail clears the email address from storage", function() {
+    pageHelpers.clearStoredEmail();
+
+    equal(pageHelpers.getStoredEmail(), "", "clearStoredEmail clears stored email");
+  });
+
+});
+
+
diff --git a/verifier/run.js b/resources/static/dialog/test/qunit/mocks/mocks.js
old mode 100755
new mode 100644
similarity index 78%
rename from verifier/run.js
rename to resources/static/dialog/test/qunit/mocks/mocks.js
index 3c93845cef5995e116f6cf0e3937f0c835f124f3..70a00e1ffc8e09b63fc58f6d70ea3fe1d21e82b4
--- a/verifier/run.js
+++ b/resources/static/dialog/test/qunit/mocks/mocks.js
@@ -1,5 +1,6 @@
-#!/usr/bin/env node
 
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -34,20 +35,5 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+BrowserID.Mocks = {};
 
-var   sys = require("sys"),
-     path = require("path"),
-       fs = require("fs"),
-  express = require("express");
-
-var PRIMARY_HOST = "127.0.0.1";
-var PRIMARY_PORT = 62800;
-
-var handler = require("./app.js");
-
-var app = express.createServer();
-
-// let the specific server interact directly with the express server to register their middleware
-if (handler.setup) handler.setup(app);
-
-app.listen(PRIMARY_PORT, PRIMARY_HOST);
diff --git a/resources/static/dialog/test/qunit/mocks/xhr.js b/resources/static/dialog/test/qunit/mocks/xhr.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c09c8cabc78adfae74fb6c98dd6106dd98ab1fd
--- /dev/null
+++ b/resources/static/dialog/test/qunit/mocks/xhr.js
@@ -0,0 +1,162 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global wrappedAsyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+BrowserID.Mocks.xhr = (function() {
+  var  contextInfo = {
+      server_time: new Date().getTime(),
+      csrf_token: "csrf",
+      authenticated: false
+    };
+
+  // this cert is meaningless, but it has the right format
+  var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
+
+  /**
+   * This is the results table, the keys are the request type, url, and 
+   * a "selector" for testing.  The right is the expected return value, already 
+   * decoded.  If a result is "undefined", the request's error handler will be 
+   * called.
+   */
+  var xhr = {
+    results: {
+      "get /wsapi/session_context valid": contextInfo,   
+      "get /wsapi/session_context invalid": contextInfo,
+      // We are going to test for XHR failures for session_context using 
+      // call to serverTime.  We are going to use the flag contextAjaxError
+      "get /wsapi/session_context ajaxError": contextInfo, 
+      "get /wsapi/session_context throttle": contextInfo, 
+      "get /wsapi/session_context contextAjaxError": undefined,  
+      "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },  
+      "get /wsapi/email_for_token?token=token invalid": { success: false },  
+      "post /wsapi/authenticate_user valid": { success: true },
+      "post /wsapi/authenticate_user invalid": { success: false },
+      "post /wsapi/authenticate_user ajaxError": undefined,
+      "post /wsapi/cert_key valid": random_cert,
+      "post /wsapi/cert_key invalid": undefined,
+      "post /wsapi/cert_key ajaxError": undefined,
+      "post /wsapi/complete_email_addition valid": { success: true },
+      "post /wsapi/complete_email_addition invalid": { success: false },
+      "post /wsapi/complete_email_addition ajaxError": undefined,
+      "post /wsapi/stage_user valid": { success: true },
+      "post /wsapi/stage_user invalid": { success: false },
+      "post /wsapi/stage_user throttle": 403,
+      "post /wsapi/stage_user ajaxError": undefined,
+      "get /wsapi/user_creation_status?email=registered%40testuser.com pending": { status: "pending" },
+      "get /wsapi/user_creation_status?email=registered%40testuser.com complete": { status: "complete" },
+      "get /wsapi/user_creation_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
+      "get /wsapi/user_creation_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
+      "get /wsapi/user_creation_status?email=registered%40testuser.com ajaxError": undefined,
+      "post /wsapi/complete_user_creation valid": { success: true },
+      "post /wsapi/complete_user_creation invalid": { success: false },
+      "post /wsapi/complete_user_creation ajaxError": undefined,
+      "post /wsapi/logout valid": { success: true },
+      "post /wsapi/logout ajaxError": undefined,
+      "get /wsapi/have_email?email=registered%40testuser.com valid": { email_known: true },
+      "get /wsapi/have_email?email=registered%40testuser.com throttle": { email_known: true },
+      "get /wsapi/have_email?email=registered%40testuser.com ajaxError": undefined,
+      "get /wsapi/have_email?email=unregistered%40testuser.com valid": { email_known: false },
+      "post /wsapi/remove_email valid": { success: true },
+      "post /wsapi/remove_email invalid": { success: false },
+      "post /wsapi/remove_email ajaxError": undefined,
+      "post /wsapi/account_cancel valid": { success: true },
+      "post /wsapi/account_cancel invalid": { success: false },
+      "post /wsapi/account_cancel ajaxError": undefined,
+      "post /wsapi/stage_email valid": { success: true },
+      "post /wsapi/stage_email invalid": { success: false },
+      "post /wsapi/stage_email throttle": 403,
+      "post /wsapi/stage_email ajaxError": undefined,
+      "post /wsapi/cert_key ajaxError": undefined,
+      "get /wsapi/email_addition_status?email=registered%40testuser.com pending": { status: "pending" },
+      "get /wsapi/email_addition_status?email=registered%40testuser.com complete": { status: "complete" },
+      "get /wsapi/email_addition_status?email=registered%40testuser.com mustAuth": { status: "mustAuth" },
+      "get /wsapi/email_addition_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
+      "get /wsapi/email_addition_status?email=registered%40testuser.com ajaxError": undefined,
+      "get /wsapi/list_emails valid": {"testuser@testuser.com":{}},
+      "get /wsapi/list_emails multiple": {"testuser@testuser.com":{}, "testuser2@testuser.com":{}},
+      "get /wsapi/list_emails no_identities": [],
+      "get /wsapi/list_emails ajaxError": undefined
+    },
+
+    setContextInfo: function(field, value) {
+      contextInfo[field] = value;
+    },
+
+    useResult: function(result) {
+      xhr.resultType = result;
+    },
+
+    getLastRequest: function() {
+      return this.req;
+    },
+
+    ajax: function(obj) {
+      //console.log("ajax request");
+      var type = obj.type ? obj.type.toLowerCase() : "get";
+
+      var req = this.req = {
+        type: type,
+        url: obj.url,
+        data: obj.data
+      };
+
+
+      if(type === "post" && !obj.data.csrf) {
+        ok(false, "missing csrf token on POST request");
+      }
+
+      var resName = req.type + " " + req.url + " " + xhr.resultType;
+      var result = xhr.results[resName];
+
+      var type = typeof result;
+      if(!(type == "number" || type == "undefined")) {
+        if(obj.success) {
+          obj.success(result);
+        }
+      }
+      else if (obj.error) {
+        // Invalid result - either invalid URL, invalid GET/POST or 
+        // invalid resultType
+        obj.error({ status: result || 400 }, "errorStatus", "errorThrown");
+      }
+    }
+  };
+
+
+  return xhr;
+}());
+
+
diff --git a/browserid/static/dialog/test/qunit/pages/add_email_address_test.js b/resources/static/dialog/test/qunit/pages/add_email_address_test.js
similarity index 63%
rename from browserid/static/dialog/test/qunit/pages/add_email_address_test.js
rename to resources/static/dialog/test/qunit/pages/add_email_address_test.js
index 1d0d0a016d7f9b44515d643b18d6f6f0c53672d5..d353a848f9b27f4357411ab19c8dd400339453ae 100644
--- a/browserid/static/dialog/test/qunit/pages/add_email_address_test.js
+++ b/resources/static/dialog/test/qunit/pages/add_email_address_test.js
@@ -34,36 +34,24 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/js/pages/add_email_address", function() {
+steal.plugins("jquery").then("/dialog/test/qunit/mocks/xhr", "/dialog/resources/network", "/js/pages/add_email_address", function() {
   "use strict";
 
   var bid = BrowserID,
       network = bid.Network,
       storage = bid.Storage,
-      emailForVerificationTokenFailure = false,
-      completeEmailRegistrationFailure = false,
+      xhr = bid.Mocks.xhr,
       validToken = true;
   
-  var netMock = {
-    emailForVerificationToken: function(token, onSuccess, onFailure) {
-      emailForVerificationTokenFailure ? onFailure() : onSuccess("testuser@testuser.com");
-    },
-
-    completeEmailRegistration: function(token, onSuccess, onFailure) {
-      completeEmailRegistrationFailure ? onFailure() : onSuccess(validToken);
-    }
-  };
-
   module("pages/add_email_address", {
     setup: function() {
-      BrowserID.User.setNetwork(netMock);  
-      emailForVerificationTokenFailure = completeEmailRegistrationFailure = false;
-      validToken = true;
+      network.setXHR(xhr);  
+      xhr.useResult("valid");
       $(".error").stop().hide();
       $(".website").text("");
     },
     teardown: function() {
-      BrowserID.User.setNetwork(network);  
+      network.setXHR($);  
       $(".error").stop().hide();
       $(".website").text("");
     }
@@ -74,39 +62,57 @@ steal.plugins("jquery").then("/js/pages/add_email_address", function() {
 
     bid.addEmailAddress("token");
     
-    equal($("#email").text(), "testuser@testuser.com", "email set");
-    ok($("#siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
-    equal($("#siteinfo .website").text(), "browserid.org", "origin is updated");
+    setTimeout(function() {
+      equal($("#email").text(), "testuser@testuser.com", "email set");
+      ok($("#siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
+      equal($("#siteinfo .website").text(), "browserid.org", "origin is updated");
+      start();
+    }, 500);
+    stop();
   });
 
   test("addEmailAddress with good token and nosite", function() {
     bid.addEmailAddress("token");
     
-    equal($("#email").text(), "testuser@testuser.com", "email set");
-    equal($("#siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
-    equal($("#siteinfo .website").text(), "", "origin is not updated");
+    setTimeout(function() {
+      equal($("#email").text(), "testuser@testuser.com", "email set");
+      equal($("#siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
+      equal($("#siteinfo .website").text(), "", "origin is not updated");
+      start();
+    }, 500);
+    stop();
   });
 
   test("addEmailAddress with bad token", function() {
-    validToken = false;
+    xhr.useResult("invalid");
 
     bid.addEmailAddress("token");
-    ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
+    setTimeout(function() {
+      ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
+      start();
+    }, 500);
+    stop();
   });
 
   test("addEmailAddress with emailForVerficationToken XHR failure", function() {
-    validToken = true;
-    emailForVerificationTokenFailure = true;
+    xhr.useResult("ajaxError");
     bid.addEmailAddress("token");
 
-    ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+    setTimeout(function() {
+      ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+      start();
+    }, 500);
+    stop();
   });
 
   test("addEmailAddress with completeEmailRegistration XHR failure", function() {
-    validToken = true;
-    completeEmailRegistrationFailure = true;
+    xhr.useResult("ajaxError");
     bid.addEmailAddress("token");
 
-    ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+    setTimeout(function() {
+      ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+      start();
+    }, 500);
+    stop();
   });
 });
diff --git a/resources/static/dialog/test/qunit/pages/forgot_unit_test.js b/resources/static/dialog/test/qunit/pages/forgot_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ea45fed4411b9c4f4e2bcf001bc2c1724ee4286e
--- /dev/null
+++ b/resources/static/dialog/test/qunit/pages/forgot_unit_test.js
@@ -0,0 +1,129 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/test/qunit/mocks/xhr", "/dialog/resources/network", "/dialog/resources/user", "/js/pages/forgot", function() {
+  "use strict";
+
+  var bid = BrowserID,
+      network = bid.Network,
+      user = bid.User,
+      xhr = bid.Mocks.xhr,
+      CHECK_DELAY = 500;
+
+  module("pages/forgot", {
+    setup: function() {
+      network.setXHR(xhr);
+      $(".error").stop().hide();
+      xhr.useResult("valid");
+      bid.forgot();
+    },
+    teardown: function() {
+      network.setXHR($);
+      $(".error").stop().hide();
+      $(".website").text("");
+      bid.forgot.reset();
+    }
+  });
+
+  test("requestPasswordReset with invalid email", function() {
+    $("#email").val("invalid");
+
+    xhr.useResult("invalid");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with known email", function() {
+    $("#email").val("registered@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      ok($(".emailsent").is(":visible"), "email sent successfully");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with unknown email", function() {
+    $("#email").val("unregistered@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with throttling", function() {
+    xhr.useResult("throttle");
+
+    $("#email").val("throttled@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+  });
+
+  test("requestPasswordReset with XHR Error", function() {
+    xhr.useResult("ajaxError");
+
+    $("#email").val("testuser@testuser.com");
+    bid.forgot.submit();
+
+    setTimeout(function() {
+      equal($(".emailsent").is(":visible"), false, "email not sent");
+      equal($(".doh").is(":visible"), true, "XHR error message is displayed");
+      start();
+    }, CHECK_DELAY);
+
+    stop();
+
+  });
+
+});
diff --git a/resources/static/dialog/test/qunit/qunit.js b/resources/static/dialog/test/qunit/qunit.js
new file mode 100644
index 0000000000000000000000000000000000000000..414860785aafa74b1a0bd39c40630e57ed9fb2d4
--- /dev/null
+++ b/resources/static/dialog/test/qunit/qunit.js
@@ -0,0 +1,39 @@
+steal("/dialog/resources/browserid.js",
+      "/dialog/resources/browser-support.js",
+      "/dialog/resources/error-messages.js",
+      "/dialog/resources/storage.js",
+      "/dialog/resources/tooltip.js",
+      "/dialog/resources/validation.js",
+      "/dialog/resources/underscore-min.js",
+      "/dialog/test/qunit/mocks/mocks.js",
+      "/dialog/test/qunit/mocks/xhr.js")
+  .plugins(
+    "jquery", 
+    "jquery/controller",
+    "jquery/controller/subscribe",
+    "jquery/controller/view",
+    "jquery/view/ejs",
+    "funcunit/qunit")
+	.views('testBodyTemplate.ejs',
+         'wait.ejs',
+         'pickemail.ejs',
+         'offline.ejs',
+         'error.ejs')
+  .then("js/browserid_unit_test")
+  .then("js/page_helpers_unit_test")
+  .then("include_unit_test")
+  .then("relay/relay_unit_test")
+  .then("pages/add_email_address_test")
+  .then("pages/forgot_unit_test")
+  .then("resources/tooltip_unit_test")
+  .then("resources/channel_unit_test")
+  .then("resources/browser-support_unit_test")
+  .then("resources/validation_unit_test")
+  .then("resources/storage_unit_test")
+  .then("resources/network_unit_test")
+  .then("resources/user_unit_test")
+  .then("controllers/page_controller_unit_test")
+  .then("controllers/pickemail_controller_unit_test")
+  .then("controllers/dialog_controller_unit_test")
+  .then("controllers/authenticate_controller_unit_test")
+
diff --git a/resources/static/dialog/test/qunit/relay/relay_unit_test.js b/resources/static/dialog/test/qunit/relay/relay_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae7e6dc6109e84fd1d7e6957d0e208a14218212f
--- /dev/null
+++ b/resources/static/dialog/test/qunit/relay/relay_unit_test.js
@@ -0,0 +1,183 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", function() {
+  "use strict";
+
+  var winMock = {},
+      relay = BrowserID.Relay;
+
+  var channelMock = {
+    build: function(options) {
+      this.options = options;
+
+      channelMock.bindMessage = channelMock.cb = channelMock.status = 
+        channelMock.errorCode = channelMock.verboseError = undefined;
+
+      return {
+        bind: function(message, cb) {
+          channelMock.bindMessage = message;
+          channelMock.cb = cb;
+        }
+      }
+    },
+
+    // Mock in the receiving of the RPC call from the RP.
+    receiveGetVerifiedEmail: function() {
+      // cb is the mock callback that is passed to Channel.bind
+      channelMock.cb({
+        origin: "Origin", 
+        delayReturn: function() {},
+        complete: function(status) {
+          channelMock.status = status;
+        },
+        error: function(code, verboseMessage) {
+          channelMock.errorCode = code;
+          channelMock.verboseError = verboseMessage;
+        }
+      });
+    }
+
+  };
+
+
+  module("relay/relay", {
+    setup: function() {
+      relay.init({
+        window: winMock,
+        channel: channelMock
+      });
+    },
+    teardown: function() {
+      relay.init({
+        window: window.parent,
+        channel: Channel 
+      });
+    }
+  });
+
+  test("Can open the relay, happy case", function() {
+    relay.open(); 
+
+    /**
+     * Check to make sure channel build is correct
+     */
+    equal(channelMock.options.window, winMock, "opening to the correct window");
+    equal(channelMock.options.origin, "*", "accept messages from anybody");
+    equal(channelMock.options.scope, "mozid", "mozid namespace");
+
+    /**
+     * Check to make sure the correct message is bound
+     */
+    equal(channelMock.bindMessage, "getVerifiedEmail", "bound to getVerifiedEmail");
+  });
+
+  test("channel.getVerifiedEmail before registerDialog", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      equal(origin, "Origin", "Origin set correctly");
+      equal(typeof completeCB, "function", "A completion callback is specified");
+
+      start();
+    });
+
+    stop();
+  });
+
+  test("registerDialog before channel.getVerifiedEmail", function() {
+    relay.open();
+
+    relay.registerClient(function(origin, completeCB) {
+      equal(origin, "Origin", "Origin set correctly");
+      equal(typeof completeCB, "function", "A completion callback is specified");
+
+      start();
+    });
+
+    channelMock.receiveGetVerifiedEmail();
+
+    stop();
+  });
+
+  test("calling the completeCB with assertion", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB("assertion", null);
+      equal(channelMock.status, "assertion", "channel gets the correct assertion");
+      start();
+    });
+
+    stop();
+  });
+
+
+  test("calling the completeCB with null assertion", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB(null, null);
+      strictEqual(channelMock.status, null, "channel gets the null assertion");
+      start();
+    });
+
+    stop();
+  });
+
+  test("calling the completeCB with error", function() {
+    relay.open();
+
+    channelMock.receiveGetVerifiedEmail();
+
+    relay.registerClient(function(origin, completeCB) {
+      completeCB(null, "canceled");
+
+      equal(channelMock.errorCode, "canceled", "error callback called with error code");
+      ok(channelMock.verboseError, "verbose error code set");
+
+      start();
+    });
+
+    stop();
+  });
+});
diff --git a/resources/static/dialog/test/qunit/resources/browser-support_unit_test.js b/resources/static/dialog/test/qunit/resources/browser-support_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..a26857f01eef422ad03c0cc49fbb4d9257957cef
--- /dev/null
+++ b/resources/static/dialog/test/qunit/resources/browser-support_unit_test.js
@@ -0,0 +1,102 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      support = bid.BrowserSupport,
+      stubWindow,
+      stubNavigator;
+
+  module("browser-support", {
+    setup: function() {
+      // Hard coded goodness for testing purposes
+      stubNavigator = {
+        appName: "Netscape",
+        userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1"
+      };
+
+      stubWindow = {
+        localStorage: {},
+        postMessage: function() {}
+      };
+
+      support.setTestEnv(stubNavigator, stubWindow);
+    },
+
+    teardown: function() {
+    }
+  });
+  
+  test("browser without localStorage", function() {
+    delete stubWindow.localStorage;
+
+    equal(support.isSupported(), false, "window.localStorage is required");
+    equal(support.getNoSupportReason(), "LOCALSTORAGE", "correct reason");
+  });
+
+
+  test("browser without postMessage", function() {
+    delete stubWindow.postMessage;
+
+    equal(support.isSupported(), false, "window.postMessage is required");
+    equal(support.getNoSupportReason(), "POSTMESSAGE", "correct reason");
+  });
+
+  test("Fake being IE8 - unsupported intentionally", function() {
+    stubNavigator.appName = "Microsoft Internet Explorer";
+    stubNavigator.userAgent = "MSIE 8.0";
+
+    equal(support.isSupported(), false, "IE8 is not supported");
+    equal(support.getNoSupportReason(), "IE_VERSION", "correct reason");
+  });
+
+  test("Fake being IE9 - supported", function() {
+    stubNavigator.appName = "Microsoft Internet Explorer";
+    stubNavigator.userAgent = "MSIE 9.0";
+
+    equal(support.isSupported(), true, "IE9 is supported");
+    equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good");
+  });
+
+  test("Firefox 7.01 with postMessage, localStorage", function() {
+    equal(support.isSupported(), true, "Firefox 7.01 is supported");
+    equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good");
+  });
+});
+
+
diff --git a/resources/static/dialog/test/qunit/resources/channel_unit_test.js b/resources/static/dialog/test/qunit/resources/channel_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd9621638a5b131270a3296f3455fd7b18fa7f1b
--- /dev/null
+++ b/resources/static/dialog/test/qunit/resources/channel_unit_test.js
@@ -0,0 +1,162 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", function() {
+  var channel = BrowserID.Channel;
+
+  var navMock = {
+    id: {},
+  };
+
+  // Mock in the window object as well as the frame relay
+  var winMock = {
+    location: {
+      href: "browserid.org/sign_in#1234"
+    },
+    opener: {
+      frames: {
+        browserid_relay_1234: {
+          BrowserID: {
+            Relay: {
+              registerClient: function(callback) {
+                // mock of the registerClient function in the BrowserID.Channel.
+                callback("origin", function onComplete(success, error) {
+                  winMock.success = success;
+                  winMock.error = error;
+                });
+              }
+            }
+          }
+        }
+      }
+    }
+  };
+
+  module("resources/channel", {
+    setup: function() {
+    },
+
+    teardown: function() {
+      channel.init({
+        window: window,
+        navigator: navigator
+      });
+    }
+  });
+
+  test("window.setupChannel exists for legacy uses", function() {
+    ok(typeof window.setupChannel, "function", "window.setupChannel exists for legacy uses");
+  });
+  
+  test("IFRAME channel with assertion", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onsuccess("assertion");
+        equal(winMock.success, "assertion", "assertion made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+  test("IFRAME channel with null assertion", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onsuccess(null);
+        strictEqual(winMock.success, null, "null assertion made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+  test("IFRAME channel relaying error", function() {
+    channel.init({
+      window: winMock,
+      navigator: navMock
+    });
+
+    channel.open({
+      getVerifiedEmail: function(origin, onsuccess, onerror) {
+        onerror("error");
+        strictEqual(winMock.error, "error", "error made it to the relay");
+        start();
+      }
+    });
+
+    stop();
+  });
+
+  test("IFRAME channel with error on open", function() {
+    var winMockWithoutRelay = $.extend(true, {}, winMock);
+    delete winMockWithoutRelay.opener.frames.browserid_relay_1234; 
+
+    channel.init({
+      window: winMockWithoutRelay,
+      navigator: navMock
+    });
+
+    // Do this manually so we can test if getVerifiedEmail gets called.
+    try {
+      channel.open({
+        getVerifiedEmail: function(origin, onsuccess, onerror) {
+          ok(false, "getVerifiedEmail should never be called on channel error");
+          start();
+        }
+      });
+    } catch(e) {
+      equal(e.toString(), "relay frame not found", "exception caught when trying to open channel that does not exist");
+      start();
+    }
+
+    stop();
+  });
+
+});
+
diff --git a/browserid/static/dialog/test/qunit/resources/network_unit_test.js b/resources/static/dialog/test/qunit/resources/network_unit_test.js
similarity index 77%
rename from browserid/static/dialog/test/qunit/resources/network_unit_test.js
rename to resources/static/dialog/test/qunit/resources/network_unit_test.js
index ac5b06c785ffba87efe2ea7f3d2b3404aefb6b16..25b95e2fca5adb2dad7aed5a001dbda30d96779a 100644
--- a/browserid/static/dialog/test/qunit/resources/network_unit_test.js
+++ b/resources/static/dialog/test/qunit/resources/network_unit_test.js
@@ -34,10 +34,11 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", function() {
+steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", "/dialog/test/qunit/mocks/xhr", function() {
   "use strict";
 
-  var testName;
+  var testName,
+  xhr = BrowserID.Mocks.xhr;
 
   function wrappedAsyncTest(name, test) {
     asyncTest(name, function() {
@@ -61,8 +62,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
     var handle;
 
-    var subscriber = function() {
+    var subscriber = function(message, info) {
       ok(true, "xhr error notified application");
+      ok(info.network.url, "url is in network info");
+      ok(info.network.type, "request type is in network info");
+      equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
+      equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
       wrappedStart();
       OpenAjax.hub.unsubscribe(handle);
     };
@@ -85,8 +90,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     args.push(function onSuccess(authenticated) {
       ok(false, "XHR failure should never pass");
       wrappedStart();
-    }, function onFailure() {
+    }, function onFailure(info) {
       ok(true, "XHR failure should never pass");
+      ok(info.network.url, "url is in network info");
+      ok(info.network.type, "request type is in network info");
+      equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
+      equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
       wrappedStart();
     });
 
@@ -97,105 +106,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   }
 
-  var network = BrowserID.Network,
-      contextInfo = {
-        server_time: new Date().getTime(),
-        csrf_token: "csrf",
-        authenticated: false
-      };
-
-
-  /**
-   * This is the results table, the keys are the request type, url, and 
-   * a "selector" for testing.  The right is the expected return value, already 
-   * decoded.  If a result is "undefined", the request's error handler will be 
-   * called.
-   */
-  var xhr = {
-    results: {
-      "get /wsapi/session_context valid": contextInfo,   
-      "get /wsapi/session_context invalid": contextInfo,
-      // We are going to test for XHR failures for session_context using 
-      // call to serverTime.  We are going to use the flag contextAjaxError
-      "get /wsapi/session_context ajaxError": contextInfo, 
-      "get /wsapi/session_context contextAjaxError": undefined,  
-      "post /wsapi/authenticate_user valid": { success: true },
-      "post /wsapi/authenticate_user invalid": { success: false },
-      "post /wsapi/authenticate_user ajaxError": undefined,
-      "post /wsapi/complete_email_addition valid": { success: true },
-      "post /wsapi/complete_email_addition invalid": { success: false },
-      "post /wsapi/complete_email_addition ajaxError": undefined,
-      "post /wsapi/stage_user valid": { success: true },
-      "post /wsapi/stage_user invalid": { success: false },
-      "post /wsapi/stage_user ajaxError": undefined,
-      "get /wsapi/user_creation_status?email=address notcreated": undefined, // undefined because server returns 400 error
-      "get /wsapi/user_creation_status?email=address pending": { status: "pending" },
-      "get /wsapi/user_creation_status?email=address complete": { status: "complete" },
-      "get /wsapi/user_creation_status?email=address ajaxError": undefined,
-      "post /wsapi/complete_user_creation valid": { success: true },
-      "post /wsapi/complete_user_creation invalid": { success: false },
-      "post /wsapi/complete_user_creation ajaxError": undefined,
-      "post /wsapi/logout valid": { success: true },
-      "post /wsapi/logout ajaxError": undefined,
-      "get /wsapi/have_email?email=address taken": { email_known: true },
-      "get /wsapi/have_email?email=address nottaken" : { email_known: false },
-      "get /wsapi/have_email?email=address ajaxError" : undefined,
-      "post /wsapi/remove_email valid": { success: true },
-      "post /wsapi/remove_email invalid": { success: false },
-      "post /wsapi/remove_email ajaxError": undefined,
-      "post /wsapi/account_cancel valid": { success: true },
-      "post /wsapi/account_cancel invalid": { success: false },
-      "post /wsapi/account_cancel ajaxError": undefined,
-      "post /wsapi/stage_email valid": { success: true },
-      "post /wsapi/stage_email invalid": { success: false },
-      "post /wsapi/stage_email ajaxError": undefined,
-      "get /wsapi/email_addition_status?email=address notcreated": undefined, // undefined because server returns 400 error
-      "get /wsapi/email_addition_status?email=address pending": { status: "pending" },
-      "get /wsapi/email_addition_status?email=address complete": { status: "complete" },
-      "get /wsapi/email_addition_status?email=address ajaxError": undefined
-    },
-
-    useResult: function(result) {
-      xhr.resultType = result;
-    },
-
-    getLastRequest: function() {
-      return this.req;
-    },
-
-    ajax: function(obj) {
-      //console.log("ajax request");
-      var type = obj.type ? obj.type.toLowerCase() : "get";
-
-      var req = this.req = {
-        type: type,
-        url: obj.url,
-        data: obj.data
-      };
-
-
-      if(type === "post" && !obj.data.csrf) {
-        ok(false, "missing csrf token on POST request");
-      }
-
-      var resName = req.type + " " + req.url + " " + xhr.resultType;
-      var result = xhr.results[resName];
-
-      if(result) {
-        if(obj.success) {
-          obj.success(result);
-        }
-      }
-      else if (obj.error) {
-        // Invalid result - either invalid URL, invalid GET/POST or 
-        // invalid resultType
-        obj.error();
-      }
-    }
-  }
+  var network = BrowserID.Network;
 
-
-  module("network", {
+  module("/resources/network", {
     setup: function() {
       network.setXHR(xhr);
       xhr.useResult("valid");
@@ -241,7 +154,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
 
   wrappedAsyncTest("checkAuth with valid authentication", function() {
-    contextInfo.authenticated = true;
+    xhr.setContextInfo("authenticated", true);
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, true, "we have an authentication");
       wrappedStart();
@@ -255,7 +168,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("checkAuth with invalid authentication", function() {
     xhr.useResult("invalid");
-    contextInfo.authenticated = false;
+    xhr.setContextInfo("authenticated", false);
 
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, false, "we are not authenticated");
@@ -272,7 +185,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("checkAuth with XHR failure", function() {
     xhr.useResult("ajaxError");
-    contextInfo.authenticated = false;
+    xhr.setContextInfo("authenticated", false);
 
     // Do not convert this to failureCheck, we do this manually because 
     // checkAuth does not make an XHR request.  Since it does not make an XHR 
@@ -365,6 +278,20 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("createUser throttled", function() {
+    xhr.useResult("throttle");
+
+    network.createUser("validuser", "origin", function onSuccess(added) {
+      equal(added, false, "throttled email returns onSuccess but with false as the value");
+      wrappedStart();
+    }, function onFailure() {
+      ok(false);
+      wrappedStart();
+    });
+
+    stop();
+  });
+
   wrappedAsyncTest("createUser with XHR failure", function() {
     notificationCheck(network.createUser, "validuser", "origin");
   });
@@ -376,7 +303,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   wrappedAsyncTest("checkUserRegistration with pending email", function() {
     xhr.useResult("pending");
 
-    network.checkUserRegistration("address", function(status) {
+    network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
       wrappedStart();
     }, function onFailure() {
@@ -390,7 +317,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   wrappedAsyncTest("checkUserRegistration with complete email", function() {
     xhr.useResult("complete");
 
-    network.checkUserRegistration("address", function(status) {
+    network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
       wrappedStart();
     }, function onFailure() {
@@ -402,11 +329,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
   wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
-    notificationCheck(network.checkUserRegistration, "address");
+    notificationCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
   wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
-    failureCheck(network.checkUserRegistration, "address");
+    failureCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
   wrappedAsyncTest("completeUserRegistration with valid token", function() {
@@ -479,9 +406,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
   wrappedAsyncTest("emailRegistered with taken email", function() {
-    xhr.useResult("taken");
-
-    network.emailRegistered("address", function(taken) {
+    network.emailRegistered("registered@testuser.com", function(taken) {
       equal(taken, true, "a taken email is marked taken");
       wrappedStart();
     }, function onFailure() {
@@ -493,9 +418,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
   wrappedAsyncTest("emailRegistered with nottaken email", function() {
-    xhr.useResult("nottaken");
-
-    network.emailRegistered("address", function(taken) {
+    network.emailRegistered("unregistered@testuser.com", function(taken) {
       equal(taken, false, "a not taken email is not marked taken");
       wrappedStart();
     }, function onFailure() {
@@ -507,11 +430,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
   wrappedAsyncTest("emailRegistered with XHR failure", function() {
-    notificationCheck(network.emailRegistered, "address");
+    notificationCheck(network.emailRegistered, "registered@testuser.com");
   });
 
   wrappedAsyncTest("emailRegistered with XHR failure", function() {
-    failureCheck(network.emailRegistered, "address");
+    failureCheck(network.emailRegistered, "registered@testuser.com");
   });
 
 
@@ -540,6 +463,20 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("addEmail throttled", function() {
+    xhr.useResult("throttle");
+
+    network.addEmail("address", "origin", function onSuccess(added) {
+      equal(added, false, "throttled email returns onSuccess but with false as the value");
+      wrappedStart();
+    }, function onFailure() {
+      ok(false);
+      wrappedStart();
+    });
+
+    stop();
+  });
+
   wrappedAsyncTest("addEmail with XHR failure", function() {
     notificationCheck(network.addEmail, "address", "origin");
   });
@@ -551,7 +488,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   wrappedAsyncTest("checkEmailRegistration pending", function() {
     xhr.useResult("pending");
 
-    network.checkEmailRegistration("address", function(status) {
+    network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
       wrappedStart();
     }, function onFailure() {
@@ -565,7 +502,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   wrappedAsyncTest("checkEmailRegistration complete", function() {
     xhr.useResult("complete");
 
-    network.checkEmailRegistration("address", function(status) {
+    network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
       wrappedStart();
     }, function onFailure() {
@@ -706,7 +643,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("serverTime", function() {
     // I am forcing the server time to be 1.25 seconds off.
-    contextInfo.server_time = new Date().getTime() - 1250;
+    xhr.setContextInfo("server_time", new Date().getTime() - 1250);
     network.serverTime(function onSuccess(time) {
       var diff = Math.abs((new Date()) - time);
       equal(1245 < diff && diff < 1255, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)");
diff --git a/browserid/static/dialog/test/qunit/resources/storage_unit_test.js b/resources/static/dialog/test/qunit/resources/storage_unit_test.js
similarity index 100%
rename from browserid/static/dialog/test/qunit/resources/storage_unit_test.js
rename to resources/static/dialog/test/qunit/resources/storage_unit_test.js
diff --git a/browserid/run.js b/resources/static/dialog/test/qunit/resources/tooltip_unit_test.js
old mode 100755
new mode 100644
similarity index 57%
rename from browserid/run.js
rename to resources/static/dialog/test/qunit/resources/tooltip_unit_test.js
index 761fc95d6ef8c533bbe7c814bdb90aeb5935b537..85dae3a28f8e2ca87b09a4ebe4a1248cf49024ad
--- a/browserid/run.js
+++ b/resources/static/dialog/test/qunit/resources/tooltip_unit_test.js
@@ -1,5 +1,5 @@
-#!/usr/bin/env node
-
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */                                             
+/*globals BrowserID: true, _:true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -13,7 +13,7 @@
  * for the specific language governing rights and limitations under the
  * License.
  *
- * The Original Code is Mozilla BrowserID.
+ * The Original Code is Mozilla bid.
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2011
@@ -34,44 +34,47 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/tooltip", function() {
+  "use strict";
 
-var  path = require("path"),
-fs = require("fs"),
-express = require("express"),
-logger = require("../libs/logging.js").logger;
+  var bid = BrowserID,
+      tooltip = bid.Tooltip
 
-const amMain = (process.argv[1] === __filename);
+  module("/resources/tooltip", {
+    setup: function() {
+    },
+    teardown: function() {
+    }
+  });
 
-const PRIMARY_HOST = "127.0.0.1";
-const PRIMARY_PORT = 62700;
 
-var handler = require("./app.js");
+  test("show short tooltip, min of 2.5 seconds", function() {
+    var startTime = new Date().getTime();
 
-var app = undefined;
+    tooltip.showTooltip("#shortTooltip", function() {
+      console.log("calling tooltip back");
+      var endTime = new Date().getTime();
+      var diff = endTime - startTime;
+      ok(2000 <= diff && diff <= 3000, diff + " - minimum of 2 seconds, max of 3 seconds");
 
-exports.runServer = function() {
-  if (app) return;
+      start();
+    });
 
-  app = express.createServer();
+    stop();
+  });
 
-  // let the specific server interact directly with the connect server to register their middleware
-  if (handler.setup) handler.setup(app);
+  test("show long tooltip, takes about 5 seconds", function() {
+    var startTime = new Date().getTime();
 
-  // use the express 'static' middleware for serving of static files (cache headers, HTTP range, etc)
-  app.use(express.static(path.join(__dirname, "static")));
+    tooltip.showTooltip("#longTooltip", function() {
+      var endTime = new Date().getTime();
+      var diff = endTime - startTime;
+      ok(diff >= 4500, diff + " - longer tooltip is on the screen for a bit longer");
 
-  app.listen(PRIMARY_PORT, PRIMARY_HOST);
-};
+      start();
+    });
 
-exports.stopServer = function(cb) {
-  if (!app) return;
-  app.on('close', function() {
-    cb();
+    stop();
   });
-  app.close();
-  app = undefined;
-}
-
-// when directly invoked from the command line, we'll start the server
-if (amMain) exports.runServer();
 
+});
diff --git a/browserid/static/dialog/test/qunit/resources/user_unit_test.js b/resources/static/dialog/test/qunit/resources/user_unit_test.js
similarity index 75%
rename from browserid/static/dialog/test/qunit/resources/user_unit_test.js
rename to resources/static/dialog/test/qunit/resources/user_unit_test.js
index c03c3610bb1d6fd60571c04de664999005801071..387ea689d28d4e43806f2e30a354e8b65d39c021 100644
--- a/browserid/static/dialog/test/qunit/resources/user_unit_test.js
+++ b/resources/static/dialog/test/qunit/resources/user_unit_test.js
@@ -40,132 +40,16 @@ var jwcert = require("./jwcert");
 
 steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", function() {
   var lib = BrowserID.User,
-      network = BrowserID.Network,
       storage = BrowserID.Storage,
+      xhr = BrowserID.Mocks.xhr,
       testOrigin = "testOrigin";
 
   // I generated these locally, they are used nowhere else.
   var pubkey = {"algorithm":"RS","n":"56063028070432982322087418176876748072035482898334811368408525596198252519267108132604198004792849077868951906170812540713982954653810539949384712773390200791949565903439521424909576832418890819204354729217207360105906039023299561374098942789996780102073071760852841068989860403431737480182725853899733706069","e":"65537"};
 
-  var privkey = {"algorithm":"RS","n":"56063028070432982322087418176876748072035482898334811368408525596198252519267108132604198004792849077868951906170812540713982954653810539949384712773390200791949565903439521424909576832418890819204354729217207360105906039023299561374098942789996780102073071760852841068989860403431737480182725853899733706069","e":"65537","d":"786150156350274055174913976906933968265264030754683486390396799104417261473770120296370873955240982995278496143719986037141619777024457729427415826765728988003471373990098269492312035966334999128083733012526716409629032119935282516842904344253703738413658199885458117908331858717294515041118355034573371553"};
-
   // this cert is meaningless, but it has the right format
   var random_cert = "eyJhbGciOiJSUzEyOCJ9.eyJpc3MiOiJpc3N1ZXIuY29tIiwiZXhwIjoxMzE2Njk1MzY3NzA3LCJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU2MDYzMDI4MDcwNDMyOTgyMzIyMDg3NDE4MTc2ODc2NzQ4MDcyMDM1NDgyODk4MzM0ODExMzY4NDA4NTI1NTk2MTk4MjUyNTE5MjY3MTA4MTMyNjA0MTk4MDA0NzkyODQ5MDc3ODY4OTUxOTA2MTcwODEyNTQwNzEzOTgyOTU0NjUzODEwNTM5OTQ5Mzg0NzEyNzczMzkwMjAwNzkxOTQ5NTY1OTAzNDM5NTIxNDI0OTA5NTc2ODMyNDE4ODkwODE5MjA0MzU0NzI5MjE3MjA3MzYwMTA1OTA2MDM5MDIzMjk5NTYxMzc0MDk4OTQyNzg5OTk2NzgwMTAyMDczMDcxNzYwODUyODQxMDY4OTg5ODYwNDAzNDMxNzM3NDgwMTgyNzI1ODUzODk5NzMzNzA2MDY5IiwiZSI6IjY1NTM3In0sInByaW5jaXBhbCI6eyJlbWFpbCI6InRlc3R1c2VyQHRlc3R1c2VyLmNvbSJ9fQ.aVIO470S_DkcaddQgFUXciGwq2F_MTdYOJtVnEYShni7I6mqBwK3fkdWShPEgLFWUSlVUtcy61FkDnq2G-6ikSx1fUZY7iBeSCOKYlh6Kj9v43JX-uhctRSB2pI17g09EUtvmb845EHUJuoowdBLmLa4DSTdZE-h4xUQ9MsY7Ik";
 
-  var credentialsValid, unknownEmails, keyRefresh, syncValid, userEmails, 
-      userCheckCount = 0,
-      emailCheckCount = 0,
-      registrationResponse,
-      xhrFailure = false,
-      validToken = true; 
-
-  var netStub = {
-    reset: function() {
-      credentialsValid = syncValid = true;
-      unknownEmails = [];
-      keyRefresh = [];
-      userEmails = {"testuser@testuser.com": {}};
-      registrationResponse = "complete";
-      xhrFailure = false;
-    },
-
-    checkUserRegistration: function(email, onSuccess, onFailure) {
-      userCheckCount++;
-      var status = userCheckCount === 2 ? registrationResponse : "pending";
-
-      xhrFailure ? onFailure() : onSuccess(status);
-    },
-
-    authenticate: function(email, password, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(credentialsValid);
-    },
-
-    checkAuth: function(onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(credentialsValid);
-    },
-
-    emailRegistered: function(email, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(email === "registered");
-    },
-
-    addEmail: function(email, origin, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(true);
-    },
-
-    checkEmailRegistration: function(email, onSuccess, onFailure) {
-      emailCheckCount++;
-      var status = emailCheckCount === 2 ? registrationResponse : "pending";
-
-      xhrFailure ? onFailure() : onSuccess(status);
-    },
-
-    emailForVerificationToken: function(token, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess("testuser@testuser.com");
-    },
-
-    completeEmailRegistration: function(token, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(validToken);
-    },
-
-    removeEmail: function(email, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess();
-    },
-
-    listEmails: function(onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(userEmails);
-    },
-
-    certKey: function(email, pubkey, onSuccess, onFailure) {
-      if (syncValid) {
-        xhrFailure ? onFailure() : onSuccess(random_cert);
-      }
-      else {
-        onFailure();
-      }
-    },
-    
-    syncEmails: function(issued_identities, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess({
-        unknown_emails: unknownEmails,
-        key_refresh: keyRefresh
-      });
-    },
-
-    setKey: function(email, keypair, onSuccess, onFailure) {
-      if (syncValid) {
-        xhrFailure ? onFailure() : onSuccess();
-      }
-      else {
-        onFailure();
-      }
-    },
-
-    createUser: function(email, origin, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(true);
-    },
-
-    setPassword: function(password, onSuccess) {
-      xhrFailure ? onFailure() : onSuccess();
-    },
-
-    requestPasswordReset: function(email, origin, onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(true);
-    },
-
-    cancelUser: function(onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess();
-    },
-
-    serverTime: function(onSuccess, onFailure) {
-      xhrFailure ? onFailure() : onSuccess(new Date());
-    },
-
-    logout: function(onSuccess, onFailure) {
-      credentialsValid = false;
-      xhrFailure ? onFailure() : onSuccess();
-    }
-  };
-
 
   function testAssertion(assertion) {
     equal(typeof assertion, "string", "An assertion was correctly generated");
@@ -202,17 +86,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     */
   }
 
-  module("user", {
+  module("resources/user", {
     setup: function() {
-      lib.setNetwork(netStub);
+      BrowserID.Network.setXHR(xhr);
+      xhr.useResult("valid");
       lib.clearStoredEmailKeypairs();
-      netStub.reset();
-      userCheckCount = 0;
-      emailCheckCount = 0;
-      validToken = true;
     },
     teardown: function() {
-      lib.setNetwork(BrowserID.Network);
+      BrowserID.Network.setXHR($);
     }
   });
 
@@ -280,8 +161,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("createUser with user creation refused", function() {
+    xhr.useResult("throttle");
+
+    lib.createUser("testuser@testuser.com", function(status) {
+      equal(status, false, "user creation refused");
+      start();
+    }, failure("createUser failure"));
+
+    stop();
+  });
+
   test("createUser with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
 
     lib.createUser("testuser@testuser.com", function(status) {
       ok(false, "xhr failure should never succeed");
@@ -294,18 +186,15 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
-  /**
-   * The next three tests use the mock network harness.  The tests are testing 
-   * the polling action and whether `waitForUserValidation` reacts as expected
-   * to the various network responses.  The network harness simulates multiple 
-   * calls to `checkUserRegistration`, attempting to simulate real use 
-   * interaction to verify the email address, the first call to 
-   * `checkUserRegistration` returns `pending`, the second returns the value 
-   * stored in `registrationResponse`.
-   */
   test("waitForUserValidation with `complete` response", function() {
-    lib.waitForUserValidation("testuser@testuser.com", function(status) {
+    storage.setStagedOnBehalfOf(testOrigin);
+
+    xhr.useResult("complete");
+
+    lib.waitForUserValidation("registered@testuser.com", function(status) {
       equal(status, "complete", "complete response expected");
+
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       start();
     }, failure("waitForUserValidation failure"));
 
@@ -313,10 +202,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForUserValidation with `mustAuth` response", function() {
-    registrationResponse = "mustAuth";
+    storage.setStagedOnBehalfOf(testOrigin);
+
+    xhr.useResult("mustAuth");
 
-    lib.waitForUserValidation("testuser@testuser.com", function(status) {
+    lib.waitForUserValidation("registered@testuser.com", function(status) {
       equal(status, "mustAuth", "mustAuth response expected");
+
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       start();
     }, failure("waitForUserValidation failure"));
 
@@ -324,12 +217,15 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForUserValidation with `noRegistration` response", function() {
-    registrationResponse = "noRegistration";
+    xhr.useResult("noRegistration");
 
-    lib.waitForUserValidation("baduser@testuser.com", function(status) {
+    storage.setStagedOnBehalfOf(testOrigin);
+    lib.waitForUserValidation("registered@testuser.com", function(status) {
       ok(false, "not expecting success")
+
       start();
     }, function(status) {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       ok(status, "noRegistration", "noRegistration response causes failure");
       start();
     });
@@ -338,12 +234,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForUserValidation with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
 
-    lib.waitForUserValidation("baduser@testuser.com", function(status) {
+    storage.setStagedOnBehalfOf(testOrigin);
+    lib.waitForUserValidation("registered@testuser.com", function(status) {
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared on XHR failure");
       ok(true, "xhr failure should always be a failure"); 
       start();
     });
@@ -351,7 +249,51 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("verifyUser with a good token", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      
+      ok(info.valid, "token was valid");
+      equal(info.email, "testuser@testuser.com", "email part of info");
+      equal(info.origin, testOrigin, "origin in info");
+      equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed");
+
+      start();
+    }, failure("verifyUser failure"));
+
+    stop();
+  });
+
+  test("verifyUser with a bad token", function() {
+    xhr.useResult("invalid");
 
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      
+      equal(info.valid, false, "bad token calls onSuccess with a false validity");
+
+      start();
+    }, failure("verifyUser failure"));
+
+    stop();
+
+  });
+
+  test("verifyUser with an XHR failure", function() {
+    xhr.useResult("ajaxError");
+
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+      
+    stop();
+  });
+
+  /*
   test("setPassword", function() {
     lib.setPassword("password", function() {
       // XXX fill this in.
@@ -361,11 +303,53 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
     stop();
   });
+*/
+  test("requestPasswordReset with known email", function() {
+    lib.requestPasswordReset("registered@testuser.com", function(status) {
+      equal(status.success, true, "password reset for known user");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
 
-  test("requestPasswordReset", function() {
-    lib.requestPasswordReset("address", function(reset) {
-      // XXX fill this in.
-      ok(true);
+  test("requestPasswordReset with unknown email", function() {
+    lib.requestPasswordReset("unregistered@testuser.com", function(status) {
+      equal(status.success, false, "password not reset for unknown user");
+      equal(status.reason, "invalid_user", "invalid_user is the reason");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("requestPasswordReset with throttle", function() {
+    xhr.useResult("throttle");
+    lib.requestPasswordReset("registered@testuser.com", function(status) {
+      equal(status.success, false, "password not reset for throttle");
+      equal(status.reason, "throttle", "password reset was throttled");
+      start();
+    }, function() {
+      ok(false, "onFailure should not be called"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("requestPasswordReset with XHR failure", function() {
+    xhr.useResult("ajaxError");
+    lib.requestPasswordReset("registered@testuser.com", function(status) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
       start();
     });
 
@@ -385,7 +369,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("authenticate with invalid credentials", function() {
-    credentialsValid = false;
+    xhr.useResult("invalid");
     lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) {
       equal(false, authenticated, "invalid authentication.");
       start();
@@ -397,7 +381,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("authenticate with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) {
       ok(false, "xhr failure should never succeed");
       start();
@@ -412,7 +396,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthentication with valid authentication", function() {
-    credentialsValid = true;
+    xhr.setContextInfo("authenticated", true);
     lib.checkAuthentication(function(authenticated) {
       equal(authenticated, true, "We are authenticated!");
       start();
@@ -424,7 +408,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthentication with invalid authentication", function() {
-    credentialsValid = false;
+    xhr.setContextInfo("authenticated", false);
     lib.checkAuthentication(function(authenticated) {
       equal(authenticated, false, "We are not authenticated!");
       start();
@@ -436,7 +420,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthentication with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("contextAjaxError");
     lib.checkAuthentication(function(authenticated) {
       ok(false, "xhr failure should never succeed");
       start();
@@ -451,7 +435,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthenticationAndSync with valid authentication", function() {
-    credentialsValid = true;
+    xhr.setContextInfo("authenticated", true);
+
     lib.checkAuthenticationAndSync(function onSuccess() {},
     function onComplete(authenticated) {
       equal(authenticated, true, "We are authenticated!");
@@ -464,7 +449,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthenticationAndSync with invalid authentication", function() {
-    credentialsValid = false;
+    xhr.setContextInfo("authenticated", false);
+
     lib.checkAuthenticationAndSync(function onSuccess() {
         ok(false, "We are not authenticated!");
         start();
@@ -478,9 +464,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("checkAuthenticationAndSync with XHR failure", function() {
-    xhrFailure = true;
+    xhr.setContextInfo("authenticated", true);
+    xhr.useResult("ajaxError");
+
     lib.checkAuthenticationAndSync(function onSuccess() {
-      ok(false, "xhr failure should never succeed");
     }, function onComplete() {
       ok(false, "xhr failure should never succeed");
       
@@ -495,7 +482,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("isEmailRegistered with registered email", function() {
-    lib.isEmailRegistered("registered", function(registered) {
+    lib.isEmailRegistered("registered@testuser.com", function(registered) {
       ok(registered);
       start();
     }, function onFailure() {
@@ -506,8 +493,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
-  test("isEmailRegistered with non-registered email", function() {
-    lib.isEmailRegistered("nonregistered", function(registered) {
+  test("isEmailRegistered with unregistered email", function() {
+    lib.isEmailRegistered("unregistered@testuser.com", function(registered) {
       equal(registered, false);
       start();
     }, function onFailure() {
@@ -519,7 +506,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("isEmailRegistered with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.isEmailRegistered("registered", function(registered) {
       ok(false, "xhr failure should never succeed");
       start();
@@ -538,7 +525,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identities = lib.getStoredEmailKeypairs();
       equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation.");
 
-
       equal(storage.getStagedOnBehalfOf(), lib.getHostname(), "initiatingOrigin is stored"); 
 
       start();
@@ -547,8 +533,25 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("addEmail with addition refused", function() {
+    xhr.useResult("throttle");
+
+    lib.addEmail("testemail@testemail.com", function(added) {
+      equal(added, false, "user addition was refused");
+
+      var identities = lib.getStoredEmailKeypairs();
+      equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation.");
+
+      equal(typeof storage.getStagedOnBehalfOf(), "undefined", "initiatingOrigin is not stored"); 
+
+      start();
+    }, failure("addEmail failure"));
+
+    stop();
+  });
+
   test("addEmail with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.addEmail("testemail@testemail.com", function(added) {
       ok(false, "xhr failure should never succeed");
       start();
@@ -561,18 +564,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
 
-
-  /**
-   * The next three tests use the mock network harness.  The tests are testing 
-   * the polling action and whether `waitForEmailValidation` reacts as expected
-   * to the various network responses.  The network harness simulates multiple 
-   * calls to `checkEmailRegistration`, attempting to simulate real use 
-   * interaction to verify the email address, the first call to 
-   * `checkEmailRegistration` returns `pending`, the second returns the value 
-   * stored in `registrationResponse`.
-   */
  test("waitForEmailValidation `complete` response", function() {
-    lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+    storage.setStagedOnBehalfOf(testOrigin);
+
+    xhr.useResult("complete");
+    lib.waitForEmailValidation("registered@testuser.com", function(status) {
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       equal(status, "complete", "complete response expected");
       start();
     }, failure("waitForEmailValidation failure"));
@@ -581,9 +578,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForEmailValidation `mustAuth` response", function() {
-    registrationResponse = "mustAuth";
+    storage.setStagedOnBehalfOf(testOrigin);
+    xhr.useResult("mustAuth");
 
-    lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+    lib.waitForEmailValidation("registered@testuser.com", function(status) {
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       equal(status, "mustAuth", "mustAuth response expected");
       start();
     }, failure("waitForEmailValidation failure"));
@@ -592,12 +591,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForEmailValidation with `noRegistration` response", function() {
-    registrationResponse = "noRegistration";
+    storage.setStagedOnBehalfOf(testOrigin);
+    xhr.useResult("noRegistration");
 
-    lib.waitForEmailValidation("baduser@testuser.com", function(status) {
+    lib.waitForEmailValidation("registered@testuser.com", function(status) {
       ok(false, "not expecting success")
       start();
     }, function(status) {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       ok(status, "noRegistration", "noRegistration response causes failure");
       start();
     });
@@ -607,11 +608,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
  test("waitForEmailValidation XHR failure", function() {
-    xhrFailure = true;
-    lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+    storage.setStagedOnBehalfOf(testOrigin);
+    xhr.useResult("ajaxError");
+
+    lib.waitForEmailValidation("registered@testuser.com", function(status) {
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       ok(true, "xhr failure should always be a failure"); 
       start();
     });
@@ -636,7 +640,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("verifyEmail with a bad token", function() {
-    validToken = false;
+    xhr.useResult("invalid");
 
     lib.verifyEmail("token", function onSuccess(info) {
       
@@ -650,7 +654,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("verifyEmail with an XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
 
     lib.verifyEmail("token", function onSuccess(info) {
       ok(false, "xhr failure should never succeed");
@@ -664,7 +668,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("syncEmailKeypair with successful sync", function() {
-    syncValid = true;
     lib.syncEmailKeypair("testemail@testemail.com", function(keypair) {
       var identity = lib.getStoredEmailKeypair("testemail@testemail.com");
 
@@ -680,7 +683,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("syncEmailKeypair with invalid sync", function() {
-    syncValid = false;
+    xhr.useResult("invalid");
     lib.syncEmailKeypair("testemail@testemail.com", function(keypair) {
       ok(false, "sync was invalid, this should have failed");
       start();
@@ -695,7 +698,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("syncEmailKeypair with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.syncEmailKeypair("testemail@testemail.com", function(keypair) {
       ok(false, "xhr failure should never succeed");
       start();
@@ -735,7 +738,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   test("removeEmail with XHR failure", function() {
     storage.addEmail("testemail@testemail.com", {pub: "pub", priv: "priv"});
 
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.removeEmail("testemail@testemail.com", function() {
       ok(false, "xhr failure should never succeed");
       start();
@@ -751,8 +754,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("syncEmails with no pre-loaded identities and no identities to add", function() {
-    userEmails = {};
-
+    xhr.useResult("no_identities");
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
       ok(true, "we have synced identities");
@@ -764,8 +766,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("syncEmails with no pre-loaded identities and identities to add", function() {
-    userEmails = {"testuser@testuser.com": {}};
-
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
       ok("testuser@testuser.com" in identities, "Our new email is added");
@@ -777,7 +777,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("syncEmails with identities preloaded and none to add", function() {
-    userEmails = {"testuser@testuser.com": {}};
     storage.addEmail("testuser@testuser.com", {});
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
@@ -792,8 +791,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
   test("syncEmails with identities preloaded and one to add", function() {
     storage.addEmail("testuser@testuser.com", {pubkey: pubkey, cert: random_cert});
-    userEmails = {"testuser@testuser.com": {pubkey: pubkey, cert: random_cert},
-                  "testuser2@testuser.com": {pubkey: pubkey, cert: random_cert}};
+
+    xhr.useResult("multiple");
 
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
@@ -810,7 +809,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   test("syncEmails with identities preloaded and one to remove", function() {
     storage.addEmail("testuser@testuser.com", {pub: pubkey, cert: random_cert});
     storage.addEmail("testuser2@testuser.com", {pub: pubkey, cert: random_cert});
-    userEmails = {"testuser@testuser.com":  { pub: pubkey, cert: random_cert}};
 
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
@@ -825,7 +823,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
   test("syncEmails with one to refresh", function() {
     storage.addEmail("testuser@testuser.com", {pub: pubkey, cert: random_cert});
-    keyRefresh = ["testuser@testuser.com"]; 
 
     lib.syncEmails(function onSuccess() {
       var identities = lib.getStoredEmailKeypairs();
@@ -837,7 +834,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("syncEmails with XHR failure", function() {
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
 
     lib.syncEmails(function onSuccess() {
       ok(false, "xhr failure should never succeed");
@@ -888,7 +885,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
   test("getAssertion with XHR failure", function() {
     lib.setOrigin(testOrigin);
-    xhrFailure = true;
+    xhr.useResult("ajaxError");
 
     lib.syncEmailKeypair("testuser@testuser.com", function() {
       ok(false, "xhr failure should never succeed");
@@ -903,9 +900,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("logoutUser", function(onSuccess) {
-    credentialsValid = true;
-    keyRefresh = ["testuser@testuser.com"]; 
-
     lib.authenticate("testuser@testuser.com", "testuser", function(authenticated) {
       lib.syncEmails(function() {
         var storedIdentities = storage.getEmails();
@@ -915,7 +909,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
           storedIdentities = storage.getEmails();
           equal(_.size(storedIdentities), 0, "All items have been removed on logout");
 
-          equal(credentialsValid, false, "credentials were invalidated in logout");
           start();
         }, failure("logoutUser failure"));
       }, failure("syncEmails failure"));
@@ -925,12 +918,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("logoutUser with XHR failure", function(onSuccess) {
-    credentialsValid = true;
-    keyRefresh = ["testuser@testuser.com"]; 
-
     lib.authenticate("testuser@testuser.com", "testuser", function(authenticated) {
       lib.syncEmails(function() {
-         xhrFailure = true;
+         xhr.useResult("ajaxError");
 
         lib.logoutUser(function() {
           ok(false, "xhr failure should never succeed");
@@ -958,7 +948,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("cancelUser with XHR failure", function(onSuccess) {
-     xhrFailure = true;
+    xhr.useResult("ajaxError");
     lib.cancelUser(function() {
       ok(false, "xhr failure should never succeed");
       start();
diff --git a/browserid/static/dialog/test/qunit/resources/validation_unit_test.js b/resources/static/dialog/test/qunit/resources/validation_unit_test.js
similarity index 76%
rename from browserid/static/dialog/test/qunit/resources/validation_unit_test.js
rename to resources/static/dialog/test/qunit/resources/validation_unit_test.js
index 1366573ce557a706e38e0d79ae67a7ad66b5a4f6..37925f8174bb184647f14f600f12e729707fe60c 100644
--- a/browserid/static/dialog/test/qunit/resources/validation_unit_test.js
+++ b/resources/static/dialog/test/qunit/resources/validation_unit_test.js
@@ -46,7 +46,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
     tooltipShown = true;
   }
 
-  module("validation", {
+  module("resources/validation", {
     setup: function() {
       origShowTooltip = bid.Tooltip.showTooltip;
       bid.Tooltip.showTooltip = showTooltip;
@@ -98,10 +98,77 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
   test("email with empty email", function() {
     var valid = validation.email("");
 
-    equal(valid, valid, "missing email is missing");
+    equal(valid, false, "missing email is missing");
     equal(tooltipShown, true, "missing email shows no tooltip");
   });
 
+  test("email with Capital Letters in local side", function() {
+    var valid = validation.email("X@y.z");
+
+    equal(valid, true, "capital letters allowed in local side");
+    equal(tooltipShown, false, "capital letters in local side causes no tooltip");
+  });
+
+  test("email with Capital Letters in domain side", function() {
+    var valid = validation.email("x@Y.z");
+
+    equal(valid, false, "capital letters not allowed in domain side");
+    equal(tooltipShown, true, "missing email shows no tooltip");
+  });
+
+
+  test("email with 64 characters in local side", function() {
+    var local = "";
+
+    for(var i = 0; i < 64; i++) {
+      local += "a";
+    }
+
+    var valid = validation.email(local + "@y.z");
+
+    equal(valid, true, "64 characters allowed in local side");
+    equal(tooltipShown, false, "64 characters causes no error");
+  });
+
+  test("email with more than 64 characters in local side", function() {
+    var local = "";
+
+    for(var i = 0; i <= 64; i++) {
+      local += "a";
+    }
+
+    var valid = validation.email(local + "@y.z");
+
+    equal(valid, false, "only 64 characters allowed in local side");
+    equal(tooltipShown, true, "65 characters causes an error");
+  });
+
+  test("email with 254 characters", function() {
+    var domain = "";
+
+    for(var i = 0; i < 248; i++) {
+      domain += "a";
+    }
+
+    var valid = validation.email("x@" + domain * ".com");
+
+    equal(valid, false, "254 characters allowed in total address");
+    equal(tooltipShown, true, "254 characters causes no error");
+  });
+
+  test("email with more than 254 characters", function() {
+    var domain = "";
+
+    for(var i = 0; i <= 248; i++) {
+      domain += "a";
+    }
+
+    var valid = validation.email("x@" + domain * ".com");
+
+    equal(valid, false, "only 254 characters allowed in total address");
+    equal(tooltipShown, true, "> 254 characters causes an error");
+  });
+
   test("email with invalid email", function() {
     var valid = validation.email("testuser@testuser");
 
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..78c6b3f94291f023102f7d6fd92c6a421709ba2a
--- /dev/null
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -0,0 +1,60 @@
+  <strong>Sign in using</strong>
+  <div class="form_section">
+      <ul class="inputs">
+
+          <li>
+              <label for="email" class="serif">Email</label>
+              <input id="email" class="sans" type="email" autocapitalize="off" autocorrect="off" value="<%= email %>" maxlength="254"/>
+
+              <div id="email_format" class="tooltip" for="email">
+                This field must be an email address.
+              </div>
+
+              <div id="email_required" class="tooltip" for="email">
+                The email field is required.
+              </div>
+
+              <div id="could_not_add" class="tooltip" for="email">
+                We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+              </div>
+          </li>
+
+          <li id="hint_section" class="start">
+              <p>Enter your email address to sign in to <strong><%= sitename %></strong></p>
+          </li>
+
+          <li id="create_text_section" class="newuser">
+              <p><strong>Welcome to BrowserID!</strong></p>
+              <p>This email looks new, so let's get you set up.</p>
+          </li>
+
+          <li id="password_section" class="returning">
+
+              <label for="password" class="half serif">Password</label>
+              <div class="half right">
+                  <a id="forgotPassword" href="#">forgot your password?</a>
+              </div>
+              <input id="password" class="sans" type="password" maxlength="80">
+
+
+              <div id="password_required" class="tooltip" for="password">
+                The password field is required.
+              </div>
+
+              <div id="cannot_authenticate" class="tooltip" for="password">
+                The account cannot be logged in with this username and password.
+              </div>
+          </li>
+      
+      </ul>
+
+      <div class="submit cf">
+          <button class="start">next</button>
+          <button class="newuser">Verify Email</button>
+
+          <button class="returning">sign in</button>
+
+          <button class="forgot">Reset Password</button>
+          <button id="cancel_forgot_password" class="forgot">Cancel</button>
+      </div>
+  </div>
diff --git a/browserid/static/dialog/views/confirmemail.ejs b/resources/static/dialog/views/confirmemail.ejs
similarity index 100%
rename from browserid/static/dialog/views/confirmemail.ejs
rename to resources/static/dialog/views/confirmemail.ejs
diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..537d2471f5a80b3305428035e80b950569a20ba9
--- /dev/null
+++ b/resources/static/dialog/views/error.ejs
@@ -0,0 +1,42 @@
+
+  <h2>We are very sorry, there has been an error!</h2>
+
+  <p>
+    To retry, you will have to close BrowserID and try again.  More info can be found by opening the expanded info below.
+  </p>
+
+  <a href="#" id="openMoreInfo">See more info</a>
+
+  <ul id="moreInfo">
+    <% if (typeof action !== "undefined") { %>
+      <li>
+        <strong id="action">Action: </strong><%= action.title %>
+
+        <% if (action.message) { %>
+          <p>
+            <%= action.message %>
+          </p>  
+        <% } %>
+      </li>
+    <% } %>
+
+    <% if (typeof network !== "undefined") { %>
+      <li>
+
+        <strong id="network">Network Info:</strong> <%= network.type %>: <%= network.url %>
+
+        <p>
+          <strong>Response Code - </strong> <%= network.textStatus %> 
+        </p>
+
+        <% if (network.errorThrown) { %>
+          <p>
+            <strong>Error Type:</strong> <%= network.errorThrown %> 
+          </p>  
+        <% } %>
+
+      </li>
+
+    <% } %>
+
+  </ul>
diff --git a/resources/static/dialog/views/offline.ejs b/resources/static/dialog/views/offline.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..677951bef1fc6a0fb4cb93eab91ed11c48f7d452
--- /dev/null
+++ b/resources/static/dialog/views/offline.ejs
@@ -0,0 +1,8 @@
+
+  <h2 id="offline">You are offline!</h2>
+
+  <p>
+    We are sorry, but we cannot communicate with BrowserID while you are offline.
+  </p>
+
+
diff --git a/browserid/static/dialog/views/pickemail.ejs b/resources/static/dialog/views/pickemail.ejs
similarity index 79%
rename from browserid/static/dialog/views/pickemail.ejs
rename to resources/static/dialog/views/pickemail.ejs
index de6582dadd816f542618943c87fd77c27d96a14a..d0ca8939fd53453f73732b09a0ee541e52f7a39b 100644
--- a/browserid/static/dialog/views/pickemail.ejs
+++ b/resources/static/dialog/views/pickemail.ejs
@@ -4,8 +4,11 @@
       <ul class="inputs">
           <% _.each(identities, function(email_obj, email_address) { %>
               <li>
-                  <label for="<%= email_address %>" class="serif">
-                    <input type="radio" name="email" id="<%= email_address %>" value="<%= email_address %>" />
+
+                  <label for="<%= email_address %>" class="serif<% if (email_address === siteemail) { %> preselected<% } %>">
+                    <input type="radio" name="email" id="<%= email_address %>" value="<%= email_address %>" 
+                      <% if (email_address === siteemail) { %> checked="checked" <% } %> 
+                    />
                     <%= email_address %>
                   </label>
               </li>
@@ -26,7 +29,7 @@
       <ul class="inputs">
           <li>
               <label for="newEmail" class="serif">New email address</label>
-              <input id="newEmail" name="newEmail" type="email" class="sans" autocapitalize="off" autocorrect="off" />
+              <input id="newEmail" name="newEmail" type="email" class="sans" autocapitalize="off" autocorrect="off" maxlength="254"/>
 
               <div id="email_format" class="tooltip" for="newEmail">
                 This field must be an email address.
@@ -37,7 +40,7 @@
               </div>
 
               <div id="could_not_add" class="tooltip" for="newEmail">
-                This email address could not be added.
+                We just sent an email to that address!  If you really want to send another, wait a minute or two and try again.
               </div>
 
               <div id="already_taken" class="tooltip" for="newEmail">
diff --git a/browserid/static/dialog/views/testBodyTemplate.ejs b/resources/static/dialog/views/testBodyTemplate.ejs
similarity index 100%
rename from browserid/static/dialog/views/testBodyTemplate.ejs
rename to resources/static/dialog/views/testBodyTemplate.ejs
diff --git a/browserid/static/dialog/views/wait.ejs b/resources/static/dialog/views/wait.ejs
similarity index 100%
rename from browserid/static/dialog/views/wait.ejs
rename to resources/static/dialog/views/wait.ejs
diff --git a/browserid/static/favicon.ico b/resources/static/favicon.ico
similarity index 100%
rename from browserid/static/favicon.ico
rename to resources/static/favicon.ico
diff --git a/browserid/static/funcunit/.gitignore b/resources/static/funcunit/.gitignore
similarity index 100%
rename from browserid/static/funcunit/.gitignore
rename to resources/static/funcunit/.gitignore
diff --git a/browserid/static/funcunit/.gitmodules b/resources/static/funcunit/.gitmodules
similarity index 100%
rename from browserid/static/funcunit/.gitmodules
rename to resources/static/funcunit/.gitmodules
diff --git a/browserid/static/funcunit/README b/resources/static/funcunit/README
similarity index 100%
rename from browserid/static/funcunit/README
rename to resources/static/funcunit/README
diff --git a/browserid/static/funcunit/autosuggest/auto_suggest.js b/resources/static/funcunit/autosuggest/auto_suggest.js
similarity index 100%
rename from browserid/static/funcunit/autosuggest/auto_suggest.js
rename to resources/static/funcunit/autosuggest/auto_suggest.js
diff --git a/browserid/static/funcunit/autosuggest/autosuggest.css b/resources/static/funcunit/autosuggest/autosuggest.css
similarity index 100%
rename from browserid/static/funcunit/autosuggest/autosuggest.css
rename to resources/static/funcunit/autosuggest/autosuggest.css
diff --git a/browserid/static/funcunit/autosuggest/autosuggest.html b/resources/static/funcunit/autosuggest/autosuggest.html
similarity index 100%
rename from browserid/static/funcunit/autosuggest/autosuggest.html
rename to resources/static/funcunit/autosuggest/autosuggest.html
diff --git a/browserid/static/funcunit/autosuggest/autosuggest.js b/resources/static/funcunit/autosuggest/autosuggest.js
similarity index 100%
rename from browserid/static/funcunit/autosuggest/autosuggest.js
rename to resources/static/funcunit/autosuggest/autosuggest.js
diff --git a/browserid/static/funcunit/autosuggest/autosuggest_test.js b/resources/static/funcunit/autosuggest/autosuggest_test.js
similarity index 100%
rename from browserid/static/funcunit/autosuggest/autosuggest_test.js
rename to resources/static/funcunit/autosuggest/autosuggest_test.js
diff --git a/browserid/static/funcunit/autosuggest/funcunit.html b/resources/static/funcunit/autosuggest/funcunit.html
similarity index 100%
rename from browserid/static/funcunit/autosuggest/funcunit.html
rename to resources/static/funcunit/autosuggest/funcunit.html
diff --git a/browserid/static/funcunit/build.js b/resources/static/funcunit/build.js
similarity index 100%
rename from browserid/static/funcunit/build.js
rename to resources/static/funcunit/build.js
diff --git a/browserid/static/funcunit/dependencies.json b/resources/static/funcunit/dependencies.json
similarity index 100%
rename from browserid/static/funcunit/dependencies.json
rename to resources/static/funcunit/dependencies.json
diff --git a/browserid/static/funcunit/docs.html b/resources/static/funcunit/docs.html
similarity index 100%
rename from browserid/static/funcunit/docs.html
rename to resources/static/funcunit/docs.html
diff --git a/browserid/static/funcunit/drivers/selenium.js b/resources/static/funcunit/drivers/selenium.js
similarity index 100%
rename from browserid/static/funcunit/drivers/selenium.js
rename to resources/static/funcunit/drivers/selenium.js
diff --git a/browserid/static/funcunit/drivers/standard.js b/resources/static/funcunit/drivers/standard.js
similarity index 100%
rename from browserid/static/funcunit/drivers/standard.js
rename to resources/static/funcunit/drivers/standard.js
diff --git a/browserid/static/funcunit/envjs b/resources/static/funcunit/envjs
similarity index 100%
rename from browserid/static/funcunit/envjs
rename to resources/static/funcunit/envjs
diff --git a/browserid/static/funcunit/envjs.bat b/resources/static/funcunit/envjs.bat
similarity index 100%
rename from browserid/static/funcunit/envjs.bat
rename to resources/static/funcunit/envjs.bat
diff --git a/browserid/static/funcunit/funcunit.html b/resources/static/funcunit/funcunit.html
similarity index 100%
rename from browserid/static/funcunit/funcunit.html
rename to resources/static/funcunit/funcunit.html
diff --git a/browserid/static/funcunit/funcunit.js b/resources/static/funcunit/funcunit.js
similarity index 100%
rename from browserid/static/funcunit/funcunit.js
rename to resources/static/funcunit/funcunit.js
diff --git a/browserid/static/funcunit/generate_docs.html b/resources/static/funcunit/generate_docs.html
similarity index 100%
rename from browserid/static/funcunit/generate_docs.html
rename to resources/static/funcunit/generate_docs.html
diff --git a/browserid/static/funcunit/index.html b/resources/static/funcunit/index.html
similarity index 100%
rename from browserid/static/funcunit/index.html
rename to resources/static/funcunit/index.html
diff --git a/browserid/static/funcunit/java/extensions/fakesteal.js b/resources/static/funcunit/java/extensions/fakesteal.js
similarity index 100%
rename from browserid/static/funcunit/java/extensions/fakesteal.js
rename to resources/static/funcunit/java/extensions/fakesteal.js
diff --git a/browserid/static/funcunit/java/extensions/wrapped.js b/resources/static/funcunit/java/extensions/wrapped.js
similarity index 100%
rename from browserid/static/funcunit/java/extensions/wrapped.js
rename to resources/static/funcunit/java/extensions/wrapped.js
diff --git a/browserid/static/funcunit/java/selenium-java-client-driver.jar b/resources/static/funcunit/java/selenium-java-client-driver.jar
similarity index 100%
rename from browserid/static/funcunit/java/selenium-java-client-driver.jar
rename to resources/static/funcunit/java/selenium-java-client-driver.jar
diff --git a/browserid/static/funcunit/java/selenium-server-standalone-2.0b3.jar b/resources/static/funcunit/java/selenium-server-standalone-2.0b3.jar
similarity index 100%
rename from browserid/static/funcunit/java/selenium-server-standalone-2.0b3.jar
rename to resources/static/funcunit/java/selenium-server-standalone-2.0b3.jar
diff --git a/browserid/static/funcunit/java/user-extensions.js b/resources/static/funcunit/java/user-extensions.js
similarity index 100%
rename from browserid/static/funcunit/java/user-extensions.js
rename to resources/static/funcunit/java/user-extensions.js
diff --git a/browserid/static/funcunit/loader.js b/resources/static/funcunit/loader.js
similarity index 100%
rename from browserid/static/funcunit/loader.js
rename to resources/static/funcunit/loader.js
diff --git a/browserid/static/funcunit/pages/example.js b/resources/static/funcunit/pages/example.js
similarity index 100%
rename from browserid/static/funcunit/pages/example.js
rename to resources/static/funcunit/pages/example.js
diff --git a/browserid/static/funcunit/pages/follow.js b/resources/static/funcunit/pages/follow.js
similarity index 100%
rename from browserid/static/funcunit/pages/follow.js
rename to resources/static/funcunit/pages/follow.js
diff --git a/browserid/static/funcunit/pages/init.js b/resources/static/funcunit/pages/init.js
similarity index 100%
rename from browserid/static/funcunit/pages/init.js
rename to resources/static/funcunit/pages/init.js
diff --git a/browserid/static/funcunit/pages/mastering.js b/resources/static/funcunit/pages/mastering.js
similarity index 100%
rename from browserid/static/funcunit/pages/mastering.js
rename to resources/static/funcunit/pages/mastering.js
diff --git a/browserid/static/funcunit/pages/selenium.js b/resources/static/funcunit/pages/selenium.js
similarity index 100%
rename from browserid/static/funcunit/pages/selenium.js
rename to resources/static/funcunit/pages/selenium.js
diff --git a/browserid/static/funcunit/pages/setup.js b/resources/static/funcunit/pages/setup.js
similarity index 100%
rename from browserid/static/funcunit/pages/setup.js
rename to resources/static/funcunit/pages/setup.js
diff --git a/browserid/static/funcunit/pages/standalone.js b/resources/static/funcunit/pages/standalone.js
similarity index 100%
rename from browserid/static/funcunit/pages/standalone.js
rename to resources/static/funcunit/pages/standalone.js
diff --git a/browserid/static/funcunit/pages/writing.js b/resources/static/funcunit/pages/writing.js
similarity index 100%
rename from browserid/static/funcunit/pages/writing.js
rename to resources/static/funcunit/pages/writing.js
diff --git a/browserid/static/funcunit/qunit.html b/resources/static/funcunit/qunit.html
similarity index 100%
rename from browserid/static/funcunit/qunit.html
rename to resources/static/funcunit/qunit.html
diff --git a/browserid/static/funcunit/qunit/qunit.css b/resources/static/funcunit/qunit/qunit.css
similarity index 100%
rename from browserid/static/funcunit/qunit/qunit.css
rename to resources/static/funcunit/qunit/qunit.css
diff --git a/browserid/static/funcunit/qunit/qunit.js b/resources/static/funcunit/qunit/qunit.js
similarity index 100%
rename from browserid/static/funcunit/qunit/qunit.js
rename to resources/static/funcunit/qunit/qunit.js
diff --git a/browserid/static/funcunit/qunit/rhino/rhino.js b/resources/static/funcunit/qunit/rhino/rhino.js
similarity index 100%
rename from browserid/static/funcunit/qunit/rhino/rhino.js
rename to resources/static/funcunit/qunit/rhino/rhino.js
diff --git a/browserid/static/funcunit/qunit/test/qunit.html b/resources/static/funcunit/qunit/test/qunit.html
similarity index 100%
rename from browserid/static/funcunit/qunit/test/qunit.html
rename to resources/static/funcunit/qunit/test/qunit.html
diff --git a/browserid/static/funcunit/qunit/test/test.js b/resources/static/funcunit/qunit/test/test.js
similarity index 100%
rename from browserid/static/funcunit/qunit/test/test.js
rename to resources/static/funcunit/qunit/test/test.js
diff --git a/browserid/static/funcunit/resources/jquery.js b/resources/static/funcunit/resources/jquery.js
similarity index 100%
rename from browserid/static/funcunit/resources/jquery.js
rename to resources/static/funcunit/resources/jquery.js
diff --git a/browserid/static/funcunit/resources/json.js b/resources/static/funcunit/resources/json.js
similarity index 100%
rename from browserid/static/funcunit/resources/json.js
rename to resources/static/funcunit/resources/json.js
diff --git a/browserid/static/funcunit/resources/selector.js b/resources/static/funcunit/resources/selector.js
similarity index 100%
rename from browserid/static/funcunit/resources/selector.js
rename to resources/static/funcunit/resources/selector.js
diff --git a/browserid/static/funcunit/resources/selenium_start.js b/resources/static/funcunit/resources/selenium_start.js
similarity index 100%
rename from browserid/static/funcunit/resources/selenium_start.js
rename to resources/static/funcunit/resources/selenium_start.js
diff --git a/browserid/static/funcunit/scripts/run.js b/resources/static/funcunit/scripts/run.js
similarity index 100%
rename from browserid/static/funcunit/scripts/run.js
rename to resources/static/funcunit/scripts/run.js
diff --git a/browserid/static/funcunit/settings.js b/resources/static/funcunit/settings.js
similarity index 100%
rename from browserid/static/funcunit/settings.js
rename to resources/static/funcunit/settings.js
diff --git a/browserid/static/funcunit/summary.ejs b/resources/static/funcunit/summary.ejs
similarity index 100%
rename from browserid/static/funcunit/summary.ejs
rename to resources/static/funcunit/summary.ejs
diff --git a/browserid/static/funcunit/syn/.gitignore b/resources/static/funcunit/syn/.gitignore
similarity index 100%
rename from browserid/static/funcunit/syn/.gitignore
rename to resources/static/funcunit/syn/.gitignore
diff --git a/browserid/static/funcunit/syn/README b/resources/static/funcunit/syn/README
similarity index 100%
rename from browserid/static/funcunit/syn/README
rename to resources/static/funcunit/syn/README
diff --git a/browserid/static/funcunit/syn/browsers.js b/resources/static/funcunit/syn/browsers.js
similarity index 100%
rename from browserid/static/funcunit/syn/browsers.js
rename to resources/static/funcunit/syn/browsers.js
diff --git a/browserid/static/funcunit/syn/build.js b/resources/static/funcunit/syn/build.js
similarity index 100%
rename from browserid/static/funcunit/syn/build.js
rename to resources/static/funcunit/syn/build.js
diff --git a/browserid/static/funcunit/syn/demo.html b/resources/static/funcunit/syn/demo.html
similarity index 100%
rename from browserid/static/funcunit/syn/demo.html
rename to resources/static/funcunit/syn/demo.html
diff --git a/browserid/static/funcunit/syn/demo/record.js b/resources/static/funcunit/syn/demo/record.js
similarity index 100%
rename from browserid/static/funcunit/syn/demo/record.js
rename to resources/static/funcunit/syn/demo/record.js
diff --git a/browserid/static/funcunit/syn/drag/drag.html b/resources/static/funcunit/syn/drag/drag.html
similarity index 100%
rename from browserid/static/funcunit/syn/drag/drag.html
rename to resources/static/funcunit/syn/drag/drag.html
diff --git a/browserid/static/funcunit/syn/drag/drag.js b/resources/static/funcunit/syn/drag/drag.js
similarity index 100%
rename from browserid/static/funcunit/syn/drag/drag.js
rename to resources/static/funcunit/syn/drag/drag.js
diff --git a/browserid/static/funcunit/syn/drag/qunit.html b/resources/static/funcunit/syn/drag/qunit.html
similarity index 100%
rename from browserid/static/funcunit/syn/drag/qunit.html
rename to resources/static/funcunit/syn/drag/qunit.html
diff --git a/browserid/static/funcunit/syn/drag/test/qunit/drag_test.js b/resources/static/funcunit/syn/drag/test/qunit/drag_test.js
similarity index 100%
rename from browserid/static/funcunit/syn/drag/test/qunit/drag_test.js
rename to resources/static/funcunit/syn/drag/test/qunit/drag_test.js
diff --git a/browserid/static/funcunit/syn/drag/test/qunit/qunit.js b/resources/static/funcunit/syn/drag/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/funcunit/syn/drag/test/qunit/qunit.js
rename to resources/static/funcunit/syn/drag/test/qunit/qunit.js
diff --git a/browserid/static/funcunit/syn/key.js b/resources/static/funcunit/syn/key.js
similarity index 100%
rename from browserid/static/funcunit/syn/key.js
rename to resources/static/funcunit/syn/key.js
diff --git a/browserid/static/funcunit/syn/mouse.js b/resources/static/funcunit/syn/mouse.js
similarity index 100%
rename from browserid/static/funcunit/syn/mouse.js
rename to resources/static/funcunit/syn/mouse.js
diff --git a/browserid/static/funcunit/syn/qunit.html b/resources/static/funcunit/syn/qunit.html
similarity index 100%
rename from browserid/static/funcunit/syn/qunit.html
rename to resources/static/funcunit/syn/qunit.html
diff --git a/browserid/static/funcunit/syn/recorder.html b/resources/static/funcunit/syn/recorder.html
similarity index 100%
rename from browserid/static/funcunit/syn/recorder.html
rename to resources/static/funcunit/syn/recorder.html
diff --git a/browserid/static/funcunit/syn/resources/jquery.event.drag.js b/resources/static/funcunit/syn/resources/jquery.event.drag.js
similarity index 100%
rename from browserid/static/funcunit/syn/resources/jquery.event.drag.js
rename to resources/static/funcunit/syn/resources/jquery.event.drag.js
diff --git a/browserid/static/funcunit/syn/resources/jquery.event.drop.js b/resources/static/funcunit/syn/resources/jquery.event.drop.js
similarity index 100%
rename from browserid/static/funcunit/syn/resources/jquery.event.drop.js
rename to resources/static/funcunit/syn/resources/jquery.event.drop.js
diff --git a/browserid/static/funcunit/syn/resources/jquery.js b/resources/static/funcunit/syn/resources/jquery.js
similarity index 100%
rename from browserid/static/funcunit/syn/resources/jquery.js
rename to resources/static/funcunit/syn/resources/jquery.js
diff --git a/browserid/static/funcunit/syn/resources/qunit/qunit.css b/resources/static/funcunit/syn/resources/qunit/qunit.css
similarity index 100%
rename from browserid/static/funcunit/syn/resources/qunit/qunit.css
rename to resources/static/funcunit/syn/resources/qunit/qunit.css
diff --git a/browserid/static/funcunit/syn/resources/qunit/qunit.js b/resources/static/funcunit/syn/resources/qunit/qunit.js
similarity index 100%
rename from browserid/static/funcunit/syn/resources/qunit/qunit.js
rename to resources/static/funcunit/syn/resources/qunit/qunit.js
diff --git a/browserid/static/funcunit/syn/syn.js b/resources/static/funcunit/syn/syn.js
similarity index 100%
rename from browserid/static/funcunit/syn/syn.js
rename to resources/static/funcunit/syn/syn.js
diff --git a/browserid/static/funcunit/syn/synthetic.html b/resources/static/funcunit/syn/synthetic.html
similarity index 100%
rename from browserid/static/funcunit/syn/synthetic.html
rename to resources/static/funcunit/syn/synthetic.html
diff --git a/browserid/static/funcunit/syn/synthetic.js b/resources/static/funcunit/syn/synthetic.js
similarity index 100%
rename from browserid/static/funcunit/syn/synthetic.js
rename to resources/static/funcunit/syn/synthetic.js
diff --git a/browserid/static/funcunit/syn/test/clickbasic.html b/resources/static/funcunit/syn/test/clickbasic.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/clickbasic.html
rename to resources/static/funcunit/syn/test/clickbasic.html
diff --git a/browserid/static/funcunit/syn/test/qunit/h3.html b/resources/static/funcunit/syn/test/qunit/h3.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/h3.html
rename to resources/static/funcunit/syn/test/qunit/h3.html
diff --git a/browserid/static/funcunit/syn/test/qunit/key_test.js b/resources/static/funcunit/syn/test/qunit/key_test.js
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/key_test.js
rename to resources/static/funcunit/syn/test/qunit/key_test.js
diff --git a/browserid/static/funcunit/syn/test/qunit/mouse_test.js b/resources/static/funcunit/syn/test/qunit/mouse_test.js
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/mouse_test.js
rename to resources/static/funcunit/syn/test/qunit/mouse_test.js
diff --git a/browserid/static/funcunit/syn/test/qunit/page1.html b/resources/static/funcunit/syn/test/qunit/page1.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/page1.html
rename to resources/static/funcunit/syn/test/qunit/page1.html
diff --git a/browserid/static/funcunit/syn/test/qunit/page2.html b/resources/static/funcunit/syn/test/qunit/page2.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/page2.html
rename to resources/static/funcunit/syn/test/qunit/page2.html
diff --git a/browserid/static/funcunit/syn/test/qunit/qunit.js b/resources/static/funcunit/syn/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/qunit.js
rename to resources/static/funcunit/syn/test/qunit/qunit.js
diff --git a/browserid/static/funcunit/syn/test/qunit/syn_test.js b/resources/static/funcunit/syn/test/qunit/syn_test.js
similarity index 100%
rename from browserid/static/funcunit/syn/test/qunit/syn_test.js
rename to resources/static/funcunit/syn/test/qunit/syn_test.js
diff --git a/browserid/static/funcunit/syn/test/submit.html b/resources/static/funcunit/syn/test/submit.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/submit.html
rename to resources/static/funcunit/syn/test/submit.html
diff --git a/browserid/static/funcunit/syn/test/submitted.html b/resources/static/funcunit/syn/test/submitted.html
similarity index 100%
rename from browserid/static/funcunit/syn/test/submitted.html
rename to resources/static/funcunit/syn/test/submitted.html
diff --git a/browserid/static/funcunit/template.html b/resources/static/funcunit/template.html
similarity index 100%
rename from browserid/static/funcunit/template.html
rename to resources/static/funcunit/template.html
diff --git a/browserid/static/funcunit/test/drag.html b/resources/static/funcunit/test/drag.html
similarity index 100%
rename from browserid/static/funcunit/test/drag.html
rename to resources/static/funcunit/test/drag.html
diff --git a/browserid/static/funcunit/test/findclosest.html b/resources/static/funcunit/test/findclosest.html
similarity index 100%
rename from browserid/static/funcunit/test/findclosest.html
rename to resources/static/funcunit/test/findclosest.html
diff --git a/browserid/static/funcunit/test/funcunit/find_closest_test.js b/resources/static/funcunit/test/funcunit/find_closest_test.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/find_closest_test.js
rename to resources/static/funcunit/test/funcunit/find_closest_test.js
diff --git a/browserid/static/funcunit/test/funcunit/funcunit.js b/resources/static/funcunit/test/funcunit/funcunit.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/funcunit.js
rename to resources/static/funcunit/test/funcunit/funcunit.js
diff --git a/browserid/static/funcunit/test/funcunit/funcunit_test.js b/resources/static/funcunit/test/funcunit/funcunit_test.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/funcunit_test.js
rename to resources/static/funcunit/test/funcunit/funcunit_test.js
diff --git a/browserid/static/funcunit/test/funcunit/open_test.js b/resources/static/funcunit/test/funcunit/open_test.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/open_test.js
rename to resources/static/funcunit/test/funcunit/open_test.js
diff --git a/browserid/static/funcunit/test/funcunit/protodrag_test.js b/resources/static/funcunit/test/funcunit/protodrag_test.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/protodrag_test.js
rename to resources/static/funcunit/test/funcunit/protodrag_test.js
diff --git a/browserid/static/funcunit/test/funcunit/syn_test.js b/resources/static/funcunit/test/funcunit/syn_test.js
similarity index 100%
rename from browserid/static/funcunit/test/funcunit/syn_test.js
rename to resources/static/funcunit/test/funcunit/syn_test.js
diff --git a/browserid/static/funcunit/test/jquery.event.drag.js b/resources/static/funcunit/test/jquery.event.drag.js
similarity index 100%
rename from browserid/static/funcunit/test/jquery.event.drag.js
rename to resources/static/funcunit/test/jquery.event.drag.js
diff --git a/browserid/static/funcunit/test/jquery.event.drop.js b/resources/static/funcunit/test/jquery.event.drop.js
similarity index 100%
rename from browserid/static/funcunit/test/jquery.event.drop.js
rename to resources/static/funcunit/test/jquery.event.drop.js
diff --git a/browserid/static/funcunit/test/jquery.js b/resources/static/funcunit/test/jquery.js
similarity index 100%
rename from browserid/static/funcunit/test/jquery.js
rename to resources/static/funcunit/test/jquery.js
diff --git a/browserid/static/funcunit/test/myapp.html b/resources/static/funcunit/test/myapp.html
similarity index 100%
rename from browserid/static/funcunit/test/myapp.html
rename to resources/static/funcunit/test/myapp.html
diff --git a/browserid/static/funcunit/test/myotherapp.html b/resources/static/funcunit/test/myotherapp.html
similarity index 100%
rename from browserid/static/funcunit/test/myotherapp.html
rename to resources/static/funcunit/test/myotherapp.html
diff --git a/browserid/static/funcunit/test/protodrag/dragdrop.js b/resources/static/funcunit/test/protodrag/dragdrop.js
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/dragdrop.js
rename to resources/static/funcunit/test/protodrag/dragdrop.js
diff --git a/browserid/static/funcunit/test/protodrag/effects.js b/resources/static/funcunit/test/protodrag/effects.js
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/effects.js
rename to resources/static/funcunit/test/protodrag/effects.js
diff --git a/browserid/static/funcunit/test/protodrag/funcunit_test.js b/resources/static/funcunit/test/protodrag/funcunit_test.js
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/funcunit_test.js
rename to resources/static/funcunit/test/protodrag/funcunit_test.js
diff --git a/browserid/static/funcunit/test/protodrag/myapp.html b/resources/static/funcunit/test/protodrag/myapp.html
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/myapp.html
rename to resources/static/funcunit/test/protodrag/myapp.html
diff --git a/browserid/static/funcunit/test/protodrag/prototype.js b/resources/static/funcunit/test/protodrag/prototype.js
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/prototype.js
rename to resources/static/funcunit/test/protodrag/prototype.js
diff --git a/browserid/static/funcunit/test/protodrag/scriptaculous.js b/resources/static/funcunit/test/protodrag/scriptaculous.js
similarity index 100%
rename from browserid/static/funcunit/test/protodrag/scriptaculous.js
rename to resources/static/funcunit/test/protodrag/scriptaculous.js
diff --git a/browserid/static/funcunit/test/qunit/qunit.js b/resources/static/funcunit/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/funcunit/test/qunit/qunit.js
rename to resources/static/funcunit/test/qunit/qunit.js
diff --git a/browserid/static/funcunit/test/run.js b/resources/static/funcunit/test/run.js
similarity index 100%
rename from browserid/static/funcunit/test/run.js
rename to resources/static/funcunit/test/run.js
diff --git a/browserid/static/funcunit/update b/resources/static/funcunit/update
similarity index 100%
rename from browserid/static/funcunit/update
rename to resources/static/funcunit/update
diff --git a/browserid/static/i/a_better_way.png b/resources/static/i/a_better_way.png
similarity index 100%
rename from browserid/static/i/a_better_way.png
rename to resources/static/i/a_better_way.png
diff --git a/browserid/static/i/arrow.png b/resources/static/i/arrow.png
similarity index 100%
rename from browserid/static/i/arrow.png
rename to resources/static/i/arrow.png
diff --git a/browserid/static/i/bg.png b/resources/static/i/bg.png
similarity index 100%
rename from browserid/static/i/bg.png
rename to resources/static/i/bg.png
diff --git a/browserid/static/i/blink.gif b/resources/static/i/blink.gif
similarity index 100%
rename from browserid/static/i/blink.gif
rename to resources/static/i/blink.gif
diff --git a/browserid/static/i/browserid_logo_lil.png b/resources/static/i/browserid_logo_lil.png
similarity index 100%
rename from browserid/static/i/browserid_logo_lil.png
rename to resources/static/i/browserid_logo_lil.png
diff --git a/browserid/static/i/browserid_logo_sm.png b/resources/static/i/browserid_logo_sm.png
similarity index 100%
rename from browserid/static/i/browserid_logo_sm.png
rename to resources/static/i/browserid_logo_sm.png
diff --git a/browserid/static/i/card.png b/resources/static/i/card.png
similarity index 100%
rename from browserid/static/i/card.png
rename to resources/static/i/card.png
diff --git a/browserid/static/i/check.png b/resources/static/i/check.png
similarity index 100%
rename from browserid/static/i/check.png
rename to resources/static/i/check.png
diff --git a/browserid/static/i/count.png b/resources/static/i/count.png
similarity index 100%
rename from browserid/static/i/count.png
rename to resources/static/i/count.png
diff --git a/resources/static/i/firefox_logo.png b/resources/static/i/firefox_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..c55a338081cc1e0f1d2e78fc50226695c0b37488
Binary files /dev/null and b/resources/static/i/firefox_logo.png differ
diff --git a/browserid/static/i/hint.png b/resources/static/i/hint.png
similarity index 100%
rename from browserid/static/i/hint.png
rename to resources/static/i/hint.png
diff --git a/browserid/static/i/icon.png b/resources/static/i/icon.png
similarity index 100%
rename from browserid/static/i/icon.png
rename to resources/static/i/icon.png
diff --git a/browserid/static/i/labs-logo-small.png b/resources/static/i/labs-logo-small.png
similarity index 100%
rename from browserid/static/i/labs-logo-small.png
rename to resources/static/i/labs-logo-small.png
diff --git a/browserid/static/i/lock.png b/resources/static/i/lock.png
similarity index 100%
rename from browserid/static/i/lock.png
rename to resources/static/i/lock.png
diff --git a/browserid/static/i/sign_in_blue.png b/resources/static/i/sign_in_blue.png
similarity index 100%
rename from browserid/static/i/sign_in_blue.png
rename to resources/static/i/sign_in_blue.png
diff --git a/browserid/static/i/sign_in_green.png b/resources/static/i/sign_in_green.png
similarity index 100%
rename from browserid/static/i/sign_in_green.png
rename to resources/static/i/sign_in_green.png
diff --git a/browserid/static/i/sign_in_grey.png b/resources/static/i/sign_in_grey.png
similarity index 100%
rename from browserid/static/i/sign_in_grey.png
rename to resources/static/i/sign_in_grey.png
diff --git a/browserid/static/i/sign_in_orange.png b/resources/static/i/sign_in_orange.png
similarity index 100%
rename from browserid/static/i/sign_in_orange.png
rename to resources/static/i/sign_in_orange.png
diff --git a/browserid/static/i/sign_in_red.png b/resources/static/i/sign_in_red.png
similarity index 100%
rename from browserid/static/i/sign_in_red.png
rename to resources/static/i/sign_in_red.png
diff --git a/browserid/static/i/slit.png b/resources/static/i/slit.png
similarity index 100%
rename from browserid/static/i/slit.png
rename to resources/static/i/slit.png
diff --git a/browserid/static/i/sprite.png b/resources/static/i/sprite.png
similarity index 100%
rename from browserid/static/i/sprite.png
rename to resources/static/i/sprite.png
diff --git a/browserid/static/i/sunny.png b/resources/static/i/sunny.png
similarity index 100%
rename from browserid/static/i/sunny.png
rename to resources/static/i/sunny.png
diff --git a/browserid/static/i/times.gif b/resources/static/i/times.gif
similarity index 100%
rename from browserid/static/i/times.gif
rename to resources/static/i/times.gif
diff --git a/browserid/static/i/tutorial_1.png b/resources/static/i/tutorial_1.png
similarity index 100%
rename from browserid/static/i/tutorial_1.png
rename to resources/static/i/tutorial_1.png
diff --git a/browserid/static/i/tutorial_2.png b/resources/static/i/tutorial_2.png
similarity index 100%
rename from browserid/static/i/tutorial_2.png
rename to resources/static/i/tutorial_2.png
diff --git a/browserid/static/i/tutorial_3.png b/resources/static/i/tutorial_3.png
similarity index 100%
rename from browserid/static/i/tutorial_3.png
rename to resources/static/i/tutorial_3.png
diff --git a/browserid/static/include.js b/resources/static/include.js
similarity index 93%
rename from browserid/static/include.js
rename to resources/static/include.js
index 9f16d5d47007e557a9c7987df3a98f23471be0fb..cebeafc44cb225688d5ed9b36a062a33f9492e5f 100644
--- a/browserid/static/include.js
+++ b/resources/static/include.js
@@ -557,55 +557,88 @@
     };
   })();
 
-  function getInternetExplorerVersion() {
-    var rv = -1; // Return value assumes failure.
-    if (navigator.appName == 'Microsoft Internet Explorer') {
-      var ua = navigator.userAgent;
-      var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
-      if (re.exec(ua) != null)
-        rv = parseFloat(RegExp.$1);
+  var BrowserSupport = (function() {
+    var win = window,
+        nav = navigator,
+        reason;
+
+    // For unit testing
+    function setTestEnv(newNav, newWindow) {
+      nav = newNav;
+      win = newWindow;
     }
 
-    return rv;
-  }
-
-  function checkIE() {
-    var ieVersion = getInternetExplorerVersion(),
-        ieNosupport = ieVersion > -1 && ieVersion < 9,
-        message;
+    function getInternetExplorerVersion() {
+      var rv = -1; // Return value assumes failure.
+      if (nav.appName == 'Microsoft Internet Explorer') {
+        var ua = nav.userAgent;
+        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+        if (re.exec(ua) != null)
+          rv = parseFloat(RegExp.$1);
+      }
 
-    if(ieNosupport) {
-      message = "Unfortunately, your version of Internet Explorer is not yet supported.\n" +
-            'If you are using Internet Explorer 9, turn off "Compatibility View".';
+      return rv;
     }
 
-    return message;
-  }
+    function checkIE() {
+      var ieVersion = getInternetExplorerVersion(),
+          ieNosupport = ieVersion > -1 && ieVersion < 9;
 
-  function explicitNosupport() {
-    var message = checkIE();
+      if(ieNosupport) {
+        return "IE_VERSION";
+      }
+    }
 
-    if (message) {
-       message += "\nWe are working hard to bring BrowserID support to your browser!";
-       alert(message);
+    function explicitNosupport() {
+      return checkIE();
     }
 
-    return message;
-  }
+    function checkLocalStorage() {
+      var localStorage = 'localStorage' in win && win['localStorage'] !== null;
+      if(!localStorage) {
+        return "LOCALSTORAGE";
+      }
+    }
 
-  function checkRequirements() {
-    var localStorage = 'localStorage' in window && window['localStorage'] !== null;
-    var postMessage = !!window.postMessage;
-    var json = true;
+    function checkPostMessage() {
+      if(!win.postMessage) {
+        return "POSTMESSAGE";
+      }
+    }
 
-    var explicitNo = explicitNosupport()
+    function isSupported() {
+      reason = checkLocalStorage() || checkPostMessage() || explicitNosupport();
 
-    if(!explicitNo && !(localStorage && postMessage && json)) {
-      alert("Unfortunately, your browser does not meet the minimum HTML5 support required for BrowserID.");
+      return !reason;
     }
 
-    return localStorage && postMessage && json && !(explicitNo);
-  }
+    function getNoSupportReason() {
+      return reason;
+    }
+
+    return {
+      /**
+       * Set the test environment.
+       * @method setTestEnv
+       */
+      setTestEnv: setTestEnv,
+      /**
+       * Check whether the current browser is supported
+       * @method isSupported
+       * @returns {boolean}
+       */
+      isSupported: isSupported,
+      /**
+       * Called after isSupported, if isSupported returns false.  Gets the reason 
+       * why browser is not supported.
+       * @method getNoSupportReason
+       * @returns {string}
+       */
+      getNoSupportReason: getNoSupportReason
+    };
+    
+  }());
+
 
   // this is for calls that are non-interactive
   function _open_hidden_iframe(doc) {
@@ -636,16 +669,20 @@
     return iframe;
   }
   
-  function _open_window() {
+  function _open_window(url) {
+    url = url || "about:blank";
     // we open the window initially blank, and only after our relay frame has
     // been constructed do we update the location.  This is done because we
     // must launch the window inside a click handler, but we should wait to
     // start loading it until our relay iframe is instantiated and ready.
     // see issue #287 & #286
-    return window.open(
-      "about:blank",
+    var dialog = window.open(
+      url,
       "_mozid_signin",
       isFennec ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375");
+
+    dialog.focus();
+    return dialog;
   }
 
   function _attach_event(element, name, listener) {
@@ -684,16 +721,17 @@
 
     // keep track of these so that we can re-use/re-focus an already open window.
     navigator.id.getVerifiedEmail = function(callback) {
-      if(!checkRequirements()) {
-        return;
-      }
-
       if (w) {
         // if there is already a window open, just focus the old window.
         w.focus();
         return;
       }
 
+      if (!BrowserSupport.isSupported()) {
+        w = _open_window(ipServer + "/unsupported_dialog");
+        return;
+      }
+
       var frameid = _get_relayframe_id();
       var iframe = _open_relayframe("browserid_relay_" + frameid);
       w = _open_window();
@@ -712,8 +750,7 @@
           // has a problem re-attaching new iframes with the same name.  Code inside
           // of frames with the same name sometimes does not get run.
           // See https://bugzilla.mozilla.org/show_bug.cgi?id=350023
-          w.location = ipServer + "/sign_in#" + frameid;
-          w.focus();
+          w = _open_window(ipServer + "/sign_in#" + frameid);
         }
       });
 
@@ -721,8 +758,10 @@
         chan.destroy();
         chan = null;
 
-        w.close();
-        w = null;
+        if (w) {
+          w.close();
+          w = null;
+        }
 
         iframe.parentNode.removeChild(iframe);
         iframe = null;
diff --git a/browserid/static/jquery/.gitignore b/resources/static/jquery/.gitignore
similarity index 100%
rename from browserid/static/jquery/.gitignore
rename to resources/static/jquery/.gitignore
diff --git a/browserid/static/jquery/README b/resources/static/jquery/README
similarity index 100%
rename from browserid/static/jquery/README
rename to resources/static/jquery/README
diff --git a/browserid/static/jquery/build.js b/resources/static/jquery/build.js
similarity index 100%
rename from browserid/static/jquery/build.js
rename to resources/static/jquery/build.js
diff --git a/browserid/static/jquery/buildAll.js b/resources/static/jquery/buildAll.js
similarity index 100%
rename from browserid/static/jquery/buildAll.js
rename to resources/static/jquery/buildAll.js
diff --git a/browserid/static/jquery/class/class.html b/resources/static/jquery/class/class.html
similarity index 100%
rename from browserid/static/jquery/class/class.html
rename to resources/static/jquery/class/class.html
diff --git a/browserid/static/jquery/class/class.js b/resources/static/jquery/class/class.js
similarity index 100%
rename from browserid/static/jquery/class/class.js
rename to resources/static/jquery/class/class.js
diff --git a/browserid/static/jquery/class/class_test.js b/resources/static/jquery/class/class_test.js
similarity index 100%
rename from browserid/static/jquery/class/class_test.js
rename to resources/static/jquery/class/class_test.js
diff --git a/browserid/static/jquery/class/qunit.html b/resources/static/jquery/class/qunit.html
similarity index 100%
rename from browserid/static/jquery/class/qunit.html
rename to resources/static/jquery/class/qunit.html
diff --git a/browserid/static/jquery/controller/controller.html b/resources/static/jquery/controller/controller.html
similarity index 100%
rename from browserid/static/jquery/controller/controller.html
rename to resources/static/jquery/controller/controller.html
diff --git a/browserid/static/jquery/controller/controller.js b/resources/static/jquery/controller/controller.js
similarity index 100%
rename from browserid/static/jquery/controller/controller.js
rename to resources/static/jquery/controller/controller.js
diff --git a/browserid/static/jquery/controller/controller_test.js b/resources/static/jquery/controller/controller_test.js
similarity index 100%
rename from browserid/static/jquery/controller/controller_test.js
rename to resources/static/jquery/controller/controller_test.js
diff --git a/browserid/static/jquery/controller/history/history.html b/resources/static/jquery/controller/history/history.html
similarity index 100%
rename from browserid/static/jquery/controller/history/history.html
rename to resources/static/jquery/controller/history/history.html
diff --git a/browserid/static/jquery/controller/history/history.js b/resources/static/jquery/controller/history/history.js
similarity index 100%
rename from browserid/static/jquery/controller/history/history.js
rename to resources/static/jquery/controller/history/history.js
diff --git a/browserid/static/jquery/controller/history/html5/html5.js b/resources/static/jquery/controller/history/html5/html5.js
similarity index 100%
rename from browserid/static/jquery/controller/history/html5/html5.js
rename to resources/static/jquery/controller/history/html5/html5.js
diff --git a/browserid/static/jquery/controller/history/html5/qunit.html b/resources/static/jquery/controller/history/html5/qunit.html
similarity index 100%
rename from browserid/static/jquery/controller/history/html5/qunit.html
rename to resources/static/jquery/controller/history/html5/qunit.html
diff --git a/browserid/static/jquery/controller/history/html5/qunit/qunit.js b/resources/static/jquery/controller/history/html5/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/controller/history/html5/qunit/qunit.js
rename to resources/static/jquery/controller/history/html5/qunit/qunit.js
diff --git a/browserid/static/jquery/controller/history/qunit.html b/resources/static/jquery/controller/history/qunit.html
similarity index 100%
rename from browserid/static/jquery/controller/history/qunit.html
rename to resources/static/jquery/controller/history/qunit.html
diff --git a/browserid/static/jquery/controller/history/qunit/qunit.js b/resources/static/jquery/controller/history/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/controller/history/qunit/qunit.js
rename to resources/static/jquery/controller/history/qunit/qunit.js
diff --git a/browserid/static/jquery/controller/pages/document.js b/resources/static/jquery/controller/pages/document.js
similarity index 100%
rename from browserid/static/jquery/controller/pages/document.js
rename to resources/static/jquery/controller/pages/document.js
diff --git a/browserid/static/jquery/controller/pages/listening.js b/resources/static/jquery/controller/pages/listening.js
similarity index 100%
rename from browserid/static/jquery/controller/pages/listening.js
rename to resources/static/jquery/controller/pages/listening.js
diff --git a/browserid/static/jquery/controller/pages/plugin.js b/resources/static/jquery/controller/pages/plugin.js
similarity index 100%
rename from browserid/static/jquery/controller/pages/plugin.js
rename to resources/static/jquery/controller/pages/plugin.js
diff --git a/browserid/static/jquery/controller/qunit.html b/resources/static/jquery/controller/qunit.html
similarity index 100%
rename from browserid/static/jquery/controller/qunit.html
rename to resources/static/jquery/controller/qunit.html
diff --git a/browserid/static/jquery/controller/subscribe/funcunit.html b/resources/static/jquery/controller/subscribe/funcunit.html
similarity index 100%
rename from browserid/static/jquery/controller/subscribe/funcunit.html
rename to resources/static/jquery/controller/subscribe/funcunit.html
diff --git a/browserid/static/jquery/controller/subscribe/subscribe.html b/resources/static/jquery/controller/subscribe/subscribe.html
similarity index 100%
rename from browserid/static/jquery/controller/subscribe/subscribe.html
rename to resources/static/jquery/controller/subscribe/subscribe.html
diff --git a/browserid/static/jquery/controller/subscribe/subscribe.js b/resources/static/jquery/controller/subscribe/subscribe.js
similarity index 100%
rename from browserid/static/jquery/controller/subscribe/subscribe.js
rename to resources/static/jquery/controller/subscribe/subscribe.js
diff --git a/browserid/static/jquery/controller/view/qunit.html b/resources/static/jquery/controller/view/qunit.html
similarity index 100%
rename from browserid/static/jquery/controller/view/qunit.html
rename to resources/static/jquery/controller/view/qunit.html
diff --git a/browserid/static/jquery/controller/view/test/qunit/controller_view_test.js b/resources/static/jquery/controller/view/test/qunit/controller_view_test.js
similarity index 100%
rename from browserid/static/jquery/controller/view/test/qunit/controller_view_test.js
rename to resources/static/jquery/controller/view/test/qunit/controller_view_test.js
diff --git a/browserid/static/jquery/controller/view/test/qunit/qunit.js b/resources/static/jquery/controller/view/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/controller/view/test/qunit/qunit.js
rename to resources/static/jquery/controller/view/test/qunit/qunit.js
diff --git a/browserid/static/jquery/controller/view/test/qunit/views/init.micro b/resources/static/jquery/controller/view/test/qunit/views/init.micro
similarity index 100%
rename from browserid/static/jquery/controller/view/test/qunit/views/init.micro
rename to resources/static/jquery/controller/view/test/qunit/views/init.micro
diff --git a/browserid/static/jquery/controller/view/view.js b/resources/static/jquery/controller/view/view.js
similarity index 100%
rename from browserid/static/jquery/controller/view/view.js
rename to resources/static/jquery/controller/view/view.js
diff --git a/browserid/static/jquery/dom/closest/closest.js b/resources/static/jquery/dom/closest/closest.js
similarity index 100%
rename from browserid/static/jquery/dom/closest/closest.js
rename to resources/static/jquery/dom/closest/closest.js
diff --git a/browserid/static/jquery/dom/compare/compare.html b/resources/static/jquery/dom/compare/compare.html
similarity index 100%
rename from browserid/static/jquery/dom/compare/compare.html
rename to resources/static/jquery/dom/compare/compare.html
diff --git a/browserid/static/jquery/dom/compare/compare.js b/resources/static/jquery/dom/compare/compare.js
similarity index 100%
rename from browserid/static/jquery/dom/compare/compare.js
rename to resources/static/jquery/dom/compare/compare.js
diff --git a/browserid/static/jquery/dom/compare/compare_test.js b/resources/static/jquery/dom/compare/compare_test.js
similarity index 100%
rename from browserid/static/jquery/dom/compare/compare_test.js
rename to resources/static/jquery/dom/compare/compare_test.js
diff --git a/browserid/static/jquery/dom/compare/qunit.html b/resources/static/jquery/dom/compare/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/compare/qunit.html
rename to resources/static/jquery/dom/compare/qunit.html
diff --git a/browserid/static/jquery/dom/cookie/cookie.js b/resources/static/jquery/dom/cookie/cookie.js
similarity index 100%
rename from browserid/static/jquery/dom/cookie/cookie.js
rename to resources/static/jquery/dom/cookie/cookie.js
diff --git a/browserid/static/jquery/dom/cur_styles/cur_styles.html b/resources/static/jquery/dom/cur_styles/cur_styles.html
similarity index 100%
rename from browserid/static/jquery/dom/cur_styles/cur_styles.html
rename to resources/static/jquery/dom/cur_styles/cur_styles.html
diff --git a/browserid/static/jquery/dom/cur_styles/cur_styles.js b/resources/static/jquery/dom/cur_styles/cur_styles.js
similarity index 100%
rename from browserid/static/jquery/dom/cur_styles/cur_styles.js
rename to resources/static/jquery/dom/cur_styles/cur_styles.js
diff --git a/browserid/static/jquery/dom/cur_styles/cur_styles_test.js b/resources/static/jquery/dom/cur_styles/cur_styles_test.js
similarity index 100%
rename from browserid/static/jquery/dom/cur_styles/cur_styles_test.js
rename to resources/static/jquery/dom/cur_styles/cur_styles_test.js
diff --git a/browserid/static/jquery/dom/cur_styles/qunit.html b/resources/static/jquery/dom/cur_styles/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/cur_styles/qunit.html
rename to resources/static/jquery/dom/cur_styles/qunit.html
diff --git a/browserid/static/jquery/dom/cur_styles/test/curStyles.micro b/resources/static/jquery/dom/cur_styles/test/curStyles.micro
similarity index 100%
rename from browserid/static/jquery/dom/cur_styles/test/curStyles.micro
rename to resources/static/jquery/dom/cur_styles/test/curStyles.micro
diff --git a/browserid/static/jquery/dom/dimensions/dimensions.html b/resources/static/jquery/dom/dimensions/dimensions.html
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/dimensions.html
rename to resources/static/jquery/dom/dimensions/dimensions.html
diff --git a/browserid/static/jquery/dom/dimensions/dimensions.js b/resources/static/jquery/dom/dimensions/dimensions.js
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/dimensions.js
rename to resources/static/jquery/dom/dimensions/dimensions.js
diff --git a/browserid/static/jquery/dom/dimensions/qunit.html b/resources/static/jquery/dom/dimensions/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/qunit.html
rename to resources/static/jquery/dom/dimensions/qunit.html
diff --git a/browserid/static/jquery/dom/dimensions/test/qunit/curStyles.micro b/resources/static/jquery/dom/dimensions/test/qunit/curStyles.micro
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/test/qunit/curStyles.micro
rename to resources/static/jquery/dom/dimensions/test/qunit/curStyles.micro
diff --git a/browserid/static/jquery/dom/dimensions/test/qunit/dimensions_test.js b/resources/static/jquery/dom/dimensions/test/qunit/dimensions_test.js
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/test/qunit/dimensions_test.js
rename to resources/static/jquery/dom/dimensions/test/qunit/dimensions_test.js
diff --git a/browserid/static/jquery/dom/dimensions/test/qunit/outer.micro b/resources/static/jquery/dom/dimensions/test/qunit/outer.micro
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/test/qunit/outer.micro
rename to resources/static/jquery/dom/dimensions/test/qunit/outer.micro
diff --git a/browserid/static/jquery/dom/dimensions/test/qunit/qunit.js b/resources/static/jquery/dom/dimensions/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/dom/dimensions/test/qunit/qunit.js
rename to resources/static/jquery/dom/dimensions/test/qunit/qunit.js
diff --git a/browserid/static/jquery/dom/dom.js b/resources/static/jquery/dom/dom.js
similarity index 100%
rename from browserid/static/jquery/dom/dom.js
rename to resources/static/jquery/dom/dom.js
diff --git a/browserid/static/jquery/dom/fixture/fixture.html b/resources/static/jquery/dom/fixture/fixture.html
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixture.html
rename to resources/static/jquery/dom/fixture/fixture.html
diff --git a/browserid/static/jquery/dom/fixture/fixture.js b/resources/static/jquery/dom/fixture/fixture.js
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixture.js
rename to resources/static/jquery/dom/fixture/fixture.js
diff --git a/browserid/static/jquery/dom/fixture/fixture_test.js b/resources/static/jquery/dom/fixture/fixture_test.js
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixture_test.js
rename to resources/static/jquery/dom/fixture/fixture_test.js
diff --git a/browserid/static/jquery/dom/fixture/fixtures/foo.json b/resources/static/jquery/dom/fixture/fixtures/foo.json
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixtures/foo.json
rename to resources/static/jquery/dom/fixture/fixtures/foo.json
diff --git a/browserid/static/jquery/dom/fixture/fixtures/foobar.json b/resources/static/jquery/dom/fixture/fixtures/foobar.json
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixtures/foobar.json
rename to resources/static/jquery/dom/fixture/fixtures/foobar.json
diff --git a/browserid/static/jquery/dom/fixture/fixtures/messages.html b/resources/static/jquery/dom/fixture/fixtures/messages.html
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixtures/messages.html
rename to resources/static/jquery/dom/fixture/fixtures/messages.html
diff --git a/browserid/static/jquery/dom/fixture/fixtures/test.json b/resources/static/jquery/dom/fixture/fixtures/test.json
similarity index 100%
rename from browserid/static/jquery/dom/fixture/fixtures/test.json
rename to resources/static/jquery/dom/fixture/fixtures/test.json
diff --git a/browserid/static/jquery/dom/fixture/qunit.html b/resources/static/jquery/dom/fixture/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/fixture/qunit.html
rename to resources/static/jquery/dom/fixture/qunit.html
diff --git a/browserid/static/jquery/dom/form_params/form_params.html b/resources/static/jquery/dom/form_params/form_params.html
similarity index 100%
rename from browserid/static/jquery/dom/form_params/form_params.html
rename to resources/static/jquery/dom/form_params/form_params.html
diff --git a/browserid/static/jquery/dom/form_params/form_params.js b/resources/static/jquery/dom/form_params/form_params.js
similarity index 100%
rename from browserid/static/jquery/dom/form_params/form_params.js
rename to resources/static/jquery/dom/form_params/form_params.js
diff --git a/browserid/static/jquery/dom/form_params/qunit.html b/resources/static/jquery/dom/form_params/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/form_params/qunit.html
rename to resources/static/jquery/dom/form_params/qunit.html
diff --git a/browserid/static/jquery/dom/form_params/test/qunit/basics.micro b/resources/static/jquery/dom/form_params/test/qunit/basics.micro
similarity index 100%
rename from browserid/static/jquery/dom/form_params/test/qunit/basics.micro
rename to resources/static/jquery/dom/form_params/test/qunit/basics.micro
diff --git a/browserid/static/jquery/dom/form_params/test/qunit/checkbox.micro b/resources/static/jquery/dom/form_params/test/qunit/checkbox.micro
similarity index 100%
rename from browserid/static/jquery/dom/form_params/test/qunit/checkbox.micro
rename to resources/static/jquery/dom/form_params/test/qunit/checkbox.micro
diff --git a/browserid/static/jquery/dom/form_params/test/qunit/form_params_test.js b/resources/static/jquery/dom/form_params/test/qunit/form_params_test.js
similarity index 100%
rename from browserid/static/jquery/dom/form_params/test/qunit/form_params_test.js
rename to resources/static/jquery/dom/form_params/test/qunit/form_params_test.js
diff --git a/browserid/static/jquery/dom/form_params/test/qunit/qunit.js b/resources/static/jquery/dom/form_params/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/dom/form_params/test/qunit/qunit.js
rename to resources/static/jquery/dom/form_params/test/qunit/qunit.js
diff --git a/browserid/static/jquery/dom/form_params/test/qunit/truthy.micro b/resources/static/jquery/dom/form_params/test/qunit/truthy.micro
similarity index 100%
rename from browserid/static/jquery/dom/form_params/test/qunit/truthy.micro
rename to resources/static/jquery/dom/form_params/test/qunit/truthy.micro
diff --git a/browserid/static/jquery/dom/range/qunit.html b/resources/static/jquery/dom/range/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/range/qunit.html
rename to resources/static/jquery/dom/range/qunit.html
diff --git a/browserid/static/jquery/dom/range/range.html b/resources/static/jquery/dom/range/range.html
similarity index 100%
rename from browserid/static/jquery/dom/range/range.html
rename to resources/static/jquery/dom/range/range.html
diff --git a/browserid/static/jquery/dom/range/range.js b/resources/static/jquery/dom/range/range.js
similarity index 100%
rename from browserid/static/jquery/dom/range/range.js
rename to resources/static/jquery/dom/range/range.js
diff --git a/browserid/static/jquery/dom/range/range_test.js b/resources/static/jquery/dom/range/range_test.js
similarity index 100%
rename from browserid/static/jquery/dom/range/range_test.js
rename to resources/static/jquery/dom/range/range_test.js
diff --git a/browserid/static/jquery/dom/selection/qunit.html b/resources/static/jquery/dom/selection/qunit.html
similarity index 100%
rename from browserid/static/jquery/dom/selection/qunit.html
rename to resources/static/jquery/dom/selection/qunit.html
diff --git a/browserid/static/jquery/dom/selection/selection.html b/resources/static/jquery/dom/selection/selection.html
similarity index 100%
rename from browserid/static/jquery/dom/selection/selection.html
rename to resources/static/jquery/dom/selection/selection.html
diff --git a/browserid/static/jquery/dom/selection/selection.js b/resources/static/jquery/dom/selection/selection.js
similarity index 100%
rename from browserid/static/jquery/dom/selection/selection.js
rename to resources/static/jquery/dom/selection/selection.js
diff --git a/browserid/static/jquery/dom/selection/selection_test.js b/resources/static/jquery/dom/selection/selection_test.js
similarity index 100%
rename from browserid/static/jquery/dom/selection/selection_test.js
rename to resources/static/jquery/dom/selection/selection_test.js
diff --git a/browserid/static/jquery/dom/within/within.js b/resources/static/jquery/dom/within/within.js
similarity index 100%
rename from browserid/static/jquery/dom/within/within.js
rename to resources/static/jquery/dom/within/within.js
diff --git a/browserid/static/jquery/download/btn.png b/resources/static/jquery/download/btn.png
similarity index 100%
rename from browserid/static/jquery/download/btn.png
rename to resources/static/jquery/download/btn.png
diff --git a/browserid/static/jquery/download/dependencies.json b/resources/static/jquery/download/dependencies.json
similarity index 100%
rename from browserid/static/jquery/download/dependencies.json
rename to resources/static/jquery/download/dependencies.json
diff --git a/browserid/static/jquery/download/download.css b/resources/static/jquery/download/download.css
similarity index 100%
rename from browserid/static/jquery/download/download.css
rename to resources/static/jquery/download/download.css
diff --git a/browserid/static/jquery/download/download.html b/resources/static/jquery/download/download.html
similarity index 100%
rename from browserid/static/jquery/download/download.html
rename to resources/static/jquery/download/download.html
diff --git a/browserid/static/jquery/download/download.js b/resources/static/jquery/download/download.js
similarity index 100%
rename from browserid/static/jquery/download/download.js
rename to resources/static/jquery/download/download.js
diff --git a/browserid/static/jquery/download/test/controllerpage.html b/resources/static/jquery/download/test/controllerpage.html
similarity index 100%
rename from browserid/static/jquery/download/test/controllerpage.html
rename to resources/static/jquery/download/test/controllerpage.html
diff --git a/browserid/static/jquery/download/test/jquery-1.4.3.js b/resources/static/jquery/download/test/jquery-1.4.3.js
similarity index 100%
rename from browserid/static/jquery/download/test/jquery-1.4.3.js
rename to resources/static/jquery/download/test/jquery-1.4.3.js
diff --git a/browserid/static/jquery/download/test/run.js b/resources/static/jquery/download/test/run.js
similarity index 100%
rename from browserid/static/jquery/download/test/run.js
rename to resources/static/jquery/download/test/run.js
diff --git a/browserid/static/jquery/event/default/default.html b/resources/static/jquery/event/default/default.html
similarity index 100%
rename from browserid/static/jquery/event/default/default.html
rename to resources/static/jquery/event/default/default.html
diff --git a/browserid/static/jquery/event/default/default.js b/resources/static/jquery/event/default/default.js
similarity index 100%
rename from browserid/static/jquery/event/default/default.js
rename to resources/static/jquery/event/default/default.js
diff --git a/browserid/static/jquery/event/default/default_pause_test.html b/resources/static/jquery/event/default/default_pause_test.html
similarity index 100%
rename from browserid/static/jquery/event/default/default_pause_test.html
rename to resources/static/jquery/event/default/default_pause_test.html
diff --git a/browserid/static/jquery/event/default/default_pause_test.js b/resources/static/jquery/event/default/default_pause_test.js
similarity index 100%
rename from browserid/static/jquery/event/default/default_pause_test.js
rename to resources/static/jquery/event/default/default_pause_test.js
diff --git a/browserid/static/jquery/event/default/default_test.js b/resources/static/jquery/event/default/default_test.js
similarity index 100%
rename from browserid/static/jquery/event/default/default_test.js
rename to resources/static/jquery/event/default/default_test.js
diff --git a/browserid/static/jquery/event/default/defaultjquery.html b/resources/static/jquery/event/default/defaultjquery.html
similarity index 100%
rename from browserid/static/jquery/event/default/defaultjquery.html
rename to resources/static/jquery/event/default/defaultjquery.html
diff --git a/browserid/static/jquery/event/default/qunit.html b/resources/static/jquery/event/default/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/default/qunit.html
rename to resources/static/jquery/event/default/qunit.html
diff --git a/browserid/static/jquery/event/destroyed/destroyed.html b/resources/static/jquery/event/destroyed/destroyed.html
similarity index 100%
rename from browserid/static/jquery/event/destroyed/destroyed.html
rename to resources/static/jquery/event/destroyed/destroyed.html
diff --git a/browserid/static/jquery/event/destroyed/destroyed.js b/resources/static/jquery/event/destroyed/destroyed.js
similarity index 100%
rename from browserid/static/jquery/event/destroyed/destroyed.js
rename to resources/static/jquery/event/destroyed/destroyed.js
diff --git a/browserid/static/jquery/event/destroyed/destroyed_menu.html b/resources/static/jquery/event/destroyed/destroyed_menu.html
similarity index 100%
rename from browserid/static/jquery/event/destroyed/destroyed_menu.html
rename to resources/static/jquery/event/destroyed/destroyed_menu.html
diff --git a/browserid/static/jquery/event/destroyed/qunit.html b/resources/static/jquery/event/destroyed/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/destroyed/qunit.html
rename to resources/static/jquery/event/destroyed/qunit.html
diff --git a/browserid/static/jquery/event/destroyed/test/qunit/destroyed_test.js b/resources/static/jquery/event/destroyed/test/qunit/destroyed_test.js
similarity index 100%
rename from browserid/static/jquery/event/destroyed/test/qunit/destroyed_test.js
rename to resources/static/jquery/event/destroyed/test/qunit/destroyed_test.js
diff --git a/browserid/static/jquery/event/destroyed/test/qunit/qunit.js b/resources/static/jquery/event/destroyed/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/event/destroyed/test/qunit/qunit.js
rename to resources/static/jquery/event/destroyed/test/qunit/qunit.js
diff --git a/browserid/static/jquery/event/drag/drag.html b/resources/static/jquery/event/drag/drag.html
similarity index 100%
rename from browserid/static/jquery/event/drag/drag.html
rename to resources/static/jquery/event/drag/drag.html
diff --git a/browserid/static/jquery/event/drag/drag.js b/resources/static/jquery/event/drag/drag.js
similarity index 100%
rename from browserid/static/jquery/event/drag/drag.js
rename to resources/static/jquery/event/drag/drag.js
diff --git a/browserid/static/jquery/event/drag/drag_test.js b/resources/static/jquery/event/drag/drag_test.js
similarity index 100%
rename from browserid/static/jquery/event/drag/drag_test.js
rename to resources/static/jquery/event/drag/drag_test.js
diff --git a/browserid/static/jquery/event/drag/limit/limit.html b/resources/static/jquery/event/drag/limit/limit.html
similarity index 100%
rename from browserid/static/jquery/event/drag/limit/limit.html
rename to resources/static/jquery/event/drag/limit/limit.html
diff --git a/browserid/static/jquery/event/drag/limit/limit.js b/resources/static/jquery/event/drag/limit/limit.js
similarity index 100%
rename from browserid/static/jquery/event/drag/limit/limit.js
rename to resources/static/jquery/event/drag/limit/limit.js
diff --git a/browserid/static/jquery/event/drag/qunit.html b/resources/static/jquery/event/drag/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/drag/qunit.html
rename to resources/static/jquery/event/drag/qunit.html
diff --git a/browserid/static/jquery/event/drag/scroll/scroll.js b/resources/static/jquery/event/drag/scroll/scroll.js
similarity index 100%
rename from browserid/static/jquery/event/drag/scroll/scroll.js
rename to resources/static/jquery/event/drag/scroll/scroll.js
diff --git a/browserid/static/jquery/event/drag/step/step.html b/resources/static/jquery/event/drag/step/step.html
similarity index 100%
rename from browserid/static/jquery/event/drag/step/step.html
rename to resources/static/jquery/event/drag/step/step.html
diff --git a/browserid/static/jquery/event/drag/step/step.js b/resources/static/jquery/event/drag/step/step.js
similarity index 100%
rename from browserid/static/jquery/event/drag/step/step.js
rename to resources/static/jquery/event/drag/step/step.js
diff --git a/browserid/static/jquery/event/drop/drop.html b/resources/static/jquery/event/drop/drop.html
similarity index 100%
rename from browserid/static/jquery/event/drop/drop.html
rename to resources/static/jquery/event/drop/drop.html
diff --git a/browserid/static/jquery/event/drop/drop.js b/resources/static/jquery/event/drop/drop.js
similarity index 100%
rename from browserid/static/jquery/event/drop/drop.js
rename to resources/static/jquery/event/drop/drop.js
diff --git a/browserid/static/jquery/event/drop/drop_test.js b/resources/static/jquery/event/drop/drop_test.js
similarity index 100%
rename from browserid/static/jquery/event/drop/drop_test.js
rename to resources/static/jquery/event/drop/drop_test.js
diff --git a/browserid/static/jquery/event/event.js b/resources/static/jquery/event/event.js
similarity index 100%
rename from browserid/static/jquery/event/event.js
rename to resources/static/jquery/event/event.js
diff --git a/browserid/static/jquery/event/handle/handle.js b/resources/static/jquery/event/handle/handle.js
similarity index 100%
rename from browserid/static/jquery/event/handle/handle.js
rename to resources/static/jquery/event/handle/handle.js
diff --git a/browserid/static/jquery/event/hashchange/hashchange.js b/resources/static/jquery/event/hashchange/hashchange.js
similarity index 100%
rename from browserid/static/jquery/event/hashchange/hashchange.js
rename to resources/static/jquery/event/hashchange/hashchange.js
diff --git a/browserid/static/jquery/event/hover/hover.html b/resources/static/jquery/event/hover/hover.html
similarity index 100%
rename from browserid/static/jquery/event/hover/hover.html
rename to resources/static/jquery/event/hover/hover.html
diff --git a/browserid/static/jquery/event/hover/hover.js b/resources/static/jquery/event/hover/hover.js
similarity index 100%
rename from browserid/static/jquery/event/hover/hover.js
rename to resources/static/jquery/event/hover/hover.js
diff --git a/browserid/static/jquery/event/hover/qunit.html b/resources/static/jquery/event/hover/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/hover/qunit.html
rename to resources/static/jquery/event/hover/qunit.html
diff --git a/browserid/static/jquery/event/hover/test/qunit/hover_test.js b/resources/static/jquery/event/hover/test/qunit/hover_test.js
similarity index 100%
rename from browserid/static/jquery/event/hover/test/qunit/hover_test.js
rename to resources/static/jquery/event/hover/test/qunit/hover_test.js
diff --git a/browserid/static/jquery/event/hover/test/qunit/qunit.js b/resources/static/jquery/event/hover/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/event/hover/test/qunit/qunit.js
rename to resources/static/jquery/event/hover/test/qunit/qunit.js
diff --git a/browserid/static/jquery/event/key/key.html b/resources/static/jquery/event/key/key.html
similarity index 100%
rename from browserid/static/jquery/event/key/key.html
rename to resources/static/jquery/event/key/key.html
diff --git a/browserid/static/jquery/event/key/key.js b/resources/static/jquery/event/key/key.js
similarity index 100%
rename from browserid/static/jquery/event/key/key.js
rename to resources/static/jquery/event/key/key.js
diff --git a/browserid/static/jquery/event/key/key_test.js b/resources/static/jquery/event/key/key_test.js
similarity index 100%
rename from browserid/static/jquery/event/key/key_test.js
rename to resources/static/jquery/event/key/key_test.js
diff --git a/browserid/static/jquery/event/key/qunit.html b/resources/static/jquery/event/key/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/key/qunit.html
rename to resources/static/jquery/event/key/qunit.html
diff --git a/browserid/static/jquery/event/livehack/livehack.js b/resources/static/jquery/event/livehack/livehack.js
similarity index 100%
rename from browserid/static/jquery/event/livehack/livehack.js
rename to resources/static/jquery/event/livehack/livehack.js
diff --git a/browserid/static/jquery/event/pause/pause.html b/resources/static/jquery/event/pause/pause.html
similarity index 100%
rename from browserid/static/jquery/event/pause/pause.html
rename to resources/static/jquery/event/pause/pause.html
diff --git a/browserid/static/jquery/event/pause/pause.js b/resources/static/jquery/event/pause/pause.js
similarity index 100%
rename from browserid/static/jquery/event/pause/pause.js
rename to resources/static/jquery/event/pause/pause.js
diff --git a/browserid/static/jquery/event/pause/pause_test.js b/resources/static/jquery/event/pause/pause_test.js
similarity index 100%
rename from browserid/static/jquery/event/pause/pause_test.js
rename to resources/static/jquery/event/pause/pause_test.js
diff --git a/browserid/static/jquery/event/pause/qunit.html b/resources/static/jquery/event/pause/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/pause/qunit.html
rename to resources/static/jquery/event/pause/qunit.html
diff --git a/browserid/static/jquery/event/resize/demo.html b/resources/static/jquery/event/resize/demo.html
similarity index 100%
rename from browserid/static/jquery/event/resize/demo.html
rename to resources/static/jquery/event/resize/demo.html
diff --git a/browserid/static/jquery/event/resize/qunit.html b/resources/static/jquery/event/resize/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/resize/qunit.html
rename to resources/static/jquery/event/resize/qunit.html
diff --git a/browserid/static/jquery/event/resize/resize.html b/resources/static/jquery/event/resize/resize.html
similarity index 100%
rename from browserid/static/jquery/event/resize/resize.html
rename to resources/static/jquery/event/resize/resize.html
diff --git a/browserid/static/jquery/event/resize/resize.js b/resources/static/jquery/event/resize/resize.js
similarity index 100%
rename from browserid/static/jquery/event/resize/resize.js
rename to resources/static/jquery/event/resize/resize.js
diff --git a/browserid/static/jquery/event/resize/resize_test.js b/resources/static/jquery/event/resize/resize_test.js
similarity index 100%
rename from browserid/static/jquery/event/resize/resize_test.js
rename to resources/static/jquery/event/resize/resize_test.js
diff --git a/browserid/static/jquery/event/selection/qunit.html b/resources/static/jquery/event/selection/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/selection/qunit.html
rename to resources/static/jquery/event/selection/qunit.html
diff --git a/browserid/static/jquery/event/selection/selection.html b/resources/static/jquery/event/selection/selection.html
similarity index 100%
rename from browserid/static/jquery/event/selection/selection.html
rename to resources/static/jquery/event/selection/selection.html
diff --git a/browserid/static/jquery/event/selection/selection.js b/resources/static/jquery/event/selection/selection.js
similarity index 100%
rename from browserid/static/jquery/event/selection/selection.js
rename to resources/static/jquery/event/selection/selection.js
diff --git a/browserid/static/jquery/event/selection/selection_test.js b/resources/static/jquery/event/selection/selection_test.js
similarity index 100%
rename from browserid/static/jquery/event/selection/selection_test.js
rename to resources/static/jquery/event/selection/selection_test.js
diff --git a/browserid/static/jquery/event/swipe/qunit.html b/resources/static/jquery/event/swipe/qunit.html
similarity index 100%
rename from browserid/static/jquery/event/swipe/qunit.html
rename to resources/static/jquery/event/swipe/qunit.html
diff --git a/browserid/static/jquery/event/swipe/swipe.html b/resources/static/jquery/event/swipe/swipe.html
similarity index 100%
rename from browserid/static/jquery/event/swipe/swipe.html
rename to resources/static/jquery/event/swipe/swipe.html
diff --git a/browserid/static/jquery/event/swipe/swipe.js b/resources/static/jquery/event/swipe/swipe.js
similarity index 100%
rename from browserid/static/jquery/event/swipe/swipe.js
rename to resources/static/jquery/event/swipe/swipe.js
diff --git a/browserid/static/jquery/event/swipe/swipe_test.js b/resources/static/jquery/event/swipe/swipe_test.js
similarity index 100%
rename from browserid/static/jquery/event/swipe/swipe_test.js
rename to resources/static/jquery/event/swipe/swipe_test.js
diff --git a/browserid/static/jquery/generate/app b/resources/static/jquery/generate/app
similarity index 100%
rename from browserid/static/jquery/generate/app
rename to resources/static/jquery/generate/app
diff --git a/browserid/static/jquery/generate/controller b/resources/static/jquery/generate/controller
similarity index 100%
rename from browserid/static/jquery/generate/controller
rename to resources/static/jquery/generate/controller
diff --git a/browserid/static/jquery/generate/funcunit b/resources/static/jquery/generate/funcunit
similarity index 100%
rename from browserid/static/jquery/generate/funcunit
rename to resources/static/jquery/generate/funcunit
diff --git a/browserid/static/jquery/generate/model b/resources/static/jquery/generate/model
similarity index 100%
rename from browserid/static/jquery/generate/model
rename to resources/static/jquery/generate/model
diff --git a/browserid/static/jquery/generate/page b/resources/static/jquery/generate/page
similarity index 100%
rename from browserid/static/jquery/generate/page
rename to resources/static/jquery/generate/page
diff --git a/browserid/static/jquery/generate/plugin b/resources/static/jquery/generate/plugin
similarity index 100%
rename from browserid/static/jquery/generate/plugin
rename to resources/static/jquery/generate/plugin
diff --git a/browserid/static/jquery/generate/scaffold b/resources/static/jquery/generate/scaffold
similarity index 100%
rename from browserid/static/jquery/generate/scaffold
rename to resources/static/jquery/generate/scaffold
diff --git a/browserid/static/jquery/generate/templates/app/(application_name).css.ejs b/resources/static/jquery/generate/templates/app/(application_name).css.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/(application_name).css.ejs
rename to resources/static/jquery/generate/templates/app/(application_name).css.ejs
diff --git a/browserid/static/jquery/generate/templates/app/(application_name).html.ejs b/resources/static/jquery/generate/templates/app/(application_name).html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/(application_name).html.ejs
rename to resources/static/jquery/generate/templates/app/(application_name).html.ejs
diff --git a/browserid/static/jquery/generate/templates/app/(application_name).js.ejs b/resources/static/jquery/generate/templates/app/(application_name).js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/(application_name).js.ejs
rename to resources/static/jquery/generate/templates/app/(application_name).js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/controllers/.ignore b/resources/static/jquery/generate/templates/app/controllers/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/controllers/.ignore
rename to resources/static/jquery/generate/templates/app/controllers/.ignore
diff --git a/browserid/static/jquery/generate/templates/app/docs/.ignore b/resources/static/jquery/generate/templates/app/docs/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/docs/.ignore
rename to resources/static/jquery/generate/templates/app/docs/.ignore
diff --git a/browserid/static/jquery/generate/templates/app/fixtures/.ignore b/resources/static/jquery/generate/templates/app/fixtures/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/fixtures/.ignore
rename to resources/static/jquery/generate/templates/app/fixtures/.ignore
diff --git a/browserid/static/jquery/generate/templates/app/funcunit.html.ejs b/resources/static/jquery/generate/templates/app/funcunit.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/funcunit.html.ejs
rename to resources/static/jquery/generate/templates/app/funcunit.html.ejs
diff --git a/browserid/static/jquery/generate/templates/app/models/.ignore b/resources/static/jquery/generate/templates/app/models/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/models/.ignore
rename to resources/static/jquery/generate/templates/app/models/.ignore
diff --git a/browserid/static/jquery/generate/templates/app/qunit.html.ejs b/resources/static/jquery/generate/templates/app/qunit.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/qunit.html.ejs
rename to resources/static/jquery/generate/templates/app/qunit.html.ejs
diff --git a/browserid/static/jquery/generate/templates/app/resources/.ignore b/resources/static/jquery/generate/templates/app/resources/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/resources/.ignore
rename to resources/static/jquery/generate/templates/app/resources/.ignore
diff --git a/browserid/static/jquery/generate/templates/app/scripts/build.html.ejs b/resources/static/jquery/generate/templates/app/scripts/build.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/scripts/build.html.ejs
rename to resources/static/jquery/generate/templates/app/scripts/build.html.ejs
diff --git a/browserid/static/jquery/generate/templates/app/scripts/build.js.ejs b/resources/static/jquery/generate/templates/app/scripts/build.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/scripts/build.js.ejs
rename to resources/static/jquery/generate/templates/app/scripts/build.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/scripts/clean.js.ejs b/resources/static/jquery/generate/templates/app/scripts/clean.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/scripts/clean.js.ejs
rename to resources/static/jquery/generate/templates/app/scripts/clean.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/scripts/docs.js.ejs b/resources/static/jquery/generate/templates/app/scripts/docs.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/scripts/docs.js.ejs
rename to resources/static/jquery/generate/templates/app/scripts/docs.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/test/funcunit/(application_name)_test.js.ejs b/resources/static/jquery/generate/templates/app/test/funcunit/(application_name)_test.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/test/funcunit/(application_name)_test.js.ejs
rename to resources/static/jquery/generate/templates/app/test/funcunit/(application_name)_test.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/test/funcunit/funcunit.js.ejs b/resources/static/jquery/generate/templates/app/test/funcunit/funcunit.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/test/funcunit/funcunit.js.ejs
rename to resources/static/jquery/generate/templates/app/test/funcunit/funcunit.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/test/qunit/(application_name)_test.js.ejs b/resources/static/jquery/generate/templates/app/test/qunit/(application_name)_test.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/test/qunit/(application_name)_test.js.ejs
rename to resources/static/jquery/generate/templates/app/test/qunit/(application_name)_test.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/test/qunit/qunit.js.ejs b/resources/static/jquery/generate/templates/app/test/qunit/qunit.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/test/qunit/qunit.js.ejs
rename to resources/static/jquery/generate/templates/app/test/qunit/qunit.js.ejs
diff --git a/browserid/static/jquery/generate/templates/app/views/.ignore b/resources/static/jquery/generate/templates/app/views/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/app/views/.ignore
rename to resources/static/jquery/generate/templates/app/views/.ignore
diff --git a/browserid/static/jquery/generate/templates/controller/(underscore).html.ejs b/resources/static/jquery/generate/templates/controller/(underscore).html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/controller/(underscore).html.ejs
rename to resources/static/jquery/generate/templates/controller/(underscore).html.ejs
diff --git a/browserid/static/jquery/generate/templates/controller/(underscore).js.ejs b/resources/static/jquery/generate/templates/controller/(underscore).js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/controller/(underscore).js.ejs
rename to resources/static/jquery/generate/templates/controller/(underscore).js.ejs
diff --git a/browserid/static/jquery/generate/templates/controller/(underscore)_test.js.ejs b/resources/static/jquery/generate/templates/controller/(underscore)_test.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/controller/(underscore)_test.js.ejs
rename to resources/static/jquery/generate/templates/controller/(underscore)_test.js.ejs
diff --git a/browserid/static/jquery/generate/templates/controller/funcunit.html.ejs b/resources/static/jquery/generate/templates/controller/funcunit.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/controller/funcunit.html.ejs
rename to resources/static/jquery/generate/templates/controller/funcunit.html.ejs
diff --git a/browserid/static/jquery/generate/templates/controller/views/.ignore b/resources/static/jquery/generate/templates/controller/views/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/controller/views/.ignore
rename to resources/static/jquery/generate/templates/controller/views/.ignore
diff --git a/browserid/static/jquery/generate/templates/model/fixtures.link b/resources/static/jquery/generate/templates/model/fixtures.link
similarity index 100%
rename from browserid/static/jquery/generate/templates/model/fixtures.link
rename to resources/static/jquery/generate/templates/model/fixtures.link
diff --git a/browserid/static/jquery/generate/templates/model/models.link b/resources/static/jquery/generate/templates/model/models.link
similarity index 100%
rename from browserid/static/jquery/generate/templates/model/models.link
rename to resources/static/jquery/generate/templates/model/models.link
diff --git a/browserid/static/jquery/generate/templates/model/test/qunit.link b/resources/static/jquery/generate/templates/model/test/qunit.link
similarity index 100%
rename from browserid/static/jquery/generate/templates/model/test/qunit.link
rename to resources/static/jquery/generate/templates/model/test/qunit.link
diff --git a/browserid/static/jquery/generate/templates/page.ejs b/resources/static/jquery/generate/templates/page.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/page.ejs
rename to resources/static/jquery/generate/templates/page.ejs
diff --git a/browserid/static/jquery/generate/templates/plugin/(application_name).html.ejs b/resources/static/jquery/generate/templates/plugin/(application_name).html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/(application_name).html.ejs
rename to resources/static/jquery/generate/templates/plugin/(application_name).html.ejs
diff --git a/browserid/static/jquery/generate/templates/plugin/(application_name).js.ejs b/resources/static/jquery/generate/templates/plugin/(application_name).js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/(application_name).js.ejs
rename to resources/static/jquery/generate/templates/plugin/(application_name).js.ejs
diff --git a/browserid/static/jquery/generate/templates/plugin/docs/.gitignore b/resources/static/jquery/generate/templates/plugin/docs/.gitignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/docs/.gitignore
rename to resources/static/jquery/generate/templates/plugin/docs/.gitignore
diff --git a/browserid/static/jquery/generate/templates/plugin/fixtures/.ignore b/resources/static/jquery/generate/templates/plugin/fixtures/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/fixtures/.ignore
rename to resources/static/jquery/generate/templates/plugin/fixtures/.ignore
diff --git a/browserid/static/jquery/generate/templates/plugin/funcunit.html.ejs b/resources/static/jquery/generate/templates/plugin/funcunit.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/funcunit.html.ejs
rename to resources/static/jquery/generate/templates/plugin/funcunit.html.ejs
diff --git a/browserid/static/jquery/generate/templates/plugin/qunit.html.ejs b/resources/static/jquery/generate/templates/plugin/qunit.html.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/qunit.html.ejs
rename to resources/static/jquery/generate/templates/plugin/qunit.html.ejs
diff --git a/browserid/static/jquery/generate/templates/plugin/resources/.ignore b/resources/static/jquery/generate/templates/plugin/resources/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/resources/.ignore
rename to resources/static/jquery/generate/templates/plugin/resources/.ignore
diff --git a/browserid/static/jquery/generate/templates/plugin/scripts.link b/resources/static/jquery/generate/templates/plugin/scripts.link
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/scripts.link
rename to resources/static/jquery/generate/templates/plugin/scripts.link
diff --git a/browserid/static/jquery/generate/templates/plugin/test.link b/resources/static/jquery/generate/templates/plugin/test.link
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/test.link
rename to resources/static/jquery/generate/templates/plugin/test.link
diff --git a/browserid/static/jquery/generate/templates/plugin/views/.ignore b/resources/static/jquery/generate/templates/plugin/views/.ignore
similarity index 100%
rename from browserid/static/jquery/generate/templates/plugin/views/.ignore
rename to resources/static/jquery/generate/templates/plugin/views/.ignore
diff --git a/browserid/static/jquery/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs b/resources/static/jquery/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs
rename to resources/static/jquery/generate/templates/scaffold/controllers/(underscore)_controller.js.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/fixtures/(plural).json.get.ejs b/resources/static/jquery/generate/templates/scaffold/fixtures/(plural).json.get.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/fixtures/(plural).json.get.ejs
rename to resources/static/jquery/generate/templates/scaffold/fixtures/(plural).json.get.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/models/(underscore).js.ejs b/resources/static/jquery/generate/templates/scaffold/models/(underscore).js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/models/(underscore).js.ejs
rename to resources/static/jquery/generate/templates/scaffold/models/(underscore).js.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs b/resources/static/jquery/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs
rename to resources/static/jquery/generate/templates/scaffold/test/funcunit/(underscore)_controller_test.js.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs b/resources/static/jquery/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs
rename to resources/static/jquery/generate/templates/scaffold/test/qunit/(underscore)_test.js.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs b/resources/static/jquery/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs
rename to resources/static/jquery/generate/templates/scaffold/views/(underscore)/edit.ejs.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/views/(underscore)/init.ejs.ejs b/resources/static/jquery/generate/templates/scaffold/views/(underscore)/init.ejs.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/views/(underscore)/init.ejs.ejs
rename to resources/static/jquery/generate/templates/scaffold/views/(underscore)/init.ejs.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/views/(underscore)/list.ejs.ejs b/resources/static/jquery/generate/templates/scaffold/views/(underscore)/list.ejs.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/views/(underscore)/list.ejs.ejs
rename to resources/static/jquery/generate/templates/scaffold/views/(underscore)/list.ejs.ejs
diff --git a/browserid/static/jquery/generate/templates/scaffold/views/(underscore)/show.ejs.ejs b/resources/static/jquery/generate/templates/scaffold/views/(underscore)/show.ejs.ejs
similarity index 100%
rename from browserid/static/jquery/generate/templates/scaffold/views/(underscore)/show.ejs.ejs
rename to resources/static/jquery/generate/templates/scaffold/views/(underscore)/show.ejs.ejs
diff --git a/browserid/static/jquery/generate/test/app_plugin_model_controller.js b/resources/static/jquery/generate/test/app_plugin_model_controller.js
similarity index 100%
rename from browserid/static/jquery/generate/test/app_plugin_model_controller.js
rename to resources/static/jquery/generate/test/app_plugin_model_controller.js
diff --git a/browserid/static/jquery/generate/test/run.js b/resources/static/jquery/generate/test/run.js
similarity index 100%
rename from browserid/static/jquery/generate/test/run.js
rename to resources/static/jquery/generate/test/run.js
diff --git a/browserid/static/jquery/generate/test/scaffold.js b/resources/static/jquery/generate/test/scaffold.js
similarity index 100%
rename from browserid/static/jquery/generate/test/scaffold.js
rename to resources/static/jquery/generate/test/scaffold.js
diff --git a/browserid/static/jquery/jquery.js b/resources/static/jquery/jquery.js
similarity index 100%
rename from browserid/static/jquery/jquery.js
rename to resources/static/jquery/jquery.js
diff --git a/browserid/static/jquery/lang/deparam/deparam.js b/resources/static/jquery/lang/deparam/deparam.js
similarity index 100%
rename from browserid/static/jquery/lang/deparam/deparam.js
rename to resources/static/jquery/lang/deparam/deparam.js
diff --git a/browserid/static/jquery/lang/deparam/deparam_test.js b/resources/static/jquery/lang/deparam/deparam_test.js
similarity index 100%
rename from browserid/static/jquery/lang/deparam/deparam_test.js
rename to resources/static/jquery/lang/deparam/deparam_test.js
diff --git a/browserid/static/jquery/lang/deparam/qunit.html b/resources/static/jquery/lang/deparam/qunit.html
similarity index 100%
rename from browserid/static/jquery/lang/deparam/qunit.html
rename to resources/static/jquery/lang/deparam/qunit.html
diff --git a/browserid/static/jquery/lang/json/json.js b/resources/static/jquery/lang/json/json.js
similarity index 100%
rename from browserid/static/jquery/lang/json/json.js
rename to resources/static/jquery/lang/json/json.js
diff --git a/browserid/static/jquery/lang/lang.html b/resources/static/jquery/lang/lang.html
similarity index 100%
rename from browserid/static/jquery/lang/lang.html
rename to resources/static/jquery/lang/lang.html
diff --git a/browserid/static/jquery/lang/lang.js b/resources/static/jquery/lang/lang.js
similarity index 100%
rename from browserid/static/jquery/lang/lang.js
rename to resources/static/jquery/lang/lang.js
diff --git a/browserid/static/jquery/lang/lang_test.js b/resources/static/jquery/lang/lang_test.js
similarity index 100%
rename from browserid/static/jquery/lang/lang_test.js
rename to resources/static/jquery/lang/lang_test.js
diff --git a/browserid/static/jquery/lang/openajax/openajax.html b/resources/static/jquery/lang/openajax/openajax.html
similarity index 100%
rename from browserid/static/jquery/lang/openajax/openajax.html
rename to resources/static/jquery/lang/openajax/openajax.html
diff --git a/browserid/static/jquery/lang/openajax/openajax.js b/resources/static/jquery/lang/openajax/openajax.js
similarity index 100%
rename from browserid/static/jquery/lang/openajax/openajax.js
rename to resources/static/jquery/lang/openajax/openajax.js
diff --git a/browserid/static/jquery/lang/qunit.html b/resources/static/jquery/lang/qunit.html
similarity index 100%
rename from browserid/static/jquery/lang/qunit.html
rename to resources/static/jquery/lang/qunit.html
diff --git a/browserid/static/jquery/lang/rsplit/rsplit.js b/resources/static/jquery/lang/rsplit/rsplit.js
similarity index 100%
rename from browserid/static/jquery/lang/rsplit/rsplit.js
rename to resources/static/jquery/lang/rsplit/rsplit.js
diff --git a/browserid/static/jquery/lang/vector/vector.js b/resources/static/jquery/lang/vector/vector.js
similarity index 100%
rename from browserid/static/jquery/lang/vector/vector.js
rename to resources/static/jquery/lang/vector/vector.js
diff --git a/browserid/static/jquery/model/associations/associations.html b/resources/static/jquery/model/associations/associations.html
similarity index 100%
rename from browserid/static/jquery/model/associations/associations.html
rename to resources/static/jquery/model/associations/associations.html
diff --git a/browserid/static/jquery/model/associations/associations.js b/resources/static/jquery/model/associations/associations.js
similarity index 100%
rename from browserid/static/jquery/model/associations/associations.js
rename to resources/static/jquery/model/associations/associations.js
diff --git a/browserid/static/jquery/model/associations/qunit.html b/resources/static/jquery/model/associations/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/associations/qunit.html
rename to resources/static/jquery/model/associations/qunit.html
diff --git a/browserid/static/jquery/model/associations/test/qunit/associations_test.js b/resources/static/jquery/model/associations/test/qunit/associations_test.js
similarity index 100%
rename from browserid/static/jquery/model/associations/test/qunit/associations_test.js
rename to resources/static/jquery/model/associations/test/qunit/associations_test.js
diff --git a/browserid/static/jquery/model/associations/test/qunit/qunit.js b/resources/static/jquery/model/associations/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/model/associations/test/qunit/qunit.js
rename to resources/static/jquery/model/associations/test/qunit/qunit.js
diff --git a/browserid/static/jquery/model/backup/backup.html b/resources/static/jquery/model/backup/backup.html
similarity index 100%
rename from browserid/static/jquery/model/backup/backup.html
rename to resources/static/jquery/model/backup/backup.html
diff --git a/browserid/static/jquery/model/backup/backup.js b/resources/static/jquery/model/backup/backup.js
similarity index 100%
rename from browserid/static/jquery/model/backup/backup.js
rename to resources/static/jquery/model/backup/backup.js
diff --git a/browserid/static/jquery/model/backup/qunit.html b/resources/static/jquery/model/backup/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/backup/qunit.html
rename to resources/static/jquery/model/backup/qunit.html
diff --git a/browserid/static/jquery/model/backup/qunit/qunit.js b/resources/static/jquery/model/backup/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/model/backup/qunit/qunit.js
rename to resources/static/jquery/model/backup/qunit/qunit.js
diff --git a/browserid/static/jquery/model/demo-convert.html b/resources/static/jquery/model/demo-convert.html
similarity index 100%
rename from browserid/static/jquery/model/demo-convert.html
rename to resources/static/jquery/model/demo-convert.html
diff --git a/browserid/static/jquery/model/demo-dom.html b/resources/static/jquery/model/demo-dom.html
similarity index 100%
rename from browserid/static/jquery/model/demo-dom.html
rename to resources/static/jquery/model/demo-dom.html
diff --git a/browserid/static/jquery/model/demo-encapsulate.html b/resources/static/jquery/model/demo-encapsulate.html
similarity index 100%
rename from browserid/static/jquery/model/demo-encapsulate.html
rename to resources/static/jquery/model/demo-encapsulate.html
diff --git a/browserid/static/jquery/model/demo-events.html b/resources/static/jquery/model/demo-events.html
similarity index 100%
rename from browserid/static/jquery/model/demo-events.html
rename to resources/static/jquery/model/demo-events.html
diff --git a/browserid/static/jquery/model/demo-setter.html b/resources/static/jquery/model/demo-setter.html
similarity index 100%
rename from browserid/static/jquery/model/demo-setter.html
rename to resources/static/jquery/model/demo-setter.html
diff --git a/browserid/static/jquery/model/fixtures/school.json b/resources/static/jquery/model/fixtures/school.json
similarity index 100%
rename from browserid/static/jquery/model/fixtures/school.json
rename to resources/static/jquery/model/fixtures/school.json
diff --git a/browserid/static/jquery/model/fixtures/schools.json b/resources/static/jquery/model/fixtures/schools.json
similarity index 100%
rename from browserid/static/jquery/model/fixtures/schools.json
rename to resources/static/jquery/model/fixtures/schools.json
diff --git a/browserid/static/jquery/model/guesstype/guesstype.js b/resources/static/jquery/model/guesstype/guesstype.js
similarity index 100%
rename from browserid/static/jquery/model/guesstype/guesstype.js
rename to resources/static/jquery/model/guesstype/guesstype.js
diff --git a/browserid/static/jquery/model/guesstype/guesstype_test.js b/resources/static/jquery/model/guesstype/guesstype_test.js
similarity index 100%
rename from browserid/static/jquery/model/guesstype/guesstype_test.js
rename to resources/static/jquery/model/guesstype/guesstype_test.js
diff --git a/browserid/static/jquery/model/list/cookie/cookie.html b/resources/static/jquery/model/list/cookie/cookie.html
similarity index 100%
rename from browserid/static/jquery/model/list/cookie/cookie.html
rename to resources/static/jquery/model/list/cookie/cookie.html
diff --git a/browserid/static/jquery/model/list/cookie/cookie.js b/resources/static/jquery/model/list/cookie/cookie.js
similarity index 100%
rename from browserid/static/jquery/model/list/cookie/cookie.js
rename to resources/static/jquery/model/list/cookie/cookie.js
diff --git a/browserid/static/jquery/model/list/cookie/qunit.html b/resources/static/jquery/model/list/cookie/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/list/cookie/qunit.html
rename to resources/static/jquery/model/list/cookie/qunit.html
diff --git a/browserid/static/jquery/model/list/cookie/qunit/qunit.js b/resources/static/jquery/model/list/cookie/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/model/list/cookie/qunit/qunit.js
rename to resources/static/jquery/model/list/cookie/qunit/qunit.js
diff --git a/browserid/static/jquery/model/list/list-insert.html b/resources/static/jquery/model/list/list-insert.html
similarity index 100%
rename from browserid/static/jquery/model/list/list-insert.html
rename to resources/static/jquery/model/list/list-insert.html
diff --git a/browserid/static/jquery/model/list/list.html b/resources/static/jquery/model/list/list.html
similarity index 100%
rename from browserid/static/jquery/model/list/list.html
rename to resources/static/jquery/model/list/list.html
diff --git a/browserid/static/jquery/model/list/list.js b/resources/static/jquery/model/list/list.js
similarity index 100%
rename from browserid/static/jquery/model/list/list.js
rename to resources/static/jquery/model/list/list.js
diff --git a/browserid/static/jquery/model/list/local/local.js b/resources/static/jquery/model/list/local/local.js
similarity index 100%
rename from browserid/static/jquery/model/list/local/local.js
rename to resources/static/jquery/model/list/local/local.js
diff --git a/browserid/static/jquery/model/list/memory.html b/resources/static/jquery/model/list/memory.html
similarity index 100%
rename from browserid/static/jquery/model/list/memory.html
rename to resources/static/jquery/model/list/memory.html
diff --git a/browserid/static/jquery/model/list/qunit.html b/resources/static/jquery/model/list/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/list/qunit.html
rename to resources/static/jquery/model/list/qunit.html
diff --git a/browserid/static/jquery/model/list/test/qunit/list_test.js b/resources/static/jquery/model/list/test/qunit/list_test.js
similarity index 100%
rename from browserid/static/jquery/model/list/test/qunit/list_test.js
rename to resources/static/jquery/model/list/test/qunit/list_test.js
diff --git a/browserid/static/jquery/model/list/test/qunit/qunit.js b/resources/static/jquery/model/list/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/model/list/test/qunit/qunit.js
rename to resources/static/jquery/model/list/test/qunit/qunit.js
diff --git a/browserid/static/jquery/model/model.js b/resources/static/jquery/model/model.js
similarity index 100%
rename from browserid/static/jquery/model/model.js
rename to resources/static/jquery/model/model.js
diff --git a/browserid/static/jquery/model/modelBinder.html b/resources/static/jquery/model/modelBinder.html
similarity index 100%
rename from browserid/static/jquery/model/modelBinder.html
rename to resources/static/jquery/model/modelBinder.html
diff --git a/browserid/static/jquery/model/pages/deferreds.js b/resources/static/jquery/model/pages/deferreds.js
similarity index 100%
rename from browserid/static/jquery/model/pages/deferreds.js
rename to resources/static/jquery/model/pages/deferreds.js
diff --git a/browserid/static/jquery/model/pages/encapsulate.js b/resources/static/jquery/model/pages/encapsulate.js
similarity index 100%
rename from browserid/static/jquery/model/pages/encapsulate.js
rename to resources/static/jquery/model/pages/encapsulate.js
diff --git a/browserid/static/jquery/model/pages/events.js b/resources/static/jquery/model/pages/events.js
similarity index 100%
rename from browserid/static/jquery/model/pages/events.js
rename to resources/static/jquery/model/pages/events.js
diff --git a/browserid/static/jquery/model/pages/typeconversion.js b/resources/static/jquery/model/pages/typeconversion.js
similarity index 100%
rename from browserid/static/jquery/model/pages/typeconversion.js
rename to resources/static/jquery/model/pages/typeconversion.js
diff --git a/browserid/static/jquery/model/qunit.html b/resources/static/jquery/model/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/qunit.html
rename to resources/static/jquery/model/qunit.html
diff --git a/browserid/static/jquery/model/service/json_rest/json_rest.js b/resources/static/jquery/model/service/json_rest/json_rest.js
similarity index 100%
rename from browserid/static/jquery/model/service/json_rest/json_rest.js
rename to resources/static/jquery/model/service/json_rest/json_rest.js
diff --git a/browserid/static/jquery/model/service/service.js b/resources/static/jquery/model/service/service.js
similarity index 100%
rename from browserid/static/jquery/model/service/service.js
rename to resources/static/jquery/model/service/service.js
diff --git a/browserid/static/jquery/model/service/twitter/twitter.html b/resources/static/jquery/model/service/twitter/twitter.html
similarity index 100%
rename from browserid/static/jquery/model/service/twitter/twitter.html
rename to resources/static/jquery/model/service/twitter/twitter.html
diff --git a/browserid/static/jquery/model/service/twitter/twitter.js b/resources/static/jquery/model/service/twitter/twitter.js
similarity index 100%
rename from browserid/static/jquery/model/service/twitter/twitter.js
rename to resources/static/jquery/model/service/twitter/twitter.js
diff --git a/browserid/static/jquery/model/service/yql/yql.html b/resources/static/jquery/model/service/yql/yql.html
similarity index 100%
rename from browserid/static/jquery/model/service/yql/yql.html
rename to resources/static/jquery/model/service/yql/yql.html
diff --git a/browserid/static/jquery/model/service/yql/yql.js b/resources/static/jquery/model/service/yql/yql.js
similarity index 100%
rename from browserid/static/jquery/model/service/yql/yql.js
rename to resources/static/jquery/model/service/yql/yql.js
diff --git a/browserid/static/jquery/model/test/4.json b/resources/static/jquery/model/test/4.json
similarity index 100%
rename from browserid/static/jquery/model/test/4.json
rename to resources/static/jquery/model/test/4.json
diff --git a/browserid/static/jquery/model/test/create.json b/resources/static/jquery/model/test/create.json
similarity index 100%
rename from browserid/static/jquery/model/test/create.json
rename to resources/static/jquery/model/test/create.json
diff --git a/browserid/static/jquery/model/test/people.json b/resources/static/jquery/model/test/people.json
similarity index 100%
rename from browserid/static/jquery/model/test/people.json
rename to resources/static/jquery/model/test/people.json
diff --git a/browserid/static/jquery/model/test/person.json b/resources/static/jquery/model/test/person.json
similarity index 100%
rename from browserid/static/jquery/model/test/person.json
rename to resources/static/jquery/model/test/person.json
diff --git a/browserid/static/jquery/model/test/qunit/findAll.json b/resources/static/jquery/model/test/qunit/findAll.json
similarity index 100%
rename from browserid/static/jquery/model/test/qunit/findAll.json
rename to resources/static/jquery/model/test/qunit/findAll.json
diff --git a/browserid/static/jquery/model/test/qunit/model_test.js b/resources/static/jquery/model/test/qunit/model_test.js
similarity index 100%
rename from browserid/static/jquery/model/test/qunit/model_test.js
rename to resources/static/jquery/model/test/qunit/model_test.js
diff --git a/browserid/static/jquery/model/test/qunit/qunit.js b/resources/static/jquery/model/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/model/test/qunit/qunit.js
rename to resources/static/jquery/model/test/qunit/qunit.js
diff --git a/browserid/static/jquery/model/test/schools.json b/resources/static/jquery/model/test/schools.json
similarity index 100%
rename from browserid/static/jquery/model/test/schools.json
rename to resources/static/jquery/model/test/schools.json
diff --git a/browserid/static/jquery/model/test/update4.json b/resources/static/jquery/model/test/update4.json
similarity index 100%
rename from browserid/static/jquery/model/test/update4.json
rename to resources/static/jquery/model/test/update4.json
diff --git a/browserid/static/jquery/model/validations/qunit.html b/resources/static/jquery/model/validations/qunit.html
similarity index 100%
rename from browserid/static/jquery/model/validations/qunit.html
rename to resources/static/jquery/model/validations/qunit.html
diff --git a/browserid/static/jquery/model/validations/qunit/validations_test.js b/resources/static/jquery/model/validations/qunit/validations_test.js
similarity index 100%
rename from browserid/static/jquery/model/validations/qunit/validations_test.js
rename to resources/static/jquery/model/validations/qunit/validations_test.js
diff --git a/browserid/static/jquery/model/validations/validations.html b/resources/static/jquery/model/validations/validations.html
similarity index 100%
rename from browserid/static/jquery/model/validations/validations.html
rename to resources/static/jquery/model/validations/validations.html
diff --git a/browserid/static/jquery/model/validations/validations.js b/resources/static/jquery/model/validations/validations.js
similarity index 100%
rename from browserid/static/jquery/model/validations/validations.js
rename to resources/static/jquery/model/validations/validations.js
diff --git a/browserid/static/jquery/qunit.html b/resources/static/jquery/qunit.html
similarity index 100%
rename from browserid/static/jquery/qunit.html
rename to resources/static/jquery/qunit.html
diff --git a/browserid/static/jquery/test/qunit/integration.js b/resources/static/jquery/test/qunit/integration.js
similarity index 100%
rename from browserid/static/jquery/test/qunit/integration.js
rename to resources/static/jquery/test/qunit/integration.js
diff --git a/browserid/static/jquery/test/qunit/qunit.js b/resources/static/jquery/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/test/qunit/qunit.js
rename to resources/static/jquery/test/qunit/qunit.js
diff --git a/browserid/static/jquery/test/run.js b/resources/static/jquery/test/run.js
similarity index 100%
rename from browserid/static/jquery/test/run.js
rename to resources/static/jquery/test/run.js
diff --git a/browserid/static/jquery/test/template.ejs b/resources/static/jquery/test/template.ejs
similarity index 100%
rename from browserid/static/jquery/test/template.ejs
rename to resources/static/jquery/test/template.ejs
diff --git a/browserid/static/jquery/test/thing.json b/resources/static/jquery/test/thing.json
similarity index 100%
rename from browserid/static/jquery/test/thing.json
rename to resources/static/jquery/test/thing.json
diff --git a/browserid/static/jquery/tie/qunit.html b/resources/static/jquery/tie/qunit.html
similarity index 100%
rename from browserid/static/jquery/tie/qunit.html
rename to resources/static/jquery/tie/qunit.html
diff --git a/browserid/static/jquery/tie/tie.html b/resources/static/jquery/tie/tie.html
similarity index 100%
rename from browserid/static/jquery/tie/tie.html
rename to resources/static/jquery/tie/tie.html
diff --git a/browserid/static/jquery/tie/tie.js b/resources/static/jquery/tie/tie.js
similarity index 100%
rename from browserid/static/jquery/tie/tie.js
rename to resources/static/jquery/tie/tie.js
diff --git a/browserid/static/jquery/tie/tie_test.js b/resources/static/jquery/tie/tie_test.js
similarity index 100%
rename from browserid/static/jquery/tie/tie_test.js
rename to resources/static/jquery/tie/tie_test.js
diff --git a/browserid/static/jquery/update b/resources/static/jquery/update
similarity index 100%
rename from browserid/static/jquery/update
rename to resources/static/jquery/update
diff --git a/browserid/static/jquery/view/compress.js b/resources/static/jquery/view/compress.js
similarity index 100%
rename from browserid/static/jquery/view/compress.js
rename to resources/static/jquery/view/compress.js
diff --git a/browserid/static/jquery/view/ejs/ejs.html b/resources/static/jquery/view/ejs/ejs.html
similarity index 100%
rename from browserid/static/jquery/view/ejs/ejs.html
rename to resources/static/jquery/view/ejs/ejs.html
diff --git a/browserid/static/jquery/view/ejs/ejs.js b/resources/static/jquery/view/ejs/ejs.js
similarity index 100%
rename from browserid/static/jquery/view/ejs/ejs.js
rename to resources/static/jquery/view/ejs/ejs.js
diff --git a/browserid/static/jquery/view/ejs/funcunit.html b/resources/static/jquery/view/ejs/funcunit.html
similarity index 100%
rename from browserid/static/jquery/view/ejs/funcunit.html
rename to resources/static/jquery/view/ejs/funcunit.html
diff --git a/browserid/static/jquery/view/ejs/other.js b/resources/static/jquery/view/ejs/other.js
similarity index 100%
rename from browserid/static/jquery/view/ejs/other.js
rename to resources/static/jquery/view/ejs/other.js
diff --git a/browserid/static/jquery/view/ejs/qunit.html b/resources/static/jquery/view/ejs/qunit.html
similarity index 100%
rename from browserid/static/jquery/view/ejs/qunit.html
rename to resources/static/jquery/view/ejs/qunit.html
diff --git a/browserid/static/jquery/view/ejs/test/qunit/ejs_test.js b/resources/static/jquery/view/ejs/test/qunit/ejs_test.js
similarity index 100%
rename from browserid/static/jquery/view/ejs/test/qunit/ejs_test.js
rename to resources/static/jquery/view/ejs/test/qunit/ejs_test.js
diff --git a/browserid/static/jquery/view/ejs/test/qunit/qunit.js b/resources/static/jquery/view/ejs/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/view/ejs/test/qunit/qunit.js
rename to resources/static/jquery/view/ejs/test/qunit/qunit.js
diff --git a/browserid/static/jquery/view/fulljslint.js b/resources/static/jquery/view/fulljslint.js
similarity index 100%
rename from browserid/static/jquery/view/fulljslint.js
rename to resources/static/jquery/view/fulljslint.js
diff --git a/browserid/static/jquery/view/helpers/helpers.js b/resources/static/jquery/view/helpers/helpers.js
similarity index 100%
rename from browserid/static/jquery/view/helpers/helpers.js
rename to resources/static/jquery/view/helpers/helpers.js
diff --git a/browserid/static/jquery/view/jaml/jaml.js b/resources/static/jquery/view/jaml/jaml.js
similarity index 100%
rename from browserid/static/jquery/view/jaml/jaml.js
rename to resources/static/jquery/view/jaml/jaml.js
diff --git a/browserid/static/jquery/view/micro/micro.js b/resources/static/jquery/view/micro/micro.js
similarity index 100%
rename from browserid/static/jquery/view/micro/micro.js
rename to resources/static/jquery/view/micro/micro.js
diff --git a/browserid/static/jquery/view/qunit.html b/resources/static/jquery/view/qunit.html
similarity index 100%
rename from browserid/static/jquery/view/qunit.html
rename to resources/static/jquery/view/qunit.html
diff --git a/browserid/static/jquery/view/test/compression/compression.html b/resources/static/jquery/view/test/compression/compression.html
similarity index 100%
rename from browserid/static/jquery/view/test/compression/compression.html
rename to resources/static/jquery/view/test/compression/compression.html
diff --git a/browserid/static/jquery/view/test/compression/compression.js b/resources/static/jquery/view/test/compression/compression.js
similarity index 100%
rename from browserid/static/jquery/view/test/compression/compression.js
rename to resources/static/jquery/view/test/compression/compression.js
diff --git a/browserid/static/jquery/view/test/compression/production.js b/resources/static/jquery/view/test/compression/production.js
similarity index 100%
rename from browserid/static/jquery/view/test/compression/production.js
rename to resources/static/jquery/view/test/compression/production.js
diff --git a/browserid/static/jquery/view/test/compression/run.js b/resources/static/jquery/view/test/compression/run.js
similarity index 100%
rename from browserid/static/jquery/view/test/compression/run.js
rename to resources/static/jquery/view/test/compression/run.js
diff --git a/browserid/static/jquery/view/test/compression/views/keep.me b/resources/static/jquery/view/test/compression/views/keep.me
similarity index 100%
rename from browserid/static/jquery/view/test/compression/views/keep.me
rename to resources/static/jquery/view/test/compression/views/keep.me
diff --git a/browserid/static/jquery/view/test/qunit/deferred.ejs b/resources/static/jquery/view/test/qunit/deferred.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/deferred.ejs
rename to resources/static/jquery/view/test/qunit/deferred.ejs
diff --git a/browserid/static/jquery/view/test/qunit/deferreds.ejs b/resources/static/jquery/view/test/qunit/deferreds.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/deferreds.ejs
rename to resources/static/jquery/view/test/qunit/deferreds.ejs
diff --git a/browserid/static/jquery/view/test/qunit/hookup.ejs b/resources/static/jquery/view/test/qunit/hookup.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/hookup.ejs
rename to resources/static/jquery/view/test/qunit/hookup.ejs
diff --git a/browserid/static/jquery/view/test/qunit/large.ejs b/resources/static/jquery/view/test/qunit/large.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/large.ejs
rename to resources/static/jquery/view/test/qunit/large.ejs
diff --git a/browserid/static/jquery/view/test/qunit/nested_plugin.ejs b/resources/static/jquery/view/test/qunit/nested_plugin.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/nested_plugin.ejs
rename to resources/static/jquery/view/test/qunit/nested_plugin.ejs
diff --git a/browserid/static/jquery/view/test/qunit/plugin.ejs b/resources/static/jquery/view/test/qunit/plugin.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/plugin.ejs
rename to resources/static/jquery/view/test/qunit/plugin.ejs
diff --git a/browserid/static/jquery/view/test/qunit/qunit.js b/resources/static/jquery/view/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/qunit.js
rename to resources/static/jquery/view/test/qunit/qunit.js
diff --git a/browserid/static/jquery/view/test/qunit/temp.ejs b/resources/static/jquery/view/test/qunit/temp.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/temp.ejs
rename to resources/static/jquery/view/test/qunit/temp.ejs
diff --git a/browserid/static/jquery/view/test/qunit/template.ejs b/resources/static/jquery/view/test/qunit/template.ejs
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/template.ejs
rename to resources/static/jquery/view/test/qunit/template.ejs
diff --git a/browserid/static/jquery/view/test/qunit/template.jaml b/resources/static/jquery/view/test/qunit/template.jaml
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/template.jaml
rename to resources/static/jquery/view/test/qunit/template.jaml
diff --git a/browserid/static/jquery/view/test/qunit/template.micro b/resources/static/jquery/view/test/qunit/template.micro
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/template.micro
rename to resources/static/jquery/view/test/qunit/template.micro
diff --git a/browserid/static/jquery/view/test/qunit/template.tmpl b/resources/static/jquery/view/test/qunit/template.tmpl
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/template.tmpl
rename to resources/static/jquery/view/test/qunit/template.tmpl
diff --git a/browserid/static/jquery/view/test/qunit/view_test.js b/resources/static/jquery/view/test/qunit/view_test.js
similarity index 100%
rename from browserid/static/jquery/view/test/qunit/view_test.js
rename to resources/static/jquery/view/test/qunit/view_test.js
diff --git a/browserid/static/jquery/view/tmpl/test.tmpl b/resources/static/jquery/view/tmpl/test.tmpl
similarity index 100%
rename from browserid/static/jquery/view/tmpl/test.tmpl
rename to resources/static/jquery/view/tmpl/test.tmpl
diff --git a/browserid/static/jquery/view/tmpl/tmpl.js b/resources/static/jquery/view/tmpl/tmpl.js
similarity index 100%
rename from browserid/static/jquery/view/tmpl/tmpl.js
rename to resources/static/jquery/view/tmpl/tmpl.js
diff --git a/browserid/static/jquery/view/tmpl/tmpl_test.js b/resources/static/jquery/view/tmpl/tmpl_test.js
similarity index 100%
rename from browserid/static/jquery/view/tmpl/tmpl_test.js
rename to resources/static/jquery/view/tmpl/tmpl_test.js
diff --git a/browserid/static/jquery/view/view.html b/resources/static/jquery/view/view.html
similarity index 100%
rename from browserid/static/jquery/view/view.html
rename to resources/static/jquery/view/view.html
diff --git a/browserid/static/jquery/view/view.js b/resources/static/jquery/view/view.js
similarity index 100%
rename from browserid/static/jquery/view/view.js
rename to resources/static/jquery/view/view.js
diff --git a/browserid/static/js.bat b/resources/static/js.bat
similarity index 100%
rename from browserid/static/js.bat
rename to resources/static/js.bat
diff --git a/browserid/static/js/browserid.js b/resources/static/js/browserid.js
similarity index 77%
rename from browserid/static/js/browserid.js
rename to resources/static/js/browserid.js
index 7ed223b97bc95ec1492183dba38ccddfda3c44c9..0e698186572e127be8725629db0067c3c0dcfa1d 100644
--- a/browserid/static/js/browserid.js
+++ b/resources/static/js/browserid.js
@@ -34,8 +34,6 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-window.BrowserID = window.BrowserID || {};
-
 $(function() {
   "use strict";
 
@@ -43,21 +41,11 @@ $(function() {
    * For the main page
    */
 
-  function getParameterByName( name ) {
-    name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
-    var regexS = "[\\?&]"+name+"=([^&#]*)";
-    var regex = new RegExp( regexS );
-    var results = regex.exec( window.location.href );
-    if( results == null )
-      return "";
-    else
-      return decodeURIComponent(results[1].replace(/\+/g, " "));
-  }
-
-  var token = getParameterByName("token"),
-      path = document.location.pathname,
-      bid = BrowserID,
-      user = bid.User;
+  var bid = BrowserID,
+      pageHelpers = bid.PageHelpers,
+      user = bid.User,
+      token = pageHelpers.getParameterByName("token"),
+      path = document.location.pathname;
 
   if (!path || path === "/") {
     bid.index();
@@ -90,21 +78,20 @@ $(function() {
     });
   });
 
+  $(".display_always,.display_auth,.display_nonauth").hide();
+
+  var ANIMATION_TIME = 500;
   user.checkAuthentication(function(authenticated) {
+    $(".display_always").fadeIn(ANIMATION_TIME);
+
     if (authenticated) {
-      $("#content").fadeIn("slow");
+      $(".display_auth").fadeIn(ANIMATION_TIME);
       if ($('#emailList').length) {
         bid.manageAccount();
       }
     }
     else {
-      // If vAlign exists (main page), it takes precedence over content.
-      if( $("#vAlign").length) {
-        $("#vAlign").fadeIn("slow");
-      }
-      else {
-        $("#content").fadeIn("slow");
-      }
+      $(".display_nonauth").fadeIn(ANIMATION_TIME);
     }
   });
 
diff --git a/browserid/static/js/highlight.js b/resources/static/js/highlight.js
similarity index 100%
rename from browserid/static/js/highlight.js
rename to resources/static/js/highlight.js
diff --git a/browserid/static/js/html5shim.js b/resources/static/js/html5shim.js
similarity index 100%
rename from browserid/static/js/html5shim.js
rename to resources/static/js/html5shim.js
diff --git a/browserid/static/js/jquery-1.6.2.min.js b/resources/static/js/jquery-1.6.2.min.js
similarity index 100%
rename from browserid/static/js/jquery-1.6.2.min.js
rename to resources/static/js/jquery-1.6.2.min.js
diff --git a/browserid/static/js/json2.js b/resources/static/js/json2.js
similarity index 100%
rename from browserid/static/js/json2.js
rename to resources/static/js/json2.js
diff --git a/resources/static/js/page_helpers.js b/resources/static/js/page_helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e0bb323885460864dbb5ddd2582ff1504de1ccc
--- /dev/null
+++ b/resources/static/js/page_helpers.js
@@ -0,0 +1,93 @@
+/*globals BrowserID: true, _: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+BrowserID.PageHelpers = (function() {
+  "use strict";
+
+  var win = window,
+      locStorage = win.localStorage,
+      bid = BrowserID;
+
+  function setStoredEmail(email) {
+    locStorage.signInEmail = email;
+  }
+
+  function onEmailKeyUp(event) {
+    var email = $("#email").val();
+    setStoredEmail(email);
+  }
+
+  function prefillEmail() {
+    // If the user tried to sign in on the sign up page with an existing email, 
+    // place that email in the email field, then focus the password.
+    var el = $("#email"),
+        email = locStorage.signInEmail;
+
+    if (email) {
+      el.val(email);
+      if ($("#password").length) $("#password").focus();
+    }
+
+    el.keyup(onEmailKeyUp);
+  }
+  
+  function clearStoredEmail() {
+    locStorage.removeItem("signInEmail");
+  }
+
+  function getStoredEmail() {
+    return locStorage.signInEmail || "";
+  }
+
+  function getParameterByName( name ) {
+    name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
+    var regexS = "[\\?&]"+name+"=([^&#]*)";
+    var regex = new RegExp( regexS );
+    var results = regex.exec( win.location.href );
+    if( results === null )
+      return "";
+    else
+      return decodeURIComponent(results[1].replace(/\+/g, " "));
+  }
+
+  return {
+    setupEmail: prefillEmail,
+    setStoredEmail: setStoredEmail,
+    clearStoredEmail: clearStoredEmail,
+    getStoredEmail: getStoredEmail,
+    getParameterByName: getParameterByName
+  };
+}());
diff --git a/browserid/static/js/pages/add_email_address.js b/resources/static/js/pages/add_email_address.js
similarity index 100%
rename from browserid/static/js/pages/add_email_address.js
rename to resources/static/js/pages/add_email_address.js
diff --git a/resources/static/js/pages/forgot.js b/resources/static/js/pages/forgot.js
new file mode 100644
index 0000000000000000000000000000000000000000..32004aef4ed8602904b56fd38a62f18ff2abf114
--- /dev/null
+++ b/resources/static/js/pages/forgot.js
@@ -0,0 +1,92 @@
+/*globals BrowserID: true, $:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+BrowserID.forgot = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      user = bid.User,
+      pageHelpers = bid.PageHelpers,
+      tooltip = bid.Tooltip;
+
+  function submit(event) {
+    if (event) event.preventDefault();
+
+    // GET RID OF THIS HIDE CRAP AND USE CSS!
+    $(".notifications .notification").hide();
+
+    var email = $("#email").val(),
+        valid = bid.Validation.email(email);
+
+    if (valid) {
+      user.requestPasswordReset(email, function onSuccess(info) {
+        if (info.success) {
+          pageHelpers.clearStoredEmail();
+          $('#sent_to_email').html(email);
+          $('#forminputs').fadeOut();
+          $(".notifications .notification.emailsent").fadeIn();
+        }
+        else {
+          var tooltipEl = info.reason === "throttle" ? "#could_not_add" : "#not_registered";
+          tooltip.showTooltip(tooltipEl);
+        }
+      }, function onFailure() {
+        $(".notifications .notification.doh").fadeIn();
+      });
+    }
+  };
+
+  function init() {
+    $("form input[autofocus]").focus();
+
+    pageHelpers.setupEmail();
+
+    $("#signUpForm").bind("submit", submit);
+  }
+
+  function reset() {
+    $("#signUpForm").unbind("submit", submit);
+  }
+
+
+  var forgot = init;
+  forgot.submit = submit; 
+  forgot.reset = reset;
+
+  return forgot;
+
+}());
+
diff --git a/browserid/static/js/pages/index.js b/resources/static/js/pages/index.js
similarity index 100%
rename from browserid/static/js/pages/index.js
rename to resources/static/js/pages/index.js
diff --git a/browserid/static/js/pages/manage_account.js b/resources/static/js/pages/manage_account.js
similarity index 100%
rename from browserid/static/js/pages/manage_account.js
rename to resources/static/js/pages/manage_account.js
diff --git a/browserid/static/js/pages/signin.js b/resources/static/js/pages/signin.js
similarity index 86%
rename from browserid/static/js/pages/signin.js
rename to resources/static/js/pages/signin.js
index 1df6ec7b827cf0eba6174b7a1d2d9301a58dfee2..69590b274a4aa2b63db65589d50d8bbd775a6958 100644
--- a/browserid/static/js/pages/signin.js
+++ b/resources/static/js/pages/signin.js
@@ -39,23 +39,13 @@
 
   var bid = BrowserID,
       user = bid.User,
+      pageHelpers = bid.PageHelpers,
       validation = bid.Validation;
 
-  function prefillEmail() {
-    // If the user tried to sign in on the sign up page with an existing email, 
-    // place that email in the email field, then focus the password.
-    var email = window.localStorage.signInEmail;
-    if (email) {
-      $("#email").val(email);
-      window.localStorage.removeItem('signInEmail');
-      $("#password").focus();
-    }
-  }
-
   bid.signIn = function () {
     $("form input[autofocus]").focus();
 
-    prefillEmail();
+    pageHelpers.setupEmail();
 
     $("#signUpForm").bind("submit", function(event) {
       event.preventDefault();
@@ -68,6 +58,7 @@
       if (valid) {
         user.authenticate(email, password, function onSuccess(authenticated) {
           if (authenticated) {
+            pageHelpers.clearStoredEmail();
             document.location = "/";
           }
           else {
diff --git a/browserid/static/js/pages/signup.js b/resources/static/js/pages/signup.js
similarity index 96%
rename from browserid/static/js/pages/signup.js
rename to resources/static/js/pages/signup.js
index c60e3f01e88fb2145528ca7e67bf5dd254f2d04d..429fbccc66c0bfcb29a5eed5324101cee7ca9dcf 100644
--- a/browserid/static/js/pages/signup.js
+++ b/resources/static/js/pages/signup.js
@@ -39,10 +39,10 @@
 
   var bid = BrowserID,
       user = bid.User,
+      pageHelpers = bid.PageHelpers,
       ANIMATION_SPEED = 250;
 
   bid.signUp = function () {
-
     function replaceWithMessage(selector) {
         $('.forminputs').fadeOut(ANIMATION_SPEED, function() {
           $(selector).fadeIn(ANIMATION_SPEED);
@@ -61,6 +61,8 @@
     $(function () {
       $("form input[autofocus]").focus();
 
+      pageHelpers.setupEmail();
+
       $("#email").bind("keyup", function(event) {
         if (event.which !== 13) {
           $(".notification").fadeOut(ANIMATION_SPEED);
@@ -79,6 +81,7 @@
 
         user.isEmailRegistered(email, function(registered) {
           if (!registered) {
+            pageHelpers.clearStoredEmail();
             user.createUser(email, function onSuccess(keypair) {
               $('#sentToEmail').html(email);
               replaceWithMessage(".emailsent");
@@ -87,7 +90,6 @@
           else {
             $('#registeredEmail').html(email);
             showNotice(".alreadyRegistered");
-            window.localStorage.signInEmail = email;
           }
         }, onFailure);
       });
diff --git a/browserid/static/js/pages/verify_email_address.js b/resources/static/js/pages/verify_email_address.js
similarity index 98%
rename from browserid/static/js/pages/verify_email_address.js
rename to resources/static/js/pages/verify_email_address.js
index 07642d077dc1639d25939d9e7510d3f6abc8a578..6c97f534739ff52494eaab9f051991526c3891ac 100644
--- a/browserid/static/js/pages/verify_email_address.js
+++ b/resources/static/js/pages/verify_email_address.js
@@ -37,8 +37,7 @@
 (function() {
   "use strict";
 
-  var bid = BrowserID,
-      tooltip = bid.Tooltip;
+  var bid = BrowserID;
 
   function showError(el) {
     $(el).fadeIn(250);
diff --git a/resources/static/relay/relay.js b/resources/static/relay/relay.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9c86f4c13d0b4122eb43610ae4662769ce1c06e
--- /dev/null
+++ b/resources/static/relay/relay.js
@@ -0,0 +1,156 @@
+/*global Channel: true, errorOut: true */
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+
+(function() {
+  "use strict";
+
+  window.console = window.console || {
+    log: function() {}
+  };
+
+  BrowserID.Relay = (function() {
+    var transaction,
+        origin,
+        channel = Channel,
+        win = window,
+        registerCB;
+
+
+    function init(options) {
+      origin = transaction = registerCB = undefined;
+
+      if(options.window) {
+        win = options.window;
+      }
+
+      if(options.channel) {
+        channel = options.channel;
+      }
+    }
+
+    function open() {
+      var rpc = channel.build({
+        window: win,
+        origin: "*",
+        scope: "mozid"
+      });
+
+      rpc.bind("getVerifiedEmail", function(trans, s) {
+        trans.delayReturn(true);
+        origin = trans.origin;
+        transaction = trans;
+
+        // If the client has run early and already registered its registration 
+        // callback, call it now.
+        if (registerCB) {
+          registerCB(origin, completionCB);  
+        }
+      });
+    }
+
+    function registerClient(callback) {
+      // If the origin is ready, call the callback immediately.
+      if (origin) {
+        callback(origin, completionCB);
+      }
+      else {
+        registerCB = callback;
+      }
+    }
+
+    function errorOut(code) {
+      function getVerboseMessage(code) {
+        var msgs = {
+          "canceled": "user canceled selection",
+          "notImplemented": "the user tried to invoke behavior that's not yet implemented",
+          "serverError": "a technical problem was encountered while trying to communicate with BrowserID servers."
+        };
+        var msg = msgs[code];
+        if (!msg) {
+          alert("need verbose message for " + code);
+          msg = "unknown error";
+            }
+        return msg;
+      }
+      transaction.error(code, getVerboseMessage(code));
+    }
+
+    /**
+     * The client calls this to relay a message back to the RP whenever it is 
+     * complete.  This function is passed to the client when the client does 
+     * its registerClient.
+     */
+    function completionCB(status, error) {
+        if(error) {
+          errorOut(error);
+        }
+        else {
+          try {
+            transaction.complete(status);
+          } catch(e) {
+            // The relay function is called a second time after the 
+            // initial success, when the window is closing.
+          }
+        }
+    }
+
+
+    return {
+      /**
+       * Initialize the relay. 
+       * @method init
+       * @param {object} [options] - options used to override window, channel 
+       * for unit testing.
+       */
+      init: init,
+
+      /**
+       * Open the relay with the parent window.
+       * @method open
+       */
+      open: open,
+
+      /**
+       * Register a client to use the relay
+       * @method registerClient
+       */
+      registerClient: registerClient
+    };
+  }());
+
+}());
diff --git a/browserid/static/steal/.gitignore b/resources/static/steal/.gitignore
similarity index 100%
rename from browserid/static/steal/.gitignore
rename to resources/static/steal/.gitignore
diff --git a/browserid/static/steal/README b/resources/static/steal/README
similarity index 100%
rename from browserid/static/steal/README
rename to resources/static/steal/README
diff --git a/browserid/static/steal/build/apps/apps.js b/resources/static/steal/build/apps/apps.js
similarity index 100%
rename from browserid/static/steal/build/apps/apps.js
rename to resources/static/steal/build/apps/apps.js
diff --git a/browserid/static/steal/build/apps/test.js b/resources/static/steal/build/apps/test.js
similarity index 100%
rename from browserid/static/steal/build/apps/test.js
rename to resources/static/steal/build/apps/test.js
diff --git a/browserid/static/steal/build/build.js b/resources/static/steal/build/build.js
similarity index 100%
rename from browserid/static/steal/build/build.js
rename to resources/static/steal/build/build.js
diff --git a/browserid/static/steal/build/pluginify/parse.js b/resources/static/steal/build/pluginify/parse.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/parse.js
rename to resources/static/steal/build/pluginify/parse.js
diff --git a/browserid/static/steal/build/pluginify/pluginify.js b/resources/static/steal/build/pluginify/pluginify.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/pluginify.js
rename to resources/static/steal/build/pluginify/pluginify.js
diff --git a/browserid/static/steal/build/pluginify/test/firstFunc.js b/resources/static/steal/build/pluginify/test/firstFunc.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/test/firstFunc.js
rename to resources/static/steal/build/pluginify/test/firstFunc.js
diff --git a/browserid/static/steal/build/pluginify/test/pluginify_test.js b/resources/static/steal/build/pluginify/test/pluginify_test.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/test/pluginify_test.js
rename to resources/static/steal/build/pluginify/test/pluginify_test.js
diff --git a/browserid/static/steal/build/pluginify/test/secondFunc.js b/resources/static/steal/build/pluginify/test/secondFunc.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/test/secondFunc.js
rename to resources/static/steal/build/pluginify/test/secondFunc.js
diff --git a/browserid/static/steal/build/pluginify/test/test_steals.js b/resources/static/steal/build/pluginify/test/test_steals.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/test/test_steals.js
rename to resources/static/steal/build/pluginify/test/test_steals.js
diff --git a/browserid/static/steal/build/pluginify/test/weirdRegexps.js b/resources/static/steal/build/pluginify/test/weirdRegexps.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/test/weirdRegexps.js
rename to resources/static/steal/build/pluginify/test/weirdRegexps.js
diff --git a/browserid/static/steal/build/pluginify/tokens.js b/resources/static/steal/build/pluginify/tokens.js
similarity index 100%
rename from browserid/static/steal/build/pluginify/tokens.js
rename to resources/static/steal/build/pluginify/tokens.js
diff --git a/browserid/static/steal/build/scripts/compiler.jar b/resources/static/steal/build/scripts/compiler.jar
similarity index 100%
rename from browserid/static/steal/build/scripts/compiler.jar
rename to resources/static/steal/build/scripts/compiler.jar
diff --git a/browserid/static/steal/build/scripts/scripts.js b/resources/static/steal/build/scripts/scripts.js
similarity index 100%
rename from browserid/static/steal/build/scripts/scripts.js
rename to resources/static/steal/build/scripts/scripts.js
diff --git a/browserid/static/steal/build/scripts/yui.jar b/resources/static/steal/build/scripts/yui.jar
similarity index 100%
rename from browserid/static/steal/build/scripts/yui.jar
rename to resources/static/steal/build/scripts/yui.jar
diff --git a/browserid/static/steal/build/styles/cssmin.js b/resources/static/steal/build/styles/cssmin.js
similarity index 100%
rename from browserid/static/steal/build/styles/cssmin.js
rename to resources/static/steal/build/styles/cssmin.js
diff --git a/browserid/static/steal/build/styles/styles.js b/resources/static/steal/build/styles/styles.js
similarity index 100%
rename from browserid/static/steal/build/styles/styles.js
rename to resources/static/steal/build/styles/styles.js
diff --git a/browserid/static/steal/build/styles/test/app/app.css b/resources/static/steal/build/styles/test/app/app.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/app/app.css
rename to resources/static/steal/build/styles/test/app/app.css
diff --git a/browserid/static/steal/build/styles/test/app/app.html b/resources/static/steal/build/styles/test/app/app.html
similarity index 100%
rename from browserid/static/steal/build/styles/test/app/app.html
rename to resources/static/steal/build/styles/test/app/app.html
diff --git a/browserid/static/steal/build/styles/test/app/app.js b/resources/static/steal/build/styles/test/app/app.js
similarity index 100%
rename from browserid/static/steal/build/styles/test/app/app.js
rename to resources/static/steal/build/styles/test/app/app.js
diff --git a/browserid/static/steal/build/styles/test/app/production.css b/resources/static/steal/build/styles/test/app/production.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/app/production.css
rename to resources/static/steal/build/styles/test/app/production.css
diff --git a/browserid/static/steal/build/styles/test/css/css1.css b/resources/static/steal/build/styles/test/css/css1.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/css/css1.css
rename to resources/static/steal/build/styles/test/css/css1.css
diff --git a/browserid/static/steal/build/styles/test/css/justin.png b/resources/static/steal/build/styles/test/css/justin.png
similarity index 100%
rename from browserid/static/steal/build/styles/test/css/justin.png
rename to resources/static/steal/build/styles/test/css/justin.png
diff --git a/browserid/static/steal/build/styles/test/css2.css b/resources/static/steal/build/styles/test/css2.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/css2.css
rename to resources/static/steal/build/styles/test/css2.css
diff --git a/browserid/static/steal/build/styles/test/multiline.css b/resources/static/steal/build/styles/test/multiline.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/multiline.css
rename to resources/static/steal/build/styles/test/multiline.css
diff --git a/browserid/static/steal/build/styles/test/page.html b/resources/static/steal/build/styles/test/page.html
similarity index 100%
rename from browserid/static/steal/build/styles/test/page.html
rename to resources/static/steal/build/styles/test/page.html
diff --git a/browserid/static/steal/build/styles/test/production.css b/resources/static/steal/build/styles/test/production.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/production.css
rename to resources/static/steal/build/styles/test/production.css
diff --git a/browserid/static/steal/build/styles/test/productionCompare.css b/resources/static/steal/build/styles/test/productionCompare.css
similarity index 100%
rename from browserid/static/steal/build/styles/test/productionCompare.css
rename to resources/static/steal/build/styles/test/productionCompare.css
diff --git a/browserid/static/steal/build/styles/test/styles_test.js b/resources/static/steal/build/styles/test/styles_test.js
similarity index 100%
rename from browserid/static/steal/build/styles/test/styles_test.js
rename to resources/static/steal/build/styles/test/styles_test.js
diff --git a/browserid/static/steal/build/styles/test/upload.PNG b/resources/static/steal/build/styles/test/upload.PNG
similarity index 100%
rename from browserid/static/steal/build/styles/test/upload.PNG
rename to resources/static/steal/build/styles/test/upload.PNG
diff --git a/browserid/static/steal/build/test/basicpage.html b/resources/static/steal/build/test/basicpage.html
similarity index 100%
rename from browserid/static/steal/build/test/basicpage.html
rename to resources/static/steal/build/test/basicpage.html
diff --git a/browserid/static/steal/build/test/basicsource.js b/resources/static/steal/build/test/basicsource.js
similarity index 100%
rename from browserid/static/steal/build/test/basicsource.js
rename to resources/static/steal/build/test/basicsource.js
diff --git a/browserid/static/steal/build/test/foreign.html b/resources/static/steal/build/test/foreign.html
similarity index 100%
rename from browserid/static/steal/build/test/foreign.html
rename to resources/static/steal/build/test/foreign.html
diff --git a/browserid/static/steal/build/test/foreign.js b/resources/static/steal/build/test/foreign.js
similarity index 100%
rename from browserid/static/steal/build/test/foreign.js
rename to resources/static/steal/build/test/foreign.js
diff --git a/browserid/static/steal/build/test/https.html b/resources/static/steal/build/test/https.html
similarity index 100%
rename from browserid/static/steal/build/test/https.html
rename to resources/static/steal/build/test/https.html
diff --git a/browserid/static/steal/build/test/https.js b/resources/static/steal/build/test/https.js
similarity index 100%
rename from browserid/static/steal/build/test/https.js
rename to resources/static/steal/build/test/https.js
diff --git a/browserid/static/steal/build/test/removecode.js b/resources/static/steal/build/test/removecode.js
similarity index 100%
rename from browserid/static/steal/build/test/removecode.js
rename to resources/static/steal/build/test/removecode.js
diff --git a/browserid/static/steal/build/test/run.js b/resources/static/steal/build/test/run.js
similarity index 100%
rename from browserid/static/steal/build/test/run.js
rename to resources/static/steal/build/test/run.js
diff --git a/browserid/static/steal/build/test/stealpage.html b/resources/static/steal/build/test/stealpage.html
similarity index 100%
rename from browserid/static/steal/build/test/stealpage.html
rename to resources/static/steal/build/test/stealpage.html
diff --git a/browserid/static/steal/build/test/stealprodpage.html b/resources/static/steal/build/test/stealprodpage.html
similarity index 100%
rename from browserid/static/steal/build/test/stealprodpage.html
rename to resources/static/steal/build/test/stealprodpage.html
diff --git a/browserid/static/steal/build/test/test.js b/resources/static/steal/build/test/test.js
similarity index 100%
rename from browserid/static/steal/build/test/test.js
rename to resources/static/steal/build/test/test.js
diff --git a/browserid/static/steal/buildjs b/resources/static/steal/buildjs
similarity index 100%
rename from browserid/static/steal/buildjs
rename to resources/static/steal/buildjs
diff --git a/browserid/static/steal/clean/beautify.js b/resources/static/steal/clean/beautify.js
similarity index 100%
rename from browserid/static/steal/clean/beautify.js
rename to resources/static/steal/clean/beautify.js
diff --git a/browserid/static/steal/clean/clean.js b/resources/static/steal/clean/clean.js
similarity index 100%
rename from browserid/static/steal/clean/clean.js
rename to resources/static/steal/clean/clean.js
diff --git a/browserid/static/steal/clean/jslint.js b/resources/static/steal/clean/jslint.js
similarity index 100%
rename from browserid/static/steal/clean/jslint.js
rename to resources/static/steal/clean/jslint.js
diff --git a/browserid/static/steal/clean/test/clean_test.js b/resources/static/steal/clean/test/clean_test.js
similarity index 100%
rename from browserid/static/steal/clean/test/clean_test.js
rename to resources/static/steal/clean/test/clean_test.js
diff --git a/browserid/static/steal/clean/test/test.js b/resources/static/steal/clean/test/test.js
similarity index 100%
rename from browserid/static/steal/clean/test/test.js
rename to resources/static/steal/clean/test/test.js
diff --git a/browserid/static/steal/clean/test/testEnd.js b/resources/static/steal/clean/test/testEnd.js
similarity index 100%
rename from browserid/static/steal/clean/test/testEnd.js
rename to resources/static/steal/clean/test/testEnd.js
diff --git a/browserid/static/steal/cleanjs b/resources/static/steal/cleanjs
similarity index 100%
rename from browserid/static/steal/cleanjs
rename to resources/static/steal/cleanjs
diff --git a/browserid/static/steal/coffee/coffee-script.js b/resources/static/steal/coffee/coffee-script.js
similarity index 100%
rename from browserid/static/steal/coffee/coffee-script.js
rename to resources/static/steal/coffee/coffee-script.js
diff --git a/browserid/static/steal/coffee/coffee.js b/resources/static/steal/coffee/coffee.js
similarity index 100%
rename from browserid/static/steal/coffee/coffee.js
rename to resources/static/steal/coffee/coffee.js
diff --git a/browserid/static/steal/dev/dev.js b/resources/static/steal/dev/dev.js
similarity index 100%
rename from browserid/static/steal/dev/dev.js
rename to resources/static/steal/dev/dev.js
diff --git a/browserid/static/steal/end.js b/resources/static/steal/end.js
similarity index 100%
rename from browserid/static/steal/end.js
rename to resources/static/steal/end.js
diff --git a/browserid/static/steal/generate/app b/resources/static/steal/generate/app
similarity index 100%
rename from browserid/static/steal/generate/app
rename to resources/static/steal/generate/app
diff --git a/browserid/static/steal/generate/ejs.js b/resources/static/steal/generate/ejs.js
similarity index 100%
rename from browserid/static/steal/generate/ejs.js
rename to resources/static/steal/generate/ejs.js
diff --git a/browserid/static/steal/generate/generate.js b/resources/static/steal/generate/generate.js
similarity index 100%
rename from browserid/static/steal/generate/generate.js
rename to resources/static/steal/generate/generate.js
diff --git a/browserid/static/steal/generate/inflector.js b/resources/static/steal/generate/inflector.js
similarity index 100%
rename from browserid/static/steal/generate/inflector.js
rename to resources/static/steal/generate/inflector.js
diff --git a/browserid/static/steal/generate/system.js b/resources/static/steal/generate/system.js
similarity index 100%
rename from browserid/static/steal/generate/system.js
rename to resources/static/steal/generate/system.js
diff --git a/browserid/static/steal/generate/templates/app/(application_name).css.ejs b/resources/static/steal/generate/templates/app/(application_name).css.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/(application_name).css.ejs
rename to resources/static/steal/generate/templates/app/(application_name).css.ejs
diff --git a/browserid/static/steal/generate/templates/app/(application_name).html.ejs b/resources/static/steal/generate/templates/app/(application_name).html.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/(application_name).html.ejs
rename to resources/static/steal/generate/templates/app/(application_name).html.ejs
diff --git a/browserid/static/steal/generate/templates/app/(application_name).js.ejs b/resources/static/steal/generate/templates/app/(application_name).js.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/(application_name).js.ejs
rename to resources/static/steal/generate/templates/app/(application_name).js.ejs
diff --git a/browserid/static/steal/generate/templates/app/docs/.ignore b/resources/static/steal/generate/templates/app/docs/.ignore
similarity index 100%
rename from browserid/static/steal/generate/templates/app/docs/.ignore
rename to resources/static/steal/generate/templates/app/docs/.ignore
diff --git a/browserid/static/steal/generate/templates/app/resources/.ignore b/resources/static/steal/generate/templates/app/resources/.ignore
similarity index 100%
rename from browserid/static/steal/generate/templates/app/resources/.ignore
rename to resources/static/steal/generate/templates/app/resources/.ignore
diff --git a/browserid/static/steal/generate/templates/app/resources/example.coffee.ejs b/resources/static/steal/generate/templates/app/resources/example.coffee.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/resources/example.coffee.ejs
rename to resources/static/steal/generate/templates/app/resources/example.coffee.ejs
diff --git a/browserid/static/steal/generate/templates/app/resources/example.js.ejs b/resources/static/steal/generate/templates/app/resources/example.js.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/resources/example.js.ejs
rename to resources/static/steal/generate/templates/app/resources/example.js.ejs
diff --git a/browserid/static/steal/generate/templates/app/resources/example.less.ejs b/resources/static/steal/generate/templates/app/resources/example.less.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/resources/example.less.ejs
rename to resources/static/steal/generate/templates/app/resources/example.less.ejs
diff --git a/browserid/static/steal/generate/templates/app/scripts/build.html.ejs b/resources/static/steal/generate/templates/app/scripts/build.html.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/scripts/build.html.ejs
rename to resources/static/steal/generate/templates/app/scripts/build.html.ejs
diff --git a/browserid/static/steal/generate/templates/app/scripts/build.js.ejs b/resources/static/steal/generate/templates/app/scripts/build.js.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/scripts/build.js.ejs
rename to resources/static/steal/generate/templates/app/scripts/build.js.ejs
diff --git a/browserid/static/steal/generate/templates/app/scripts/clean.js.ejs b/resources/static/steal/generate/templates/app/scripts/clean.js.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/app/scripts/clean.js.ejs
rename to resources/static/steal/generate/templates/app/scripts/clean.js.ejs
diff --git a/browserid/static/steal/generate/templates/app/test/.ignore b/resources/static/steal/generate/templates/app/test/.ignore
similarity index 100%
rename from browserid/static/steal/generate/templates/app/test/.ignore
rename to resources/static/steal/generate/templates/app/test/.ignore
diff --git a/browserid/static/steal/generate/templates/page.ejs b/resources/static/steal/generate/templates/page.ejs
similarity index 100%
rename from browserid/static/steal/generate/templates/page.ejs
rename to resources/static/steal/generate/templates/page.ejs
diff --git a/browserid/static/steal/generate/test/run.js b/resources/static/steal/generate/test/run.js
similarity index 100%
rename from browserid/static/steal/generate/test/run.js
rename to resources/static/steal/generate/test/run.js
diff --git a/browserid/static/steal/get/basic.js b/resources/static/steal/get/basic.js
similarity index 100%
rename from browserid/static/steal/get/basic.js
rename to resources/static/steal/get/basic.js
diff --git a/browserid/static/steal/get/dummysteal.js b/resources/static/steal/get/dummysteal.js
similarity index 100%
rename from browserid/static/steal/get/dummysteal.js
rename to resources/static/steal/get/dummysteal.js
diff --git a/browserid/static/steal/get/get.js b/resources/static/steal/get/get.js
similarity index 100%
rename from browserid/static/steal/get/get.js
rename to resources/static/steal/get/get.js
diff --git a/browserid/static/steal/get/gets.json b/resources/static/steal/get/gets.json
similarity index 100%
rename from browserid/static/steal/get/gets.json
rename to resources/static/steal/get/gets.json
diff --git a/browserid/static/steal/get/getter.js b/resources/static/steal/get/getter.js
similarity index 100%
rename from browserid/static/steal/get/getter.js
rename to resources/static/steal/get/getter.js
diff --git a/browserid/static/steal/get/git.js b/resources/static/steal/get/git.js
similarity index 100%
rename from browserid/static/steal/get/git.js
rename to resources/static/steal/get/git.js
diff --git a/browserid/static/steal/get/github.js b/resources/static/steal/get/github.js
similarity index 100%
rename from browserid/static/steal/get/github.js
rename to resources/static/steal/get/github.js
diff --git a/browserid/static/steal/get/json.js b/resources/static/steal/get/json.js
similarity index 100%
rename from browserid/static/steal/get/json.js
rename to resources/static/steal/get/json.js
diff --git a/browserid/static/steal/get/test/.gitignore b/resources/static/steal/get/test/.gitignore
similarity index 100%
rename from browserid/static/steal/get/test/.gitignore
rename to resources/static/steal/get/test/.gitignore
diff --git a/browserid/static/steal/get/test/.gitmodules b/resources/static/steal/get/test/.gitmodules
similarity index 100%
rename from browserid/static/steal/get/test/.gitmodules
rename to resources/static/steal/get/test/.gitmodules
diff --git a/browserid/static/steal/get/test/README b/resources/static/steal/get/test/README
similarity index 100%
rename from browserid/static/steal/get/test/README
rename to resources/static/steal/get/test/README
diff --git a/browserid/static/steal/get/test/get_test.js b/resources/static/steal/get/test/get_test.js
similarity index 100%
rename from browserid/static/steal/get/test/get_test.js
rename to resources/static/steal/get/test/get_test.js
diff --git a/browserid/static/steal/get/test/stealCode1.js b/resources/static/steal/get/test/stealCode1.js
similarity index 100%
rename from browserid/static/steal/get/test/stealCode1.js
rename to resources/static/steal/get/test/stealCode1.js
diff --git a/browserid/static/steal/getjs b/resources/static/steal/getjs
similarity index 100%
rename from browserid/static/steal/getjs
rename to resources/static/steal/getjs
diff --git a/browserid/static/steal/js b/resources/static/steal/js
similarity index 100%
rename from browserid/static/steal/js
rename to resources/static/steal/js
diff --git a/browserid/static/steal/js.bat b/resources/static/steal/js.bat
similarity index 100%
rename from browserid/static/steal/js.bat
rename to resources/static/steal/js.bat
diff --git a/browserid/static/steal/less/less.js b/resources/static/steal/less/less.js
similarity index 100%
rename from browserid/static/steal/less/less.js
rename to resources/static/steal/less/less.js
diff --git a/browserid/static/steal/less/less.less b/resources/static/steal/less/less.less
similarity index 100%
rename from browserid/static/steal/less/less.less
rename to resources/static/steal/less/less.less
diff --git a/browserid/static/steal/less/less_engine.js b/resources/static/steal/less/less_engine.js
similarity index 100%
rename from browserid/static/steal/less/less_engine.js
rename to resources/static/steal/less/less_engine.js
diff --git a/browserid/static/steal/less/less_test.js b/resources/static/steal/less/less_test.js
similarity index 100%
rename from browserid/static/steal/less/less_test.js
rename to resources/static/steal/less/less_test.js
diff --git a/browserid/static/steal/less/qunit.html b/resources/static/steal/less/qunit.html
similarity index 100%
rename from browserid/static/steal/less/qunit.html
rename to resources/static/steal/less/qunit.html
diff --git a/browserid/static/steal/make.js b/resources/static/steal/make.js
similarity index 100%
rename from browserid/static/steal/make.js
rename to resources/static/steal/make.js
diff --git a/browserid/static/steal/parse/parse.js b/resources/static/steal/parse/parse.js
similarity index 100%
rename from browserid/static/steal/parse/parse.js
rename to resources/static/steal/parse/parse.js
diff --git a/browserid/static/steal/parse/parse_test.js b/resources/static/steal/parse/parse_test.js
similarity index 100%
rename from browserid/static/steal/parse/parse_test.js
rename to resources/static/steal/parse/parse_test.js
diff --git a/browserid/static/steal/parse/test/stealCode1.js b/resources/static/steal/parse/test/stealCode1.js
similarity index 100%
rename from browserid/static/steal/parse/test/stealCode1.js
rename to resources/static/steal/parse/test/stealCode1.js
diff --git a/browserid/static/steal/parse/test/testCode.js b/resources/static/steal/parse/test/testCode.js
similarity index 100%
rename from browserid/static/steal/parse/test/testCode.js
rename to resources/static/steal/parse/test/testCode.js
diff --git a/browserid/static/steal/parse/tokens.js b/resources/static/steal/parse/tokens.js
similarity index 100%
rename from browserid/static/steal/parse/tokens.js
rename to resources/static/steal/parse/tokens.js
diff --git a/browserid/static/steal/patchfile b/resources/static/steal/patchfile
similarity index 100%
rename from browserid/static/steal/patchfile
rename to resources/static/steal/patchfile
diff --git a/browserid/static/steal/pluginifyjs b/resources/static/steal/pluginifyjs
similarity index 100%
rename from browserid/static/steal/pluginifyjs
rename to resources/static/steal/pluginifyjs
diff --git a/browserid/static/steal/rhino/blank.html b/resources/static/steal/rhino/blank.html
similarity index 100%
rename from browserid/static/steal/rhino/blank.html
rename to resources/static/steal/rhino/blank.html
diff --git a/browserid/static/steal/rhino/build.js b/resources/static/steal/rhino/build.js
similarity index 100%
rename from browserid/static/steal/rhino/build.js
rename to resources/static/steal/rhino/build.js
diff --git a/browserid/static/steal/rhino/docs.js b/resources/static/steal/rhino/docs.js
similarity index 100%
rename from browserid/static/steal/rhino/docs.js
rename to resources/static/steal/rhino/docs.js
diff --git a/browserid/static/steal/rhino/empty.html b/resources/static/steal/rhino/empty.html
similarity index 100%
rename from browserid/static/steal/rhino/empty.html
rename to resources/static/steal/rhino/empty.html
diff --git a/browserid/static/steal/rhino/env.js b/resources/static/steal/rhino/env.js
similarity index 100%
rename from browserid/static/steal/rhino/env.js
rename to resources/static/steal/rhino/env.js
diff --git a/browserid/static/steal/rhino/file.js b/resources/static/steal/rhino/file.js
similarity index 100%
rename from browserid/static/steal/rhino/file.js
rename to resources/static/steal/rhino/file.js
diff --git a/browserid/static/steal/rhino/js.jar b/resources/static/steal/rhino/js.jar
similarity index 100%
rename from browserid/static/steal/rhino/js.jar
rename to resources/static/steal/rhino/js.jar
diff --git a/browserid/static/steal/rhino/loader b/resources/static/steal/rhino/loader
similarity index 100%
rename from browserid/static/steal/rhino/loader
rename to resources/static/steal/rhino/loader
diff --git a/browserid/static/steal/rhino/loader.bat b/resources/static/steal/rhino/loader.bat
similarity index 100%
rename from browserid/static/steal/rhino/loader.bat
rename to resources/static/steal/rhino/loader.bat
diff --git a/browserid/static/steal/rhino/loader.js b/resources/static/steal/rhino/loader.js
similarity index 100%
rename from browserid/static/steal/rhino/loader.js
rename to resources/static/steal/rhino/loader.js
diff --git a/browserid/static/steal/rhino/prompt.js b/resources/static/steal/rhino/prompt.js
similarity index 100%
rename from browserid/static/steal/rhino/prompt.js
rename to resources/static/steal/rhino/prompt.js
diff --git a/browserid/static/steal/rhino/steal.js b/resources/static/steal/rhino/steal.js
similarity index 100%
rename from browserid/static/steal/rhino/steal.js
rename to resources/static/steal/rhino/steal.js
diff --git a/browserid/static/steal/rhino/test.js b/resources/static/steal/rhino/test.js
similarity index 100%
rename from browserid/static/steal/rhino/test.js
rename to resources/static/steal/rhino/test.js
diff --git a/browserid/static/steal/rhino/utils.js b/resources/static/steal/rhino/utils.js
similarity index 100%
rename from browserid/static/steal/rhino/utils.js
rename to resources/static/steal/rhino/utils.js
diff --git a/browserid/static/steal/steal.js b/resources/static/steal/steal.js
similarity index 100%
rename from browserid/static/steal/steal.js
rename to resources/static/steal/steal.js
diff --git a/browserid/static/steal/steal.production.js b/resources/static/steal/steal.production.js
similarity index 100%
rename from browserid/static/steal/steal.production.js
rename to resources/static/steal/steal.production.js
diff --git a/browserid/static/steal/test/absoluteurl.html b/resources/static/steal/test/absoluteurl.html
similarity index 100%
rename from browserid/static/steal/test/absoluteurl.html
rename to resources/static/steal/test/absoluteurl.html
diff --git a/browserid/static/steal/test/absoluteurl/absoluteurl.js b/resources/static/steal/test/absoluteurl/absoluteurl.js
similarity index 100%
rename from browserid/static/steal/test/absoluteurl/absoluteurl.js
rename to resources/static/steal/test/absoluteurl/absoluteurl.js
diff --git a/browserid/static/steal/test/absoluteurl/alert.js b/resources/static/steal/test/absoluteurl/alert.js
similarity index 100%
rename from browserid/static/steal/test/absoluteurl/alert.js
rename to resources/static/steal/test/absoluteurl/alert.js
diff --git a/browserid/static/steal/test/another/two.js b/resources/static/steal/test/another/two.js
similarity index 100%
rename from browserid/static/steal/test/another/two.js
rename to resources/static/steal/test/another/two.js
diff --git a/browserid/static/steal/test/envjs/qunit.html b/resources/static/steal/test/envjs/qunit.html
similarity index 100%
rename from browserid/static/steal/test/envjs/qunit.html
rename to resources/static/steal/test/envjs/qunit.html
diff --git a/browserid/static/steal/test/envjs/qunit.js b/resources/static/steal/test/envjs/qunit.js
similarity index 100%
rename from browserid/static/steal/test/envjs/qunit.js
rename to resources/static/steal/test/envjs/qunit.js
diff --git a/browserid/static/steal/test/funcunit.html b/resources/static/steal/test/funcunit.html
similarity index 100%
rename from browserid/static/steal/test/funcunit.html
rename to resources/static/steal/test/funcunit.html
diff --git a/browserid/static/steal/test/funcunit/funcunit.js b/resources/static/steal/test/funcunit/funcunit.js
similarity index 100%
rename from browserid/static/steal/test/funcunit/funcunit.js
rename to resources/static/steal/test/funcunit/funcunit.js
diff --git a/browserid/static/steal/test/funcunit/steal_test.js b/resources/static/steal/test/funcunit/steal_test.js
similarity index 100%
rename from browserid/static/steal/test/funcunit/steal_test.js
rename to resources/static/steal/test/funcunit/steal_test.js
diff --git a/browserid/static/steal/test/one/four.js b/resources/static/steal/test/one/four.js
similarity index 100%
rename from browserid/static/steal/test/one/four.js
rename to resources/static/steal/test/one/four.js
diff --git a/browserid/static/steal/test/one/one.js b/resources/static/steal/test/one/one.js
similarity index 100%
rename from browserid/static/steal/test/one/one.js
rename to resources/static/steal/test/one/one.js
diff --git a/browserid/static/steal/test/qunit.html b/resources/static/steal/test/qunit.html
similarity index 100%
rename from browserid/static/steal/test/qunit.html
rename to resources/static/steal/test/qunit.html
diff --git a/browserid/static/steal/test/qunit/one.css b/resources/static/steal/test/qunit/one.css
similarity index 100%
rename from browserid/static/steal/test/qunit/one.css
rename to resources/static/steal/test/qunit/one.css
diff --git a/browserid/static/steal/test/qunit/qunit.js b/resources/static/steal/test/qunit/qunit.js
similarity index 100%
rename from browserid/static/steal/test/qunit/qunit.js
rename to resources/static/steal/test/qunit/qunit.js
diff --git a/browserid/static/steal/test/qunit/steal_test.js b/resources/static/steal/test/qunit/steal_test.js
similarity index 100%
rename from browserid/static/steal/test/qunit/steal_test.js
rename to resources/static/steal/test/qunit/steal_test.js
diff --git a/browserid/static/steal/test/run.js b/resources/static/steal/test/run.js
similarity index 100%
rename from browserid/static/steal/test/run.js
rename to resources/static/steal/test/run.js
diff --git a/browserid/static/steal/test/steal.html b/resources/static/steal/test/steal.html
similarity index 100%
rename from browserid/static/steal/test/steal.html
rename to resources/static/steal/test/steal.html
diff --git a/browserid/static/steal/test/test.js b/resources/static/steal/test/test.js
similarity index 100%
rename from browserid/static/steal/test/test.js
rename to resources/static/steal/test/test.js
diff --git a/browserid/static/steal/test/three.js b/resources/static/steal/test/three.js
similarity index 100%
rename from browserid/static/steal/test/three.js
rename to resources/static/steal/test/three.js
diff --git a/browserid/static/steal/test/two.css b/resources/static/steal/test/two.css
similarity index 100%
rename from browserid/static/steal/test/two.css
rename to resources/static/steal/test/two.css
diff --git a/browserid/static/steal/update b/resources/static/steal/update
similarity index 100%
rename from browserid/static/steal/update
rename to resources/static/steal/update
diff --git a/browserid/views/about.ejs b/resources/views/about.ejs
similarity index 95%
rename from browserid/views/about.ejs
rename to resources/views/about.ejs
index f8bd46c0a6e8c8abb0a579e8442f7056fca9c1a1..3bfe7009df690c0bb641074c635702e0332f3210 100644
--- a/browserid/views/about.ejs
+++ b/resources/views/about.ejs
@@ -1,4 +1,4 @@
-<div id="content">
+<div id="content" class="display_always">
     <div id="about">
 
         <div class="video">
diff --git a/resources/views/dialog.ejs b/resources/views/dialog.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..d2b634a26338b2e79f40b510c8351d1aba641e22
--- /dev/null
+++ b/resources/views/dialog.ejs
@@ -0,0 +1,35 @@
+    <section id="formWrap">
+      <form novalidate> 
+        <div id="favicon">
+            <div class="vertical">
+                <strong id="sitename"></strong>
+            </div>
+        </div>
+
+        <div id="signIn">
+            <div class="arrow"></div>
+            <div class="table">
+                <div class="vertical contents">
+                </div>
+            </div>
+        </div>
+      </form>
+    </section>
+
+
+    <section id="wait">
+        <div class="table">
+            <div class="vertical contents">
+                <h2>Communicating with server</h2>
+                <p>Just a moment while we talk with the server.</p>
+            </div>
+        </div>
+    </section>
+
+
+    <section id="error">
+        <div class="table">
+            <div class="vertical contents">
+            </div>
+        </div>
+    </section>
diff --git a/browserid/views/dialog.ejs b/resources/views/dialog_layout.ejs
similarity index 53%
rename from browserid/views/dialog.ejs
rename to resources/views/dialog_layout.ejs
index bf766dfdae7a176d976d3fca573e38a5638efab0..13dd4d98f92ddc60880e28d7105492ba153837ae 100644
--- a/browserid/views/dialog.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -24,41 +24,7 @@
           </header>
 
           <div id="content">
-              <section id="formWrap">
-                <form novalidate> 
-                  <div id="favicon">
-                      <div class="vertical">
-                          <strong id="sitename"></strong>
-                      </div>
-                  </div>
-
-                  <div id="signIn">
-                      <div class="arrow"></div>
-                      <div class="table">
-                          <div class="vertical contents">
-                          </div>
-                      </div>
-                  </div>
-                </form>
-              </section>
-
-
-              <section id="wait">
-                  <div class="table">
-                      <div class="vertical contents">
-                          <h2>Communicating with server</h2>
-                          <p>Just a moment while we talk with the server.</p>
-                      </div>
-                  </div>
-              </section>
-
-
-              <section id="error">
-                  <div class="table">
-                      <div class="vertical contents">
-                      </div>
-                  </div>
-              </section>
+            <%- body %>
           </div>
 
           <footer>
@@ -79,12 +45,14 @@
 
       </div>
 
-      <script type="text/html" id="templateTooltip">
-        <div class="tooltip">
-          {{ contents }}
-        </div>
-      </script>
-      <script type="text/javascript" src="/vepbundle"></script>
-      <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script>
+      <% if (useJavascript !== false) { %>
+          <script type="text/html" id="templateTooltip">
+            <div class="tooltip">
+              {{ contents }}
+            </div>
+          </script>
+          <script type="text/javascript" src="/vepbundle"></script>
+          <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script>
+      <% } %>
 	</body>
 </html>
diff --git a/browserid/views/forgot.ejs b/resources/views/forgot.ejs
similarity index 55%
rename from browserid/views/forgot.ejs
rename to resources/views/forgot.ejs
index b280f3f463253fa63f3673465396e16c2031b611..763420c42f8d2d04d8ee660550bb85a42fd0a2ab 100644
--- a/browserid/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -1,7 +1,7 @@
-<div id="vAlign">   
+<div id="vAlign" class="display_always">   
     <div id="signUpFormWrap">
         <!-- XXX this form submits to nowhere -->
-        <form id="signUpForm" class="cf authform">
+        <form id="signUpForm" class="cf authform" novalidate>
             <h1 class="serif">Forgot Password</h1>
             <div class="notifications">
                 <div class="notification error doh">Doh! Something went wrong :-( </div>
@@ -12,8 +12,26 @@
             <ul class="inputs">
                 <li>
                     <label class="serif" for="email">Email Address</label>
-                    <input class="sans" id="email" autofocus required placeholder="Your Email" type="email" x-moz-errormessage="Please enter the email address you would like to use">
+                    <input class="sans" id="email" autofocus required placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
+
+                    <div id="email_format" class="tooltip" for="email">
+                      This field must be an email address.
+                    </div>
+
+                    <div id="email_required" class="tooltip" for="email">
+                      The email field is required.
+                    </div>
+
+                    <div id="could_not_add" class="tooltip" for="email">
+                      We just sent an email to that address! If you really want to send another, wait a minute or two and try again.
+                    </div>
+
+                    <div id="not_registered" class="tooltip" for="email">
+                      Non existent user!
+                    </div>
                 </li>
+
+
             </ul>
             <div class="submit cf">
                 <div class="remember cf">
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..6873159afed75c94a1ed99549570c79f19f4ad85
--- /dev/null
+++ b/resources/views/index.ejs
@@ -0,0 +1,43 @@
+  <div id="content" style="display:none" class="display_auth">
+      <div id="manage">
+          <h1 class="serif">Account Manager</h1>
+          <div class="edit cf">
+              <strong>Your Email Addresses</strong>
+
+              <a id="manageAccounts" href="#">edit</a>
+              <a id="cancelManage" href="#">done</a>
+          </div>
+          <ul id="emailList">
+          </ul>
+          <div id="disclaimer">You may, at any time, <a href="#" id="cancelAccount">cancel your account</a></div>
+      </div>
+  </div>
+
+  <div id="vAlign" class="display_nonauth">
+      <div id="signUp">
+          <div id="card"><img src="/i/slit.png"></div>
+          <div id="hint"></div>
+          <div id="status"></div>
+
+          <p>Connect with <em>BrowserID</em>, the safest &amp; easiest way to sign in.</p>
+          <p>
+            <a class="granted info" href="/about">Take the tour</a> 
+            <span class="require-js">or 
+              <a href="/signup" class="button granted create">sign up</a>
+            </span>
+          </p>
+      </div>
+  </div>
+
+  <script type="text/html" id="templateUser">
+    <li class="identity cf">
+      <div class="email">{{ email }}</div>
+      <div class="activity cf">
+        <button class="delete">remove</button>
+        <!-- removed registration info. We want to replace this with Last Used At ... -->
+        <!-- <abbr title="Registered: {{ created }}" class="status">Registered {{ relative }}.</abbr>-->
+      </div>
+    </li>
+  </script>
+
+
diff --git a/browserid/views/layout.ejs b/resources/views/layout.ejs
similarity index 94%
rename from browserid/views/layout.ejs
rename to resources/views/layout.ejs
index d012c52ef50d190e05aeb0348c131e27f1b279d4..c8009278cbd81dbc653f4d5c123d01671a6733a7 100644
--- a/browserid/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -19,6 +19,8 @@
     <script src="/js/json2.js" type="text/javascript"></script>
     <script src="/dialog/resources/underscore-min.js" type="text/javascript"></script>
     <script src="/dialog/resources/browserid-extensions.js" type="text/javascript"></script>
+    <script src="/dialog/resources/browserid.js" type="text/javascript"></script>
+    <script src="/js/page_helpers.js" type="text/javascript"></script>
     <script src="/js/browserid.js" type="text/javascript"></script>
     <script src="/js/pages/index.js" type="text/javascript"></script>
     <script src="/dialog/resources/storage.js" type="text/javascript"></script>
@@ -33,7 +35,7 @@
     <script src="/js/pages/signin.js" type="text/javascript"></script>
     <script src="/js/pages/signup.js" type="text/javascript"></script>
   <% } %>
-  <title><%- title %></title>
+  <title>BrowserID: <%- title %></title>
 </head>
 <body>
 
@@ -43,6 +45,7 @@
         <ul class="cf">
             <li><a class="home" href="/"></a></li>
         </ul>
+
         <ul class="nav cf">
             <li><a href="/about">How it works</a></li>
             <li><a href="https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site" target="_blank">Developers</a></li>
diff --git a/browserid/views/privacy.ejs b/resources/views/privacy.ejs
similarity index 95%
rename from browserid/views/privacy.ejs
rename to resources/views/privacy.ejs
index 1c007ff475b0a6245e355f97805f15da4738a73b..07b662d193210086615d8be2ea35f2702c41a1a3 100644
--- a/browserid/views/privacy.ejs
+++ b/resources/views/privacy.ejs
@@ -1,4 +1,4 @@
-<div id="content">
+<div id="content" class="display_always">
     <div id="legal">
       <h2>Privacy & BrowserID</h2>
       
@@ -57,7 +57,7 @@
       <p>Mozilla is an open organization that believes in sharing as much information as possible about its products, its operations, and its  associations with its wider community. As such, BrowserID Service users should expect that Mozilla will make all Usage Statistics publicly  available at some point. However, any publicly available Usage Statistics will only be reported on an aggregate, anonymous basis. No Personal Information or Potentially Personal Information will be  available in any of these public reports.</p>
       
       <h5>How to Disable or Opt-Out of BrowserID</h5>
-      <p>If at any time, you decide you no longer want to use the BrowserID Service, you may cancel your BrowserID Account by visiting <tt>https://browserid.org/</tt>, signing in using any of your email addresses and your password, clicking the "edit" button, and clicking "remove" next to each of your email addresses.</p>
+      <p>If at any time, you decide you no longer want to use the BrowserID Service, you may cancel your BrowserID Account by visiting <kbd>https://browserid.org/</kbd>, signing in using any of your email addresses and your password, clicking the "edit" button, and clicking "remove" next to each of your email addresses.</p>
 
       <h5>Other Disclosures</h5>
       <p>In certain other limited situations, Mozilla may disclose your Personal Information, such as when necessary to protect our  websites and operations (e.g., against attacks); to protect the rights, privacy, safety, or property of Mozilla or its users; to enforce our  terms of service; and to pursue available legal remedies. Additionally, Mozilla may need to transfer Personal Information to an affiliate or successor in the event of a change of our corporate structure or status, such as in the event of a restructuring, sale, or bankruptcy.</p>
@@ -79,14 +79,14 @@
       
       <h5>For More Information</h5>
 <p>You may request access, correction, or deletion of Personal Information or Potentially Personal Information, as permitted by law. We will seek to comply with such requests, provided that we have sufficient information to identify the Personal Information or Potentially Personal Information related to you. Any such requests or other questions or concerns regarding this Policy and Mozilla's data protection practices should be addressed to:</p>
-      <blockquote><pre>
-        Mozilla Corporation
-        Attn: Legal Notices – Privacy
-        650 Castro Street, Suite 300
-        Mountain View, CA 94041-2072
-        Phone: +1-650-903-0800
-        E-mail: privacy@mozilla.com
-        </pre>
-      </blockquote>
+<blockquote><pre>
+Mozilla Corporation 
+Attn: Legal Notices – Privacy 
+650 Castro Street, Suite 300 
+Mountain View, CA 94041-2072 
+Phone: +1-650-903-0800 
+E-mail: <a href="mailto:privacy@mozilla.com">privacy@mozilla.com</a>
+</pre>
+</blockquote>
     </div>
 </div>
diff --git a/browserid/views/relay.ejs b/resources/views/relay.ejs
similarity index 62%
rename from browserid/views/relay.ejs
rename to resources/views/relay.ejs
index b6f4cc4f87f190231ac5fffe8912ea1a9c778823..1fc38b014dd59ab903ef989de9604ece89eb63c2 100644
--- a/browserid/views/relay.ejs
+++ b/resources/views/relay.ejs
@@ -13,8 +13,20 @@
       <% } else { %>
         <script type='text/javascript' 
           src='https://browserid.org/dialog/resources/jschannel.js'></script>
+        <script type='text/javascript' 
+          src='https://browserid.org/dialog/resources/browserid.js'></script>
         <script type='text/javascript' 
           src='https://browserid.org/relay/relay.js'></script>
       <% } %>
+
+      <script type="text/javascript">
+        var relay = BrowserID.Relay;
+        relay.init({
+          channel: Channel,
+          window: window.parent
+        });
+
+        relay.open();
+      </script>
 	</body>
 </html>
diff --git a/browserid/views/signin.ejs b/resources/views/signin.ejs
similarity index 89%
rename from browserid/views/signin.ejs
rename to resources/views/signin.ejs
index 54089605e911f90aa02d54fbdd087e51f17f8548..9c4a90874ac5ec267182e9152746cf911e3ed1be 100644
--- a/browserid/views/signin.ejs
+++ b/resources/views/signin.ejs
@@ -1,4 +1,4 @@
-<div id="vAlign">
+<div id="vAlign" class="disply_always">
     <div id="signUpFormWrap">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
@@ -11,7 +11,7 @@
             <ul class="inputs">
                 <li>
                     <label class="serif" for="email">Email Address</label>
-                    <input class="sans" id="email" autofocus placeholder="Your Email" type="email" tabindex="1">
+                    <input class="sans" id="email" autofocus placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" tabindex="1" maxlength="254" />
 
                     <div id="email_format" class="tooltip" for="email">
                       This field must be an email address.
@@ -49,3 +49,7 @@
     </div>
 </div>
 
+<noscript>
+  We're sorry, but to sign in to BrowserID, you must have Javascript enabled.
+</noscript>
+
diff --git a/browserid/views/signup.ejs b/resources/views/signup.ejs
similarity index 86%
rename from browserid/views/signup.ejs
rename to resources/views/signup.ejs
index 29a6f662fbeba3818ed61d68d665b5386ce15b33..cc95cd345924ba515e9ee5f54addbfb4b930003a 100644
--- a/browserid/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -1,4 +1,4 @@
-<div id="vAlign">   
+<div id="vAlign" class="display_always">   
     <div id="signUpFormWrap">
         <!-- XXX this form submits to nowhere -->
         <form id="signUpForm" class="cf authform" novalidate>
@@ -13,7 +13,7 @@
             <ul class="inputs forminputs">
                 <li>
                     <label class="serif" for="email">Email Address</label>
-                    <input class="sans" id="email" autofocus placeholder="Your Email" type="email" />
+                    <input class="sans" id="email" autofocus placeholder="Your Email" type="email" autocapitalize="off" autocorrect="off" maxlength="254" />
 
                     <div id="email_format" class="tooltip" for="email">
                       This field must be an email address.
@@ -36,3 +36,9 @@
     </div>
 </div>
 
+<noscript>
+  We're sorry, but to sign up for BrowserID, you must have Javascript enabled.
+</noscript>
+
+
+
diff --git a/browserid/views/tos.ejs b/resources/views/tos.ejs
similarity index 99%
rename from browserid/views/tos.ejs
rename to resources/views/tos.ejs
index 53bae5ecb4ceed9c3164dcacbb091e6d6f9f892c..fe33bca8de3621d044749c3b821ac6a702585014 100644
--- a/browserid/views/tos.ejs
+++ b/resources/views/tos.ejs
@@ -1,4 +1,4 @@
-<div id="content">
+<div id="content" class="display_always">
   <div id="legal">
   <h2>Terms of Service &mdash; Overview</h2>
   
diff --git a/resources/views/unsupported_dialog.ejs b/resources/views/unsupported_dialog.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..eac8bf8ade00ba07b18986924c30729c6a355c2f
--- /dev/null
+++ b/resources/views/unsupported_dialog.ejs
@@ -0,0 +1,28 @@
+  <section id="error" style="display: block" class="unsupported">
+      <div class="table">
+          <div class="vertical contents">
+              <div id="reason">
+                We're sorry, but currently your browser isn't supported.
+              </div>
+
+              <div id="alternative">
+
+                <div id="borderbox">
+                  <a href="http://getfirefox.com" target="_blank">
+                    <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" />
+                  </a>
+
+                  <p>
+                    BrowserID works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a>
+                  </p>
+
+                  <p class="lighter">
+                    and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>
+                  </p>
+                </div>
+
+              </div>
+
+          </div>
+      </div>
+  </section>
diff --git a/browserid/views/verifyemail.ejs b/resources/views/verifyemail.ejs
similarity index 82%
rename from browserid/views/verifyemail.ejs
rename to resources/views/verifyemail.ejs
index ea5f075deb102ca457dec25ac992045f83a6e2ef..d9ee49385494c4069cba00874bbe0023e661a419 100644
--- a/browserid/views/verifyemail.ejs
+++ b/resources/views/verifyemail.ejs
@@ -1,4 +1,4 @@
-<div id="vAlign">   
+<div id="vAlign" class="display_always">   
     <div id="signUpFormWrap">
         <div id="signUpForm" class="cf">
             <h1 class="serif">Email Verification</h1>
@@ -17,9 +17,9 @@
                 <strong id="email">Your address</strong> has been verified!
 
                 <span id="siteinfo">
-                  Your new address has been used to sign in to 
+                  Your new address is set up and you should now be signed in.
+                  You may now close this window and go back to 
                   <strong class="website"></strong> 
-                  which is shown in its original window or tab.
                 </span>
             </p>
         </div>
diff --git a/browserid/views/verifyuser.ejs b/resources/views/verifyuser.ejs
similarity index 94%
rename from browserid/views/verifyuser.ejs
rename to resources/views/verifyuser.ejs
index 3cca2c5284310bb92600274b5e04e6efc1b181fb..b173f3580063d842a085e950083669dabf57a4d5 100644
--- a/browserid/views/verifyuser.ejs
+++ b/resources/views/verifyuser.ejs
@@ -1,4 +1,4 @@
-<div id="vAlign">   
+<div id="vAlign" class="display_always">   
   <div id="signUpFormWrap">
     <ul class="notifications">
         <li class="notification error" id="badtoken">There was a problem with your signup link.  Has this address already been registered?</li>
@@ -14,7 +14,7 @@
       <ul class="inputs">
         <li>
             <label class="serif" for="email">Email Address</label>
-            <input class="youraddress sans" id="email" placeholder="Your Email" type="email" value="" disabled="disabled">
+            <input class="youraddress sans" id="email" placeholder="Your Email" type="email" value="" disabled="disabled" maxlength="254">
         </li>
         <li>
             <label class="serif" for="password">New Password</label>
diff --git a/rp/index2.html b/rp/index2.html
deleted file mode 100644
index cdc25d3781af1b7fc01affcba7c6b7276833ed2c..0000000000000000000000000000000000000000
--- a/rp/index2.html
+++ /dev/null
@@ -1,141 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<title>
-BrowserID Relying Party
-</title>
-<link href='http://fonts.googleapis.com/css?family=Permanent+Marker' rel='stylesheet' type='text/css'>
-<style type="text/css">
-
-body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif; }
-a:link, a:visited { font-style: italic; text-decoration: none; color: #008; }
-a:hover { border-bottom: 2px solid black ; }
-.number { font-family: 'Permanent Marker', arial, serif; font-size: 4em; float: left; padding: 0; margin: 0; vertical-align: top; width: 1.3em}
-.title { font-size: 2em; font-weight: bold; text-align: center; margin: 1.5em; }
-.intro { font-size: 1.2em; width: 600px; margin: auto; }
-.step { width: 600px; margin: auto; margin-top: 1em;}
-.desc { padding-top: 1.5em; min-height: 4.5em;}
-.output {
-  font-family: 'lucida console', monaco, 'andale mono', 'bitstream vera sans mono', consolas, monospace;
-  border: 3px solid #666;
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-  padding: .5em;
-  margin: .5em;
-  color: #ccc;
-  background-color: #333;
-/*  white-space: pre;*/
-  font-size: .9em;
-  width:600px;
-  word-wrap: break-word;
-}
-
-</style>
-</head>
-<body>
-<div class="title">
-  Example BrowserID Relying Party - Specific Identity
-</div>
-
-<div class="intro">
-  This is the simplest possible (stateless, static, client-only)  BrowserID Relying Party.  It
-  demonstrates the steps required to use BrowserID to verify the identity of a user.<br />
-  <a id="tokenGetter" href="#">Click here to preauth</a>. <span id="token"></span><br />
-  <a id="badpartyStarter" href="#">Click here to kick off the bad party</a>.<br />
-  <a id="partyStarter" href="#">Click here to kick off the party</a>.<br />  Here's what will happen:
-</div>
-
-<div class="step">
-  <div class="number">1.</div>
-  <div class="desc"><b>Browser Interaction:</b> Upon clicking the link above, the webpage will call <tt>navigator.id.getSpecificVerifiedEmail()</tt> to indicate to the browser that it would like an identity for the user</div>
-</div>
-
-<div class="step">
-  <div class="number">2.</div>
-  <div class="desc"><b>User Interaction:</b> The browser will spawn a dialog that the user can interact with the select what identity they want to provide to the site </div>
-</div>
-
-<div class="step">
-  <div class="number">3.</div>
-  <div class="desc"><b>Assertion Generation:</b> Upon selection of an identity, an <i>assertion</i> will be returned to the webpage via a callback, it looks like this: </div>
-  <div class="output" id="oAssertion">...waiting for party commencement...</div>
-</div>
-
-<div class="step">
-  <div class="number">4.</div>
-  <div class="desc"><b>Assertion Verification:</b> This site can then send that assertion up to a <i>verification server</i> which cryptographically checks that the identity embedded in the verification actually belongs to the browser in use by the monkey at the keyboard.  The request looks like </div>
-  <div class="output" id="oVerificationRequest">...waiting for party commencement...</div>
-</div>
-
-<div class="step">
-  <div class="number">5.</div>
-  <div class="desc"><b>Verification Response</b>: The verification server responds to the site to tell it whether or not the verification blob is valid:</div>
-  <div class="output" id="oVerificationResponse">...waiting for party commencement...</div>
-</div>
-
-<div class="step">
-  <div class="number">6.</div>
-  <div class="desc"><b>All Done!</b>  The site can now create an account keyed on the users identity (email address), set cookies, etc!  Signing in again is just re-running these same steps.</div>
-</div>
-
-</body>
-<script src="jquery-min.js"></script>
-<script src="https://browserid.org/include.js"></script>
-<script>
-  function dumpObject(obj) {
-    var htmlRep = "";
-    for (var k in obj) {
-      if (obj.hasOwnProperty(k) && typeof obj[k] === 'string') {
-        htmlRep += "<b>" + k + ":</b> " + obj[k] + "<br/>";
-      } else if (k === 'valid-until') {
-        htmlRep += "<b>" + k + ":</b> " + (new Date(obj[k])).toString() + "<br/>";
-      }
-
-    }
-    return htmlRep;
-  }
-
-  function start_the_party(email, token) {
-      navigator.id.getSpecificVerifiedEmail(email, token, function(assertion) {
-        // Now we'll send this assertion over to the verification server for validation
-        $("#oAssertion").empty().html(dumpObject(assertion));
-
-        var url = "http://browserid.org/verify?assertion=" + window.encodeURIComponent(assertion) +
-                  "&audience=" + window.encodeURIComponent("rp.eyedee.me");
-        $("#oVerificationRequest").empty().text(url);
-
-        $.ajax({
-          url: url,
-          success: function(data, textStatus, jqXHR) {
-            $("#oVerificationResponse").empty().text(JSON.stringify(data, null, 4));
-          },
-          error: function(jqXHR, textStatus, errorThrown) {
-            $("#oVerificationResponse").empty().text(jqXHR.responseText);
-          }
-        })
-      }, function(code, msg) {
-        alert("something very bad happened! ("+code+"): " + msg); 
-      });
-  }
-
-  $(document).ready(function() {
-    var GUID = null;
-    $("#tokenGetter").click(function() {
-      navigator.id.preauthEmail("ben@adida.net", function(guid) {      
-        $('#token').html(guid);
-        GUID=guid;
-      });
-    });
-
-    $("#partyStarter").click(function() {
-      start_the_party('ben@adida.net', GUID);
-    });
-
-    $("#badpartyStarter").click(function() {
-      start_the_party('ben2@adida.net', GUID);
-    });
-  });
-</script>
-
-</html>
diff --git a/run.js b/run.js
deleted file mode 100755
index 0d82815313779d4df0fe965102506067b9801eaa..0000000000000000000000000000000000000000
--- a/run.js
+++ /dev/null
@@ -1,189 +0,0 @@
-#!/usr/bin/env node
-
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla BrowserID.
- *
- * The Initial Developer of the Original Code is Mozilla.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-// a little node webserver designed to run the unit tests herein
-
-var      sys = require("sys"),
-        http = require("http"),
-         url = require("url"),
-        path = require("path"),
-          fs = require("fs"),
-     express = require("express"),
-substitution = require('./libs/substitute.js');
-
-// when running under the harness, let's also output log messages to the terminal
-require('./libs/logging.js').enableConsoleLogging();
-
-var configuration = require('./libs/configuration.js');
-
-var PRIMARY_HOST = process.env.IP_ADDRESS || "127.0.0.1";
-
-var boundServers = [ ];
-
-var subs = undefined;
-function substitutionMiddleware(req, resp, next) {
-  if (!subs) {
-    subs = { };
-    for (var i = 0; i < boundServers.length; i++) {
-      var o = boundServers[i]
-      var a = o.server.address();
-      var from = o.name;
-      var to = "http://" + a.address + ":" + a.port;
-      subs[from] = to;
-
-      // now do another replacement to catch bare hostnames sans http(s)
-      // and explicit cases where port is appended
-      var fromWithPort;
-      if (from.substr(0,5) === 'https') {
-        from = from.substr(8);
-        fromWithPort = from + ":443";
-      } else {
-        from = from.substr(7);
-        fromWithPort = from + ":80";
-      }
-      to = to.substr(7);
-
-      if (o.subPath) to += o.subPath;
-
-      subs[fromWithPort] = to;
-      subs[from] = to;
-    }
-  }
-  (substitution.substitute(subs))(req, resp, next);
-}
-
-function createServer(obj) {
-  var app = express.createServer();
-
-  // this file is a *test* harness, to make it go, we'll insert a little
-  // handler that substitutes output, changing production URLs to
-  // developement URLs.
-  app.use(substitutionMiddleware);
-
-  // let the specific server interact directly with the express server to
-  // register their middleware, routes, etc...
-  if (obj.setup) obj.setup(app);
-
-  // now set up the static resource servin'
-  var p = obj.path, ps = path.join(p, "static");
-  try { if (fs.statSync(ps).isDirectory()) p = ps; } catch(e) { }
-  app.use(express.static(p));
-
-  // and listen!
-  app.listen(obj.port, PRIMARY_HOST);
-  return app;
-};
-
-// start up webservers on ephemeral ports for each subdirectory here.
-var dirs = [
-  // the reference verification server.  A version is hosted at
-  // browserid.org and may be used, or the RP may perform their
-  // own verification.
-  {
-    name: "https://browserid.org/verify",
-    subPath: "/",
-    path: path.join(__dirname, "verifier")
-  },
-  // An example relying party.
-  {
-    name: "http://rp.eyedee.me",
-    path: path.join(__dirname, "rp")
-  },
-
-  // BrowserID: the secondary + ip + more.
-  {
-    name: "https://browserid.org",
-    path: path.join(__dirname, "browserid")
-  }
-];
-
-function formatLink(server, extraPath) {
-  var addr = server.address();
-  var url = 'http://' + addr.address + ':' + addr.port;
-  if (extraPath) {
-    url += extraPath;
-  }
-  return url;
-}
-
-console.log("Running test servers:");
-
-var port_num=10000;
-dirs.forEach(function(dirObj) {
-  if (!fs.statSync(dirObj.path).isDirectory()) return;
-  // does this server have a js handler for custom request handling?
-  var handlerPath = path.join(dirObj.path, "app.js");
-  var runJS = {};
-  try {
-    var runJSExists = false;
-    try { runJSExists = fs.statSync(handlerPath).isFile() } catch(e) {};
-    if (runJSExists) runJS = require(handlerPath);
-  } catch(e) {
-    console.log("Error loading " + handlerPath + ": " + e);
-    process.exit(1);
-  }
-
-  var so = {
-    path: dirObj.path,
-    server: undefined,
-    port: port_num++,
-    name: dirObj.name,
-    handler: runJS.handler,
-    setup: runJS.setup,
-    shutdown: runJS.shutdown,
-    subPath: dirObj.subPath
-  };
-  so.server = createServer(so)
-  boundServers.push(so);
-  console.log("  " + dirObj.name + ": " + formatLink(so.server));
-});
-
-process.on('SIGINT', function () {
-  console.log('\nSIGINT recieved! trying to shut down gracefully...');
-  boundServers.forEach(function(bs) {
-    if (bs.shutdown) bs.shutdown();
-    bs.server.on('close', function() {
-      console.log("server shutdown,", bs.server.connections, "connections still open...");
-    });
-    bs.server.close();
-  });
-  // exit more harshly in 700ms
-  setTimeout(function() {
-    console.log("exiting...");
-    process.exit(0);
-  }, 700);
-});
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
new file mode 100644
index 0000000000000000000000000000000000000000..368194a57e5c834075678777fc41efd46ef658c4
--- /dev/null
+++ b/scripts/browserid.spec
@@ -0,0 +1,45 @@
+%define _rootdir /opt/browserid
+
+Name:          browserid-server
+Version:       0.2011.10.13
+Release:       1%{?dist}
+Summary:       BrowserID server
+Packager:      Pete Fritchman <petef@mozilla.com>
+Group:         Development/Libraries
+License:       MPL 1.1+/GPL 2.0+/LGPL 2.1+
+URL:           https://github.com/mozilla/browserid
+Source0:       %{name}.tar.gz
+BuildRoot:     %{_tmppath}/%{name}-%{version}-%{release}-root
+AutoReqProv:   no
+Requires:      openssl nodejs
+BuildRequires: gcc-c++ git jre make npm openssl-devel
+
+%description
+browserid server & web home for browserid.org
+
+%prep
+%setup -q -n browserid
+
+%build
+npm install
+export PATH=$PWD/node_modules/.bin:$PATH
+(cd browserid && ./compress.sh)
+git log -1 --oneline > browserid/static/ver.txt
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_rootdir}
+for f in browserid libs node_modules verifier *.json *.js; do
+    cp -rp $f %{buildroot}%{_rootdir}/$dir
+done
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%{_rootdir}
+
+%changelog
+* Tue Oct 18 2011 Pete Fritchman <petef@mozilla.com>
+- Initial version
diff --git a/browserid/compress.sh b/scripts/compress.sh
similarity index 61%
rename from browserid/compress.sh
rename to scripts/compress.sh
index 3a493518a47c68c4aae8037fcf3931ac12a50e2f..6233646e5d25ab275e4efb3e87b30ae77697922e 100755
--- a/browserid/compress.sh
+++ b/scripts/compress.sh
@@ -1,5 +1,7 @@
 #!/bin/sh
 
+cd $(dirname "$0")/..
+
 UGLIFY=`which uglifyjs 2> /dev/null`
 if [ ! -x "$UGLIFY" ]; then
     echo "uglifyjs not found in your path.  can't create production resources.  disaster."
@@ -12,13 +14,13 @@ if [ ! -x "$JAVA" ]; then
     exit 1
 fi
 
-YUI_LOCATION=`pwd`'/static/steal/build/scripts/yui.jar'
+YUI_LOCATION=`pwd`'/resources/static/steal/build/scripts/yui.jar'
 
 echo ''
 echo '****Compressing include.js****'
 echo ''
 
-cd static
+cd resources/static
 mv include.js include.orig.js
 $UGLIFY include.orig.js > include.js
 
@@ -37,7 +39,7 @@ cat popup.css m.css > production.css
 $JAVA -jar $YUI_LOCATION production.css -o production.min.css
 
 cd ../../relay
-cat ../dialog/resources/jschannel.js relay.js > production.js
+cat ../dialog/resources/jschannel.js ../dialog/resources/browserid.js relay.js > production.js
 $UGLIFY < production.js > production.min.js
 mv production.min.js production.js
 
@@ -48,7 +50,7 @@ echo ''
 
 cd ../js
 # re-minimize everything together
-cat jquery-1.6.2.min.js json2.js browserid.js ../dialog/resources/underscore-min.js ../dialog/resources/browserid-extensions.js ../dialog/resources/storage.js ../dialog/resources/network.js ../dialog/resources/user.js ../dialog/resources/tooltip.js ../dialog/resources/validation.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/manage_account.js pages/signin.js pages/signup.js pages/forgot.js > lib.js
+cat jquery-1.6.2.min.js json2.js ../dialog/resources/browserid.js page_helpers.js browserid.js ../dialog/resources/underscore-min.js ../dialog/resources/browserid-extensions.js ../dialog/resources/storage.js ../dialog/resources/network.js ../dialog/resources/user.js ../dialog/resources/tooltip.js ../dialog/resources/validation.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/manage_account.js pages/signin.js pages/signup.js pages/forgot.js > lib.js
 $UGLIFY < lib.js > lib.min.js
 
 cd ../css
diff --git a/scripts/rpmbuild.sh b/scripts/rpmbuild.sh
new file mode 100755
index 0000000000000000000000000000000000000000..46f30b1eeea913a6d94135524d19fbccd6e7db77
--- /dev/null
+++ b/scripts/rpmbuild.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -e
+
+progname=$(basename $0)
+
+cd $(dirname $0)/..    # top level of the checkout
+
+curdir=$(basename $PWD)
+if [ "$curdir" != "browserid" ]; then
+    echo "$progname: git checkout must be in a dir named 'browserid'" >&2
+    exit 1
+fi
+
+mkdir -p rpmbuild/SOURCES rpmbuild/SPECS
+rm -rf rpmbuild/RPMS
+
+tar -C .. --exclude rpmbuild -czf \
+    $PWD/rpmbuild/SOURCES/browserid-server.tar.gz browserid
+
+set +e
+
+rpmbuild --define "_topdir $PWD/rpmbuild" -ba scripts/browserid.spec
+rc=$?
+if [ $rc -eq 0 ]; then
+    ls -l $PWD/rpmbuild/RPMS/*/*.rpm
+else
+    echo "$progname: failed to build browserid RPM (rpmbuild rc=$rc)" >&2
+fi
+
+exit $rc
diff --git a/scripts/run_all_tests.sh b/scripts/run_all_tests.sh
index ed6ab62f2f1afaa86b8cfe1974258dddb0e0d0bc..3976cd66fab79b019c05aa4c05f6cbfee37cccb8 100755
--- a/scripts/run_all_tests.sh
+++ b/scripts/run_all_tests.sh
@@ -16,9 +16,9 @@ cd $BASEDIR
 for env in test_json test_mysql ; do
   export NODE_ENV=$env
   $SCRIPT_DIR/test_db_connectivity.js
-  if [ $? = 0 ] ; then 
+  if [ $? = 0 ] ; then
       echo "Testing with NODE_ENV=$env"
-      for file in browserid/tests/*.js ; do
+      for file in tests/*.js ; do
           echo $file
           vows $file
           if [[ $? != 0 ]] ; then
diff --git a/scripts/run_locally.js b/scripts/run_locally.js
new file mode 100755
index 0000000000000000000000000000000000000000..25ded3dcce690b0fae6302c17b56af024eb81917
--- /dev/null
+++ b/scripts/run_locally.js
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+
+const
+spawn = require('child_process').spawn,
+path = require('path');
+
+exports.daemons = daemons = {};
+
+const HOST = process.env['IP_ADDRESS'] || process.env['HOST'] || "127.0.0.1";
+
+var daemonsToRun = {
+  verifier: {
+    PORT: 10000,
+    HOST: HOST
+  },
+  keysigner: {
+    PORT: 10003,
+    HOST: HOST
+  },
+  example: {
+    path: path.join(__dirname, "..", "scripts", "serve_example.js"),
+    PORT: 10001,
+    HOST: HOST
+  },
+  browserid: {
+    PORT: 10002,
+    HOST: HOST
+  }
+};
+
+// all spawned processes should log to console
+process.env['LOG_TO_CONSOLE'] = 1;
+
+// all spawned processes will communicate with the local browserid
+process.env['BROWSERID_URL'] = 'http://' + HOST + ":10002";
+process.env['VERIFIER_URL'] = 'http://' + HOST + ":10000/verify";
+process.env['KEYSIGNER_URL'] = 'http://' + HOST + ":10003";
+
+Object.keys(daemonsToRun).forEach(function(k) {
+  Object.keys(daemonsToRun[k]).forEach(function(ek) {
+    process.env[ek] = daemonsToRun[k][ek];
+  });
+  var pathToScript = daemonsToRun[k].path || path.join(__dirname, "..", "bin", k);
+  var p = spawn('node', [ pathToScript ]);
+
+  function dump(d) {
+    d.toString().split('\n').forEach(function(d) {
+      if (d.length === 0) return;
+      console.log(k, '(' + p.pid + '):', d);
+    });
+  }
+
+  p.stdout.on('data', dump);
+  p.stderr.on('data', dump);
+
+  console.log("spawned", k, "("+pathToScript+") with pid", p.pid);
+  Object.keys(daemonsToRun[k]).forEach(function(ek) {
+    delete process.env[ek];
+  });
+
+  daemons[k] = p;
+
+  p.on('exit', function (code, signal) {
+    console.log(k, 'exited with code', code, (signal ? 'on signal ' + signal : ""));
+    delete daemons[k];
+    Object.keys(daemons).forEach(function (k) { daemons[k].kill(); });
+    if (Object.keys(daemons).length === 0) {
+      console.log("all daemons torn down, exiting...");
+    }
+  });
+});
+
+process.on('SIGINT', function () {
+  console.log('\nSIGINT recieved! trying to shut down gracefully...');
+  Object.keys(daemons).forEach(function (k) { daemons[k].kill('SIGINT'); });
+});
diff --git a/scripts/serve_example.js b/scripts/serve_example.js
new file mode 100755
index 0000000000000000000000000000000000000000..904081830eca2f7392ede8c1d7434c488d402b01
--- /dev/null
+++ b/scripts/serve_example.js
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+
+// finally, let's run a tiny webserver for the example code.
+const
+express = require('express'),
+path = require('path'),
+urlparse = require('urlparse'),
+postprocess = require('postprocess');
+
+var exampleServer = express.createServer();
+
+exampleServer.use(express.logger());
+
+if (process.env['BROWSERID_URL']) {
+  var burl = urlparse(process.env['BROWSERID_URL']).validate().normalize().originOnly().toString();
+  console.log('using browserid server at ' + burl);
+
+  exampleServer.use(postprocess.middleware(function(req, buffer) {
+    return buffer.toString().replace(new RegExp('https://browserid.org', 'g'), burl);
+  }));
+}
+
+exampleServer.use(express.static(path.join(__dirname, "..", "example")));
+
+exampleServer.listen(
+  process.env['PORT'] || 10001,
+  process.env['HOST'] || process.env['IP_ADDRESS'] || "127.0.0.1",
+  function() {
+    var addy = exampleServer.address();
+    console.log("http://" + addy.address + ":" + addy.port);
+  });
diff --git a/scripts/test_db_connectivity.js b/scripts/test_db_connectivity.js
index 2a446931742ee6b531700c78839d835acaee104c..acdfdad023c69204e2a8838fc5e734d9c9e0427e 100755
--- a/scripts/test_db_connectivity.js
+++ b/scripts/test_db_connectivity.js
@@ -4,8 +4,8 @@
 // the database using the present configuration.
 
 const
-configuration = require('../libs/configuration.js'),
-db = require('../browserid/lib/db.js');
+configuration = require('../lib/configuration.js'),
+db = require('../lib/db.js');
 
 var dbCfg = configuration.get('database');
 
diff --git a/test.sh b/test.sh
deleted file mode 100755
index b41801ed1cbc80f3f397258f877b52aa5a4e0910..0000000000000000000000000000000000000000
--- a/test.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-npm test
diff --git a/browserid/tests/ca-test.js b/tests/ca-test.js
similarity index 97%
rename from browserid/tests/ca-test.js
rename to tests/ca-test.js
index 2dfd984ee4120a048df26965b0b3537c76941316..c990798d0e1439fb3ba835171b27e6bafaac4742 100755
--- a/browserid/tests/ca-test.js
+++ b/tests/ca-test.js
@@ -41,8 +41,8 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js'),
-ca = require('../lib/ca.js'),
+email = require('browserid/email.js'),
+ca = require('browserid/ca.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
 jws = require('jwcrypto/jws');
diff --git a/browserid/tests/cert-emails-test.js b/tests/cert-emails-test.js
similarity index 90%
rename from browserid/tests/cert-emails-test.js
rename to tests/cert-emails-test.js
index 2b3ef1cafe5b7229983a55dca63725dcf146f07c..b8e353fe3057b5829c60bfb901199f23ed9257a3 100755
--- a/browserid/tests/cert-emails-test.js
+++ b/tests/cert-emails-test.js
@@ -41,8 +41,8 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js'),
-ca = require('../lib/ca.js'),
+email = require('browserid/email.js'),
+ca = require('browserid/ca.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
 jws = require('jwcrypto/jws'),
@@ -58,21 +58,36 @@ start_stop.addStartupBatches(suite);
 // ever time a new token is sent out, let's update the global
 // var 'token'
 var token = undefined;
-email.setInterceptor(function(email, site, secret) { token = secret; });
+start_stop.browserid.on('token', function(secret) {
+  token = secret;
+});
 
 // INFO: some of these tests are repeat of sync-emails... to set
 // things up properly for key certification
 
 // create a new account via the api with (first address)
 suite.addBatch({
-  "stage an account": {
+  "staging an account": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'syncer@somehost.com',
       pubkey: 'fakekey',
       site:'fakesite.com'
     }),
-    "yields a sane token": function(r, err) {
-      assert.strictEqual(typeof token, 'string');
+    "succeeds": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
     }
   }
 });
@@ -112,13 +127,13 @@ suite.addBatch({
       assert.strictEqual(r.code, 400);
     }
   },
-  "cert key invoked with just an email": {  
+  "cert key invoked with just an email": {
     topic: wsapi.post(cert_key_url, { email: 'syncer@somehost.com' }),
     "returns a 400" : function(r, err) {
       assert.strictEqual(r.code, 400);
     }
   },
-  "cert key invoked with proper argument": {  
+  "cert key invoked with proper argument": {
     topic: wsapi.post(cert_key_url, { email: 'syncer@somehost.com', pubkey: kp.publicKey.serialize() }),
     "returns a response with a proper content-type" : function(r, err) {
       assert.strictEqual(r.code, 200);
diff --git a/browserid/tests/cookie-session-security-test.js b/tests/cookie-session-security-test.js
similarity index 96%
rename from browserid/tests/cookie-session-security-test.js
rename to tests/cookie-session-security-test.js
index 3a1d18817dd5a44f2d73c27099c7ffd088de1624..dad6e25452b47e29e050ecd97b2177153f5582c6 100755
--- a/browserid/tests/cookie-session-security-test.js
+++ b/tests/cookie-session-security-test.js
@@ -41,9 +41,9 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-wcli = require('../../libs/wsapi_client');
-email = require('../lib/email.js'),
-ca = require('../lib/ca.js'),
+wcli = require('wsapi_client');
+email = require('browserid/email.js'),
+ca = require('browserid/ca.js'),
 jwcert = require('jwcrypto/jwcert'),
 jwk = require('jwcrypto/jwk'),
 jws = require('jwcrypto/jws');
diff --git a/browserid/tests/db-test.js b/tests/db-test.js
similarity index 98%
rename from browserid/tests/db-test.js
rename to tests/db-test.js
index c4c1136bf02850e2ceab1989d8e198f9947973d1..6f7d92c5eabb7e658747d877ffc01e93dbcfaa3d 100755
--- a/browserid/tests/db-test.js
+++ b/tests/db-test.js
@@ -37,19 +37,22 @@
 
 require('./lib/test_env.js');
 
+// add lib/ to the require path
+
 const
 assert = require('assert'),
 vows = require('vows'),
-db = require('../lib/db.js'),
 fs = require('fs'),
 path = require('path'),
-configuration = require('../../libs/configuration.js');
+db = require('db.js'),
+configuration = require('configuration.js');
 
 var suite = vows.describe('db');
 // disable vows (often flakey?) async error behavior
 suite.options.error = false;
 
 var dbCfg = configuration.get('database');
+dbCfg.drop_on_close = true;
 
 suite.addBatch({
   "onReady": {
@@ -192,7 +195,7 @@ suite.addBatch({
         db.isStaged('lloyd@somewhe.re', function(r) { cb(secret, r); });
       },
       "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); },
-      "and lets you verify it": {
+      "lets you verify it": {
         topic: function(secret, r) {
           db.gotVerificationSecret(secret, undefined, this.callback);
         },
diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..52e86a126631e8c13183ca86c2fd032ab7aa5707
--- /dev/null
+++ b/tests/email-throttling-test.js
@@ -0,0 +1,162 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require('./lib/test_env.js');
+
+const
+assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js');
+
+var suite = vows.describe('email-throttling');
+
+var token;
+
+// start up a pristine server
+start_stop.addStartupBatches(suite);
+
+// now stage a registration (causing an email to be sent)
+suite.addBatch({
+  "staging a registration": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      site:'fakesite.com'
+    }),
+    "returns 200": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "immediately staging another": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      site:'fakesite.com'
+    }),
+    "is throttled": function(r, err) {
+      assert.strictEqual(r.code, 403);
+    }
+  }
+});
+
+suite.addBatch({
+  "finishing creating the first account": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_user_creation', { token: token, pass: 'firstfakepass' }).call(this);
+    },
+    "works": function(r, err) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(true, JSON.parse(r.body).success);
+      token = undefined;
+    }
+  }
+});
+
+suite.addBatch({
+  "add a new email address to our account": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: 'second@fakeemail.com',
+      site:'fakesite.com'
+    }),
+    "works": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "re-adding that same new email address a second time": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: 'second@fakeemail.com',
+      site:'fakesite.com'
+    }),
+    "is throttled with a 403": function(r, err) {
+      assert.strictEqual(r.code, 403);
+    }
+  }
+});
+
+suite.addBatch({
+  "and when we attempt to finish adding the email address": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_email_addition', { token: token }).call(this);
+    },
+    "it works swimmingly": function(r, err) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+      token = undefined;
+    }
+  }
+});
+
+
+// shut the server down and cleanup
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/browserid/tests/forgotten-email-test.js b/tests/forgotten-email-test.js
similarity index 84%
rename from browserid/tests/forgotten-email-test.js
rename to tests/forgotten-email-test.js
index 8fa595b469eec88174cfe2b9c08150dc8fcac0d5..87774f4a812059525096d799d5b843976cf491de 100755
--- a/browserid/tests/forgotten-email-test.js
+++ b/tests/forgotten-email-test.js
@@ -41,29 +41,39 @@ const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js');
+email = require('browserid/email.js');
 
 var suite = vows.describe('forgotten-email');
 
-// disable vows (often flakey?) async error behavior
-suite.options.error = false;
-
 start_stop.addStartupBatches(suite);
 
-// ever time a new token is sent out, let's update the global
+// every time a new token is sent out, let's update the global
 // var 'token'
 var token = undefined;
-email.setInterceptor(function(email, site, secret) { token = secret; });
 
 // create a new account via the api with (first address)
 suite.addBatch({
-  "stage first account": {
+  "staging an account": {
     topic: wsapi.post('/wsapi/stage_user', {
       email: 'first@fakeemail.com',
       site:'fakesite.com'
     }),
-    "the token is sane": function(r, err) {
-      assert.strictEqual('string', typeof token);
+    "works": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
   }
 });
@@ -76,6 +86,7 @@ suite.addBatch({
     "account created": function(r, err) {
       assert.equal(r.code, 200);
       assert.strictEqual(true, JSON.parse(r.body).success);
+      token = undefined;
     }
   }
 });
@@ -97,8 +108,22 @@ suite.addBatch({
       email: 'second@fakeemail.com',
       site:'fakesite.com'
     }),
-    "the token is sane": function(r, err) {
-      assert.strictEqual('string', typeof token);
+    "works": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
   }
 });
@@ -112,6 +137,7 @@ suite.addBatch({
     "account created": function(r, err) {
       assert.equal(r.code, 200);
       assert.strictEqual(JSON.parse(r.body).success, true);
+      token = undefined;
     }
   }
 });
@@ -146,8 +172,22 @@ suite.addBatch({
       email: 'first@fakeemail.com',
       site:'otherfakesite.com'
     }),
-    "the token is sane": function(r, err) {
-      assert.strictEqual('string', typeof token);
+    "works": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
   }
 });
diff --git a/tests/lib/start-stop.js b/tests/lib/start-stop.js
new file mode 100644
index 0000000000000000000000000000000000000000..67b49c9dcb36a32e7d71b05e2bd90a41563afcdc
--- /dev/null
+++ b/tests/lib/start-stop.js
@@ -0,0 +1,198 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const
+assert = require('assert'),
+fs = require('fs'),
+path = require('path'),
+wsapi = require('./wsapi.js'),
+spawn = require('child_process').spawn,
+events = require('events'),
+config = require('configuration'),
+db = require('db');
+
+var proc = undefined;
+
+process.on('exit', function () {
+  if (proc) { proc.kill(); }
+});
+
+exports.browserid = new events.EventEmitter;
+
+function setupProc(proc) {
+  var m, sentReady = false;
+
+  proc.stdout.on('data', function(x) {
+//    console.log(x.toString());
+    var tokenRegex = new RegExp('token=([A-Za-z0-9]+)$', 'm');
+
+    if (!sentReady && /^browserid.*127\.0\.0\.1:10002/.test(x)) {
+      exports.browserid.emit('ready');
+      sentReady = true;
+    } else if (m = tokenRegex.exec(x)) {
+      exports.browserid.emit('token', m[1]);
+    }
+  });
+}
+
+function removeVarDir() {
+  try {
+    fs.readdirSync(varPath).forEach(function(f) {
+        fs.unlinkSync(path.join(varPath, f));
+    });
+    fs.rmdirSync(varPath);
+  } catch(e) {}
+}
+
+exports.addStartupBatches = function(suite) {
+
+  // disable vows (often flakey?) async error behavior
+  suite.options.error = false;
+
+  // propogate our ephemeral database parameters down to
+  // child processes so that all process are communicating
+  // with the same db
+  suite.addBatch({
+    "specifying an ephemeral database": {
+      topic: function() {
+        if (config.get('database').driver === 'mysql') {
+          process.env['MYSQL_DATABASE_NAME'] = config.get('database').database;
+        } else if (config.get('database').driver === 'json') {
+          process.env['JSON_DATABASE_PATH'] = config.get('database').path;
+        }
+        return true;
+      },
+      "should work": function(x) {
+        var cfg = process.env['MYSQL_DATABASE_NAME'] || process.env['JSON_DATABASE_PATH'];
+        assert.equal(typeof cfg, 'string');
+      }
+    }
+  });
+
+  suite.addBatch({
+    "opening the database": {
+      topic: function() {
+        var cfg = config.get('database');
+        cfg.drop_on_close = true;
+        db.open(cfg, this.callback);
+      },
+      "should work fine": function(r) {
+        assert.isUndefined(r);
+      }
+    }
+  });
+
+  suite.addBatch({
+    "run the server": {
+      topic: function() {
+        var pathToHarness = path.join(__dirname, '..', '..', 'scripts', 'run_locally.js');
+        proc = spawn('node', [ pathToHarness ])
+        setupProc(proc);
+        exports.browserid.on('ready', this.callback);
+      },
+      "server should be running": {
+        topic: wsapi.get('/__heartbeat__'),
+        "server is running": function (r, err) {
+          assert.equal(r.code, 200);
+          assert.equal(r.body, 'ok');
+        }
+      }
+    }
+  });
+};
+
+exports.addRestartBatch = function(suite) {
+  // stop the server
+  suite.addBatch({
+    "stop the server": {
+      topic: function() {
+        var cb = this.callback;
+        proc.kill('SIGINT');
+        proc.on('exit', this.callback);
+      },
+      "stopped": function(x) {
+        assert.strictEqual(x, 0);
+      }
+    }
+  });
+
+  suite.addBatch({
+    "run the server": {
+      topic: function() {
+        var pathToHarness = path.join(__dirname, '..', '..', 'scripts', 'run_locally.js');
+        proc = spawn('node', [ pathToHarness ])
+        setupProc(proc);
+        exports.browserid.on('ready', this.callback);
+      },
+      "server should be running": {
+        topic: wsapi.get('/__heartbeat__'),
+        "server is running": function (r, err) {
+          assert.equal(r.code, 200);
+          assert.equal(r.body, 'ok');
+        }
+      }
+    }
+  });
+
+};
+
+exports.addShutdownBatches = function(suite) {
+  // stop the server
+  suite.addBatch({
+    "stop the server": {
+      topic: function() {
+        var cb = this.callback;
+        proc.kill('SIGINT');
+        proc.on('exit', this.callback);
+      },
+      "stopped": function(x) {
+        assert.strictEqual(x, 0);
+      }
+    }
+  });
+
+  // clean up
+  suite.addBatch({
+    "closing the database": {
+      topic: function() {
+        db.close(this.callback);
+      },
+      "should work": function(err) {
+        assert.isUndefined(err);
+      }
+    }
+  });
+}
+
diff --git a/browserid/tests/lib/test_env.js b/tests/lib/test_env.js
similarity index 96%
rename from browserid/tests/lib/test_env.js
rename to tests/lib/test_env.js
index a98816c4ad723f88d2654b44b2e0acc972965bfc..9017cebc5152e551a731735391252584fe9379ce 100644
--- a/browserid/tests/lib/test_env.js
+++ b/tests/lib/test_env.js
@@ -44,4 +44,6 @@ if (undefined === process.env['NODE_ENV']) {
   process.env['NODE_ENV'] = 'test_json';
 } else if (process.env['NODE_ENV'].substr(0,5) !== 'test_') {
   console.log("(Woah.  Running tests without a test_ configuration.  Is this *really* what you want?)");
-} 
+}
+
+require.paths.unshift(require('path').join(__dirname, '..', '..', 'lib'));
diff --git a/browserid/tests/lib/wsapi.js b/tests/lib/wsapi.js
similarity index 96%
rename from browserid/tests/lib/wsapi.js
rename to tests/lib/wsapi.js
index a738c16cee4c71e8f68257444473d02a12affa82..d41d253142eda893ef146fc2019e67c24cbecf3b 100644
--- a/browserid/tests/lib/wsapi.js
+++ b/tests/lib/wsapi.js
@@ -34,14 +34,14 @@
  * ***** END LICENSE BLOCK ***** */
 
 const
-wcli = require('../../../libs/wsapi_client');
+wcli = require('wsapi_client');
 
 // the client "context"
 var context = {};
 
 // the configuration
 var configuration = {
-  browserid: 'http://127.0.0.1:62700/'
+  browserid: 'http://127.0.0.1:10002/'
 }
 
 exports.clearCookies = function() {
diff --git a/browserid/tests/list-emails-wsapi-test.js b/tests/list-emails-wsapi-test.js
similarity index 89%
rename from browserid/tests/list-emails-wsapi-test.js
rename to tests/list-emails-wsapi-test.js
index e31480e25f5d263331a555bb646ea5b1cd415f9d..1cc60423148f69d2cdaf8cc3b1d90fb26f80f5ac 100755
--- a/browserid/tests/list-emails-wsapi-test.js
+++ b/tests/list-emails-wsapi-test.js
@@ -40,8 +40,7 @@ require('./lib/test_env.js');
 const assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
-wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js');
+wsapi = require('./lib/wsapi.js');
 
 var suite = vows.describe('forgotten-email');
 
@@ -53,7 +52,6 @@ start_stop.addStartupBatches(suite);
 // ever time a new token is sent out, let's update the global
 // var 'token'
 var token = undefined;
-email.setInterceptor(function(email, site, secret) { token = secret; });
 
 // create a new account via the api with (first address)
 suite.addBatch({
@@ -62,8 +60,22 @@ suite.addBatch({
       email: 'syncer@somehost.com',
       site:'fakesite.com'
     }),
-    "yields a sane token": function(r, err) {
-      assert.strictEqual(typeof token, 'string');
+    "works": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
   }
 });
@@ -76,6 +88,7 @@ suite.addBatch({
     "works": function(r, err) {
       assert.equal(r.code, 200);
       assert.strictEqual(JSON.parse(r.body).success, true);
+      token = undefined;
     }
   }
 });
diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..2a5b530e46fadb15eddb06682b1459ecddae8f20
--- /dev/null
+++ b/tests/page-requests-test.js
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+http = require('http'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js');
+
+var suite = vows.describe('page requests');
+
+// start up a pristine server
+start_stop.addStartupBatches(suite);
+
+// This set of tests check to make sure all of the expected pages are served
+// up with the correct status codes.  We use Lloyd's wsapi client as our REST
+// interface.
+
+
+// Taken from the vows page.
+function assertStatus(code) {
+  return function (res, err) {
+    assert.equal(res.code, code);
+  };
+}
+
+function respondsWith(status) {
+  var context = {
+    topic: function () {
+      // Get the current context's name, such as "POST /"
+      // and split it at the space.
+      var req    = this.context.name.split(/ +/), // ["POST", "/"]
+          method = req[0].toLowerCase(),         // "post"
+          path   = req[1];                       // "/"
+
+      // Perform the contextual client request,
+      // with the above method and path.
+      wsapi[method](path).call(this);
+    }
+  };
+
+  // Create and assign the vow to the context.
+  // The description is generated from the expected status code
+  // and the status name, from node's http module.
+  context['should respond with a ' + status + ' '
+         + http.STATUS_CODES[status]] = assertStatus(status);
+
+  return context;
+}
+
+suite.addBatch({
+  'GET /':                       respondsWith(200),
+  'GET /signup':                 respondsWith(200),
+  'GET /forgot':                 respondsWith(200),
+  'GET /signin':                 respondsWith(200),
+  'GET /about':                  respondsWith(200),
+  'GET /tos':                    respondsWith(200),
+  'GET /privacy':                respondsWith(200),
+  'GET /verify_email_address':   respondsWith(200),
+  'GET /add_email_address':      respondsWith(200),
+  'GET /pk':                     respondsWith(200),
+  'GET /vepbundle':              respondsWith(200),
+  'GET /signin':                 respondsWith(200),
+  'GET /unsupported_dialog':     respondsWith(200),
+  'GET /developers':             respondsWith(200),
+  'GET /manage':                 respondsWith(302),
+  'GET /users':                  respondsWith(302),
+  'GET /users/':                 respondsWith(302),
+  'GET /primaries':              respondsWith(302),
+  'GET /primaries/':             respondsWith(302),
+  'GET /developers':             respondsWith(302)
+});
+
+// shut the server down and cleanup
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/browserid/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js
similarity index 92%
rename from browserid/tests/password-bcrypt-update-test.js
rename to tests/password-bcrypt-update-test.js
index d5693eeae05b6bdd67cca8a11bd3aece966c2471..ad0483652b4db4414215e6a7489fe062f0a7d065 100755
--- a/browserid/tests/password-bcrypt-update-test.js
+++ b/tests/password-bcrypt-update-test.js
@@ -42,9 +42,8 @@ require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js'),
-db = require('../lib/db.js'),
-config = require('../../libs/configuration.js'),
+db = require('db.js'),
+config = require('configuration.js'),
 bcrypt = require('bcrypt');
 
 var suite = vows.describe('password-length');
@@ -59,9 +58,6 @@ const TEST_EMAIL = 'update@passwd.bcrypt',
 
 // surpress console output of emails with a noop email interceptor
 var token = undefined;
-email.setInterceptor(function(email, site, secret) {
-  token = secret;
-});
 
 suite.addBatch({
   "get csrf token": {
@@ -89,6 +85,20 @@ suite.addBatch({
   }
 });
 
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
 // create a new account via the api with (first address)
 suite.addBatch({
   "setting password": {
@@ -121,13 +131,15 @@ suite.addBatch({
 suite.addBatch({
   "updating work factor": {
     topic: function() {
-      config.set('bcrypt_work_factor', 8);
+      process.env['BCRYPT_WORK_FACTOR'] = 8;
       return true;
     },
     "succeeds": function() {}
   }
 });
 
+start_stop.addRestartBatch(suite);
+
 // at authentication time we should see the password get updated
 suite.addBatch({
   "re-authentication": {
diff --git a/browserid/tests/password-length-test.js b/tests/password-length-test.js
similarity index 97%
rename from browserid/tests/password-length-test.js
rename to tests/password-length-test.js
index 6bd243cc478132e9c68231299a32a74f4b0715b7..055afd202e0f66e00acc59d5f5f5c465a6173dff 100755
--- a/browserid/tests/password-length-test.js
+++ b/tests/password-length-test.js
@@ -42,7 +42,7 @@ require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
 wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js');
+email = require('browserid/email.js');
 
 var suite = vows.describe('password-length');
 
@@ -51,9 +51,11 @@ suite.options.error = false;
 
 start_stop.addStartupBatches(suite);
 
-// surpress console output of emails with a noop email interceptor
+// surpress console output of emails with a noop email intercepto
 var token = undefined;
-email.setInterceptor(function(email, site, secret) { token = secret; });
+start_stop.browserid.on('token', function(secret) {
+  token = secret;
+});
 
 suite.addBatch({
   "get csrf token": {
@@ -79,7 +81,7 @@ suite.addBatch({
       assert.equal(r.code, 200);
     }
   }
-})
+});
 
 // create a new account via the api with (first address)
 suite.addBatch({
@@ -113,7 +115,6 @@ suite.addBatch({
     }
   }
 });
-
 start_stop.addShutdownBatches(suite);
 
 // run or export the suite.
diff --git a/browserid/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js
similarity index 90%
rename from browserid/tests/registration-status-wsapi-test.js
rename to tests/registration-status-wsapi-test.js
index b4bc4be330d65e913d657740e413bd939152cdb1..e0d40aa7762a1062482d8954cd200bd3fa03e6b4 100755
--- a/browserid/tests/registration-status-wsapi-test.js
+++ b/tests/registration-status-wsapi-test.js
@@ -41,8 +41,7 @@ const
 assert = require('assert'),
 vows = require('vows'),
 start_stop = require('./lib/start-stop.js'),
-wsapi = require('./lib/wsapi.js'),
-email = require('../lib/email.js');
+wsapi = require('./lib/wsapi.js');
 
 var suite = vows.describe('registration-status-wsapi');
 
@@ -52,7 +51,6 @@ var suite = vows.describe('registration-status-wsapi');
 // ever time a new token is sent out, let's update the global
 // var 'token'
 var token = undefined;
-email.setInterceptor(function(email, site, secret) {token = secret; });
 
 // start up a pristine server
 start_stop.addStartupBatches(suite);
@@ -82,10 +80,25 @@ suite.addBatch({
       email: 'first@fakeemail.com',
       site:'fakesite.com'
     }),
-    "the token is sane": function(r, err) {
-      assert.strictEqual('string', typeof token);
+    "returns 200": function(r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
-  }});
+  }
+});
 
 suite.addBatch({
   "comparing token to email": {
@@ -129,6 +142,7 @@ suite.addBatch({
     },
     "works": function(r, err) {
       assert.equal(r.code, 200);
+      token = undefined;
     }
   }
 });
@@ -186,8 +200,22 @@ suite.addBatch({
       email: 'first@fakeemail.com',
       site:'secondfakesite.com'
     }),
-    "yields a valid token": function(r, err) {
-      assert.strictEqual('string', typeof token);
+    "yields a HTTP 200": function (r, err) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      if (token) return token;
+      else start_stop.browserid.once('token', this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
     }
   }
 });
@@ -211,6 +239,7 @@ suite.addBatch({
     },
     "and returns a 200 code": function(r, err) {
       assert.equal(r.code, 200);
+      token = undefined;
     }
   }
 });