diff --git a/ChangeLog b/ChangeLog
index d53464c50e904e25d8776a43ca2e5f8a72775f5b..af3561ed7cd8f8b671182c7892d12d1e016af713 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,19 @@
-train-2012.03.14 (in progress):
+train-2012.03.28 (in progress):
+
+train-2012.03.14:
+  * BrowserID now speaks Bulgarian
+  * Fix regressions related to ToS/PP feature (#841): #1303
+  * Fix regressions related to improved email selection on iOS (#1133): #1304
+  * "delegation of authority" implemented: #1271, #864
+  * visual improvements: #403
+  * improved algorithm for finding best language for a given user: #1128
+  * frontend tests now run on every commit under travisci: #635
+  * improve the way that .well-known/browserid is cached: #1205
+  * l10n fixes: #1124, #1300
+  * tools/build/dev env cleanup: #1235, #1234, #1257, #1245, #1055
+  * improvements to developer tool for checking primary support - scripts/checks_primary_support
+  * documentation improvements: #1264, #1287, #1288
+  * logging improvements: #1254, #1255, #1283, #1291
 
 train-2012.03.01:
   * When the user authenticates log them in automatically without going to the email picker: #198
diff --git a/config/l10n-all.json b/config/l10n-all.json
index 19ef4adfe56b8c873358010f79323af0562ff883..2d2b46ec5f8ebc49c92c1d1a0e3296cafc3cb7be 100644
--- a/config/l10n-all.json
+++ b/config/l10n-all.json
@@ -1,6 +1,6 @@
 {
 "supported_languages": [
-    "af", "ca", "cs", "da", "db-LB", "de", "el", "en-US", "eo", "es",
+    "af", "bg", "ca", "cs", "da", "db-LB", "de", "el", "en-US", "eo", "es",
     "es-MX", "et", "eu", "fi", "fr", "fy", "ga", "gd", "gl", "he", "hr",
     "hu", "it", "ja", "ko", "lij", "lt", "ml", "nl", "pa", "pl", "pt",
     "pt-BR", "rm", "ro", "ru", "sk", "sl", "son", "sq", "sr", "sv", "tr",
diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js
index 75fdc53fec4ec7e51f493819773fc5dcec901c7d..d9dfc7821720c4b91c4617a2d85d7a3c2ee9b9e8 100644
--- a/resources/static/include_js/include.js
+++ b/resources/static/include_js/include.js
@@ -947,6 +947,19 @@
       loginCanceled: [ ]
     };
 
+    var compatMode = undefined;
+    function checkCompat(requiredMode) {
+      if (requiredMode === true) {
+        try { console.log("this site uses deprecated APIs (see documentation for navigator.id.request())"); } catch(e) { }
+      }
+
+      if (compatMode === undefined) compatMode = requiredMode;
+      else if (compatMode != requiredMode) {
+        throw "you cannot combine browserid event APIs with navigator.id.getVerifiedEmail() or navigator.id.get()" +
+              "this site should instead use navigator.id.request() and the browserid event API";
+      }
+    }
+
     function emitEvent(type, params) {
       if (listeners[type]) {
         var evt = document.createEvent('Event');
@@ -1001,10 +1014,7 @@
       }
     }
 
-    navigator.id.addEventListener = function(type, listener/*, useCapture */) {
-      // allocate iframe if it is not allocated
-      _open_hidden_iframe();
-
+    function internalAddEventListener(type, listener) {
       // add event to listeners table if it's not there already
       if (!listeners[type]) throw "unsupported event type: '" + type + "'";
 
@@ -1013,22 +1023,35 @@
         if (listeners[type][i] === listener) return;
       }
       listeners[type].push(listener);
-    };
+    }
 
-    navigator.id.removeEventListener = function(type, listener/*, useCapture */) {
-      if (!useCapture) useCapture = false;
+    navigator.id.addEventListener = function(type, listener) {
+      checkCompat(false);
+
+      // allocate iframe if it is not allocated
+      _open_hidden_iframe();
+      internalAddEventListener(type,listener);
+    };
 
+    function internalRemoveEventListener(type, listener ) {
       // remove event from listeners table
       var i;
       for (i = 0; i < listeners[type].length; i++) {
         if (listeners[type][i] === listener) break;
       }
-      if (i < listeners[type][i] === listener) {
+      if (i < listeners[type][i].length) {
         listeners[type].splice(i, 1);
       }
+    }
+
+    navigator.id.removeEventListener = function(type, listener/*, useCapture */) {
+      checkCompat(false);
+      internalRemoveEventListener(type, listener);
     };
 
     navigator.id.logout = function() {
+      checkCompat(false);
+
       // allocate iframe if it is not allocated
       _open_hidden_iframe();
 
@@ -1037,6 +1060,8 @@
     };
 
     navigator.id.setLoggedInUser = function(email) {
+      checkCompat(false);
+
       // 1. allocate iframe if it is not allocated
       _open_hidden_iframe();
 
@@ -1044,11 +1069,34 @@
       commChan.notify({ method: 'loggedInUser', params: email });
     };
 
+    // backwards compatibility function
     navigator.id.get = function(callback, options) {
-      // backwards compatibility function
+      checkCompat(true);
+
+      if (options && options.silent) {
+        if (callback) setTimeout(function() { callback(null); }, 0);
+      } else {
+        function handleEvent(e) {
+          internalRemoveEventListener('login', handleEvent);
+          callback((e && e.assertion) ? e.assertion : null);
+        }
+        internalAddEventListener('login', handleEvent);
+        internalRequest(options);
+      }
+    };
+
+    // backwards compatibility function
+    navigator.id.getVerifiedEmail = function(callback) {
+      checkCompat(true);
+      navigator.id.get(callback);
     };
 
     navigator.id.request = function(options) {
+      checkCompat(false);
+      return internalRequest(options);
+    };
+
+    function internalRequest(options) {
       // focus an existing window
       if (w) {
         try {
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 6c891230e6540bbf3c7c23b66feb967c982d1327..b3690391a7cb1d00ad458972779f74314136f2cc 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.03.14
+Version:       0.2012.03.28
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Pete Fritchman <petef@mozilla.com>
diff --git a/scripts/check_primary_support b/scripts/check_primary_support
index fa0137afafc0edb21511a84df252ee27acd6badb..4861b74aa14ac6ac8ef4cb10eff60ffe986d558f 100755
--- a/scripts/check_primary_support
+++ b/scripts/check_primary_support
@@ -7,6 +7,7 @@
 const
 https = require('https'),
 und = require('underscore'),
+urlp = require('url'),
 util = require('util'),
 
 primary = require('../lib/primary'),
@@ -29,8 +30,13 @@ primary.checkSupport(domain, function(err, urls, publicKey) {
   console.log('Priary domain: ', domain);
   console.log('Public Key: ', publicKey);
 
-  getResource('auth', urls.auth, urls, function () {
-    getResource('prov', urls.prov, urls);
+  var authopts = {
+      xframe: false
+  };
+  getResource('auth', urls.auth, urls, authopts, function () {
+    getResource('prov', urls.prov, urls, {
+      xframe: true
+    });
   });
 
 });
@@ -38,14 +44,14 @@ primary.checkSupport(domain, function(err, urls, publicKey) {
 /**
  * Retrieve one of their urls and examine aspects of it for issues
  */
-function getResource(mode, url, urls, cb) {
-    console.log('Checking ', url);
+function getResource(mode, url, urls, opts, cb) {
+  var path = urlp.parse(url).path;
   var body = "",
       r = https.request({
     host: domain,
-    path: url,
+    path: path,
     method: 'GET'
-  }, checkResource(urls, body));
+  }, checkResource(url, opts, body));
   r.on('data', function (chunk) {
     body += chunk;
   });
@@ -74,20 +80,21 @@ function getResource(mode, url, urls, cb) {
  *
  * Do the provisioning and signin resources look kosher?
  */
-function checkResource (urls, body) {
+function checkResource (url, opts, body) {
   return function (resp) {
     // Their are no X-Frame options
     if (resp.statusCode != 200) {
-      console.log("ERROR: HTTP status code=", resp.statusCode);
+      console.log("ERROR: HTTP status code=", resp.statusCode, url);
     } else {
-      var xframe = und.filter(Object.keys(resp.headers), function (header) {
-        return header.toLowerCase() == 'x-frame-options';
-      });
-      if (xframe.length == 1) {
-        console.log("ERROR: X-Frame-Options=", resp.headers[xframe[0]], ", BrowserID will not be able to communicate with your site." +
-            " Suppress X-Frame-Options for /.well-known/browserid, " + urls.auth + ' and ' + urls.prov);
+      if (opts.xframe === true) {
+        var xframe = und.filter(Object.keys(resp.headers), function (header) {
+          return header.toLowerCase() == 'x-frame-options';
+        });
+        if (xframe.length == 1) {
+          console.log("ERROR: X-Frame-Options=", resp.headers[xframe[0]], ", BrowserID will not be able to communicate with your site." +
+              " Suppress X-Frame-Options for ", url);
+        }
       }
-
       resp.setEncoding('utf8');
     }
   };
diff --git a/scripts/deploy/ssh.js b/scripts/deploy/ssh.js
index 29c504ab24be86c8e8123d978c7ab8c7ab2dcf25..290abf1d322745ef8f4fbc17eac90345f289e606 100644
--- a/scripts/deploy/ssh.js
+++ b/scripts/deploy/ssh.js
@@ -36,3 +36,8 @@ exports.copySSL = function(host, pub, priv, cb) {
     });
   });
 };
+
+exports.addSSHPubKey = function(host, pubkey, cb) {
+  var cmd = 'ssh -o "StrictHostKeyChecking no" ec2-user@' + host + " 'echo \'" + pubkey + "\' >> .ssh/authorized_keys'";
+  child_process.exec(cmd, cb);
+};
diff --git a/scripts/deploy_dev.js b/scripts/deploy_dev.js
index f615705719ea99e2a8f7c75fc5fbdcd9a34dbb46..21c2308170cc8ca5a2ab8a2de76e4653575bdcc6 100755
--- a/scripts/deploy_dev.js
+++ b/scripts/deploy_dev.js
@@ -24,6 +24,10 @@ function DevDeployer() {
 
   this.sslpub = process.env['DEV_SSL_PUB'];
   this.sslpriv = process.env['DEV_SSL_PRIV'];
+  this.keypairs = [];
+  if (process.env['ADDITIONAL_KEYPAIRS']) {
+    this.keypairs = process.env['ADDITIONAL_KEYPAIRS'].split(',');
+  }
 
   if (!this.sslpub || !this.sslpriv) {
     throw("you must provide ssl cert paths via DEV_SSL_PUB & DEV_SSL_PRIV");
@@ -63,7 +67,21 @@ DevDeployer.prototype.configure = function(cb) {
   var config = { public_url: "https://dev.diresworb.org" };
   ssh.copyUpConfig(self.deets.ipAddress, config, function (err) {
     if (err) return cb(err);
-    ssh.copySSL(self.deets.ipAddress, self.sslpub, self.sslpriv, cb);
+    ssh.copySSL(self.deets.ipAddress, self.sslpub, self.sslpriv, function(err) {
+      if (err) return cb(err);
+
+      // now copy up addtional keypairs
+      var i = 0;
+      function copyNext() {
+        if (i == self.keypairs.length) return cb(null);
+        ssh.addSSHPubKey(self.deets.ipAddress, self.keypairs[i++], function(err) {
+          if (err) return cb(err);
+          self.emit('progress', "key added...");
+          copyNext();
+        });
+      }
+      copyNext();
+    });
   });
 }
 
diff --git a/scripts/deploy_server.js b/scripts/deploy_server.js
index 130bcd5ae06cb9276c2a0fa53343ddb0a9145036..f38338ee1bba2f6338472d75c2dd3a3385df2a97 100755
--- a/scripts/deploy_server.js
+++ b/scripts/deploy_server.js
@@ -176,7 +176,7 @@ Deployer.prototype.checkForUpdates = function() {
 var deployer = new Deployer();
 
 var currentLogFile = null;
-// a directory where we'll deployment logs
+// a directory where we'll keep deployment logs
 var deployLogDir = process.env['DEPLOY_LOG_DIR'] || temp.mkdirSync();
 
 var deployingSHA = null;