tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

FxAccounts.sys.mjs (51554B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 import { FxAccountsStorageManager } from "resource://gre/modules/FxAccountsStorage.sys.mjs";
      8 
      9 import {
     10  ATTACHED_CLIENTS_CACHE_DURATION,
     11  ERRNO_INVALID_AUTH_TOKEN,
     12  ERROR_AUTH_ERROR,
     13  ERROR_INVALID_PARAMETER,
     14  ERROR_NO_ACCOUNT,
     15  ERROR_TO_GENERAL_ERROR_CLASS,
     16  ERROR_UNKNOWN,
     17  ERROR_UNVERIFIED_ACCOUNT,
     18  FXA_PWDMGR_PLAINTEXT_FIELDS,
     19  FXA_PWDMGR_REAUTH_ALLOWLIST,
     20  FXA_PWDMGR_SECURE_FIELDS,
     21  OAUTH_CLIENT_ID,
     22  ON_ACCOUNT_STATE_CHANGE_NOTIFICATION,
     23  ONLOGIN_NOTIFICATION,
     24  ONLOGOUT_NOTIFICATION,
     25  ON_PRELOGOUT_NOTIFICATION,
     26  ONVERIFIED_NOTIFICATION,
     27  ON_DEVICE_DISCONNECTED_NOTIFICATION,
     28  POLL_SESSION,
     29  PREF_ACCOUNT_ROOT,
     30  PREF_LAST_FXA_USER_EMAIL,
     31  PREF_LAST_FXA_USER_UID,
     32  SERVER_ERRNO_TO_ERROR,
     33  log,
     34  logPII,
     35  logManager,
     36 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     37 
     38 const lazy = {};
     39 
     40 ChromeUtils.defineESModuleGetters(lazy, {
     41  CryptoUtils: "moz-src:///services/crypto/modules/utils.sys.mjs",
     42  FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
     43  FxAccountsCommands: "resource://gre/modules/FxAccountsCommands.sys.mjs",
     44  FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
     45  FxAccountsDevice: "resource://gre/modules/FxAccountsDevice.sys.mjs",
     46  FxAccountsKeys: "resource://gre/modules/FxAccountsKeys.sys.mjs",
     47  FxAccountsOAuth: "resource://gre/modules/FxAccountsOAuth.sys.mjs",
     48  FxAccountsProfile: "resource://gre/modules/FxAccountsProfile.sys.mjs",
     49  FxAccountsTelemetry: "resource://gre/modules/FxAccountsTelemetry.sys.mjs",
     50 });
     51 
     52 ChromeUtils.defineLazyGetter(lazy, "mpLocked", () => {
     53  return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
     54    .Utils.mpLocked;
     55 });
     56 
     57 ChromeUtils.defineLazyGetter(lazy, "ensureMPUnlocked", () => {
     58  return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
     59    .Utils.ensureMPUnlocked;
     60 });
     61 
     62 XPCOMUtils.defineLazyPreferenceGetter(
     63  lazy,
     64  "FXA_ENABLED",
     65  "identity.fxaccounts.enabled",
     66  true
     67 );
     68 
     69 export const ERROR_INVALID_ACCOUNT_STATE = "ERROR_INVALID_ACCOUNT_STATE";
     70 
     71 // An AccountState object holds all state related to one specific account.
     72 // It is considered "private" to the FxAccounts modules.
     73 // Only one AccountState is ever "current" in the FxAccountsInternal object -
     74 // whenever a user logs out or logs in, the current AccountState is discarded,
     75 // making it impossible for the wrong state or state data to be accidentally
     76 // used.
     77 // In addition, it has some promise-related helpers to ensure that if an
     78 // attempt is made to resolve a promise on a "stale" state (eg, if an
     79 // operation starts, but a different user logs in before the operation
     80 // completes), the promise will be rejected.
     81 // It is intended to be used thusly:
     82 // somePromiseBasedFunction: function() {
     83 //   let currentState = this.currentAccountState;
     84 //   return someOtherPromiseFunction().then(
     85 //     data => currentState.resolve(data)
     86 //   );
     87 // }
     88 // If the state has changed between the function being called and the promise
     89 // being resolved, the .resolve() call will actually be rejected.
     90 export function AccountState(storageManager) {
     91  this.storageManager = storageManager;
     92  this.inFlightTokenRequests = new Map();
     93  this.promiseInitialized = this.storageManager
     94    .getAccountData()
     95    .then(data => {
     96      this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
     97    })
     98    .catch(err => {
     99      log.error("Failed to initialize the storage manager", err);
    100      // Things are going to fall apart, but not much we can do about it here.
    101    });
    102 }
    103 
    104 AccountState.prototype = {
    105  oauthTokens: null,
    106  whenKeysReadyDeferred: null,
    107 
    108  // If the storage manager has been nuked then we are no longer current.
    109  get isCurrent() {
    110    return this.storageManager != null;
    111  },
    112 
    113  abort() {
    114    if (this.whenKeysReadyDeferred) {
    115      this.whenKeysReadyDeferred.reject(
    116        new Error("Key fetching aborted; Another user signing in")
    117      );
    118      this.whenKeysReadyDeferred = null;
    119    }
    120    this.inFlightTokenRequests.clear();
    121    return this.signOut();
    122  },
    123 
    124  // Clobber all cached data and write that empty data to storage.
    125  async signOut() {
    126    this.cert = null;
    127    this.keyPair = null;
    128    this.oauthTokens = null;
    129    this.inFlightTokenRequests.clear();
    130 
    131    // Avoid finalizing the storageManager multiple times (ie, .signOut()
    132    // followed by .abort())
    133    if (!this.storageManager) {
    134      return;
    135    }
    136    const storageManager = this.storageManager;
    137    this.storageManager = null;
    138 
    139    await storageManager.deleteAccountData();
    140    await storageManager.finalize();
    141  },
    142 
    143  // Get user account data. Optionally specify explicit field names to fetch
    144  // (and note that if you require an in-memory field you *must* specify the
    145  // field name(s).)
    146  getUserAccountData(fieldNames = null) {
    147    if (!this.isCurrent) {
    148      return Promise.reject(new Error("Another user has signed in"));
    149    }
    150    return this.storageManager.getAccountData(fieldNames).then(result => {
    151      return this.resolve(result);
    152    });
    153  },
    154 
    155  async updateUserAccountData(updatedFields) {
    156    if ("uid" in updatedFields) {
    157      const existing = await this.getUserAccountData(["uid"]);
    158      if (existing.uid != updatedFields.uid) {
    159        throw new Error(
    160          "The specified credentials aren't for the current user"
    161        );
    162      }
    163      // We need to nuke uid as storage will complain if we try and
    164      // update it (even when the value is the same)
    165      updatedFields = Cu.cloneInto(updatedFields, {}); // clone it first
    166      delete updatedFields.uid;
    167    }
    168    if (!this.isCurrent) {
    169      return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
    170    }
    171    return this.storageManager.updateAccountData(updatedFields);
    172  },
    173 
    174  resolve(result) {
    175    if (!this.isCurrent) {
    176      log.info(
    177        "An accountState promise was resolved, but was actually rejected" +
    178          " due to the account state changing. This can happen if a new account signed in, or" +
    179          " the account was signed out. Originally resolved with, ",
    180        result
    181      );
    182      return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
    183    }
    184    return Promise.resolve(result);
    185  },
    186 
    187  reject(error) {
    188    // It could be argued that we should just let it reject with the original
    189    // error - but this runs the risk of the error being (eg) a 401, which
    190    // might cause the consumer to attempt some remediation and cause other
    191    // problems.
    192    if (!this.isCurrent) {
    193      log.info(
    194        "An accountState promise was rejected, but we are ignoring that" +
    195          " reason and rejecting it due to the account state changing. This can happen if" +
    196          " a different account signed in or the account was signed out" +
    197          " originally resolved with, ",
    198        error
    199      );
    200      return Promise.reject(new Error(ERROR_INVALID_ACCOUNT_STATE));
    201    }
    202    return Promise.reject(error);
    203  },
    204 
    205  // Abstractions for storage of cached tokens - these are all sync, and don't
    206  // handle revocation etc - it's just storage (and the storage itself is async,
    207  // but we don't return the storage promises, so it *looks* sync)
    208  // These functions are sync simply so we can handle "token races" - when there
    209  // are multiple in-flight requests for the same scope, we can detect this
    210  // and revoke the redundant token.
    211 
    212  // A preamble for the cache helpers...
    213  _cachePreamble() {
    214    if (!this.isCurrent) {
    215      throw new Error(ERROR_INVALID_ACCOUNT_STATE);
    216    }
    217  },
    218 
    219  // Set a cached token. |tokenData| must have a 'token' element, but may also
    220  // have additional fields.
    221  // The 'get' functions below return the entire |tokenData| value.
    222  setCachedToken(scopeArray, tokenData) {
    223    this._cachePreamble();
    224    if (!tokenData.token) {
    225      throw new Error("No token");
    226    }
    227    let key = getScopeKey(scopeArray);
    228    this.oauthTokens[key] = tokenData;
    229    // And a background save...
    230    this._persistCachedTokens();
    231  },
    232 
    233  // Return data for a cached token or null (or throws on bad state etc)
    234  getCachedToken(scopeArray) {
    235    this._cachePreamble();
    236    let key = getScopeKey(scopeArray);
    237    let result = this.oauthTokens[key];
    238    if (result) {
    239      // later we might want to check an expiry date - but we currently
    240      // have no such concept, so just return it.
    241      log.trace("getCachedToken returning cached token");
    242      return result;
    243    }
    244    return null;
    245  },
    246 
    247  // Remove a cached token from the cache.  Does *not* revoke it from anywhere.
    248  // Returns the entire token entry if found, null otherwise.
    249  removeCachedToken(token) {
    250    this._cachePreamble();
    251    let data = this.oauthTokens;
    252    for (let [key, tokenValue] of Object.entries(data)) {
    253      if (tokenValue.token == token) {
    254        delete data[key];
    255        // And a background save...
    256        this._persistCachedTokens();
    257        return tokenValue;
    258      }
    259    }
    260    return null;
    261  },
    262 
    263  // A hook-point for tests.  Returns a promise that's ignored in most cases
    264  // (notable exceptions are tests and when we explicitly are saving the entire
    265  // set of user data.)
    266  _persistCachedTokens() {
    267    this._cachePreamble();
    268    return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch(
    269      err => {
    270        log.error("Failed to update cached tokens", err);
    271      }
    272    );
    273  },
    274 };
    275 
    276 /* Given an array of scopes, make a string key by normalizing. */
    277 function getScopeKey(scopeArray) {
    278  let normalizedScopes = scopeArray.map(item => item.toLowerCase());
    279  return normalizedScopes.sort().join("|");
    280 }
    281 
    282 function getPropertyDescriptor(obj, prop) {
    283  return (
    284    Object.getOwnPropertyDescriptor(obj, prop) ||
    285    getPropertyDescriptor(Object.getPrototypeOf(obj), prop)
    286  );
    287 }
    288 
    289 /**
    290 * Copies properties from a given object to another object.
    291 *
    292 * @param from (object)
    293 *        The object we read property descriptors from.
    294 * @param to (object)
    295 *        The object that we set property descriptors on.
    296 * @param thisObj (object)
    297 *        The object that will be used to .bind() all function properties we find to.
    298 * @param keys ([...])
    299 *        The names of all properties to be copied.
    300 */
    301 function copyObjectProperties(from, to, thisObj, keys) {
    302  for (let prop of keys) {
    303    // Look for the prop in the prototype chain.
    304    let desc = getPropertyDescriptor(from, prop);
    305 
    306    if (typeof desc.value == "function") {
    307      desc.value = desc.value.bind(thisObj);
    308    }
    309 
    310    if (desc.get) {
    311      desc.get = desc.get.bind(thisObj);
    312    }
    313 
    314    if (desc.set) {
    315      desc.set = desc.set.bind(thisObj);
    316    }
    317 
    318    Object.defineProperty(to, prop, desc);
    319  }
    320 }
    321 
    322 /**
    323 * The public API.
    324 *
    325 * TODO - *all* non-underscore stuff here should have sphinx docstrings so
    326 * that docs magically appear on https://firefox-source-docs.mozilla.org/
    327 * (although |./mach doc| is broken on windows (bug 1232403) and on Linux for
    328 * markh (some obscure npm issue he gave up on) - so later...)
    329 */
    330 export class FxAccounts {
    331  constructor(mocks = null) {
    332    this._internal = new FxAccountsInternal();
    333    if (mocks) {
    334      // it's slightly unfortunate that we need to mock the main "internal" object
    335      // before calling initialize, primarily so a mock `newAccountState` is in
    336      // place before initialize calls it, but we need to initialize the
    337      // "sub-object" mocks after. This can probably be fixed, but whatever...
    338      copyObjectProperties(
    339        mocks,
    340        this._internal,
    341        this._internal,
    342        Object.keys(mocks).filter(key => !["device", "commands"].includes(key))
    343      );
    344    }
    345    this._internal.initialize();
    346    // allow mocking our "sub-objects" too.
    347    if (mocks) {
    348      for (let subobject of [
    349        "currentAccountState",
    350        "keys",
    351        "fxaPushService",
    352        "device",
    353        "commands",
    354      ]) {
    355        if (typeof mocks[subobject] == "object") {
    356          copyObjectProperties(
    357            mocks[subobject],
    358            this._internal[subobject],
    359            this._internal[subobject],
    360            Object.keys(mocks[subobject])
    361          );
    362        }
    363      }
    364    }
    365  }
    366 
    367  get commands() {
    368    return this._internal.commands;
    369  }
    370 
    371  static get config() {
    372    return lazy.FxAccountsConfig;
    373  }
    374 
    375  get device() {
    376    return this._internal.device;
    377  }
    378 
    379  get keys() {
    380    return this._internal.keys;
    381  }
    382 
    383  get telemetry() {
    384    return this._internal.telemetry;
    385  }
    386 
    387  _withCurrentAccountState(func) {
    388    return this._internal.withCurrentAccountState(func);
    389  }
    390 
    391  _withVerifiedAccountState(func) {
    392    return this._internal.withVerifiedAccountState(func);
    393  }
    394 
    395  _withSessionToken(func, mustBeVerified = true) {
    396    return this._internal.withSessionToken(func, mustBeVerified);
    397  }
    398 
    399  /**
    400   * Returns an array listing all the OAuth clients connected to the
    401   * authenticated user's account. This includes browsers and web sessions - no
    402   * filtering is done of the set returned by the FxA server. This is using a cached
    403   * result if it's not older than 4 hours. If the cached data is too old or
    404   * missing, it fetches new data and updates the cache.
    405   *
    406   * @typedef {object} AttachedClient
    407   * @property {string} id - OAuth `client_id` of the client.
    408   * @property {number} lastAccessedDaysAgo - How many days ago the client last
    409   *    accessed the FxA server APIs.
    410   *
    411   * @returns {Array.<AttachedClient>} A list of attached clients.
    412   */
    413  async listAttachedOAuthClients(forceRefresh = false) {
    414    const now = Date.now();
    415 
    416    // Check if cached data is still valid
    417    if (
    418      this._cachedClients &&
    419      now - this._cachedClientsTimestamp < ATTACHED_CLIENTS_CACHE_DURATION &&
    420      !forceRefresh
    421    ) {
    422      return this._cachedClients;
    423    }
    424 
    425    // Cache is empty or expired, fetch new data
    426    let clients = null;
    427    try {
    428      clients = await this._fetchAttachedOAuthClients();
    429      this._cachedClients = clients;
    430      this._cachedClientsTimestamp = now;
    431    } catch (error) {
    432      log.error("Could not update attached clients list ", error);
    433      clients = [];
    434    }
    435 
    436    return clients;
    437  }
    438 
    439  /**
    440   * This private method actually fetches the clients from the server.
    441   * It should not be called directly by consumers of this API.
    442   *
    443   * @returns {Array.<AttachedClient>}
    444   * @private
    445   */
    446  async _fetchAttachedOAuthClients() {
    447    const ONE_DAY = 24 * 60 * 60 * 1000;
    448 
    449    return this._withSessionToken(async sessionToken => {
    450      const response =
    451        await this._internal.fxAccountsClient.attachedClients(sessionToken);
    452      const attachedClients = response.body;
    453      const timestamp = response.headers["x-timestamp"];
    454      const now =
    455        timestamp !== undefined
    456          ? new Date(parseInt(timestamp, 10))
    457          : Date.now();
    458      return attachedClients.map(client => {
    459        const daysAgo = client.lastAccessTime
    460          ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0)
    461          : null;
    462        return {
    463          id: client.clientId,
    464          lastAccessedDaysAgo: daysAgo,
    465        };
    466      });
    467    });
    468  }
    469 
    470  /**
    471   * Get an OAuth token for the user.
    472   *
    473   * @param options
    474   *        {
    475   *          scope: (string/array) the oauth scope(s) being requested. As a
    476   *                 convenience, you may pass a string if only one scope is
    477   *                 required, or an array of strings if multiple are needed.
    478   *          ttl: (number) OAuth token TTL in seconds.
    479   *        }
    480   *
    481   * @return Promise.<string | Error>
    482   *        The promise resolves the oauth token as a string or rejects with
    483   *        an error object ({error: ERROR, details: {}}) of the following:
    484   *          INVALID_PARAMETER
    485   *          NO_ACCOUNT
    486   *          UNVERIFIED_ACCOUNT
    487   *          NETWORK_ERROR
    488   *          AUTH_ERROR
    489   *          UNKNOWN_ERROR
    490   */
    491  async getOAuthToken(options = {}) {
    492    try {
    493      return await this._internal.getOAuthToken(options);
    494    } catch (err) {
    495      throw this._internal._errorToErrorClass(err);
    496    }
    497  }
    498 
    499  /**
    500   * Gets both the OAuth token and the users scoped keys for that token
    501   * and verifies that both operations were done for the same user,
    502   * preventing race conditions where a caller
    503   * can get the key for one user, and the id of another if the user
    504   * is rapidly switching between accounts
    505   *
    506   * @param options
    507   *        {
    508   *          scope: string the oauth scope being requested. This must
    509   *          be a scope with an associated key, otherwise an error
    510   *          will be thrown that the key is not available.
    511   *          ttl: (number) OAuth token TTL in seconds
    512   *        }
    513   *
    514   * @return Promise.<Object | Error>
    515   * The promise resolve to both the access token being requested, and the scoped key
    516   *        {
    517   *         token: (string) access token
    518   *         key: (object) the scoped key object
    519   *        }
    520   * The promise can reject, with one of the errors `getOAuthToken`, `FxAccountKeys.getKeyForScope`, or
    521   * error if the user changed in-between operations
    522   */
    523  getOAuthTokenAndKey(options = {}) {
    524    return this._withCurrentAccountState(async () => {
    525      const key = await this.keys.getKeyForScope(options.scope);
    526      const token = await this.getOAuthToken(options);
    527      return { token, key };
    528    });
    529  }
    530 
    531  /**
    532   * Remove an OAuth token from the token cache. Callers should call this
    533   * after they determine a token is invalid, so a new token will be fetched
    534   * on the next call to getOAuthToken().
    535   *
    536   * @param options
    537   *        {
    538   *          token: (string) A previously fetched token.
    539   *        }
    540   * @return Promise.<undefined> This function will always resolve, even if
    541   *         an unknown token is passed.
    542   */
    543  removeCachedOAuthToken(options) {
    544    return this._internal.removeCachedOAuthToken(options);
    545  }
    546 
    547  /**
    548   * Get details about the user currently signed in to Firefox Accounts.
    549   *
    550   * @return Promise
    551   *        The promise resolves to the credentials object of the signed-in user:
    552   *        {
    553   *          email: String: The user's email address
    554   *          uid: String: The user's unique id
    555   *          verified: Boolean: Firefox verification status. If false, the account should
    556   *                    be considered partially logged-in to this Firefox. This may be false
    557   *                    even if the underying account verfied status is true.
    558   *          displayName: String or null if not known.
    559   *          avatar: URL of the avatar for the user. May be the default
    560   *                  avatar, or null in edge-cases (eg, if there's an account
    561   *                  issue, etc
    562   *          avatarDefault: boolean - whether `avatar` is specific to the user
    563   *                         or the default avatar.
    564   *        }
    565   *
    566   *        or null if no user is signed in. This function never fails except
    567   *        in pathological cases (eg, file-system errors, etc)
    568   */
    569  getSignedInUser(addnFields = []) {
    570    // Note we don't return the session token, but use it to see if we
    571    // should fetch the profile. Ditto scopedKeys re verified.
    572    const ACCT_DATA_FIELDS = [
    573      "email",
    574      "uid",
    575      "verified",
    576      "scopedKeys",
    577      "sessionToken",
    578    ];
    579    const PROFILE_FIELDS = ["displayName", "avatar", "avatarDefault"];
    580    return this._withCurrentAccountState(async currentState => {
    581      const data = await currentState.getUserAccountData(
    582        ACCT_DATA_FIELDS.concat(addnFields)
    583      );
    584      if (!data) {
    585        return null;
    586      }
    587      if (!lazy.FXA_ENABLED) {
    588        await this.signOut();
    589        return null;
    590      }
    591      delete data.scopedKeys;
    592 
    593      let profileData = null;
    594      if (data.sessionToken) {
    595        delete data.sessionToken;
    596        try {
    597          profileData = await this._internal.profile.getProfile();
    598        } catch (error) {
    599          log.error("Could not retrieve profile data", error);
    600        }
    601      }
    602      for (let field of PROFILE_FIELDS) {
    603        data[field] = profileData ? profileData[field] : null;
    604      }
    605      // and email is a special case - if we have profile data we prefer the
    606      // email from that, as the email we stored for the account itself might
    607      // not have been updated if the email changed since the user signed in.
    608      if (profileData && profileData.email) {
    609        data.email = profileData.email;
    610      }
    611      return data;
    612    });
    613  }
    614 
    615  /**
    616   * Checks the status of the account. Resolves with Promise<boolean>, where
    617   * true indicates the account status is OK and false indicates there's some
    618   * issue with the account - either that there's no user currently signed in,
    619   * the entire account has been deleted (in which case there will be no user
    620   * signed in after this call returns), or that the user must reauthenticate (in
    621   * which case `this.hasLocalSession()` will return `false` after this call
    622   * returns).
    623   *
    624   * Typically used when some external code which uses, for example, oauth tokens
    625   * received a 401 error using the token, or that this external code has some
    626   * other reason to believe the account status may be bad. Note that this will
    627   * be called automatically in many cases - for example, if calls to fetch the
    628   * profile, or fetch keys, etc return a 401, there's no need to call this
    629   * function.
    630   *
    631   * Because this hits the server, you should only call this method when you have
    632   * good reason to believe the session very recently became invalid (eg, because
    633   * you saw an auth related exception from a remote service.)
    634   */
    635  checkAccountStatus() {
    636    // Note that we don't use _withCurrentAccountState here because that will
    637    // cause an exception to be thrown if we end up signing out due to the
    638    // account not existing, which isn't what we want here.
    639    let state = this._internal.currentAccountState;
    640    return this._internal.checkAccountStatus(state);
    641  }
    642 
    643  /**
    644   * Checks if we have a valid local session state for the current account.
    645   *
    646   * @return Promise
    647   *        Resolves with a boolean, with true indicating that we appear to
    648   *        have a valid local session, or false if we need to reauthenticate
    649   *        with the content server to obtain one.
    650   *        Note that this only checks local state, although typically that's
    651   *        OK, because we drop the local session information whenever we detect
    652   *        we are in this state. However, see checkAccountStatus() for a way to
    653   *        check the account and session status with the server, which can be
    654   *        considered the canonical, albiet expensive, way to determine the
    655   *        status of the account.
    656   */
    657  hasLocalSession() {
    658    return this._withCurrentAccountState(async state => {
    659      let data = await state.getUserAccountData(["sessionToken"]);
    660      return !!(data && data.sessionToken);
    661    });
    662  }
    663 
    664  /**
    665   * Returns a promise that resolves to true if we can currently connect (ie,
    666   * sign in, or re-connect after a password change) to a Firefox Account.
    667   * If this returns false, the caller can assume that some UI was shown
    668   * which tells the user why we could not connect.
    669   *
    670   * Currently, the primary password being locked is the only reason why
    671   * this returns false, and in this scenario, the primary password unlock
    672   * dialog will have been shown.
    673   *
    674   * This currently doesn't need to return a promise, but does so that
    675   * future enhancements, such as other explanatory UI which requires
    676   * async can work without modification of the call-sites.
    677   */
    678  static canConnectAccount() {
    679    return Promise.resolve(!lazy.mpLocked() || lazy.ensureMPUnlocked());
    680  }
    681 
    682  /**
    683   * Send a message to a set of devices in the same account
    684   *
    685   * @param deviceIds: (null/string/array) The device IDs to send the message to.
    686   *                   If null, will be sent to all devices.
    687   *
    688   * @param excludedIds: (null/string/array) If deviceIds is null, this may
    689   *                     list device IDs which should not receive the message.
    690   *
    691   * @param payload: (object) The payload, which will be JSON.stringified.
    692   *
    693   * @param TTL: How long the message should be retained before it is discarded.
    694   */
    695  // XXX - used only by sync to tell other devices that the clients collection
    696  // has changed so they should sync asap. The API here is somewhat vague (ie,
    697  // "an object"), but to be useful across devices, the payload really needs
    698  // formalizing. We should try and do something better here.
    699  notifyDevices(deviceIds, excludedIds, payload, TTL) {
    700    return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL);
    701  }
    702 
    703  /**
    704   * Resend the verification email for the currently signed-in user.
    705   *
    706   */
    707  resendVerificationEmail() {
    708    return this._withSessionToken((token, _currentState) => {
    709      return this._internal.fxAccountsClient.resendVerificationEmail(token);
    710    }, false);
    711  }
    712 
    713  async signOut(localOnly) {
    714    // Note that we do not use _withCurrentAccountState here, otherwise we
    715    // end up with an exception due to the user signing out before the call is
    716    // complete - but that's the entire point of this method :)
    717    return this._internal.signOut(localOnly);
    718  }
    719 
    720  // XXX - we should consider killing this - the only reason it is public is
    721  // so that sync can change it when it notices the device name being changed,
    722  // and that could probably be replaced with a pref observer.
    723  updateDeviceRegistration() {
    724    return this._withCurrentAccountState(_ => {
    725      return this._internal.updateDeviceRegistration();
    726    });
    727  }
    728 
    729  /**
    730   * Generate a log file for the FxA action that just completed
    731   * and refresh the input & output streams.
    732   */
    733  async flushLogFile() {
    734    const logType = await logManager.resetFileLog();
    735    if (logType == logManager.ERROR_LOG_WRITTEN) {
    736      console.error(
    737        "FxA encountered an error - see about:sync-log for the log file."
    738      );
    739    }
    740    Services.obs.notifyObservers(null, "service:log-manager:flush-log-file");
    741  }
    742 }
    743 
    744 var FxAccountsInternal = function () {};
    745 
    746 /**
    747 * The internal API's prototype.
    748 */
    749 FxAccountsInternal.prototype = {
    750  // Make a local copy of this constant so we can mock it in testing
    751  POLL_SESSION,
    752 
    753  // The timeout (in ms) we use to poll for a verified mail for the first
    754  // VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD minutes if the user has
    755  // logged-in in this session.
    756  VERIFICATION_POLL_TIMEOUT_INITIAL: 60000, // 1 minute.
    757  // All the other cases (> 5 min, on restart etc).
    758  VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 5 * 60000, // 5 minutes.
    759  // After X minutes, the polling will slow down to _SUBSEQUENT if we have
    760  // logged-in in this session.
    761  VERIFICATION_POLL_START_SLOWDOWN_THRESHOLD: 5,
    762 
    763  _fxAccountsClient: null,
    764 
    765  // All significant initialization should be done in this initialize() method
    766  // to help with our mocking story.
    767  initialize() {
    768    ChromeUtils.defineLazyGetter(this, "fxaPushService", function () {
    769      return Cc["@mozilla.org/fxaccounts/push;1"].getService(Ci.nsISupports)
    770        .wrappedJSObject;
    771    });
    772 
    773    this.keys = new lazy.FxAccountsKeys(this);
    774 
    775    if (!this.observerPreloads) {
    776      // A registry of promise-returning functions that `notifyObservers` should
    777      // call before sending notifications. Primarily used so parts of Firefox
    778      // which have yet to load for performance reasons can be force-loaded, and
    779      // thus not miss notifications.
    780      this.observerPreloads = [
    781        // Sync
    782        () => {
    783          let { Weave } = ChromeUtils.importESModule(
    784            "resource://services-sync/main.sys.mjs"
    785          );
    786          return Weave.Service.promiseInitialized;
    787        },
    788      ];
    789    }
    790 
    791    // This holds the list of attached clients from the /account/attached_clients endpoint
    792    // Most calls to that endpoint generally don't need fresh data so we try to prevent
    793    // as many network requests as possible
    794    this._cachedAttachedClients = null;
    795    this._cachedAttachedClientsTimestamp = 0;
    796    // This object holds details about, and storage for, the current user. It
    797    // is replaced when a different user signs in. Instead of using it directly,
    798    // you should try and use `withCurrentAccountState`.
    799    this.currentAccountState = this.newAccountState();
    800  },
    801 
    802  async withCurrentAccountState(func) {
    803    const state = this.currentAccountState;
    804    let result;
    805    try {
    806      result = await func(state);
    807    } catch (ex) {
    808      return state.reject(ex);
    809    }
    810    return state.resolve(result);
    811  },
    812 
    813  async withVerifiedAccountState(func) {
    814    return this.withCurrentAccountState(async state => {
    815      let data = await state.getUserAccountData();
    816      if (!data) {
    817        // No signed-in user
    818        throw this._error(ERROR_NO_ACCOUNT);
    819      }
    820 
    821      if (!data.verified) {
    822        // Signed-in user has not verified email
    823        throw this._error(ERROR_UNVERIFIED_ACCOUNT);
    824      }
    825      return func(state);
    826    });
    827  },
    828 
    829  async withSessionToken(func, mustBeVerified = true) {
    830    const state = this.currentAccountState;
    831    let data = await state.getUserAccountData();
    832    if (!data) {
    833      // No signed-in user
    834      throw this._error(ERROR_NO_ACCOUNT);
    835    }
    836 
    837    if (mustBeVerified && !data.verified) {
    838      // Signed-in user has not verified email
    839      throw this._error(ERROR_UNVERIFIED_ACCOUNT);
    840    }
    841 
    842    if (!data.sessionToken) {
    843      throw this._error(ERROR_AUTH_ERROR, "no session token");
    844    }
    845    try {
    846      // Anyone who needs the session token is going to send it to the server,
    847      // so there's a chance we'll see an auth related error - so handle that
    848      // here rather than requiring each caller to remember to.
    849      let result = await func(data.sessionToken, state);
    850      return state.resolve(result);
    851    } catch (err) {
    852      return this._handleTokenError(err);
    853    }
    854  },
    855 
    856  get fxAccountsClient() {
    857    if (!this._fxAccountsClient) {
    858      this._fxAccountsClient = new lazy.FxAccountsClient();
    859    }
    860    return this._fxAccountsClient;
    861  },
    862 
    863  // The profile object used to fetch the actual user profile.
    864  _profile: null,
    865  get profile() {
    866    if (!this._profile) {
    867      let profileServerUrl = Services.urlFormatter.formatURLPref(
    868        "identity.fxaccounts.remote.profile.uri"
    869      );
    870      this._profile = new lazy.FxAccountsProfile({
    871        fxa: this,
    872        profileServerUrl,
    873      });
    874    }
    875    return this._profile;
    876  },
    877 
    878  _commands: null,
    879  get commands() {
    880    if (!this._commands) {
    881      // FxAccountsCommands registers a shutdown blocker and must not be
    882      // instantiated if shutdown already started.
    883      if (
    884        !Services.startup.isInOrBeyondShutdownPhase(
    885          Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
    886        )
    887      ) {
    888        this._commands = new lazy.FxAccountsCommands(this);
    889      }
    890    }
    891    return this._commands;
    892  },
    893 
    894  _device: null,
    895  get device() {
    896    if (!this._device) {
    897      this._device = new lazy.FxAccountsDevice(this);
    898    }
    899    return this._device;
    900  },
    901 
    902  _oauth: null,
    903  get oauth() {
    904    if (!this._oauth) {
    905      this._oauth = new lazy.FxAccountsOAuth(this.fxAccountsClient, this.keys);
    906    }
    907    return this._oauth;
    908  },
    909 
    910  _telemetry: null,
    911  get telemetry() {
    912    if (!this._telemetry) {
    913      this._telemetry = new lazy.FxAccountsTelemetry(this);
    914    }
    915    return this._telemetry;
    916  },
    917 
    918  beginOAuthFlow(scopes) {
    919    return this.oauth.beginOAuthFlow(scopes);
    920  },
    921 
    922  completeOAuthFlow(sessionToken, code, state) {
    923    return this.oauth.completeOAuthFlow(sessionToken, code, state);
    924  },
    925 
    926  setScopedKeys(scopedKeys) {
    927    return this.keys.setScopedKeys(scopedKeys);
    928  },
    929 
    930  // A hook-point for tests who may want a mocked AccountState or mocked storage.
    931  newAccountState(credentials) {
    932    let storage = new FxAccountsStorageManager();
    933    storage.initialize(credentials);
    934    return new AccountState(storage);
    935  },
    936 
    937  notifyDevices(deviceIds, excludedIds, payload, TTL) {
    938    if (typeof deviceIds == "string") {
    939      deviceIds = [deviceIds];
    940    }
    941    return this.withSessionToken(sessionToken => {
    942      return this.fxAccountsClient.notifyDevices(
    943        sessionToken,
    944        deviceIds,
    945        excludedIds,
    946        payload,
    947        TTL
    948      );
    949    });
    950  },
    951 
    952  /**
    953   * Return the current time in milliseconds as an integer.  Allows tests to
    954   * manipulate the date to simulate token expiration.
    955   */
    956  now() {
    957    return this.fxAccountsClient.now();
    958  },
    959 
    960  /**
    961   * Return clock offset in milliseconds, as reported by the fxAccountsClient.
    962   * This can be overridden for testing.
    963   *
    964   * The offset is the number of milliseconds that must be added to the client
    965   * clock to make it equal to the server clock.  For example, if the client is
    966   * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
    967   */
    968  get localtimeOffsetMsec() {
    969    return this.fxAccountsClient.localtimeOffsetMsec;
    970  },
    971 
    972  /**
    973   * Ask the server whether the user's email has been verified
    974   */
    975  checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
    976    if (!sessionToken) {
    977      return Promise.reject(
    978        new Error("checkEmailStatus called without a session token")
    979      );
    980    }
    981    return this.fxAccountsClient
    982      .recoveryEmailStatus(sessionToken, options)
    983      .catch(error => this._handleTokenError(error));
    984  },
    985 
    986  // set() makes sure that polling is happening, if necessary.
    987  // get() does not wait for verification, and returns an object even if
    988  // unverified. The caller of get() must check .verified .
    989  // The "fxaccounts:onverified" event will fire only when the verified
    990  // state goes from false to true, so callers must register their observer
    991  // and then call get(). In particular, it will not fire when the account
    992  // was found to be verified in a previous boot: if our stored state says
    993  // the account is verified, the event will never fire. So callers must do:
    994  //   register notification observer (go)
    995  //   userdata = get()
    996  //   if (userdata.verified()) {go()}
    997 
    998  /**
    999   * Set the current user signed in to Firefox Accounts.
   1000   *
   1001   * @param credentials
   1002   *        The credentials object obtained by logging in or creating
   1003   *        an account on the FxA server:
   1004   *        {
   1005   *          authAt: The time (seconds since epoch) that this record was
   1006   *                  authenticated
   1007   *          email: The users email address
   1008   *          keyFetchToken: a keyFetchToken which has not yet been used
   1009   *          sessionToken: Session for the FxA server
   1010   *          uid: The user's unique id
   1011   *          unwrapBKey: used to unwrap kB, derived locally from the
   1012   *                      password (not revealed to the FxA server)
   1013   *          verified: true/false
   1014   *        }
   1015   * @return Promise
   1016   *         The promise resolves to null when the data is saved
   1017   *         successfully and is rejected on error.
   1018   */
   1019  async setSignedInUser(credentials) {
   1020    if (!lazy.FXA_ENABLED) {
   1021      throw new Error("Cannot call setSignedInUser when FxA is disabled.");
   1022    }
   1023    for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
   1024      Services.prefs.clearUserPref(pref);
   1025    }
   1026    log.debug("setSignedInUser - aborting any existing flows");
   1027    const signedInUser = await this.currentAccountState.getUserAccountData();
   1028    if (signedInUser) {
   1029      await this._signOutServer(
   1030        signedInUser.sessionToken,
   1031        signedInUser.oauthTokens
   1032      );
   1033    }
   1034    await this.abortExistingFlow();
   1035    const currentAccountState = (this.currentAccountState =
   1036      this.newAccountState(
   1037        Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
   1038      ));
   1039    // This promise waits for storage, but not for verification.
   1040    // We're telling the caller that this is durable now (although is that
   1041    // really something we should commit to? Why not let the write happen in
   1042    // the background? Already does for updateAccountData ;)
   1043    await currentAccountState.promiseInitialized;
   1044    await this.notifyObservers(ONLOGIN_NOTIFICATION);
   1045    await this.updateDeviceRegistration();
   1046    return currentAccountState.resolve();
   1047  },
   1048 
   1049  /**
   1050   * Update account data for the currently signed in user.
   1051   *
   1052   * @param credentials
   1053   *        The credentials object containing the fields to be updated.
   1054   *        This object must contain the |uid| field and it must
   1055   *        match the currently signed in user.
   1056   */
   1057  updateUserAccountData(credentials) {
   1058    log.debug(
   1059      "updateUserAccountData called with fields",
   1060      Object.keys(credentials)
   1061    );
   1062    if (logPII()) {
   1063      log.debug("updateUserAccountData called with data", credentials);
   1064    }
   1065    let currentAccountState = this.currentAccountState;
   1066    return currentAccountState.promiseInitialized.then(() => {
   1067      if (!credentials.uid) {
   1068        throw new Error("The specified credentials have no uid");
   1069      }
   1070      return currentAccountState.updateUserAccountData(credentials);
   1071    });
   1072  },
   1073 
   1074  /*
   1075   * Reset state such that any previous flow is canceled.
   1076   */
   1077  abortExistingFlow() {
   1078    if (this._profile) {
   1079      this._profile.tearDown();
   1080      this._profile = null;
   1081    }
   1082    if (this._commands) {
   1083      this._commands = null;
   1084    }
   1085    if (this._device) {
   1086      this._device.reset();
   1087    }
   1088    // We "abort" the accountState and assume our caller is about to throw it
   1089    // away and replace it with a new one.
   1090    return this.currentAccountState.abort();
   1091  },
   1092 
   1093  /**
   1094   * Destroyes an OAuth Token by sending a request to the FxA server
   1095   *
   1096   * @param {object} tokenData
   1097   *  The token's data, with `tokenData.token` being the token itself
   1098   */
   1099  destroyOAuthToken(tokenData) {
   1100    return this.fxAccountsClient.oauthDestroy(OAUTH_CLIENT_ID, tokenData.token);
   1101  },
   1102 
   1103  _destroyAllOAuthTokens(tokenInfos) {
   1104    if (!tokenInfos) {
   1105      return Promise.resolve();
   1106    }
   1107    // let's just destroy them all in parallel...
   1108    let promises = [];
   1109    for (let tokenInfo of Object.values(tokenInfos)) {
   1110      promises.push(this.destroyOAuthToken(tokenInfo));
   1111    }
   1112    return Promise.all(promises);
   1113  },
   1114 
   1115  // We need to do a one-off migration of a preference to protect against
   1116  // accidentally merging sync data.
   1117  // We replace a previously hashed email with a hashed uid.
   1118  _migratePreviousAccountNameHashPref(uid) {
   1119    if (Services.prefs.prefHasUserValue(PREF_LAST_FXA_USER_EMAIL)) {
   1120      Services.prefs.setStringPref(
   1121        PREF_LAST_FXA_USER_UID,
   1122        lazy.CryptoUtils.sha256Base64(uid)
   1123      );
   1124      Services.prefs.clearUserPref(PREF_LAST_FXA_USER_EMAIL);
   1125    }
   1126  },
   1127 
   1128  async signOut(localOnly) {
   1129    let sessionToken;
   1130    let tokensToRevoke;
   1131    const data = await this.currentAccountState.getUserAccountData();
   1132    // Save the sessionToken, tokens before resetting them in _signOutLocal().
   1133    if (data) {
   1134      sessionToken = data.sessionToken;
   1135      tokensToRevoke = data.oauthTokens;
   1136      this._migratePreviousAccountNameHashPref(data.uid);
   1137    }
   1138    await this.notifyObservers(ON_PRELOGOUT_NOTIFICATION);
   1139    await this._signOutLocal();
   1140    if (!localOnly) {
   1141      // Do this in the background so *any* slow request won't
   1142      // block the local sign out.
   1143      Services.tm.dispatchToMainThread(async () => {
   1144        await this._signOutServer(sessionToken, tokensToRevoke);
   1145        lazy.FxAccountsConfig.resetConfigURLs();
   1146        this.notifyObservers("testhelper-fxa-signout-complete");
   1147      });
   1148    } else {
   1149      // We want to do this either way -- but if we're signing out remotely we
   1150      // need to wait until we destroy the oauth tokens if we want that to succeed.
   1151      lazy.FxAccountsConfig.resetConfigURLs();
   1152    }
   1153    return this.notifyObservers(ONLOGOUT_NOTIFICATION);
   1154  },
   1155 
   1156  async _signOutLocal() {
   1157    for (const pref of Services.prefs.getChildList(PREF_ACCOUNT_ROOT)) {
   1158      Services.prefs.clearUserPref(pref);
   1159    }
   1160    await this.currentAccountState.signOut();
   1161    // this "aborts" this.currentAccountState but doesn't make a new one.
   1162    await this.abortExistingFlow();
   1163    this.currentAccountState = this.newAccountState();
   1164    return this.currentAccountState.promiseInitialized;
   1165  },
   1166 
   1167  async _signOutServer(sessionToken, tokensToRevoke) {
   1168    log.debug("Unsubscribing from FxA push.");
   1169    try {
   1170      await this.fxaPushService.unsubscribe();
   1171    } catch (err) {
   1172      log.error("Could not unsubscribe from push.", err);
   1173    }
   1174    if (sessionToken) {
   1175      log.debug("Destroying session and device.");
   1176      try {
   1177        await this.fxAccountsClient.signOut(sessionToken, { service: "sync" });
   1178      } catch (err) {
   1179        log.error("Error during remote sign out of Firefox Accounts", err);
   1180      }
   1181    } else {
   1182      log.warn("Missing session token; skipping remote sign out");
   1183    }
   1184    log.debug("Destroying all OAuth tokens.");
   1185    try {
   1186      await this._destroyAllOAuthTokens(tokensToRevoke);
   1187    } catch (err) {
   1188      log.error("Error during destruction of oauth tokens during signout", err);
   1189    }
   1190  },
   1191 
   1192  getUserAccountData(fieldNames = null) {
   1193    return this.currentAccountState.getUserAccountData(fieldNames);
   1194  },
   1195 
   1196  async notifyObservers(topic, data) {
   1197    for (let f of this.observerPreloads) {
   1198      try {
   1199        await f();
   1200      } catch (O_o) {}
   1201    }
   1202    log.debug("Notifying observers of " + topic);
   1203    Services.obs.notifyObservers(null, topic, data);
   1204  },
   1205 
   1206  /**
   1207   * Does the actual fetch of an oauth token for getOAuthToken()
   1208   * using the account session token.
   1209   *
   1210   * It's split out into a separate method so that we can easily
   1211   * stash in-flight calls in a cache.
   1212   *
   1213   * @param {string} scopeString
   1214   * @param {number} ttl
   1215   * @returns {Promise<string>}
   1216   * @private
   1217   */
   1218  async _doTokenFetchWithSessionToken(sessionToken, scopeString, ttl) {
   1219    const result = await this.fxAccountsClient.accessTokenWithSessionToken(
   1220      sessionToken,
   1221      OAUTH_CLIENT_ID,
   1222      scopeString,
   1223      ttl
   1224    );
   1225    return result.access_token;
   1226  },
   1227 
   1228  getOAuthToken(options = {}) {
   1229    log.debug("getOAuthToken enter");
   1230    let scope = options.scope;
   1231    if (typeof scope === "string") {
   1232      scope = [scope];
   1233    }
   1234 
   1235    if (!scope || !scope.length) {
   1236      return Promise.reject(
   1237        this._error(
   1238          ERROR_INVALID_PARAMETER,
   1239          "Missing or invalid 'scope' option"
   1240        )
   1241      );
   1242    }
   1243 
   1244    return this.withSessionToken(async (sessionToken, currentState) => {
   1245      // Early exit for a cached token.
   1246      let cached = currentState.getCachedToken(scope);
   1247      if (cached) {
   1248        log.debug("getOAuthToken returning a cached token");
   1249        return cached.token;
   1250      }
   1251 
   1252      // Build the string we use in our "inflight" map and that we send to the
   1253      // server. Because it's used as a key in the map we sort the scopes.
   1254      let scopeString = scope.sort().join(" ");
   1255 
   1256      // We keep a map of in-flight requests to avoid multiple promise-based
   1257      // consumers concurrently requesting the same token.
   1258      let maybeInFlight = currentState.inFlightTokenRequests.get(scopeString);
   1259      if (maybeInFlight) {
   1260        log.debug("getOAuthToken has an in-flight request for this scope");
   1261        return maybeInFlight;
   1262      }
   1263 
   1264      // We need to start a new fetch and stick the promise in our in-flight map
   1265      // and remove it when it resolves.
   1266      let promise = this._doTokenFetchWithSessionToken(
   1267        sessionToken,
   1268        scopeString,
   1269        options.ttl
   1270      )
   1271        .then(token => {
   1272          // As a sanity check, ensure something else hasn't raced getting a token
   1273          // of the same scope. If something has we just make noise rather than
   1274          // taking any concrete action because it should never actually happen.
   1275          if (currentState.getCachedToken(scope)) {
   1276            log.error(`detected a race for oauth token with scope ${scope}`);
   1277          }
   1278          // If we got one, cache it.
   1279          if (token) {
   1280            let entry = { token };
   1281            currentState.setCachedToken(scope, entry);
   1282          }
   1283          return token;
   1284        })
   1285        .finally(() => {
   1286          // Remove ourself from the in-flight map. There's no need to check the
   1287          // result of .delete() to handle a signout race, because setCachedToken
   1288          // above will fail in that case and cause the entire call to fail.
   1289          currentState.inFlightTokenRequests.delete(scopeString);
   1290        });
   1291 
   1292      currentState.inFlightTokenRequests.set(scopeString, promise);
   1293      return promise;
   1294    });
   1295  },
   1296 
   1297  /**
   1298   * Remove an OAuth token from the token cache
   1299   * and makes a network request to FxA server to destroy the token.
   1300   *
   1301   * @param options
   1302   *        {
   1303   *          token: (string) A previously fetched token.
   1304   *        }
   1305   * @return Promise.<undefined> This function will always resolve, even if
   1306   *         an unknown token is passed.
   1307   */
   1308  removeCachedOAuthToken(options) {
   1309    if (!options.token || typeof options.token !== "string") {
   1310      throw this._error(
   1311        ERROR_INVALID_PARAMETER,
   1312        "Missing or invalid 'token' option"
   1313      );
   1314    }
   1315    return this.withCurrentAccountState(currentState => {
   1316      let existing = currentState.removeCachedToken(options.token);
   1317      if (existing) {
   1318        // background destroy.
   1319        this.destroyOAuthToken(existing).catch(err => {
   1320          log.warn("FxA failed to revoke a cached token", err);
   1321        });
   1322      }
   1323    });
   1324  },
   1325 
   1326  /**
   1327   * Sets the user to be verified in the account state,
   1328   */
   1329  async setUserVerified() {
   1330    await this.withCurrentAccountState(async currentState => {
   1331      const userData = await currentState.getUserAccountData();
   1332      if (!userData.verified) {
   1333        await currentState.updateUserAccountData({ verified: true });
   1334      }
   1335    });
   1336    await this.notifyObservers(ONVERIFIED_NOTIFICATION);
   1337  },
   1338 
   1339  // _handle* methods used by push, used when the account/device status is
   1340  // changed on a different device.
   1341  async _handleAccountDestroyed(uid) {
   1342    let state = this.currentAccountState;
   1343    const accountData = await state.getUserAccountData();
   1344    const localUid = accountData ? accountData.uid : null;
   1345    if (!localUid) {
   1346      log.info(
   1347        `Account destroyed push notification received, but we're already logged-out`
   1348      );
   1349      return null;
   1350    }
   1351    if (uid == localUid) {
   1352      const data = JSON.stringify({ isLocalDevice: true });
   1353      await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
   1354      return this.signOut(true);
   1355    }
   1356    log.info(
   1357      `The destroyed account uid doesn't match with the local uid. ` +
   1358        `Local: ${localUid}, account uid destroyed: ${uid}`
   1359    );
   1360    return null;
   1361  },
   1362 
   1363  async _handleDeviceDisconnection(deviceId) {
   1364    let state = this.currentAccountState;
   1365    const accountData = await state.getUserAccountData();
   1366    if (!accountData || !accountData.device) {
   1367      // Nothing we can do here.
   1368      return;
   1369    }
   1370    const localDeviceId = accountData.device.id;
   1371    const isLocalDevice = deviceId == localDeviceId;
   1372    if (isLocalDevice) {
   1373      this.signOut(true);
   1374    }
   1375    const data = JSON.stringify({ isLocalDevice });
   1376    await this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, data);
   1377  },
   1378 
   1379  async _handleEmailUpdated(newEmail) {
   1380    await this.currentAccountState.updateUserAccountData({ email: newEmail });
   1381  },
   1382 
   1383  /*
   1384   * Coerce an error into one of the general error cases:
   1385   *          NETWORK_ERROR
   1386   *          AUTH_ERROR
   1387   *          UNKNOWN_ERROR
   1388   *
   1389   * These errors will pass through:
   1390   *          INVALID_PARAMETER
   1391   *          NO_ACCOUNT
   1392   *          UNVERIFIED_ACCOUNT
   1393   */
   1394  _errorToErrorClass(aError) {
   1395    if (aError.errno) {
   1396      let error = SERVER_ERRNO_TO_ERROR[aError.errno];
   1397      return this._error(
   1398        ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN,
   1399        aError
   1400      );
   1401    } else if (
   1402      aError.message &&
   1403      (aError.message === "INVALID_PARAMETER" ||
   1404        aError.message === "NO_ACCOUNT" ||
   1405        aError.message === "UNVERIFIED_ACCOUNT" ||
   1406        aError.message === "AUTH_ERROR")
   1407    ) {
   1408      return aError;
   1409    }
   1410    return this._error(ERROR_UNKNOWN, aError);
   1411  },
   1412 
   1413  _error(aError, aDetails) {
   1414    log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {
   1415      aError,
   1416      aDetails,
   1417    });
   1418    let reason = new Error(aError);
   1419    if (aDetails) {
   1420      reason.details = aDetails;
   1421    }
   1422    return reason;
   1423  },
   1424 
   1425  // Attempt to update the auth server with whatever device details are stored
   1426  // in the account data. Returns a promise that always resolves, never rejects.
   1427  // If the promise resolves to a value, that value is the device id.
   1428  updateDeviceRegistration() {
   1429    return this.device.updateDeviceRegistration();
   1430  },
   1431 
   1432  /**
   1433   * Delete all the persisted credentials we store for FxA. After calling
   1434   * this, the user will be forced to re-authenticate to continue.
   1435   *
   1436   * @return Promise resolves when the user data has been persisted
   1437   */
   1438  dropCredentials(state) {
   1439    // Delete all fields except those required for the user to
   1440    // reauthenticate.
   1441    let updateData = {};
   1442    let clearField = field => {
   1443      if (!FXA_PWDMGR_REAUTH_ALLOWLIST.has(field)) {
   1444        updateData[field] = null;
   1445      }
   1446    };
   1447    FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField);
   1448    FXA_PWDMGR_SECURE_FIELDS.forEach(clearField);
   1449 
   1450    return state.updateUserAccountData(updateData);
   1451  },
   1452 
   1453  async checkAccountStatus(state) {
   1454    log.info("checking account status...");
   1455    let data = await state.getUserAccountData(["uid", "sessionToken"]);
   1456    if (!data) {
   1457      log.info("account status: no user");
   1458      return false;
   1459    }
   1460    // If we have a session token, then check if that remains valid - if this
   1461    // works we know the account must also be OK.
   1462    if (data.sessionToken) {
   1463      if (await this.fxAccountsClient.sessionStatus(data.sessionToken)) {
   1464        log.info("account status: ok");
   1465        return true;
   1466      }
   1467    }
   1468    let exists = await this.fxAccountsClient.accountStatus(data.uid);
   1469    if (!exists) {
   1470      // Delete all local account data. Since the account no longer
   1471      // exists, we can skip the remote calls.
   1472      log.info("account status: deleted");
   1473      await this._handleAccountDestroyed(data.uid);
   1474    } else {
   1475      // Note that we may already have been in a "needs reauth" state (ie, if
   1476      // this function was called when we already had no session token), but
   1477      // that's OK - re-notifying etc should cause no harm.
   1478      log.info("account status: needs reauthentication");
   1479      await this.dropCredentials(this.currentAccountState);
   1480      // Notify the account state has changed so the UI updates.
   1481      await this.notifyObservers(ON_ACCOUNT_STATE_CHANGE_NOTIFICATION);
   1482    }
   1483    return false;
   1484  },
   1485 
   1486  async _handleTokenError(err) {
   1487    if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) {
   1488      throw err;
   1489    }
   1490    log.warn("handling invalid token error", err);
   1491    // Note that we don't use `withCurrentAccountState` here as that will cause
   1492    // an error to be thrown if we sign out due to the account not existing.
   1493    let state = this.currentAccountState;
   1494    let ok = await this.checkAccountStatus(state);
   1495    if (ok) {
   1496      log.warn("invalid token error, but account state appears ok?");
   1497    }
   1498    // always re-throw the error.
   1499    throw err;
   1500  },
   1501 };
   1502 
   1503 let fxAccountsSingleton = null;
   1504 
   1505 export function getFxAccountsSingleton() {
   1506  if (fxAccountsSingleton) {
   1507    return fxAccountsSingleton;
   1508  }
   1509 
   1510  fxAccountsSingleton = new FxAccounts();
   1511  return fxAccountsSingleton;
   1512 }