Backed out 3 changesets (bug 1383663) for timeouts in browser_aboutAccounts.js and failures in test_web_channel.js
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 16 Sep 2017 10:40:00 -0700
changeset 430841 753df1b7c5edb84daf585e0fb434f67755c60be2
parent 430840 f384a524cac6e769af1498d1b67ec33834a551d5
child 430842 e7b8c72dc8680052be19e3ca6275ed9c74ab6184
push id7771
push userryanvm@gmail.com
push dateSun, 17 Sep 2017 03:17:38 +0000
treeherdermozilla-beta@3d2edf73fb90 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1383663
milestone57.0a1
backs outf384a524cac6e769af1498d1b67ec33834a551d5
9d26a627e2f825eee2276d7644ea3c1455b628b4
c7d46e2e8ddca7bd5137846f12892ea1ac27a583
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 3 changesets (bug 1383663) for timeouts in browser_aboutAccounts.js and failures in test_web_channel.js Backed out changeset f384a524cac6 (bug 1383663) Backed out changeset 9d26a627e2f8 (bug 1383663) Backed out changeset c7d46e2e8ddc (bug 1383663) MozReview-Commit-ID: 88xbdeVJtK2
browser/base/content/aboutaccounts/aboutaccounts.js
browser/components/preferences/in-content/sync.js
services/crypto/modules/utils.js
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/FxAccountsProfile.jsm
services/fxaccounts/FxAccountsStorage.jsm
services/fxaccounts/FxAccountsWebChannel.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/test_profile.js
services/sync/modules/UIState.jsm
services/sync/tests/unit/test_uistate.js
tools/lint/eslint/modules.json
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -11,27 +11,88 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 
 var fxAccountsCommon = {};
 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
 
 // for master-password utilities
 Cu.import("resource://services-sync/util.js");
 
+const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
+
 const ACTION_URL_PARAM = "action";
 
 const OBSERVER_TOPICS = [
   fxAccountsCommon.ONVERIFIED_NOTIFICATION,
   fxAccountsCommon.ONLOGOUT_NOTIFICATION,
 ];
 
 function log(msg) {
   // dump("FXA: " + msg + "\n");
 }
 
+function getPreviousAccountNameHash() {
+  try {
+    return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
+  } catch (_) {
+    return "";
+  }
+}
+
+function setPreviousAccountNameHash(acctName) {
+  Services.prefs.setStringPref(PREF_LAST_FXA_USER, sha256(acctName));
+}
+
+function needRelinkWarning(acctName) {
+  let prevAcctHash = getPreviousAccountNameHash();
+  return prevAcctHash && prevAcctHash != sha256(acctName);
+}
+
+// Given a string, returns the SHA265 hash in base64
+function sha256(str) {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  // Data is an array of bytes.
+  let data = converter.convertToByteArray(str, {});
+  let hasher = Cc["@mozilla.org/security/hash;1"]
+                 .createInstance(Ci.nsICryptoHash);
+  hasher.init(hasher.SHA256);
+  hasher.update(data, data.length);
+
+  return hasher.finish(true);
+}
+
+function promptForRelink(acctName) {
+  let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
+  let continueLabel = sb.GetStringFromName("continue.label");
+  let title = sb.GetStringFromName("relinkVerify.title");
+  let description = sb.formatStringFromName("relinkVerify.description",
+                                            [acctName], 1);
+  let body = sb.GetStringFromName("relinkVerify.heading") +
+             "\n\n" + description;
+  let ps = Services.prompt;
+  let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
+                    (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
+                    ps.BUTTON_POS_1_DEFAULT;
+  let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags,
+                                     continueLabel, null, null, null,
+                                     {});
+  return pressed == 0; // 0 is the "continue" button
+}
+
+// If the last fxa account used for sync isn't this account, we display
+// a modal dialog checking they really really want to do this...
+// (This is sync-specific, so ideally would be in sync's identity module,
+// but it's a little more seamless to do here, and sync is currently the
+// only fxa consumer, so...
+function shouldAllowRelink(acctName) {
+  return !needRelinkWarning(acctName) || promptForRelink(acctName);
+}
+
 function updateDisplayedEmail(user) {
   let emailDiv = document.getElementById("email");
   if (emailDiv && user) {
     emailDiv.textContent = user.email;
   }
 }
 
 var wrapper = {
@@ -46,16 +107,17 @@ var wrapper = {
 
     let iframe = document.getElementById("remote");
     this.iframe = iframe;
     let docShell = this.iframe.frameLoader.docShell;
     docShell.QueryInterface(Ci.nsIWebProgress);
     docShell.addProgressListener(this.iframeListener,
                                  Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
                                  Ci.nsIWebProgress.NOTIFY_LOCATION);
+    iframe.addEventListener("load", this);
 
     // Ideally we'd just merge urlParams with new URL(url).searchParams, but our
     // URLSearchParams implementation doesn't support iteration (bug 1085284).
     let urlParamStr = urlParams.toString();
     if (urlParamStr) {
       url += (url.includes("?") ? "&" : "?") + urlParamStr;
     }
     this.url = url;
@@ -101,17 +163,143 @@ var wrapper = {
     },
 
     onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
       if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
         aRequest.cancel(Components.results.NS_BINDING_ABORTED);
         setErrorPage("networkError");
       }
     },
