diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7983595abb83961fe7e6694b753357d9cd9c929c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,20 @@
+language: node_js
+
+before_install:
+ - sudo apt-get install libgmp3-dev
+ - "mysql -e 'create database browserid;'"
+
+node_js:
+ - 0.6
+
+notifications:
+  irc: "irc.mozilla.org#identity"
+
+env:
+ - MYSQL_USER=root
+
+mysql:
+  adapter: mysql2
+  username: root
+  encoding: utf8
+  database: browserid
diff --git a/ChangeLog b/ChangeLog
index d9626bc253858a3e334ff28c076a554015a4fd3a..245d20964c8af2516c948e2778d5894fd6f94842 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,24 @@
-train-2011.02.08 (in progress):
+train-2012.02.29 (in progress):
 
-train-2011.02.02:
+train-2012.02.16:
+  * improve failure mode when cookies are disabled (especially on iOS): #1056
+  * serve static css/js resources from perma URLs to improve load times - #620
+  * improve UI flows concerning cancelation during primary sign in: #983 #1036
+  * localization improvements: #1040, #1045, #1048, #1062, #1081, #1113
+  * cosmetic dialog fixes: #1062, #1058, #892, #1117
+  * fix bug preventing email addresses with under-bars in hostnames: #1074
+  * Mobile specific cosmetic improvements: #1072
+  * don't localize developer targeted error strings: #1051
+  * remove obsolete code: #1082
+  * sort email addresses alphabetically in dialog picker: #130
+  * improve error messages: #835, #1056
+  * improve log messages: #1069
+  * wsapi semantic improvements: #1083, #835
+  * logging in with a primary email address no longer forces you to re-enter your password when subsequently using a secondary address: #1049
+  * Fix IE specific issue where cookies with same name on domain and subdomain would collide: #296
+  * long emails look better: #1100
+
+train-2012.02.02:
   * i18n support, now BrowserID speaks your language: #926, #936, #977, #1013, #1031
   * improved error screens on slow server responses: #913, #915
   * better cache headers on all html resources (which Vary by Accept-Languages): #226, #620, #920, #938
@@ -21,8 +39,10 @@ train-2011.02.02:
   * (hotfix 2012.02.07) Fix the missing email address in the "check your email" screen for the forgot password flow.  #1058
   * (hotfix 2012.02.07) Modify build process to pick up locales from a .json file
   * (hotfix 2012.02.07) fix production-locales.sh script to defer to the environment for configuration
+  * (hotfix 2012.02.13) fix for IE users not seeing error screens sometimes: #1087
+  * (hotfix 2012.02.22) add banner announcing brand change
 
-train-2011.01.18:
+train-2012.01.18:
   * support for 3rd party primary identity providers: #761, #904, #865
   * loadgen improvements
   * Re-license under MPL2: #859 (& #827)
@@ -45,7 +65,7 @@ train-2011.01.18:
   * (hotfix 2012.01.31) fix silent assertions: #972
   * (hotfix 2012.02.01) fix verification of email on a browser other than the initiator: #973, #1026 (and maybe others)
 
-train-2011.01.05:
+train-2012.01.05:
   * client entropy pool mixes in randomness from server for better browser RNG: #298, #800
   * new assertion format that avoids double (base64) encoding - 33% smaller: #507
   * Turn license URL in ToS into a clickable link: #382
diff --git a/README.md b/README.md
index 16c9a2836fb4fed60c9e9af913b25acad527124f..b683b966a0ae82417b1918ba28438eabbbb334c4 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Unit tests can be run by invoking `npm test` at the top level, and you
 should run them often.  Like before committing code.  To fully test
 the code you should install mysql and have a well permissions `test`
 user (can create and drop databases).  If you don't have mysql installed,
-code testing is still possible (it just uses a little json database).
+code testing is still possible (it just uses a little JSON database).
 
 ## Development model
 
diff --git a/bin/browserid b/bin/browserid
index cf199e123b0bbbc93ae1a04370d4e0cc9e504878..5802ba36d9abc4a58aca7e50b1b04f04ba02047c 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -13,6 +13,8 @@ urlparse = require('urlparse'),
 express = require('express');
 
 const
+assets = require('../lib/static_resources').all,
+cachify = require('connect-cachify'),
 i18n = require('../lib/i18n.js'),
 wsapi = require('../lib/wsapi.js'),
 httputils = require('../lib/httputils.js'),
@@ -131,6 +133,16 @@ app.use(function(req, resp, next) {
   return next();
 });
 
+var static_root = path.join(__dirname, "..", "resources", "static");
+
+app.use(cachify.setup(assets(config.get('supported_languages')), 
+        {
+          prefix: 'v',
+          production: config.get('use_minified_resources'),
+          root: static_root,
+        }));
+
+
 // #7 - perform response substitution to support local/dev/beta environments
 // (specifically, this replaces URLs in responses, e.g. https://browserid.org
 //  with https://diresworb.org)
@@ -168,14 +180,7 @@ app.use(function(req, res, next) {
   next();
 });
 
