diff --git a/.gitignore b/.gitignore
index d1de4672c9cba194cca3c6165f0cb02ff7c0ab45..a556e41097bac6949842f2906a553797bc629bbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@
 /resources/static/build
 /resources/static/production
 /resources/static/i18n
+/resources/static/common/js/lib/bidbundle.js
+/resources/static/dialog/views/site
 .DS_Store
 Thumbs.db
 /locale
diff --git a/ChangeLog b/ChangeLog
index a8d23e7d8fe2df43a442c6a8a9021516d5ef0d65..6c574d645a7cbb690b42cf1f03e3ccd61cd74eee 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+train-2012.09.14:
+
+train-2012.08.31:
+  * Test/example code fixes: #2345, #2363
+  * allow id.watch() to be invoked from <head>: #2252
+  * KPI additions for reset password flow: #2281
+  * Improvements for developing on windows: #2263
+  * Code cleanup: #2347
+  * Documentation improvements: #2279
+  * Host sign-in button images on our service.
+
 train-2012.08.17:
   * Reseting your password now logs you out everywhere: #2026, #2307
   * SCHEMA CHANGE: add TIMESTAMP lastPasswordReset to user table for #2026
diff --git a/automation-tests/123done/tests/test_change_password.py b/automation-tests/123done/tests/test_change_password.py
index 621e694b57ded8647fffcd31647ff45044973afa..8648fe9aa907c7c17f82325d25cd6288cd8ef49f 100644
--- a/automation-tests/123done/tests/test_change_password.py
+++ b/automation-tests/123done/tests/test_change_password.py
@@ -27,9 +27,8 @@ class TestChangePassword:
         email = inbox.find_by_index(0)
 
         # Load the BrowserID link from the email in the browser
-        mozwebqa.selenium.get(email.verify_user_link)
         from browserid.pages.complete_registration import CompleteRegistration
-        CompleteRegistration(mozwebqa.selenium, mozwebqa.timeout)
+        CompleteRegistration(mozwebqa, email.verify_user_link)
 
         mozwebqa.selenium.get(mozwebqa.server_base_url)
         from browserid.pages.account_manager import AccountManager
@@ -44,6 +43,7 @@ class TestChangePassword:
         account_manager.click_password_done()
 
         account_manager.click_sign_out()
+        mozwebqa.selenium.execute_script('localStorage.clear()')
 
         home_pg.go_to_home_page()
 
diff --git a/automation-tests/123done/tests/test_new_user.py b/automation-tests/123done/tests/test_new_user.py
index ed04e9c7e0be50d9f901a48fb50b212db948feab..1ac70a8f8fc890b488df5c700a06a28568b190bd 100644
--- a/automation-tests/123done/tests/test_new_user.py
+++ b/automation-tests/123done/tests/test_new_user.py
@@ -27,13 +27,8 @@ class TestNewAccount:
         email = inbox.find_by_index(0)
 
         # Load the BrowserID link from the email in the browser
-        mozwebqa.selenium.get(email.verify_user_link)
         from browserid.pages.complete_registration import CompleteRegistration
-        complete_registration = CompleteRegistration(mozwebqa.selenium, mozwebqa.timeout)
-
-        # Check the message on the registration page reflects a successful registration!
-        Assert.contains("Thank you for signing up with Persona.", complete_registration.thank_you)
+        complete_registration = CompleteRegistration(mozwebqa, email.verify_user_link)
 
         home_pg.go_to_home_page()
-
         Assert.equal(home_pg.logged_in_user_email, user['email'])
diff --git a/automation-tests/browserid/pages/account_manager.py b/automation-tests/browserid/pages/account_manager.py
index 3b97ce51193a4cfc4f2ca583878350541755f78d..4a4e036dcd5f77c254beec95328d2d8132ca1216 100644
--- a/automation-tests/browserid/pages/account_manager.py
+++ b/automation-tests/browserid/pages/account_manager.py
@@ -28,9 +28,7 @@ class AccountManager(Base):
 
     @property
     def signed_in(self):
-        WebDriverWait(self.selenium, self.timeout).until(
-            lambda s: s.execute_script('return jQuery.active == 0'))
-        return 'not_authenticated' not in self.selenium.find_element(By.TAG_NAME, 'body').get_attribute('class')
+        return not self.selenium.find_element(By.CSS_SELECTOR, 'body.not_authenticated')
 
     @property
     def emails(self):
diff --git a/automation-tests/browserid/pages/complete_registration.py b/automation-tests/browserid/pages/complete_registration.py
index 141c1ea1fe33a2e89594cc6d625ab34d85423d83..be8c97efad722105c3615d447f1a588f9bf6a71b 100644
--- a/automation-tests/browserid/pages/complete_registration.py
+++ b/automation-tests/browserid/pages/complete_registration.py
@@ -12,20 +12,39 @@ from selenium.webdriver.support.ui import WebDriverWait
 
 class CompleteRegistration(Base):
 
+    _page_title = 'Mozilla Persona: Complete Registration'
     _email_locator = (By.ID, 'email')
     _password_locator = (By.ID, 'password')
     _finish_locator = (By.CSS_SELECTOR, 'div.submit > button')
     _thank_you_locator = (By.ID, 'congrats')
 
-    def __init__(self, selenium, timeout, expect='success'):
-        Base.__init__(self, selenium, timeout)
+    def __init__(self, mozwebqa, url, expect='redirect'):
+        """
+        class init method
+        :Args:
+        - url - the confirmation url from the email
+        - expect - redirect/success/reset/verify (default redirect)
+        """
+        Base.__init__(self, mozwebqa.selenium, mozwebqa.timeout)
+        print "the url" + url
+        self.selenium.get(url)
 
-        if expect == 'success':
+        if expect == 'redirect':
             WebDriverWait(self.selenium, self.timeout).until(
-                lambda s: s.find_element(*self._thank_you_locator).is_displayed())
+                lambda s: s.title != self._page_title,
+                "Complete Registration page did not redirect")
+        elif expect == 'success':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: 'Thank you' in s.find_element(*self._thank_you_locator).text,
+                "Complete Registration did not succeed")
+        elif expect == 'reset':
+            WebDriverWait(self.selenium, self.timeout).until(
+                lambda s: 'verified' in s.find_element(*self._thank_you_locator).text,
+                "Complete Registration did not succeed")
         elif expect == 'verify':
             WebDriverWait(self.selenium, self.timeout).until(
-                lambda s: s.find_element(*self._password_locator).is_displayed())
+                lambda s: s.find_element(*self._password_locator).is_displayed(),
+                "password field did not become visible")
         else:
             raise Exception('Unknown expect value: %s' % expect)
 
diff --git a/automation-tests/run.py b/automation-tests/run.py
index 39812f92833b369cd25492c45135f9548952ff5c..47ebc8f6c34552787867ec424f494d0b2594c1b1 100755
--- a/automation-tests/run.py
+++ b/automation-tests/run.py
@@ -5,6 +5,7 @@ import os
 import platform
 import subprocess
 import sys
+import pkg_resources
 
 
 # used to check for existence of virtualenv and pip.
@@ -59,13 +60,10 @@ def main():
         exit(1)
 
     # 2. check that virtualenv and pip exist. if not, bail.
