diff --git a/.gitignore b/.gitignore
index 1823f2e5796b4df5d7c5694ff922d2e906b415a8..b0346bd65581b6b621f1720e7f860c7d88efd317 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
 .\#*
 /node_modules
 /var
+/rpmbuild
+
diff --git a/ChangeLog b/ChangeLog
index 1fae2614600faf99ac11af7a4c03c85bfe8d3299..4afca93deb2025ee9e51d24542e7db884357f639 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+train-2011.10.27:
+  * link fixing ('need help?' to point to SUMO): #378
+  * unit tests repaired: #469 (broken in fix to #82)
+  * improve handling of network errors: #448
+  * improve styling and language of email confirmation page: #349
+  * logging improvements: #455
+  * RPM generation script created (for installation of browserid on redhat [moz prod] boxes): #478
+  * SCHEMA CHANGES to improve database performance and scalability: #480
+  * change the health check call from '/ping.txt' to '/__heartbeat__': #481
+  * remove application level network timeouts (let the network stack do its job, the user can cancel if they get sick of it): #485
+  * improve messaging for unsupported browsers: #273, #484
+  * developer documentation improvements: #496
+
 train-2011.10.20:
   * android < 3.0 now supported: #461
   * properly set assertion expiration time to when they expire, not when they're issued: #433, #457, #458
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 07fe7cb6e67c0bed051f18f273711ae7a05ba5dd..1e8b7b606fff18ce905173a46c624df1fbc287e8 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -156,16 +156,16 @@ post update hook, annotated to help you follow along:
 ### 5. get node servers running
 
 At this point, pushing code to gitolite will cause /home/browserid/code to be updated.  Now
-we need to get the servers running!  Manually we can verify that the servers will run. 
+we need to get the servers running!  Manually we can verify that the servers will run.
 For the browser id server:
 
-    cd /home/browserid/code/browserid && sudo -u www-data ./run.js  
+    cd /home/browserid/code/browserid && sudo -u www-data ./run.js
 
 And for the verifier:
 
-    cd /home/browserid/code/verifier && sudo -u www-data ./run.js  
+    cd /home/browserid/code/verifier && sudo -u www-data ./run.js
 
-Now let's set up [monit] to restart the node.js servers:  
+Now let's set up [monit] to restart the node.js servers:
 
   1. install monit: `sudo apt-get install monit`
   2. enable monit by editing `/etc/default/monit`
@@ -181,7 +181,7 @@ include /etc/monit.d/*
 
 <pre>
 #!/bin/bash
-/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &    
+/usr/local/bin/node $1 > $(dirname $1)/error.log 2>&1 &
 </pre>
 
   5. create a file to run the verifier at `/etc/monit.d/verifier`:
@@ -192,7 +192,7 @@ check host verifier with address 127.0.0.1
         as uid "www-data" and gid "www-data"
     stop program  = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/verifier/run.js'"
     if failed port 62800 protocol HTTP
-        request /ping.txt
+        request /__heartbeat__
         with timeout 10 seconds
         then restart
 </pre>
@@ -205,7 +205,7 @@ check host browserid.org with address 127.0.0.1
         as uid "www-data" and gid "www-data"
     stop program  = "/usr/bin/pkill -f '/usr/local/bin/node /home/browserid/code/browserid/run.js'"
     if failed port 62700 protocol HTTP
-        request /ping.txt
+        request /__heartbeat__
         with timeout 10 seconds
         then restart
 </pre>
diff --git a/README.md b/README.md
index 9ecb6efd18c83e9d3d9e9af4ded21a727c745e29..908b6ea4a28cb2eed5b084f9ed015dac2e7339c1 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,12 @@ or changes will be made.
 
 2. Boot up the VM:
 
-    $ cd browserid
-    $ vagrant up
-    $ vagrant ssh
-    vagrant@lucid32:browserid$ node ./run.js
+```
+cd browserid
+vagrant up
+vagrant ssh vagrant@lucid32:browserid
+node ./run.js
+```
 
 `vagrant up` will take a while. Go get a cup of coffee. This is because it downloads the 500MB VM.
 
diff --git a/browserid.spec b/browserid.spec
new file mode 100644
index 0000000000000000000000000000000000000000..368194a57e5c834075678777fc41efd46ef658c4
--- /dev/null
+++ b/browserid.spec
@@ -0,0 +1,45 @@
+%define _rootdir /opt/browserid
+
+Name:          browserid-server
+Version:       0.2011.10.13
+Release:       1%{?dist}
+Summary:       BrowserID server
+Packager:      Pete Fritchman <petef@mozilla.com>
+Group:         Development/Libraries
+License:       MPL 1.1+/GPL 2.0+/LGPL 2.1+
+URL:           https://github.com/mozilla/browserid
+Source0:       %{name}.tar.gz
+BuildRoot:     %{_tmppath}/%{name}-%{version}-%{release}-root
+AutoReqProv:   no
+Requires:      openssl nodejs
+BuildRequires: gcc-c++ git jre make npm openssl-devel
+
+%description
+browserid server & web home for browserid.org
+
+%prep
+%setup -q -n browserid
+
+%build
+npm install
+export PATH=$PWD/node_modules/.bin:$PATH
+(cd browserid && ./compress.sh)
+git log -1 --oneline > browserid/static/ver.txt
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_rootdir}
+for f in browserid libs node_modules verifier *.json *.js; do
+    cp -rp $f %{buildroot}%{_rootdir}/$dir
+done
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%{_rootdir}
+
+%changelog
+* Tue Oct 18 2011 Pete Fritchman <petef@mozilla.com>
+- Initial version
diff --git a/browserid/app.js b/browserid/app.js
index a0f26efc28471de7427facc9fd6f4e6ed105a007..6f661ee7c23063c93e6e44bfcfaaafda91e8e68f 100644
--- a/browserid/app.js
+++ b/browserid/app.js
@@ -45,6 +45,7 @@ express = require('express'),
 secrets = require('../libs/secrets.js'),
 db = require('./lib/db.js'),
 configuration = require('../libs/configuration.js'),
+heartbeat = require('../libs/heartbeat.js'),
 substitution = require('../libs/substitute.js');
 metrics = require("../libs/metrics.js"),
 logger = require("../libs/logging.js").logger;
@@ -79,11 +80,16 @@ function router(app) {
     metrics.userEntry(req);
     res.render('dialog.ejs', {
       title: 'A Better Way to Sign In',
-      layout: false,
+      layout: 'dialog_layout.ejs',
+      useJavascript: true,
       production: configuration.get('use_minified_resources')
     });
   });
 
+  app.get("/unsupported_dialog", function(req,res) {
+    res.render('unsupported_dialog.ejs', {layout: 'dialog_layout.ejs', useJavascript: false});
+  });
+
   // simple redirects (internal for now)
   app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html',true));
 
@@ -102,9 +108,6 @@ function router(app) {
     res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true});
   });
 
-  // BA removed .html URLs. If we have 404s,
-  // we should set up some redirects
-  
   app.get("/signup", function(req, res) {
     res.render('signup.ejs', {title: 'Sign Up', fullpage: false});
   });
@@ -141,7 +144,7 @@ function router(app) {
   REDIRECTS = {
     "/manage": "/",
     "/users": "/",
-    "/users/": "/",    
+    "/users/": "/",
     "/primaries" : "/developers",
     "/primaries/" : "/developers",
     "/developers" : "https://github.com/mozilla/browserid/wiki/How-to-Use-BrowserID-on-Your-Site"
@@ -160,6 +163,9 @@ function router(app) {
   // register all the WSAPI handlers
   wsapi.setup(app);
 
+  // setup health check / heartbeat
+  heartbeat.setup(app);
+
   // the public key
   app.get("/pk", function(req, res) {
     res.json(ca.PUBLIC_KEY.toSimpleObject());
diff --git a/browserid/lib/db_mysql.js b/browserid/lib/db_mysql.js
index 06bd219bf904cd91e58c2398abb282bc108de24c..9a7dbc8385a3226ec906b436da232fc4dd3e3f53 100644
--- a/browserid/lib/db_mysql.js
+++ b/browserid/lib/db_mysql.js
@@ -49,6 +49,7 @@
  *
  *
  *    +------ staged ----------+
+ *    |*int id                 |
  *    |*string secret          |
  *    | bool new_acct          |
  *    | string existing        |
@@ -67,10 +68,28 @@ var client = undefined;
 // may get defined at open() time causing a database to be dropped upon connection closing.
 var drop_on_close = undefined;
 
+// If you change these schemas, please notify <services-ops@mozilla.com>
 const schemas = [
-  "CREATE TABLE IF NOT EXISTS user   ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );",
-  "CREATE TABLE IF NOT EXISTS email  ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, INDEX(user), address VARCHAR(255) UNIQUE, INDEX(address) );",
-  "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);"
+  "CREATE TABLE IF NOT EXISTS user (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "passwd CHAR(64) NOT NULL" +
+    ") ENGINE=InnoDB;",
+
+  "CREATE TABLE IF NOT EXISTS email (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "user BIGINT NOT NULL," +
+    "address VARCHAR(255) UNIQUE NOT NULL," + 
+    "FOREIGN KEY user_fkey (user) REFERENCES user(id)" +
+    ") ENGINE=InnoDB;",
+
+  "CREATE TABLE IF NOT EXISTS staged (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "secret CHAR(48) UNIQUE NOT NULL," +
+    "new_acct BOOL NOT NULL," +
+    "existing VARCHAR(255)," +
+    "email VARCHAR(255) UNIQUE NOT NULL," +
+    "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" + 
+    ") ENGINE=InnoDB;",
 ];
 
 // log an unexpected database error
@@ -111,37 +130,39 @@ exports.open = function(cfg, cb) {
   }
 
   // now create the databse
-  client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
-    if (err) {
-      logUnexpectedError(err);
-      cb(err);
-      return;
-    }
-    client.useDatabase(database, function(err) {
+  if (cfg.create_schema || cfg.unit_test) {
+    client.query("CREATE DATABASE IF NOT EXISTS " + database, function(err) {
       if (err) {
         logUnexpectedError(err);
         cb(err);
         return;
       }
+      client.useDatabase(database, function(err) {
+        if (err) {
+          logUnexpectedError(err);
+          cb(err);
+          return;
+        }
 
-      // now create tables
-      function createNextTable(i) {
-        if (i < schemas.length) {
-          client.query(schemas[i], function(err) {
-            if (err) {
-              logUnexpectedError(err);
-              cb(err);
-            } else {
-              createNextTable(i+1);
-            }
-          });
-        } else {
-          cb();
+        // now create tables
+        function createNextTable(i) {
+          if (i < schemas.length) {
+            client.query(schemas[i], function(err) {
+              if (err) {
+                logUnexpectedError(err);
+                cb(err);
+              } else {
+                createNextTable(i+1);
+              }
+            });
+          } else {
+            cb();
+          }
         }
-      }
-      createNextTable(0);
+        createNextTable(0);
+      });
     });
-  });
+  };
 };
 
 exports.close = function(cb) {
diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js
index 9cd1bf8b0493d7926ac25f3f3971ff1ae792f4cf..9c75fdbfc731bbbc1769f39ecb25efac83b9938b 100644
--- a/browserid/lib/wsapi.js
+++ b/browserid/lib/wsapi.js
@@ -278,7 +278,7 @@ function setup(app) {
 
         db.gotVerificationSecret(req.body.token, hash, function(err, email) {
           if (err) {
-            logger.error("error completing the verification: " + err);
+            logger.warn("couldn't complete email verification: " + err);
             resp.json({ success: false });
           } else {
             // FIXME: not sure if we want to do this (ba)
@@ -360,7 +360,7 @@ function setup(app) {
   app.post('/wsapi/complete_email_addition', checkParams(["token"]), function(req, resp) {
     db.gotVerificationSecret(req.body.token, undefined, function(e) {
       if (e) {
-        logger.error("error completing the verification: " + e);
+        logger.warn("couldn't complete email verification: " + e);
         resp.json({ success: false });
       } else {
         resp.json({ success: true });
diff --git a/browserid/static/css/style.css b/browserid/static/css/style.css
index 9023ebbcbdb73a7e511d960582ad802c73eb8c69..20c2e610d2f6ed9a9971c51a1d6bd409893d5059 100644
--- a/browserid/static/css/style.css
+++ b/browserid/static/css/style.css
@@ -21,30 +21,24 @@ body {
   overflow-y: scroll;
 }
 
-/*header {
-    border-top: 4px solid #333;
-    background-color: #333;
-    padding: 0 20% 0 20%;
-    margin: 0;
-    background: #008;
-    height: 230px;
-    color #fff;
-    min-width: 800px;
-    display: block;
+/* for floats */
+.cf:after {
+  content: ".";
+  display: block;
+  clear: both;
+  visibility: hidden;
+  line-height: 0;
+  height: 0;
 }
 
-footer {
-    background-color: #F1F1F1;
-    border-top: 2px solid #ddd;
-    margin: 0;
-    margin-top: 100px;
-    padding: 0;
-    height: 200px;
-    font-size: 1.1em;
-    display: block;
+html[xmlns] .cf {
+  display: block;
+}
+
+* html .cf {
+  height: 1%;
 }
 
-*/
 .sans {
   font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
 }
@@ -115,11 +109,6 @@ hr {
   display: none;
 }
 
-/*
-.authenticated #content {
-  display: block;
-}
-*/
 #about {
   font-family: 'Droid Serif', Georgia, serif;
   font-size: 14px;
@@ -623,7 +612,7 @@ h1 {
   display: inline-block;
 }
 
-#signUpForm {
+#signUpForm, #congrats {
   padding: 20px;
   background-color: rgba(0,0,0,0.035);
 
@@ -656,33 +645,6 @@ h1 {
   float: left;
 }
 
-a.forgot {
-  color: #888784;
-  text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
-  font-size: 11px;
-}
-
-.notifications {
-  list-style-type: none;
-}
-
-.notifications > .notification {
-  margin-top: 20px;
-  padding: 5px;
-  line-height: 16px;
-  -webkit-border-radius: 3px;
-     -moz-border-radius: 3px;
-       -o-border-radius: 3px;
-          border-radius: 3px;
-  display: none;
-  text-align: center;
-}
-
-.notifications .notification.error {
-  color: red;
-  background-color: rgba(255,0,0,0.25);
-}
-
 #signUpForm .red {
   color: red;
 }
@@ -748,6 +710,64 @@ a.forgot {
   float: right;
 }
 
+#signUpForm .error {
+  margin-top: 20px;
+  color: red;
+  background-color: rgba(255,0,0,0.25);
+  padding: 5px;
+  line-height: 16px;
+
+  -webkit-border-radius: 3px;
+     -moz-border-radius: 3px;
+       -o-border-radius: 3px;
+          border-radius: 3px;
+}
+
+
+#congrats #siteinfo, #congrats {
+  display: none;
+}
+
+#congrats p {
+    color: #62615F;
+    display: block;
+    text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
+}
+
+#congrats .website {
+    display: block;
+    text-align: center;
+}
+
+
+a.forgot {
+  color: #888784;
+  text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
+  font-size: 11px;
+}
+
+.notifications {
+  list-style-type: none;
+}
+
+.notifications > .notification {
+  margin-top: 20px;
+  padding: 5px;
+  line-height: 16px;
+  -webkit-border-radius: 3px;
+     -moz-border-radius: 3px;
+       -o-border-radius: 3px;
+          border-radius: 3px;
+  display: none;
+  text-align: center;
+}
+
+.notifications .notification.error {
+  color: red;
+  background-color: rgba(255,0,0,0.25);
+}
+
+
 #header {
   position: absolute;
   top: 0;