-app.use(express.static(path.join(__dirname, "..", "resources", "static")));
-
-// custom 404 page
-app.use(function(req, res,next) {
-  res.statusCode = 404;
-  res.write("Cannot find this resource");
-  res.end();
-});
+app.use(express.static(static_root));
 
 // open the databse
 db.open(config.get('database'), function (error) {
diff --git a/bin/verifier b/bin/verifier
index 50210785b86e0f0db18e9fa0b9e7829c4f6042bb..d19cfc2cd096e4fa9b39c9245d2bb48376bd87ac 100755
--- a/bin/verifier
+++ b/bin/verifier
@@ -129,13 +129,6 @@ app.post('/verify', function(req, resp, next) {
 // shutdown when /code_update is invoked
 shutdown.installUpdateHandler(app);
 
-// custom 404
-app.use(function(req, res,next) {
-  res.statusCode = 404;
-  res.write("Cannot find this resource");
-  res.end();
-});
-
 // shutdown nicely on signals
 shutdown.handleTerminationSignals(app, function() {
   cc.exit();
diff --git a/config/l10n-all.json b/config/l10n-all.json
index 920a42d54c46543059940cdb4f520111cc631bf2..3f78aa2cc1e09ae9e2d64cc4894cf9d533ba661a 100644
--- a/config/l10n-all.json
+++ b/config/l10n-all.json
@@ -1,9 +1,10 @@
 {
-  "supported_languages": [
-    "af", "ca", "cs", "da", "de", "el", "en-US", "eo", "es", "es-MX", "et", "eu",
-    "fi", "fr", "fy", "ga", "gd", "gl", "hr", "it", "ja", "lij", "lt",
-    "ml", "nl", "pl", "pt", "pt-BR", "rm", "ro", "ru", "sk", "sl", "son",
-    "sq", "sr", "tr", "zh-CN", "zh-TW",
+"supported_languages": [
+    "af", "ca", "cs", "da", "de", "el", "en-US", "eo", "es", "es-MX",
+    "et", "eu", "fi", "fr", "fy", "ga", "gd", "gl", "he", "hr", "hu",
+    "it", "ja", "lij", "lt", "ml", "nl", "pa", "pl", "pt", "pt-BR",
+    "rm", "ro", "ru", "sk", "sl", "son", "sq", "sr", "sv", "tr", "uk",
+    "zh-CN", "zh-TW",
     "it-CH", "db-LB"
   ]
 }
\ No newline at end of file
diff --git a/docs/AWS_DEPLOYMENT.md b/docs/AWS_DEPLOYMENT.md
index 4071567e366499de26596de5dcd71543e448055e..8bcced1d0be07d65c85c2e7bf9068a693b25d26c 100644
--- a/docs/AWS_DEPLOYMENT.md
+++ b/docs/AWS_DEPLOYMENT.md
@@ -19,8 +19,11 @@ Once you have these things, you'll need to relay them to deployment
 scripts via your environment.  you might put something like this
 in your .bashrc:
 
+    # This is your Access Key ID from your AWS Security Credentials
     export AWS_ID=<your id>
+    # This is your Secret Access Key from your AWS Security Credentials
     export AWS_SECRET=<your secret>
+    # This is a magic credential you get from lloyd
     export BROWSERID_DEPLOY_DNS_KEY=98...33
 
 ## test!
diff --git a/example/rp/TOS.html b/example/rp/TOS.html
new file mode 100644
index 0000000000000000000000000000000000000000..c61b94818cad466262fff27ad5492dcd150c1bd3
--- /dev/null
+++ b/example/rp/TOS.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This is my ToS...  I pour out.
+</body>
+</html>
diff --git a/example/rp/index.html b/example/rp/index.html
index 7c9492c92dcfe92a20afad8f53219d9ee3695012..d14fa1ad693c3b72b16dd057c7b6a1da0e5eaeb2 100644
--- a/example/rp/index.html
+++ b/example/rp/index.html
@@ -35,6 +35,9 @@ pre {
   word-wrap: break-word;
 }
 
+.specify ul { padding-left: 0px; }
+.specify li { list-style: none; }
+
 @media screen and (max-width: 640px) {
   .intro, .output, .step {
     width: 90%;
@@ -54,13 +57,26 @@ pre {
 </div>
 
 <div class="specify">
-  What flavor of assertion would you like? <br/>
-  <p>
-    <input type="checkbox" id="silent">&nbsp;Silent <br/>
-    <input type="checkbox" id="allowPersistent">&nbsp;Allow persistent sign-in <br/>
-    <input type="text" id="requiredEmail" width="80">&nbsp;Require a specific email <br/>
+  <p>What flavor of assertion would you like?</p>
+  <ul>
+    <li>
+      <input type="checkbox" id="silent"> 
+      <label for="silent">Silent</label>
+    </li><li>
+      <input type="checkbox" id="allowPersistent">
+      <label for="allowPersistent">Allow persistent sign-in</label>
+    </li><li>
+      <input type="checkbox" id="privacy">
+      <label for="privacy">Supply a privacy policy</label>
+    </li><li>
+      <input type="checkbox" id="tos">
+      <label for="tos">Supply a ToS</label>
+    </li><li>
+      <input type="text" id="requiredEmail" width="80">
+      <label for="requiredEmail">Require a specific email</label><br />
+    </li>
+  </ul>
     <button>Get an assertion</button>
-  </p>
 </div>
 
 <div class="verifierResp">
@@ -113,6 +129,8 @@ $(document).ready(function() {
     }, {
       silent: $('#silent').attr('checked'),
       allowPersistent: $('#allowPersistent').attr('checked'),
+      privacyURL: $('#privacy').attr('checked') ? "/privacy.html" : undefined,
+      tosURL: $('#tos').attr('checked') ? "/TOS.html" : undefined,
       requiredEmail: requiredEmail
     });
   });
diff --git a/example/rp/privacy.html b/example/rp/privacy.html
new file mode 100644
index 0000000000000000000000000000000000000000..7fe9a3994759da46f77e48b49402438efcfb7cc0
--- /dev/null
+++ b/example/rp/privacy.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+This is my privacy policy.  When you tip me over...
+</body>
+</html>
diff --git a/lib/browserid/fake_verification.js b/lib/browserid/fake_verification.js
index 9d1df8bb6ce20a8a76ac5f841d3fecd4db05cfb1..03f33f1c15af0fbeb0f68cd43ce6f0f3e782bed1 100644
--- a/lib/browserid/fake_verification.js
+++ b/lib/browserid/fake_verification.js
@@ -12,7 +12,8 @@ const
 configuration = require('../configuration.js'),
 url = require('url'),
 db = require('../db.js');
-logger = require('../logging.js').logger;
+logger = require('../logging.js').logger,
+wsapi = require('../wsapi');
 
 logger.warn("HEAR YE: Fake verfication enabled, aceess via /wsapi/fake_verification?email=foo@bar.com");
 logger.warn("THIS IS NEVER OK IN A PRODUCTION ENVIRONMENT");
@@ -20,7 +21,8 @@ logger.warn("THIS IS NEVER OK IN A PRODUCTION ENVIRONMENT");
 exports.addVerificationWSAPI = function(app) {
   app.get('/wsapi/fake_verification', function(req, res) {
     var email = url.parse(req.url, true).query['email'];
-    db.verificationSecretForEmail(email, function(secret) {
+    db.verificationSecretForEmail(email, function(err, secret) {
+      if (err) return wsapi.databaseDown(resp, err);
       if (secret) res.write(secret);
       else res.writeHead(400, {"Content-Type": "text/plain"});
       res.end();
diff --git a/lib/browserid/views.js b/lib/browserid/views.js
index d87269bd1bb467ce5ee0f20174bf28cd17569286..0ca111100a1211e636c1e17cda6a818486223e0b 100644
--- a/lib/browserid/views.js
+++ b/lib/browserid/views.js
@@ -9,7 +9,9 @@ logger = require('../logging.js').logger,
 fs = require('fs'),
 connect = require('connect'),
 config = require('../configuration.js'),
-util = require('util');
+und = require('underscore'),
+util = require('util'),
+httputils = require('../httputils.js');
 
 // all templated content, redirects, and renames are handled here.
 // anything that is not an api, and not static
@@ -90,6 +92,10 @@ exports.setup = function(app) {
     renderCachableView(req, res, 'unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
   });
 
+  app.get("/cookies_disabled", function(req,res) {
+    renderCachableView(req, res, 'cookies_disabled.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+  });
+
   // Used for a relay page for communication.
   app.get("/relay", function(req, res, next) {
     // Allow the relay to be run within a frame
@@ -149,18 +155,19 @@ exports.setup = function(app) {
     renderCachableView(req, res, 'add_email_address.ejs', {title: 'Verify Email Address', fullpage: false});
   });
 
-  /**
-   *
-   * XXX benadida or lloyd, I tried to use straight up regexp to do this, but.
-   * is there a better way to do this?
-   */
-  function QUnit(req, res) {
-    res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false});
+  // serve up testing templates.  but NOT in staging or production.  see GH-1044
+  if ([ 'https://browserid.org', 'https://diresworb.org' ].indexOf(config.get('public_url')) === -1) {
+    // serve test.ejs to /test or /test/ or /test/index.html
+    app.get(/^\/test\/(?:index.html)?$/, function (req, res) {
+      res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false});
+    });
+  } else {
+    // this is stage or production, explicitly disable all resources under /test
+    app.get(/^\/test/, function(req, res) {
+      httputils.notFound("Cannot " + req.method + " " + req.url);
+    });
   }
 
-  app.get("/test", QUnit);
-  app.get("/test/index.html", QUnit);
-
   // REDIRECTS
   REDIRECTS = {
     "/manage": "/",
diff --git a/lib/configuration.js b/lib/configuration.js
index 78eead9e9a37dfb31eff37c96b9fdc452f775e46..1b443b5754a3c9a52f9afb56814c7a37d673c622 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -76,7 +76,10 @@ var conf = module.exports = convict({
   },
   database: {
     driver: 'string ["json", "mysql"] = "json"',
-    user: 'string?',
+    user: {
+      format: 'string?',
+      env: 'MYSQL_USER'
+    },
     create_schema: 'boolean = true',
     may_write: 'boolean = true',
     name: {
@@ -84,7 +87,16 @@ var conf = module.exports = convict({
       env: 'DATABASE_NAME'
     },
     password: 'string?',
-    host: 'string?'
+    host: 'string?',
+    max_query_time_ms: {
+      format: 'integer = 5000',
+      doc: "The maximum amount of time we'll allow a query to run before considering the database to be sick",
+      env: 'MAX_QUERY_TIME_MS'
+    },
+    max_reconnect_attempts: {
+      format: 'integer = 1',
+      doc: "The maximum number of times we'll attempt to reconnect to the database before failing all outstanding queries"
+    }
   },
   smtp: {
     host: 'string?',
diff --git a/lib/db.js b/lib/db.js
index c0670cad8e46ccab2ea7c8f92e6d76e2dd9ab5cf..6765c28fbc71ad132d2c3e5dd69352954c2e7b50 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -39,7 +39,7 @@ exports.open = function(cfg, cb) {
       ready = true;
       waiting.forEach(function(f) { f() });
       waiting = [];
-      if (cb) cb();
+      if (cb) cb(null);
     }
   });
 };
diff --git a/lib/db/json.js b/lib/db/json.js
index 266ae22b3d29e14993defe99528e447d3d6af7e8..b9a4c64088258d22a75452e88521af511239dc28 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -81,41 +81,40 @@ exports.open = function(cfg, cb) {
   logger.debug("opening JSON database: " + dbPath);
 
   sync();
-
-  setTimeout(cb, 0);
+  process.nextTick(function() { cb(null); });
 };
 
 exports.closeAndRemove = function(cb) {
   // if the file cannot be removed, it's not an error, just means it was never
   // written or deleted by a different process
   try { fs.unlinkSync(dbPath); } catch(e) { }
-  setTimeout(function() { cb(undefined); }, 0);
+  process.nextTick(function() { cb(null); });
 };
 
 exports.close = function(cb) {
   // don't flush database here to disk, the database is flushed synchronously when
   // written - If we were to flush here we could overwrite changes made by
   // another process - see issue #557
-  setTimeout(function() { cb(undefined) }, 0);
+  process.nextTick(function() { cb(null) });
 };
 
 exports.emailKnown = function(email, cb) {
   sync();
   var m = jsel.match(".emails ." + ESC(email), db.users);
-  setTimeout(function() { cb(m.length > 0) }, 0);
+  process.nextTick(function() { cb(null, m.length > 0) });
 };
 
 exports.emailType = function(email, cb) {
   sync();
   var m = jsel.match(".emails ." + ESC(email), db.users);
-  process.nextTick(function() { cb(m.length ? m[0].type : undefined); });
+  process.nextTick(function() { cb(null, m.length ? m[0].type : undefined); });
 };
 
 exports.isStaged = function(email, cb) {
   if (cb) {
     setTimeout(function() {
       sync();
-      cb(db.stagedEmails.hasOwnProperty(email));
+      cb(null, db.stagedEmails.hasOwnProperty(email));
     }, 0);
   }
 };
@@ -127,7 +126,7 @@ exports.lastStaged = function(email, cb) {
     if (db.stagedEmails.hasOwnProperty(email)) {
       d = new Date(db.staged[db.stagedEmails[email]].when);
     }
-    setTimeout(function() { cb(d); }, 0);
+    setTimeout(function() { cb(null, d); }, 0);
   }
 };
 
@@ -135,7 +134,7 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
   sync();
   var m = jsel.match(".emails:has(."+ESC(lhs)+"):has(."+ESC(rhs)+")", db.users);
   process.nextTick(function() {
-    cb(m && m.length == 1);
+    cb(null, m && m.length == 1);
   });
 };
 
@@ -145,7 +144,7 @@ exports.emailToUID = function(email, cb) {
   if (m.length === 0) m = undefined;
   else m = m[0];
   process.nextTick(function() {
-    cb(m);
+    cb(null, m);
   });
 };
 
@@ -153,7 +152,7 @@ exports.userOwnsEmail = function(uid, email, cb) {
   sync();
   var m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(uid) + ")):has(.emails > ." + ESC(email) + ")", db.users);
   process.nextTick(function() {
-    cb(m && m.length == 1);
+    cb(null, m && m.length == 1);
   });
 };
 
@@ -172,7 +171,7 @@ function addEmailToAccount(userID, email, type, cb) {
       emails[0][email] = { type: type };
       flush();
     }
-    cb();
+    cb(null);
   });
 }
 
@@ -187,7 +186,7 @@ exports.stageUser = function(email, cb) {
     };
     db.stagedEmails[email] = secret;
     flush();
-    setTimeout(function() { cb(secret); }, 0);
+    process.nextTick(function() { cb(null, secret); });
   });
 };
 
@@ -204,7 +203,7 @@ exports.stageEmail = function(existing_user, new_email, cb) {
     db.stagedEmails[new_email] = secret;
     flush();
 
-    setTimeout(function() { cb(secret); }, 0);
+    process.nextTick(function() { cb(null, secret); });
   });
 };
 
@@ -219,14 +218,14 @@ exports.createUserWithPrimaryEmail = function(email, cb) {
   });
   flush();
   process.nextTick(function() {
-    cb(undefined, uid);
+    cb(null, uid);
   });
 };
 
 exports.haveVerificationSecret = function(secret, cb) {
   process.nextTick(function() {
     sync();
-    cb(!!(db.staged[secret]));
+    cb(null, !!(db.staged[secret]));
   });
 };
 
@@ -235,8 +234,8 @@ exports.emailForVerificationSecret = function(secret, cb) {
   process.nextTick(function() {
     sync();
     if (!db.staged[secret]) return cb("no such secret");
-    exports.checkAuth(db.staged[secret].existing_user, function (hash) {
-      cb(undefined, {
+    exports.checkAuth(db.staged[secret].existing_user, function (err, hash) {
+      cb(err, {
         email: db.staged[secret].email,
         needs_password: !hash
       });
@@ -247,7 +246,7 @@ exports.emailForVerificationSecret = function(secret, cb) {
 exports.verificationSecretForEmail = function(email, cb) {
   setTimeout(function() {
     sync();
-    cb(db.stagedEmails[email]);
+    cb(null, db.stagedEmails[email]);
   }, 0);
 };
 
@@ -261,7 +260,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
   delete db.stagedEmails[o.email];
   flush();
   if (o.type === 'add_account') {
-    exports.emailKnown(o.email, function(known) {
+    exports.emailKnown(o.email, function(err, known) {
       function createAccount() {
         var emailVal = {};
         emailVal[o.email] = { type: 'secondary' };
@@ -272,7 +271,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
           emails: emailVal
         });
         flush();
-        cb(undefined, o.email, uid);
+        cb(null, o.email, uid);
       }
 
       // if this email address is known and a user has completed a re-verification of this email
@@ -291,7 +290,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
       }
     });
   } else if (o.type === 'add_email') {
-    exports.emailKnown(o.email, function(known) {
+    exports.emailKnown(o.email, function(err, known) {
       function addIt() {
         addEmailToAccount(o.existing_user, o.email, 'secondary', function(e) {
           cb(e, o.email, o.existing_user);
@@ -313,7 +312,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
 
 exports.addPrimaryEmailToAccount = function(userID, emailToAdd, cb) {
   sync();
-  exports.emailKnown(emailToAdd, function(known) {
+  exports.emailKnown(emailToAdd, function(err, known) {
     function addIt() {
       addEmailToAccount(userID, emailToAdd, 'primary', cb);
     }
@@ -336,7 +335,7 @@ exports.checkAuth = function(userID, cb) {
     if (m.length === 0) m = undefined;
     else m = m[0];
   }
-  process.nextTick(function() { cb(m) });
+  process.nextTick(function() { cb(null, m) });
 };
 
 exports.userKnown = function(userID, cb) {
@@ -344,7 +343,7 @@ exports.userKnown = function(userID, cb) {
   var m = jsel.match(":root > object:has(:root > .id:expr(x=" + ESC(userID) + "))", db.users);
   if (m.length === 0) m = undefined;
   else m = m[0];
-  process.nextTick(function() { cb(m) });
+  process.nextTick(function() { cb(null, m) });
 };
 
 exports.updatePassword = function(userID, hash, cb) {
@@ -378,7 +377,7 @@ exports.removeEmail = function(authenticated_user, email, cb) {
     delete emails[email];
     flush();
   }
-  setTimeout(function() { cb(); }, 0);
+  setTimeout(function() { cb(null); }, 0);
 };
 
 function removeEmailNoCheck(email, cb) {
@@ -389,7 +388,7 @@ function removeEmailNoCheck(email, cb) {
     delete emails[email];
     flush();
   }
-  process.nextTick(function() { cb(); });
+  process.nextTick(function() { cb(null); });
 };
 
 exports.cancelAccount = function(authenticated_uid, cb) {
@@ -405,7 +404,7 @@ exports.cancelAccount = function(authenticated_uid, cb) {
     flush();
   }
 
-  process.nextTick(function() { cb(); });
+  process.nextTick(function() { cb(null); });
 };
 
 exports.addTestUser = function(email, hash, cb) {
@@ -419,10 +418,10 @@ exports.addTestUser = function(email, hash, cb) {
       emails: emailVal
     });
     flush();
-    cb();
+    cb(null);
   });
 };
 
 exports.ping = function(cb) {
-  setTimeout(function() { cb(); }, 0);
+  process.nextTick(function() { cb(null); });
 };
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index 8b8839635b94a59297da75df9651d1457412d4f3..2931097bcde0d45dd58fdeef5265e1c1524a2a6f 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -29,13 +29,34 @@
  */
 
 const
-mysql = require('mysql'),
+mysql = require('./mysql_wrapper.js'),
 secrets = require('../secrets.js'),
 logger = require('../logging.js').logger,
-statsd = require('../statsd');
+conf = require('../configuration.js');
 
 var client = undefined;
 
+// for testing!  when 'STALL_MYSQL_WHEN_PRESENT' is defined in the environment,
+// it causes the driver to simulate stalling whent said file is present
+if (conf.get('env') === 'test_mysql' && process.env['STALL_MYSQL_WHEN_PRESENT']) {
+  logger.debug('database driver will be stalled when file is present: ' +
+               process.env['STALL_MYSQL_WHEN_PRESENT']);
+  const fs = require('fs');
+  fs.watchFile(
+    process.env['STALL_MYSQL_WHEN_PRESENT'],
+    { persistent: false, interval: 1 },
+    function (curr, prev) {
+      // stall the database driver when specified file is present
+      fs.stat(process.env['STALL_MYSQL_WHEN_PRESENT'], function(err, stats) {
+        if (client) {
+          var stall = !(err && err.code === 'ENOENT');
+          logger.debug("database driver is " + (stall ? "stalled" : "unblocked"));
+          client.stall(stall);
+        }
+      });
+    });
+}
+
 // If you change these schemas, please notify <services-ops@mozilla.com>
 const schemas = [
   "CREATE TABLE IF NOT EXISTS user (" +
@@ -68,7 +89,7 @@ function logUnexpectedError(detail) {
   var where;
   try { dne; } catch (e) { where = e.stack.split('\n')[2].trim(); };
   // now log it!
-  logger.error("unexpected database failure: " + detail + " -- " + where);
+  logger.warn("unexpected database failure: " + detail + " -- " + where);
 }
 
 // open & create the mysql database
@@ -98,27 +119,6 @@ exports.open = function(cfg, cb) {
     options.database = database;
     client = mysql.createClient(options);
 
-    // replace .query with a function that times queries and
-    // logs to statsd
-    var realQuery = client.query;
-    client.query = function() {
-      var startTime = new Date();
-      var client_cb;
-      var new_cb = function() {
-        var reqTime = new Date - startTime;
-        statsd.timing('query_time', reqTime);
-        if (client_cb) client_cb.apply(null, arguments);
-      };
-      var args = Array.prototype.slice.call(arguments);
-      if (typeof args[args.length - 1] === 'function') {
-        client_cb = args[args.length - 1];
-        args[args.length - 1] = new_cb;
-      } else {
-        args.push(new_cb);
-      }
-      realQuery.apply(client, args);
-    };
-
     client.ping(function(err) {
       logger.debug("connection to database " + (err ? ("fails: " + err) : "established"));
       cb(err);
@@ -176,7 +176,7 @@ exports.close = function(cb) {
   client.end(function(err) {
     client = undefined;
     if (err) logUnexpectedError(err);
-    if (cb) cb(err);
+    if (cb) cb(err === undefined ? null : err);
   });
 };
 
@@ -198,8 +198,7 @@ exports.emailKnown = function(email, cb) {
   client.query(
     "SELECT COUNT(*) as N FROM email WHERE address = ?", [ email ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
-      cb(rows && rows.length > 0 && rows[0].N > 0);
+      cb(err, rows && rows.length > 0 && rows[0].N > 0);
     }
   );
 };
@@ -208,8 +207,7 @@ exports.userKnown = function(uid, cb) {
   client.query(
     "SELECT COUNT(*) as N FROM user WHERE id = ?", [ uid ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
-      cb(rows && rows.length > 0 && rows[0].N > 0);
+      cb(err, rows && rows.length > 0 && rows[0].N > 0);
     }
   );
 };
@@ -218,8 +216,7 @@ exports.emailType = function(email, cb) {
   client.query(
     "SELECT type FROM email WHERE address = ?", [ email ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
-      cb((rows && rows.length > 0) ? rows[0].type : undefined);
+      cb(err, (rows && rows.length > 0) ? rows[0].type : undefined);
     }
   );
 }
@@ -228,8 +225,7 @@ exports.isStaged = function(email, cb) {
   client.query(
     "SELECT COUNT(*) as N FROM staged WHERE email = ?", [ email ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
-      cb(rows && rows.length > 0 && rows[0].N > 0);
+      cb(err, rows && rows.length > 0 && rows[0].N > 0);
     }
   );
 }
@@ -238,9 +234,9 @@ 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));
+      if (err) cb(err);
+      else if (!rows || rows.length === 0) cb(null);
+      else cb(null, new Date(rows[0].ts * 1000));
     }
   );
 };
@@ -252,10 +248,7 @@ exports.stageUser = function(email, cb) {
                  'ON DUPLICATE KEY UPDATE secret=?, existing_user=NULL, new_acct=TRUE, ts=NOW()',
                  [ secret, email, secret],
                  function(err) {
-                   if (err) {
-                     logUnexpectedError(err);
-                     cb(undefined, err);
-                   } else cb(secret);
+                   cb(err, err ? undefined : secret);
                  });
   });
 };
@@ -265,8 +258,7 @@ exports.haveVerificationSecret = function(secret, cb) {
   client.query(
     "SELECT count(*) as n FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
-      if (err) cb(false);
-      else cb(rows.length === 1 && rows[0].n === 1);
+      cb(err, rows && rows.length === 1 && rows[0].n === 1);
     });
 };
 
@@ -274,14 +266,15 @@ exports.emailForVerificationSecret = function(secret, cb) {
   client.query(
     "SELECT * FROM staged WHERE secret = ?", [ secret ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
+      if (err) return cb("database unavailable");
+
       // if the record was not found, fail out
       if (!rows || rows.length != 1) return cb("no such secret");
 
       var o = rows[0];
 
       // if the record was found and this is for a new_acct, return the email
-      if (o.new_acct) return cb(undefined, { email: o.email, needs_password: false });
+      if (o.new_acct) return cb(null, { email: o.email, needs_password: false });
 
       // we need a userid.  the old schema had an 'existing' field which was an email
       // address.  the new schema has an 'existing_user' field which is a userid.
@@ -289,8 +282,8 @@ exports.emailForVerificationSecret = function(secret, cb) {
       // and can be removed in feb 2012 some time.  maybe for valentines day?
       if (typeof o.existing_user === 'number') doCheckAuth(o.existing_user);
       else if (typeof o.existing === 'string') {
-        exports.emailToUID(o.existing, function(uid) {
-          if (uid === undefined) return cb('acct associated with staged email doesn\'t exist');
+        exports.emailToUID(o.existing, function(err, uid) {
+          if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist');
           doCheckAuth(uid);
         });
       }
@@ -301,8 +294,8 @@ exports.emailForVerificationSecret = function(secret, cb) {
         // are associated with the acct at the moment, then there will not be a
         // password set and the user will need to set one with the addition of
         // this addresss)
-        exports.checkAuth(uid, function(hash) {
-          cb(undefined, {
+        exports.checkAuth(uid, function(err, hash) {
+          cb(err, {
             email: o.email,
             needs_password: !hash
           });
@@ -315,8 +308,7 @@ exports.verificationSecretForEmail = function(email, cb) {
   client.query(
     "SELECT secret FROM staged WHERE email = ?", [ email ],
     function(err, rows) {
-      if (err) logUnexpectedError(err);
-      cb((rows && rows.length > 0) ? rows[0].secret : undefined);
+      cb(err, (rows && rows.length > 0) ? rows[0].secret : undefined);
     });
 };
 
@@ -329,14 +321,14 @@ function addEmailToUser(userID, email, type, cb) {
     "DELETE FROM email WHERE address = ?",
     [ email ],
     function(err, info) {
-      if (err) { logUnexpectedError(err); cb(err); return; }
+      if (err) return cb(err);
       else {
         client.query(
           "INSERT INTO email(user, address, type) VALUES(?, ?, ?)",
           [ userID, email, type ],
           function(err, info) {
             if (err) logUnexpectedError(err);
-            cb(err ? err : undefined, email, userID);
+            cb(err, email, userID);
           });
       }
     });
@@ -363,7 +355,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
             "INSERT INTO user(passwd) VALUES(?)",
             [ hash ],
             function(err, info) {
-              if (err) { logUnexpectedError(err); cb(err); return; }
+              if (err) return cb(err);
               addEmailToUser(info.insertId, o.email, 'secondary', cb);
             });
         } else {
@@ -374,7 +366,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) {
           if (typeof o.existing_user === 'number') doAddEmail(o.existing_user);
           else if (typeof o.existing === 'string') {
             exports.emailToUID(o.existing, function(uid) {
-              if (uid === undefined) return cb('acct associated with staged email doesn\'t exist');
+              if (err || uid === undefined) return cb('acct associated with staged email doesn\'t exist');
               doAddEmail(uid);
             });
           }
@@ -400,14 +392,13 @@ exports.createUserWithPrimaryEmail = function(email, cb) {
   client.query(
     "INSERT INTO user() VALUES()",
     function(err, info) {
-      if (err) { logUnexpectedError(err); cb(err); return; }
+      if (err) return cb(err);
       var uid = info.insertId;
       client.query(
         "INSERT INTO email(user, address, type) VALUES(?, ?, ?)",
         [ uid, email, 'primary' ],
         function(err, info) {
-          if (err) logUnexpectedError(err);
-          cb(err ? err : undefined, uid);
+          cb(err, uid);
         });
     });
 };
@@ -417,8 +408,7 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
     'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ( SELECT user FROM email WHERE address = ? );',
     [ lhs, rhs ],
     function (err, rows) {
-      if (err) cb(false);
-      else cb(rows.length === 1 && rows[0].n === 1);
+      cb(err, rows && rows.length === 1 && rows[0].n === 1);
     });
 }
 
@@ -427,8 +417,7 @@ exports.userOwnsEmail = function(uid, email, cb) {
     'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ?',
     [ email, uid ],
     function (err, rows) {
-      if (err) cb(false);
-      else cb(rows.length === 1 && rows[0].n === 1);
+      cb(err, rows && rows.length === 1 && rows[0].n === 1);
     });
 }
 
@@ -439,11 +428,7 @@ exports.stageEmail = function(existing_user, new_email, cb) {
                  'ON DUPLICATE KEY UPDATE secret=?, existing_user=?, new_acct=FALSE, ts=NOW()',
                  [ secret, existing_user, new_email, secret, existing_user],
                  function(err) {
-                   if (err) {
-                     logUnexpectedError(err);
-                     cb(undefined, err);
-                   }
-                   else cb(secret);
+                   cb(err, err ? undefined : secret);
                  });
   });
 };
@@ -453,8 +438,7 @@ exports.emailToUID = function(email, cb) {
     'SELECT user FROM email WHERE address = ?',
     [ email ],
     function (err, rows) {
-      if (err) logUnexpectedError(err);
-      cb((rows && rows.length == 1) ? rows[0].user : undefined);
+      cb(err, (rows && rows.length == 1) ? rows[0].user : undefined);
     });
 };
 
@@ -463,8 +447,7 @@ exports.checkAuth = function(uid, cb) {
     'SELECT passwd FROM user WHERE id = ?',
     [ uid ],
     function (err, rows) {
-      if (err) logUnexpectedError(err);
-      cb((rows && rows.length == 1) ? rows[0].passwd : undefined);
+      cb(err, (rows && rows.length == 1) ? rows[0].passwd : undefined);
     });
 }
 
@@ -473,8 +456,10 @@ exports.updatePassword = function(uid, hash, cb) {
     'UPDATE user SET passwd = ? WHERE id = ?',
     [ hash, uid ],
     function (err, rows) {
-      if (err) logUnexpectedError(err);
-      cb((err || rows.affectedRows !== 1) ? ("no record with email " + email) : undefined);
+      if (!err && (!rows || rows.affectedRows !== 1)) {
+        err = "no record with email " + email;
+      }
+      cb(err);
     });
 }
 
@@ -504,7 +489,9 @@ exports.listEmails = function(uid, cb) {
 };
 
 exports.removeEmail = function(authenticated_user, email, cb) {
-  exports.userOwnsEmail(authenticated_user, email, function(ok) {
+  exports.userOwnsEmail(authenticated_user, email, function(err, ok) {
+    if (err) return cb(err);
+
     if (!ok) {
       logger.warn(authenticated_user + ' attempted to delete an email that doesn\'t belong to her: ' + email);
       cb("authenticated user doesn't have permission to remove specified email " + email);
@@ -515,18 +502,16 @@ exports.removeEmail = function(authenticated_user, email, cb) {
       'DELETE FROM email WHERE address = ?',
       [ email ],
       function(err, info) {
-        if (err) logUnexpectedError(err);
-        // smash null into undefined
-        cb(err ? err : undefined);
+        cb(err);
       });
   });
 };
 
 exports.cancelAccount = function(uid, cb) {
-  function reportErr(err) { if (err) logUnexpectedError(err); }
-  client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], reportErr);
-  client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], reportErr);
-  process.nextTick(cb);
+  client.query("DELETE LOW_PRIORITY FROM email WHERE user = ?", [ uid ], function(err) {
+    if (err) return cb(err);
+    client.query("DELETE LOW_PRIORITY FROM user WHERE id = ?", [ uid ], cb);
+  });
 };
 
 exports.addTestUser = function(email, hash, cb) {
@@ -534,17 +519,14 @@ exports.addTestUser = function(email, hash, cb) {
     "INSERT INTO user(passwd) VALUES(?)",
     [ hash ],
     function(err, info) {
-      if (err) {
-        logUnexpectedError(err);
-        cb(err);
-        return;
-      }
+      if (err) return cb(err);
+
       client.query(
         "INSERT INTO email(user, address) VALUES(?, ?)",
         [ info.insertId, email ],
         function(err, info) {
           if (err) logUnexpectedError(err);
-          cb(err ? err : undefined, email);
+          cb(err, err ? null : email);
         });
     });
 };
diff --git a/lib/db/mysql_wrapper.js b/lib/db/mysql_wrapper.js
new file mode 100644
index 0000000000000000000000000000000000000000..91c4634827d590ed31bc007ad03decbd4fcbec8b
--- /dev/null
+++ b/lib/db/mysql_wrapper.js
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This abstraction wraps the mysql driver and provides application level
+ * queueing, as well as query timing and reconnect upon an apparently "stalled"
+ * driver
+ */
+
+const
+mysql = require('mysql'),
+statsd = require('../statsd'),
+logger = require('../logging.js').logger,
+config = require('../configuration.js');
+
+exports.createClient = function(options) {
+  // the application level query queue
+  var queryQueue = [];
+  // The slowQueryTimer is !null when a query is running, and holds
+  // the result from setTimeout.  This variable is both a means to
+  // check if a query is running (only one runs at a time), and as
+  // the timeout handle.
+  var slowQueryTimer = null;
+  // how many consecutive failures have we seen when running queries?
+  var consecutiveFailures = 0;
+  // a testing feature.  By calling `client.stall` you can
+  // cause responses to be dropped which will trigger slow query detection
+  var stalled = false;
+
+  var client = {
+    stall: function(stalledState) {
+      stalled = stalledState;
+    },
+    realClient: null,
+    _resetConnection: function() {
+      if (this.realClient) this.realClient.destroy();
+      this.realClient = mysql.createClient(options);
+      this.realClient.on('error', function(e) {
+        logger.warn("database connection down: " + e.toString());
+      });
+    },
+    ping: function(cb) {
+      this.realClient.ping(cb);
+    },
+    _runNextQuery: function() {
+      var self = this;
+
+      if (slowQueryTimer !== null || !queryQueue.length) return;
+
+      var work = queryQueue.shift();
+
+      function invokeCallback(cb, err, rez) {
+        if (cb) {
+          process.nextTick(function() {
+            try {
+              cb(err, rez);
+            } catch(e) {
+              logger.error('database query callback failed: ' + e.toString());
+            }
+          });
+        }
+      }
+
+      slowQueryTimer = setTimeout(function() {
+        if (++consecutiveFailures > config.get('database.max_reconnect_attempts')) {
+          // if we can't run the query multiple times in a row, we'll fail all outstanding
+          // queries, and reinitialize the connection, so that the process stays up and
+          // retries mysql connection the next time a request which requires db interaction
+          // comes in.
+          queryQueue.unshift(work);
+          logger.warn("cannot reconnect to mysql! " + queryQueue.length + " outstanding queries #fail.");
+          queryQueue.forEach(function(work) {
+            invokeCallback(work.cb, "database connection unavailable");
+          });
+          queryQueue = [];
+          self._resetConnection();
+          slowQueryTimer = null;
+        } else {
+          logger.warn("Query taking more than " + config.get('database.max_query_time_ms') + "ms!  reconnecting to mysql");
+          // we'll fail the long running query, because we cannot
+          // meaningfully know whether or not it completed in the case where
+          // the driver is unresponsive.
+          invokeCallback(work.cb, "database connection unavailable");
+          self._resetConnection();
+          slowQueryTimer = null;
+          self._runNextQuery();
+        }
+      }, config.get('database.max_query_time_ms'));
+
+      this.realClient.query(work.query, work.args, function(err, r) {
+        // if we want to simulate a "stalled" mysql connection, we simply
+        // ignore the results from a query.
+        if (stalled) return;
+
+        clearTimeout(slowQueryTimer);
+        slowQueryTimer = null;
+        consecutiveFailures = 0;
+
+        // report query time for all queries via statsd
+        var reqTime = new Date - work.startTime;
+        statsd.timing('query_time', reqTime);
+
+        // report failed queries via statsd
+        if (err) statsd.increment('failed_query'); 
+
+        invokeCallback(work.cb, err, r);
+        self._runNextQuery();
+      });
+    },
+    query: function() {
+      var client_cb;
+      var args = Array.prototype.slice.call(arguments);
+      var query = args.shift();
+      if (args.length && typeof args[args.length - 1] === 'function') {
+        client_cb = args.pop();
+      }
+      args = args.length ? args[0] : [];
+      queryQueue.push({
+        query: query,
+        args: args,
+        cb: client_cb,
+        // record the time .query was called by the application for
+        // true end to end query timing in statsd
+        startTime: new Date()
+      });
+      this._runNextQuery();
+    },
+    end: function(cb) {
+      this.realClient.end(cb);
+    },
+    useDatabase: function(db, cb) {
+      this.realClient.useDatabase(db, cb);
+    }
+  };
+  client._resetConnection();
+  client.database = client.realClient.database;
+  return client;
+};
diff --git a/lib/httputils.js b/lib/httputils.js
index 2491e5ee34a978b30c91aa53c4b69d772f89887d..81e68334d52bcf82484bef7e0d086f8ab471e17f 100644
--- a/lib/httputils.js
+++ b/lib/httputils.js
@@ -5,53 +5,37 @@
 // various little utilities to make crafting boilerplate responses
 // simple
 
-exports.fourOhFour = function(resp, reason)
-{
-  resp.writeHead(404, {"Content-Type": "text/plain"});
-  resp.write("Not Found");
-  if (reason) {
-    resp.write(": " + reason);
+function sendResponse(resp, content, reason, code) {
+  if (content) {
+    if (reason) content += ": " + reason;
+  } else if (reason) {
+    content = reason;
+  } else {
+    content = "";
   }
-  resp.end();
+  resp.send(content, {"Content-Type": "text/plain"}, code);
+}
+
+exports.notFound = function(resp, reason) {
+  sendResponse(resp, "Not Found", reason, 404);
 };
 
-exports.serverError = function(resp, reason)
-{
-  resp.writeHead(500, {"Content-Type": "text/plain"});
-  if (reason) resp.write(reason);
-  resp.end();
+exports.serverError = function(resp, reason) {
+  sendResponse(resp, "Server Error", reason, 500);
 };
 
-exports.badRequest = function(resp, reason)
-{
-  resp.writeHead(400, {"Content-Type": "text/plain"});
-  resp.write("Bad Request");
-  if (reason) {
-    resp.write(": " + reason);
-  }
-  resp.end();
+exports.serviceUnavailable = function(resp, reason) {
+  sendResponse(resp, "Service Unavailable", reason, 503);
 };
 
-exports.forbidden = function(resp, reason)
-{
-  resp.writeHead(403, {"Content-Type": "text/plain"});
-  resp.write("Forbidden");
-  if (reason) {
-    resp.write(": " + reason);
-  }
-  resp.end();
+exports.badRequest = function(resp, reason) {
+  sendResponse(resp, "Bad Request", reason, 400);
 };
 
-exports.jsonResponse = function(resp, obj)
-{
-  resp.writeHead(200, {"Content-Type": "application/json"});
-  if (obj !== undefined) resp.write(JSON.stringify(obj));
-  resp.end();
+exports.forbidden = function(resp, reason) {
+  sendResponse(resp, "Forbidden", reason, 403);
 };
 
-exports.xmlResponse = function(resp, doc)
-{
-  resp.writeHead(200, {"Content-Type": "text/xml"});
-  if (doc !== undefined) resp.write(doc);
-  resp.end();
+exports.throttled = function(resp, reason) {
+  sendResponse(resp, "Too Many Requests", reason, 429);
 };
diff --git a/lib/static_resources.js b/lib/static_resources.js
new file mode 100644
index 0000000000000000000000000000000000000000..859d3d88ca5f9497ee03fbdcdafcd4e3c41705c0
--- /dev/null
+++ b/lib/static_resources.js
@@ -0,0 +1,150 @@
+var i18n = require('./i18n'),
+    und = require('underscore');
+
+/**
+ * Module for managing all the known static assets in browserid.
+ * In filenames/paths below, you may use ``:locale`` as a url
+ * variable to be expanded later.
+ *
+ * These settings affect usage of cachify and eventually our
+ * asset build steps.
+ *
+ * Be careful editing common_js, as it will affect all
+ * minified scripts that depend on that variable. IE re-ordering
+ * the list or removing a script.
+ */
+
+// Common to browserid.js dialog.js
+var common_js = [
+  '/lib/jquery-1.7.1.min.js',
+  '/lib/winchan.js',
+  '/lib/underscore-min.js',
+  '/lib/vepbundle.js',
+  '/lib/ejs.js',
+  '/shared/javascript-extensions.js',
+  '/i18n/:locale/client.json',
+  '/shared/gettext.js',
+  '/shared/browserid.js',
+  '/lib/hub.js',
+  '/lib/dom-jquery.js',
+  '/lib/module.js',
+  '/lib/jschannel.js',
+  '/shared/templates.js',
+  '/shared/renderer.js',
+  '/shared/class.js',
+  '/shared/mediator.js',
+  '/shared/tooltip.js',
+  '/shared/validation.js',
+  '/shared/helpers.js',
+  '/shared/screens.js',
+  '/shared/browser-support.js',
+  '/shared/wait-messages.js',
+  '/shared/error-messages.js',
+  '/shared/error-display.js',
+  '/shared/storage.js',
+  '/shared/xhr.js',
+  '/shared/network.js',
+  '/shared/provisioning.js',
+  '/shared/user.js',
+  '/shared/modules/page_module.js',
+  '/shared/modules/xhr_delay.js',
+  '/shared/modules/xhr_disable_form.js',
+  '/shared/modules/cookie_check.js'
+];
+
+var browserid_min_js = '/production/:locale/browserid.js';
+var browserid_js = und.flatten([
+  common_js,
+  [
+    '/pages/page_helpers.js',
+    '/pages/index.js',
+    '/pages/start.js',
+    '/pages/add_email_address.js',
+    '/pages/verify_email_address.js',
+    '/pages/forgot.js',
+    '/pages/manage_account.js',
+    '/pages/signin.js',
+    '/pages/signup.js'
+  ]
+]);
+
+var dialog_min_js = '/production/:locale/dialog.js';
+var dialog_js = und.flatten([
+  common_js,
+  [
+    '/lib/urlparse.js',
+
+    '/shared/command.js',
+    '/shared/history.js',
+    '/shared/state_machine.js',
+
+    '/dialog/resources/internal_api.js',
+    '/dialog/resources/helpers.js',
+    '/dialog/resources/state.js',
+
+    '/dialog/controllers/actions.js',
+    '/dialog/controllers/dialog.js',
+    '/dialog/controllers/authenticate.js',
+    '/dialog/controllers/forgot_password.js',
+    '/dialog/controllers/check_registration.js',
+    '/dialog/controllers/pick_email.js',
+    '/dialog/controllers/add_email.js',
+    '/dialog/controllers/required_email.js',
+    '/dialog/controllers/verify_primary_user.js',
+    '/dialog/controllers/provision_primary_user.js',
+    '/dialog/controllers/primary_user_provisioned.js',
+    '/dialog/controllers/email_chosen.js',
+
+    '/dialog/start.js'
+  ]]);
+
+exports.resources = resources = {
+  '/production/dialog.css': [
+    '/css/common.css',
+    '/dialog/css/popup.css',
+    '/dialog/css/m.css'
+  ],
+  '/production/browserid.css': [
+    '/css/common.css',
+    '/css/style.css',
+    '/css/m.css'
+  ]
+};
+resources[dialog_min_js] = dialog_js;
+resources[browserid_min_js] = browserid_js;
+
+var replace = function(path, locale) { return path.replace(':locale', locale); };
+
+/**
+ * Returns all filenames of static resources
+ * in a connect-cachify compatible format.
+ *
+ * @langs - array of languages we support
+ * @return { minified_file: [dependent, files] }
+ *
+ * Languages will be converted to locales. Filenames and list of files
+ * will be expanded to match all the permutations.
+ */
+exports.all = function(langs) {
+  var res = {};
+  for (var f in resources) {
+    langs.forEach(function (lang) {
+      var l = i18n.localeFrom(lang);
+      res[replace(f, l)] = getResources(f, l);
+    });
+  }
+  return res;
+};
+
+/**
+ * Get all resource urls for a specified resource based on the locale
+ */
+exports.getResources = getResources = function(path, locale) {
+  var res = [];
+  if (resources[path]) {
+    resources[path].forEach(function(r) {
+      res.push(replace(r, locale));
+    });
+  }
+  return res;
+};
diff --git a/lib/wsapi.js b/lib/wsapi.js
index 8df4ff9aca5077991229e8474787774bb9ac602e..e42d6f828259d12a496d568d35a4088762aab268 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -38,7 +38,18 @@ var abide = i18n.abide({
 });
 
 const COOKIE_SECRET = secrets.hydrateSecret('browserid_cookie', config.get('var_path'));
-const COOKIE_KEY = 'browserid_state';
+var COOKIE_KEY = 'browserid_state';
+
+// to support testing of browserid, we'll add a hash fragment to the cookie name for
+// sites other than browserid.org.  This is to address a bug in IE, see issue #296
+if (config.get('public_url').indexOf('https://browserid.org') !== 0) {
+  const crypto = require('crypto');
+  var hash = crypto.createHash('md5');
+  hash.update(config.get('public_url'));
+  COOKIE_KEY += "_" + hash.digest('hex').slice(0, 6);
+}
+
+logger.info('session cookie name is: ' + COOKIE_KEY);
 
 function clearAuthenticatedUser(session) {
   session.reset(['csrf']);
@@ -69,8 +80,15 @@ function authenticateSession(session, uid, level) {
   if (['assertion', 'password'].indexOf(level) === -1)
     throw "invalid authentication level: " + level;
 
-  session.userid = uid;
-  session.auth_level = level;
+  // if the user is *already* authenticated as this uid with an equal or better
+  // level of auth, let's not lower them.  Issue #1049
+  if (session.userid === uid && session.auth_level === 'password' &&
+      session.auth_level !== level) {
+    logger.info("not resetting cookies to 'assertion' authenticate a user who is already password authenticated"); 
+  } else {
+    session.userid = uid;
+    session.auth_level = level;
+  }
 }
 
 function checkPassword(pass) {
@@ -89,6 +107,11 @@ function langContext(req) {
   };
 }
 
+function databaseDown(res, err) {
+  logger.warn('database is down, cannot process request: ' + err);
+  httputils.serviceUnavailable(res, "database unavailable");
+}
+
 // common functions exported, for use by different api calls
 exports.clearAuthenticatedUser = clearAuthenticatedUser;
 exports.isAuthed = isAuthed;
@@ -97,6 +120,7 @@ exports.authenticateSession = authenticateSession;
 exports.checkPassword = checkPassword;
 exports.fowardWritesTo = undefined;
 exports.langContext = langContext;
+exports.databaseDown = databaseDown;
 
 exports.setup = function(options, app) {
   const WSAPI_PREFIX = '/wsapi/';
@@ -182,7 +206,7 @@ exports.setup = function(options, app) {
 
                 if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
                   logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
-                  return httputils.badRequest(resp, "no cookie");
+                  return httputils.forbidden(resp, "no cookie");
                 }
 
                 // and the token must match what is sent in the post body
diff --git a/lib/wsapi/account_cancel.js b/lib/wsapi/account_cancel.js
index c91428f8745102980299e4b0bfa1d4b31b59099c..a0e3644ab3183aeda642259f4e40a967886aa449 100644
--- a/lib/wsapi/account_cancel.js
+++ b/lib/wsapi/account_cancel.js
@@ -4,7 +4,7 @@
 
 const
 db = require('../db.js'),
-httputils = require('../httputils'),
+wsapi = require('../wsapi'),
 logger = require('../logging.js').logger;
 
 exports.method = 'post';
@@ -15,8 +15,7 @@ exports.i18n = false;
 exports.process = function(req, res) {
   db.cancelAccount(req.session.userid, function(error) {
     if (error) {
-      logger.error("error canceling account : " + error.toString());
-      httputils.badRequest(res, error.toString());
+      wsapi.databaseDown(res, error);
     } else {
       res.json({ success: true });
     }});
diff --git a/lib/wsapi/add_email_with_assertion.js b/lib/wsapi/add_email_with_assertion.js
index e0ab5a626ce38087d88974cacd306318992a36d3..e8649ceb64911cfc659eefc2ae44c5da639d9980 100644
--- a/lib/wsapi/add_email_with_assertion.js
+++ b/lib/wsapi/add_email_with_assertion.js
@@ -38,10 +38,7 @@ exports.process = function(req, res) {
       if (err) {
         logger.warn('cannot add primary email "' + email + '" to acct with uid "'
                     + req.session.userid + '": ' + err);
-        return res.json({
-          success: false,
-          reason: "database error"
-        });
+        return wsapi.databaseDown(res, err);
       }
 
       // success!
diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js
index 0cd29bd0595afd5121533bf6adbb89faaec2582d..bfccae4b9af457f43520e46dcfc2aa81de5d67fa 100644
--- a/lib/wsapi/address_info.js
+++ b/lib/wsapi/address_info.js
@@ -4,7 +4,8 @@
 
 const
 db = require('../db.js'),
-primary = require('../primary.js');
+primary = require('../primary.js'),
+wsapi = require('../wsapi.js');
 
 // return information about an email address.
 //   type:  is this an address with 'primary' or 'secondary' support?
@@ -27,28 +28,25 @@ exports.process = function(req, resp) {
   var email = url.parse(req.url, true).query['email'];
   var m = emailRegex.exec(email);
   if (!m) {
-    resp.sendHeader(400);
-    resp.json({ "error": "invalid email address" });
-    return;
+    return httputils.badRequest(resp, "invalid email address");
   }
 
   primary.checkSupport(m[1], function(err, rv) {
     if (err) {
       logger.warn('error checking "' + m[1] + '" for primary support: ' + err);
-      resp.sendHeader(500);
-      resp.json({ "error": "can't check email address" });
-      return;
+      return httputils.serverError(resp, "can't check email address");
     }
 
     if (rv) {
       rv.type = 'primary';
       resp.json(rv);
     } else {
-      db.emailKnown(email, function(known) {
-        resp.json({
-          type: 'secondary',
-          known: known
-        });
+      db.emailKnown(email, function(err, known) {
+        if (err) {
+          return wsapi.databaseDown(resp, err);
+        } else {
+          resp.json({ type: 'secondary', known: known });
+        }
       });
     }
   });
diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js
index 3cd5075e9fd7351103cd97d43dada13efc7e5405..8781151358379e93e4f3e0bbe09c182c62b75667 100644
--- a/lib/wsapi/auth_with_assertion.js
+++ b/lib/wsapi/auth_with_assertion.js
@@ -33,10 +33,13 @@ exports.process = function(req, res) {
     }
 
     // 2. if valid, does the user exist?
-    db.emailType(email, function(type) {
+    db.emailType(email, function(err, type) {
+      if (err) return wsapi.databaseDown(res, err);
+
       // if this is a known primary email, authenticate the user and we're done!
       if (type === 'primary') {
-        return db.emailToUID(email, function(uid) {
+        return db.emailToUID(email, function(err, uid) {
+          if (err) return wsapi.databaseDown(res, err);
           if (!uid) return res.json({ success: false, reason: "internal error" });
           wsapi.authenticateSession(req.session, uid, 'assertion');
           return res.json({ success: true });
diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js
index 22cc195e9360ec46c3455c48c54ad4ec7d1ba405..b1715a1b4c21fce281502e366ab4c1b47b8877fd 100644
--- a/lib/wsapi/authenticate_user.js
+++ b/lib/wsapi/authenticate_user.js
@@ -27,12 +27,16 @@ exports.process = function(req, res) {
     return res.json(r);
   }
 
-  db.emailToUID(req.body.email, function(uid) {
+  db.emailToUID(req.body.email, function(err, uid) {
+    if (err) return wsapi.databaseDown(res, err);
+
     if (typeof uid !== 'number') {
       return fail('no such user');
     }
 
-    db.checkAuth(uid, function(hash) {
+    db.checkAuth(uid, function(err, hash) {
+      if (err) return wsapi.databaseDown(res, err);
+
       if (typeof hash !== 'string') {
         return fail('no password set for user');
       }
diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js
index 84b1b7582dd865174d381eae326723a3661c410a..9b642eb341646f360b8e4288dd166ed15b6d6713 100644
--- a/lib/wsapi/cert_key.js
+++ b/lib/wsapi/cert_key.js
@@ -8,7 +8,8 @@ httputils = require('../httputils'),
 logger = require('../logging.js').logger,
 forward = require('../http_forward.js'),
 config = require('../configuration.js'),
-urlparse = require('urlparse');
+urlparse = require('urlparse'),
+wsapi = require('../wsapi.js');
 
 exports.method = 'post';
 exports.writes_db = false;
@@ -17,7 +18,9 @@ exports.args = ['email','pubkey'];
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  db.userOwnsEmail(req.session.userid, req.body.email, function(owned) {
+  db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) {
+    if (err) return wsapi.databaseDown(res, err);
+
     // not same account? big fat error
     if (!owned) return httputils.badRequest(res, "that email does not belong to you");
 
@@ -26,9 +29,8 @@ exports.process = function(req, res) {
     keysigner.path = '/wsapi/cert_key';
     forward(keysigner, req, res, function(err) {
       if (err) {
-        logger.error("error forwarding request: " + err);
-        res.sendHeader(500);
-        res.json({ "error": "can't contact keysigner" });
+        logger.error("error forwarding request to keysigner: " + err);
+        httputils.serverError(res, "can't contact keysigner");
         return;
       }
     });
diff --git a/lib/wsapi/complete_email_addition.js b/lib/wsapi/complete_email_addition.js
index 7c61b60a6c85700c1a623ce8b1dab6ab9b2f5aac..1359b49c379efab649d942f988f37f8c466c7dfa 100644
--- a/lib/wsapi/complete_email_addition.js
+++ b/lib/wsapi/complete_email_addition.js
@@ -21,6 +21,10 @@ exports.process = function(req, res) {
   // is currently NULL - this would occur in the case where this is the
   // first secondary address to be added to an account
   db.emailForVerificationSecret(req.body.token, function(err, r) {
+    if (err === 'database unavailable') {
+      return wsapi.databaseDown(res, err);
+    }
+
     if (!err && r.needs_password && !req.body.pass) {
       err = "user must choose a password";
     }
@@ -40,7 +44,7 @@ exports.process = function(req, res) {
     db.gotVerificationSecret(req.body.token, req.body.pass, function(e, email, uid) {
       if (e) {
         logger.warn("couldn't complete email verification: " + e);
-        res.json({ success: false });
+        wsapi.databaseDown(res, e);
       } else {
         // now do we need to set the password?
         if (r.needs_password && req.body.pass) {
@@ -52,12 +56,13 @@ exports.process = function(req, res) {
             db.updatePassword(uid, hash, function(err) {
               if (err) {
                 logger.warn("couldn't update password during email verification: " + err);
+                wsapi.databaseDown(res, err);
               } else {
-                // XXX: what if our software 503s?  User doens't get a password set and
+                // XXX: what if our software 503s?  User doesn't get a password set and
                 // cannot change it.
                 wsapi.authenticateSession(req.session, uid, 'password');
+                res.json({ success: !err });
               }
-              res.json({ success: !err });
             });
           });
         } else {
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index 5249c90bb96e74b4f48098c8b826768a2444cccd..882351b630f784c34528302de0682ff9870859fc 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -26,17 +26,17 @@ exports.process = function(req, res) {
   // We should check to see if the verification secret is valid *before*
   // bcrypting the password (which is expensive), to prevent a possible
   // DoS attack.
-  db.haveVerificationSecret(req.body.token, function(known) {
+  db.haveVerificationSecret(req.body.token, function(err, known) {
+    if (err) return wsapi.databaseDown(res, err);
+
     if (!known) return res.json({ success: false} );
 
     // now bcrypt the password
     wsapi.bcryptPassword(req.body.pass, function (err, hash) {
       if (err) {
-        console.log(err);
         if (err.indexOf('exceeded') != -1) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
-          res.status(503);
-          return res.json({ success: false, reason: "server is too busy" });
+          return httputils.serviceUnavailable("server is too busy");
         }
         logger.error("can't bcrypt: " + err);
         return res.json({ success: false });
@@ -45,7 +45,7 @@ exports.process = function(req, res) {
       db.gotVerificationSecret(req.body.token, hash, function(err, email, uid) {
         if (err) {
           logger.warn("couldn't complete email verification: " + err);
-          res.json({ success: false });
+          wsapi.databaseDown(res, err);
         } else {
           // FIXME: not sure if we want to do this (ba)
           // at this point the user has set a password associated with an email address
diff --git a/lib/wsapi/create_account_with_assertion.js b/lib/wsapi/create_account_with_assertion.js
index 58cf266465c7d636e5eeb55f21202f883a5264bd..13f96d395fb6b56c7f4fa85adfcbc38472ea48cf 100644
--- a/lib/wsapi/create_account_with_assertion.js
+++ b/lib/wsapi/create_account_with_assertion.js
@@ -27,11 +27,7 @@ exports.process = function(req, res) {
     }
 
     db.createUserWithPrimaryEmail(email, function(err, uid) {
-      if (err) {
-        // yikes.  couldn't write database?
-        logger.error('error creating user with primary email address for "'+email+'": ' + err);
-        return httputils.serverError(res);
-      }
+      if (err) return wsapi.databaseDown(res);
       res.json({ success: true, userid: uid });
     });
   });
diff --git a/lib/wsapi/email_addition_status.js b/lib/wsapi/email_addition_status.js
index 833820e39aaf7db6ed4d32f02cb85894ba8639e8..5a7a3017d536a095cb2072137e94a6c6fa436021 100644
--- a/lib/wsapi/email_addition_status.js
+++ b/lib/wsapi/email_addition_status.js
@@ -3,7 +3,8 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const
-db = require('../db.js');
+db = require('../db.js'),
+wsapi = require('../wsapi.js');
 
 /* First half of account creation.  Stages a user account for creation.
  * this involves creating a secret url that must be delivered to the
@@ -25,15 +26,19 @@ exports.process = function(req, res) {
   db.userOwnsEmail(
     req.session.userid,
     email,
-    function(registered) {
-      if (registered) {
+    function(err, registered) {
+      if (err) {
+        wsapi.databaseDown(res, err);
+      } else if (registered) {
         delete req.session.pendingAddition;
         res.json({ status: 'complete' });
       } else if (!req.session.pendingAddition) {
         res.json({ status: 'failed' });
       } else {
-        db.haveVerificationSecret(req.session.pendingAddition, function (known) {
-          if (known) {
+        db.haveVerificationSecret(req.session.pendingAddition, function (err, known) {
+          if (err) {
+            return wsapi.databaseDown(res, err);
+          } else if (known) {
             return res.json({ status: 'pending' });
           } else {
             delete req.session.pendingAddition;
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index 4856cdcf0873a6fa3405134e83324faf419242a4..bfb122a747e8a514d1aca045fb3f14d7567fc6de 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -3,7 +3,8 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const
-db = require('../db.js');
+db = require('../db.js'),
+httputils = require('../httputils.js');
 
 /* First half of account creation.  Stages a user account for creation.
  * this involves creating a secret url that must be delivered to the
@@ -20,10 +21,14 @@ exports.i18n = false;
 exports.process = function(req, res) {
   db.emailForVerificationSecret(req.query.token, function(err, r) {
     if (err) {
-      res.json({
-        success: false,
-        reason: err
-      });
+      if (err === 'database unavailable') {
+        httputils.serviceUnavailable(res, err);
+      } else {
+        res.json({
+          success: false,
+          reason: err
+        });
+      }
     } else {
       res.json({
         success: true,
diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js
index 05b88930b230be51ff30f1f2e18064f1d1ed8400..ec832546bc261fa1970197a5eda4c0e7312afcd0 100644
--- a/lib/wsapi/have_email.js
+++ b/lib/wsapi/have_email.js
@@ -3,7 +3,8 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const
-db = require('../db.js');
+db = require('../db.js'),
+wsapi = require('../wsapi.js');
 
 // return if an email is known to browserid
 
@@ -15,7 +16,8 @@ exports.i18n = false;
 
 exports.process = function(req, resp) {
   var email = url.parse(req.url, true).query['email'];
-  db.emailKnown(email, function(known) {
+  db.emailKnown(email, function(err, known) {
+    if (err) return wsapi.databaseDown(resp, err);
     resp.json({ email_known: known });
   });
 };
diff --git a/lib/wsapi/list_emails.js b/lib/wsapi/list_emails.js
index fbe4fd64b5fa4bdc9cab607ceccd2cba638c4ca4..6da607007c3b967b53ccc4b6d77834c705db4387 100644
--- a/lib/wsapi/list_emails.js
+++ b/lib/wsapi/list_emails.js
@@ -4,7 +4,8 @@
 
 const
 db = require('../db.js'),
-logger = require('../logging.js').logger;
+logger = require('../logging.js').logger,
+wsapi = require('../wsapi.js');
 
 // returns a list of emails owned by the user:
 //
@@ -21,7 +22,7 @@ exports.i18n = false;
 exports.process = function(req, resp) {
   logger.debug('listing emails for user ' + req.session.userid);
   db.listEmails(req.session.userid, function(err, emails) {
-    if (err) httputils.serverError(resp, err);
+    if (err) wsapi.databaseDown(resp, err);
     else resp.json(emails);
   });
 };
diff --git a/lib/wsapi/remove_email.js b/lib/wsapi/remove_email.js
index fe7dc3c93265dfb2210628604ebe18b7b60a3b84..145adf03deab484899eb96fffacfb55b1d3b0d90 100644
--- a/lib/wsapi/remove_email.js
+++ b/lib/wsapi/remove_email.js
@@ -4,6 +4,7 @@
 
 const
 db = require('../db.js'),
+wsapi = require('../wsapi'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger;
 
@@ -18,8 +19,12 @@ exports.process = function(req, res) {
 
   db.removeEmail(req.session.userid, email, function(error) {
     if (error) {
-      logger.error("error removing email " + email);
-      httputils.badRequest(res, error.toString());
+      logger.warn("error removing email " + email);
+      if (error === 'database connection unavailable') {
+        wsapi.databaseDown(res, error);
+      } else {
+        httputils.badRequest(res, error.toString());
+      }
     } else {
       res.json({ success: true });
     }});
diff --git a/lib/wsapi/session_context.js b/lib/wsapi/session_context.js
index 08a82cf01e0234808ab7923327bc6f6612087e8a..8b7f9e13d7a058f28192ae354cfe55b2a9e5b09b 100644
--- a/lib/wsapi/session_context.js
+++ b/lib/wsapi/session_context.js
@@ -59,8 +59,10 @@ exports.process = function(req, res) {
     logger.debug("user is not authenticated");
     sendResponse();
   } else {
-    db.userKnown(req.session.userid, function (known) {
-      if (!known) {
+    db.userKnown(req.session.userid, function (err, known) {
+      if (err) {
+        return wsapi.databaseDown(res, err);
+      } else if (!known) {
         logger.debug("user is authenticated with an account that doesn't exist in the database");
         wsapi.clearAuthenticatedUser(req.session);
       } else {
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
index c5b562304f4ca262e5eff7b2e66e025bb435b783..8acda357269408aacc0fb5741172dd9015c4709c 100644
--- a/lib/wsapi/stage_email.js
+++ b/lib/wsapi/stage_email.js
@@ -22,16 +22,20 @@ exports.args = ['email','site'];
 exports.i18n = true;
 
 exports.process = function(req, res) {
-  db.lastStaged(req.body.email, function (last) {
+  db.lastStaged(req.body.email, function (err, last) {
+    if (err) return wsapi.databaseDown(res, err);
+
     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(res, "throttling.  try again later.");
+      return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
 
     try {
       // on failure stageEmail may throw
-      db.stageEmail(req.session.userid, req.body.email, function(secret) {
+      db.stageEmail(req.session.userid, req.body.email, function(err, secret) {
+        if (err) return wsapi.databaseDown(res, err);
+
         var langContext = wsapi.langContext(req);
 
         // store the email being added in session data
diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js
index dc5f0aef04cb392a3e02de3f31de20f450bf2242..14bb947e148a270e4f5b25c7714a1d24f94114fd 100644
--- a/lib/wsapi/stage_user.js
+++ b/lib/wsapi/stage_user.js
@@ -27,17 +27,21 @@ exports.process = function(req, resp) {
   // staging a user logs you out.
   wsapi.clearAuthenticatedUser(req.session);
 
-  db.lastStaged(req.body.email, function (last) {
+  db.lastStaged(req.body.email, function (err, last) {
+    if (err) return wsapi.databaseDown(resp, err);
+
     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.");
+      return httputils.throttled(resp, "Too many emails sent to that address, try again later.");
     }
 
     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) {
+      db.stageUser(req.body.email, function(err, secret) {
+        if (err) return wsapi.databaseDown(resp, err);
+
         // store the email being registered in the session data
         if (!req.session) req.session = {};
 
diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js
index e98a285b055ef56b747c8a7edb6743bffb41a495..d7a395c3a49a7cf4d8330bf30b703443ce0fe3de 100644
--- a/lib/wsapi/update_password.js
+++ b/lib/wsapi/update_password.js
@@ -24,7 +24,9 @@ exports.process = function(req, res) {
     });
   }
 
-  db.checkAuth(req.session.userid, function(hash) {
+  db.checkAuth(req.session.userid, function(err, hash) {
+    if (err) return wsapi.databaseDown(res, err);
+
     if (typeof hash !== 'string' || typeof req.body.oldpass !== 'string')
     {
       return res.json({ success: false });
@@ -62,9 +64,10 @@ exports.process = function(req, res) {
           var success = true;
           if (err) {
             logger.error("error updating bcrypted password for email " + req.body.email, err);
-            success = false;
+            wsapi.databaseDown(res, err);
+          } else {
+            res.json({ success: success });
           }
-          return res.json({ success: success });
         });
       });
     });
diff --git a/lib/wsapi/user_creation_status.js b/lib/wsapi/user_creation_status.js
index e866430862a653f1a4bbd24c425d4db0d434079f..e6812c9c26bd722aef6f17f074b04bffaff1402c 100644
--- a/lib/wsapi/user_creation_status.js
+++ b/lib/wsapi/user_creation_status.js
@@ -17,8 +17,9 @@ exports.process = function(req, res) {
 
   // if the user is authenticated as the user in question, we're done
   if (wsapi.isAuthed(req, 'assertion')) {
-    db.userOwnsEmail(req.session.userid, email, function(owned) {
-      if (owned) res.json({ status: 'complete' });
+    db.userOwnsEmail(req.session.userid, email, function(err, owned) {
+      if (err) wsapi.databaseDown(res, err);
+      else if (owned) res.json({ status: 'complete' });
       else notAuthed();
     });
   } else {
@@ -34,7 +35,9 @@ exports.process = function(req, res) {
 
     // if the secret is still in the database, it hasn't yet been verified and
     // verification is still pending
-    db.haveVerificationSecret(req.session.pendingCreation, function (known) {
+    db.haveVerificationSecret(req.session.pendingCreation, function (err, known) {
+      if (err) return wsapi.databaseDown(res, err);
+
       if (known) return res.json({ status: 'pending' });
       // if the secret isn't known, and we're not authenticated, then the user must authenticate
       // (maybe they verified the URL on a different browser, or maybe they canceled the account
diff --git a/package.json b/package.json
index 864d5d385db0721159015f52f142a8fee0baa0fc..a5fc6a2919e3bd6bb7b41adc813a623b16c337fa 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
         "convict": "0.0.6",
         "cjson": "0.0.6",
         "client-sessions": "0.0.3",
+        "connect-cachify": "0.0.8",
         "connect-cookie-session": "0.0.2",
         "connect-logger-statsd": "0.0.1",
         "ejs": "0.4.3",
@@ -22,11 +23,12 @@
         "node-statsd": "https://github.com/downloads/lloyd/node-statsd/3a73de.tgz",
         "nodemailer": "0.1.18",
         "optimist": "0.2.8",
-        "postprocess": "0.2.1",
+        "postprocess": "0.2.4",
         "semver": "1.0.12",
         "temp": "0.2.0",
         "uglify-js": "1.0.6",
         "uglifycss": "0.0.4",
+        "underscore": "1.3.1",
         "urlparse": "0.0.1",
         "winston": "0.5.6"
     },
diff --git a/resources/static/css/common.css b/resources/static/css/common.css
index d0c57bd5a9c6d22f92006f3579a2c0e1353750a6..558d082f2b8ab8fb4acf974711df9f78466ed97a 100644
--- a/resources/static/css/common.css
+++ b/resources/static/css/common.css
@@ -21,7 +21,7 @@ body {
   font-size: 13px;
   line-height: 21px;
   background-image: url('/i/bg.png');
-  overflow-y: scroll;
+  overflow-y: auto;
 }
 
 /* for floats */
@@ -146,7 +146,6 @@ button,
     font-family: 'Droid Serif', Georgia, serif;
     color: #fff;
     text-shadow: -1px -1px 0 #37A6FF;
-    text-transform: lowercase;
     cursor: pointer;
 
     -webkit-box-shadow: 0 0 0 1px #76C2FF inset;
diff --git a/resources/static/css/m.css b/resources/static/css/m.css
index c8004bb1d30b84a130f6c05e4d63c6dd2e37ac25..66274ef26d56b47b296906d4d9c91ba91a02b8c0 100644
--- a/resources/static/css/m.css
+++ b/resources/static/css/m.css
@@ -99,7 +99,7 @@
     padding: 0 10px;
     font-size: 16px;
     line-height: 21px;
-    margin: 122px 0 122px;
+    margin: 0 0 90px;  /* Add a bottom margin so the footer is never overlapped. */
   }
 
   #signUp p {
@@ -231,4 +231,8 @@
     float: right;
   }
 
+  #newsbanner {
+    margin: 115px 0 28px 0; /* put a margin-top on so that it does not go under the header */
+
+  }
 }
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index 0dfb71a30495e00c7a74da99a02c8c5b1a1df328..7e42ec91215f19a5f1aafd04aed6b5032fa7e6f2 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -808,3 +808,18 @@ footer {
   bottom: 0;
 }
 
+#newsbanner {
+  margin-top: 60px; /* put a margin-top on so that it does not go under the header */
+  background-color: #faca33;
+  line-height: 32px;
+  border-radius: 4px;
+  margin-bottom: 20px;
+  text-align: center;
+  color: #626160;
+  text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
+  -webkit-transition: all 500ms;
+  -moz-transition: all 500ms;
+  -ms-transition: all 500ms;
+  -o-transition: all 500ms;
+  transition: all 500ms;
+}
diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index bdc78df16ac55c6df3abe6f23d312c187ca83e25..320877daf875844026c6909919e5660447cc13c7 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -67,10 +67,6 @@ BrowserID.Modules.Actions = (function() {
       this.renderError(template, info);
     },
 
-    doOffline: function() {
-      this.renderError("offline", {});
-    },
-
     doCancel: function() {
       if(onsuccess) onsuccess(null);
     },
diff --git a/resources/static/dialog/controllers/add_email.js b/resources/static/dialog/controllers/add_email.js
index 1ef8d01fb3622c74f955da684fddbd8db3cd0d9b..8711e69d1c9506c5478ae89a6e62fd312c8a31f5 100644
--- a/resources/static/dialog/controllers/add_email.js
+++ b/resources/static/dialog/controllers/add_email.js
@@ -9,7 +9,6 @@ BrowserID.Modules.AddEmail = (function() {
   var bid = BrowserID,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
-      cancelEvent = dialogHelpers.cancelEvent,
       errors = bid.Errors,
       complete = helpers.complete,
       tooltip = bid.Tooltip;
@@ -37,7 +36,7 @@ BrowserID.Modules.AddEmail = (function() {
 
       self.renderDialog("add_email", options);
 
-      self.bind("#cancel", "click", cancelEvent(cancelAddEmail));
+      self.click("#cancel", cancelAddEmail);
       Module.sc.start.call(self, options);
     },
     submit: addEmail
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index c8f376d3de4ed5d928d04489c386b3885376adac..be2b793250f2de1a54e424bdbb3a2e2ac30d0ac3 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -14,7 +14,6 @@ BrowserID.Modules.Authenticate = (function() {
       tooltip = bid.Tooltip,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
-      cancelEvent = helpers.cancelEvent,
       complete = helpers.complete,
       dom = bid.DOM,
       lastEmail = "",
@@ -62,6 +61,7 @@ BrowserID.Modules.Authenticate = (function() {
       } else {
         createSecondaryUserState.call(self);
       }
+      $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px');
     }
   }
 
@@ -151,16 +151,19 @@ BrowserID.Modules.Authenticate = (function() {
       var self=this;
       self.renderDialog("authenticate", {
         sitename: user.getHostname(),
-        email: lastEmail
+        email: lastEmail,
+        privacy_url: options.privacyURL,
+        tos_url: options.tosURL
       });
 
       $(".newuser,.forgot,.returning,.start").hide();
 
       self.bind("#email", "keyup", emailKeyUp);
-      self.bind("#forgotPassword", "click", cancelEvent(forgotPassword));
+      self.click("#forgotPassword", forgotPassword);
 
       Module.sc.start.call(self, options);
       initialState.call(self, options);
+      $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px');
     }
 
     // BEGIN TESTING API
diff --git a/resources/static/dialog/controllers/check_registration.js b/resources/static/dialog/controllers/check_registration.js
index 05ed707771575c346337b447980bb4b441c2a25b..4cf14f76f9b573111f7b15cf6a3f594583461f75 100644
--- a/resources/static/dialog/controllers/check_registration.js
+++ b/resources/static/dialog/controllers/check_registration.js
@@ -22,8 +22,8 @@ BrowserID.Modules.CheckRegistration = (function() {
       self.verifier = options.verifier;
       self.verificationMessage = options.verificationMessage;
 
-      self.bind("#back", "click", self.back);
-      self.bind("#cancel", "click", self.cancel);
+      self.click("#back", self.back);
+      self.click("#cancel", self.cancel);
 
       Module.sc.start.call(self, options);
     },
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index 7192887df535bad47060c338a62ee6acefc9c327..9d5c2693bbeeacea1e703d51b0403967feff70b3 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -16,15 +16,6 @@ BrowserID.Modules.Dialog = (function() {
       channel,
       sc;
 
-  function checkOnline() {
-    if (false && 'onLine' in navigator && !navigator.onLine) {
-      this.publish("offline");
-      return false;
-    }
-
-    return true;
-  }
-
   function startActions(onsuccess, onerror) {
     var actions = BrowserID.Modules.Actions.create();
     actions.start({
@@ -90,6 +81,14 @@ BrowserID.Modules.Dialog = (function() {
     this.publish("window_unload");
   }
 
+  function fixupURL(origin, url) {
+    var u;
+    if (/^http/.test(url)) u = URLParse(url);
+    else if (/^\//.test(url)) u = URLParse(origin + url);
+    else throw "relative urls not allowed: (" + url + ")";
+    return u.validate().normalize().toString();
+  }
+
   var Dialog = bid.Modules.PageModule.extend({
     start: function(options) {
       var self=this;
@@ -121,35 +120,41 @@ BrowserID.Modules.Dialog = (function() {
       var actions = startActions.call(self, success, error);
       startStateMachine.call(self, actions);
 
-      if(checkOnline.call(self)) {
-        params = params || {};
-
-        params.hostname = user.getHostname();
-
-        // XXX Perhaps put this into the state machine.
-        self.bind(win, "unload", onWindowUnload);
-
-        if(hash.indexOf("#CREATE_EMAIL=") === 0) {
-          var email = hash.replace(/#CREATE_EMAIL=/, "");
-          params.type = "primary";
-          params.email = email;
-          params.add = false;
-        }
-        else if(hash.indexOf("#ADD_EMAIL=") === 0) {
-          var email = hash.replace(/#ADD_EMAIL=/, "");
-          params.type = "primary";
-          params.email = email;
-          params.add = true;
+      params = params || {};
+      params.hostname = user.getHostname();
+
+      // verify params
+      if (params.tosURL && params.privacyURL) {
+        try {
+          params.tosURL = fixupURL(origin_url, params.tosURL);
+          params.privacyURL = fixupURL(origin_url, params.privacyURL);
+        } catch(e) {
+          return self.renderError("error", {
+            action: {
+              title: "error in " + origin_url,
+              message: "improper usage of API: " + e
+            }
+          });
         }
+      }
 
-        /*
-        if(hash.indexOf("REQUIRED=true") > -1) {
-          params.requiredEmail = params.email;
-        }
-        */
+      // XXX Perhaps put this into the state machine.
+      self.bind(win, "unload", onWindowUnload);
 
-        self.publish("start", params);
+      if(hash.indexOf("#CREATE_EMAIL=") === 0) {
+        var email = hash.replace(/#CREATE_EMAIL=/, "");
+        params.type = "primary";
+        params.email = email;
+        params.add = false;
+      }
+      else if(hash.indexOf("#ADD_EMAIL=") === 0) {
+        var email = hash.replace(/#ADD_EMAIL=/, "");
+        params.type = "primary";
+        params.email = email;
+        params.add = true;
       }
+
+      self.publish("start", params);
     }
 
     // BEGIN TESTING API
diff --git a/resources/static/dialog/controllers/forgot_password.js b/resources/static/dialog/controllers/forgot_password.js
index 69ae248a5ada141e4146cd1160189944def76f81..268f72417b625a66a07ade614a8a1b8d26ceb8e2 100644
--- a/resources/static/dialog/controllers/forgot_password.js
+++ b/resources/static/dialog/controllers/forgot_password.js
@@ -10,7 +10,6 @@ BrowserID.Modules.ForgotPassword = (function() {
       bid = BrowserID,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
-      cancelEvent = dialogHelpers.cancelEvent,
       dom = bid.DOM;
 
   function resetPassword() {
@@ -31,7 +30,7 @@ BrowserID.Modules.ForgotPassword = (function() {
         requiredEmail: options.requiredEmail
       });
 
-      self.bind("#cancel", "click", cancelEvent(cancelResetPassword));
+      self.click("#cancel", cancelResetPassword);
 
       Module.sc.start.call(self, options);
     },
diff --git a/resources/static/dialog/controllers/pick_email.js b/resources/static/dialog/controllers/pick_email.js
index ce39bd9fde150e60c7595c6f1056a8f3c75fd969..f268c105f475a89abaeacda3c9aad7b0dd3c2676 100644
--- a/resources/static/dialog/controllers/pick_email.js
+++ b/resources/static/dialog/controllers/pick_email.js
@@ -11,7 +11,6 @@ BrowserID.Modules.PickEmail = (function() {
       errors = bid.Errors,
       storage = bid.Storage,
       helpers = bid.Helpers,
-      cancelEvent = helpers.cancelEvent,
       dialogHelpers = helpers.Dialog,
       dom = bid.DOM,
       sc;
@@ -65,6 +64,25 @@ BrowserID.Modules.PickEmail = (function() {
     return identities;
   }
 
+  function proxyEventToInput(event) {
+    // iOS will not select a radio/checkbox button if the user clicks on the
+    // corresponding label.  Because of this, if the user clicks on the label,
+    // an even is manually fired on the the radio button.  This only applies
+    // if the user clicks on the actual label, not on any input elements
+    // contained within the label. This restriction is necessary or else we
+    // would be in a never ending loop that would continually toggle the state
+    // of any check boxes.
+    if(dom.is(event.target, "label")) {
+      // Must prevent standard acting browsers from taking care of the click or
+      // else it acts like two consecutive clicks.  For radio buttons this will
+      // just toggle state.
+      event.preventDefault();
+
+      var target = dom.getAttr(event.target, "for");
+      dom.fireEvent("#" + target, event.type);
+    }
+  }
+
   var Module = bid.Modules.PageModule.extend({
     start: function(options) {
       var origin = user.getOrigin(),
@@ -79,10 +97,12 @@ BrowserID.Modules.PickEmail = (function() {
         identities: getSortedIdentities(),
         siteemail: storage.site.get(origin, "email"),
         allow_persistent: options.allow_persistent || false,
-        remember: storage.site.get(origin, "remember") || false
+        remember: storage.site.get(origin, "remember") || false,
+        privacy_url: options.privacyURL,
+        tos_url: options.tosURL
       });
       dom.getElements("body").css("opacity", "1");
-
+      $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px');
       if (dom.getElements("#selectEmail input[type=radio]:visible").length === 0) {
         // If there is only one email address, the radio button is never shown,
         // instead focus the sign in button so that the user can click enter.
@@ -90,7 +110,11 @@ BrowserID.Modules.PickEmail = (function() {
         dom.focus("#signInButton");
       }
 
-      self.bind("#useNewEmail", "click", cancelEvent(addEmail));
+      self.click("#useNewEmail", addEmail);
+      // The click function does not pass the event to the function.  The event
+      // is needed for the label handler so that the correct radio button is
+      // selected.
+      self.bind("#selectEmail label", "click", proxyEventToInput);
 
       sc.start.call(self, options);
 
diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js
index 205c1df7650415dfcbfe2e91e1cad0a6a8aa4dd6..788a1f2dcd4e0cd8f9822fd926f606d32d2d9708 100644
--- a/resources/static/dialog/controllers/required_email.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -14,7 +14,6 @@ BrowserID.Modules.RequiredEmail = (function() {
       dialogHelpers = helpers.Dialog,
       dom = bid.DOM,
       assertion,
-      cancelEvent = dialogHelpers.cancelEvent,
       email,
       auth_level,
       primaryInfo,
@@ -117,13 +116,16 @@ BrowserID.Modules.RequiredEmail = (function() {
         // a user could not be looking at stale data and/or authenticate as
         // somebody else.
         var emailInfo = user.getStoredEmailKeypair(email);
+        //alert(auth_level + ' ' + JSON.stringify(emailInfo) + JSON.stringify(options));
         if(emailInfo && emailInfo.type === "secondary") {
           // secondary user, show the password field if they are not
           // authenticated to the "password" level.
           showTemplate({
             signin: true,
             password: auth_level !== "password",
-            secondary_auth: secondaryAuth
+            secondary_auth: secondaryAuth,
+            privacy_url: options.privacyURL,
+            tos_url: options.tosURL
           });
           ready();
         }
@@ -160,7 +162,9 @@ BrowserID.Modules.RequiredEmail = (function() {
               // user is authenticated, but does not control address
               // OR
               // address is unknown, make the user verify.
-              showTemplate({ verify: true });
+              showTemplate({ verify: true,
+                             privacy_url: options.privacyURL,
+                             tos_url: options.tosURL  });
             }
             else {
               // We've made it all this way.  It is a user who is not logged in
@@ -179,14 +183,19 @@ BrowserID.Modules.RequiredEmail = (function() {
           signin: false,
           password: false,
           secondary_auth: false,
-          primary: false
+          primary: false,
+          privacy_url: undefined,
+          tos_url: undefined
         }, options);
+
         self.renderDialog("required_email", options);
 
-        self.bind("#sign_in", "click", cancelEvent(signIn));
-        self.bind("#verify_address", "click", cancelEvent(verifyAddress));
-        self.bind("#forgotPassword", "click", cancelEvent(forgotPassword));
-        self.bind("#cancel", "click", cancelEvent(cancel));
+        self.click("#sign_in", signIn);
+        self.click("#verify_address", verifyAddress);
+        self.click("#forgotPassword", forgotPassword);
+        self.click("#cancel", cancel);
+
+        $('p.tospp').css('width', (240 - $('#signIn button:visible').outerWidth()) + 'px');
       }
 
       RequiredEmail.sc.start.call(self, options);
diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js
index 5721e82c03224f117b5e0c687e6cdd6203f38403..3be01935aae8f5984e94d4da7a275c37b7ee53f2 100644
--- a/resources/static/dialog/controllers/verify_primary_user.js
+++ b/resources/static/dialog/controllers/verify_primary_user.js
@@ -13,8 +13,7 @@ BrowserID.Modules.VerifyPrimaryUser = (function() {
       email,
       auth_url,
       helpers = bid.Helpers,
-      complete = helpers.complete,
-      cancelEvent = helpers.Dialog.cancelEvent;
+      complete = helpers.complete;
 
   function verify(callback) {
     this.publish("primary_user_authenticating");
@@ -51,7 +50,7 @@ BrowserID.Modules.VerifyPrimaryUser = (function() {
       data.requiredEmail = data.requiredEmail || false;
       self.renderDialog("verify_primary_user", data);
 
-      self.bind("#cancel", "click", cancelEvent(cancel));
+      self.click("#cancel", cancel);
 
       sc.start.call(self, data);
     },
diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css
index fafc2636fb6a81a21d12c0d6b9fb6beb7cee8f9d..909148297701d7c36b41dc8f9572f310e95f84fe 100644
--- a/resources/static/dialog/css/m.css
+++ b/resources/static/dialog/css/m.css
@@ -51,13 +51,11 @@
   }
 
   #signIn {
-      max-width: none;
       padding: 10px;
   }
 
-  #signIn .table {
+  #signIn .container {
       width: 100%;
-      margin: 0;
   }
 
   #signIn form {
@@ -101,13 +99,18 @@
   }
 
   #signIn .vertical {
-    padding-bottom: 0;
+    padding: 10px;
   }
 
   #signIn .vertical ul li {
     margin-top: 20px;
   }
 
+  #selectEmail > .inputs > li > label {
+    margin: 0;
+    padding: 15px 1px;
+  }
+
   #signIn .submit {
     position: static;
     line-height: 40px;
@@ -150,20 +153,13 @@
       height: 250px;
   }
 
-  #error .vertical,
-  #error.unsupported .vertical {
+  #error .vertical {
     width: auto;
   }
 
-  #error .vertical > div,
-  #error.unsupported .vertical > div {
+  #error .vertical > div {
     display: block;
     height: auto;
     padding: 10px;
   }
 
-  #error #borderbox {
-    border-left: none;
-    padding: 0;
-  }
-
diff --git a/resources/static/dialog/css/popup.css b/resources/static/dialog/css/popup.css
index 600016f1f56b390339468600baea1e6c90e377b9..db0e7d8d72efd340242483bae44f9d59369448fb 100644
--- a/resources/static/dialog/css/popup.css
+++ b/resources/static/dialog/css/popup.css
@@ -18,22 +18,26 @@ h2 {
 }
 
 
+.vertical {
+    height: 250px;
+}
+
 .table {
     display: table;
     width: 100%;
 }
 
-.vertical {
-    height: 250px;
+.table .vertical {
     display: table-cell;
     vertical-align: middle;
-    width: 100%;
 }
 
 #content {
     position: relative;
     height: 250px;
     overflow: hidden;
+    /* Fix for IE6 not displaying the unsupported dialog correctly */
+    _width: 100%;
 }
 
 section {
@@ -83,14 +87,16 @@ section > .contents {
   background-image: url("/i/bg.png");
 }
 
+
 .waiting #wait {
     z-index: 1;
     -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
     opacity: 1;
 }
 
-.error #error {
+.error #error, #error.unsupported, #error.cookies_disabled {
     z-index: 3;
+    -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
     opacity: 1;
 }
 
@@ -102,33 +108,36 @@ section > .contents {
 
 
 
-#error.unsupported .vertical {
-    width: 630px;
-    margin: 0 auto;
-    display: block;
+#error.unsupported {
+  padding: 20px 0;
 }
 
-
-#error.unsupported .vertical > div {
-    display: table-cell;
-    vertical-align: middle;
-    padding: 0 10px;
-    height: 250px;
+.unsupported {
+  /*
+   * These are fixes for IE6 - IE6 does not support the combination #id.class
+   * selector, so we have to use just the class, and then prepend the css
+   * attributes with _ so only IE6 renders them.
+   */
+  _padding: 20px 0;
+  _width: 100%;
+  _height: 100%;
 }
 
-#error #borderbox {
-    border-left: 1px solid #777;
-    padding: 20px 0;
+#error.unsupported h2 {
+  margin: 0 0 20px;
 }
 
-#error #borderbox img {
+#error img {
     border: none;
 }
 
-#error #alternative .lighter {
+#error .lighter {
     color: #777;
 }
 
+#wait .vertical, #error .vertical, #delay .vertical {
+    padding: 0 20px;
+}
 
 #formWrap {
     background-color: #fff;
@@ -143,9 +152,12 @@ section > .contents {
     top: 0;
 }
 
-#signIn .table {
+#signIn .container {
+    /**
+     * Set the width of the container for when the arrow animation happens
+     * otherwise the buttons slide right with the arrow
+     */
     width: 325px;
-    margin-right: 40px;
 }
 
 .arrow {
@@ -193,12 +205,12 @@ div#required_email {
 }
 
 #signIn .vertical {
-    padding: 0 20px;
+    padding: 20px 52px 20px 20px;
+    position: relative;
 }
 
 #signIn .vertical ul {
   list-style-type: none;
-  position: relative;
 }
 
 #signIn .vertical ul li {
@@ -212,9 +224,13 @@ div#required_email {
 #signIn .submit {
     line-height: 28px;
     position: absolute;
-    bottom: 0;
+    bottom: 20px;
     left: 0;
-    right: 0;
+    right: 52px;
+}
+
+#signIn .submit {
+  margin-left: 20px;
 }
 
 #signIn .submit > p {
@@ -255,6 +271,17 @@ label.selectable {
 
 .inputs > li > label {
     color: #333;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+#signIn #selectEmail > .inputs > li {
+    margin: 0;
+}
+
+#selectEmail > .inputs > li > label {
+    padding: 5px 1px;
+    white-space: nowrap;
 }
 
 .inputs > li > label.preselected {
@@ -272,6 +299,21 @@ label.selectable {
     text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
 }
 
+#signIn .submit > p.tospp {
+    /* width comes from controller/<page>.js p.tospp.css('width') update */
+    bottom: 0px;
+    color: #333;
+    font-size: 11px;
+    line-height: 1.2;
+    position: absolute; 
+    text-align: justify;
+    left; 0px;
+}
+
+.tospp a {
+    color: #549FDC;
+}
+
 footer .learn a {
     color: #549FDC;
 }
@@ -319,20 +361,19 @@ footer {
 
 .inputs {
     margin: 1em 0 .5em;
-    padding: 0 1em;
     line-height: 18px;
     max-height: 130px;
     overflow-y: auto;
 }
 
-.pickemail .inputs {
-    position: relative;
+/* Some languages have long text for the "sign in" and "use a different email"
+ * buttons.  If the user >= 6 emails in these languages, the buttons overlap.
+ * This shrinks the email address box by one address to prevent this overlap.
+ */
+#selectEmail .inputs {
+    max-height: 115px;
 }
 
-.form_section {
-    height: 176px;
-    position: relative;
-}
 
 .add {
     font-size: 80%;
@@ -343,19 +384,14 @@ footer {
 }
 
 label[for=remember] {
-  display: inline;
-  margin-left: 13px;
+  display: inline-block;
+  margin-bottom: 10px;
 }
 
 #thisIsNotMe {
-  margin-right: 10px;
   float: right;
 }
 
-#useNewEmail {
-  margin-left: 0.8em;
-}
-
 a.emphasize {
   background-color: #F0EFED;
   color: #4E4E4E;
@@ -366,7 +402,7 @@ a.emphasize {
 }
 
 .submit > button {
-    margin: 0 5px 0 0;
+    margin: 0 0 0 5px;
 }
 
 #newEmail {
@@ -386,4 +422,3 @@ a.emphasize {
 #checkemail {
     text-align: center;
 }
-
diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js
index 22a3098e78db35eba06155414b2251cf0a8f3f13..2b6a0fceb26b214bfb96f1109ca9151ad43dce48 100644
--- a/resources/static/dialog/resources/state.js
+++ b/resources/static/dialog/resources/state.js
@@ -32,15 +32,13 @@ BrowserID.State = (function() {
         },
         cancelState = self.popState.bind(self);
 
-    subscribe("offline", function(msg, info) {
-      startState("doOffline");
-    });
-
     subscribe("start", function(msg, info) {
       info = info || {};
 
       self.hostname = info.hostname;
       self.allowPersistent = !!info.allowPersistent;
+      self.privacyURL = info.privacyURL;
+      self.tosURL = info.tosURL;
       requiredEmail = info.requiredEmail;
 
       if ((typeof(requiredEmail) !== "undefined") && (!bid.verifyEmail(requiredEmail))) {
@@ -72,7 +70,9 @@ BrowserID.State = (function() {
 
       if (requiredEmail) {
         startState("doAuthenticateWithRequiredEmail", {
-          email: requiredEmail
+          email: requiredEmail,
+          privacyURL: self.privacyURL,
+          tosURL: self.tosURL
         });
       }
       else if (authenticated) {
@@ -83,6 +83,9 @@ BrowserID.State = (function() {
     });
 
     subscribe("authenticate", function(msg, info) {
+      info = info || {};
+      info.privacyURL = self.privacyURL;
+      info.tosURL = self.tosURL;
       startState("doAuthenticate", info);
     });
 
@@ -131,7 +134,7 @@ BrowserID.State = (function() {
         else if(info.add) {
           // Add the pick_email in case the user cancels the add_email screen.
           // The user needs something to go "back" to.
-          publish("pick_email", info);
+          publish("pick_email");
           publish("add_email", info);
         }
         else {
@@ -157,11 +160,15 @@ BrowserID.State = (function() {
     subscribe("pick_email", function() {
       startState("doPickEmail", {
         origin: self.hostname,
-        allow_persistent: self.allowPersistent
+        allow_persistent: self.allowPersistent,
+        privacyURL: self.privacyURL,
+        tosURL: self.tosURL
       });
     });
 
     subscribe("email_chosen", function(msg, info) {
+      info = info || {};
+
       var email = info.email,
           idInfo = storage.getEmail(email);
 
@@ -190,7 +197,9 @@ BrowserID.State = (function() {
               // screen.
               startState("doAuthenticateWithRequiredEmail", {
                 email: email,
-                secondary_auth: true
+                secondary_auth: true,
+                privacyURL: self.privacyURL,
+                tosURL: self.tosURL
               });
             }
             else {
@@ -214,7 +223,7 @@ BrowserID.State = (function() {
     });
 
     subscribe("authenticated", function(msg, info) {
-      publish("pick_email");
+      publish("email_chosen", info);
     });
 
     subscribe("forgot_password", function(msg, info) {
@@ -234,7 +243,7 @@ BrowserID.State = (function() {
         startState("doAssertionGenerated", info.assertion);
       }
       else {
-        startState("doPickEmail");
+        publish("pick_email");
       }
     });
 
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
index 7e08dedb9834d0eb6ae564abd7870b173404efe7..c23560dba67c1e4da420b898ed806de99596cc9b 100644
--- a/resources/static/dialog/views/authenticate.ejs
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -32,7 +32,7 @@
 
           <li id="create_text_section" class="newuser">
               <p><strong><%= gettext('Welcome to BrowserID!') %></strong></p>
-              <p><%= gettext('This email looks new, so let&#39;s get you set up.') %></p>
+              <p><%= gettext("This email looks new, so let's get you set up.") %></p>
           </li>
 
           <li class="returning">
@@ -55,9 +55,19 @@
       </ul>
 
       <div class="submit cf">
+        <% if (privacy_url && tos_url) { %>
+          <p class="tospp">
+            <%= format(
+                  gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'),
+                       [ gettext('next'), 
+                         format(' href="%s" target="_new"', [tos_url]), 
+                         format(' href="%s" target="_new"', [privacy_url])
+                       ]) %>
+          </p>
+      <% } %>
           <button class="start" tabindex="3"><%= gettext('next') %></button>
           <button class="newuser" tabindex="3"><%= gettext('verify email') %></button>
 
-          <button class="returning" tabindex="3"><%= gettext('select email') %></button>
+          <button class="returning" tabindex="3"><%= gettext('sign in') %></button>
       </div>
   </div>
diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs
index b21fa0afbe96c3ac1ee37e6ab2caaefeca04c8bd..4a61ab7092777177336739789d7c521f6950e660 100644
--- a/resources/static/dialog/views/error.ejs
+++ b/resources/static/dialog/views/error.ejs
@@ -7,24 +7,33 @@
     <h2 id="error_503">
       <%= gettext("We are very sorry, the server is under extreme load!") %>
     </h2>
+  <% } else if (typeof network !== "undefined" && network.status == 403) { %>
+    <h2 id="error_403">
+      <%= gettext("BrowserID requires cookies") %>
+    </h2>
+    <%= format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/en-US/kb/Websites%20say%20cookies%20are%20blocked'"]) %>
   <% } else { %>
     <h2 id="defaultError">
       <%= gettext("We are very sorry, there has been an error!") %>
     </h2>
   <% } %>
 
-  <p>
-  <% if (typeof dialog !== "undefined" && dialog !== false) { %>
-    <%= gettext("To retry, you will have to reload the page and try again.") %>
-  <% } else { %>
-    <%= gettext("To retry, you will have to close this window and try again.") %>
+  <% if (!(typeof network !== "undefined" && network.status == 403)) { %>
+    <p>
+      <% if (typeof dialog !== "undefined" && dialog !== false) { %>
+        <%= gettext("Please reload the page and try again.") %>
+      <% } else { %>
+        <%= gettext("Please close this window and try again.") %>
+      <% } %>
+    </p>
   <% } %>
-  </p>
 
   <% if(typeof action !== "undefined" || typeof network !== "undefined") { %>
-    <a href="#" id="openMoreInfo">
-      <%= gettext("See more info") %>
-    </a>
+    <p>
+      <a href="#" id="openMoreInfo">
+        <%= gettext("See more info") %>
+      </a>
+    </p>
 
     <ul id="moreInfo">
       <% if (typeof action !== "undefined") { %>
@@ -45,7 +54,7 @@
           <strong id="network">Network Info:</strong> <%= network.type %>: <%= network.url %>
 
           <p>
-            <strong>Response Code - </strong> <%= network.textStatus %>
+            <strong>Response Code - </strong> <%= network.status %>
           </p>
 
           <% if (network.responseText) { %>
diff --git a/resources/static/dialog/views/forgot_password.ejs b/resources/static/dialog/views/forgot_password.ejs
index 68b0d5749adbcdcc0c616e1d4bba5bd848fcb9fb..7c7a21f0450def891bd846a36fabde65e2ca174d 100644
--- a/resources/static/dialog/views/forgot_password.ejs
+++ b/resources/static/dialog/views/forgot_password.ejs
@@ -24,7 +24,7 @@
       </ul>
 
       <div class="submit cf">
-          <button tabindex="1"><%= gettext('Reset Password') %></button>
-          <a href="#" id="cancel" tabindex="2"><%= gettext('Cancel') %></a>
+          <button tabindex="1"><%= gettext('reset password') %></button>
+          <a href="#" id="cancel" tabindex="2"><%= gettext('cancel') %></a>
       </div>
   </div>
diff --git a/resources/static/dialog/views/offline.ejs b/resources/static/dialog/views/offline.ejs
deleted file mode 100644
index 942e98958271b64dd78554a3fe6d2295dde58362..0000000000000000000000000000000000000000
--- a/resources/static/dialog/views/offline.ejs
+++ /dev/null
@@ -1,12 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-
-  <h2 id="offline"><%= gettext('You are offline!') %></h2>
-
-  <p>
-    <%= gettext('We are sorry, but we cannot communicate with BrowserID while you are offline.') %>
-  </p>
-
-
diff --git a/resources/static/dialog/views/pick_email.ejs b/resources/static/dialog/views/pick_email.ejs
index 95a10c155209a67457c058121d7914bdaadb669c..bd14335643b521591ac68e93b966f797338fab01 100644
--- a/resources/static/dialog/views/pick_email.ejs
+++ b/resources/static/dialog/views/pick_email.ejs
@@ -9,11 +9,10 @@
       <ul class="inputs">
           <% _.each(identities, function(item) { var emailAddress = item.address; var cleanedEmail = emailAddress.replace("@","_").replace(".", "_"); %>
               <li>
-
-                  <label for="<%= cleanedEmail %>" class="serif<% if (emailAddress === siteemail) { %> preselected<% } %> selectable">
+                  <label for="<%= cleanedEmail %>" class="serif<% if (emailAddress === siteemail) { %> preselected<% } %> selectable" title="<%= emailAddress %>">
                     <input type="radio" name="email" id="<%= cleanedEmail %>" value="<%= emailAddress %>"
-                      <% if (emailAddress === siteemail) { %> checked="checked" <% } %>
-                    />
+                        <% if (emailAddress === siteemail) { %> checked="checked" <% } %>
+                      />
                     <%= emailAddress %>
                   </label>
               </li>
@@ -22,18 +21,31 @@
       <a id="useNewEmail" class="emphasize" href="#"><%= gettext('Use a different email') %></a>
 
       <div class="submit add cf">
+      <% if (allow_persistent || (privacy_url && tos_url)) { %>
+        <p class="tospp">
+      <% } %>
 
-          <% if (allow_persistent) { %>
+<% if (allow_persistent) { %>
             <label for="remember" class="selectable">
               <input type="checkbox" id="remember" name="remember" <% if (remember) { %> checked="checked" <% } %> />
               <%= gettext('Always sign in using this email') %>
             </label>
           <% } %>
+      <% if (privacy_url && tos_url) { %>
+<%= format(
+          gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'),
+                   [ gettext('sign in'), 
+                     format(' href="%s" target="_new"', [tos_url]), 
+                     format(' href="%s" target="_new"', [privacy_url])
+                   ]) %>
+      <% } %>
+
+      <% if (allow_persistent || (privacy_url && tos_url)) { %>
+        </p>
+      <% } %>          
 
           <button id="signInButton"><%= gettext('sign in') %></button>
-
-          <p>
-          </p>
+        <br style="clear: both" />
       </div>
   </div>
 
diff --git a/resources/static/dialog/views/required_email.ejs b/resources/static/dialog/views/required_email.ejs
index 2f8cf5472ac565e6c724d6695ad9235a32bef697..f80c47e95f42644797e682b0022a9c069cc5c52f 100644
--- a/resources/static/dialog/views/required_email.ejs
+++ b/resources/static/dialog/views/required_email.ejs
@@ -52,6 +52,16 @@
       </ul>
 
       <div class="submit cf">
+        <% if (privacy_url && tos_url) { %>
+          <p class="tospp">
+            <%= format(
+                  gettext('By clicking %s, you confirm that you accept this site\'s <a %s>Terms of Use</a> and <a %s>Privacy Policy</a>.'),
+                       [ gettext('sign in'), 
+                         format(' href="%s" target="_new"', [tos_url]),
+                         format(' href="%s" target="_new"', [privacy_url])
+                       ]) %>
+          </p>
+        <% } %>
           <% if (signin) { %>
             <button id="sign_in" tabindex="3"><%= gettext("sign in") %></button>
           <% } else if (verify) { %>
diff --git a/resources/static/dialog/views/verify_primary_user.ejs b/resources/static/dialog/views/verify_primary_user.ejs
index ea43a338118f5409606a1296c3e32299830d74dc..8fde3e6cd650e30ca85419dd64f2734173ac9421 100644
--- a/resources/static/dialog/views/verify_primary_user.ejs
+++ b/resources/static/dialog/views/verify_primary_user.ejs
@@ -24,7 +24,7 @@
       </ul>
 
       <div class="submit cf">
-          <button id="VerifyWithPrimary"><%= gettext("Verify") %></button>
+          <button id="verifyWithPrimary"><%= gettext("verify") %></button>
       </div>
   </div>
 
@@ -38,8 +38,8 @@
     </p>
 
     <div class="submit cf">
-      <button id="verifyWithPrimary"><%= gettext("Verify") %></button>
-      <a href="#" id="cancel"><%= gettext("Cancel") %></a>
+      <button id="verifyWithPrimary"><%= gettext("verify") %></button>
+      <a href="#" id="cancel"><%= gettext("cancel") %></a>
     </div>
 
   </div>
diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js
index e835b8bd2047671cac878f414e966d300b462b18..89fef47ee479e6352bff774465fe3fb99b370beb 100644
--- a/resources/static/include_js/include.js
+++ b/resources/static/include_js/include.js
@@ -354,7 +354,7 @@
             }
           }, timeout);
         }
-        
+
         var onMessage = function(origin, method, m) {
           // if an observer was specified at allocation time, invoke it
           if (typeof cfg.gotMessageObserver === 'function') {
@@ -656,7 +656,10 @@
     // checking Mobile Firefox (Fennec)
     function isFennec() {
       try {
-        return (navigator.userAgent.indexOf('Fennec/') != -1);
+        // We must check for both XUL and Java versions of Fennec.  Both have
+        // distinct UA strings.
+        return (userAgent.indexOf('Fennec/') != -1) ||  // XUL
+                 (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1);   // Java
       } catch(e) {};
       return false;
     }
@@ -831,7 +834,7 @@
           ieNosupport = ieVersion > -1 && ieVersion < 8;
 
       if(ieNosupport) {
-        return "IE_VERSION";
+        return "BAD_IE_VERSION";
       }
     }
 
@@ -840,30 +843,55 @@
     }
 
     function checkLocalStorage() {
-      var localStorage = 'localStorage' in win && win['localStorage'] !== null;
-      if(!localStorage) {
-        return "LOCALSTORAGE";
+      // Firefox/Fennec/Chrome blow up when trying to access or
+      // write to localStorage. We must do two explicit checks, first
+      // whether the browser has localStorage.  Second, we must check
+      // whether the localStorage can be written to.  Firefox (at v11)
+      // throws an exception when querying win['localStorage']
+      // when cookies are disabled. Chrome (v17) excepts when trying to
+      // write to localStorage when cookies are disabled. If an
+      // exception is thrown, then localStorage is disabled. If no
+      // exception is thrown, hasLocalStorage will be true if the
+      // browser supports localStorage and it can be written to.
+      try {
+        var hasLocalStorage = 'localStorage' in win
+                        // Firefox will except here if cookies are disabled.
+                        && win['localStorage'] !== null;
+
+        if(hasLocalStorage) {
+          // browser has localStorage, check if it can be written to. If
+          // cookies are disabled, some browsers (Chrome) will except here.
+          win['localStorage'].setItem("test", "true");
+          win['localStorage'].removeItem("test");
+        }
+        else {
+          // Browser does not have local storage.
+          return "LOCALSTORAGE_NOT_SUPPORTED";
+        }
+      } catch(e) {
+          return "LOCALSTORAGE_DISABLED";
       }
     }
 
     function checkPostMessage() {
       if(!win.postMessage) {
-        return "POSTMESSAGE";
+        return "POSTMESSAGE_NOT_SUPPORTED";
       }
     }
 
     function checkJSON() {
       if(!(window.JSON && window.JSON.stringify && window.JSON.parse)) {
-        return "JSON";
+        return "JSON_NOT_SUPPORTED";
       }
     }
 
     function isSupported() {
-      reason = checkLocalStorage() || checkPostMessage() || checkJSON() || explicitNosupport();
+      reason = explicitNosupport() || checkLocalStorage() || checkPostMessage() || checkJSON();
 
       return !reason;
     }
 
+
     function getNoSupportReason() {
       return reason;
     }
@@ -911,10 +939,15 @@
 
   if (!navigator.id.getVerifiedEmail || navigator.id._getVerifiedEmailIsShimmed) {
     var ipServer = "https://browserid.org";
-    var isFennec = navigator.userAgent.indexOf('Fennec/') != -1;
+    var userAgent = navigator.userAgent;
+    // We must check for both XUL and Java versions of Fennec.  Both have
+    // distinct UA strings.
+    var isFennec = (userAgent.indexOf('Fennec/') != -1) ||  // XUL
+                     (userAgent.indexOf('Firefox/') != -1 && userAgent.indexOf('Android') != -1);   // Java
+
     var windowOpenOpts =
       (isFennec ? undefined :
-       "menubar=0,location=1,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375");
+       "menubar=0,location=1,resizable=1,scrollbars=1,status=0,dialog=1,width=700,height=375");
 
     var w;
 
@@ -942,8 +975,15 @@
         }
 
         if (!BrowserSupport.isSupported()) {
+          var reason = BrowserSupport.getNoSupportReason(),
+              url = "unsupported_dialog";
+
+          if(reason === "LOCALSTORAGE_DISABLED") {
+            url = "cookies_disabled";
+          }
+
           w = window.open(
-            ipServer + "/unsupported_dialog",
+            ipServer + "/" + url,
             null,
             windowOpenOpts);
           return;
diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js
index 889d165bf42ce1080d6326e6559431ba148a0d18..860c033277fa2579044a22216426b7ac88bbd8eb 100644
--- a/resources/static/lib/dom-jquery.js
+++ b/resources/static/lib/dom-jquery.js
@@ -296,6 +296,20 @@ BrowserID.DOM = ( function() {
          */
         focus: function( elementToFocus ) {
           jQuery( elementToFocus ).focus();
+        },
+
+        /**
+         * Check the current matched set of elements against
+         * a selector or element and return true if at least
+         * one of these elements matches the given arguments.
+         * @method is
+         * @param {selector || element} elementToCheck
+         * @param {string} type
+         * @returns {boolean} true if elementToCheck matches the specified
+         * type, false otw.
+         */
+        is: function( elementToCheck, type ) {
+          return jQuery( elementToCheck ).is( type );
         }
 
 
diff --git a/resources/static/lib/ejs.js b/resources/static/lib/ejs.js
index 49d95252400213f7c1f2105344541485ebcb50aa..31a9df53fb1b958f4059a2d33c1fa300d61e4cab 100644
--- a/resources/static/lib/ejs.js
+++ b/resources/static/lib/ejs.js
@@ -1,5 +1,5 @@
 (function(){
-    
+
 
 var rsplit = function(string, regex) {
 	var result = regex.exec(string),retArr = new Array(), first_idx, last_idx, first_bit;
@@ -11,10 +11,10 @@ var rsplit = function(string, regex) {
 			first_bit = string.substring(0,first_idx);
 			retArr.push(string.substring(0,first_idx));
 			string = string.slice(first_idx);
-		}		
+		}
 		retArr.push(result[0]);
 		string = string.slice(result[0].length);
-		result = regex.exec(string);	
+		result = regex.exec(string);
 	}
 	if (! string == '')
 	{
@@ -32,7 +32,7 @@ extend = function(d, s){
 }
 
 
-EJS = function( options ){
+window.EJS = function( options ){
 	options = typeof options == "string" ? {view: options} : options
     this.set_options(options);
 	if(options.precompiled){
@@ -76,7 +76,7 @@ EJS = function( options ){
 
 	template.compile(options, this.name);
 
-	
+
 	EJS.update(this.name, this);
 	this.template = template;
 };
@@ -145,7 +145,7 @@ EJS.endExt = function(path, match){
 
 /* @Static*/
 EJS.Scanner = function(source, left, right) {
-	
+
     extend(this,
         {left_delimiter: 	left +'%',
          right_delimiter: 	'%'+right,
@@ -155,7 +155,7 @@ EJS.Scanner = function(source, left, right) {
          left_comment: 	left+'%#'})
 
 	this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ;
-	
+
 	this.source = source;
 	this.stag = null;
 	this.lines = 0;
@@ -166,15 +166,15 @@ EJS.Scanner.to_text = function(input){
         return '';
     if(input instanceof Date)
 		return input.toDateString();
-	if(input.toString) 
+	if(input.toString)
         return input.toString();
 	return '';
 };
 
 EJS.Scanner.prototype = {
   scan: function(block) {
-     scanline = this.scanline;
-	 regex = this.SplitRegexp;
+   var scanline = this.scanline,
+       regex = this.SplitRegexp;
 	 if (! this.source == '')
 	 {
 	 	 var source_split = rsplit(this.source, /\n/);
@@ -212,7 +212,7 @@ EJS.Buffer = function(pre_cmd, post_cmd) {
 	}
 };
 EJS.Buffer.prototype = {
-	
+
   push: function(cmd) {
 	this.line.push(cmd);
   },
@@ -230,17 +230,17 @@ EJS.Buffer.prototype = {
 			this.push(pre_cmd[i]);
 		}
 		this.script = this.script + this.line.join('; ');
-		line = null;
+		this.line = null;
 	}
   }
- 	
+
 };
 
 
 EJS.Compiler = function(source, left) {
     this.pre_cmd = ['var ___ViewO = [];'];
 	this.post_cmd = new Array();
-	this.source = ' ';	
+	this.source = ' ';
 	if (source != null)
 	{
 		if (typeof source == 'string')
@@ -250,7 +250,7 @@ EJS.Compiler = function(source, left) {
 			this.source = source;
 		}else if (source.innerHTML){
 			this.source = source.innerHTML;
-		} 
+		}
 		if (typeof this.source != 'string'){
 			this.source = "";
 		}
@@ -276,7 +276,7 @@ EJS.Compiler.prototype = {
 	this.out = '';
 	var put_cmd = "___ViewO.push(";
 	var insert_cmd = put_cmd;
-	var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);		
+	var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);
 	var content = '';
 	var clean = function(content)
 	{
@@ -352,7 +352,7 @@ EJS.Compiler.prototype = {
 	buff.close();
 	this.out = buff.script + ";";
 	var to_be_evaled = '/*'+name+'*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {'+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";
-	
+
 	try{
 		eval(to_be_evaled);
 	}catch(e){
@@ -397,13 +397,13 @@ EJS.Compiler.prototype = {
 					</td>
 				</tr>
 	</tbody></table>
- * 
+ *
  */
 EJS.config = function(options){
 	EJS.cache = options.cache != null ? options.cache : EJS.cache;
 	EJS.type = options.type != null ? options.type : EJS.type;
 	EJS.ext = options.ext != null ? options.ext : EJS.ext;
-	
+
 	var templates_directory = EJS.templates_directory || {}; //nice and private container
 	EJS.templates_directory = templates_directory;
 	EJS.get = function(path, cache){
@@ -411,12 +411,12 @@ EJS.config = function(options){
 		if(templates_directory[path]) return templates_directory[path];
   		return null;
 	};
-	
-	EJS.update = function(path, template) { 
+
+	EJS.update = function(path, template) {
 		if(path == null) return;
 		templates_directory[path] = template ;
 	};
-	
+
 	EJS.INVALID_PATH =  -1;
 };
 EJS.config( {cache: true, type: '<', ext: '.ejs' } );
@@ -425,7 +425,7 @@ EJS.config( {cache: true, type: '<', ext: '.ejs' } );
 
 /**
  * @constructor
- * By adding functions to EJS.Helpers.prototype, those functions will be available in the 
+ * By adding functions to EJS.Helpers.prototype, those functions will be available in the
  * views.
  * @init Creates a view helper.  This function is called internally.  You should never call it.
  * @param {Object} data The data passed to the view.  Helpers have access to it through this._data
@@ -452,7 +452,7 @@ EJS.Helpers.prototype = {
      * For a given value, tries to create a human representation.
      * @param {Object} input the value being converted.
      * @param {Object} null_text what text should be present if input == null or undefined, defaults to ''
-     * @return {String} 
+     * @return {String}
      */
 	to_text: function(input, null_text) {
 	    if(input == null || input === undefined) return null_text || '';
@@ -471,21 +471,21 @@ EJS.Helpers.prototype = {
 	        catch(e) { continue;}
 	   }
 	}
-	
+
 	EJS.request = function(path){
 	   var request = new EJS.newRequest()
 	   request.open("GET", path, false);
-	   
+
 	   try{request.send(null);}
 	   catch(e){return null;}
-	   
+
 	   if ( request.status == 404 || request.status == 2 ||(request.status == 0 && request.responseText == '') ) return null;
-	   
+
 	   return request.responseText
 	}
 	EJS.ajax_request = function(params){
 		params.method = ( params.method ? params.method : 'GET')
-		
+
 		var request = new EJS.newRequest();
 		request.onreadystatechange = function(){
 			if(request.readyState == 4){
@@ -502,4 +502,4 @@ EJS.Helpers.prototype = {
 	}
 
 
-})();
\ No newline at end of file
+})();
diff --git a/resources/static/lib/urlparse.js b/resources/static/lib/urlparse.js
new file mode 100644
index 0000000000000000000000000000000000000000..a4fe85475202dd07f525798048c45241f2d04ca0
--- /dev/null
+++ b/resources/static/lib/urlparse.js
@@ -0,0 +1,191 @@
+/**
+ * urlparse.js
+ *
+ * Includes parseUri (c) Steven Levithan <steven@levithan.com> Under the MIT License
+ *
+ * Features:
+ *  + parse a url into components
+ *  + url validiation
+ *  + semantically lossless normalization
+ *  + url prefix matching
+ *
+ * window.URLParse(string) -
+ *   parse a url using the 'parseUri' algorithm, returning an object containing various
+ *   uri components. returns an object with the following properties (all optional):
+ *
+ *   PROPERTIES:
+ *     anchor - stuff after the #
+ *     authority - everything after the :// and before the path.  Including user auth, host, and port
+ *     directory - path with trailing filename and everything after removed
+ *     file - path without directory
+ *     host - host
+ *     password - password part when user:pass@ is prepended to host
+ *     path - full path, sans query or anchor
+ *     port - port, when present in url
+ *     query - ?XXX
+ *     relative -
+ *     scheme - url scheme (http, file, https, etc.)
+ *     source - full string passed to URLParse()
+ *     user - user part when user:pass@ is prepended to host
+ *     userInfo -
+ *
+ *   FUNCTIONS:
+ *     (string) toString() - generate a string representation of the url
+ *
+ *     (this) validate() - validate the url, possbly throwing a string exception
+ *        if determined to not be a valid URL.  Returns this, thus may be chained.
+ *
+ *     (this) normalize() - perform in-place modification of the url to place it in a normal
+ *        (and verbose) form. Returns this, thus may be chained.
+ *
+ *     (bool) contains(str) - returns whether the object upon which contains() is called is a
+ *        "url prefix" for the passed in string, after normalization.
+ *
+ *     (this) originOnly() - removes everything that would occur after port, including
+ *        path, query, and anchor.
+ *
+ */
+
+(function() {
+    /* const */ var INV_URL = "invalid url: ";
+    var parseURL = function(s) {
+        var toString = function() {
+            var str = this.scheme + "://";
+            if (this.user) str += this.user;
+            if (this.password) str += ":" + this.password;
+            if (this.user || this.password) str += "@";
+            if (this.host) str += this.host;
+            if (this.port) str += ":" + this.port;
+            if (this.path) str += this.path;
+            if (this.query) str += "?" + this.query;
+            if (this.anchor) str += "#" + this.anchor;
+            return str;
+        };
+
+        var originOnly = function() {
+            this.path = this.query = this.anchor = undefined;
+            return this;
+        };
+
+        var validate = function() {
+            if (!this.scheme) throw INV_URL +"missing scheme";
+            if (this.scheme !== 'http' && this.scheme !== 'https')
+                throw INV_URL + "unsupported scheme: " + this.scheme;
+            if (!this.host) throw INV_URL + "missing host";
+            if (this.port) {
+                var p = parseInt(this.port);
+                if (!this.port.match(/^\d+$/)) throw INV_URL + "non-numeric numbers in port";
+                if (p <= 0 || p >= 65536) throw INV_URL + "port out of range (" +this.port+")";
+            }
+            if (this.path && this.path.indexOf('/') != 0) throw INV_URL + "path must start with '/'";
+
+            return this;
+        };
+
+        var normalize = function() {
+            // lowercase scheme
+            if (this.scheme) this.scheme = this.scheme.toLowerCase();
+
+            // for directory references, append trailing slash
+            if (!this.path) this.path = "/";
+
+            // remove port numbers same as default
+            if (this.port === "80" && 'http' === this.scheme) delete this.port;
+            if (this.port === "443" && 'https' === this.scheme) delete this.port;
+
+            // remove dot segments from path, algorithm
+            // http://tools.ietf.org/html/rfc3986#section-5.2.4
+            this.path = (function (p) {
+                var out = [];
+                while (p) {
+                    if (p.indexOf('../') === 0) p = p.substr(3);
+                    else if (p.indexOf('./') === 0) p = p.substr(2);
+                    else if (p.indexOf('/./') === 0) p = p.substr(2);
+                    else if (p === '/.') p = '/';
+                    else if (p.indexOf('/../') === 0 || p === '/..') {
+                        if (out.length > 0) out.pop();
+                        p = '/' + p.substr(4);
+                    } else if (p === '.' || p === '..') p = '';
+                    else {
+                        var m = p.match(/^\/?([^\/]*)/);
+                        // remove path match from input
+                        p = p.substr(m[0].length);
+                        // add path to output
+                        out.push(m[1]);
+                    }
+                }
+                return '/' + out.join('/');
+            })(this.path);
+
+            // XXX: upcase chars in % escaping?
+
+            // now we need to update all members
+            var n = parseURL(this.toString()),
+            i = 14,
+            o = parseUri.options;
+
+            while (i--) {
+                var k = o.key[i];
+                if (n[k] && typeof(n[k]) === 'string') this[k] = n[k];
+                else if (this[k] && typeof(this[k]) === 'string') delete this[k];
+            }
+
+            return this;
+        };
+
+        var contains = function(str) {
+            try {
+                this.validate();
+                var prefix = parseURL(this.toString()).normalize().toString();
+                var url = parseURL(str).validate().normalize().toString();
+                return (url.indexOf(prefix) === 0);
+            } catch(e) {
+                console.log(e);
+                // if any exceptions are raised, then the comparison fails
+                return false;
+            }
+        };
+
+        // parseUri 1.2.2
+        // (c) Steven Levithan <stevenlevithan.com>
+        // MIT License
+        var parseUri = function(str) {
+            var o   = parseUri.options,
+            m   = o.parser.exec(str),
+            uri = {},
+            i   = 14;
+
+            while (i--) if (m[i]) uri[o.key[i]] = m[i];
+
+            if (uri[o.key[12]]) {
+                uri[o.q.name] = {};
+                uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+                    if ($1) uri[o.q.name][$1] = $2;
+                });
+            }
+            // member functions
+            uri.toString = toString;
+            uri.validate = validate;
+            uri.normalize = normalize;
+            uri.contains = contains;
+            uri.originOnly = originOnly;
+            return uri;
+        };
+
+        parseUri.options = {
+            key: ["source","scheme","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
+            q:   {
+                name:   "queryKey",
+                parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+            },
+            parser: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/
+        };
+        // end parseUri
+
+        // parse URI using the parseUri code and return the resultant object
+        return parseUri(s);
+    };
+
+  if (typeof exports === 'undefined') window.URLParse = parseURL;
+  else module.exports = parseURL;
+})();
diff --git a/resources/static/pages/page_helpers.js b/resources/static/pages/page_helpers.js
index b15cf2eb8002c985150dd4f92af6e76628756511..375584079fd592cc22019a2d72ca6ec8602f5dac 100644
--- a/resources/static/pages/page_helpers.js
+++ b/resources/static/pages/page_helpers.js
@@ -63,6 +63,7 @@ BrowserID.PageHelpers = (function() {
   function showFailure(error, info, callback) {
     info = $.extend(info || {}, { action: error, dialog: false });
     bid.Screens.error.show("error", info);
+    errorDisplay.start();
     callback && callback(false);
   }
 
diff --git a/resources/static/pages/signin.js b/resources/static/pages/signin.js
index 27633981de20533c67c13fd6bf90c66aecd8a159..813cce5cdbcebade2f83524a24d140e8e7284193 100644
--- a/resources/static/pages/signin.js
+++ b/resources/static/pages/signin.js
@@ -13,7 +13,6 @@ BrowserID.signIn = (function() {
       helpers = bid.Helpers,
       errors = bid.Errors,
       pageHelpers = bid.PageHelpers,
-      cancelEvent = pageHelpers.cancelEvent,
       doc = document,
       winchan = window.WinChan,
       verifyEmail,
@@ -144,7 +143,7 @@ BrowserID.signIn = (function() {
 
       pageHelpers.setupEmail();
 
-      self.bind("#authWithPrimary", "click", cancelEvent(authWithPrimary));
+      self.click("#authWithPrimary", authWithPrimary);
       self.bind("#email", "change", onEmailChange);
       self.bind("#email", "keyup", onEmailChange);
 
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index bb0b665454977f9e0d490aa99520f5828d6c7cab..3616066baf88c9d62d56639f12b4078ed88ce2c7 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -46,8 +46,8 @@ BrowserID.Errors = (function(){
     },
 
     cookiesDisabled: {
-      title: gettext("We are sorry, BrowserID requires cookies"),
-      message: gettext("BrowserID requires your browser's cookies to be enabled to operate. Please enable your browser's cookies and try again")
+      title: gettext("BrowserID requires cookies"),
+      message: format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/en-US/kb/Websites%20say%20cookies%20are%20blocked'"])
     },
 
     cookiesEnabled: {
@@ -78,11 +78,6 @@ BrowserID.Errors = (function(){
       title: "Logout Failed"
     },
 
-    offline: {
-      title: gettext("You are offline!"),
-      message: gettext("Unfortunately, BrowserID cannot communicate while offline!")
-    },
-
     primaryAuthentication: {
       title: "Authenticating with Identity Provider",
       message: "We had trouble communicating with your email provider, please try again!"
diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js
index f968a016994e6569f498a67df93dde1c83643aaf..b47ecf8ac7a2a496634f1b6c4ed87e437fb3e3d0 100644
--- a/resources/static/shared/modules/page_module.js
+++ b/resources/static/shared/modules/page_module.js
@@ -52,7 +52,7 @@ BrowserID.Modules.PageModule = (function() {
     start: function(options) {
       var self=this;
       self.bind("form", "submit", cancelEvent(onSubmit));
-      self.bind("#thisIsNotMe", "click", cancelEvent(self.close.bind(self, "notme")));
+      self.click("#thisIsNotMe", self.close.bind(self, "notme"));
     },
 
     stop: function() {
@@ -86,6 +86,17 @@ BrowserID.Modules.PageModule = (function() {
       });
     },
 
+    /**
+     * Shortcut to bind a click handler
+     * @method click
+     * @param {string}
+     * @param {function} callback
+     * @param {object} [context] - optional context, if not given, use this.
+     */
+    click: function(target, callback, context) {
+      this.bind(target, "click", cancelEvent(callback), context);
+    },
+
     unbindAll: function() {
       var self=this,
           evt;
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index a16b38df9b737888eb61b435ac5e0df12f4a7931..be58ccc312ca02b373365023ce7121e82b64da58 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -13,7 +13,6 @@ BrowserID.Network = (function() {
       domain_key_creation_time,
       auth_status,
       code_version,
-      cookies_enabled,
       time_until_delay,
       mediator = bid.Mediator,
       xhr = bid.XHR,
@@ -29,7 +28,6 @@ BrowserID.Network = (function() {
     domain_key_creation_time = result.domain_key_creation_time;
     auth_status = result.auth_level;
     code_version = result.code_version;
-    cookies_enabled = result.cookies_enabled || true;
 
     // seed the PRNG
     // FIXME: properly abstract this out, probably by exposing a jwcrypto
@@ -186,8 +184,8 @@ BrowserID.Network = (function() {
           complete(onComplete, status.success);
         },
         error: function(info) {
-          // 403 is throttling.
-          if (info.network.status === 403) {
+          // 429 is throttling.
+          if (info.network.status === 429) {
             complete(onComplete, false);
           }
           else complete(onFailure, info);
@@ -392,8 +390,8 @@ BrowserID.Network = (function() {
           complete(onComplete, response.success);
         },
         error: function(info) {
-          // 403 is throttling.
-          if (info.network.status === 403) {
+          // 429 is throttling.
+          if (info.network.status === 429) {
             complete(onComplete, false);
           }
           else complete(onFailure, info);
@@ -570,8 +568,19 @@ BrowserID.Network = (function() {
      * @method cookiesEnabled
      */
     cookiesEnabled: function(onComplete, onFailure) {
+      // Make sure we get context first or else we will needlessly send
+      // a cookie to the server.
       withContext(function() {
-        complete(onComplete, cookies_enabled);
+        try {
+          // set a test cookie with a duration of 1 second.
+          // NOTE - The Android 3.3 default browser will still pass this.
+          // http://stackoverflow.com/questions/8509387/android-browser-not-respecting-cookies-disabled/9264996#9264996
+          document.cookie = "test=true; max-age=1";
+          var enabled = document.cookie.indexOf("test") > -1;
+          complete(onComplete, enabled);
+        } catch(e) {
+          complete(onComplete, false);
+        }
       }, onFailure);
     }
   };
diff --git a/resources/static/test/cases/controllers/actions.js b/resources/static/test/cases/controllers/actions.js
index 20593ecac0c0a8920a89d67386f861beaa5cafa0..c670633e8a71193cb9443778085c5702d2f4aac9 100644
--- a/resources/static/test/cases/controllers/actions.js
+++ b/resources/static/test/cases/controllers/actions.js
@@ -54,17 +54,6 @@
     });
   });
 
-  asyncTest("doOffline - print offline error screen", function() {
-    createController({
-      ready: function() {
-        controller.doOffline();
-        ok($("#error .contents").text().length, "contents have been written");
-        ok($("#error #offline").text().length, "offline error message has been written");
-        start();
-      }
-    });
-  });
-
   asyncTest("doProvisionPrimaryUser - start the provision_primary_user service", function() {
     createController({
       ready: function() {
diff --git a/resources/static/test/cases/controllers/authenticate.js.bak b/resources/static/test/cases/controllers/authenticate.js.bak
deleted file mode 100644
index a77ba9c3d829b753c1bcce1feff8aa6e5801f5e5..0000000000000000000000000000000000000000
--- a/resources/static/test/cases/controllers/authenticate.js.bak
+++ /dev/null
@@ -1,257 +0,0 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-(function() {
-  "use strict";
-
-  var controller,
-      el = $("body"),
-      bid = BrowserID,
-      storage = bid.Storage,
-      network = bid.Network,
-      xhr = bid.Mocks.xhr,
-      emailRegistered = false,
-      userCreated = true,
-      mediator = bid.Mediator,
-      registrations = [],
-      testHelpers = bid.TestHelpers,
-      register = testHelpers.register,
-      provisioning = bid.Mocks.Provisioning;
-
-  function reset() {
-    emailRegistered = false;
-    userCreated = true;
-  }
-
-  function createController(options) {
-    options = options || {};
-    controller = bid.Modules.Authenticate.create();
-    controller.start(options);
-  }
-
-  module("controllers/authenticate", {
-    setup: function() {
-      reset();
-      testHelpers.setup();
-    },
-
-    teardown: function() {
-      if (controller) {
-        try {
-          controller.destroy();
-        } catch(e) {
-          // may already be destroyed from close inside of the controller.
-        }
-      }
-      reset();
-      testHelpers.teardown();
-    }
-  });
-
-  asyncTest("providing primary email address - only show email address", function() {
-    $("#email").val("");
-    createController({
-      email: "registered@testuser.com",
-      type: "primary",
-      ready: function() {
-        equal($("#email").val(), "registered@testuser.com", "email prefilled");
-        equal(false, "need a test");
-        start();
-      }
-    });
-  });
-
-  asyncTest("providing known secondary - show password", function() {
-    $("#email").val("");
-    createController({
-      email: "registered@testuser.com",
-      type: "secondary",
-      known: true,
-      ready: function() {
-        equal($("#email").val(), "registered@testuser.com", "email prefilled");
-        equal(false, "need a test");
-        start();
-      }
-    });
-  });
-
-  asyncTest("providing unknown secondary address - show email address, nothing more", function() {
-    $("#email").val("");
-    createController({
-      email: "unregistered@testuser.com",
-      type: "secondary",
-      known: false,
-      ready: function() {
-        equal($("#email").val(), "unregistered@testuser.com", "email prefilled");
-        equal(false, "need a test");
-        start();
-      }
-    });
-  });
-
-  function testUserUnregistered() {
-    register("create_user", function() {
-      ok(true, "email was valid, user not registered");
-      start();
-    });
-
-    controller.checkEmail();
-  }
-
-  function testUserUnregistered() {
-    var createUserCalled = false;
-    register("create_user", function() {
-      createUserCalled = true;
-    });
-
-    controller.checkEmail(function() {
-      equal(createUserCalled, true, "create_user was triggered");
-      start();
-    });
-  }
-
-  asyncTest("checkEmail with unknown secondary email, expect 'create_user' message", function() {
-    createController();
-    $("#email").val("unregistered@testuser.com");
-    xhr.useResult("unknown_secondary");
-
-    testUserUnregistered();
-  });
-
-  asyncTest("checkEmail with email with leading/trailing whitespace, user not registered, expect 'create_user' message", function() {
-    createController();
-    $("#email").val("    unregistered@testuser.com   ");
-    xhr.useResult("unknown_secondary");
-
-    testUserUnregistered();
-  });
-
-  asyncTest("checkEmail with normal email, user registered, expect 'enter_password' message", function() {
-    createController();
-    $("#email").val("registered@testuser.com");
-    xhr.useResult("known_secondary");
-
-    register("enter_password", function() {
-      ok(true, "email was valid, user registered");
-      start();
-    });
-
-    controller.checkEmail();
-  });
-
-  asyncTest("checkEmail with email that has IdP support, expect 'primary_user' message", function() {
-    createController();
-    $("#email").val("unregistered@testuser.com");
-    xhr.useResult("primary");
-
-    register("primary_user", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "email correctly passed");
-      equal(info.auth, "https://auth_url", "IdP authentication URL passed");
-      equal(info.prov, "https://prov_url", "IdP provisioning URL passed");
-      start();
-    });
-
-    controller.checkEmail();
-  });
-
-  function testAuthenticated() {
-    register("authenticated", function() {
-      ok(true, "user authenticated as expected");
-      start();
-    });
-    controller.authenticate();
-  }
-
-  asyncTest("normal authentication is kosher", function() {
-    createController();
-    $("#email").val("registered@testuser.com");
-    $("#password").val("password");
-
-    testAuthenticated();
-  });
-
-  asyncTest("leading/trailing whitespace on the username is stripped for authentication", function() {
-    createController();
-    $("#email").val("    registered@testuser.com    ");
-    $("#password").val("password");
-
-    testAuthenticated();
-  });
-
-  asyncTest("forgotPassword triggers forgot_password message", function() {
-    createController();
-    $("#email").val("registered@testuser.com");
-
-    register("forgot_password", function(msg, info) {
-      equal(info.email, "registered@testuser.com", "forgot_password with correct email triggered");
-      start();
-    });
-
-    controller.forgotPassword();
-  });
-
-  asyncTest("createUser with valid email", function() {
-    createController();
-    $("#email").val("unregistered@testuser.com");
-    xhr.useResult("unknown_secondary");
-
-    register("user_staged", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered");
-      start();
-    });
-
-    controller.createUser();
-  });
-
-  asyncTest("createUser with invalid email", function() {
-    createController();
-    $("#email").val("unregistered");
-
-    var handlerCalled = false;
-    register("user_staged", function(msg, info) {
-      handlerCalled = true;
-    });
-
-    controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with invalid email");
-      start();
-    });
-  });
-
-  asyncTest("createUser with valid email but throttling", function() {
-    createController();
-    $("#email").val("unregistered@testuser.com");
-
-    var handlerCalled = false;
-    register("user_staged", function(msg, info) {
-      handlerCalled = true;
-    });
-
-    xhr.useResult("throttle");
-    controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with throttling");
-      equal(bid.Tooltip.shown, true, "tooltip is shown");
-      start();
-    });
-  });
-
-  asyncTest("createUser with valid email, XHR error", function() {
-    createController();
-    $("#email").val("unregistered@testuser.com");
-
-    var handlerCalled = false;
-    register("user_staged", function(msg, info) {
-      handlerCalled = true;
-    });
-
-    xhr.useResult("ajaxError");
-    controller.createUser(function() {
-      equal(handlerCalled, false, "bad jiji, user_staged should not have been called with XHR error");
-      start();
-    });
-  });
-
-}());
-
diff --git a/resources/static/test/cases/controllers/pick_email.js b/resources/static/test/cases/controllers/pick_email.js
index b11e116dcc8b22bbba20bfc37e3eb3d3ffaa8b1c..8a1309a3762046461dfcca4eae65132bcd713947 100644
--- a/resources/static/test/cases/controllers/pick_email.js
+++ b/resources/static/test/cases/controllers/pick_email.js
@@ -64,7 +64,7 @@
     var radioButton = $("input[type=radio]").eq(0);
     ok(radioButton.is(":checked"), "the email address we specified is checked");
 
-    var label = radioButton.parent();
+    var label = $("label[for=" + radioButton.attr("id") + "]");
     ok(label.hasClass("preselected"), "the label has the preselected class");
   });
 
@@ -155,5 +155,47 @@
     controller.addEmail();
   });
 
+  test("click on an email label and radio button - select corresponding radio button", function() {
+    storage.addEmail("testuser@testuser.com", {});
+    storage.addEmail("testuser2@testuser.com", {});
+
+    createController(false);
+
+    equal($("#testuser_testuser_com").is(":checked"), false, "radio button is not selected before click.");
+
+    // selects testuser@testuser.com
+    $("label[for=testuser_testuser_com]").trigger("click");
+    equal($("#testuser_testuser_com").is(":checked"), true, "radio button is correctly selected");
+
+    // selects testuser2@testuser.com
+    $("#testuser2_testuser_com").trigger("click");
+    equal($("#testuser2_testuser_com").is(":checked"), true, "radio button is correctly selected");
+  });
+
+  test("click on the 'Always sign in...' label and checkbox - correct toggling", function() {
+    createController(true);
+
+    var label = $("label[for=remember]"),
+        checkbox = $("#remember").removeAttr("checked");
+
+    equal(checkbox.is(":checked"), false, "checkbox is not yet checked");
+
+    // toggle checkbox to on clicking on label
+    label.trigger("click");
+    equal(checkbox.is(":checked"), true, "checkbox is correctly checked");
+
+    // toggle checkbox to off clicking on label
+    label.trigger("click");
+    equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked");
+
+    // toggle checkbox to on clicking on checkbox
+    checkbox.trigger("click");
+    equal(checkbox.is(":checked"), true, "checkbox is correctly checked");
+
+    // toggle checkbox to off clicking on checkbox
+    checkbox.trigger("click");
+    equal(checkbox.is(":checked"), false, "checkbox is correctly unchecked");
+  });
+
 }());
 
diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/add_email_address_test.js
index 8cbcce9abde9424fe67fea11d404cfae30fe3c27..79fe35d5e22cb38c4888550cd30c91643f728d3d 100644
--- a/resources/static/test/cases/pages/add_email_address_test.js
+++ b/resources/static/test/cases/pages/add_email_address_test.js
@@ -25,7 +25,6 @@
     },
     teardown: function() {
       testHelpers.teardown();
-      $("#page_head").empty();
     }
   });
 
@@ -134,10 +133,7 @@
   });
 
   asyncTest("password: too long of a password", function() {
-    var tooLong = "";
-    for(var i = 0; i < 81; i++) {
-      tooLong += (i % 10);
-    }
+    var tooLong = testHelpers.generateString(81);
     $("#password").val(tooLong);
     $("#vpassword").val(tooLong);
 
diff --git a/resources/static/test/cases/pages/manage_account.js b/resources/static/test/cases/pages/manage_account.js
index f92596004f3fead7c1b5ad9be2d269a3dc9c97b0..5e1ddb2cca1febf5e0cf97fb6230366a44b176ab 100644
--- a/resources/static/test/cases/pages/manage_account.js
+++ b/resources/static/test/cases/pages/manage_account.js
@@ -201,14 +201,10 @@
   asyncTest("changePassword with too long of a password - tooltip", function() {
     bid.manageAccount(mocks, function() {
       $("#old_password").val("oldpassword");
-      var tooLong = "";
-      for(var i = 0; i < 81; i++) {
-        tooLong += (i % 10);
-      }
-      $("#new_password").val(tooLong);
+      $("#new_password").val(testHelpers.generateString(81));
 
       bid.manageAccount.changePassword(function(status) {
-        equal(status, false, "on too short of a password, status is false");
+        equal(status, false, "on too long of a password, status is false");
         testHelpers.testTooltipVisible();
         start();
       });
diff --git a/resources/static/test/cases/pages/page_helpers.js b/resources/static/test/cases/pages/page_helpers.js
index 77293232e367abaa16bd8f355f2f97a2453a4b90..5df6b052f6f8d79a3539b7d453da436bdc361824 100644
--- a/resources/static/test/cases/pages/page_helpers.js
+++ b/resources/static/test/cases/pages/page_helpers.js
@@ -147,10 +147,22 @@
     });
   });
 
-  asyncTest("showFailure shows a failure screen", function() {
-    pageHelpers.showFailure({}, errors.offline, function() {
+  asyncTest("showFailure - show a failure screen, extended info can be opened", function() {
+    pageHelpers.showFailure("error", { network: 400, status: "error"}, function() {
       testHelpers.testErrorVisible();
-      start();
+
+      // We have to make sure the error screen itself is visible and that the
+      // extra info is hidden so when we click on the extra info it opens.
+      $("#error").show();
+      $("#moreInfo").hide();
+      $("#openMoreInfo").trigger("click");
+
+      // Add a bit of delay to wait for the animation
+      setTimeout(function() {
+        equal($("#moreInfo").is(":visible"), true, "extra info is visible after click");
+        start();
+      }, 100);
+
     });
   });
 
diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js
index a4c3ef06d7407dd377bcb79b1310da0c59424357..c11ae2574bd2b46e071d12037a981eed2ca1907c 100644
--- a/resources/static/test/cases/pages/verify_email_address_test.js
+++ b/resources/static/test/cases/pages/verify_email_address_test.js
@@ -11,6 +11,7 @@
       storage = bid.Storage,
       xhr = bid.Mocks.xhr,
       testHelpers = bid.TestHelpers,
+      testTooltipVisible = testHelpers.testTooltipVisible,
       validToken = true;
 
   module("pages/verify_email_address", {
@@ -82,6 +83,35 @@
 
       bid.verifyEmailAddress.submit(function() {
         equal($("#congrats").is(":visible"), false, "congrats is not visible, missing password");
+        testTooltipVisible();
+        start();
+      });
+    });
+  });
+
+  asyncTest("submit with good token, too short of a password", function() {
+    bid.verifyEmailAddress("token", function() {
+      var pass = testHelpers.generateString(6);
+      $("#password").val(pass);
+      $("#vpassword").val(pass);
+
+      bid.verifyEmailAddress.submit(function() {
+        equal($("#congrats").is(":visible"), false, "congrats is not visible, too short of a password");
+        testTooltipVisible();
+        start();
+      });
+    });
+  });
+
+  asyncTest("submit with good token, too long of a password", function() {
+    bid.verifyEmailAddress("token", function() {
+      var pass = testHelpers.generateString(81);
+      $("#password").val(pass);
+      $("#vpassword").val(pass);
+
+      bid.verifyEmailAddress.submit(function() {
+        equal($("#congrats").is(":visible"), false, "congrats is not visible, too long of a password");
+        testTooltipVisible();
         start();
       });
     });
@@ -96,6 +126,7 @@
 
     bid.verifyEmailAddress.submit(function() {
       equal($("#congrats").is(":visible"), false, "congrats is not visible, missing verification password");
+      testTooltipVisible();
       start();
     });
 
@@ -109,6 +140,7 @@
 
     bid.verifyEmailAddress.submit(function() {
       equal($("#congrats").is(":visible"), false, "congrats is not visible, different passwords");
+      testTooltipVisible();
       start();
     });
 
diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js
index a9e95a48b27d6f9bcdddb780e85731a8b2c1059c..c951c5ec98f1b11a55af5b52b98c58a17522a534 100644
--- a/resources/static/test/cases/resources/state.js
+++ b/resources/static/test/cases/resources/state.js
@@ -67,12 +67,6 @@
     equal(error, "start: controller must be specified", "creating a state machine without a controller fails");
   });
 
-  test("offline does offline", function() {
-    mediator.publish("offline");
-
-    equal(actions.called.doOffline, true, "controller is offline");
-  });
-
   test("user_staged - call doConfirmUser", function() {
     mediator.publish("user_staged", {
       email: "testuser@testuser.com"
@@ -170,10 +164,11 @@
     ok(actions.called.doEmailChosen, "doEmailChosen called");
   });
 
-  test("authenticated", function() {
-    mediator.publish("authenticated");
+  test("authenticated - call doEmailChosen", function() {
+    storage.addEmail("testuser@testuser.com", {});
+    mediator.publish("authenticated", { email: "testuser@testuser.com" });
 
-    ok(actions.called.doPickEmail, "doPickEmail has been called");
+    ok(actions.called.doEmailChosen, "doEmailChosen has been called");
   });
 
   test("forgot_password", function() {
@@ -359,4 +354,12 @@
     equal(error, "invalid email", "expected exception thrown");
   });
 
+  test("null assertion generated - preserve original options in doPickEmail", function() {
+    mediator.publish("start", { allowPersistent: true });
+    mediator.publish("assertion_generated", { assertion: null });
+
+    equal(actions.called.doPickEmail, true, "doPickEmail callled");
+    equal(actions.info.doPickEmail.allow_persistent, true, "allow_persistent preserved");
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/helpers.js b/resources/static/test/cases/shared/helpers.js
index 755aeabc77d5eb4737080caa8bc620fd270665b5..8384bc151cd41b57ef6c1433fa8afcb6df541018 100644
--- a/resources/static/test/cases/shared/helpers.js
+++ b/resources/static/test/cases/shared/helpers.js
@@ -7,15 +7,17 @@
   "use strict";
 
   var bid = BrowserID,
-      helpers = bid.Helpers;
+      helpers = bid.Helpers,
+      testHelpers = bid.TestHelpers;
 
   module("shared/helpers", {
     setup: function() {
+      testHelpers.setup();
       bid.Renderer.render("#page_head", "site/add_email_address", {});
     },
 
     teardown: function() {
-      $("#page_head").empty();
+      testHelpers.teardown();
     }
   });
 
diff --git a/resources/static/test/cases/shared/modules/cookie_check.js b/resources/static/test/cases/shared/modules/cookie_check.js
index 86e49d7ca00cd65ead97619e4c02fb60b8a42671..b6358ba50fe1571fec66ee894aa40d8cacd4a6d3 100644
--- a/resources/static/test/cases/shared/modules/cookie_check.js
+++ b/resources/static/test/cases/shared/modules/cookie_check.js
@@ -50,19 +50,5 @@
     });
   });
 
-  /*
-  // XXX - disabling this test until we have the full solution for how we are going
-  // to interact with the backend.
-  asyncTest("createController with cookies disabled - ready returns with false status, error shown", function() {
-    transport.setContextInfo("cookies_enabled", false);
-    createController({
-      ready: function(status) {
-        testHelpers.testErrorVisible();
-        equal(status, false, "cookies are disabled, false status");
-        start();
-      }
-    });
-  });
-  */
 }());
 
diff --git a/resources/static/test/cases/shared/modules/page_module.js b/resources/static/test/cases/shared/modules/page_module.js
index 7db90097c54912a67da1d48541f5960803317827..7baac2497bae62c255f945e57fc8a260e7201ee8 100644
--- a/resources/static/test/cases/shared/modules/page_module.js
+++ b/resources/static/test/cases/shared/modules/page_module.js
@@ -109,6 +109,18 @@
    $("body").trigger("click");
   });
 
+  asyncTest("click - bind a click handler, handler does not get event", function() {
+    createController();
+
+    controller.click("body", function(event) {
+      equal(typeof event, "undefined", "event is undefined");
+      strictEqual(this, controller, "context is correct");
+      start();
+    });
+
+    $("body").trigger("click");
+  });
+
   asyncTest("unbindAll removes all listeners", function() {
     createController();
     var listenerCalled = false;
diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js
index ba08d8f894ccc074c6421e2a42416aa9b79560c7..64e6b809ff5e587981d73fc1ceb7ed77c21d8057 100644
--- a/resources/static/test/cases/shared/network.js
+++ b/resources/static/test/cases/shared/network.js
@@ -541,18 +541,11 @@
     failureCheck(network.changePassword, "oldpassword", "newpassword");
   });
 
-  asyncTest("cookiesEnabled with cookies enabled", function() {
-    transport.setContextInfo("cookies_enabled", true);
-
+  asyncTest("cookiesEnabled with cookies enabled - return true status", function() {
     network.cookiesEnabled(function(status) {
       equal(status, true, "cookies are enabled, correct status");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("cookiesEnabled with XHR failure", function() {
-    transport.useResult("contextAjaxError");
-    failureCheck(network.cookiesEnabled);
-  });
-
 }());
diff --git a/resources/static/test/cases/shared/renderer.js b/resources/static/test/cases/shared/renderer.js
index 2ac4b75c701fa3cb1723cdecc3ab63753ce44326..ab6f605d9a8a00ce841d8bb9ea618bef7ff18b3c 100644
--- a/resources/static/test/cases/shared/renderer.js
+++ b/resources/static/test/cases/shared/renderer.js
@@ -7,40 +7,32 @@
   "use strict";
 
   var bid = BrowserID,
-      renderer = bid.Renderer;
+      renderer = bid.Renderer,
+      testHelpers = bid.TestHelpers;
 
   module("shared/renderer", {
     setup: function() {
-
+      testHelpers.setup();
     },
 
     teardown: function() {
-
+      testHelpers.teardown();
     }
   });
 
   test("render template loaded using XHR", function() {
-    $("#formWrap .contents").empty();
-    $("#templateInput").remove();
-
     renderer.render("#formWrap .contents", "test_template_with_input");
 
     ok($("#templateInput").length, "template written when loaded using XHR");
   });
 
   test("render template from memory", function() {
-    $("#formWrap .contents").empty();
-    $("#templateInput").remove();
-
     renderer.render("#formWrap .contents", "inMemoryTemplate");
 
     ok($("#templateInput").length, "template written when loaded from memory");
   });
 
   test("append template to element", function() {
-    $("#formWrap .contents").empty();
-    $("#templateInput").remove();
-
     renderer.append("#formWrap", "inMemoryTemplate");
 
     ok($("#formWrap > #templateInput").length && $("#formWrap > .contents"), "template appended to element instead of overwriting it");
diff --git a/resources/static/test/cases/shared/screens.js b/resources/static/test/cases/shared/screens.js
index f34c1ae9aace7241df52afd5ca818fe6556e55f7..98f75978889c33ddbae761a25fb628dfa71c67b9 100644
--- a/resources/static/test/cases/shared/screens.js
+++ b/resources/static/test/cases/shared/screens.js
@@ -8,23 +8,21 @@
 
   var bid = BrowserID,
       screens = bid.Screens,
+      testHelpers = bid.TestHelpers,
       el;
 
   module("shared/screens", {
     setup: function() {
-
+      testHelpers.setup();
     },
 
     teardown: function() {
-      if (el) {
-        el.empty();
-      }
+      testHelpers.teardown();
     }
   });
 
   test("form", function() {
     el = $("#formWrap .contents");
-    el.empty();
     screens.form.show("test_template_with_input");
 
     ok($("#templateInput").length, "the template has been written");
@@ -38,7 +36,6 @@
 
   test("wait", function() {
     var el = $("#wait .contents");
-    el.empty();
     screens.wait.show("test_template_with_input");
 
     ok($("#templateInput").length, "the template has been written");
@@ -52,7 +49,6 @@
 
   test("error", function() {
     var el = $("#error .contents");
-    el.empty();
     screens.error.show("test_template_with_input");
 
     ok($("#templateInput").length, "the template has been written");
@@ -66,7 +62,6 @@
 
   test("XHR 503 (server unavailable) error", function() {
     var el = $("#error .contents");
-    el.empty();
 
     screens.error.show("error", {
       network: {
@@ -76,4 +71,16 @@
 
     ok($("#error_503").length, "503 header is shown");
   });
+
+  test("XHR 403 (Forbidden) error - show the 403, cookies required error", function() {
+    var el = $("#error .contents");
+
+    screens.error.show("error", {
+      network: {
+        status: 403
+      }
+    });
+
+    ok($("#error_403").length, "403 header is shown");
+  });
 }());
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index e74cccc1a24659f5dd930d61bbb5cee7bb8c0db2..6b82fb2362359f5607173d8f1a2679a60c50160c 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -13,8 +13,7 @@ BrowserID.Mocks.xhr = (function() {
       authenticated: false,
       auth_level: undefined,
       code_version: "ABC123",
-      random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=",
-      cookies_enabled: true
+      random_seed: "H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo="
     };
 
   // this cert is meaningless, but it has the right format
@@ -52,7 +51,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/stage_user unknown_secondary": { success: true },
       "post /wsapi/stage_user valid": { success: true },
       "post /wsapi/stage_user invalid": { success: false },
-      "post /wsapi/stage_user throttle": 403,
+      "post /wsapi/stage_user throttle": 429,
       "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" },
@@ -79,7 +78,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/stage_email unknown_secondary": { success: true },
       "post /wsapi/stage_email known_secondary": { success: true },
       "post /wsapi/stage_email invalid": { success: false },
-      "post /wsapi/stage_email throttle": 403,
+      "post /wsapi/stage_email throttle": 429,
       "post /wsapi/stage_email ajaxError": undefined,
       "post /wsapi/cert_key ajaxError": undefined,
       "get /wsapi/email_addition_status?email=registered%40testuser.com pending": { status: "pending" },
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 507c885c6341c2534a1429bbe7808a00b3a21ec6..493ea1db0756779b69735a7f18c104b51c1d3e26 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -68,15 +68,13 @@ BrowserID.TestHelpers = (function() {
       $("body").stop().show();
       $("body")[0].className = "";
 
-      var el = $("#controller_head");
-      el.find("#formWrap .contents").html("");
-      el.find("#wait .contents").html("");
       $(".error").removeClass("error");
-      $("#error").stop().html("<div class='contents'></div>").hide();
+      $("#error").hide();
       $(".notification").stop().hide();
       $("form").show();
       screens.wait.hide();
       screens.error.hide();
+      screens.delay.hide();
       tooltip.reset();
       provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
       user.reset();
@@ -96,12 +94,9 @@ BrowserID.TestHelpers = (function() {
       });
       network.init();
       storage.clear();
-      $(".error").removeClass("error");
-      $("#error").stop().html("<div class='contents'></div>").hide();
-      $(".notification").stop().hide();
-      $("form").show();
       screens.wait.hide();
       screens.error.hide();
+      screens.delay.hide();
       tooltip.reset();
       provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
       user.reset();
@@ -185,6 +180,17 @@ BrowserID.TestHelpers = (function() {
       }
 
       cb && cb.apply(null, args);
+    },
+
+    /**
+     * Generate a long string
+     */
+    generateString: function(length) {
+      var str = "";
+      for(var i = 0; i < length; i++) {
+        str += (i % 10);
+      }
+      return str;
     }
   };
 
diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs
index 13b6a5af6ddb10247c8a323cf3dc16a22ee03da9..979cea68aa1953636faecadd5c2370d81a366722 100644
--- a/resources/views/add_email_address.ejs
+++ b/resources/views/add_email_address.ejs
@@ -10,7 +10,7 @@
         </ul>
 
         <form id="signUpForm" class="cf">
-            <p class="hint siteinfo"><%= gettext('Finish signing into: ') %><strong><span class="website"></span></strong></p>
+            <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p>
 
             <h1 class="serif"><%= gettext('Email Verification') %></h1>
 
@@ -46,7 +46,7 @@
             </ul>
 
             <div class="submit cf password_entry">
-                <button><%= gettext('Finish') %></button>
+                <button><%= gettext('finish') %></button>
             </div>
 
 
diff --git a/resources/views/cookies_disabled.ejs b/resources/views/cookies_disabled.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..01fb4baea956430a667abf0d4883a46a9ad6c01c
--- /dev/null
+++ b/resources/views/cookies_disabled.ejs
@@ -0,0 +1,17 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+  <section id="error" class="cookies_disabled">
+      <div class="table">
+        <div class="vertical contents">
+          <h2 id="reason">
+            <%= gettext("BrowserID requires cookies") %>
+          </h2>
+
+          <p>
+            <%- format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/en-US/kb/Websites%20say%20cookies%20are%20blocked'"]) %>
+          </p>
+        </div>
+      </div>
+  </section>
diff --git a/resources/views/dialog.ejs b/resources/views/dialog.ejs
index 7551a7b1c26192ddf3546c7178c662653b177bd0..775879d65d29259d3a78248d8e8128cdea63c151 100644
--- a/resources/views/dialog.ejs
+++ b/resources/views/dialog.ejs
@@ -11,9 +11,8 @@
 
         <div id="signIn">
             <div class="arrow"></div>
-            <div class="table">
-                <div class="vertical contents">
-                </div>
+            <div class="container">
+              <div class="vertical contents"></div>
             </div>
         </div>
       </form>
@@ -21,6 +20,7 @@
 
 
     <section id="wait">
+        <!-- because each section is an absolutely positioned element, we have to use the inner table container element to be able to vertically/horizontally center correctly.  Without the table element, the layout gets all messed up. -->
         <div class="table">
             <div class="vertical contents">
                 <h2><%= gettext('Communicating with server') %></h2>
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index edefc45f38b623d3fb1c086d43f80eb72ade47c9..24f728902afed069c42fcc80ae65b87912906a3b 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -12,13 +12,7 @@
   <!--[if lt IE 9]>
     <script src="/lib/html5shim.js"></script>
   <![endif]-->
-  <% if (production) { %>
-      <link href="/production/dialog.css" rel="stylesheet" type="text/css">
-  <% } else { %>
-      <link href="/css/common.css" rel="stylesheet" type="text/css">
-      <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css">
-      <link href="/dialog/css/m.css" rel="stylesheet" type="text/css">
-  <% } %>
+  <%- cachify_css('/production/dialog.css') %>
   <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css">
   <title><%= gettext('BrowserID') %></title>
 </head>
@@ -44,7 +38,7 @@
                 </ul-->
 
                 <div class="learn">
-<%- gettext('BrowserID is the fast and secure way to sign in &mdash; <a target="_blank" href="/about">learn more</a>') %>
+<%- format(gettext('BrowserID is the fast and secure way to sign in &mdash; <a %s>learn more</a>'), [" href='/about' target='_blank'"]) %>
                 </div>
 
           </footer>
@@ -52,67 +46,7 @@
       </div>
 
       <% if (useJavascript !== false) { %>
-        <% if (production) { %>
-          <script src="/production/<%= locale %>/dialog.js"></script>
-        <% } else { %>
-          <script src="/lib/jquery-1.7.1.min.js"></script>
-          <script src="/lib/winchan.js"></script>
-          <script src="/lib/underscore-min.js"></script>
-          <script src="/lib/vepbundle.js"></script>
-          <script src="/lib/ejs.js"></script>
-          <script src="/shared/javascript-extensions.js"></script>
-          <script src="/i18n/<%= locale %>/client.json"></script>
-          <script src="/shared/gettext.js"></script>
-          <script src="/shared/browserid.js"></script>
-          <script src="/lib/hub.js"></script>
-          <script src="/lib/dom-jquery.js"></script>
-          <script src="/lib/module.js"></script>
-          <script src="/lib/jschannel.js"></script>
-
-          <script src="/shared/templates.js"></script>
-          <script src="/shared/renderer.js"></script>
-          <script src="/shared/class.js"></script>
-          <script src="/shared/mediator.js"></script>
-          <script src="/shared/tooltip.js"></script>
-          <script src="/shared/validation.js"></script>
-          <script src="/shared/helpers.js"></script>
-          <script src="/shared/screens.js"></script>
-          <script src="/shared/browser-support.js"></script>
-          <script src="/shared/wait-messages.js"></script>
-          <script src="/shared/error-messages.js"></script>
-          <script src="/shared/error-display.js"></script>
-          <script src="/shared/storage.js"></script>
-          <script src="/shared/xhr.js"></script>
-          <script src="/shared/network.js"></script>
-          <script src="/shared/provisioning.js"></script>
-          <script src="/shared/user.js"></script>
-          <script src="/shared/command.js"></script>
-          <script src="/shared/history.js"></script>
-          <script src="/shared/state_machine.js"></script>
-
-          <script src="/shared/modules/page_module.js"></script>
-          <script src="/shared/modules/xhr_delay.js"></script>
-          <script src="/shared/modules/xhr_disable_form.js"></script>
-          <script src="/shared/modules/cookie_check.js"></script>
-
-          <script src="/dialog/resources/internal_api.js"></script>
-          <script src="/dialog/resources/helpers.js"></script>
-          <script src="/dialog/resources/state.js"></script>
-
-          <script src="/dialog/controllers/actions.js"></script>
-          <script src="/dialog/controllers/dialog.js"></script>
-          <script src="/dialog/controllers/authenticate.js"></script>
-          <script src="/dialog/controllers/forgot_password.js"></script>
-          <script src="/dialog/controllers/check_registration.js"></script>
-          <script src="/dialog/controllers/pick_email.js"></script>
-          <script src="/dialog/controllers/add_email.js"></script>
-          <script src="/dialog/controllers/required_email.js"></script>
-          <script src="/dialog/controllers/verify_primary_user.js"></script>
-          <script src="/dialog/controllers/provision_primary_user.js"></script>
-          <script src="/dialog/controllers/primary_user_provisioned.js"></script>
-          <script src="/dialog/controllers/email_chosen.js"></script>
-          <script src="/dialog/start.js"></script>
-        <% } %>
+        <%- cachify_js(util.format('/production/%s/dialog.js', locale)) %>
       <% } %>
 	</body>
 </html>
diff --git a/resources/views/index.ejs b/resources/views/index.ejs
index b07203f655059f705ee9ea99c8da847928294d35..57f1c8d7b96254292d735fb74bf6bab32639dd4a 100644
--- a/resources/views/index.ejs
+++ b/resources/views/index.ejs
@@ -52,6 +52,10 @@
   </div>
 
   <div id="vAlign" class="display_nonauth">
+      <div id="newsbanner">
+        BrowserID is graduating: we're launching <b>Mozilla Persona</b>. Find out more on <a href="http://identity.mozilla.com/">the identity blog</a>.
+      </div>
+
       <div id="signUp">
           <div id="card"><img src="/i/slit.png"></div>
           <div id="hint"></div>
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index 118292f3363b6f28469566b4867885fc17f204b2..c72eb8247356adf821333daaf9b6c26df15d3deb 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -11,62 +11,8 @@
     <script src="/lib/html5shim.js"></script>
   <![endif]-->
   <link href='https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic' rel='stylesheet' type='text/css'>
-  <% if (production) { %>
-    <link rel="stylesheet" type="text/css" href="/production/browserid.css">
-    <script src="/production/<%= locale %>/browserid.js"></script>
-  <% } else { %>
-    <link rel="stylesheet" href="/css/common.css" type="text/css" media="screen">
-    <link rel="stylesheet" href="/css/style.css" type="text/css" media="screen">
-    <link rel="stylesheet" href="/css/m.css" type="text/css" media="screen">
-
-    <script src="/lib/vepbundle.js"></script>
-    <script src="/lib/jquery-1.7.1.min.js"></script>
-    <script src="/lib/underscore-min.js"></script>
-    <script src="/lib/ejs.js"></script>
-    <script src="/shared/javascript-extensions.js"></script>
-    <script src="/i18n/<%= locale %>/client.json"></script>
-    <script src="/shared/gettext.js"></script>
-    <script src="/shared/browserid.js"></script>
-    <script src="/lib/dom-jquery.js"></script>
-    <script src="/lib/module.js"></script>
-    <script src="/lib/jschannel.js"></script>
-    <script src="/lib/winchan.js"></script>
-    <script src="/lib/hub.js"></script>
-
-    <script src="/shared/templates.js"></script>
-    <script src="/shared/renderer.js"></script>
-    <script src="/shared/class.js"></script>
-    <script src="/shared/mediator.js"></script>
-    <script src="/shared/tooltip.js"></script>
-    <script src="/shared/validation.js"></script>
-    <script src="/shared/helpers.js"></script>
-    <script src="/shared/screens.js"></script>
-    <script src="/shared/browser-support.js"></script>
-    <script src="/shared/wait-messages.js"></script>
-    <script src="/shared/error-messages.js"></script>
-    <script src="/shared/error-display.js"></script>
-    <script src="/shared/mediator.js"></script>
-    <script src="/shared/storage.js"></script>
-    <script src="/shared/xhr.js"></script>
-    <script src="/shared/network.js"></script>
-    <script src="/shared/provisioning.js"></script>
-    <script src="/shared/user.js"></script>
-
-    <script src="/shared/modules/page_module.js"></script>
-    <script src="/shared/modules/xhr_delay.js"></script>
-    <script src="/shared/modules/xhr_disable_form.js"></script>
-    <script src="/shared/modules/cookie_check.js"></script>
-
-    <script src="/pages/page_helpers.js"></script>
-    <script src="/pages/index.js"></script>
-    <script src="/pages/start.js"></script>
-    <script src="/pages/add_email_address.js"></script>
-    <script src="/pages/verify_email_address.js"></script>
-    <script src="/pages/forgot.js"></script>
-    <script src="/pages/manage_account.js"></script>
-    <script src="/pages/signin.js"></script>
-    <script src="/pages/signup.js"></script>
-  <% } %>
+  <%- cachify_css('/production/browserid.css') %>
+  <%- cachify_js(util.format('/production/%s/browserid.js', locale)) %>
   <title><%= format(gettext("BrowserID: %s"), [title]) %></title>
 </head>
 <body>
@@ -97,8 +43,8 @@
 
     <footer id="footer">
         <ul class="cf">
-            <li><%- format(gettext('By the <a href="%s" target="_blank">Identity Team</a> @ <a href="%s" target="_blank">Mozilla Labs</a>'),
-                           ['http://identity.mozilla.com', 'http://mozillalabs.com']) %></li>
+            <li><%- format(gettext('By the <a %s>Identity Team</a> @ <a %s>Mozilla Labs</a>'),
+                           [" href='http://identity.mozilla.com' target='_blank'", " href='http://mozillalabs.com' target='_blank'"]) %></li>
             <li>&mdash;</li>
             <li><a href="/privacy"><%= gettext('Privacy') %></a></li>
             <li><a href="/tos"><%= gettext('TOS') %></a></li>
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 073bc76d5ba52cbdb380a5c162154872b6a0ec83..036d577407b9b3fa9cb039104b09bfd1b30bdc67 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -21,7 +21,7 @@
 		<ol id="qunit-tests"></ol>
 		<div id="qunit-test-area"></div>
 
-    <div style="position: absolute; top: -1000px; left: 100px; right: 100px; height: 300px;">
+    <div id="qunit-fixture" style="position: absolute; top: -1000px; left: 100px; right: 100px; height: 300px;">
         <a href="#" onclick="$('#contents').hide(); return false;">Close</a>
         <h3>Test Contents, this will be updated and can be safely ignored</h3>
 
diff --git a/resources/views/unsupported_dialog.ejs b/resources/views/unsupported_dialog.ejs
index 2fc05e2206682df54ff3fc433364666be24091d2..fc39f468a3d4ce646b45065f0a7276fcfccfcd4a 100644
--- a/resources/views/unsupported_dialog.ejs
+++ b/resources/views/unsupported_dialog.ejs
@@ -3,30 +3,22 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
   <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>
+      <h2>
+        We are sorry, but currently your browser is not supported.
+      </h2>
 
-              <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>
+      <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>
+        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>
+      <p class="lighter">
+        and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>
+      </p>
 
-              </div>
 
-          </div>
-      </div>
   </section>
diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs
index 4df7506ed2be239db5aeeff28049931b1820ede6..e86a1f5b6a41afa004449cae41925c9636ca6104 100644
--- a/resources/views/verify_email_address.ejs
+++ b/resources/views/verify_email_address.ejs
@@ -6,12 +6,12 @@
     <div id="signUpFormWrap">
         <ul class="notifications">
             <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li>
-            <li class="notification error" id="cannotcommunicate"><%= gettext('Error comunicating with server.') %></li>
+            <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></li>
             <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li>
         </ul>
 
         <form id="signUpForm" class="cf">
-            <p class="hint siteinfo"><%= gettext('Finish signing into: ') %><strong><span class="website"></span></strong></p>
+            <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p>
             <h1 class="serif"><%= gettext('Last step!') %></h1>
 
             <ul class="inputs">
@@ -46,7 +46,7 @@
             </ul>
 
             <div class="submit cf">
-                <button><%= gettext('Finish') %></button>
+                <button><%= gettext('finish') %></button>
             </div>
 
         </form>
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index e2af1e255723775418a627d91f7aedf4c2db8b86..6ed39bbfd82a3d7c4c310e7b9d8de75e05742771 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.02.08
+Version:       0.2012.02.29
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Pete Fritchman <petef@mozilla.com>
@@ -12,7 +12,7 @@ Source0:       %{name}.tar.gz
 BuildRoot:     %{_tmppath}/%{name}-%{version}-%{release}-root
 AutoReqProv:   no
 Requires:      openssl nodejs
-BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel perl perl-JSON perl-Locale-PO
+BuildRequires: gcc-c++ git jre make npm openssl-devel expat-devel
 
 %description
 browserid server & web home for browserid.org
@@ -37,6 +37,8 @@ mkdir -p %{buildroot}%{_rootdir}
 for f in bin lib locale node_modules resources scripts *.json; do
     cp -rp $f %{buildroot}%{_rootdir}/
 done
+mkdir -p %{buildroot}%{_rootdir}/config
+cp -p config/l10n-all.json %{buildroot}%{_rootdir}/config
 
 %clean
 rm -rf %{buildroot}
diff --git a/scripts/check_po.sh b/scripts/check_po.sh
new file mode 100755
index 0000000000000000000000000000000000000000..19278dc05951e59ff5d5e28d0b44d6ce928f3baa
--- /dev/null
+++ b/scripts/check_po.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# syntax:
+# check-po.sh
+
+for lang in `find locale -type f -name "*.po"`; do
+    dir=`dirname $lang`
+    stem=`basename $lang .po`
+    printf "${lang}: "
+    msgfmt --statistics ${dir}/${stem}.po
+done
+rm messages.mo
diff --git a/scripts/compress-locales.sh b/scripts/compress-locales.sh
index 6264656b5a54c0f49201a78717ecdf968ca4d9e4..d3a7c0ec72cf28f30df6fcb3e92cab69f87c6289 100755
--- a/scripts/compress-locales.sh
+++ b/scripts/compress-locales.sh
@@ -52,7 +52,7 @@ for locale in $locales; do
     mkdir -p $BUILD_PATH/../i18n/$locale
     # Touch as the trigger locale doesn't really exist
     touch $BUILD_PATH/../i18n/${locale}/client.json
-    cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js
+    cat lib/jquery-1.7.1.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/javascript-extensions.js i18n/${locale}/client.json shared/gettext.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js lib/jschannel.js $BUILD_PATH/templates.js shared/renderer.js shared/class.js shared/mediator.js shared/tooltip.js shared/validation.js shared/helpers.js shared/screens.js shared/browser-support.js shared/wait-messages.js shared/error-messages.js shared/error-display.js shared/storage.js shared/xhr.js shared/network.js shared/provisioning.js shared/user.js shared/command.js shared/history.js shared/state_machine.js shared/modules/page_module.js shared/modules/xhr_delay.js shared/modules/xhr_disable_form.js shared/modules/cookie_check.js lib/urlparse.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgot_password.js dialog/controllers/check_registration.js dialog/controllers/pick_email.js dialog/controllers/add_email.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/provision_primary_user.js dialog/controllers/primary_user_provisioned.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/$locale/dialog.uncompressed.js
 done
 
 echo ''
diff --git a/scripts/deploy/vm.js b/scripts/deploy/vm.js
index c79475d091386d0f72b1e3b9e39edf47edbc2ce6..dd6d9b77699d7eacb06fce571d3c99f5a3a38fe6 100644
--- a/scripts/deploy/vm.js
+++ b/scripts/deploy/vm.js
@@ -4,7 +4,7 @@ jsel = require('JSONSelect'),
 key = require('./key.js'),
 sec = require('./sec.js');
 
-const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-2d34e444';
+const BROWSERID_TEMPLATE_IMAGE_ID = 'ami-5678aa3f';
 
 function extractInstanceDeets(horribleBlob) {
   var instance = {};
diff --git a/scripts/deploy_server.js b/scripts/deploy_server.js
index f8a6acc9d83da90ca9ece8eb047771d8bac9bc64..130bcd5ae06cb9276c2a0fa53343ddb0a9145036 100755
--- a/scripts/deploy_server.js
+++ b/scripts/deploy_server.js
@@ -263,10 +263,10 @@ deployer.on('error', function(r) {
 });
 
 
-// we check every 30 minutes no mattah what. (checks are cheap)
+// we check every 3 minutes no mattah what. (checks are cheap, github webhooks are flakey)
 setInterval(function () {
   deployer.checkForUpdates();
-}, (1000 * 60 * 30));
+}, (1000 * 60 * 3));
 
 // check for updates at startup
 deployer.on('ready', function() {
diff --git a/scripts/extract_po.sh b/scripts/extract_po.sh
index 5407ff2644e5a0fb2f62b17ff1f7baae304a1185..92ef55b724b1532428069106eeb59f2f8cc493cd 100755
--- a/scripts/extract_po.sh
+++ b/scripts/extract_po.sh
@@ -3,16 +3,17 @@
 # syntax:
 # extract-po.sh
 
+# No -j on first line, to clear out .pot file (Issue#1170)
 
 # messages.po is server side strings
-xgettext  -j --keyword=_ -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=messages.pot\
- `find lib -name '*.js' | grep -v 'i18n.js'`
+xgettext  --keyword=_ -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=messages.pot\
+ `find lib -name '*.js' | grep -v 'i18n.js' | grep -v jwcrypto`
 xgettext -j -L PHP --keyword=_ --output-dir=locale/templates/LC_MESSAGES --output=messages.pot `find resources/views -name '*.ejs'`
 xgettext -j -L PHP --keyword=_ --output-dir=locale/templates/LC_MESSAGES --output=messages.pot `find lib/browserid -name '*.ejs'`
 
 # client.po 
 # js
-xgettext -j -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=client.pot\
+xgettext -L Perl --output-dir=locale/templates/LC_MESSAGES --from-code=utf-8 --output=client.pot\
  `find resources/static -name '*.js' | grep -v /lib/ | grep -v /build/ | grep -v /production/ | grep -v 'gettext.js'`
 xgettext -j -L Perl --output-dir=locale/templates/LC_MESSAGES --output=client.pot `find resources/static/dialog/ -name '*.js' | grep -v include.js`
 # ejs
diff --git a/scripts/test_db_connectivity.js b/scripts/test_db_connectivity.js
index 3b1bad4283c7252c0c3f8b99466585097d85c43b..364d44a759b6846e1a3fd3fdfe383353c70ec4ef 100755
--- a/scripts/test_db_connectivity.js
+++ b/scripts/test_db_connectivity.js
@@ -21,8 +21,8 @@ var dbCfg = configuration.get('database');
 // don't bother creating the schema
 delete dbCfg.create_schema;
 
-db.open(dbCfg, function (r) {
-  if (r && r.message === "Unknown database 'browserid'") r = undefined;
+db.open(dbCfg, function (err, r) {
+  if (err && err.message === "Unknown database 'browserid'") r = undefined;
   function end() { process.exit(r === undefined ? 0 : 1); }
   if (r === undefined) db.close(end);
   else end();
diff --git a/tests/cache-header-tests.js b/tests/cache-header-tests.js
index 0b772e610625229309d55c6754199410e8229b35..690af835b47845bf58764b9a58668b2665b9f603 100755
--- a/tests/cache-header-tests.js
+++ b/tests/cache-header-tests.js
@@ -93,6 +93,7 @@ suite.addBatch({
   '/sign_in': hasProperCacheHeaders('/sign_in'),
   '/communication_iframe': hasProperCacheHeaders('/communication_iframe'),
   '/unsupported_dialog': hasProperCacheHeaders('/unsupported_dialog'),
+  '/cookies_disabled': hasProperCacheHeaders('/cookies_disabled'),
   '/relay': hasProperCacheHeaders('/relay'),
   '/authenticate_with_primary': hasProperCacheHeaders('/authenticate_with_primary'),
   '/signup': hasProperCacheHeaders('/signup'),
diff --git a/tests/cookie-session-security-test.js b/tests/cookie-session-security-test.js
index 9ca0f4d109ba31bce04acffe5187d061b234fc60..dd3466090546e782684210debf352d2e15e024e4 100755
--- a/tests/cookie-session-security-test.js
+++ b/tests/cookie-session-security-test.js
@@ -52,7 +52,7 @@ suite.addBatch({
           wsapi.clearCookies();
 
           // mess up the cookie
-          var the_match = first_cookie.match(/browserid_state=([^;]*);/);
+          var the_match = first_cookie.match(/browserid_state(?:_[a-z0-9]+)?=([^;]*);/);
           assert.isNotNull(the_match);
           var new_cookie_val = the_match[1].substring(0, the_match[1].length - 1);
           wsapi.injectCookies({browserid_state: new_cookie_val});
diff --git a/tests/db-test.js b/tests/db-test.js
index e25def6e19a64badc16fb1248086f5382b79c62a..65abaffa2926f260fd0157a7ee27dbf9f36d5565 100755
--- a/tests/db-test.js
+++ b/tests/db-test.js
@@ -36,8 +36,8 @@ suite.addBatch({
     topic: function() {
       db.open(dbCfg, this.callback);
     },
-    "and its ready": function(r) {
-      assert.isUndefined(r);
+    "and its ready": function(err) {
+      assert.isNull(err);
     },
     "doesn't prevent onReady": {
       topic: function() { db.onReady(this.callback); },
@@ -54,7 +54,8 @@ suite.addBatch({
     topic: function() {
       db.isStaged('lloyd@nowhe.re', this.callback);
     },
-    "isStaged returns false": function (r) {
+    "isStaged returns false": function (err, r) {
+      assert.isNull(err);
       assert.isFalse(r);
     }
   },
@@ -62,7 +63,8 @@ suite.addBatch({
     topic: function() {
       db.emailKnown('lloyd@nowhe.re', this.callback);
     },
-    "emailKnown returns false": function (r) {
+    "emailKnown returns false": function (err, r) {
+      assert.isNull(err);
       assert.isFalse(r);
     }
   }
@@ -73,13 +75,14 @@ suite.addBatch({
     topic: function() {
       db.stageUser('lloyd@nowhe.re', this.callback);
     },
-    "staging returns a valid secret": function(r) {
+    "staging returns a valid secret": function(err, r) {
+      assert.isNull(err);
       secret = r;
       assert.isString(secret);
       assert.strictEqual(secret.length, 48);
     },
     "fetch email for given secret": {
-      topic: function(secret) {
+      topic: function(err, secret) {
         db.emailForVerificationSecret(secret, this.callback);
       },
       "matches expected email": function(err, r) {
@@ -87,10 +90,11 @@ suite.addBatch({
       }
     },
     "fetch secret for email": {
-      topic: function(secret) {
+      topic: function(err, secret) {
         db.verificationSecretForEmail('lloyd@nowhe.re', this.callback);
       },
-      "matches expected secret": function(storedSecret) {
+      "matches expected secret": function(err, storedSecret) {
+        assert.isNull(err);
         assert.strictEqual(storedSecret, secret);
       }
     }
@@ -102,7 +106,8 @@ suite.addBatch({
     topic: function() {
       db.isStaged('lloyd@nowhe.re', this.callback);
     },
-    " as staged after it is": function (r) {
+    " as staged after it is": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, true);
     }
   },
@@ -110,7 +115,8 @@ suite.addBatch({
     topic: function() {
       db.emailKnown('lloyd@nowhe.re', this.callback);
     },
-    " as known when it is only staged": function (r) {
+    " as known when it is only staged": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, false);
     }
   }
@@ -121,8 +127,8 @@ suite.addBatch({
     topic: function() {
       db.gotVerificationSecret(secret, 'fakepasswordhash', this.callback);
     },
-    "gotVerificationSecret completes without error": function (r) {
-      assert.strictEqual(r, undefined);
+    "gotVerificationSecret completes without error": function (err, r) {
+      assert.isNull(err);
     }
   }
 });
@@ -132,7 +138,8 @@ suite.addBatch({
     topic: function() {
       db.isStaged('lloyd@nowhe.re', this.callback);
     },
-    "as staged immediately after its verified": function (r) {
+    "as staged immediately after its verified": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, false);
     }
   },
@@ -140,7 +147,8 @@ suite.addBatch({
     topic: function() {
       db.emailKnown('lloyd@nowhe.re', this.callback);
     },
-    "when it is": function (r) {
+    "when it is": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, true);
     }
   }
@@ -150,11 +158,12 @@ suite.addBatch({
   "checkAuth returns": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID('lloyd@nowhe.re', function(uid) {
+      db.emailToUID('lloyd@nowhe.re', function(err, uid) {
         db.checkAuth(uid, cb);
       });
     },
-    "the correct password": function(r) {
+    "the correct password": function(err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, "fakepasswordhash");
     }
   }
@@ -165,14 +174,16 @@ suite.addBatch({
     topic: function() {
       db.emailToUID('lloyd@nowhe.re', this.callback);
     },
-    "returns a valid userid": function(r) {
+    "returns a valid userid": function(err, r) {
+      assert.isNull(err);
       assert.isNumber(r);
     },
     "returns a UID": {
-      topic: function(uid) {
+      topic: function(err, uid) {
         db.userOwnsEmail(uid, 'lloyd@nowhe.re', this.callback);
       },
-      "that owns the original email": function(r) {
+      "that owns the original email": function(err, r) {
+        assert.isNull(err);
         assert.ok(r);
       }
     }
@@ -180,38 +191,48 @@ suite.addBatch({
 });
 
 suite.addBatch({
-  "getting a UID, then": {
+  "getting a UID": {
     topic: function() {
       db.emailToUID('lloyd@nowhe.re', this.callback);
     },
-    "staging an email": {
-      topic: function(uid) {
+    "does not error": function(err, uid) {
+      assert.isNull(err);
+    },
+    "then staging an email": {
+      topic: function(err, uid) {
         db.stageEmail(uid, 'lloyd@somewhe.re', this.callback);
       },
-      "yields a valid secret": function(secret) {
+      "yields a valid secret": function(err, secret) {
+        assert.isNull(err);
         assert.isString(secret);
         assert.strictEqual(secret.length, 48);
       },
       "then": {
-        topic: function(secret) {
+        topic: function(err, secret) {
           var cb = this.callback;
-          db.isStaged('lloyd@somewhe.re', function(r) { cb(secret, r); });
+          db.isStaged('lloyd@somewhe.re', function(err, r) { cb(secret, r); });
         },
         "makes it visible via isStaged": function(sekret, r) { assert.isTrue(r); },
         "lets you verify it": {
           topic: function(secret, r) {
             db.gotVerificationSecret(secret, undefined, this.callback);
           },
-          "successfully": function(r) {
-            assert.isUndefined(r);
+          "successfully": function(err, r) {
+            assert.isNull(err);
           },
           "and knownEmail": {
             topic: function() { db.emailKnown('lloyd@somewhe.re', this.callback); },
-            "returns true": function(r) { assert.isTrue(r); }
+            "returns true": function(err, r) {
+              assert.isNull(err);
+              assert.isTrue(r);
+            }
           },
           "and isStaged": {
             topic: function() { db.isStaged('lloyd@somewhe.re', this.callback); },
-            "returns false": function(r) { assert.isFalse(r); }
+            "returns false": function(err, r) {
+              assert.isNull(err);
+              assert.isFalse(r);
+            }
           }
         }
       }
@@ -226,7 +247,8 @@ suite.addBatch({
       topic: function() {
         db.emailsBelongToSameAccount('lloyd@nowhe.re', 'lloyd@somewhe.re', this.callback);
       },
-      "when they do": function(r) {
+      "when they do": function(err, r) {
+        assert.isNull(err);
         assert.isTrue(r);
       }
     },
@@ -234,7 +256,8 @@ suite.addBatch({
       topic: function() {
         db.emailsBelongToSameAccount('lloyd@anywhe.re', 'lloyd@somewhe.re', this.callback);
       },
-      "when they don't": function(r) {
+      "when they don't": function(err, r) {
+        assert.isNull(err);
         assert.isFalse(r);
       }
     }
@@ -246,7 +269,8 @@ suite.addBatch({
     topic: function() {
       db.emailType('lloyd@anywhe.re', this.callback);
     },
-    "is null": function (r) {
+    "is null": function (err, r) {
+      assert.isNull(err);
       assert.isUndefined(r);
     }
   },
@@ -254,7 +278,8 @@ suite.addBatch({
     topic: function() {
       db.emailType('lloyd@somewhe.re', this.callback);
     },
-    "is 'secondary'": function (r) {
+    "is 'secondary'": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, 'secondary');
     }
   },
@@ -262,7 +287,8 @@ suite.addBatch({
     topic: function() {
       db.emailType('lloyd@nowhe.re', this.callback);
     },
-    "is 'secondary'": function (r) {
+    "is 'secondary'": function (err, r) {
+      assert.isNull(err);
       assert.strictEqual(r, 'secondary');
     }
   }
@@ -272,18 +298,20 @@ suite.addBatch({
   "removing an existing email": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID("lloyd@somewhe.re", function(uid) {
+      db.emailToUID("lloyd@somewhe.re", function(err, uid) {
         db.removeEmail(uid, "lloyd@nowhe.re", cb);
       });
     },
-    "returns no error": function(r) {
+    "returns no error": function(err, r) {
+      assert.isNull(err);
       assert.isUndefined(r);
     },
     "causes emailKnown": {
       topic: function() {
         db.emailKnown('lloyd@nowhe.re', this.callback);
       },
-      "to return false": function (r) {
+      "to return false": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, false);
       }
     }
@@ -295,14 +323,15 @@ suite.addBatch({
     topic: function() {
       db.createUserWithPrimaryEmail("lloyd@primary.domain", this.callback);
     },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
+    "returns no error": function(err, r) {
+      assert.isNull(err);
     },
     "causes emailKnown": {
       topic: function() {
         db.emailKnown('lloyd@primary.domain', this.callback);
       },
-      "to return true": function (r) {
+      "to return true": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, true);
       }
     },
@@ -310,7 +339,8 @@ suite.addBatch({
       topic: function() {
         db.emailType('lloyd@primary.domain', this.callback);
       },
-      "to return 'primary'": function (r) {
+      "to return 'primary'": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, 'primary');
       }
     }
@@ -321,18 +351,19 @@ suite.addBatch({
   "adding a primary email to that account": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID('lloyd@primary.domain', function(uid) {
+      db.emailToUID('lloyd@primary.domain', function(err, uid) {
         db.addPrimaryEmailToAccount(uid, "lloyd2@primary.domain", cb);
       });
     },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
+    "returns no error": function(err) {
+      assert.isNull(err);
     },
     "causes emailKnown": {
       topic: function() {
         db.emailKnown('lloyd2@primary.domain', this.callback);
       },
-      "to return true": function (r) {
+      "to return true": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, true);
       }
     },
@@ -340,7 +371,8 @@ suite.addBatch({
       topic: function() {
         db.emailType('lloyd@primary.domain', this.callback);
       },
-      "to return 'primary'": function (r) {
+      "to return 'primary'": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, 'primary');
       }
     }
@@ -348,18 +380,19 @@ suite.addBatch({
   "adding a primary email to an account with only secondaries": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID('lloyd@somewhe.re', function(uid) {
+      db.emailToUID('lloyd@somewhe.re', function(err, uid) {
         db.addPrimaryEmailToAccount(uid, "lloyd3@primary.domain", cb);
       });
     },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
+    "returns no error": function(err) {
+      assert.isNull(err);
     },
     "causes emailKnown": {
       topic: function() {
         db.emailKnown('lloyd3@primary.domain', this.callback);
       },
-      "to return true": function (r) {
+      "to return true": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, true);
       }
     },
@@ -367,7 +400,8 @@ suite.addBatch({
       topic: function() {
         db.emailType('lloyd3@primary.domain', this.callback);
       },
-      "to return 'primary'": function (r) {
+      "to return 'primary'": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, 'primary');
       }
     }
@@ -378,18 +412,19 @@ suite.addBatch({
   "adding a registered primary email to an account": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID('lloyd@primary.domain', function(uid) {
+      db.emailToUID('lloyd@primary.domain', function(err, uid) {
         db.addPrimaryEmailToAccount(uid, "lloyd3@primary.domain", cb);
       });
     },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
+    "returns no error": function(err) {
+      assert.isNull(err);
     },
     "and emailKnown": {
       topic: function() {
         db.emailKnown('lloyd3@primary.domain', this.callback);
       },
-      "still returns true": function (r) {
+      "still returns true": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, true);
       }
     },
@@ -397,7 +432,8 @@ suite.addBatch({
       topic: function() {
         db.emailType('lloyd@primary.domain', this.callback);
       },
-      "still returns 'primary'": function (r) {
+      "still returns 'primary'": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, 'primary');
       }
     },
@@ -405,7 +441,8 @@ suite.addBatch({
       topic: function() {
         db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@somewhe.re', this.callback);
       },
-      "from original account": function(r) {
+      "from original account": function(err, r) {
+        assert.isNull(err);
         assert.isFalse(r);
       }
     },
@@ -413,7 +450,8 @@ suite.addBatch({
       topic: function() {
         db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@primary.domain', this.callback);
       },
-      "to new account": function(r) {
+      "to new account": function(err, r) {
+        assert.isNull(err);
         assert.isTrue(r);
       }
     }
@@ -424,18 +462,19 @@ suite.addBatch({
   "canceling an account": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID("lloyd@somewhe.re", function(uid) {
+      db.emailToUID("lloyd@somewhe.re", function(err, uid) {
         db.cancelAccount(uid, cb);
       });
     },
-    "returns no error": function(r) {
-      assert.isUndefined(r);
+    "returns no error": function(err) {
+      assert.isNull(err);
     },
     "causes emailKnown": {
       topic: function() {
         db.emailKnown('lloyd@somewhe.re', this.callback);
       },
-      "to return false": function (r) {
+      "to return false": function (err, r) {
+        assert.isNull(err);
         assert.strictEqual(r, false);
       }
     }
@@ -448,21 +487,21 @@ suite.addBatch({
       db.close(this.callback);
     },
     "should work": function(err) {
-      assert.isUndefined(err);
+      assert.isNull(err);
     },
     "re-opening the database": {
       topic: function() {
         db.open(dbCfg, this.callback);
       },
-      "works": function(r) {
-        assert.isUndefined(r);
+      "works": function(err) {
+        assert.isNull(err);
       },
       "and then purging": {
         topic: function() {
           db.closeAndRemove(this.callback);
         },
         "works": function(r) {
-          assert.isUndefined(r);
+          assert.isNull(r);
         }
       }
     }
diff --git a/tests/email-throttling-test.js b/tests/email-throttling-test.js
index db6a7d1a38193463945238af2bcc1bd0626bf402..ba807a7829aa692ca705946e767ec9c461a7a7de 100755
--- a/tests/email-throttling-test.js
+++ b/tests/email-throttling-test.js
@@ -52,7 +52,7 @@ suite.addBatch({
       site:'fakesite.com'
     }),
     "is throttled": function(err, r) {
-      assert.strictEqual(r.code, 403);
+      assert.strictEqual(r.code, 429);
     }
   }
 });
@@ -101,8 +101,8 @@ suite.addBatch({
       email: 'second@fakeemail.com',
       site:'fakesite.com'
     }),
-    "is throttled with a 403": function(err, r) {
-      assert.strictEqual(r.code, 403);
+    "is throttled with a 429": function(err, r) {
+      assert.strictEqual(r.code, 429);
     }
   }
 });
diff --git a/tests/lib/start-stop.js b/tests/lib/start-stop.js
index d7485e469285845f34916b3f1d7a34675a3f0849..895ae04f4ec1c039b32a590e8d0351084cf5acfd 100644
--- a/tests/lib/start-stop.js
+++ b/tests/lib/start-stop.js
@@ -103,7 +103,7 @@ exports.addStartupBatches = function(suite) {
         db.open(cfg, this.callback);
       },
       "should work fine": function(r) {
-        assert.isUndefined(r);
+        assert.isNull(r);
       }
     }
   });
@@ -184,7 +184,7 @@ exports.addShutdownBatches = function(suite) {
         db.closeAndRemove(this.callback);
       },
       "should work": function(err) {
-        assert.isUndefined(err);
+        assert.isNull(err);
       }
     }
   });
diff --git a/tests/no-cookie-test.js b/tests/no-cookie-test.js
index 6a030d218fd27499e63d6746c5591b90419bc431..150d92cd448d862b228f2736abb5f86c9bec703d 100755
--- a/tests/no-cookie-test.js
+++ b/tests/no-cookie-test.js
@@ -88,10 +88,10 @@ suite.addBatch({
       }));
       req.end();
     },
-    "returns a 400 with 'no cookie' as the body": function(err, r) {
+    "returns a 403 with 'no cookie' as the body": function(err, r) {
       assert.equal(err, null);
-      assert.equal(r.code, 400);
-      assert.equal(r.body, 'Bad Request: no cookie');
+      assert.equal(r.code, 403);
+      assert.equal(r.body, 'Forbidden: no cookie');
     }
   }
 });
diff --git a/tests/page-requests-test.js b/tests/page-requests-test.js
index e26a5892ceec944eb4e8e85ae4bab80828697fed..0ee2537c058c543df04c5aabcc707c40fbda5a23 100755
--- a/tests/page-requests-test.js
+++ b/tests/page-requests-test.js
@@ -67,6 +67,7 @@ suite.addBatch({
   'GET /.well-known/browserid':  respondsWith(200),
   'GET /signin':                 respondsWith(200),
   'GET /unsupported_dialog':     respondsWith(200),
+  'GET /cookies_disabled':       respondsWith(200),
   'GET /developers':             respondsWith(200),
   'GET /manage':                 respondsWith(302),
   'GET /users':                  respondsWith(302),
@@ -75,7 +76,7 @@ suite.addBatch({
   'GET /primaries/':             respondsWith(302),
   'GET /developers':             respondsWith(302),
   'GET /developers/':            respondsWith(302),
-  'GET /test':                   respondsWith(200),
+  'GET /test':                   respondsWith(301),
   'GET /test/':                  respondsWith(200),
   'GET /include.js':             respondsWith(200),
   'GET /include.orig.js':        respondsWith(200)
diff --git a/tests/password-bcrypt-update-test.js b/tests/password-bcrypt-update-test.js
index db6a994957ec49641568b68d3ccc43655e9f2f75..c403d6ef715d1250b6671128abd9a64b1b7af923 100755
--- a/tests/password-bcrypt-update-test.js
+++ b/tests/password-bcrypt-update-test.js
@@ -87,11 +87,12 @@ suite.addBatch({
   "the password": {
     topic: function() {
       var cb = this.callback;
-      db.emailToUID(TEST_EMAIL, function(uid) {
+      db.emailToUID(TEST_EMAIL, function(err, uid) {
         db.checkAuth(uid, cb);
       });
     },
-    "is bcrypted with the expected number of rounds": function(r) {
+    "is bcrypted with the expected number of rounds": function(err, r) {
+      assert.isNull(err);
       assert.equal(typeof r, 'string');
       assert.equal(config.get('bcrypt_work_factor'), bcrypt.get_rounds(r));
     }
@@ -134,11 +135,12 @@ suite.addBatch({
     "if we recheck the auth hash": {
       topic: function() {
         var cb = this.callback;
-        db.emailToUID(TEST_EMAIL, function(uid) {
+        db.emailToUID(TEST_EMAIL, function(err, uid) {
           db.checkAuth(uid, cb);
         });
       },
-      "its bcrypted with 8 rounds": function(r) {
+      "its bcrypted with 8 rounds": function(err, r) {
+        assert.isNull(err);
         assert.equal(typeof r, 'string');
         assert.equal(8, bcrypt.get_rounds(r));
       }
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..a0753552763a36b9a1f434c1986e47b64cee4ca8
--- /dev/null
+++ b/tests/stalled-mysql-test.js
@@ -0,0 +1,379 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+if (process.env['NODE_ENV'] != 'test_mysql') process.exit(0);
+
+const assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+temp = require('temp'),
+fs = require('fs'),
+jwk = require('jwcrypto/jwk.js'),
+jwt = require('jwcrypto/jwt.js'),
+vep = require('jwcrypto/vep.js'),
+jwcert = require('jwcrypto/jwcert.js'),
+path = require('path');
+
+var suite = vows.describe('forgotten-email');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+// let's reduce the amount of time allowed for queries, so that
+// we get a faster failure and tests run quicker
+process.env['MAX_QUERY_TIME_MS'] = 250;
+
+// and let's instruct children to pretend as if the driver is
+// stalled if a file exists
+var stallFile = temp.path({suffix: '.stall'});
+process.env['STALL_MYSQL_WHEN_PRESENT'] = stallFile;
+
+start_stop.addStartupBatches(suite);
+
+// ever time a new token is sent out, let's update the global
+// var 'token'
+var token = undefined;
+
+function addStallDriverBatch(stall) {
+  suite.addBatch({
+    "changing driver state": {
+      topic: function() {
+        if (stall) fs.writeFileSync(stallFile, "");
+        else fs.unlinkSync(stallFile);
+
+        // After changing the file which indicates to child
+        // processes whether the driver should simulate a stalled
+        // state or not, we need to wait for them to detect the
+        // change.  because we use `fs.watchFile()` on a short poll,
+        // this should be nearly instantaneous.  300ms is a magic number
+        // which is hoped to allow plenty of time even on a loaded
+        // machine
+        setTimeout(this.callback, 300);
+      },
+      "completes": function(err, r) { }
+    }
+  });
+}
+
+// first stall mysql
+addStallDriverBatch(true);
+
+// call session context once to populate CSRF stuff in the
+// wsapi client lib
+suite.addBatch({
+  "get context": {
+    topic: wsapi.get('/wsapi/session_context'),
+    "works" : function(err, r) {
+      assert.isNull(err);
+    }
+  }
+});
+
+// now try all apis that can be excercised without further setup
+suite.addBatch({
+  "address_info": {
+    topic: wsapi.get('/wsapi/address_info', {
+      email: 'test@example.domain'
+    }),
+    "works": function(err, r) {
+      // address info with a primary address doesn't need db access.
+      assert.strictEqual(r.code, 200);
+    }
+  },
+  "address_info": {
+    topic: wsapi.get('/wsapi/address_info', {
+      email: 'test@non-existant.domain'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "have_email": {
+    topic: wsapi.get('/wsapi/have_email', {
+      email: 'test@example.com'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "authenticate_user": {
+    topic: wsapi.post('/wsapi/authenticate_user', {
+      email: 'test@example.com',
+      pass: 'oogabooga'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "complete_email_addition": {
+    topic: wsapi.post('/wsapi/complete_email_addition', {
+      token: 'bogus'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "complete_user_creation": {
+    topic: wsapi.post('/wsapi/complete_user_creation', {
+      token: 'bogus',
+      pass: 'fakefake'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "email_for_token": {
+    topic: wsapi.get('/wsapi/email_for_token', {
+      token: 'bogus'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "stage_user": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'bogus@bogus.edu',
+      site: 'whatev.er'
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  }
+});
+
+// now unstall the driver, we'll create an account and sign in in
+// order to test the behavior of the remaining APIs when the database
+// is stalled
+addStallDriverBatch(false);
+
+var token = undefined;
+
+suite.addBatch({
+  "account staging": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: "stalltest@whatev.er",
+      site: 'fakesite.com'
+    }),
+    "works":     function(err, r) {
+      assert.equal(r.code, 200);
+    }
+  }
+});
+
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+    },
+    "setting password": {
+      topic: function(token) {
+        wsapi.post('/wsapi/complete_user_creation', {
+          token: token,
+          pass: "somepass"
+        }).call(this);
+      },
+      "works just fine": function(err, r) {
+        assert.equal(r.code, 200);
+      }
+    }
+  }
+});
+
+// re-stall mysql
+addStallDriverBatch(true);
+
+// test remaining wsapis
+
+suite.addBatch({
+  "account_cancel": {
+    topic: wsapi.post('/wsapi/account_cancel', { }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "cert_key": {
+    topic: wsapi.post('/wsapi/cert_key', {
+      email: "test@whatev.er",
+      pubkey: "bogus"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "email_addition_status": {
+    topic: wsapi.get('/wsapi/email_addition_status', {
+      email: "test@whatev.er"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "list_emails": {
+    topic: wsapi.get('/wsapi/list_emails', {}),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "remove_email": {
+    topic: wsapi.post('/wsapi/remove_email', {
+      email: "test@whatev.er"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "session_context": {
+    topic: wsapi.get('/wsapi/session_context', { }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "stage_email": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: "test2@whatev.er",
+      site: "foo.com"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "update_password": {
+    topic: wsapi.post('/wsapi/update_password', {
+      oldpass: "oldpassword",
+      newpass: "newpassword"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "user_creation_status": {
+    topic: wsapi.get('/wsapi/user_creation_status', {
+      email: "test3@whatev.er"
+    }),
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  }
+});
+
+// now let's test apis that require an assertion, and only after verifying
+// that, hit the database
+const TEST_DOMAIN = 'example.domain',
+      TEST_EMAIL = 'testuser@' + TEST_DOMAIN,
+      TEST_ORIGIN = 'http://127.0.0.1:10002',
+      TEST_FIRST_ACCT = 'testuser@fake.domain';
+
+var g_keypair, g_cert, g_assertion;
+
+suite.addBatch({
+  "generating a keypair": {
+    topic: function() {
+      return jwk.KeyPair.generate("DS", 256)
+    },
+    "succeeds": function(r, err) {
+      assert.isObject(r);
+      assert.isObject(r.publicKey);
+      assert.isObject(r.secretKey);
+      g_keypair = r;
+    }
+  }
+});
+
+var g_privKey = jwk.SecretKey.fromSimpleObject(
+  JSON.parse(require('fs').readFileSync(
+    path.join(__dirname, '..', 'example', 'primary', 'sample.privatekey'))));
+
+
+suite.addBatch({
+  "generting a certificate": {
+    topic: function() {
+      var domain = process.env['SHIMMED_DOMAIN'];
+
+      var expiration = new Date();
+      expiration.setTime(new Date().valueOf() + 60 * 60 * 1000);
+      g_cert = new jwcert.JWCert(TEST_DOMAIN, expiration, new Date(),
+                                 g_keypair.publicKey, {email: TEST_EMAIL}).sign(g_privKey);
+      return g_cert;
+    },
+    "works swimmingly": function(cert, err) {
+      assert.isString(cert);
+      assert.lengthOf(cert.split('.'), 3);
+    }
+  }
+});
+
+suite.addBatch({
+  "generating an assertion": {
+    topic: function() {
+      var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000));
+      var tok = new jwt.JWT(null, expirationDate, TEST_ORIGIN);
+      return vep.bundleCertsAndAssertion([g_cert], tok.sign(g_keypair.secretKey));
+    },
+    "succeeds": function(r, err) {
+      assert.isString(r);
+      g_assertion = r;
+    }
+  }
+});
+
+// finally!  we have our assertion in g_assertion
+suite.addBatch({
+  "add_email_with_assertion": {
+    topic: function() {
+      wsapi.post('/wsapi/add_email_with_assertion', {
+        assertion: g_assertion
+      }).call(this);
+    },
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "auth_with_assertion": {
+    topic: function() {
+      wsapi.post('/wsapi/auth_with_assertion', {
+        assertion: g_assertion
+      }).call(this);
+    },
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  },
+  "create_account_with_assertion": {
+    topic: function() {
+      wsapi.post('/wsapi/create_account_with_assertion', {
+        assertion: g_assertion
+      }).call(this);
+    },
+    "fails with 503": function(err, r) {
+      assert.strictEqual(r.code, 503);
+    }
+  }
+});
+
+// logout doesn't need database, it should still succeed
+suite.addBatch({
+  "logout": {
+    topic: wsapi.post('/wsapi/logout', { }),
+    "succeeds": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// finally, unblock mysql so we can shut down
+addStallDriverBatch(false);
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/static-resource-test.js b/tests/static-resource-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..f7fa6603da31aec4b64ed4706383f3a8ed82d449
--- /dev/null
+++ b/tests/static-resource-test.js
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+      vows = require('vows'),
+      resources = require('../lib/static_resources');
+
+var suite = vows.describe('cache header tests');
+suite.options.error = false;
+
+var locales = ['ar', 'de', 'en_US', 'fr'];
+suite.addBatch({
+  "All resources expand": {
+    topic: function () {
+      this.callback(resources.all(locales));
+    },
+    "We get stuff": function (files) {
+      var res = resources.resources;
+      assert.ok(files['/production/dialog.css'].length >= 3);
+      // Get ride of non-localized asset bundles
+      ['/production/dialog.css', '/production/browserid.css'].forEach(
+        function (nonLocaleAsset) {
+          delete res[nonLocaleAsset];
+          delete files[nonLocaleAsset];
+        });
+
+      // Keys expand
+      // files ['/production/:locale/dialog.js']
+      // becomes ['/production/ar/dialog.js', 'production/de/dialog.js', ...]
+      assert.equal(Object.keys(files).length,
+                   Object.keys(res).length * locales.length);
+
+      // Let's use the first bundle
+      var minFile = Object.keys(files)[0];
+      var minRes = Object.keys(res)[0];
+
+      // Number of files underneath stay the same
+      assert.equal(files[minFile].length,
+                   res[minRes].length);
+      // Non-localized files underneath stay the same
+      [0, 1, 2, 3, 4, 5, 7].forEach(function (nonLocalizedIndex) {
+      assert.equal(files[minFile][nonLocalizedIndex],
+                   res[minRes][nonLocalizedIndex]);
+      });
+      // Fragile - filename with :locale...
+      // When fixing this test case... console.log(res[Object.keys(res)[0]]);
+      var localeIndex = 6;
+      assert.notEqual(files[minFile][localeIndex],
+                      res[minRes][localeIndex]);
+      var counter = 0;
+      for (var key in res) {
+        res[key].forEach(function (item) {
+          counter++;
+        });
+      }
+      assert.ok(counter > 90);
+    }
+  }
+});
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/verifier-test.js b/tests/verifier-test.js
index cdafb345bd39d7b56a236bc88490599fb44330fe..b2a455dc04807502630199c49333d5a9a42e7572 100755
--- a/tests/verifier-test.js
+++ b/tests/verifier-test.js
@@ -721,7 +721,7 @@ function make_other_issuer_tests(new_style) {
       var fakeDomainKeypair = jwk.KeyPair.generate("RS", 64);
       var newClientKeypair = jwk.KeyPair.generate("DS", 256);
       expiration = new Date(new Date().getTime() + (1000 * 60 * 60 * 6));
-      var cert = new jwcert.JWCert("lloyd.io", expiration, new Date(), newClientKeypair.publicKey,
+      var cert = new jwcert.JWCert("no.such.domain", expiration, new Date(), newClientKeypair.publicKey,
                                    {email: TEST_EMAIL}).sign(fakeDomainKeypair.secretKey);
 
       var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000));
@@ -744,7 +744,7 @@ function make_other_issuer_tests(new_style) {
       "to return a clear error message": function (err, r) {
         var resp = JSON.parse(r.body);
         assert.strictEqual(resp.status, 'failure');
-        assert.strictEqual(resp.reason, "can't get public key for lloyd.io");
+        assert.strictEqual(resp.reason, "can't get public key for no.such.domain");
       }
     }
   };