-  }
+  },
+
+  handleEvent(evt) {
+    switch (evt.type) {
+      case "load":
+        this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
+        this.iframe.removeEventListener("load", this);
+        break;
+      case "FirefoxAccountsCommand":
+        this.handleRemoteCommand(evt);
+        break;
+    }
+  },
+
+  /**
+   * onLogin handler receives user credentials from the jelly after a
+   * sucessful login and stores it in the fxaccounts service
+   *
+   * @param accountData the user's account data and credentials
+   */
+  onLogin(accountData) {
+    log("Received: 'login'. Data:" + JSON.stringify(accountData));
+
+    // We don't act on customizeSync anymore, it used to open a dialog inside
+    // the browser to selecte the engines to sync but we do it on the web now.
+    delete accountData.customizeSync;
+    // sessionTokenContext is erroneously sent by the content server.
+    // https://github.com/mozilla/fxa-content-server/issues/2766
+    // To avoid having the FxA storage manager not knowing what to do with
+    // it we delete it here.
+    delete accountData.sessionTokenContext;
+
+    // We need to confirm a relink - see shouldAllowRelink for more
+    let newAccountEmail = accountData.email;
+    // The hosted code may have already checked for the relink situation
+    // by sending the can_link_account command. If it did, then
+    // it will indicate we don't need to ask twice.
+    if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
+      // we need to tell the page we successfully received the message, but
+      // then bail without telling fxAccounts
+      this.injectData("message", { status: "login" });
+      // after a successful login we return to preferences
+      openPrefs();
+      return;
+    }
+    delete accountData.verifiedCanLinkAccount;
+
+    // Remember who it was so we can log out next time.
+    setPreviousAccountNameHash(newAccountEmail);
+
+    // A sync-specific hack - we want to ensure sync has been initialized
+    // before we set the signed-in user.
+    let xps = Cc["@mozilla.org/weave/service;1"]
+              .getService(Ci.nsISupports)
+              .wrappedJSObject;
+    xps.whenLoaded().then(() => {
+      updateDisplayedEmail(accountData);
+      return fxAccounts.setSignedInUser(accountData);
+    }).then(() => {
+      // If the user data is verified, we want it to immediately look like
+      // they are signed in without waiting for messages to bounce around.
+      if (accountData.verified) {
+        openPrefs();
+      }
+      this.injectData("message", { status: "login" });
+      // until we sort out a better UX, just leave the jelly page in place.
+      // If the account email is not yet verified, it will tell the user to
+      // go check their email, but then it will *not* change state after
+      // the verification completes (the browser will begin syncing, but
+      // won't notify the user). If the email has already been verified,
+      // the jelly will say "Welcome! You are successfully signed in as
+      // EMAIL", but it won't then say "syncing started".
+    }, (err) => this.injectData("message", { status: "error", error: err })
+    );
+  },
+
+  onCanLinkAccount(accountData) {
+    // We need to confirm a relink - see shouldAllowRelink for more
+    let ok = shouldAllowRelink(accountData.email);
+    this.injectData("message", { status: "can_link_account", data: { ok } });
+  },
+
+  /**
+   * onSignOut handler erases the current user's session from the fxaccounts service
+   */
+  onSignOut() {
+    log("Received: 'sign_out'.");
+
+    fxAccounts.signOut().then(
+      () => this.injectData("message", { status: "sign_out" }),
+      (err) => this.injectData("message", { status: "error", error: err })
+    );
+  },
+
+  handleRemoteCommand(evt) {
+    log("command: " + evt.detail.command);
+    let data = evt.detail.data;
+
+    switch (evt.detail.command) {
+      case "login":
+        this.onLogin(data);
+        break;
+      case "can_link_account":
+        this.onCanLinkAccount(data);
+        break;
+      case "sign_out":
+        this.onSignOut(data);
+        break;
+      default:
+        log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+        break;
+    }
+  },
+
+  injectData(type, content) {
+    return fxAccounts.promiseAccountsSignUpURI().then(authUrl => {
+      let data = {
+        type,
+        content
+      };
+      this.iframe.contentWindow.postMessage(data, authUrl);
+    })
+    .catch(e => {
+      console.log("Failed to inject data", e);
+      setErrorPage("configError");
+    });
+  },
 };
 
 
 // Button onclick handlers
 
 function getStarted() {
   show("remote");
 }
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -314,16 +314,23 @@ var gSyncPane = {
     }).then(isVerified => {
       if (isVerified) {
         return fxAccounts.getSignedInUserProfile();
       }
       return null;
     }).then(data => {
       let fxaLoginStatus = document.getElementById("fxaLoginStatus");
       if (data) {
+        if (data.email) {
+          // A hack to handle that the user's email address may have changed.
+          // This can probably be removed as part of bug 1383663.
+          fxaEmailAddressLabels.forEach((label) => {
+            label.value = data.email;
+          });
+        }
         if (data.displayName) {
           fxaLoginStatus.setAttribute("hasName", true);
           displayNameLabel.hidden = false;
           displayNameLabel.textContent = data.displayName;
         } else {
           fxaLoginStatus.removeAttribute("hasName");
         }
         if (data.avatar) {
--- a/services/crypto/modules/utils.js
+++ b/services/crypto/modules/utils.js
@@ -108,25 +108,16 @@ this.CryptoUtils = {
 
   sha256(message) {
     let hasher = Cc["@mozilla.org/security/hash;1"]
                  .createInstance(Ci.nsICryptoHash);
     hasher.init(hasher.SHA256);
     return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
   },
 
-  sha256Base64(message) {
-    let data = this._utf8Converter.convertToByteArray(message, {});
-    let hasher = Cc["@mozilla.org/security/hash;1"]
-                 .createInstance(Ci.nsICryptoHash);
-    hasher.init(hasher.SHA256);
-    hasher.update(data, data.length);
-    return hasher.finish(true);
-  },
-
   /**
    * Produce an HMAC key object from a key string.
    */
   makeHMACKey: function makeHMACKey(str) {
     return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str);
   },
 
   /**
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -46,19 +46,18 @@ var publicProperties = [
   "getAssertion",
   "getDeviceId",
   "getDeviceList",
   "getKeys",
   "getOAuthToken",
   "getProfileCache",
   "getSignedInUser",
   "getSignedInUserProfile",
+  "handleDeviceDisconnection",
   "handleAccountDestroyed",
-  "handleDeviceDisconnection",
-  "handleEmailUpdated",
   "hasLocalSession",
   "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "notifyDevices",
   "now",
   "promiseAccountsChangeProfileURI",
   "promiseAccountsForceSigninURI",
@@ -589,34 +588,35 @@ FxAccountsInternal.prototype = {
     });
   },
 
   /**
    * Update account data for the currently signed in user.
    *
    * @param credentials
    *        The credentials object containing the fields to be updated.
-   *        This object must contain the |uid| field and it must
+   *        This object must contain |email| and |uid| fields and they must
    *        match the currently signed in user.
    */
   updateUserAccountData(credentials) {
     log.debug("updateUserAccountData called with fields", Object.keys(credentials));
     if (logPII) {
       log.debug("updateUserAccountData called with data", credentials);
     }
     let currentAccountState = this.currentAccountState;
     return currentAccountState.promiseInitialized.then(() => {
-      return currentAccountState.getUserAccountData(["uid"]);
+      return currentAccountState.getUserAccountData(["email", "uid"]);
     }).then(existing => {
-      if (existing.uid != credentials.uid) {
+      if (existing.email != credentials.email || existing.uid != credentials.uid) {
         throw new Error("The specified credentials aren't for the current user");
       }
-      // We need to nuke uid as storage will complain if we try and
-      // update it (even when the value is the same)
+      // We need to nuke email and uid as storage will complain if we try and
+      // update them (even when the value is the same)
       credentials = Cu.cloneInto(credentials, {}); // clone it first
+      delete credentials.email;
       delete credentials.uid;
       return currentAccountState.updateUserAccountData(credentials);
     });
   },
 
   /**
    * returns a promise that fires with the assertion.  If there is no verified
    * signed-in user, fires with null.
@@ -1602,21 +1602,16 @@ FxAccountsInternal.prototype = {
     if (isLocalDevice) {
       this.signOut(true);
     }
     const data = JSON.stringify({ isLocalDevice });
     Services.obs.notifyObservers(null, ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
     return null;
   },
 
-  handleEmailUpdated(newEmail) {
-    Services.prefs.setStringPref(PREF_LAST_FXA_USER, CryptoUtils.sha256Base64(newEmail));
-    return this.currentAccountState.updateUserAccountData({ email: newEmail });
-  },
-
   async handleAccountDestroyed(uid) {
     const accountData = await this.currentAccountState.getUserAccountData();
     const localUid = accountData ? accountData.uid : null;
     if (!localUid) {
       log.info(`Account destroyed push notification received, but we're already logged-out`);
       return null;
     }
     if (uid == localUid) {
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -108,18 +108,16 @@ exports.UI_REQUEST_SIGN_IN_FLOW = "signI
 exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
 
 // The OAuth client ID for Firefox Desktop
 exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
 
 // Firefox Accounts WebChannel ID
 exports.WEBCHANNEL_ID = "account_updates";
 
-exports.PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
-
 // Server errno.
 // From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
 exports.ERRNO_ACCOUNT_ALREADY_EXISTS         = 101;
 exports.ERRNO_ACCOUNT_DOES_NOT_EXIST         = 102;
 exports.ERRNO_INCORRECT_PASSWORD             = 103;
 exports.ERRNO_UNVERIFIED_ACCOUNT             = 104;
 exports.ERRNO_INVALID_VERIFICATION_CODE      = 105;
 exports.ERRNO_NOT_VALID_JSON_BODY            = 106;
--- a/services/fxaccounts/FxAccountsProfile.jsm
+++ b/services/fxaccounts/FxAccountsProfile.jsm
@@ -66,76 +66,83 @@ this.FxAccountsProfile.prototype = {
 
   _notifyProfileChange(uid) {
     this._isNotifying = true;
     Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
     this._isNotifying = false;
   },
 
   // Cache fetched data and send out a notification so that UI can update.
-  async _cacheProfile(response) {
-    const profile = response.body;
-    const userData = await this.fxa.getSignedInUser();
-    if (profile.uid != userData.uid) {
-      throw new Error("The fetched profile does not correspond with the current account.")
-    }
+  _cacheProfile(response) {
     let profileCache = {
-      profile,
+      profile: response.body,
       etag: response.etag
     };
-    await this.fxa.setProfileCache(profileCache);
-    if (profile.email != userData.email) {
-      await this.fxa.handleEmailUpdated(profile.email);
-    }
-    log.debug("notifying profile changed for user ${uid}", userData);
-    this._notifyProfileChange(userData.uid);
-    return profile;
+
+    return this.fxa.setProfileCache(profileCache)
+      .then(() => {
+        return this.fxa.getSignedInUser();
+      })
+      .then(userData => {
+        log.debug("notifying profile changed for user ${uid}", userData);
+        this._notifyProfileChange(userData.uid);
+        return response.body;
+      });
   },
 
-  async _fetchAndCacheProfileInternal() {
-    try {
-      const profileCache = await this.fxa.getProfileCache();
-      const etag = profileCache ? profileCache.etag : null;
-      const response = await this.client.fetchProfile(etag);
-
-      // response may be null if the profile was not modified (same ETag).
-      if (!response) {
-        return null;
-      }
-      return await this._cacheProfile(response);
-    } finally {
+  _fetchAndCacheProfileInternal() {
+    let onFinally = () => {
       this._cachedAt = Date.now();
       this._currentFetchPromise = null;
     }
+    return this.fxa.getProfileCache()
+      .then(profileCache => {
+        const etag = profileCache ? profileCache.etag : null;
+        return this.client.fetchProfile(etag);
+      })
+      .then(response => {
+        // response may be null if the profile was not modified (same ETag).
+        return response ? this._cacheProfile(response) : null;
+      })
+      .then(body => { // finally block
+        onFinally();
+        // body may be null if the profile was not modified
+        return body;
+      }, err => {
+        onFinally();
+        throw err;
+      });
   },
 
   _fetchAndCacheProfile() {
     if (!this._currentFetchPromise) {
       this._currentFetchPromise = this._fetchAndCacheProfileInternal();
     }
     return this._currentFetchPromise;
   },
 
   // Returns cached data right away if available, then fetches the latest profile
   // data in the background. After data is fetched a notification will be sent
   // out if the profile has changed.
-  async getProfile() {
-    const profileCache = await this.fxa.getProfileCache();
-    if (!profileCache) {
-      return this._fetchAndCacheProfile();
-    }
-    if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
-      // Note that _fetchAndCacheProfile isn't returned, so continues
-      // in the background.
-      this._fetchAndCacheProfile().catch(err => {
-        log.error("Background refresh of profile failed", err);
+  getProfile() {
+    return this.fxa.getProfileCache()
+      .then(profileCache => {
+        if (profileCache) {
+          if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
+            // Note that _fetchAndCacheProfile isn't returned, so continues
+            // in the background.
+            this._fetchAndCacheProfile().catch(err => {
+              log.error("Background refresh of profile failed", err);
+            });
+          } else {
+            log.trace("not checking freshness of profile as it remains recent");
+          }
+          return profileCache.profile;
+        }
+        return this._fetchAndCacheProfile();
       });
-    } else {
-      log.trace("not checking freshness of profile as it remains recent");
-    }
-    return profileCache.profile;
   },
 
   QueryInterface: XPCOMUtils.generateQI([
       Ci.nsIObserver,
       Ci.nsISupportsWeakReference,
   ]),
 };
--- a/services/fxaccounts/FxAccountsStorage.jsm
+++ b/services/fxaccounts/FxAccountsStorage.jsm
@@ -203,18 +203,21 @@ this.FxAccountsStorageManager.prototype 
   // a different user, nor to set the user as signed-out.
   async updateAccountData(newFields) {
     await this._promiseInitialized;
     if (!("uid" in this.cachedPlain)) {
       // If this storage instance shows no logged in user, then you can't
       // update fields.
       throw new Error("No user is logged in");
     }
-    if (!newFields || "uid" in newFields) {
-      throw new Error("Can't change uid");
+    if (!newFields || "uid" in newFields || "email" in newFields) {
+      // Once we support
+      // user changing email address this may need to change, but it's not
+      // clear how we would be told of such a change anyway...
+      throw new Error("Can't change uid or email address");
     }
     log.debug("_updateAccountData with items", Object.keys(newFields));
     // work out what bucket.
     for (let [name, value] of Object.entries(newFields)) {
       if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
         if (value == null) {
           delete this.cachedMemory[name];
         } else {
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -24,28 +24,28 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
                                   "resource://gre/modules/FxAccounts.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsStorageManagerCanStoreField",
                                   "resource://gre/modules/FxAccountsStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Weave",
                                   "resource://services-sync/main.js");
-XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
-                                  "resource://services-crypto/utils.js");
 
 const COMMAND_PROFILE_CHANGE       = "profile:change";
 const COMMAND_CAN_LINK_ACCOUNT     = "fxaccounts:can_link_account";
 const COMMAND_LOGIN                = "fxaccounts:login";
 const COMMAND_LOGOUT               = "fxaccounts:logout";
 const COMMAND_DELETE               = "fxaccounts:delete";
 const COMMAND_SYNC_PREFERENCES     = "fxaccounts:sync_preferences";
 const COMMAND_CHANGE_PASSWORD      = "fxaccounts:change_password";
 const COMMAND_FXA_STATUS           = "fxaccounts:fxa_status";
 
+const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
+
 // These engines were added years after Sync had been introduced, they need
 // special handling since they are system add-ons and are un-available on
 // older versions of Firefox.
 const EXTRA_ENGINES = ["addresses", "creditcards"];
 
 /**
  * A helper function that extracts the message and stack from an error object.
  * Returns a `{ message, stack }` tuple. `stack` will be null if the error
@@ -454,17 +454,34 @@ this.FxAccountsWebChannelHelpers.prototy
   },
 
   /**
    * Given an account name, set the hash of the previously signed in account
    *
    * @param acctName the account name of the user's account.
    */
   setPreviousAccountNameHashPref(acctName) {
-    Services.prefs.setStringPref(PREF_LAST_FXA_USER, CryptoUtils.sha256Base64(acctName));
+    Services.prefs.setStringPref(PREF_LAST_FXA_USER, this.sha256(acctName));
+  },
+
+  /**
+   * Given a string, returns the SHA265 hash in base64
+   */
+  sha256(str) {
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                      .createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+    // Data is an array of bytes.
+    let data = converter.convertToByteArray(str, {});
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                   .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    hasher.update(data, data.length);
+
+    return hasher.finish(true);
   },
 
   /**
    * Open Sync Preferences in the current tab of the browser
    *
    * @param {Object} browser the browser in which to open preferences
    * @param {String} [entryPoint] entryPoint to use for logging
    */
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -294,25 +294,31 @@ add_task(async function test_update_acco
     email: credentials.email,
     uid: credentials.uid,
     assertion: "new_assertion",
   }
   await account.updateUserAccountData(newCreds);
   do_check_eq((await account.getSignedInUser()).assertion, "new_assertion",
               "new field value was saved");
 
-  // but we should fail attempting to change the uid.
+  // but we should fail attempting to change email or uid.
+  newCreds = {
+    email: "someoneelse@example.com",
+    uid: credentials.uid,
+    assertion: "new_assertion",
+  }
+  await Assert.rejects(account.updateUserAccountData(newCreds));
   newCreds = {
     email: credentials.email,
     uid: "another_uid",
     assertion: "new_assertion",
   }
   await Assert.rejects(account.updateUserAccountData(newCreds));
 
-  // should fail without the uid.
+  // should fail without email or uid.
   newCreds = {
     assertion: "new_assertion",
   }
   await Assert.rejects(account.updateUserAccountData(newCreds));
 
   // and should fail with a field name that's not known by storage.
   newCreds = {
     email: credentials.email,
--- a/services/fxaccounts/tests/xpcshell/test_profile.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile.js
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm");
 Cu.import("resource://gre/modules/FxAccountsProfile.jsm");
-Cu.import("resource://gre/modules/PromiseUtils.jsm");
 
 const URL_STRING = "https://example.com";
 Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings");
 
 const STATUS_SUCCESS = 200;
 
 /**
  * Mock request responder
@@ -55,21 +54,18 @@ let mockResponseError = function(error) 
 let mockClient = function(fxa) {
   let options = {
     serverURL: "http://127.0.0.1:1111/v1",
     fxa,
   }
   return new FxAccountsProfileClient(options);
 };
 
-const ACCOUNT_UID = "abc123";
-const ACCOUNT_EMAIL = "foo@bar.com";
 const ACCOUNT_DATA = {
-  uid: ACCOUNT_UID,
-  email: ACCOUNT_EMAIL
+  uid: "abc123"
 };
 
 function FxaMock() {
 }
 FxaMock.prototype = {
   currentAccountState: {
     profile: null,
     get isCurrent() {
@@ -121,23 +117,23 @@ add_test(function cacheProfile_change() 
   let profile = CreateFxAccountsProfile(fxa);
 
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
     do_check_eq(data, ACCOUNT_DATA.uid);
     do_check_true(setProfileCacheCalled);
     run_next_test();
   });
 
-  return profile._cacheProfile({ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myurl" }, etag: "bogusetag" });
+  return profile._cacheProfile({ body: { avatar: "myurl" }, etag: "bogusetag" });
 });
 
 add_test(function fetchAndCacheProfile_ok() {
   let client = mockClient(mockFxa());
   client.fetchProfile = function() {
-    return Promise.resolve({ body: { uid: ACCOUNT_UID, avatar: "myimg"} });
+    return Promise.resolve({ body: { avatar: "myimg"} });
   };
   let profile = CreateFxAccountsProfile(null, client);
   profile._cachedAt = 12345;
 
   profile._cacheProfile = function(toCache) {
     do_check_eq(toCache.body.avatar, "myimg");
     return Promise.resolve(toCache.body);
   };
@@ -168,17 +164,17 @@ add_test(function fetchAndCacheProfile_a
 });
 
 add_test(function fetchAndCacheProfile_sendsETag() {
   let fxa = mockFxa();
   fxa.profileCache = { profile: {}, etag: "bogusETag" };
   let client = mockClient(fxa);
   client.fetchProfile = function(etag) {
     do_check_eq(etag, "bogusETag");
-    return Promise.resolve({ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg"} });
+    return Promise.resolve({ body: { avatar: "myimg"} });
   };
   let profile = CreateFxAccountsProfile(fxa, client);
 
   return profile._fetchAndCacheProfile()
     .then(result => {
       run_next_test();
     });
 });
@@ -194,28 +190,36 @@ add_task(async function fetchAndCachePro
   });
   let numFetches = 0;
   let client = mockClient(mockFxa());
   client.fetchProfile = function() {
     numFetches += 1;
     return promiseProfile;
   };
   let fxa = mockFxa();
+  fxa.getProfileCache = () => {
+    // We do this because we are gonna have a race condition and fetchProfile will
+    // not be called before we check numFetches.
+    return {
+      then(thenFunc) {
+        return thenFunc();
+      }
+    }
+  };
   let profile = CreateFxAccountsProfile(fxa, client);
 
   let request1 = profile._fetchAndCacheProfile();
   profile._fetchAndCacheProfile();
-  await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise)
 
   // should be one request made to fetch the profile (but the promise returned
   // by it remains unresolved)
   do_check_eq(numFetches, 1);
 
   // resolve the promise.
-  resolveProfile({ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg"} });
+  resolveProfile({ body: { avatar: "myimg"} });
 
   // both requests should complete with the same data.
   let got1 = await request1;
   do_check_eq(got1.avatar, "myimg");
   let got2 = await request1;
   do_check_eq(got2.avatar, "myimg");
 
   // and still only 1 request was made.
@@ -233,21 +237,29 @@ add_task(async function fetchAndCachePro
   });
   let numFetches = 0;
   let client = mockClient(mockFxa());
   client.fetchProfile = function() {
     numFetches += 1;
     return promiseProfile;
   };
   let fxa = mockFxa();
+  fxa.getProfileCache = () => {
+    // We do this because we are gonna have a race condition and fetchProfile will
+    // not be called before we check numFetches.
+    return {
+      then(thenFunc) {
+        return thenFunc();
+      }
+    }
+  };
   let profile = CreateFxAccountsProfile(fxa, client);
 
   let request1 = profile._fetchAndCacheProfile();
   let request2 = profile._fetchAndCacheProfile();
-  await new Promise(res => setTimeout(res, 0)); // Yield so fetchProfile() is called (promise)
 
   // should be one request made to fetch the profile (but the promise returned
   // by it remains unresolved)
   do_check_eq(numFetches, 1);
 
   // reject the promise.
   rejectProfile("oh noes");
 
@@ -266,27 +278,27 @@ add_task(async function fetchAndCachePro
   } catch (ex) {
     if (ex != "oh noes") {
       throw ex;
     }
   }
 
   // but a new request should works.
   client.fetchProfile = function() {
-    return Promise.resolve({body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg"}});
+    return Promise.resolve({body: { avatar: "myimg"}});
   };
 
   let got = await profile._fetchAndCacheProfile();
   do_check_eq(got.avatar, "myimg");
 });
 
 add_test(function fetchAndCacheProfile_alreadyCached() {
   let cachedUrl = "cachedurl";
   let fxa = mockFxa();
-  fxa.profileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl }, etag: "bogusETag" };
+  fxa.profileCache = { profile: { avatar: cachedUrl }, etag: "bogusETag" };
   let client = mockClient(fxa);
   client.fetchProfile = function(etag) {
     do_check_eq(etag, "bogusETag");
     return Promise.resolve(null);
   };
 
   let profile = CreateFxAccountsProfile(fxa, client);
   profile._cacheProfile = function(toCache) {
@@ -301,70 +313,56 @@ add_test(function fetchAndCacheProfile_a
     });
 });
 
 // Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
 // last one doesn't kick off a new request to check the cached copy is fresh.
 add_task(async function fetchAndCacheProfileAfterThreshold() {
   let numFetches = 0;
   let client = mockClient(mockFxa());
-  client.fetchProfile = async function() {
+  client.fetchProfile = function() {
     numFetches += 1;
-    return {body: {uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg"}};
+    return Promise.resolve({ avatar: "myimg"});
   };
   let profile = CreateFxAccountsProfile(null, client);
   profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
 
   await profile.getProfile();
   do_check_eq(numFetches, 1);
 
   await profile.getProfile();
   do_check_eq(numFetches, 1);
 
   await new Promise(resolve => {
     do_timeout(1000, resolve);
   });
 
-  let origFetchAndCatch = profile._fetchAndCacheProfile;
-  let backgroundFetchDone = PromiseUtils.defer();
-  profile._fetchAndCacheProfile = async () => {
-    await origFetchAndCatch.call(profile);
-    backgroundFetchDone.resolve();
-  }
   await profile.getProfile();
-  await backgroundFetchDone.promise;
   do_check_eq(numFetches, 2);
 });
 
 // Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the
 // last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION
 // is sent.
 add_task(async function fetchAndCacheProfileBeforeThresholdOnNotification() {
   let numFetches = 0;
   let client = mockClient(mockFxa());
-  client.fetchProfile = async function() {
+  client.fetchProfile = function() {
     numFetches += 1;
-    return {body: {uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg"}};
+    return Promise.resolve({ avatar: "myimg"});
   };
   let profile = CreateFxAccountsProfile(null, client);
   profile.PROFILE_FRESHNESS_THRESHOLD = 1000;
 
   await profile.getProfile();
   do_check_eq(numFetches, 1);
 
   Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
 
-  let origFetchAndCatch = profile._fetchAndCacheProfile;
-  let backgroundFetchDone = PromiseUtils.defer();
-  profile._fetchAndCacheProfile = async () => {
-    await origFetchAndCatch.call(profile);
-    backgroundFetchDone.resolve();
-  }
   await profile.getProfile();
-  await backgroundFetchDone.promise;
   do_check_eq(numFetches, 2);
 });
 
 add_test(function tearDown_ok() {
   let profile = CreateFxAccountsProfile();
 
   do_check_true(!!profile.client);
   do_check_true(!!profile.fxa);
@@ -376,17 +374,17 @@ add_test(function tearDown_ok() {
   run_next_test();
 });
 
 add_test(function getProfile_ok() {
   let cachedUrl = "myurl";
   let didFetch = false;
 
   let fxa = mockFxa();
-  fxa.profileCache = { profile: { uid: ACCOUNT_UID, avatar: cachedUrl } };
+  fxa.profileCache = { profile: { avatar: cachedUrl } };
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfile = function() {
     didFetch = true;
     return Promise.resolve();
   };
 
   return profile.getProfile()
@@ -399,37 +397,37 @@ add_test(function getProfile_ok() {
 
 add_test(function getProfile_no_cache() {
   let fetchedUrl = "newUrl";
   let fxa = mockFxa();
   fxa.profileCache = null;
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfile = function() {
-    return Promise.resolve({ uid: ACCOUNT_UID, avatar: fetchedUrl });
+    return Promise.resolve({ avatar: fetchedUrl });
   };
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, fetchedUrl);
       run_next_test();
     });
 });
 
 add_test(function getProfile_has_cached_fetch_deleted() {
   let cachedUrl = "myurl";
 
   let fxa = mockFxa();
   let client = mockClient(fxa);
   client.fetchProfile = function() {
-    return Promise.resolve({ body: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: null } });
+    return Promise.resolve({ body: { avatar: null } });
   };
 
   let profile = CreateFxAccountsProfile(fxa, client);
-  fxa.profileCache = { profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: cachedUrl } };
+  fxa.profileCache = { profile: { avatar: cachedUrl } };
 
 // instead of checking this in a mocked "save" function, just check after the
 // observer
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function(subject, topic, data) {
     profile.getProfile()
       .then(profileData => {
         do_check_null(profileData.avatar);
         run_next_test();
@@ -439,43 +437,28 @@ add_test(function getProfile_has_cached_
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, "myurl");
     });
 });
 
 add_test(function getProfile_fetchAndCacheProfile_throws() {
   let fxa = mockFxa();
-  fxa.profileCache = { profile: { uid: ACCOUNT_UID, email: ACCOUNT_EMAIL, avatar: "myimg" } };
+  fxa.profileCache = { profile: { avatar: "myimg" } };
   let profile = CreateFxAccountsProfile(fxa);
 
   profile._fetchAndCacheProfile = () => Promise.reject(new Error());
 
   return profile.getProfile()
     .then(result => {
       do_check_eq(result.avatar, "myimg");
       run_next_test();
     });
 });
 
-add_test(function getProfile_email_changed() {
-  let fxa = mockFxa();
-  let client = mockClient(fxa);
-  client.fetchProfile = function() {
-    return Promise.resolve({ body: { uid: ACCOUNT_UID, email: "newemail@bar.com" } });
-  };
-  fxa.handleEmailUpdated = email => {
-    do_check_eq(email, "newemail@bar.com");
-    run_next_test();
-  };
-
-  let profile = CreateFxAccountsProfile(fxa, client);
-  return profile._fetchAndCacheProfile();
-});
-
 function makeObserver(aObserveTopic, aObserveFunc) {
   let callback = function(aSubject, aTopic, aData) {
     log.debug("observed " + aTopic + " " + aData);
     if (aTopic == aObserveTopic) {
       removeMe();
       aObserveFunc(aSubject, aTopic, aData);
     }
   };
--- a/services/sync/modules/UIState.jsm
+++ b/services/sync/modules/UIState.jsm
@@ -157,16 +157,19 @@ const UIStateInternal = {
       state.email = userData.email;
     }
     state.status = status;
   },
 
   _populateWithProfile(state, profile) {
     state.displayName = profile.displayName;
     state.avatarURL = profile.avatar;
+    // A hack to handle that the user's email address may have changed.
+    // This can probably be removed as part of bug 1383663.
+    state.email = profile.email;
   },
 
   async _getUserData() {
     try {
       return await this.fxAccounts.getSignedInUser();
     } catch (e) {
       // This is most likely in tests, where we quickly log users in and out.
       // The most likely scenario is a user logged out, so reflect that.
--- a/services/sync/tests/unit/test_uistate.js
+++ b/services/sync/tests/unit/test_uistate.js
@@ -48,32 +48,58 @@ add_task(async function test_refreshStat
   const fxAccountsOrig = UIStateInternal.fxAccounts;
 
   const now = new Date().toString();
   Services.prefs.setCharPref("services.sync.lastSync", now);
   UIStateInternal.syncing = false;
 
   UIStateInternal.fxAccounts = {
     getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
-    getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar" }),
+    getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar", email: "foo@bar.com" }),
     hasLocalSession: () => Promise.resolve(true),
   }
 
   let state = await UIState.refresh();
 
   equal(state.status, UIState.STATUS_SIGNED_IN);
   equal(state.email, "foo@bar.com");
   equal(state.displayName, "Foo Bar");
   equal(state.avatarURL, "https://foo/bar");
   equal(state.lastSync, now);
   equal(state.syncing, false);
 
   UIStateInternal.fxAccounts = fxAccountsOrig;
 });
 
+add_task(async function test_refreshState_preferProfileEmail() {
+  UIState.reset();
+  const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+  const now = new Date().toString();
+  Services.prefs.setCharPref("services.sync.lastSync", now);
+  UIStateInternal.syncing = false;
+
+  UIStateInternal.fxAccounts = {
+    getSignedInUser: () => Promise.resolve({ verified: true, email: "foo@bar.com" }),
+    getSignedInUserProfile: () => Promise.resolve({ displayName: "Foo Bar", avatar: "https://foo/bar", email: "bar@foo.com" }),
+    hasLocalSession: () => Promise.resolve(true),
+  }
+
+  let state = await UIState.refresh();
+
+  equal(state.status, UIState.STATUS_SIGNED_IN);
+  equal(state.email, "bar@foo.com");
+  equal(state.displayName, "Foo Bar");
+  equal(state.avatarURL, "https://foo/bar");
+  equal(state.lastSync, now);
+  equal(state.syncing, false);
+
+  UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
 add_task(async function test_refreshState_signedin_profile_unavailable() {
   UIState.reset();
   const fxAccountsOrig = UIStateInternal.fxAccounts;
 
   const now = new Date().toString();
   Services.prefs.setCharPref("services.sync.lastSync", now);
   UIStateInternal.syncing = false;
 
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -79,17 +79,17 @@
   "forms.jsm": ["FormData"],
   "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
   "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
   "frame.js": ["Collector", "Runner", "events", "runTestFile", "log", "timers", "persisted", "shutdownApplication"],
   "FrameScriptManager.jsm": ["getNewLoaderID"],
   "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
   "fxaccounts.jsm": ["Authentication"],
   "FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
-  "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
+  "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
   "FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
   "FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
   "FxAccountsPush.js": ["FxAccountsPushService"],
   "FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],
   "FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"],
   "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
   "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
   "Geometry.jsm": ["Point", "Rect"],