@@ -830,172 +850,3 @@ a.forgot {
   color: #aaa;
 }
 
-
-/* for floats */
-.cf:after {
-  content: ".";
-  display: block;
-  clear: both;
-  visibility: hidden;
-  line-height: 0;
-  height: 0;
-}
-
-html[xmlns] .cf {
-  display: block;
-}
-
-* html .cf {
-  height: 1%;
-}
-
-#signUpFormWrap {
-  margin: 122px 160px;
-}
-
-#signUpFormWrap a.signUpIn {
-  color: #549FDC;
-  text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
-}
-
-#signUpFormWrap a.space {
-  margin: 10px 20px 0;
-  display: inline-block;
-}
-
-#signUpForm,
-#congrats {
-  padding: 20px;
-  background-color: rgba(0,0,0,0.035);
-
-  -webkit-border-radius: 5px;
-     -moz-border-radius: 5px;
-       -o-border-radius: 5px;
-          border-radius: 5px;
-}
-
-#congrats {
-    display: none;
-}
-
-#congrats p {
-    color: #62615F;
-    display: block;
-    text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
-}
-
-#signUpForm label/*,
-#signUpForm p.hint*/ {
-  display: block;
-  color: #62615F;
-  text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-}
-
-/*
-
-#signUpForm p.hint {
-  margin: -20px -20px 20px;
-  padding: 10px 20px 10px 45px;
-  background-color: rgba(0,0,0,0.05);
-  background-repeat: no-repeat;
-  background-position: 20px center;
-  color: #222;
-  
-  -webkit-border-radius: 5px 5px 0 0;
-     -moz-border-radius: 5px 5px 0 0;
-       -o-border-radius: 5px 5px 0 0;
-          border-radius: 5px 5px 0 0;
-}
-*/
-#signUpForm label.half,
-.half {
-  width: 50%;
-  display: inline-block;
-  float: left;
-}
-
-a.forgot {
-  color: #888784;
-  text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
-  font-size: 11px;
-}
-
-#signUpForm .error {
-  margin-top: 20px;
-  color: red;
-  background-color: rgba(255,0,0,0.25);
-  padding: 5px;
-  line-height: 16px;
-
-  -webkit-border-radius: 3px;
-     -moz-border-radius: 3px;
-       -o-border-radius: 3px;
-          border-radius: 3px;
-}
-
-#signUpForm .red {
-  color: red;
-}
-
-#signUpForm input[type=email],
-#signUpForm input[type=password] {
-  width: 100%;
-  font-size: 14px;
-  padding: 5px;
-  border-width: 1px;
-  border-style: solid;
-  border-color: #A3A29D #C6C3B4 #C6C3B4 #A3A29D;
-  outline: none;
-
-  -webkit-border-radius: 3px;
-     -moz-border-radius: 3px;
-       -o-border-radius: 3px;
-          border-radius: 3px;
-
-  -webkit-box-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-     -moz-box-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-       -o-box-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-          box-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-}
-
-#signUpForm input[type=email]:focus,
-#signUpForm input[type=password]:focus {
-  border: 1px solid #549FDC;
-
-  -webkit-border-radius: 0;
-     -moz-border-radius: 0;
-       -o-border-radius: 0;
-          border-radius: 0;
-
-  -webkit-box-shadow: 0 0 0 1px #549FDC inset;
-     -moz-box-shadow: 0 0 0 1px #549FDC inset;
-       -o-box-shadow: 0 0 0 1px #549FDC inset;
-          box-shadow: 0 0 0 1px #549FDC inset;
-}
-
-#signUpForm .submit {
-  height: 28px;
-}
-
-#signUpForm .remember {
-  display: inline-block;
-  line-height: 28px;
-}
-
-#signUpForm .remember .checkAlign {
-  float: left;
-}
-
-#signUpForm .remember label {
-  margin-left: 5px;
-  float: left;
-}
-
-#signUpForm .create {
-  font-size: 14px;
-  height: 28px;
-  padding: 0 10px;
-  float: right;
-}
-
-
diff --git a/browserid/static/dialog/controllers/authenticate_controller.js b/browserid/static/dialog/controllers/authenticate_controller.js
index 0054f37c37efc626e98afa6d5eccc40c7a86089b..84632f5f3008fe152f92d6c27a29a3b22899e047 100644
--- a/browserid/static/dialog/controllers/authenticate_controller.js
+++ b/browserid/static/dialog/controllers/authenticate_controller.js
@@ -40,6 +40,7 @@
   var ANIMATION_TIME = 250,
       bid = BrowserID,
       user = bid.User,
+      errors = bid.Errors,
       validation = bid.Validation,
       lastEmail = "";
 
@@ -60,7 +61,7 @@
       else {
         createUserState.call(self);
       }
-    });
+    }, self.getErrorDialog(errors.isEmailRegistered));
   }
 
   function createUser(el, event) {
@@ -83,7 +84,7 @@
       else {
         // XXX can't register this email address.
       }
-    }, self.getErrorDialog(bid.Errors.createAccount));
+    }, self.getErrorDialog(errors.createUser));
   }
 
   function authenticate(el, event) {
@@ -106,10 +107,7 @@
         } else {
           bid.Tooltip.showTooltip("#cannot_authenticate");
         }
-      }, 
-      self.getErrorDialog(bid.Errors.authentication)
-    );
-
+      }, self.getErrorDialog(errors.authenticate));
   }
 
   function resetPassword(el, event) {
@@ -122,9 +120,7 @@
       self.close("reset_password", {
         email: email
       });
-    }, function() {
-      // XXX TODO error screen!
-    });
+    }, self.getErrorDialog(errors.requestPasswordReset));
   }
 
   function animateSwap(fadeOutSelector, fadeInSelector, callback) {
diff --git a/browserid/static/dialog/controllers/dialog_controller.js b/browserid/static/dialog/controllers/dialog_controller.js
index 491d983fabfa5bced7d4e27424bf97ac90c57715..49560e08de033003c50a668bb273d72ffd141d02 100644
--- a/browserid/static/dialog/controllers/dialog_controller.js
+++ b/browserid/static/dialog/controllers/dialog_controller.js
@@ -42,33 +42,40 @@
 (function() {
   "use strict";
 
-  var user = BrowserID.User;
+  var bid = BrowserID,
+      user = bid.User,
+      errors = bid.Errors,
+      offline = false;
 
   PageController.extend("Dialog", {}, {
       init: function(el) {
         var self=this;
-        //this.element.show();
 
         // keep track of where we are and what we do on success and error
         self.onsuccess = null;
         self.onerror = null;
         setupChannel(self);
         self.stateMachine();
+       
       },
         
       getVerifiedEmail: function(origin_url, onsuccess, onerror) {
-        this.onsuccess = onsuccess;
-        this.onerror = onerror;
+        var self=this;
+        self.onsuccess = onsuccess;
+        self.onerror = onerror;
+
+        if('onLine' in navigator && !navigator.onLine) {
+          self.doOffline();
+          return;
+        }
 
         user.setOrigin(origin_url);
-        
-        // get the cleaned origin.
         $("#sitename").text(user.getHostname());
 
-        this.doCheckAuth();
+        self.doCheckAuth();
 
-        var self=this;
         $(window).bind("unload", function() {
+          bid.Storage.setStagedOnBehalfOf("");
           self.doCancel();
         });
       },
@@ -78,7 +85,15 @@
         var self=this, 
             hub = OpenAjax.hub, 
             el = this.element;
-       
+
+        hub.subscribe("offline", function(msg, info) {
+          self.doOffline();
+        });
+
+        hub.subscribe("xhrError", function(msg, info) {
+          //self.doXHRError(info);
+          // XXX how are we going to handle this?
+        });
 
         hub.subscribe("user_staged", function(msg, info) {
           self.doConfirmUser(info.email);
@@ -100,7 +115,7 @@
         });
 
         hub.subscribe("assertion_generated", function(msg, info) {
-          if(info.assertion !== null) {
+          if (info.assertion !== null) {
             self.doAssertionGenerated(info.assertion);
           }
           else {
@@ -138,6 +153,15 @@
 
       },
 
+      doOffline: function() {
+        this.renderError("wait.ejs", errors.offline);
+        offline = true;
+      },
+
+      doXHRError: function(info) {
+        if (!offline) this.renderError("wait.ejs", errors.offline);  
+      },
+
       doConfirmUser: function(email) {
         this.confirmEmail = email;
 
@@ -150,7 +174,7 @@
 
       doCancel: function() {
         var self=this;
-        if(self.onsuccess) {
+        if (self.onsuccess) {
           self.onsuccess(null);
         }
       },
@@ -182,7 +206,8 @@
       doEmailConfirmed: function() {
         var self=this;
         // yay!  now we need to produce an assertion.
-        user.getAssertion(this.confirmEmail, self.doAssertionGenerated.bind(self));
+        user.getAssertion(this.confirmEmail, self.doAssertionGenerated.bind(self),
+          self.getErrorDialog(errors.getAssertion));
       },
 
       doAssertionGenerated: function(assertion) {
@@ -195,16 +220,16 @@
       },
 
       doNotMe: function() {
-        user.logoutUser(this.doAuthenticate.bind(this));
+        var self=this;
+        user.logoutUser(self.doAuthenticate.bind(self), self.getErrorDialog(errors.logoutUser));
       },
 
       syncEmails: function() {
         var self = this;
         user.syncEmails(self.doPickEmail.bind(self), 
-          self.getErrorDialog(BrowserID.Errors.signIn));
+          self.getErrorDialog(errors.signIn));
       },
 
-
       doCheckAuth: function() {
         var self=this;
         user.checkAuthenticationAndSync(function onSuccess() {}, 
@@ -215,7 +240,7 @@
               self.doAuthenticate();
             }
           }, 
-          self.getErrorDialog(BrowserID.Errors.checkAuthentication));
+          self.getErrorDialog(errors.checkAuthentication));
     }
 
   });
diff --git a/browserid/static/dialog/controllers/page_controller.js b/browserid/static/dialog/controllers/page_controller.js
index f3031469ae8640f8db89a5fa37e374d6c57a3bbb..01ddbb26a0671476402fb333de998bdb32998db5 100644
--- a/browserid/static/dialog/controllers/page_controller.js
+++ b/browserid/static/dialog/controllers/page_controller.js
@@ -48,6 +48,8 @@
       var me=this,
           bodyTemplate = options.bodyTemplate,
           bodyVars = options.bodyVars,
+          errorTemplate = options.errorTemplate,
+          errorVars = options.errorVars,
           waitTemplate = options.waitTemplate,
           waitVars = options.waitVars;
 
@@ -60,6 +62,10 @@
         me.renderWait(waitTemplate, waitVars);
       }
 
+      if(errorTemplate) {
+        me.renderError(errorTemplate, errorVars);
+      }
+
       // XXX move all of these, bleck.
       $("form").bind("submit", me.onSubmit.bind(me));
       $("#cancel").click(me.onCancel.bind(me));
@@ -95,12 +101,12 @@
 
     renderWait: function(body, body_vars) {
       this.renderTemplates("#wait", body, body_vars);
-      $("body").removeClass("error").removeClass("form").addClass("waiting");
-      $("#wait").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME);
+      $("body").removeClass("error").removeClass("form").addClass("waiting").css('opacity', 1);
+      $("#wait").stop().hide().fadeIn(ANIMATION_TIME);
     },
 
-    renderError: function(error_vars) {
-      this.renderTemplates("#error", "wait.ejs", error_vars);
+    renderError: function(body, body_vars) {
+      this.renderTemplates("#error", body, body_vars);
       $("body").removeClass("waiting").removeClass("form").addClass("error");
       $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME);
     },
@@ -144,7 +150,7 @@
      */
     getErrorDialog: function(info) {
       var self=this;
-      return self.renderError.bind(self, info);
+      return self.renderError.bind(self, "wait.ejs", info);
     },
 
     onCancel: function(event) {
diff --git a/browserid/static/dialog/controllers/pickemail_controller.js b/browserid/static/dialog/controllers/pickemail_controller.js
index 43e23513bec1d6b5cfb0a6ca6557291cdea11209..1b3679c132953e12afd296df960419aa5a3cb945 100644
--- a/browserid/static/dialog/controllers/pickemail_controller.js
+++ b/browserid/static/dialog/controllers/pickemail_controller.js
@@ -40,6 +40,7 @@
   var ANIMATION_TIME = 250,
       bid = BrowserID,
       user = bid.User,
+      errors = bid.Errors,
       body = $("body"),
       animationComplete = body.innerWidth() < 640,
       assertion;
@@ -109,13 +110,13 @@
     var self=this;
     user.getAssertion(email, function(assert) {
       assertion = assert || null;
-      tryClose.call(self);
-    });
+      startAnimation.call(self);
+    }, self.getErrorDialog(errors.getAssertion));
   }
 
   function startAnimation() {
+    var self=this;
     if(!animationComplete) {
-      var self=this;
       $("#signIn").animate({"width" : "685px"}, "slow", function () {
         // post animation
          body.delay(500).animate({ "opacity" : "0.5"}, "fast", function () {
@@ -124,6 +125,9 @@
          });
       }); 
     }
+    else {
+      tryClose.call(self);
+    }
 
   }
 
@@ -134,9 +138,7 @@
 
     var valid = checkEmail.call(self, email);
     if (valid) {
-//      self.doWait(bid.Wait.generateKey);
       getAssertion.call(self, email);
-      startAnimation.call(self);
     }
   }
 
@@ -168,7 +170,7 @@
             bid.Tooltip.showTooltip("#could_not_add");
         });
       }
-    });
+    }, self.getErrorDialog(errors.isEmailRegistered));
   }
 
 
diff --git a/browserid/static/dialog/css/m.css b/browserid/static/dialog/css/m.css
index 49f4a561375d7648adab479f5d104dab69b0c9bb..9ddb605ea69a40de8225cbc81420ceecf12ddcb4 100644
--- a/browserid/static/dialog/css/m.css
+++ b/browserid/static/dialog/css/m.css
@@ -111,6 +111,19 @@
       height: 250px;
   }
 
-}
+  #error .vertical {
+    width: auto;
+  }
+  #error .vertical > div {
+    display: block;
+    height: auto;
+    padding: 10px;
+  }
+
+  #error #borderbox {
+    border-left: none;
+    padding: 0;
+  }
+
 
 
diff --git a/browserid/static/dialog/css/popup.css b/browserid/static/dialog/css/popup.css
index b1b54243f2d023bc6e2a46f27d7a621ad3b1ba01..441efacb396edc9e034e195eca1529804a5d9305 100644
--- a/browserid/static/dialog/css/popup.css
+++ b/browserid/static/dialog/css/popup.css
@@ -18,6 +18,7 @@ body {
     font-size: 13px;
     line-height: 21px;
     background-image: url('/i/bg.png');
+    color: #000;
 }
 
 