-    if not which('pip'):
-        sys.stderr.write('pip must be installed; do "easy_install pip", ' +
-                         ' then try again\n')
-        exit(1)
-    if not which('virtualenv'):
-        sys.stderr.write('virtualenv must be installed; do "pip install ' +
-                         'virtualenv", then try again\n')
+    try:
+        pkg_resources.WorkingSet().require('pip', 'virtualenv')
+    except pkg_resources.DistributionNotFound as e:
+        sys.stderr.write('{package} must be installed\n'.format(package=e.message))
         exit(1)
 
     # 3. create the virtualenv if they asked you to install it or it's missing
diff --git a/automation-tests/run_saucelabs b/automation-tests/run_saucelabs
index 68e34ebd9976613f6f2d9af643def86920845bab..d140319beb8661acdbe801ff3ad3c8c03bb373f0 100755
--- a/automation-tests/run_saucelabs
+++ b/automation-tests/run_saucelabs
@@ -42,6 +42,7 @@ var globalPythonArgs = {
   "--saucelabs": sauceYAMLPath,
   "--webqatimeout": 90,
   "--destructive": null,
+  "-n": 5,
   "-q": null,
   '--capabilities': JSON.stringify({ "avoid-proxy":"true"})
 };
diff --git a/docs/I18N.md b/docs/I18N.md
index 1cfdfa59e37a09b06e68dc595bc901be378fc61f..8db9af698c5669d9507010ef2f6ba1b523ad21e1 100644
--- a/docs/I18N.md
+++ b/docs/I18N.md
@@ -4,13 +4,19 @@
 
 # i18n Support
 