@@ -118,16 +119,17 @@ section > .contents {
 
 #wait, #error {
     text-align: center;
-    background-image: url("/i/bg.png");
 }
 
 #wait {
     z-index: 1;
+    background-image: url("/i/bg.png");
 }
 
 #error {
     display: none;
     z-index: 2;
+    background-color: #fff;
 }
 
 #wait strong, #error strong {
@@ -135,10 +137,40 @@ section > .contents {
     font-weight: bold;
 }
 
-#error {
-    z-index: 2;
+
+#error .vertical {
+    width: 630px;
+    margin: 0 auto;
+    display: block;
+}
+
+
+#error .vertical > div {
+    display: table-cell;
+    vertical-align: middle;
+    padding: 0 10px;
+    height: 250px;
+}
+
+#error #alternative a {
+    color: #549FDC;
+    text-decoration: underline;
+}
+
+#error #borderbox {
+    border-left: 1px solid #777;
+    padding: 20px 0;
 }
 
+#error #borderbox img {
+    border: none;
+}
+
+#error #alternative .lighter {
+    color: #777;
+}
+
+
 #formWrap {
     background-color: #fff;
     background-image: none;
diff --git a/browserid/static/dialog/qunit.html b/browserid/static/dialog/qunit.html
index 108cc60fd8fbc23e0105384c865a527e58e19b92..7e8189f829a5ff161198b6c2aa4251a536757786 100644
--- a/browserid/static/dialog/qunit.html
+++ b/browserid/static/dialog/qunit.html
@@ -12,23 +12,31 @@
 		<div id="qunit-testrunner-toolbar"></div>
 		<h2 id="qunit-userAgent"></h2>
 		<div id="test-content">
-      <div id="page_controller">
+    </div>
+		<ol id="qunit-tests"></ol>
+		<div id="qunit-test-area"></div>
 
-        <div id="formWrap">
-            <div class="contents"></div>
-        </div>
+    <h3>Content below here is test content that can be ignored</h3>
 
-        <div id="wait">
-            <div class="contents"></div>
-        </div>
+    <div id="controller_head">
 
-        <div id="error">
-            <div class="contents"></div>
-        </div>
+      <div id="formWrap">
+          <div class="contents"></div>
+      </div>
 
+      <div id="wait">
+          <div class="contents"></div>
       </div>
+
+      <div id="error">
+          <div class="contents"></div>
+      </div>
+
+      <span id="email"></span>
+      <span id="cannotconfirm" class="error">Cannot confirm</span>
+      <span id="cannotcommunicate" class="error">Cannot communicate</span>
+      <span id="siteinfo" class="error"><span class="website"></span></span>
+      <span class=".hint">Hint</span>
     </div>
-		<ol id="qunit-tests"></ol>
-		<div id="qunit-test-area"></div>
 	</body>
 </html>
diff --git a/browserid/static/dialog/resources/browser-support.js b/browserid/static/dialog/resources/browser-support.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c2e34818d0e67ec6b20ed76ce0f03ce3404593a
--- /dev/null
+++ b/browserid/static/dialog/resources/browser-support.js
@@ -0,0 +1,118 @@
+/*globals BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.BrowserSupport = (function() {
+  var bid = BrowserID,
+      win = window,
+      nav = navigator,
+      reason;
+
+  // For unit testing
+  function setTestEnv(newNav, newWindow) {
+    nav = newNav;
+    win = newWindow;
+  }
+
+  function getInternetExplorerVersion() {
+    var rv = -1; // Return value assumes failure.
+    if (nav.appName == 'Microsoft Internet Explorer') {
+      var ua = nav.userAgent;
+      var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+      if (re.exec(ua) != null)
+        rv = parseFloat(RegExp.$1);
+    }
+
+    return rv;
+  }
+
+  function checkIE() {
+    var ieVersion = getInternetExplorerVersion(),
+        ieNosupport = ieVersion > -1 && ieVersion < 9;
+
+    if(ieNosupport) {
+      return "IE_VERSION";
+    }
+  }
+
+  function explicitNosupport() {
+    return checkIE();
+  }
+
+  function checkLocalStorage() {
+    var localStorage = 'localStorage' in win && win['localStorage'] !== null;
+    if(!localStorage) {
+      return "LOCALSTORAGE";
+    }
+  }
+
+  function checkPostMessage() {
+    if(!win.postMessage) {
+      return "POSTMESSAGE";
+    }
+  }
+
+  function isSupported() {
+    reason = checkLocalStorage() || checkPostMessage() || explicitNosupport();
+
+    return !reason;
+  }
+
+  function getNoSupportReason() {
+    return reason;
+  }
+
+  return {
+    /**
+     * Set the test environment.
+     * @method setTestEnv
+     */
+    setTestEnv: setTestEnv,
+    /**
+     * Check whether the current browser is supported
+     * @method isSupported
+     * @returns {boolean}
+     */
+    isSupported: isSupported,
+    /**
+     * Called after isSupported, if isSupported returns false.  Gets the reason 
+     * why browser is not supported.
+     * @method getNoSupportReason
+     * @returns {string}
+     */
+    getNoSupportReason: getNoSupportReason
+  };
+  
+}());
+
diff --git a/browserid/static/dialog/resources/error-messages.js b/browserid/static/dialog/resources/error-messages.js
index 860760fb88febb0e2645a94dda23b3cf7f314b5f..7064d82d9f4a1fdaaccd01341763679ab4a412e0 100644
--- a/browserid/static/dialog/resources/error-messages.js
+++ b/browserid/static/dialog/resources/error-messages.js
@@ -36,7 +36,7 @@ BrowserID.Errors = (function(){
   "use strict";
 
   var Errors = {
-    authentication: {
+    authenticate: {
       type: "serverError",
       title: "Error Authenticating",
       message: "There was a technical problem while trying to log you in.  Yucky!"
@@ -54,18 +54,48 @@ BrowserID.Errors = (function(){
       message: "There was a technical problem while trying to log you in.  Yucky!"
     },
 
-    createAccount: {
+    createUser: {
       type: "serverError",
       title: "Error Creating Account",
       message: "There was a technical problem while trying to create your account.  Yucky!"
     },
 
+    getAssertion: {
+      type: "serverError",
+      title: "Error Getting Assertion",
+      message: "There was a technical problem while trying to authenticate you.  Yucky!"
+    },
+
+    isEmailRegistered: {
+      type: "serverError",
+      title: "Error Checking Email Address",
+      message: "There was a technical problem while trying to check that email address.  Yucky!"
+    },
+
+    logoutUser: {
+      type: "serverError",
+      title: "Logout Failed",
+      message: "An error was encountered while signing you out.  Yucky!"
+    },
+
+    offline: {
+      type: "networkError",
+      title: "You are offline!",
+      message: "Unfortunately, BrowserID cannot communicate while offline!"
+    },
+
     registration: {
       type: "serverError",
       title: "Registration Failed",
       message: "An error was encountered and the signup cannot be completed.  Yucky!"
     },
 
+    requestPasswordReset: {
+      type: "serverError",
+      title: "Error Resetting Password",
+      message: "There was a technical problem while trying to reset your password."
+    },
+
     signIn: {
       type: "serverError",
       title: "Signin Failed",
diff --git a/browserid/static/dialog/resources/network.js b/browserid/static/dialog/resources/network.js
index 18d04125647bf441df9d2ce293c4c617db293a25..6f012b6e6e2d2a2c7b4fd9ab12e03af7ed449f76 100644
--- a/browserid/static/dialog/resources/network.js
+++ b/browserid/static/dialog/resources/network.js
@@ -37,37 +37,13 @@
 BrowserID.Network = (function() {
   "use strict";
 
-  var csrf_token;
-  var xhr = $;
-  var server_time;
-  var auth_status;
+  var csrf_token,
+      xhr = $,
+      server_time,
+      auth_status,
+      hub = window.OpenAjax && OpenAjax.hub;
 
-  function withContext(cb) {
-    if (typeof auth_status === 'boolean' && typeof csrf_token !== 'undefined') cb();
-    else {
-      xhr.ajax({
-        url: "/wsapi/session_context",
-        type: "GET",
-        success: function(result) {
-          csrf_token = result.csrf_token;
-          server_time = {
-            remote: result.server_time,
-            local: (new Date()).getTime()
-          };
-          auth_status = result.authenticated;
-          _.defer(cb);
-        },
-        dataType: "json"
-      });
-    }
-  }
-
-  function clearContext() {
-    var undef;
-    csrf_token = server_time = auth_status = undef;
-  }
-
-  function createDeferred(cb) {
+  function deferResponse(cb) {
     if (cb) {
       return function() {
         var args = _.toArray(arguments);
@@ -78,6 +54,26 @@ BrowserID.Network = (function() {
     }
   }
 
+  function xhrError(cb, errorMessage) {
+    return function() {
+      if (cb) cb();
+      hub && hub.publish("xhrError", errorMessage);
+    };
+  }
+
+  function get(options) {
+    xhr.ajax({
+      type: "GET",
+      url: options.url,
+      // We defer the responses because otherwise jQuery eats any exceptions 
+      // that are thrown in the response handlers and it becomes very difficult 
+      // to debug.
+      success: deferResponse(options.success),
+      error: deferResponse(xhrError(options.error, options.errorMessage)),
+      dataType: "json"
+    });
+  }
+
   function post(options) {
     withContext(function() {
       var data = options.data || {};
@@ -90,12 +86,44 @@ BrowserID.Network = (function() {
         type: "POST",
         url: options.url,
         data: data,
-        success: options.success,
-        error: options.error
+        // We defer the responses because otherwise jQuery eats any exceptions 
+        // that are thrown in the response handlers and it becomes very difficult 
+        // to debug.
+        success: deferResponse(options.success),
+        error: deferResponse(xhrError(options.error, options.errorMessage))
       });
-    });
+    }, options.error);
   }
 
+  function withContext(cb, onFailure) {
+    if (typeof auth_status === 'boolean' && typeof csrf_token !== 'undefined') cb();
+    else {
+      xhr.ajax({
+        url: "/wsapi/session_context",
+        success: function(result) {
+          csrf_token = result.csrf_token;
+          server_time = {
+            remote: result.server_time,
+            local: (new Date()).getTime()
+          };
+          auth_status = result.authenticated;
+          cb();
+        },
+        error: deferResponse(xhrError(onFailure))
+      });
+    }
+  }
+
+  function clearContext() {
+    var undef;
+    csrf_token = server_time = auth_status = undef;
+  }
+
+  // Not really part of the Network API, but related to networking
+  $(document).bind("offline", function() {
+    hub.publish("offline");
+  });
+
   var Network = {
     /**
      * Set the XHR object and clear all context info.  Used for testing.
@@ -133,7 +161,7 @@ BrowserID.Network = (function() {
               // session, let's set it to perhaps save a network request
               // (to fetch session context).
               auth_status = authenticated;
-              _.delay(onSuccess, 0, authenticated);
+              if(onSuccess) onSuccess(authenticated);
             } catch (e) {
               onFailure("unexpected server response: " + e);
             }
@@ -154,19 +182,20 @@ BrowserID.Network = (function() {
       withContext(function() {
         try {
           if (typeof auth_status !== 'boolean') throw "can't get authentication status!";
-          _.delay(onSuccess, 0, auth_status);
+          if (onSuccess) onSuccess(auth_status);
         } catch(e) {
           if (onFailure) onFailure(e.toString());
         }
-      });
+      }, onFailure);
     },
 
     /**
      * Log the authenticated user out
      * @method logout
      * @param {function} [onSuccess] - called on completion
+     * @param {function} [onFailure] - Called on XHR failure.
      */
-    logout: function(onSuccess) {
+    logout: function(onSuccess, onFailure) {
       post({
         url: "/wsapi/logout",
         success: function() {
@@ -176,8 +205,9 @@ BrowserID.Network = (function() {
           // FIXME: we should return a confirmation that the
           // user was successfully logged out.
           auth_status = false;
-          if (onSuccess) _.defer(onSuccess);
-        }
+          if (onSuccess) onSuccess();
+        },
+        error: onFailure
       });
     },
 
@@ -197,10 +227,7 @@ BrowserID.Network = (function() {
           site : origin
         },
         success: function(status) {
-          var staged = status.success;
-          // why a delay here? Because of the test harness?
-          // shouldn't the delay be in the test harness?
-          _.delay(onSuccess, 0, staged);
+          if (onSuccess) onSuccess(status.success);
         },
         error: onFailure
       });
@@ -215,10 +242,10 @@ BrowserID.Network = (function() {
      * I think so (BA).
      */
     emailForVerificationToken: function(token, onSuccess, onFailure) {
-      xhr.ajax({
+      get({
         url : "/wsapi/email_for_token?token=" + encodeURIComponent(token),
         success: function(data) {
-          onSuccess(data.email);
+          if (onSuccess) onSuccess(data.email);
         },
         error: onFailure
       });
@@ -231,12 +258,10 @@ BrowserID.Network = (function() {
      * @param {function} [onFailure] - Called on XHR failure.
      */
     checkUserRegistration: function(email, onSuccess, onFailure) {
-      xhr.ajax({
+      get({
         url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email),
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            _.delay(onSuccess, 0, status.status);
-          }
+          if (onSuccess) onSuccess(status.status);
         },
         error: onFailure
       });
@@ -258,9 +283,28 @@ BrowserID.Network = (function() {
           pass: password
         },
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            _.delay(onSuccess, 0, status.success);
-          }
+          if (onSuccess) onSuccess(status.success);
+        },
+        error: onFailure
+      });
+    },
+
+    /**
+     * Call with a token to prove an email address ownership.
+     * @method completeEmailRegistration
+     * @param {string} token - token proving email ownership.
+     * @param {function} [onSuccess] - Callback to call when complete.  Called 
+     * with one boolean parameter that specifies the validity of the token.
+     * @param {function} [onFailure] - Called on XHR failure.
+     */
+    completeEmailRegistration: function(token, onSuccess, onFailure) {
+      post({
+        url: "/wsapi/complete_email_addition",
+        data: {
+          token: token
+        },
+        success: function(status, textStatus, jqXHR) {
+          if (onSuccess) onSuccess(status.success);
         },
         error: onFailure
       });
@@ -275,7 +319,7 @@ BrowserID.Network = (function() {
      */
     requestPasswordReset: function(email, origin, onSuccess, onFailure) {
       if (email) {
-        this.createUser(email, origin, onSuccess, onFailure);
+        Network.createUser(email, origin, onSuccess, onFailure);
       } else {
         // TODO: if no email is provided, then what?
         throw "no email provided to password reset";
@@ -291,9 +335,7 @@ BrowserID.Network = (function() {
      */ 
     resetPassword: function(password, onSuccess, onFailure) {
       // XXX fill this in.
-      if (onSuccess) {
-        _.defer(onSuccess);
-      }
+      if (onSuccess) onSuccess();
     },
 
     /**
@@ -308,32 +350,10 @@ BrowserID.Network = (function() {
     changePassword: function(oldPassword, newPassword, onSuccess, onFailure) {
       // XXX fill this in
       if (onSuccess) {
-        _.delay(onSuccess, 0, true);
+        onSuccess(true);
       }
     },
 
-    /**
-     * Call with a token to prove an email address ownership.
-     * @method completeEmailRegistration
-     * @param {string} token - token proving email ownership.
-     * @param {function} [onSuccess] - Callback to call when complete.  Called 
-     * with one boolean parameter that specifies the validity of the token.
-     * @param {function} [onFailure] - Called on XHR failure.
-     */
-    completeEmailRegistration: function(token, onSuccess, onFailure) {
-      post({
-        url: "/wsapi/complete_email_addition",
-        data: {
-          token: token
-        },
-        success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            _.delay(onSuccess, 0, status.success);
-          }
-        },
-        error: onFailure
-      });
-    },
 
     /**
      * Cancel the current user"s account.
@@ -344,7 +364,7 @@ BrowserID.Network = (function() {
     cancelUser: function(onSuccess, onFailure) {
       post({
         url: "/wsapi/account_cancel",
-        success: createDeferred(onSuccess),
+        success: onSuccess,
         error: onFailure
       });
     },
@@ -365,8 +385,7 @@ BrowserID.Network = (function() {
           site: origin
         },
         success: function(status) {
-          var staged = status.success;
-          _.delay(onSuccess, 0, staged);
+          if (onSuccess) onSuccess(status.success);
         },
         error: onFailure
       });
@@ -380,12 +399,10 @@ BrowserID.Network = (function() {
      * @param {function} [onfailure] - called on xhr failure.
      */
     checkEmailRegistration: function(email, onSuccess, onFailure) {
-      xhr.ajax({
+      get({
         url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email),
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            _.delay(onSuccess, 0, status.status);
-          }
+          if (onSuccess) onSuccess(status.status);
         },
         error: onFailure
       });
@@ -401,12 +418,10 @@ BrowserID.Network = (function() {
      * @param {function} [onFailure] - Called on XHR failure.
      */
     emailRegistered: function(email, onSuccess, onFailure) {
-      xhr.ajax({
+      get({
         url: "/wsapi/have_email?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
-          if(onSuccess) {
-            _.delay(onSuccess, 0, data.email_known);
-          }
+          if(onSuccess) onSuccess(data.email_known);
         },
         error: onFailure
       });
@@ -426,11 +441,9 @@ BrowserID.Network = (function() {
           email: email
         },
         success: function(status, textStatus, jqXHR) {
-          if (onSuccess) {
-            _.delay(onSuccess, 0, status.success);
-          }
+          if (onSuccess) onSuccess(status.success);
         },
-        failure: onFailure
+        error: onFailure
       });
     },
 
@@ -438,15 +451,15 @@ BrowserID.Network = (function() {
      * Certify the public key for the email address.
      * @method certKey
      */
-    certKey: function(email, pubkey, onSuccess, onError) {
+    certKey: function(email, pubkey, onSuccess, onFailure) {
       post({
         url: "/wsapi/cert_key",
         data: {
           email: email,
           pubkey: pubkey.serialize()
         },
-        success: createDeferred(onSuccess),
-        error: onError
+        success: onSuccess,
+        error: onFailure
       });
     },
 
@@ -455,10 +468,9 @@ BrowserID.Network = (function() {
      * @method listEmails
      */
     listEmails: function(onSuccess, onFailure) {
-      xhr.ajax({
-        type: "GET",
+      get({
         url: "/wsapi/list_emails",
-        success: createDeferred(onSuccess),
+        success: onSuccess,
         error: onFailure
       });
     },
@@ -482,7 +494,7 @@ BrowserID.Network = (function() {
         } catch(e) {
           onFailure(e.toString());
         }
-      });
+      }, onFailure);
     }
   };
 
diff --git a/browserid/static/dialog/resources/user.js b/browserid/static/dialog/resources/user.js
index 27105f7f4e798961bd9669063cb850501671b9d1..3fde2a9a9eb795b03e40e2685abd70266e544417 100644
--- a/browserid/static/dialog/resources/user.js
+++ b/browserid/static/dialog/resources/user.js
@@ -108,6 +108,10 @@ BrowserID.User = (function() {
         //   'mustAuth' - user must authenticate
         //   'noRegistration' - no registration is in progress
         if (status === "complete" || status === "mustAuth") {
+          // As soon as the registration comes back as complete, we should 
+          // ensure that the stagedOnBehalfOf is cleared so there is no stale 
+          // data.
+          storage.setStagedOnBehalfOf("");
           if (onSuccess) {
             onSuccess(status);
           }
@@ -118,7 +122,7 @@ BrowserID.User = (function() {
         else if (onFailure) {
             onFailure(status);
         }
-      });
+      }, onFailure);
     };
 
     poll();
@@ -230,7 +234,7 @@ BrowserID.User = (function() {
       var self=this;
 
       // remember this for later
-      storage.setStagedOnBehalfOf(origin);
+      storage.setStagedOnBehalfOf(self.getHostname());
       
       network.createUser(email, origin, function(created) {
         if (onSuccess) {
@@ -250,6 +254,35 @@ BrowserID.User = (function() {
       registrationPoll(network.checkUserRegistration, email, onSuccess, onFailure);
     },
 
+    /**
+     * Verify a user
+     * @method verifyUser
+     * @param {string} token - token to verify.
+     * @param {string} password - password to set for account.
+     * @param {function} [onSuccess] - Called to give status updates.
+     * @param {function} [onFailure] - Called on error.
+     */
+    verifyUser: function(token, password, onSuccess, onFailure) {
+      network.emailForVerificationToken(token, function (email) {
+        var invalidInfo = { valid: false };
+        if (email) {
+          network.completeUserRegistration(token, password, function (valid) {
+            var info = valid ? {
+              valid: valid,
+              email: email,
+              origin: storage.getStagedOnBehalfOf()
+            } : invalidInfo;
+
+            storage.setStagedOnBehalfOf("");
+
+            if (onSuccess) onSuccess(info);
+          }, onFailure);
+        } else if(onSuccess) {
+          onSuccess(invalidInfo);
+        }
+      }, onFailure);
+    },
+
     /**
      * Set the password of the current user.
      * @method setPassword
@@ -277,7 +310,7 @@ BrowserID.User = (function() {
      * identity.
      * @method cancelUser
      * @param {function} [onSuccess] - Called whenever complete.
-     * @param {function} [onFailure] - called on failure.
+     * @param {function} [onFailure] - called on error.
      */
     cancelUser: function(onSuccess, onFailure) {
       network.cancelUser(function() {
@@ -285,7 +318,7 @@ BrowserID.User = (function() {
         if (onSuccess) {
           onSuccess();
         }
-      });
+      }, onFailure);
 
     },
 
@@ -293,7 +326,7 @@ BrowserID.User = (function() {
      * Log the current user out.
      * @method logoutUser
      * @param {function} [onSuccess] - Called whenever complete.
-     * @param {function} [onFailure] - called on failure.
+     * @param {function} [onFailure] - called on error.
      */
     logoutUser: function(onSuccess, onFailure) {
       network.logout(function() {
@@ -301,7 +334,7 @@ BrowserID.User = (function() {
         if (onSuccess) {
           onSuccess();
         }
-      });
+      }, onFailure);
     },
 
     /**
@@ -309,7 +342,7 @@ BrowserID.User = (function() {
      * be called.
      * @method syncEmails
      * @param {function} [onSuccess] - Called whenever complete.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     syncEmails: function(onSuccess, onFailure) {
       cleanupIdentities();
@@ -356,7 +389,7 @@ BrowserID.User = (function() {
         }
 
         addNextEmail();
-      });
+      }, onFailure);
     },
 
     /**
@@ -365,7 +398,7 @@ BrowserID.User = (function() {
      * @param {function} [onSuccess] - Called when check is complete with one 
      * boolean parameter, authenticated.  authenticated will be true if user is 
      * authenticated, false otw.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     checkAuthentication: function(onSuccess, onFailure) {
       network.checkAuth(function(authenticated) {
@@ -384,7 +417,7 @@ BrowserID.User = (function() {
      * but before sync starts.  Useful for displaying status messages about the 
      * sync taking a moment.
      * @param {function} [onComplete] - Called on sync completion.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     checkAuthenticationAndSync: function(onSuccess, onComplete, onFailure) {
       var self=this;
@@ -413,7 +446,7 @@ BrowserID.User = (function() {
      * @param {string} email - Email address to authenticate.
      * @param {string} password - Password.
      * @param {function} [onComplete] - Called on sync completion.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     authenticate: function(email, password, onComplete, onFailure) {
       var self=this;
@@ -452,6 +485,7 @@ BrowserID.User = (function() {
       var self = this;
       network.addEmail(email, origin, function(added) {
         if (added) {
+          storage.setStagedOnBehalfOf(self.getHostname());
           // we no longer send the keypair, since we will certify it later.
           if (onSuccess) {
             onSuccess(added);
@@ -471,12 +505,42 @@ BrowserID.User = (function() {
       registrationPoll(network.checkEmailRegistration, email, onSuccess, onFailure);
     },
 
+    /**
+     * Verify a users email address given by the token
+     * @method verifyEmail
+     * @param {string} token
+     * @param {function} [onSuccess] - Called on success.
+     *   Called with an object with valid, email, and origin if valid, called 
+     *   with only valid otw.
+     * @param {function} [onFailure] - Called on error.
+     */
+    verifyEmail: function(token, onSuccess, onFailure) {
+      network.emailForVerificationToken(token, function (email) {
+        var invalidInfo = { valid: false };
+        if (email) {
+          network.completeEmailRegistration(token, function (valid) {
+            var info = valid ? {
+              valid: valid,
+              email: email,
+              origin: storage.getStagedOnBehalfOf()
+            } : invalidInfo;
+
+            storage.setStagedOnBehalfOf("");
+
+            if (onSuccess) onSuccess(info);
+          }, onFailure);
+        } else if(onSuccess) {
+          onSuccess(invalidInfo);
+        }
+      }, onFailure);
+    },
+
     /**
      * Remove an email address.
      * @method removeEmail
      * @param {string} email - Email address to remove.
      * @param {function} [onSuccess] - Called when complete.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     removeEmail: function(email, onSuccess, onFailure) {
       if(storage.getEmail(email)) {
@@ -513,7 +577,7 @@ BrowserID.User = (function() {
      * @method getAssertion
      * @param {string} email - Email to get assertion for.
      * @param {function} [onSuccess] - Called with assertion on success.
-     * @param {function} [onFailure] - Called on failure.
+     * @param {function} [onFailure] - Called on error.
      */
     getAssertion: function(email, onSuccess, onFailure) {
       // we use the current time from the browserid servers
@@ -533,7 +597,7 @@ BrowserID.User = (function() {
             if (onSuccess) {
               onSuccess(assertion);
             }
-          });
+          }, onFailure);
         }
 
         if (storedID) {
diff --git a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js
index fca829d5c917981be1272ff7ee2148d1f089fa5c..e2df4dbb6a159fc4aa78c73b6a2e9c5842885996 100644
--- a/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js
+++ b/browserid/static/dialog/test/qunit/controllers/page_controller_unit_test.js
@@ -41,16 +41,21 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
       bodyTemplate = "testBodyTemplate.ejs",
       waitTemplate = "wait.ejs";
 
+  function reset() {
+    el = $("#controller_head");
+    el.find("#formWrap .contents").html("");
+    el.find("#wait .contents").html("");
+    el.find("#error .contents").html("");
+  }
+
   module("PageController", {
     setup: function() {
-      el = $("#page_controller");
+      reset();
     },
 
     teardown: function() {
-      el.find("#formWrap .contents").html("");
-      el.find("#wait .contents").html("");
-      el.find("#error .contents").html("");
       controller.destroy();
+      reset();
     } 
   });
 
@@ -76,10 +81,11 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     var html = el.find("#formWrap .contents").html();
     ok(html.length, "with template specified, form text is loaded");
 
+/*
 
     var input = el.find("input").eq(0);
     ok(input.is(":focus"), "make sure the first input is focused");
-
+*/
     html = el.find("#wait .contents").html();
     equal(html, "", "with body template specified, wait text is not loaded");
   });
@@ -100,6 +106,22 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     ok(html.length, "with wait template specified, wait text is loaded");
   });
 
+  test("page controller with error template renders in #error .contents", function() {
+    controller = el.page({
+      errorTemplate: waitTemplate,
+      errorVars: {
+        title: "Test title",
+        message: "Test message"
+      }
+    }).controller();
+
+    var html = el.find("#formWrap .contents").html();
+    equal(html, "", "with error template specified, form is ignored");
+
+    html = el.find("#error .contents").html();
+    ok(html.length, "with error template specified, error text is loaded");
+  });
+
   test("renderError renders an error message", function() {
     controller = el.page({
       waitTemplate: waitTemplate,
@@ -109,7 +131,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
       }
     }).controller();
    
-    controller.renderError({
+    controller.renderError("wait.ejs", {
       title: "error title",
       message: "error message"
     });
diff --git a/browserid/static/dialog/test/qunit/include_unit_test.js b/browserid/static/dialog/test/qunit/include_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..815ecf82138ce58855bffdae94d8639568269a97
--- /dev/null
+++ b/browserid/static/dialog/test/qunit/include_unit_test.js
@@ -0,0 +1,52 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then("/include.js", function() {
+  "use strict";
+
+  module("include.js");
+  
+  test("navigator.id is available", function() {
+    equal(typeof navigator.id, "object", "navigator.id namespace is available");
+  });
+
+  test("navigator.id.getVerifiedEmail is available", function() {
+    equal(typeof navigator.id.getVerifiedEmail, "function", "navigator.id.getVerifiedEmail is available");
+  });
+
+
+});
+
diff --git a/browserid/static/dialog/test/qunit/pages/add_email_address_test.js b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..1d0d0a016d7f9b44515d643b18d6f6f0c53672d5
--- /dev/null
+++ b/browserid/static/dialog/test/qunit/pages/add_email_address_test.js
@@ -0,0 +1,112 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery").then("/js/pages/add_email_address", function() {
+  "use strict";
+
+  var bid = BrowserID,
+      network = bid.Network,
+      storage = bid.Storage,
+      emailForVerificationTokenFailure = false,
+      completeEmailRegistrationFailure = false,
+      validToken = true;
+  
+  var netMock = {
+    emailForVerificationToken: function(token, onSuccess, onFailure) {
+      emailForVerificationTokenFailure ? onFailure() : onSuccess("testuser@testuser.com");
+    },
+
+    completeEmailRegistration: function(token, onSuccess, onFailure) {
+      completeEmailRegistrationFailure ? onFailure() : onSuccess(validToken);
+    }
+  };
+
+  module("pages/add_email_address", {
+    setup: function() {
+      BrowserID.User.setNetwork(netMock);  
+      emailForVerificationTokenFailure = completeEmailRegistrationFailure = false;
+      validToken = true;
+      $(".error").stop().hide();
+      $(".website").text("");
+    },
+    teardown: function() {
+      BrowserID.User.setNetwork(network);  
+      $(".error").stop().hide();
+      $(".website").text("");
+    }
+  });
+
+  test("addEmailAddress with good token and site", function() {
+    storage.setStagedOnBehalfOf("browserid.org");
+
+    bid.addEmailAddress("token");
+    
+    equal($("#email").text(), "testuser@testuser.com", "email set");
+    ok($("#siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
+    equal($("#siteinfo .website").text(), "browserid.org", "origin is updated");
+  });
+
+  test("addEmailAddress with good token and nosite", function() {
+    bid.addEmailAddress("token");
+    
+    equal($("#email").text(), "testuser@testuser.com", "email set");
+    equal($("#siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
+    equal($("#siteinfo .website").text(), "", "origin is not updated");
+  });
+
+  test("addEmailAddress with bad token", function() {
+    validToken = false;
+
+    bid.addEmailAddress("token");
+    ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
+  });
+
+  test("addEmailAddress with emailForVerficationToken XHR failure", function() {
+    validToken = true;
+    emailForVerificationTokenFailure = true;
+    bid.addEmailAddress("token");
+
+    ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+  });
+
+  test("addEmailAddress with completeEmailRegistration XHR failure", function() {
+    validToken = true;
+    completeEmailRegistrationFailure = true;
+    bid.addEmailAddress("token");
+
+    ok($("#cannotcommunicate").is(":visible"), "cannot communicate box is visible");
+  });
+});
diff --git a/browserid/static/dialog/test/qunit/qunit.js b/browserid/static/dialog/test/qunit/qunit.js
index 435935bdb970b11177eb07a37efc7aaa60a63e99..450add4ea320c385bc56072036d2781843ffaccb 100644
--- a/browserid/static/dialog/test/qunit/qunit.js
+++ b/browserid/static/dialog/test/qunit/qunit.js
@@ -1,4 +1,5 @@
 steal("/dialog/resources/browserid.js",
+      "/dialog/resources/browser-support.js",
       "/dialog/resources/storage.js",
       "/dialog/resources/tooltip.js",
       "/dialog/resources/validation.js",
@@ -12,9 +13,15 @@ steal("/dialog/resources/browserid.js",
     "funcunit/qunit")
 	.views('testBodyTemplate.ejs')
 	.views('wait.ejs')
+  .then("/dialog/controllers/page_controller.js")
   .then("browserid_unit_test")
-  .then("controllers/page_controller_unit_test")
+  .then("include_unit_test")
+  .then("pages/add_email_address_test")
+  .then("resources/browser-support_unit_test")
   .then("resources/validation_unit_test")
   .then("resources/storage_unit_test")
   .then("resources/network_unit_test")
   .then("resources/user_unit_test")
+  .then("controllers/page_controller_unit_test")
+  .then("controllers/page_controller_unit_test")
+
diff --git a/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..a26857f01eef422ad03c0cc49fbb4d9257957cef
--- /dev/null
+++ b/browserid/static/dialog/test/qunit/resources/browser-support_unit_test.js
@@ -0,0 +1,102 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.plugins("jquery", "funcunit/qunit").then(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      support = bid.BrowserSupport,
+      stubWindow,
+      stubNavigator;
+
+  module("browser-support", {
+    setup: function() {
+      // Hard coded goodness for testing purposes
+      stubNavigator = {
+        appName: "Netscape",
+        userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:7.0.1) Gecko/20100101 Firefox/7.0.1"
+      };
+
+      stubWindow = {
+        localStorage: {},
+        postMessage: function() {}
+      };
+
+      support.setTestEnv(stubNavigator, stubWindow);
+    },
+
+    teardown: function() {
+    }
+  });
+  
+  test("browser without localStorage", function() {
+    delete stubWindow.localStorage;
+
+    equal(support.isSupported(), false, "window.localStorage is required");
+    equal(support.getNoSupportReason(), "LOCALSTORAGE", "correct reason");
+  });
+
+
+  test("browser without postMessage", function() {
+    delete stubWindow.postMessage;
+
+    equal(support.isSupported(), false, "window.postMessage is required");
+    equal(support.getNoSupportReason(), "POSTMESSAGE", "correct reason");
+  });
+
+  test("Fake being IE8 - unsupported intentionally", function() {
+    stubNavigator.appName = "Microsoft Internet Explorer";
+    stubNavigator.userAgent = "MSIE 8.0";
+
+    equal(support.isSupported(), false, "IE8 is not supported");
+    equal(support.getNoSupportReason(), "IE_VERSION", "correct reason");
+  });
+
+  test("Fake being IE9 - supported", function() {
+    stubNavigator.appName = "Microsoft Internet Explorer";
+    stubNavigator.userAgent = "MSIE 9.0";
+
+    equal(support.isSupported(), true, "IE9 is supported");
+    equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good");
+  });
+
+  test("Firefox 7.01 with postMessage, localStorage", function() {
+    equal(support.isSupported(), true, "Firefox 7.01 is supported");
+    equal(typeof support.getNoSupportReason(), "undefined", "no reason, we are all good");
+  });
+});
+
+
diff --git a/browserid/static/dialog/test/qunit/resources/network_unit_test.js b/browserid/static/dialog/test/qunit/resources/network_unit_test.js
index 1f532b24b13568ae906990835a177272fa542cd6..ac5b06c785ffba87efe2ea7f3d2b3404aefb6b16 100644
--- a/browserid/static/dialog/test/qunit/resources/network_unit_test.js
+++ b/browserid/static/dialog/test/qunit/resources/network_unit_test.js
@@ -51,6 +51,52 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     start();
   }
 
+  function notificationCheck(cb) {
+    // Take the original arguments, take off the function.  Add any additional 
+    // arguments that were passed in, and then tack on the onSuccess and 
+    // onFailure to the end.  Then call the callback.
+    var args = Array.prototype.slice.call(arguments, 1);
+
+    xhr.useResult("ajaxError");
+
+    var handle;
+
+    var subscriber = function() {
+      ok(true, "xhr error notified application");
+      wrappedStart();
+      OpenAjax.hub.unsubscribe(handle);
+    };
+
+    handle = OpenAjax.hub.subscribe("xhrError", subscriber);
+
+    if (cb) {
+      cb.apply(null, args);
+    }
+
+    stop();
+  }
+
+  function failureCheck(cb) {
+    // Take the original arguments, take off the function.  Add any additional 
+    // arguments that were passed in, and then tack on the onSuccess and 
+    // onFailure to the end.  Then call the callback.
+    var args = Array.prototype.slice.call(arguments, 1);
+    
+    args.push(function onSuccess(authenticated) {
+      ok(false, "XHR failure should never pass");
+      wrappedStart();
+    }, function onFailure() {
+      ok(true, "XHR failure should never pass");
+      wrappedStart();
+    });
+
+    xhr.useResult("ajaxError");
+
+    cb.apply(null, args);
+
+    stop();
+  }
+
   var network = BrowserID.Network,
       contextInfo = {
         server_time: new Date().getTime(),
@@ -59,33 +105,54 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
       };
 
 
+  /**
+   * This is the results table, the keys are the request type, url, and 
+   * a "selector" for testing.  The right is the expected return value, already 
+   * decoded.  If a result is "undefined", the request's error handler will be 
+   * called.
+   */
   var xhr = {
     results: {
-      "get /wsapi/session_context valid": contextInfo,
+      "get /wsapi/session_context valid": contextInfo,   
       "get /wsapi/session_context invalid": contextInfo,
+      // We are going to test for XHR failures for session_context using 
+      // call to serverTime.  We are going to use the flag contextAjaxError
+      "get /wsapi/session_context ajaxError": contextInfo, 
+      "get /wsapi/session_context contextAjaxError": undefined,  
       "post /wsapi/authenticate_user valid": { success: true },
       "post /wsapi/authenticate_user invalid": { success: false },
+      "post /wsapi/authenticate_user ajaxError": undefined,
       "post /wsapi/complete_email_addition valid": { success: true },
       "post /wsapi/complete_email_addition invalid": { success: false },
+      "post /wsapi/complete_email_addition ajaxError": undefined,
       "post /wsapi/stage_user valid": { success: true },
       "post /wsapi/stage_user invalid": { success: false },
+      "post /wsapi/stage_user ajaxError": undefined,
       "get /wsapi/user_creation_status?email=address notcreated": undefined, // undefined because server returns 400 error
       "get /wsapi/user_creation_status?email=address pending": { status: "pending" },
       "get /wsapi/user_creation_status?email=address complete": { status: "complete" },
+      "get /wsapi/user_creation_status?email=address ajaxError": undefined,
       "post /wsapi/complete_user_creation valid": { success: true },
       "post /wsapi/complete_user_creation invalid": { success: false },
+      "post /wsapi/complete_user_creation ajaxError": undefined,
       "post /wsapi/logout valid": { success: true },
+      "post /wsapi/logout ajaxError": undefined,
       "get /wsapi/have_email?email=address taken": { email_known: true },
       "get /wsapi/have_email?email=address nottaken" : { email_known: false },
+      "get /wsapi/have_email?email=address ajaxError" : undefined,
       "post /wsapi/remove_email valid": { success: true },
       "post /wsapi/remove_email invalid": { success: false },
+      "post /wsapi/remove_email ajaxError": undefined,
       "post /wsapi/account_cancel valid": { success: true },
       "post /wsapi/account_cancel invalid": { success: false },
+      "post /wsapi/account_cancel ajaxError": undefined,
       "post /wsapi/stage_email valid": { success: true },
       "post /wsapi/stage_email invalid": { success: false },
+      "post /wsapi/stage_email ajaxError": undefined,
       "get /wsapi/email_addition_status?email=address notcreated": undefined, // undefined because server returns 400 error
       "get /wsapi/email_addition_status?email=address pending": { status: "pending" },
       "get /wsapi/email_addition_status?email=address complete": { status: "complete" },
+      "get /wsapi/email_addition_status?email=address ajaxError": undefined
     },
 
     useResult: function(result) {
@@ -164,6 +231,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("authenticate with XHR failure, checking whether application is notified", function() {
+    notificationCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
+  });
+
+  wrappedAsyncTest("authenticate with XHR failure after context already setup", function() {
+    failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
+  });
+
 
   wrappedAsyncTest("checkAuth with valid authentication", function() {
     contextInfo.authenticated = true;
@@ -194,6 +269,26 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
 
+
+  wrappedAsyncTest("checkAuth with XHR failure", function() {
+    xhr.useResult("ajaxError");
+    contextInfo.authenticated = false;
+
+    // Do not convert this to failureCheck, we do this manually because 
+    // checkAuth does not make an XHR request.  Since it does not make an XHR 
+    // request, we do not test whether the app is notified of an XHR failure
+    network.checkAuth(function onSuccess() {
+      ok(true, "checkAuth does not make an ajax call, all good");
+      wrappedStart();
+    }, function onFailure() {
+      ok(false, "checkAuth does not make an ajax call, should not fail");
+      wrappedStart();
+    });
+
+    stop();
+  });
+
+
   wrappedAsyncTest("logout", function() {
     network.logout(function onSuccess() {
       ok(true, "we can logout");
@@ -207,6 +302,15 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   });
 
 
+  wrappedAsyncTest("logout with XHR failure", function() {
+    notificationCheck(network.logout);
+  });
+
+  wrappedAsyncTest("logout with XHR failure", function() {
+    failureCheck(network.logout);
+  });
+
+
   wrappedAsyncTest("complete_email_addition valid", function() {
     network.completeEmailRegistration("goodtoken", function onSuccess(proven) {
       equal(proven, true, "good token proved");
@@ -230,6 +334,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+    notificationCheck(network.completeEmailRegistration, "goodtoken");
+  });
+
+  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+    failureCheck(network.completeEmailRegistration, "goodtoken");
+  });
+
   wrappedAsyncTest("createUser with valid user", function() {
     network.createUser("validuser", "origin", function onSuccess(created) {
       ok(created);
@@ -253,6 +365,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("createUser with XHR failure", function() {
+    notificationCheck(network.createUser, "validuser", "origin");
+  });
+
+  wrappedAsyncTest("createUser with XHR failure", function() {
+    failureCheck(network.createUser, "validuser", "origin");
+  });
+
   wrappedAsyncTest("checkUserRegistration with pending email", function() {
     xhr.useResult("pending");
 
@@ -281,6 +401,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+    notificationCheck(network.checkUserRegistration, "address");
+  });
+
+  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+    failureCheck(network.checkUserRegistration, "address");
+  });
+
   wrappedAsyncTest("completeUserRegistration with valid token", function() {
     network.completeUserRegistration("token", "password", function(registered) {
       ok(registered);
@@ -295,6 +423,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("completeUserRegistration with invalid token", function() {
     xhr.useResult("invalid");
+
     network.completeUserRegistration("token", "password", function(registered) {
       equal(registered, false);
       wrappedStart();
@@ -306,7 +435,16 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+    notificationCheck(network.completeUserRegistration, "token", "password");
+  });
+
+  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+    failureCheck(network.completeUserRegistration, "token", "password");
+  });
+
   wrappedAsyncTest("cancelUser valid", function() {
+
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
@@ -320,6 +458,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("cancelUser invalid", function() {
     xhr.useResult("invalid");
+
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
@@ -331,6 +470,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("cancelUser with XHR failure", function() {
+    notificationCheck(network.cancelUser);
+  });
+
+  wrappedAsyncTest("cancelUser with XHR failure", function() {
+    failureCheck(network.cancelUser);
+  });
+
   wrappedAsyncTest("emailRegistered with taken email", function() {
     xhr.useResult("taken");
 
@@ -359,6 +506,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+    notificationCheck(network.emailRegistered, "address");
+  });
+
+  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+    failureCheck(network.emailRegistered, "address");
+  });
+
 
   wrappedAsyncTest("addEmail valid", function() {
     network.addEmail("address", "origin", function onSuccess(added) {
@@ -385,6 +540,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("addEmail with XHR failure", function() {
+    notificationCheck(network.addEmail, "address", "origin");
+  });
+
+  wrappedAsyncTest("addEmail with XHR failure", function() {
+    failureCheck(network.addEmail, "address", "origin");
+  });
+
   wrappedAsyncTest("checkEmailRegistration pending", function() {
     xhr.useResult("pending");
 
@@ -413,6 +576,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+    notificationCheck(network.checkEmailRegistration, "address");
+  });
+
+  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+    failureCheck(network.checkEmailRegistration, "address");
+  });
+
 
   wrappedAsyncTest("removeEmail valid", function() {
     network.removeEmail("validemail", function onSuccess() {
@@ -429,6 +600,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   wrappedAsyncTest("removeEmail invalid", function() {
     xhr.useResult("invalid");
+
     network.removeEmail("invalidemail", function onSuccess() {
       // XXX need a test here;
       ok(true);
@@ -441,15 +613,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
-
-  wrappedAsyncTest("setKey", function() {
-    ok(true, "setKey");
-    start();
+  wrappedAsyncTest("removeEmail with XHR failure", function() {
+    notificationCheck(network.removeEmail, "validemail");
   });
 
-  wrappedAsyncTest("syncEmails", function() {
-    ok(true, "syncEmails");
-    start();
+  wrappedAsyncTest("removeEmail with XHR failure", function() {
+    failureCheck(network.removeEmail, "invalidemail");
   });
 
 
@@ -466,6 +635,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+    notificationCheck(network.requestPasswordReset, "address", "origin");
+  });
+
+  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+    failureCheck(network.requestPasswordReset, "address", "origin");
+  });
+
   wrappedAsyncTest("resetPassword", function() {
     network.resetPassword("password", function onSuccess() {
       // XXX need a test here;
@@ -479,6 +656,23 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("resetPassword with XHR failure", function() {
+    xhr.useResult("ajaxError");
+/*
+    the body of this function is not yet written
+
+    network.resetPassword("password", function onSuccess() {
+      ok(false, "XHR failure should never call success");
+      wrappedStart();
+    }, function onFailure() {
+      ok(true, "XHR failure should always call failure");
+      wrappedStart();
+    });
+    stop();
+*/
+    start();
+  });
+
   wrappedAsyncTest("changePassword", function() {
     network.changePassword("oldpassword", "newpassword", function onSuccess() {
       // XXX need a real wrappedAsyncTest here.
@@ -492,6 +686,24 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     stop();
   });
 
+  wrappedAsyncTest("changePassword with XHR failure", function() {
+    xhr.useResult("ajaxError");
+
+    /*
+    the body of this function is not yet written.
+    network.changePassword("oldpassword", "newpassword", function onSuccess() {
+      ok(false, "XHR failure should never call success");
+      wrappedStart();
+    }, function onFailure() {
+      ok(true, "XHR failure should always call failure");
+      wrappedStart();
+    });
+
+    stop();
+    */
+    start();
+  });
+
   wrappedAsyncTest("serverTime", function() {
     // I am forcing the server time to be 1.25 seconds off.
     contextInfo.server_time = new Date().getTime() - 1250;
@@ -509,4 +721,35 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
     stop();
   });
+
+  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+    notificationCheck();
+    xhr.useResult("contextAjaxError");
+
+    network.serverTime();
+  });
+
+  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+    xhr.useResult("contextAjaxError");
+
+    network.serverTime(function onSuccess(time) {
+      ok(false, "XHR failure should never call success");
+      wrappedStart();
+    }, function onFailure() {
+      ok(true, "XHR failure should always call failure");
+      wrappedStart();
+    });
+
+    stop();
+  });
+
+  wrappedAsyncTest("body offline message triggers offline message", function() {
+    OpenAjax.hub.subscribe("offline", function() {
+      ok(true, "offline event caught and application notified");
+      start();
+    });
+
+    $("body").trigger("offline");
+    stop();
+  });
 });
diff --git a/browserid/static/dialog/test/qunit/resources/user_unit_test.js b/browserid/static/dialog/test/qunit/resources/user_unit_test.js
index 7e0b15155355d61d644a5aa3a48ff7d92079ece7..48e7a4b705930a18aa72f16d7206ddec37c739c0 100644
--- a/browserid/static/dialog/test/qunit/resources/user_unit_test.js
+++ b/browserid/static/dialog/test/qunit/resources/user_unit_test.js
@@ -55,7 +55,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   var credentialsValid, unknownEmails, keyRefresh, syncValid, userEmails, 
       userCheckCount = 0,
       emailCheckCount = 0,
-      registrationResponse;
+      registrationResponse,
+      xhrFailure = false,
+      validToken = true; 
 
   var netStub = {
     reset: function() {
@@ -64,54 +66,62 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       keyRefresh = [];
       userEmails = {"testuser@testuser.com": {}};
       registrationResponse = "complete";
-    },
-
-    stageUser: function(email, password, onSuccess) {
-      onSuccess();
+      xhrFailure = false;
     },
 
     checkUserRegistration: function(email, onSuccess, onFailure) {
       userCheckCount++;
       var status = userCheckCount === 2 ? registrationResponse : "pending";
 
-      onSuccess(status);
+      xhrFailure ? onFailure() : onSuccess(status);
+    },
+
+    completeUserRegistration: function(token, password, onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess(validToken);
     },
 
     authenticate: function(email, password, onSuccess, onFailure) {
-      onSuccess(credentialsValid);
+      xhrFailure ? onFailure() : onSuccess(credentialsValid);
     },
 
     checkAuth: function(onSuccess, onFailure) {
-      onSuccess(credentialsValid);
+      xhrFailure ? onFailure() : onSuccess(credentialsValid);
     },
 
     emailRegistered: function(email, onSuccess, onFailure) {
-      onSuccess(email === "registered");
+      xhrFailure ? onFailure() : onSuccess(email === "registered");
     },
 
     addEmail: function(email, origin, onSuccess, onFailure) {
-      onSuccess(true);
+      xhrFailure ? onFailure() : onSuccess(true);
     },
 
     checkEmailRegistration: function(email, onSuccess, onFailure) {
       emailCheckCount++;
       var status = emailCheckCount === 2 ? registrationResponse : "pending";
 
-      onSuccess(status);
+      xhrFailure ? onFailure() : onSuccess(status);
+    },
+
+    emailForVerificationToken: function(token, onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess("testuser@testuser.com");
+    },
 
+    completeEmailRegistration: function(token, onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess(validToken);
     },
 
     removeEmail: function(email, onSuccess, onFailure) {
-      onSuccess();
+      xhrFailure ? onFailure() : onSuccess();
     },
 
     listEmails: function(onSuccess, onFailure) {
-      onSuccess(userEmails);
+      xhrFailure ? onFailure() : onSuccess(userEmails);
     },
 
     certKey: function(email, pubkey, onSuccess, onFailure) {
       if (syncValid) {
-        onSuccess(random_cert);
+        xhrFailure ? onFailure() : onSuccess(random_cert);
       }
       else {
         onFailure();
@@ -119,7 +129,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     },
     
     syncEmails: function(issued_identities, onSuccess, onFailure) {
-      onSuccess({
+      xhrFailure ? onFailure() : onSuccess({
         unknown_emails: unknownEmails,
         key_refresh: keyRefresh
       });
@@ -127,36 +137,36 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
     setKey: function(email, keypair, onSuccess, onFailure) {
       if (syncValid) {
-        onSuccess();
+        xhrFailure ? onFailure() : onSuccess();
       }
       else {
         onFailure();
       }
     },
 
-    createUser: function(email, origin, onSuccess) {
-      onSuccess(true);
+    createUser: function(email, origin, onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess(true);
     },
 
     setPassword: function(password, onSuccess) {
-      onSuccess();
+      xhrFailure ? onFailure() : onSuccess();
     },
 
     requestPasswordReset: function(email, origin, onSuccess, onFailure) {
-      onSuccess(true);
+      xhrFailure ? onFailure() : onSuccess(true);
     },
 
-    cancelUser: function(onSuccess) {
-      onSuccess();
+    cancelUser: function(onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess();
     },
 
-    serverTime: function(onSuccess) {
-      onSuccess(new Date());
+    serverTime: function(onSuccess, onFailure) {
+      xhrFailure ? onFailure() : onSuccess(new Date());
     },
 
-    logout: function(onSuccess) {
+    logout: function(onSuccess, onFailure) {
       credentialsValid = false;
-      onSuccess();
+      xhrFailure ? onFailure() : onSuccess();
     }
   };
 
@@ -203,6 +213,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       netStub.reset();
       userCheckCount = 0;
       emailCheckCount = 0;
+      validToken = true;
     },
     teardown: function() {
       lib.setNetwork(BrowserID.Network);
@@ -273,6 +284,20 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("createUser with XHR failure", function() {
+    xhrFailure = true;
+
+    lib.createUser("testuser@testuser.com", function(status) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
   /**
    * The next three tests use the mock network harness.  The tests are testing 
    * the polling action and whether `waitForUserValidation` reacts as expected
@@ -283,8 +308,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
    * stored in `registrationResponse`.
    */
   test("waitForUserValidation with `complete` response", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+
     lib.waitForUserValidation("testuser@testuser.com", function(status) {
       equal(status, "complete", "complete response expected");
+
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       start();
     }, failure("waitForUserValidation failure"));
 
@@ -294,8 +323,12 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   test("waitForUserValidation with `mustAuth` response", function() {
     registrationResponse = "mustAuth";
 
+    storage.setStagedOnBehalfOf(testOrigin);
+
     lib.waitForUserValidation("testuser@testuser.com", function(status) {
       equal(status, "mustAuth", "mustAuth response expected");
+
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       start();
     }, failure("waitForUserValidation failure"));
 
@@ -305,10 +338,13 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   test("waitForUserValidation with `noRegistration` response", function() {
     registrationResponse = "noRegistration";
 
+    storage.setStagedOnBehalfOf(testOrigin);
     lib.waitForUserValidation("baduser@testuser.com", function(status) {
       ok(false, "not expecting success")
+
       start();
     }, function(status) {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       ok(status, "noRegistration", "noRegistration response causes failure");
       start();
     });
@@ -316,6 +352,65 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("waitForUserValidation with XHR failure", function() {
+    xhrFailure = true;
+
+    storage.setStagedOnBehalfOf(testOrigin);
+    lib.waitForUserValidation("baduser@testuser.com", function(status) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared on XHR failure");
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+  test("verifyUser with a good token", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      
+      ok(info.valid, "token was valid");
+      equal(info.email, "testuser@testuser.com", "email part of info");
+      equal(info.origin, testOrigin, "origin in info");
+      equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed");
+
+      start();
+    }, failure("verifyUser failure"));
+
+    stop();
+  });
+
+  test("verifyUser with a bad token", function() {
+    validToken = false;
+
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      
+      equal(info.valid, false, "bad token calls onSuccess with a false validity");
+
+      start();
+    }, failure("verifyUser failure"));
+
+    stop();
+
+  });
+
+  test("verifyUser with an XHR failure", function() {
+    xhrFailure = true;
+
+    lib.verifyUser("token", "password", function onSuccess(info) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+      
+    stop();
+  });
+
   test("setPassword", function() {
     lib.setPassword("password", function() {
       // XXX fill this in.
@@ -348,7 +443,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
 
-
   test("authenticate with invalid credentials", function() {
     credentialsValid = false;
     lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) {
@@ -361,6 +455,21 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
 
+  test("authenticate with XHR failure", function() {
+    xhrFailure = true;
+    lib.authenticate("testuser@testuser.com", "testuser", function onComplete(authenticated) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+
+  });
+
+
   test("checkAuthentication with valid authentication", function() {
     credentialsValid = true;
     lib.checkAuthentication(function(authenticated) {
@@ -385,6 +494,21 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
 
+  test("checkAuthentication with XHR failure", function() {
+    xhrFailure = true;
+    lib.checkAuthentication(function(authenticated) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
+
   test("checkAuthenticationAndSync with valid authentication", function() {
     credentialsValid = true;
     lib.checkAuthenticationAndSync(function onSuccess() {},
@@ -411,6 +535,24 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+
+  test("checkAuthenticationAndSync with XHR failure", function() {
+    xhrFailure = true;
+    lib.checkAuthenticationAndSync(function onSuccess() {
+      ok(false, "xhr failure should never succeed");
+    }, function onComplete() {
+      ok(false, "xhr failure should never succeed");
+      
+      start();
+    }, function onFailure() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
   test("isEmailRegistered with registered email", function() {
     lib.isEmailRegistered("registered", function(registered) {
       ok(registered);
@@ -435,6 +577,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("isEmailRegistered with XHR failure", function() {
+    xhrFailure = true;
+    lib.isEmailRegistered("registered", function(registered) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
   test("addEmail", function() {
     lib.addEmail("testemail@testemail.com", function(added) {
       ok(added, "user was added");
@@ -442,12 +597,29 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identities = lib.getStoredEmailKeypairs();
       equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation.");
 
+
+      equal(storage.getStagedOnBehalfOf(), lib.getHostname(), "initiatingOrigin is stored"); 
+
       start();
     }, failure("addEmail failure"));
 
     stop();
   });
 
+  test("addEmail with XHR failure", function() {
+    xhrFailure = true;
+    lib.addEmail("testemail@testemail.com", function(added) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
 
   /**
    * The next three tests use the mock network harness.  The tests are testing 
@@ -459,7 +631,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
    * stored in `registrationResponse`.
    */
  test("waitForEmailValidation `complete` response", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+
     lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       equal(status, "complete", "complete response expected");
       start();
     }, failure("waitForEmailValidation failure"));
@@ -468,9 +643,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForEmailValidation `mustAuth` response", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
     registrationResponse = "mustAuth";
 
     lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+      ok(!storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       equal(status, "mustAuth", "mustAuth response expected");
       start();
     }, failure("waitForEmailValidation failure"));
@@ -479,12 +656,14 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   });
 
   test("waitForEmailValidation with `noRegistration` response", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
     registrationResponse = "noRegistration";
 
     lib.waitForEmailValidation("baduser@testuser.com", function(status) {
       ok(false, "not expecting success")
       start();
     }, function(status) {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
       ok(status, "noRegistration", "noRegistration response causes failure");
       start();
     });
@@ -492,6 +671,67 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+
+ test("waitForEmailValidation XHR failure", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+    xhrFailure = true;
+
+    lib.waitForEmailValidation("testemail@testemail.com", function(status) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
+  test("verifyEmail with a good token", function() {
+    storage.setStagedOnBehalfOf(testOrigin);
+    lib.verifyEmail("token", function onSuccess(info) {
+      
+      ok(info.valid, "token was valid");
+      equal(info.email, "testuser@testuser.com", "email part of info");
+      equal(info.origin, testOrigin, "origin in info");
+      equal(storage.getStagedOnBehalfOf(), "", "initiating origin was removed");
+
+      start();
+    }, failure("verifyEmail failure"));
+
+    stop();
+  });
+
+  test("verifyEmail with a bad token", function() {
+    validToken = false;
+
+    lib.verifyEmail("token", function onSuccess(info) {
+      
+      equal(info.valid, false, "bad token calls onSuccess with a false validity");
+
+      start();
+    }, failure("verifyEmail failure"));
+
+    stop();
+
+  });
+
+  test("verifyEmail with an XHR failure", function() {
+    xhrFailure = true;
+
+    lib.verifyEmail("token", function onSuccess(info) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+      
+    stop();
+  });
+
   test("syncEmailKeypair with successful sync", function() {
     syncValid = true;
     lib.syncEmailKeypair("testemail@testemail.com", function(keypair) {
@@ -523,6 +763,19 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("syncEmailKeypair with XHR failure", function() {
+    xhrFailure = true;
+    lib.syncEmailKeypair("testemail@testemail.com", function(keypair) {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
 
   test("removeEmail that is added", function() {
     storage.addEmail("testemail@testemail.com", {pub: "pub", priv: "priv"});
@@ -548,6 +801,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("removeEmail with XHR failure", function() {
+    storage.addEmail("testemail@testemail.com", {pub: "pub", priv: "priv"});
+
+    xhrFailure = true;
+    lib.removeEmail("testemail@testemail.com", function() {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
 
 
   test("syncEmails with no pre-loaded identities and no identities to add", function() {
@@ -636,8 +905,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("syncEmails with XHR failure", function() {
+    xhrFailure = true;
+
+    lib.syncEmails(function onSuccess() {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
 
   test("getAssertion with known email that has key", function() {
+    lib.setOrigin(testOrigin);
     lib.syncEmailKeypair("testuser@testuser.com", function() {
       lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) {
         testAssertion(assertion);
@@ -650,6 +933,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
 
   test("getAssertion with known email that does not have a key", function() {
+    lib.setOrigin(testOrigin);
     storage.addEmail("testuser@testuser.com", {});
     lib.getAssertion("testuser@testuser.com", function onSuccess(assertion) {
       testAssertion(assertion);
@@ -671,6 +955,22 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("getAssertion with XHR failure", function() {
+    lib.setOrigin(testOrigin);
+    xhrFailure = true;
+
+    lib.syncEmailKeypair("testuser@testuser.com", function() {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
+
   test("logoutUser", function(onSuccess) {
     credentialsValid = true;
     keyRefresh = ["testuser@testuser.com"]; 
@@ -693,6 +993,29 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("logoutUser with XHR failure", function(onSuccess) {
+    credentialsValid = true;
+    keyRefresh = ["testuser@testuser.com"]; 
+
+    lib.authenticate("testuser@testuser.com", "testuser", function(authenticated) {
+      lib.syncEmails(function() {
+         xhrFailure = true;
+
+        lib.logoutUser(function() {
+          ok(false, "xhr failure should never succeed");
+          start();
+        }, function() {
+          ok(true, "xhr failure should always be a failure"); 
+          start();
+        });
+
+
+      }, failure("syncEmails failure"));
+    }, failure("authenticate failure"));
+
+    stop();
+  });
+
   test("cancelUser", function(onSuccess) {
     lib.cancelUser(function() {
       var storedIdentities = storage.getEmails();
@@ -703,4 +1026,17 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     stop();
   });
 
+  test("cancelUser with XHR failure", function(onSuccess) {
+     xhrFailure = true;
+    lib.cancelUser(function() {
+      ok(false, "xhr failure should never succeed");
+      start();
+    }, function() {
+      ok(true, "xhr failure should always be a failure"); 
+      start();
+    });
+
+    stop();
+  });
+
 });
diff --git a/browserid/static/i/firefox_logo.png b/browserid/static/i/firefox_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..c55a338081cc1e0f1d2e78fc50226695c0b37488
Binary files /dev/null and b/browserid/static/i/firefox_logo.png differ
diff --git a/browserid/static/include.js b/browserid/static/include.js
index 9f16d5d47007e557a9c7987df3a98f23471be0fb..cebeafc44cb225688d5ed9b36a062a33f9492e5f 100644
--- a/browserid/static/include.js
+++ b/browserid/static/include.js
@@ -557,55 +557,88 @@
     };
   })();
 
-  function getInternetExplorerVersion() {
-    var rv = -1; // Return value assumes failure.
-    if (navigator.appName == 'Microsoft Internet Explorer') {
-      var ua = navigator.userAgent;
-      var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
-      if (re.exec(ua) != null)
-        rv = parseFloat(RegExp.$1);
+  var BrowserSupport = (function() {
+    var win = window,
+        nav = navigator,
+        reason;
+
+    // For unit testing
+    function setTestEnv(newNav, newWindow) {
+      nav = newNav;
+      win = newWindow;
     }
 
-    return rv;
-  }
-
-  function checkIE() {
-    var ieVersion = getInternetExplorerVersion(),
-        ieNosupport = ieVersion > -1 && ieVersion < 9,
-        message;
+    function getInternetExplorerVersion() {
+      var rv = -1; // Return value assumes failure.
+      if (nav.appName == 'Microsoft Internet Explorer') {
+        var ua = nav.userAgent;
+        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+        if (re.exec(ua) != null)
+          rv = parseFloat(RegExp.$1);
+      }
 
-    if(ieNosupport) {
-      message = "Unfortunately, your version of Internet Explorer is not yet supported.\n" +
-            'If you are using Internet Explorer 9, turn off "Compatibility View".';
+      return rv;
     }
 
-    return message;
-  }
+    function checkIE() {
+      var ieVersion = getInternetExplorerVersion(),
+          ieNosupport = ieVersion > -1 && ieVersion < 9;
 
-  function explicitNosupport() {
-    var message = checkIE();
+      if(ieNosupport) {
+        return "IE_VERSION";
+      }
+    }
 
-    if (message) {
-       message += "\nWe are working hard to bring BrowserID support to your browser!";
-       alert(message);
+    function explicitNosupport() {
+      return checkIE();
     }
 
-    return message;
-  }
+    function checkLocalStorage() {
+      var localStorage = 'localStorage' in win && win['localStorage'] !== null;
+      if(!localStorage) {
+        return "LOCALSTORAGE";
+      }
+    }
 
-  function checkRequirements() {
-    var localStorage = 'localStorage' in window && window['localStorage'] !== null;
-    var postMessage = !!window.postMessage;
-    var json = true;
+    function checkPostMessage() {
+      if(!win.postMessage) {
+        return "POSTMESSAGE";
+      }
+    }
 
-    var explicitNo = explicitNosupport()
+    function isSupported() {
+      reason = checkLocalStorage() || checkPostMessage() || explicitNosupport();
 
-    if(!explicitNo && !(localStorage && postMessage && json)) {
-      alert("Unfortunately, your browser does not meet the minimum HTML5 support required for BrowserID.");
+      return !reason;
     }
 
-    return localStorage && postMessage && json && !(explicitNo);
-  }
+    function getNoSupportReason() {
+      return reason;
+    }
+
+    return {
+      /**
+       * Set the test environment.
+       * @method setTestEnv
+       */
+      setTestEnv: setTestEnv,
+      /**
+       * Check whether the current browser is supported
+       * @method isSupported
+       * @returns {boolean}
+       */
+      isSupported: isSupported,
+      /**
+       * Called after isSupported, if isSupported returns false.  Gets the reason 
+       * why browser is not supported.
+       * @method getNoSupportReason
+       * @returns {string}
+       */
+      getNoSupportReason: getNoSupportReason
+    };
+    
+  }());
+
 
   // this is for calls that are non-interactive
   function _open_hidden_iframe(doc) {
@@ -636,16 +669,20 @@
     return iframe;
   }
   
-  function _open_window() {
+  function _open_window(url) {
+    url = url || "about:blank";
     // we open the window initially blank, and only after our relay frame has
     // been constructed do we update the location.  This is done because we
     // must launch the window inside a click handler, but we should wait to
     // start loading it until our relay iframe is instantiated and ready.
     // see issue #287 & #286
-    return window.open(
-      "about:blank",
+    var dialog = window.open(
+      url,
       "_mozid_signin",
       isFennec ? undefined : "menubar=0,location=0,resizable=0,scrollbars=0,status=0,dialog=1,width=700,height=375");
+
+    dialog.focus();
+    return dialog;
   }
 
   function _attach_event(element, name, listener) {
@@ -684,16 +721,17 @@
 
     // keep track of these so that we can re-use/re-focus an already open window.
     navigator.id.getVerifiedEmail = function(callback) {
-      if(!checkRequirements()) {
-        return;
-      }
-
       if (w) {
         // if there is already a window open, just focus the old window.
         w.focus();
         return;
       }
 
+      if (!BrowserSupport.isSupported()) {
+        w = _open_window(ipServer + "/unsupported_dialog");
+        return;
+      }
+
       var frameid = _get_relayframe_id();
       var iframe = _open_relayframe("browserid_relay_" + frameid);
       w = _open_window();
@@ -712,8 +750,7 @@
           // has a problem re-attaching new iframes with the same name.  Code inside
           // of frames with the same name sometimes does not get run.
           // See https://bugzilla.mozilla.org/show_bug.cgi?id=350023
-          w.location = ipServer + "/sign_in#" + frameid;
-          w.focus();
+          w = _open_window(ipServer + "/sign_in#" + frameid);
         }
       });
 
@@ -721,8 +758,10 @@
         chan.destroy();
         chan = null;
 
-        w.close();
-        w = null;
+        if (w) {
+          w.close();
+          w = null;
+        }
 
         iframe.parentNode.removeChild(iframe);
         iframe = null;
diff --git a/browserid/static/js/pages/add_email_address.js b/browserid/static/js/pages/add_email_address.js
index e5c91d305cadf9e79c50636fd36154abce90db4b..58b4198f68713ab968032690e692679406b54f1e 100644
--- a/browserid/static/js/pages/add_email_address.js
+++ b/browserid/static/js/pages/add_email_address.js
@@ -36,28 +36,35 @@
 
 (function() {
   "use strict";
+  
+  var ANIMATION_TIME=250;
+  function emailRegistrationSuccess(info) {
 
-  function emailRegistrationSuccess() {
-    $(".hint").hide();
-    $("#congrats").fadeIn(250, function() {
-      $("body").delay(1000).fadeOut(500, function() {
-        // if the close didn't work, then let's redirect the the main page where they'll
-        // get to see the ids that they've created.
-        document.location = '/';
-      });
+    $("#email").text(info.email);
+    
+    if (info.origin) {
+      $("#siteinfo .website").html(info.origin);
+      $("#siteinfo").show();
+    }
+
+    $("#signUpForm").delay(2000).fadeOut(ANIMATION_TIME, function() {
+      $("#congrats").fadeIn(ANIMATION_TIME);
     });
   }
 
   function showError(el) {
     $(".hint").hide();
-    $(el).fadeIn(250);
+    $(el).fadeIn(ANIMATION_TIME);
   }
 
   BrowserID.addEmailAddress = function(token) {
-    BrowserID.Network.completeEmailRegistration(token, function onSuccess(valid) {
-      if (valid) {
-        emailRegistrationSuccess();
-      } else {
+    var user = BrowserID.User;
+
+    user.verifyEmail(token, function onSuccess(info) {
+      if (info.valid) {
+        emailRegistrationSuccess(info);
+      }
+      else {
         showError("#cannotconfirm");
       }
     }, function onFailure() {
diff --git a/browserid/static/ping.txt b/browserid/static/ping.txt
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/browserid/tests/lib/start-stop.js b/browserid/tests/lib/start-stop.js
index f9e69b8a53d1c2021ff42fb2e01da6ee62c92311..d8a837f7e4629f3a35686552d365531baa63cb69 100644
--- a/browserid/tests/lib/start-stop.js
+++ b/browserid/tests/lib/start-stop.js
@@ -71,7 +71,7 @@ exports.addStartupBatches = function(suite) {
         return true;
       },
       "server should be running": {
-        topic: wsapi.get('/ping.txt'),
+        topic: wsapi.get('/__heartbeat__'),
         "server is running": function (r, err) {
           assert.equal(r.code, 200);
         }
diff --git a/browserid/tests/page-requests-test.js b/browserid/tests/page-requests-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..156a0611df558716b6c3d25edbc8e8fd13c3679d
--- /dev/null
+++ b/browserid/tests/page-requests-test.js
@@ -0,0 +1,116 @@
+#!/usr/bin/env node
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+http = require('http'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js');
+
+var suite = vows.describe('page requests');
+
+// start up a pristine server
+start_stop.addStartupBatches(suite);
+
+// This set of tests check to make sure all of the expected pages are served 
+// up with the correct status codes.  We use Lloyd's wsapi client as our REST 
+// interface.
+
+
+
+// Taken from the vows page.
+function assertStatus(code) {
+  return function (res, err) {
+    assert.equal(res.code, code);
+  };
+}
+
+function respondsWith(status) {
+  var context = {
+    topic: function () {
+      // Get the current context's name, such as "POST /"
+      // and split it at the space.
+      var req    = this.context.name.split(/ +/), // ["POST", "/"]
+          method = req[0].toLowerCase(),         // "post"
+          path   = req[1];                       // "/"
+
+      // Perform the contextual client request,
+      // with the above method and path.
+      wsapi[method](path).call(this);
+    }
+  };
+
+  // Create and assign the vow to the context.
+  // The description is generated from the expected status code
+  // and the status name, from node's http module.
+  context['should respond with a ' + status + ' '
+         + http.STATUS_CODES[status]] = assertStatus(status);
+
+  return context;
+}
+
+suite.addBatch({
+  'GET /':                       respondsWith(200),
+  'GET /signup':                 respondsWith(200),
+  'GET /forgot':                 respondsWith(200),
+  'GET /signin':                 respondsWith(200),
+  'GET /about':                  respondsWith(200),
+  'GET /tos':                    respondsWith(200),
+  'GET /privacy':                respondsWith(200),
+  'GET /verify_email_address':   respondsWith(200),
+  'GET /add_email_address':      respondsWith(200),
+  'GET /pk':                     respondsWith(200),
+  'GET /vepbundle':              respondsWith(200),
+  'GET /signin':                 respondsWith(200),
+  'GET /unsupported_dialog':     respondsWith(200),
+  'GET /developers':             respondsWith(200),
+  'GET /manage':                 respondsWith(302),
+  'GET /users':                  respondsWith(302),
+  'GET /users/':                 respondsWith(302),
+  'GET /primaries':              respondsWith(302),
+  'GET /primaries/':             respondsWith(302),
+  'GET /developers':             respondsWith(302)
+});
+
+// shut the server down and cleanup
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/browserid/views/dialog.ejs b/browserid/views/dialog.ejs
index 4bde9a26a0106b5e5f6664f4f357ef107b42cfc3..d2b634a26338b2e79f40b510c8351d1aba641e22 100644
--- a/browserid/views/dialog.ejs
+++ b/browserid/views/dialog.ejs
@@ -1,90 +1,35 @@
-<!doctype html>
-<html>
-<head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">  
-  <!--[if lt IE 9]>
-    <script type="text/javascript" src="/js/html5shim.js"></script>
-  <![endif]-->
-  <% if (production) { %>
-      <link href="/dialog/css/production.min.css" rel="stylesheet" type="text/css">
-  <% } else { %>
-      <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css">
-      <link href="/dialog/css/m.css" rel="stylesheet" type="text/css">
-  <% } %>
-  <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css">
-  <title>Browser ID</title>
-</head>
-  <body class="waiting">
-      <div id="wrapper">
-          <header id="header" class="cf">
-              <ul>
-                  <li><a class="home" target="_blank" href="/"></a></li>
-              </ul>
-          </header>
-
-          <div id="content">
-              <section id="formWrap">
-                <form novalidate> 
-                  <div id="favicon">
-                      <div class="vertical">
-                          <strong id="sitename"></strong>
-                      </div>
-                  </div>
-
-                  <div id="signIn">
-                      <div class="arrow"></div>
-                      <div class="table">
-                          <div class="vertical contents">
-                          </div>
-                      </div>
-                  </div>
-                </form>
-              </section>
-
-
-              <section id="wait">
-                  <div class="table">
-                      <div class="vertical contents">
-                          <h2>Communicating with server</h2>
-                          <p>Just a moment while we talk with the server.</p>
-                      </div>
-                  </div>
-              </section>
-
-
-              <section id="error">
-                  <div class="table">
-                      <div class="vertical contents">
-                      </div>
-                  </div>
-              </section>
-          </div>
-
-          <footer>
-                <ul class="cf">
-                    <li>By <a href="http://mozillalabs.com">Mozilla Labs</a></li>
-
-                    <li>&mdash;</li>           
-                    <li><a href="#">Privacy</a></li>
-                    <li><a href="#">TOS</a></li>
-                </ul>
+    <section id="formWrap">
+      <form novalidate> 
+        <div id="favicon">
+            <div class="vertical">
+                <strong id="sitename"></strong>
+            </div>
+        </div>
 
-                <div class="learn">
-                    BrowserID is the fast and secure way to sign in &mdash; <a target="_blank" href="/about">learn more</a>
+        <div id="signIn">
+            <div class="arrow"></div>
+            <div class="table">
+                <div class="vertical contents">
                 </div>
+            </div>
+        </div>
+      </form>
+    </section>
+
 
-                <a class="help" href="http://support.mozilla.com/en-US/home" target="_blank">need help?</a>
-          </footer>
+    <section id="wait">
+        <div class="table">
+            <div class="vertical contents">
+                <h2>Communicating with server</h2>
+                <p>Just a moment while we talk with the server.</p>
+            </div>
+        </div>
+    </section>
 
-      </div>
 
-      <script type="text/html" id="templateTooltip">
-        <div class="tooltip">
-          {{ contents }}
+    <section id="error">
+        <div class="table">
+            <div class="vertical contents">
+            </div>
         </div>
-      </script>
-      <script type="text/javascript" src="/vepbundle"></script>
-      <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script>
-	</body>
-</html>
+    </section>
diff --git a/browserid/views/dialog_layout.ejs b/browserid/views/dialog_layout.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..13dd4d98f92ddc60880e28d7105492ba153837ae
--- /dev/null
+++ b/browserid/views/dialog_layout.ejs
@@ -0,0 +1,58 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">  
+  <!--[if lt IE 9]>
+    <script type="text/javascript" src="/js/html5shim.js"></script>
+  <![endif]-->
+  <% if (production) { %>
+      <link href="/dialog/css/production.min.css" rel="stylesheet" type="text/css">
+  <% } else { %>
+      <link href="/dialog/css/popup.css" rel="stylesheet" type="text/css">
+      <link href="/dialog/css/m.css" rel="stylesheet" type="text/css">
+  <% } %>
+  <link href="https://fonts.googleapis.com/css?family=Droid+Serif:400,400italic,700,700italic" rel="stylesheet" type="text/css">
+  <title>Browser ID</title>
+</head>
+  <body class="waiting">
+      <div id="wrapper">
+          <header id="header" class="cf">
+              <ul>
+                  <li><a class="home" target="_blank" href="/"></a></li>
+              </ul>
+          </header>
+
+          <div id="content">
+            <%- body %>
+          </div>
+
+          <footer>
+                <ul class="cf">
+                    <li>By <a href="http://mozillalabs.com">Mozilla Labs</a></li>
+
+                    <li>&mdash;</li>           
+                    <li><a href="#">Privacy</a></li>
+                    <li><a href="#">TOS</a></li>
+                </ul>
+
+                <div class="learn">
+                    BrowserID is the fast and secure way to sign in &mdash; <a target="_blank" href="/about">learn more</a>
+                </div>
+
+                <a class="help" href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank">need help?</a>
+          </footer>
+
+      </div>
+
+      <% if (useJavascript !== false) { %>
+          <script type="text/html" id="templateTooltip">
+            <div class="tooltip">
+              {{ contents }}
+            </div>
+          </script>
+          <script type="text/javascript" src="/vepbundle"></script>
+          <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script>
+      <% } %>
+	</body>
+</html>
diff --git a/browserid/views/unsupported_dialog.ejs b/browserid/views/unsupported_dialog.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..acf52333b7893fe11294c1defcd6adcbe1227278
--- /dev/null
+++ b/browserid/views/unsupported_dialog.ejs
@@ -0,0 +1,28 @@
+  <section id="error" style="display: block">
+      <div class="table">
+          <div class="vertical contents">
+              <div id="reason">
+                We're sorry, but currently your browser isn't supported.
+              </div>
+
+              <div id="alternative">
+
+                <div id="borderbox">
+                  <a href="http://getfirefox.com" target="_blank">
+                    <img src="/i/firefox_logo.png" width="250" height="88" alt="Firefox logo" />
+                  </a>
+
+                  <p>
+                    BrowserID works with <a href="http://getfirefox.com" target="_blank" title="Get Firefox">Firefox</a>
+                  </p>
+
+                  <p class="lighter">
+                    and other <a href="http://whatbrowser.org" target="_blank">modern browsers.</a>
+                  </p>
+                </div>
+
+              </div>
+
+          </div>
+      </div>
+  </section>
diff --git a/browserid/views/verifyemail.ejs b/browserid/views/verifyemail.ejs
index 766c84b76d2a307a6108f6c865e08411d381ded3..b4f4e0eb0a3a4930b4b217fcf2dbc5285cd2db1f 100644
--- a/browserid/views/verifyemail.ejs
+++ b/browserid/views/verifyemail.ejs
@@ -2,17 +2,27 @@
     <div id="signUpFormWrap">
         <div id="signUpForm" class="cf">
             <h1 class="serif">Email Verification</h1>
+
             <ul class="notifications">
                 <li class="notification error" id="cannotconnect">Error comunicating with server.</li>
                 <li class="notification error" id="cannotconfirm">Error encountered while attempting to confirm your address. Have you previously verified this address?</li>
             </ul>
+
             <p class="hint">One moment while we attempt to confirm your email address...</p>
-        </div>
         
+        </div>
+
+        <div id="congrats">
+            <p class="serif">
+                <strong id="email">Your address</strong> has been verified!
 
-        <div id="congrats" class="serif">
-          Your email address has been verified!
+                <span id="siteinfo">
+                  Your new address is set up and you should now be signed in.
+                  You may now close this window and go back to 
+                  <strong class="website"></strong> 
+                </span>
+            </p>
         </div>
-  </div>
+    </div>
 </div>
 
diff --git a/libs/configuration.js b/libs/configuration.js
index ab43bbcb58f104b752c1abffca5215b5b0407ebe..54a24237863368ff86def66c6ba16b3af114a756 100644
--- a/libs/configuration.js
+++ b/libs/configuration.js
@@ -78,7 +78,8 @@ g_configs.production = {
   var_path: '/home/browserid/var/',
   database: {
     driver: "mysql",
-    user: 'browserid'
+    user: 'browserid',
+    create_schema: true
   },
   bcrypt_work_factor: 12,
   authentication_duration_ms: (7 * 24 * 60 * 60 * 1000),
@@ -109,6 +110,11 @@ g_configs.local =  {
   certificate_validity_ms: g_configs.production.certificate_validity_ms
 };
 
+if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
+  var fs = require('fs');
+  eval(fs.readFileSync(process.env['NODE_EXTRA_CONFIG']) + '');
+}
+
 Object.keys(g_configs).forEach(function(config) {
   if (!g_configs[config].smtp) {
     g_configs[config].smtp = {
@@ -148,12 +154,13 @@ g_config['URL'] = g_config['scheme'] + '://' + g_config['hostname'] + getPortFor
  * to re-write urls as needed for this particular environment.
  *
  * Note, for a 'local' environment, no re-write is needed because this is
- * handled at a higher level.  For a 'production' env no rewrite is necc cause
- * all source files are written for that environment.
+ * handled at a higher level.  For other environments, only perform re-writing
+ * if the host, port, or scheme are different than https://browserid.org:443
+ * (all source files always should have the production hostname written into them)
  */
 exports.performSubstitution = function(app) {
-  if (process.env['NODE_ENV'] !== 'production' &&
-      process.env['NODE_ENV'] !== 'local') {
+  if ((g_config.hostname != 'browserid.org' || g_config.port != '443' || g_config.scheme != 'https') &&
+      process.env['NODE_ENV'] !== 'local'){
     app.use(substitution.substitute({
       'https://browserid.org': g_config['URL'],
       'browserid.org:443': g_config['hostname'] + ':' + g_config['port'],
@@ -164,7 +171,7 @@ exports.performSubstitution = function(app) {
 
 // At the time this file is required, we'll determine the "process name" for this proc
 // if we can determine what type of process it is (browserid or verifier) based
-// on the path, we'll use that, otherwise we'll name it 'ephemeral'.  
+// on the path, we'll use that, otherwise we'll name it 'ephemeral'.
 if (process.argv[1] == path.join(__dirname, "..", "browserid", "run.js")) {
   g_config['process_type'] = 'browserid';
 } else if (process.argv[1] == path.join(__dirname, "..", "verifier", "run.js")) {
diff --git a/libs/heartbeat.js b/libs/heartbeat.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5f2aaf70c2b1374823b0e9795d5d92c94460108
--- /dev/null
+++ b/libs/heartbeat.js
@@ -0,0 +1,7 @@
+exports.setup = function(app) {
+  app.get("/__heartbeat__", function(req, res) {
+    res.writeHead(200);
+    res.write('ok');
+    res.end();
+  });
+};
diff --git a/libs/secrets.js b/libs/secrets.js
index 46ea829f270a236580a16743e92b8217c8bb3ea3..0b404f615cbe8bedcd6fd7469331ea50559501cb 100644
--- a/libs/secrets.js
+++ b/libs/secrets.js
@@ -57,6 +57,8 @@ exports.hydrateSecret = function(name, dir) {
 
   if (secret === undefined) {
     secret = exports.generate(128);
+    fs.writeFileSync(p, '');
+    fs.chmodSync(p, 0600);
     fs.writeFileSync(p, secret);
   }
   return secret;
diff --git a/scripts/rpmbuild.sh b/scripts/rpmbuild.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c7caae8cd57330e18d3f5b337e14a50def13467d
--- /dev/null
+++ b/scripts/rpmbuild.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -e
+
+progname=$(basename $0)
+
+cd $(dirname $0)/..    # top level of the checkout
+
+curdir=$(basename $PWD)
+if [ "$curdir" != "browserid" ]; then
+    echo "$progname: git checkout must be in a dir named 'browserid'" >&2
+    exit 1
+fi
+
+mkdir -p rpmbuild/SOURCES rpmbuild/SPECS
+rm -rf rpmbuild/RPMS
+
+tar -C .. --exclude rpmbuild -czf \
+    $PWD/rpmbuild/SOURCES/browserid-server.tar.gz browserid
+
+set +e
+
+rpmbuild --define "_topdir $PWD/rpmbuild" -ba browserid.spec
+rc=$?
+if [ $rc -eq 0 ]; then
+    ls -l $PWD/rpmbuild/RPMS/*/*.rpm
+else
+    echo "$progname: failed to build browserid RPM (rpmbuild rc=$rc)" >&2
+fi
+
+exit $rc
diff --git a/scripts/update_database.sql b/scripts/update_database.sql
new file mode 100644
index 0000000000000000000000000000000000000000..976d85f1ba97370ba79a1b80ae274b4bf4ba1cb3
--- /dev/null
+++ b/scripts/update_database.sql
@@ -0,0 +1,18 @@
+ALTER TABLE user ENGINE = InnoDB;
+ALTER TABLE user CHANGE id id BIGINT AUTO_INCREMENT;
+ALTER TABLE user CHANGE passwd passwd CHAR(64) NOT NULL;
+
+ALTER TABLE email ENGINE = InnoDB;
+ALTER TABLE email CHANGE id id BIGINT AUTO_INCREMENT;
+ALTER TABLE email CHANGE user user BIGINT NOT NULL;
+ALTER TABLE email ADD FOREIGN KEY user_fkey (user) REFERENCES user(id);
+ALTER TABLE email CHANGE address address VARCHAR(255) UNIQUE NOT NULL;
+
+ALTER TABLE staged ENGINE = InnoDB;
+ALTER TABLE staged DROP PRIMARY KEY;
+ALTER TABLE staged ADD id BIGINT AUTO_INCREMENT PRIMARY KEY;
+ALTER TABLE staged CHANGE secret secret CHAR(48) UNIQUE NOT NULL;
+ALTER TABLE staged CHANGE new_acct new_acct BOOL NOT NULL;
+ALTER TABLE staged CHANGE email email VARCHAR(255) UNIQUE NOT NULL;
+ALTER TABLE staged CHANGE ts ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
+
diff --git a/test.sh b/test.sh
deleted file mode 100755
index b41801ed1cbc80f3f397258f877b52aa5a4e0910..0000000000000000000000000000000000000000
--- a/test.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-npm test
diff --git a/verifier/app.js b/verifier/app.js
index 42d266195fcef4680ac11a3a5a83d8f1d08adeaf..9f1322e4ff6934f1acee485e28c73fd62aa79055 100644
--- a/verifier/app.js
+++ b/verifier/app.js
@@ -34,13 +34,14 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-const   path = require('path'),
-         url = require('url'),
-          fs = require('fs'),
+const    path = require('path'),
+          url = require('url'),
+           fs = require('fs'),
 certassertion = require('./lib/certassertion.js'),
-     express = require('express'),
-     metrics = require('../libs/metrics.js'),
-     logger = require('../libs/logging.js').logger;
+      express = require('express'),
+      metrics = require('../libs/metrics.js'),
+    heartbeat = require('../libs/heartbeat.js'),
+       logger = require('../libs/logging.js').logger;
 
 logger.info("verifier server starting up");
 
@@ -114,12 +115,8 @@ exports.setup = function(app) {
     process.exit();
   });
 
-  // A simple ping hook for monitoring.
-  app.get("/ping.txt", function(req ,resp) {
-    resp.writeHead(200, {"Content-Type": "text/plain"})
-    resp.write("k.");
-    resp.end();
-  });
+  // setup health check / heartbeat
+  heartbeat.setup(app);
 
   app.post('/', doVerify);
   app.post('/verify', doVerify);
diff --git a/verifier/lib/certassertion.js b/verifier/lib/certassertion.js
index fb88dbd33c4d682d29a33acd9d4c8960bf2fb61f..71590eb62721267cf841a66b72b8f34f953f2418 100644
--- a/verifier/lib/certassertion.js
+++ b/verifier/lib/certassertion.js
@@ -58,6 +58,8 @@ var publicKeys = {};
 
 // set up some default public keys
 publicKeys[configuration.get('hostname')] = secrets.PUBLIC_KEY;
+logger.debug("pre-seeded public key cache with key for " +
+             configuration.get('hostname'));
 
 function https_complete_get(host, url, successCB, errorCB) {
   https.get({host: host,path: url}, function(res) {
@@ -78,10 +80,16 @@ function https_complete_get(host, url, successCB, errorCB) {
 
 // only over SSL
 function retrieveHostPublicKey(host, successCB, errorCB) {
+  logger.debug("attempting to fetching public key for " + host);
+
   // cached?
   var cached = publicKeys[host];
-  if (cached)
+  if (cached) {
+    logger.debug("public key for " + host + " returned from cache");
     return successCB(cached);
+  }
+
+  logger.debug("performing HTTP request to fetch public key from " + host);
   
   https_complete_get(host, HOSTMETA_URL, function(hostmeta) {
     // find the location of the public key
@@ -216,4 +224,4 @@ function verify(assertion, audience, successCB, errorCB, pkRetriever) {
 
 
 exports.retrieveHostPublicKey = retrieveHostPublicKey;
-exports.verify = verify;
\ No newline at end of file
+exports.verify = verify;