+## New Localizations
+
+All Persona localization is handled by a community of volunteers using [Mozilla Verbatim](https://localize.mozilla.org/).
+
+To contribute a new localization, read [Localizing with Verbatim](https://developer.mozilla.org/en-US/docs/Localizing_with_Verbatim) and check out the ["BrowserID" project](https://localize.mozilla.org/projects/browserid/) on Verbatim. For more information on joining or starting a localization team, visit [l10n.mozilla.org](https://l10n.mozilla.org/).
+
+## Development
+
 Working with a localized version of BrowserID is totally optional for
 casual development.
 
 To get started, please [read the l10n locale doc](http://svn.mozilla.org/projects/l10n-misc/trunk/browserid/README).
 
-## Development
-
 Any copy, label, or error message that will be shown to users **should** be wrapped in a gettext function.
 
 These strings must be evaluated in the scope of a request, so we know which locale the user has.
diff --git a/docs/changes/2252.rp b/docs/changes/2252.rp
new file mode 100644
index 0000000000000000000000000000000000000000..8b95f24a90c89f1a1eb06edf4c52d83013fba725
--- /dev/null
+++ b/docs/changes/2252.rp
@@ -0,0 +1,2 @@
+RPs can now call navigator.id.watch from the head of their document. 
+
diff --git a/docs/changes/Changelog-Maintenance.md b/docs/changes/Changelog-Maintenance.md
new file mode 100644
index 0000000000000000000000000000000000000000..c37b2510b7ae3f63685ae9522dba313e21e21742
--- /dev/null
+++ b/docs/changes/Changelog-Maintenance.md
@@ -0,0 +1,35 @@
+
+# Changelog Snippets
+
+This directory contains snippets of text that should be added to the Changelogs for various audiences. For details, please see:
+
+ https://github.com/mozilla/browserid/wiki/Changelog-Maintenance
+
+Each branch submitted for merging should contain snippets aimed at any audiences that need to learn about the significant changes in that branch/
+
+Each pull-request should add files to this `docs/changes/` directory. The files should be named `$issuenumber.$type`, e.g. `1234.idp` or `6543.ops`. Patches which don't have specific issue numbers should use some other probably-unique identifier (a few words that summarize the issue) instead of the Issue number.
+
+These snippets should contain a few sentences explaining what specific audiences need to know about the effects of the patch. If an audience doesn't need to know about the changes, then the patch should not include a snippet for that audience. Many bugfixes will have no snippets at all.
+
+The various audiences, type suffixes, and what they care about, are:
+
+ * IdPs (`123.idp`): Identity Providers care about format changes to the `/.well-known/browserid` file and the public keys inside it, changes to the certificate-signing process that they must implement, and the `navigator.id` APIs used by their authentication and provisioning pages.
+ * RPs (`123.rp`): Relying Parties care about changes to the URLs of `include.js` and the verifier service, API changes of `navigator.id` and the verifier service, and new features their users may be able to take advantage of.
+ * devops/prodops (`123.ops`): Operations personnel care about changes to the way the browserid services are deployed on Mozilla servers: new processes (like the "router" and "keysigner"), new outbound network requests (firewall rules), dependencies on third-party libraries and tools (including node.js), database schema changes, significant changes to expected load or database access patterns. They should also review code that adds new database queries for efficiency.
+ * QA (`123.qa`): QA staff care about new features that need test coverage, behavioral changes which affect QA methodology, and dialog/UI changes that require "bidpom" model updates (e.g. CSS selectors).
+
+We may add other audiences in the future.
+
+Reviewers should check for snippets in the pull-requests they review, and should feel free to ask patch authors to write them when necessary.
+
+These snippets should be addressed to the audience in question, and cite published documentation (if available), like this:
+
+    RPs can now include a `siteLogo` URL in their `navigator.id.request()`
+    call, which should point to a site-relative path of a .png image file
+    that will be displayed in the sign-in dialog. See XYZ for details.
+
+## The Release Process
+
+Periodically, just before a train is branched, the Release Manager (i.e. Lloyd) will land a commit which deletes all the snippet files and merges their contents into more-nicely-formatted NEWS or ChangeLog file. The RM will exercise editorial judgement on what needs to be visible (sometimes an IdP-oriented change needs to be announced to RPs too), and gets to add detail or delete low-level entries entirely.
+
+If the prose quality of the snippets is high enough, this process may be done mechanically some day.
diff --git a/example/primary/index.html b/example/primary/index.html
index aa0390506052a88a2247f53fd71383f35fa3071f..8a4a6eea630f2fd5bceb2a776c3cde5267147f60 100644
--- a/example/primary/index.html
+++ b/example/primary/index.html
@@ -14,7 +14,7 @@ BrowserID Example Primary
 body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif; }
 .title { font-size: 2em; font-weight: bold; text-align: center; margin: 1.5em; }
 .intro { font-size: 1.2em; width: 600px; margin: auto; }
-.main { text-align: center; margin-top: 2em; font-size: 1.2em; width: 500px; margin: auto; display: none; }
+.main { text-align: center; margin-top: 2em; font-size: 1.2em; width: 500px; margin: auto; }
 #whoareyou { font-weight: bold; }
 
 </style>
@@ -32,11 +32,11 @@ body { margin: auto; font: 13px/1.5 Helvetica, Arial, 'Liberation Sans', FreeSan
   You are logged in as <span id="whoareyou"></span>.  <a id="logout" href="#">logout</a>.
 </div>
 
-<div class="main" id="logged_out">
+<form class="main" id="logged_out">
   You are not logged in.  Who would you like to be?
-  <input type="text">
+  <input type="text" autofocus>
   <button>doit</button>
-</div>
+</form>
 
 <script type="text/javascript" src="jquery.js"></script>
 <script type="text/javascript">
@@ -53,7 +53,8 @@ $(document).ready(function() {
         }
       });
   }
-  $("button").click(function(e) {
+  $("form").submit(function(e) {
+    e.preventDefault();
     $.get('/api/login', { user: $.trim($("input").val()) })
       .success(function(r) {
         updateWhoIAm();
diff --git a/example/primary/sign_in.html b/example/primary/sign_in.html
index 838080d2fb96fb83dfe87695538437e7269e1d4d..56c5764bd5f09a8d190b4801935f0f3270bcfc95 100644
--- a/example/primary/sign_in.html
+++ b/example/primary/sign_in.html
@@ -28,7 +28,7 @@ button { line-height: 20px; }
 
 <div class="main" id="logged_out">
   Sign in as <span id="who">...</span>
-  <button>doit</button>
+  <button autofocus>doit</button>
   <a href="#" id="cancel">cancel</a>
 </div>
 
diff --git a/lockdown.json b/lockdown.json
index 8b3b82d3faa8d38442a4370bfdffd5578d933e51..128ae832f881f8c1a94f28969d789556f5e88c0f 100644
--- a/lockdown.json
+++ b/lockdown.json
@@ -15,7 +15,7 @@
     "0.0.5": "971b8995078d83c80f2372f134c496e71b293a46"
   },
   "awsbox": {
-    "0.2.15": "10fe4fa8833c4a0310469446ac222dfeaf2fe17c"
+    "0.2.17": "5ab4677dc0ab725ea3d1bb5a127e0253df735e6d"
   },
   "bcrypt": {
     "0.7.1": "923e2623331211adcab6ac84ec4fcd41713e1e58"
diff --git a/package.json b/package.json
index f4be9a5a99f4cb15cd86ca72d797f8c99aad56a6..dc85a8c94d0bc4744c127f3d6bf2bb2b2f577ef7 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
     },
     "devDependencies": {
         "vows": "0.5.13",
-        "awsbox": "0.2.15",
+        "awsbox": "0.2.17",
         "irc": "0.3.3",
         "jshint": "0.7.1",
         "minimatch": "0.2.6",
@@ -47,7 +47,8 @@
     },
     "scripts": {
         "preinstall": "./scripts/lockdown",
-        "postinstall": "node ./scripts/generate_ephemeral_keys.js",
+        "postinstall": "node ./scripts/postinstall.js",
+        "postupdate": "node ./scripts/postinstall.js",
         "test": "./scripts/test",
         "start": "node ./scripts/run_locally.js"
     },
diff --git a/resources/static/common/js/lib/bidbundle.js b/resources/static/common/js/lib/bidbundle.js
deleted file mode 120000
index 00c7194bcdfbd8c359d6471c712c07c53bf442b0..0000000000000000000000000000000000000000
--- a/resources/static/common/js/lib/bidbundle.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../node_modules/jwcrypto/bidbundle.js
\ No newline at end of file
diff --git a/resources/static/common/js/lib/dom-jquery.js b/resources/static/common/js/lib/dom-jquery.js
index 78ab5814c90045bf6e8077f1481d2fdb3e15cdc7..cfbeef859da2e40648c5788049599db9364708ab 100644
--- a/resources/static/common/js/lib/dom-jquery.js
+++ b/resources/static/common/js/lib/dom-jquery.js
@@ -69,6 +69,17 @@ BrowserID.DOM = ( function() {
             return jQuery( selector ).children()[ index ];
         },
 
+        /**
+        * Find the closest ancestor that matches the selector
+        * @method closest
+        * @param {selector || element} selector - element to get children for
+        * @param {selector || element} searchFrom - element to search from
+        * @return {array} The closest ancestor matching the selector
+        */
+        closest: function( selector, searchFrom ) {
+          return jQuery( searchFrom ).closest( selector );
+        },
+
         /**
         * Iterate over a set of elements
         * @method forEach
@@ -260,6 +271,16 @@ BrowserID.DOM = ( function() {
             return el;
         },
 
+        /**
+        * Insert an element after another element
+        * @method insertAfter
+        * @param {selector || element} elementToInsert
+        * @param {selector || element} elementToInsertBefore
+        */
+        insertAfter: function( elementToInsert, elementToInsertAfter ) {
+          jQuery( elementToInsertAfter ).after( elementToInsert );
+        },
+
         /**
         * Insert an element before another element
         * @method insertBefore
diff --git a/resources/static/common/js/modules/interaction_data.js b/resources/static/common/js/modules/interaction_data.js
index 2f399e001cdd27db9c336d6ef30f45059dc13e5c..51cff25a680a0180914dc4a46bf8ee9fb7e21180 100644
--- a/resources/static/common/js/modules/interaction_data.js
+++ b/resources/static/common/js/modules/interaction_data.js
@@ -78,7 +78,11 @@ BrowserID.Modules.InteractionData = (function() {
     user_staged: "user.user_staged",
     user_confirmed: "user.user_confirmed",
     email_staged: "user.email_staged",
-    email_confirmed: "user.email_confrimed",
+    email_confirmed: "user.email_confirmed",
+    reset_password_staged: "user.reset_password_staged",
+    reset_password_confirmed: "user.reset_password_confirmed",
+    reverify_email_staged: "user.reverify_email_staged",
+    reverify_email_confirmed: "user.reverify_email_confirmed",
     notme: "user.logout",
     enter_password: "authenticate.enter_password",
     password_submit: "authenticate.password_submitted",
diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js
index afd16ccd7463f0ab45f900254028c2016f0628d2..34d1eb25d2cf59c431a3a0203321f66b9b72054d 100644
--- a/resources/static/dialog/js/misc/state.js
+++ b/resources/static/dialog/js/misc/state.js
@@ -226,8 +226,6 @@ BrowserID.State = (function() {
 
     handleState("user_confirmed", handleEmailConfirmed);
 
-    handleState("staged_address_confirmed", handleEmailConfirmed);
-
     handleState("primary_user", function(msg, info) {
       self.addPrimaryUser = !!info.add;
       var email = self.email = info.email,
@@ -370,6 +368,8 @@ BrowserID.State = (function() {
 
     handleState("reverify_email_staged", handleEmailStaged.curry("doConfirmReverifyEmail"));
 
+    handleState("reverify_email_confirmed", handleEmailConfirmed);
+
     handleState("email_valid_and_ready", function(msg, info) {
       // this state is only called after all checking is done on the email
       // address.  For secondaries, this means the email has been validated and
@@ -435,6 +435,8 @@ BrowserID.State = (function() {
       }
     });
 
+    handleState("reset_password_confirmed", handleEmailConfirmed);
+
     handleState("notme", function() {
       startAction("doNotMe");
     });
diff --git a/resources/static/dialog/js/modules/actions.js b/resources/static/dialog/js/modules/actions.js
index 02c6b5748c5eb989656bf8de8e61429172742e91..0184bfe38c7948bea1f5acae9e76367cf32213bb 100644
--- a/resources/static/dialog/js/modules/actions.js
+++ b/resources/static/dialog/js/modules/actions.js
@@ -17,18 +17,19 @@ BrowserID.Modules.Actions = (function() {
       onsuccess,
       onerror;
 
-  function startService(name, options) {
+  function startService(name, options, reported_service_name) {
+    mediator.publish("service", { name: reported_service_name || name });
+
     // Only one service outside of the main dialog allowed.
     if(runningService) {
       serviceManager.stop(runningService);
     }
+
     var module = serviceManager.start(name, options);
     if(module) {
       runningService = name;
     }
 
-    mediator.publish("service", { name: name });
-
     return module;
   }
 
@@ -98,7 +99,7 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doResetPassword: function(info) {
-      startService("set_password", _.extend(info, { password_reset: true }));
+      startService("set_password", _.extend(info, { password_reset: true }), "reset_password");
     },
 
     doStageResetPassword: function(info) {
@@ -106,7 +107,7 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doConfirmResetPassword: function(info) {
-      startRegCheckService.call(this, info, "waitForPasswordResetComplete", "staged_address_confirmed");
+      startRegCheckService.call(this, info, "waitForPasswordResetComplete", "reset_password_confirmed");
     },
 
     doStageReverifyEmail: function(info) {
@@ -114,7 +115,7 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doConfirmReverifyEmail: function(info) {
-      startRegCheckService.call(this, info, "waitForEmailReverifyComplete", "staged_address_confirmed");
+      startRegCheckService.call(this, info, "waitForEmailReverifyComplete", "reverify_email_confirmed");
     },
 
     doAssertionGenerated: function(info) {
diff --git a/resources/static/dialog/views/site b/resources/static/dialog/views/site
deleted file mode 120000
index f5a3723db214a8e9676c1dbf727502ecb94bce16..0000000000000000000000000000000000000000
--- a/resources/static/dialog/views/site
+++ /dev/null
@@ -1 +0,0 @@
-../../../views/
\ No newline at end of file
diff --git a/resources/static/i/email_sign_in_black.png b/resources/static/i/email_sign_in_black.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2f98f8b28a410587b5cf8c69bc58072627eb236
Binary files /dev/null and b/resources/static/i/email_sign_in_black.png differ
diff --git a/resources/static/i/email_sign_in_blue.png b/resources/static/i/email_sign_in_blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..5986020ac0f668e2135b1950ef2148d831a1e16f
Binary files /dev/null and b/resources/static/i/email_sign_in_blue.png differ
diff --git a/resources/static/i/email_sign_in_red.png b/resources/static/i/email_sign_in_red.png
new file mode 100644
index 0000000000000000000000000000000000000000..44dff6cd8255080c4574c14616d8b7f6b81d37bb
Binary files /dev/null and b/resources/static/i/email_sign_in_red.png differ
diff --git a/resources/static/i/persona_sign_in_black.png b/resources/static/i/persona_sign_in_black.png
index 1977689b6f046720e8b85164b62039ac157aa9d5..ac3b5632a8ae8c6a4c7389ce5bebed80d5161bca 100644
Binary files a/resources/static/i/persona_sign_in_black.png and b/resources/static/i/persona_sign_in_black.png differ
diff --git a/resources/static/i/persona_sign_in_blue.png b/resources/static/i/persona_sign_in_blue.png
index bed31b3ef5c4ef82ec27c096ab8a6835f02d7e66..ab88a715408ebf8cafcad8d4a2dfe1251efb9e09 100644
Binary files a/resources/static/i/persona_sign_in_blue.png and b/resources/static/i/persona_sign_in_blue.png differ
diff --git a/resources/static/i/persona_sign_in_red.png b/resources/static/i/persona_sign_in_red.png
index c7e935f057f5f182f6b262b2fd38a13f7a9d151c..e94e1ef78f6424bc0b18e90280b8a630ea1d1dde 100644
Binary files a/resources/static/i/persona_sign_in_red.png and b/resources/static/i/persona_sign_in_red.png differ
diff --git a/resources/static/i/plain_sign_in_black.png b/resources/static/i/plain_sign_in_black.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b2018a79831721c82d90b9c5859f7fb5828e7e3
Binary files /dev/null and b/resources/static/i/plain_sign_in_black.png differ
diff --git a/resources/static/i/plain_sign_in_blue.png b/resources/static/i/plain_sign_in_blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..82594ba8232e8d0570b459a06bc69e723a890284
Binary files /dev/null and b/resources/static/i/plain_sign_in_blue.png differ
diff --git a/resources/static/i/plain_sign_in_red.png b/resources/static/i/plain_sign_in_red.png
new file mode 100644
index 0000000000000000000000000000000000000000..64690430cf31972309607fb0ebeb9560223f85ed
Binary files /dev/null and b/resources/static/i/plain_sign_in_red.png differ
diff --git a/resources/static/i/sign_in_blue.png b/resources/static/i/sign_in_blue.png
index 919a5c7a3598ff2665d509b9dcfb03cdc1753a0f..19a9f5b148ecf4d34eecb4a5cd50d531afe956b8 100644
Binary files a/resources/static/i/sign_in_blue.png and b/resources/static/i/sign_in_blue.png differ
diff --git a/resources/static/i/sign_in_green.png b/resources/static/i/sign_in_green.png
index 7e84129b4aa2d798213addb68e2fd01c23815e3b..0d0e9b57c62fe1cd68c9b492c5712a5432e30a4e 100644
Binary files a/resources/static/i/sign_in_green.png and b/resources/static/i/sign_in_green.png differ
diff --git a/resources/static/i/sign_in_grey.png b/resources/static/i/sign_in_grey.png
index 467bde42b6c1d4d728050bf81e4cbbec32824b1d..1559d883e78426e064ff2a372fe7c4823a5162a6 100644
Binary files a/resources/static/i/sign_in_grey.png and b/resources/static/i/sign_in_grey.png differ
diff --git a/resources/static/i/sign_in_orange.png b/resources/static/i/sign_in_orange.png
index 1c7c7925d18bfd80f07e66b9248b63acfef64587..d5239532d0b4e54085e9ca37f91d4cd108472586 100644
Binary files a/resources/static/i/sign_in_orange.png and b/resources/static/i/sign_in_orange.png differ
diff --git a/resources/static/i/sign_in_red.png b/resources/static/i/sign_in_red.png
index 96d1f98fded025cafdb562d3d9e29de8fde0b7ff..de7570e7934a67b76b1ba371a7e61d6cbe04c75a 100644
Binary files a/resources/static/i/sign_in_red.png and b/resources/static/i/sign_in_red.png differ
diff --git a/resources/static/include_js/include.js b/resources/static/include_js/include.js
index 1df41c082075bbd6abed52e08d55423fbed8718b..2efabd0e17931b72f87db747f431f27d501b8d83 100644
--- a/resources/static/include_js/include.js
+++ b/resources/static/include_js/include.js
@@ -966,6 +966,8 @@
       ready: null
     };
 
+    var loggedInUser;
+
     var compatMode = undefined;
     function checkCompat(requiredMode) {
       if (requiredMode === true) {
@@ -981,18 +983,48 @@
     }
 
     var commChan,
+        waitingForDOM = false,
         browserSupported = BrowserSupport.isSupported();
 
+    function domReady(callback) {
+      if (document.addEventListener) {
+        document.addEventListener('DOMContentLoaded', function contentLoaded() {
+          document.removeEventListener('DOMContentLoaded', contentLoaded);
+          callback();
+        }, false);
+      } else if (document.attachEvent && document.readyState) {
+        document.attachEvent('onreadystatechange', function ready() {
+          var state = document.readyState;
+          // 'interactive' is the same as DOMContentLoaded,
+          // but not all browsers use it, sadly.
+          if (state === 'loaded' || state === 'complete' || state === 'interactive') {
+            document.detachEvent('onreadystatechange', ready);
+            callback();
+          }
+        });
+      }
+    }
+
+
     // this is for calls that are non-interactive
     function _open_hidden_iframe() {
       // If this is an unsupported browser, do not even attempt to add the
       // IFRAME as doing so will cause an exception to be thrown in IE6 and IE7
       // from within the communication_iframe.
       if(!browserSupported) return;
+      var doc = window.document;
+
+      // can't attach iframe and make commChan without the body
+      if (!doc.body) {
+        if (!waitingForDOM) {
+          domReady(_open_hidden_iframe);
+          waitingForDOM = true;
+        }
+        return;
+      }
 
       try {
         if (!commChan) {
-          var doc = window.document;
           var iframe = doc.createElement("iframe");
           iframe.style.display = "none";
           doc.body.appendChild(iframe);
@@ -1008,6 +1040,7 @@
               commChan.call({
                 method: 'loaded',
                 success: function(){
+                  // NOTE: Do not modify without reading GH-2017
                   if (observers.ready) observers.ready();
                 }, error: function() {
                 }
@@ -1022,6 +1055,13 @@
           commChan.bind('login', function(trans, params) {
             if (observers.login) observers.login(params);
           });
+
+          if (loggedInUser) {
+            commChan.notify({
+              method: 'loggedInUser',
+              params: loggedInUser
+            });
+          }
         }
       } catch(e) {
         // channel building failed!  let's ignore the error and allow higher
@@ -1075,22 +1115,14 @@
 
       observers.login = options.onlogin || null;
       observers.logout = options.onlogout || null;
+      // NOTE: Do not modify without reading GH-2017
       observers.ready = options.onready || null;
 
-      _open_hidden_iframe();
-
       // back compat support for loggedInEmail
       checkRenamed(options, "loggedInEmail", "loggedInUser");
+      loggedInUser = options.loggedInUser;
 
-      // check that the commChan was properly initialized before interacting with it.
-      // on unsupported browsers commChan might still be undefined, in which case
-      // we let the dialog display the "unsupported browser" message upon spawning.
-      if (typeof options.loggedInUser !== 'undefined' && commChan) {
-        commChan.notify({
-          method: 'loggedInUser',
-          params: options.loggedInUser
-        });
-      }
+      _open_hidden_iframe();
     }
 
     function internalRequest(options) {
diff --git a/resources/static/pages/js/manage_account.js b/resources/static/pages/js/manage_account.js
index 6b515f8a60bdf5520990a64cd0d8e3c4b2a524f8..119febf71a1c0f2fb6c8466b5a6b9fa704911452 100644
--- a/resources/static/pages/js/manage_account.js
+++ b/resources/static/pages/js/manage_account.js
@@ -1,4 +1,5 @@
-/*globals BrowserID:true, _: true, confirm: true, displayEmails: true */
+/*globals BrowserID: true, _: true, confirm: true, format: true, gettext: true, EJS: true */
+
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -16,79 +17,83 @@ BrowserID.manageAccount = (function() {
       pageHelpers = bid.PageHelpers,
       cancelEvent = pageHelpers.cancelEvent,
       confirmAction = confirm,
+      complete = helpers.complete,
       doc = document,
       tooltip = bid.Tooltip,
       authLevel;
 
   function syncAndDisplayEmails(oncomplete) {
+    var self=this;
     user.syncEmails(function() {
-      displayStoredEmails(oncomplete);
+      displayStoredEmails.call(self, oncomplete);
     }, pageHelpers.getFailure(errors.syncEmails, oncomplete));
   }
 
   function displayStoredEmails(oncomplete) {
     var emails = user.getSortedEmailKeypairs();
     if (_.isEmpty(emails)) {
-      $("#content").hide();
+      dom.hide("#content");
     } else {
-      $("#content").show();
-      $("#vAlign").hide();
-      displayEmails(emails);
+      dom.show("#content");
+      dom.hide("#vAlign");
+      renderEmails.call(this, emails);
     }
-    oncomplete && oncomplete();
+    complete(oncomplete);
   }
 
   function removeEmail(email, oncomplete) {
-    function complete() {
-      oncomplete && oncomplete();
-    }
-
+    var self=this;
     user.syncEmails(function() {
       var emails = user.getStoredEmailKeypairs();
       if (!emails[email]) {
-        displayStoredEmails(oncomplete);
+        displayStoredEmails.call(self, oncomplete);
       }
       else if (_.size(emails) > 1) {
         if (confirmAction(format(gettext("Remove %(email) from your Persona account?"),
                                  { email: email }))) {
           user.removeEmail(email, function() {
-            displayStoredEmails(oncomplete);
+            displayStoredEmails.call(self, oncomplete);
           }, pageHelpers.getFailure(errors.removeEmail, oncomplete));
         }
         else {
-          complete();
+          complete(oncomplete);
         }
       }
       else {
         if (confirmAction(gettext("Removing the last address will cancel your Persona account.\nAre you sure you want to continue?"))) {
           user.cancelUser(function() {
             doc.location="/";
-            complete();
+            complete(oncomplete);
           }, pageHelpers.getFailure(errors.cancelUser, oncomplete));
         }
         else {
-          complete();
+          complete(oncomplete);
         }
       }
     }, pageHelpers.getFailure(errors.syncEmails, oncomplete));
   }
 
-  function displayEmails(emails) {
-    var list = $("#emailList").empty();
+  function renderEmails(emails) {
+    var self=this,
+        list = dom.getElements("#emailList");
+
+    dom.setInner(list, "");
 
     // Set up to use mustache style templating, the normal Django style blows
     // up the node templates
     _.templateSettings = {
         interpolate : /\{\{(.+?)\}\}/g
     };
-    var template = $("#templateUser").html();
+    var template = dom.getInner("#templateUser");
 
     _(emails).each(function(item) {
       var e = item.address,
           identity = _.template(template, { email: e });
 
-      var idEl = $(identity).appendTo(list);
-      idEl.find(".delete").click(cancelEvent(removeEmail.bind(null, e)));
+      var idEl = dom.appendTo(identity, list),
+          deleteButton = dom.getDescendentElements(".delete", idEl);
+
+      self.click(deleteButton, removeEmail.curry(e));
     });
   }
 
@@ -96,30 +101,25 @@ BrowserID.manageAccount = (function() {
     if (confirmAction(gettext("Are you sure you want to cancel your Persona account?"))) {
       user.cancelUser(function() {
         doc.location="/";
-        oncomplete && oncomplete();
+        complete(oncomplete);
       }, pageHelpers.getFailure(errors.cancelUser, oncomplete));
     }
   }
 
   function startEdit(event) {
-    // XXX add some helpers in the dom library to find section.
     event.preventDefault();
-    $(event.target).closest("section").addClass("edit");
+    dom.addClass(dom.closest("section", event.target), "edit");
   }
 
   function cancelEdit(event) {
     event.preventDefault();
-    $(event.target).closest("section").removeClass("edit");
+    dom.removeClass(dom.closest("section", event.target), "edit");
   }
 
-  function changePassword(oncomplete) {
+  function submit(oncomplete) {
     var oldPassword = dom.getInner("#old_password"),
         newPassword = dom.getInner("#new_password");
 
-    function complete(status) {
-      typeof oncomplete == "function" && oncomplete(status);
-    }
-
     function changePassword() {
       user.changePassword(oldPassword, newPassword, function(status) {
         if(status) {
@@ -131,32 +131,32 @@ BrowserID.manageAccount = (function() {
           tooltip.showTooltip("#tooltipInvalidPassword");
         }
 
-        complete(status);
+        complete(oncomplete, status);
       }, pageHelpers.getFailure(errors.updatePassword, oncomplete));
     }
 
     if(!oldPassword) {
       tooltip.showTooltip("#tooltipOldRequired");
-      complete(false);
+      complete(oncomplete, false);
     }
     else if(oldPassword.length < bid.PASSWORD_MIN_LENGTH || bid.PASSWORD_MAX_LENGTH < oldPassword.length) {
       // If the old password is out of range, we know it is invalid. Show the
       // tooltip. See issue #2121
       // - https://github.com/mozilla/browserid/issues/2121
       tooltip.showTooltip("#tooltipInvalidPassword");
-      complete(false);
+      complete(oncomplete, false);
     }
     else if(!newPassword) {
       tooltip.showTooltip("#tooltipNewRequired");
-      complete(false);
+      complete(oncomplete, false);
     }
     else if(newPassword === oldPassword) {
       tooltip.showTooltip("#tooltipPasswordsSame");
-      complete(false);
+      complete(oncomplete, false);
     }
     else if(newPassword.length < bid.PASSWORD_MIN_LENGTH || bid.PASSWORD_MAX_LENGTH < newPassword.length) {
       tooltip.showTooltip("#tooltipPasswordLength");
-      complete(false);
+      complete(oncomplete, false);
     }
     else if(authLevel !== "password") {
       var email = getSecondary();
@@ -169,7 +169,7 @@ BrowserID.manageAccount = (function() {
         }
         else {
           tooltip.showTooltip("#tooltipInvalidPassword");
-          complete(false);
+          complete(oncomplete, false);
         }
       }, pageHelpers.getFailure(errors.authenticate, oncomplete));
     }
@@ -189,7 +189,7 @@ BrowserID.manageAccount = (function() {
   function displayChangePassword(oncomplete) {
     var canSetPassword = !!getSecondary();
     dom[canSetPassword ? "addClass" : "removeClass"]("body", "canSetPassword");
-    oncomplete && oncomplete();
+    complete(oncomplete);
   }
 
   function getSecondary() {
@@ -202,48 +202,49 @@ BrowserID.manageAccount = (function() {
     }
   }
 
-  function init(options, oncomplete) {
-    options = options || {};
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      options = options || {};
 
-    if (options.document) doc = options.document;
-    if (options.confirm) confirmAction = options.confirm;
+      if (options.document) doc = options.document;
+      if (options.confirm) confirmAction = options.confirm;
 
-    var template = new EJS({ text: $("#templateManage").html() });
-    var manage = template.render({});
-    $("#hAlign").after(manage);
+      var self=this,
+          oncomplete = options.ready,
+          template = new EJS({ text: dom.getInner("#templateManage") }),
+          manage = template.render({});
 
-    dom.bindEvent("#cancelAccount", "click", cancelEvent(cancelAccount));
+      dom.insertAfter(manage, "#hAlign");
 
-    dom.bindEvent("button.edit", "click", startEdit);
-    dom.bindEvent("button.done", "click", cancelEdit);
-    dom.bindEvent("#edit_password_form", "submit", cancelEvent(changePassword));
+      self.click("#cancelAccount", cancelAccount);
 
-    user.checkAuthentication(function(auth_level) {
-      authLevel = auth_level;
+      self.bind("button.edit", "click", startEdit);
+      self.bind("button.done", "click", cancelEdit);
 
-      syncAndDisplayEmails(function() {
-        displayHelpTextToNewUser();
-        displayChangePassword(oncomplete);
-      });
-    }, pageHelpers.getFailure(errors.checkAuthentication, oncomplete));
-  }
+      user.checkAuthentication(function(auth_level) {
+        authLevel = auth_level;
 
-  // BEGIN TESTING API
-  function reset() {
-    dom.unbindEvent("#cancelAccount", "click");
+        syncAndDisplayEmails.call(self, function() {
+          displayHelpTextToNewUser();
+          displayChangePassword(oncomplete);
+        });
+      }, pageHelpers.getFailure(errors.checkAuthentication, oncomplete));
 
-    dom.unbindEvent("button.edit", "click");
-    dom.unbindEvent("button.done", "click");
-    dom.unbindEvent("#edit_password_form", "submit");
-  }
+      Module.sc.start.call(self, options);
+    },
+
+    submit: submit
+
+    // BEGIN TESTING API
+    ,
+    cancelAccount: cancelAccount,
+    removeEmail: removeEmail,
+    changePassword: submit
+    // END TESTING API
+  });
 
-  init.reset = reset;
-  init.cancelAccount = cancelAccount;
-  init.removeEmail = removeEmail;
-  init.changePassword = changePassword;
-  // END TESTING API
 
-  return init;
+  return Module;
 
 }());
 
diff --git a/resources/static/pages/js/start.js b/resources/static/pages/js/start.js
index 9297dc6b206442fd4f8f242b6924b6152524da28..65a7b7121db3ad87d4a5cb0e842c9e2ccdba9f78 100644
--- a/resources/static/pages/js/start.js
+++ b/resources/static/pages/js/start.js
@@ -200,7 +200,8 @@ $(function() {
       dom.addClass("body", "authenticated");
 
       if (!path || path === "/") {
-        bid.manageAccount();
+        var module = bid.manageAccount.create();
+        module.start({});
         $(window).trigger("resize");
       }
 
diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js
index f77c0e346bdfbc385e6276f33ace5912deb4232b..10f6fad08efa6f583898e2202269f451819417c1 100644
--- a/resources/static/test/cases/dialog/js/misc/state.js
+++ b/resources/static/test/cases/dialog/js/misc/state.js
@@ -62,7 +62,7 @@
     });
   }
 
-  function testVerifyStagedAddress(startMessage, verifyScreenAction) {
+  function testVerifyStagedAddress(startMessage, confirmationMessage, verifyScreenAction) {
     // start with a site name to ensure the site name is passed to the
     // verifyScreenAction.
     mediator.publish("start", { siteName: "Unit Test Site" });
@@ -88,7 +88,7 @@
     // addresses are synced.  Add the test email and make sure the email_chosen
     // message is triggered.
     storage.addSecondaryEmail(TEST_EMAIL, { unverified: true });
-    mediator.publish("staged_address_confirmed");
+    mediator.publish(confirmationMessage);
   }
 
 
@@ -206,8 +206,8 @@
     ok(actions.info.doRPInfo.privacyPolicy, "doRPInfo called with privacyPolicy set");
   });
 
-  asyncTest("user_staged - call doConfirmUser", function() {
-    testVerifyStagedAddress("user_staged", "doConfirmUser");
+  asyncTest("user_staged to user_confirmed - call doConfirmUser", function() {
+    testVerifyStagedAddress("user_staged", "user_confirmed", "doConfirmUser");
   });
 
   asyncTest("user_confirmed - redirect to email_chosen", function() {
@@ -228,8 +228,8 @@
     }
   });
 
-  asyncTest("email_staged - call doConfirmEmail", function() {
-    testVerifyStagedAddress("email_staged", "doConfirmEmail");
+  asyncTest("email_staged to email_confirmed - call doConfirmEmail", function() {
+    testVerifyStagedAddress("email_staged", "email_confirmed", "doConfirmEmail");
   });
 
   asyncTest("primary_user with already provisioned primary user - redirect to primary_user_ready", function() {
@@ -323,8 +323,8 @@
   });
 
 
-  asyncTest("reset_password_staged to staged_address_confirmed - call doConfirmResetPassword then doEmailConfirmed", function() {
-    testVerifyStagedAddress("reset_password_staged", "doConfirmResetPassword");
+  asyncTest("reset_password_staged to reset_password_confirmed - call doConfirmResetPassword then doEmailConfirmed", function() {
+    testVerifyStagedAddress("reset_password_staged", "reset_password_confirmed", "doConfirmResetPassword");
   });
 
 
@@ -573,8 +573,8 @@
     testActionStarted("doStageReverifyEmail", { email: TEST_EMAIL });
   });
 
-  asyncTest("reverify_email_staged - call doConfirmReverifyEmail", function() {
-    testVerifyStagedAddress("reverify_email_staged", "doConfirmReverifyEmail");
+  asyncTest("reverify_email_staged to reverify_email_confirmed - call doConfirmReverifyEmail", function() {
+    testVerifyStagedAddress("reverify_email_staged", "reverify_email_confirmed", "doConfirmReverifyEmail");
   });
 
   asyncTest("window_unload - set the final KPIs", function() {
diff --git a/resources/static/test/cases/dialog/js/modules/actions.js b/resources/static/test/cases/dialog/js/modules/actions.js
index 7c44e045bcdeaa795310336ccc2db0452eea215b..db8682217aa149659dfc14b55b68b8782902e6c0 100644
--- a/resources/static/test/cases/dialog/js/modules/actions.js
+++ b/resources/static/test/cases/dialog/js/modules/actions.js
@@ -9,6 +9,7 @@
   var bid = BrowserID,
       user = bid.User,
       storage = bid.Storage,
+      mediator = bid.Mediator,
       controller,
       el,
       testHelpers = bid.TestHelpers,
@@ -19,17 +20,25 @@
     controller.start(config);
   }
 
-  function testActionStartsModule(actionName, actionOptions, expectedModule) {
+  function testActionStartsModule(actionName, actionOptions, expectedModule, expectedServiceName) {
     createController({
       ready: function() {
-        var error;
+        var error,
+            reportedServiceName;
+
+        // Check that KPI service reporting is acting as expected.
+        mediator.subscribe("service", function(msg, data) {
+          reportedServiceName = data.name;
+        });
+
         try {
           controller[actionName](actionOptions);
         } catch(e) {
           error = e;
         }
 
-        equal(error, "module not registered for " + expectedModule, "correct service started");
+        equal(error, "module not registered for " + expectedModule, "correct module started");
+        equal(reportedServiceName, expectedServiceName || expectedModule, "correct service name");
         start();
       }
     });
@@ -116,7 +125,7 @@
   });
 
   asyncTest("doResetPassword - call the set_password controller with reset_password true", function() {
-    testActionStartsModule('doResetPassword', { email: TEST_EMAIL }, "set_password");
+    testActionStartsModule('doResetPassword', { email: TEST_EMAIL }, "set_password", "reset_password");
   });
 
   asyncTest("doStageResetPassword - trigger reset_password_staged", function() {
diff --git a/resources/static/test/cases/pages/js/manage_account.js b/resources/static/test/cases/pages/js/manage_account.js
index 2b5caff3e100f3cb8d860c2bbe900ccd43059585..42cbd863bda92036f04215201e4c32a16fd8f18b 100644
--- a/resources/static/test/cases/pages/js/manage_account.js
+++ b/resources/static/test/cases/pages/js/manage_account.js
@@ -17,7 +17,8 @@
       mocks = {
         confirm: function() { return true; },
         document: { location: "" }
-      };
+      },
+      controller;
 
   module("pages/js/manage_account", {
     setup: function() {
@@ -31,6 +32,12 @@
     }
   });
 
+  function createController(options, ready) {
+    options.ready = ready;
+    controller = bid.manageAccount.create();
+    controller.start(options);
+  }
+
   function testPasswordChangeSuccess(oldPass, newPass, msg) {
     testPasswordChange(oldPass, newPass, function(status) {
       equal(status, true, msg);
@@ -54,18 +61,18 @@
   }
 
   function testPasswordChange(oldPass, newPass, testStrategy, msg) {
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       $("#old_password").val(oldPass);
       $("#new_password").val(newPass);
 
-      bid.manageAccount.changePassword(testStrategy);
+      controller.changePassword(testStrategy);
     });
   }
 
   asyncTest("no email addresses are displayed if there are no children", function() {
     xhr.useResult("no_identities");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       equal($("#emailList").children().length, 0, "no children have been added");
       start();
     });
@@ -74,7 +81,7 @@
   asyncTest("show sorted email addresses", function() {
     xhr.useResult("multiple");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       equal($("#emailList").children().length, 2, "there two children added");
 
       var firstLI = $("#testuser2_testuser_com");
@@ -89,7 +96,7 @@
   asyncTest("sync XHR error on startup", function() {
     xhr.useResult("ajaxError");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       equal(testHelpers.errorVisible(), true, "error message is visible on XHR error");
       start();
     });
@@ -99,9 +106,9 @@
     // start with multiple addresses.
     xhr.useResult("multiple");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       // switch to a single address return on the sync.
-      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
+      controller.removeEmail("testuser@testuser.com", function() {
         equal($("#emailList").children().length, 1, "after removing an email, only one remains");
         start();
       });
@@ -112,9 +119,9 @@
     // start with multiple addresses.
     xhr.useResult("multiple");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       xhr.useResult("ajaxError");
-      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
+      controller.removeEmail("testuser@testuser.com", function() {
         equal(testHelpers.errorVisible(), true, "error message is visible on XHR error");
         start();
       });
@@ -122,8 +129,8 @@
   });
 
   asyncTest("removeEmail with single email cancels account", function() {
-    bid.manageAccount(mocks, function() {
-      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
+    createController(mocks, function() {
+      controller.removeEmail("testuser@testuser.com", function() {
         equal(mocks.document.location, "/", "redirection happened");
         start();
       });
@@ -131,8 +138,8 @@
   });
 
   asyncTest("removeEmail doesn't cancel the account when removing a non-existent e-mail", function() {
-    bid.manageAccount(mocks, function() {
-      bid.manageAccount.removeEmail("non@existent.com", function() {
+    createController(mocks, function() {
+      controller.removeEmail("non@existent.com", function() {
         notEqual(mocks.document.location, "/", "redirection did not happen");
         start();
       });
@@ -140,9 +147,9 @@
   });
 
   asyncTest("removeEmail doesn't cancel the account when out of sync with the server", function() {
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       xhr.useResult("multiple");
-      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
+      controller.removeEmail("testuser@testuser.com", function() {
         notEqual(mocks.document.location, "/", "redirection did not happen");
         start();
       });
@@ -152,10 +159,10 @@
   asyncTest("removeEmail with single email cancels account and XHR error", function() {
     xhr.useResult("valid");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       xhr.useResult("ajaxError");
 
-      bid.manageAccount.removeEmail("testuser@testuser.com", function() {
+      controller.removeEmail("testuser@testuser.com", function() {
         equal(testHelpers.errorVisible(), true, "error message is visible on XHR error");
         start();
       });
@@ -163,8 +170,8 @@
   });
 
   asyncTest("cancelAccount", function() {
-    bid.manageAccount(mocks, function() {
-      bid.manageAccount.cancelAccount(function() {
+    createController(mocks, function() {
+      controller.cancelAccount(function() {
         equal(mocks.document.location, "/", "redirection happened");
         start();
       });
@@ -172,9 +179,9 @@
   });
 
   asyncTest("cancelAccount with XHR error", function() {
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       xhr.useResult("ajaxError");
-      bid.manageAccount.cancelAccount(function() {
+      controller.cancelAccount(function() {
         equal(testHelpers.errorVisible(), true, "error message is visible on XHR error");
         start();
       });
@@ -182,10 +189,10 @@
   });
 
   asyncTest("first time a user goes to page should see help text", function() {
-    bid.manageAccount(mocks,  function() {
+    createController(mocks,  function() {
       equal($("body").hasClass("newuser"), true, "body has the newuser class on first visit");
 
-      bid.manageAccount(mocks, function() {
+      createController(mocks, function() {
         equal($("body").hasClass("newuser"), false, "body does not have the newuser class on repeat visits");
         start();
       });
@@ -195,7 +202,7 @@
   asyncTest("user with only primary emails should not have 'canSetPassword' class", function() {
     xhr.useResult("primary");
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       equal($("body").hasClass("canSetPassword"), false, "canSetPassword class not added to body");
       start();
     });
@@ -204,7 +211,7 @@
   asyncTest("user with >= 1 secondary email should see have 'canSetPassword' class", function() {
     storage.addEmail("primary_user@primaryuser.com", { type: "secondary" });
 
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       equal($("body").hasClass("canSetPassword"), true, "canSetPassword class added to body");
       start();
     });
@@ -240,13 +247,13 @@
   });
 
   asyncTest("changePassword with XHR error - error message", function() {
-    bid.manageAccount(mocks, function() {
+    createController(mocks, function() {
       xhr.useResult("invalid");
 
       $("#old_password").val("oldpassword");
       $("#new_password").val("newpassword");
 
-      bid.manageAccount.changePassword(function(status) {
+      controller.changePassword(function(status) {
         equal(status, false, "on xhr error, status is false");
         start();
       });
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 8053729f5cc6a7ac726a055f218a285bcb2b55eb..73051f2c43e5468c69cf3cc9cfa99015c5595f03 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -308,7 +308,7 @@ BrowserID.TestHelpers = (function() {
     testElementFocused: function(selector, msg) {
       var focusedEl = $(":focus");
 
-      if (focusedEl.is(selector)) {
+      if ($(selector).is(":focus")) {
         ok(true, msg || selector + " is focused");
       }
       else {
@@ -317,7 +317,7 @@ BrowserID.TestHelpers = (function() {
         // check to see if it is possible to focus. If it is possible, this is
         // a failure.  If it is not possible, print a message and continue.
         // Remove the element when complete.
-        var input = $("<input type='text' />").appendTo("body").focus();
+        var input = $("<input type='radio' />").appendTo("body").focus();
         if (input.is(":focus")) {
           ok(false, msg || selector + " is focused");
           // refocus the original input element.
diff --git a/scripts/assign_issues.js b/scripts/assign_issues.js
index 01a4da318986fbc248d37e1c9f434cd3118de19e..83db42e8fe8375735cf9e2593239b393e7d323ee 100755
--- a/scripts/assign_issues.js
+++ b/scripts/assign_issues.js
@@ -8,12 +8,13 @@ const https = require('https');
 
 // people to get issues, and the issues that were assigned to them
 var people = {
-  'jedp': [],
+//  'jedp': [],
   'seanmonstar': [],
-  'ozten': [],
+//  'ozten': [],
   'lloyd': [],
   'shane-tomlinson': [],
-  'benadida': []
+  'zaach': []
+//  'benadida': []
 };
 
 var auth = process.env.AUTH;
diff --git a/scripts/awsbox/post_deploy.sh b/scripts/awsbox/post_deploy.sh
index 766cc41a06822f9d9a07f9661997867e55dcc657..ef422034083e008593539d9c9fa5197c9098185b 100755
--- a/scripts/awsbox/post_deploy.sh
+++ b/scripts/awsbox/post_deploy.sh
@@ -2,7 +2,7 @@
 
 if [ ! -f $HOME/var/root.cert ] ; then
     echo ">> generating keypair"
-    scripts/generate_ephemeral_keys.js
+    scripts/postinstall.js
     mv var/root.{cert,secretkey} $HOME/var
 else
     echo ">> no keypair needed.  you gots one"
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 570585ed39f5a04341ce6dac4c04ef24c5dc6d69..87669b91353a045ddc84d9e4a54f8aac73b8b1e0 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.08.31
+Version:       0.2012.09.14
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Gene Wood <gene@mozilla.com>
diff --git a/scripts/postinstall.js b/scripts/postinstall.js
new file mode 100644
index 0000000000000000000000000000000000000000..9af5bec429e8dd82748b8a95b986d203adc4a619
--- /dev/null
+++ b/scripts/postinstall.js
@@ -0,0 +1,42 @@
+// make symlinks
+var fs = require('fs');
+var path = require('path');
+var existsSync = fs.existsSync || path.existsSync;
+
+// symlink'ed directories work fine in both *nix and Windows
+function relativeLink(src, dest) {
+  src = path.join(__dirname, src);
+  dest = path.join(__dirname, dest);
+  var destParent = path.dirname(dest);
+  var cwd = process.cwd();
+  process.chdir(destParent);
+
+  if (existsSync(dest)) {
+    fs.unlinkSync(dest);
+  }
+  var relSrc = path.relative(destParent, src);
+  fs.symlinkSync(relSrc, dest, 'junction');
+  process.chdir(cwd);
+}
+
+// Windows requires Administrator cmd prompt to make file links,
+// so just make a copy instead.
+function copy(src, dest) {
+  src = path.join(__dirname, src);
+  dest = path.join(__dirname, dest);
+  fs.writeFileSync(dest, fs.readFileSync(src));
+}
+
+copy('../node_modules/jwcrypto/bidbundle.js', '../resources/static/common/js/lib/bidbundle.js');
+relativeLink('../resources/views', '../resources/static/dialog/views/site');
+
+
+// generate ephemeral keys
+var child_process = require('child_process');
+function node(script) {
+  var cp = child_process.spawn('node', [path.join(__dirname, script)]);
+  cp.stdout.pipe(process.stdout);
+  cp.stderr.pipe(process.stderr);
+}
+
+node('./generate_ephemeral_keys.js');