[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.9.0esr-13.5-1] 17 commits: fixup! Lox integration

Pier Angelo Vendrame (@pierov) git at gitlab.torproject.org
Tue Apr 9 16:49:28 UTC 2024



Pier Angelo Vendrame pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser


Commits:
a2c8eab3 by Henry Wilkes at 2024-04-09T13:35:51+01:00
fixup! Lox integration

Bug 42489: Add a logger for the Lox module.

- - - - -
21115918 by Henry Wilkes at 2024-04-09T13:36:00+01:00
fixup! Lox integration

Bug 42489: Change "loxid" to "loxId".

This follows the camel case convention.

- - - - -
64fd04bf by Henry Wilkes at 2024-04-09T13:36:01+01:00
fixup! Lox integration

Bug 42489: Require a loxId argument for most methods.

This ensures that a method only works on the credentials of the
*expected* loxId, rather than the newest `TorSettings.bridges.lox_id`
value which may change during a session.

We also add `#activeLoxId` to stay in sync with
`TorSettings.brigdes.lox_id`. We merge `clearInvites` into its updater.

- - - - -
2411aa86 by Henry Wilkes at 2024-04-09T13:36:01+01:00
fixup! Lox integration

Bug 42489: Make loading the stored preferences unconditional on
the #credentials value.

Also change the type of `#events` to always be an Array.

- - - - -
2b7376cd by Henry Wilkes at 2024-04-09T13:36:03+01:00
fixup! Lox integration

Bug 42489: Generated loxId is unlikely to clash with existing ones, but
we add some free logic to guarantee this.

- - - - -
4a114a5d by Henry Wilkes at 2024-04-09T13:36:09+01:00
fixup! Lox integration

Bug 42489: Return copies of Lox module internals to ensure they cannot
be edited by a caller.

- - - - -
e7248fe1 by Henry Wilkes at 2024-04-09T13:57:23+01:00
fixup! Lox integration

Bug 42489: Add notifications to the Lox module.

We ensure changes to credentials pass through #changeCredentials to
check whether we should send a notification.

- - - - -
e8846a94 by Henry Wilkes at 2024-04-09T13:57:29+01:00
fixup! Lox integration

Bug 42489: Add #assertInitialized.

- - - - -
b459399f by Henry Wilkes at 2024-04-09T16:17:17+01:00
fixup! Lox integration

Bug 42489: Change LoxError.

Move the LoxErrors types into LoxError. Only set the type if it is
specific.

- - - - -
23921826 by Henry Wilkes at 2024-04-09T16:17:25+01:00
fixup! Lox integration

Bug 42489: Change the Lox Authority URL.

- - - - -
c9db0992 by Henry Wilkes at 2024-04-09T16:17:26+01:00
fixup! Lox integration

Bug 42489: Try and re fetch the pubKey, encTable and constants if they
failed before.

For example, if they fail via a domain front request, we should try
again with `fetch` when we are bootstrapped.

- - - - -
a9522e4e by Henry Wilkes at 2024-04-09T16:17:26+01:00
fixup! Lox integration

Bug 42489: Make sure trust level strings are converted to integers.

- - - - -
63e0f941 by Henry Wilkes at 2024-04-09T16:22:17+01:00
fixup! Lox integration

Bug 42489: Tidy #attemptUpgrade by returning early with a promise.

- - - - -
40ca47e1 by Henry Wilkes at 2024-04-09T16:22:18+01:00
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection

Bug 42489: Listen for notifications from Lox module, and pass in loxId
to methods.

- - - - -
23b9aa5b by Henry Wilkes at 2024-04-09T16:22:29+01:00
fixup! Bug 40597: Implement TorSettings module

Bug 42489: Listen for notifications from Lox module.

Also, do not save the bridge_strings to the preferences if they come
from the Lox module.

In DomainFrontedRequests distinguish between reachability errors,
response errors and other errors in DomainFrontedRequest to improve Lox
error messaging.

- - - - -
46ac1528 by Henry Wilkes at 2024-04-09T16:22:53+01:00
fixup! Bug 40933: Add tor-launcher functionality

Bug 42489: Drop getLocalizedStringForError from TorLauncherUtil.

- - - - -
9abd099f by Henry Wilkes at 2024-04-09T16:22:53+01:00
fixup! Add TorStrings module for localization

Bug 42489: Drop nserror strings.

- - - - -


8 changed files:

- browser/components/torpreferences/content/connectionPane.js
- browser/components/torpreferences/content/loxInviteDialog.js
- browser/components/torpreferences/content/provideBridgeDialog.js
- toolkit/components/lox/Lox.sys.mjs
- toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
- toolkit/modules/DomainFrontedRequests.sys.mjs
- toolkit/modules/TorSettings.sys.mjs
- toolkit/torbutton/chrome/locale/en-US/torlauncher.properties


Changes:

=====================================
browser/components/torpreferences/content/connectionPane.js
=====================================
@@ -36,7 +36,7 @@ const { TorStrings } = ChromeUtils.importESModule(
   "resource://gre/modules/TorStrings.sys.mjs"
 );
 
-const { Lox } = ChromeUtils.importESModule(
+const { Lox, LoxTopics } = ChromeUtils.importESModule(
   "resource://gre/modules/Lox.sys.mjs"
 );
 
@@ -1319,27 +1319,18 @@ const gLoxStatus = {
     this._invitesButton.addEventListener("click", () => {
       gSubDialog.open(
         "chrome://browser/content/torpreferences/loxInviteDialog.xhtml",
-        {
-          features: "resizable=yes",
-          closedCallback: () => {
-            // TODO: Listen for events from Lox, rather than call _updateInvites
-            // directly.
-            this._updateInvites();
-          },
-        }
+        { features: "resizable=yes" }
       );
     });
     this._unlockAlertButton.addEventListener("click", () => {
-      // TODO: Have a way to ensure that the cleared event data matches the
-      // current _loxId
-      Lox.clearEventData();
-      // TODO: Listen for events from Lox, rather than call _updateUnlocks
-      // directly.
-      this._updateUnlocks();
+      Lox.clearEventData(this._loxId);
     });
 
     Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
-    // TODO: Listen for new events from Lox, when it is supported.
+    Services.obs.addObserver(this, LoxTopics.UpdateEvents);
+    Services.obs.addObserver(this, LoxTopics.UpdateNextUnlock);
+    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
+    Services.obs.addObserver(this, LoxTopics.NewInvite);
 
     // NOTE: Before initializedPromise completes, this area is hidden.
     TorSettings.initializedPromise.then(() => {
@@ -1352,6 +1343,10 @@ const gLoxStatus = {
    */
   uninit() {
     Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
+    Services.obs.removeObserver(this, LoxTopics.UpdateEvents);
+    Services.obs.removeObserver(this, LoxTopics.UpdateNextUnlock);
+    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
+    Services.obs.removeObserver(this, LoxTopics.NewInvite);
   },
 
   observe(subject, topic, data) {
@@ -1365,6 +1360,18 @@ const gLoxStatus = {
           this._updateLoxId();
         }
         break;
+      case LoxTopics.UpdateNextUnlock:
+        this._updateNextUnlock();
+        break;
+      case LoxTopics.UpdateEvents:
+        this._updatePendingEvents();
+        break;
+      case LoxTopics.UpdateRemainingInvites:
+        this._updateRemainingInvites();
+        break;
+      case LoxTopics.NewInvite:
+        this._updateHaveExistingInvites();
+        break;
     }
   },
 
@@ -1384,43 +1391,126 @@ const gLoxStatus = {
       TorSettings.bridges.source === TorBridgeSource.Lox
         ? TorSettings.bridges.lox_id
         : "";
-    if (loxId !== this._loxId) {
-      this._loxId = loxId;
-      this._updateUnlocks();
-      this._updateInvites();
+    if (loxId === this._loxId) {
+      return;
     }
+    this._loxId = loxId;
+    // We unset _nextUnlock to ensure the areas no longer use the old value for
+    // the new loxId.
+    this._updateNextUnlock(true);
+    this._updateRemainingInvites();
+    this._updateHaveExistingInvites();
+    this._updatePendingEvents();
   },
 
   /**
-   * Update the display of the current or next unlock.
+   * The remaining invites shown, or null if uninitialized or no loxId.
+   *
+   * @type {integer?}
    */
-  async _updateUnlocks() {
-    // Cache the loxId before we await.
-    const loxId = this._loxId;
-
-    if (!loxId) {
-      // NOTE: This area should already be hidden by the change in Lox source,
-      // but we clean up for the next non-empty id.
-      this._area.classList.remove("show-unlock-alert");
-      this._area.classList.remove("show-next-unlock");
+  _remainingInvites: null,
+  /**
+   * Update the shown value.
+   */
+  _updateRemainingInvites() {
+    const numInvites = this._loxId
+      ? Lox.getRemainingInviteCount(this._loxId)
+      : null;
+    if (numInvites === this._remainingInvites) {
       return;
     }
-
-    let pendingEvents;
-    let nextUnlock;
-    let numInvites;
-    // Fetch the latest events or details about the next unlock.
-    try {
-      nextUnlock = await Lox.getNextUnlock();
-      pendingEvents = Lox.getEventData();
-      numInvites = Lox.getRemainingInviteCount();
-    } catch (e) {
-      console.error("Failed get get lox updates", e);
+    this._remainingInvites = numInvites;
+    this._updateUnlockArea();
+    this._updateInvitesArea();
+  },
+  /**
+   * Whether we have existing invites, or null if uninitialized or no loxId.
+   *
+   * @type {boolean?}
+   */
+  _haveExistingInvites: null,
+  /**
+   * Update the shown value.
+   */
+  _updateHaveExistingInvites() {
+    const haveInvites = this._loxId ? !!Lox.getInvites().length : null;
+    if (haveInvites === this._haveExistingInvites) {
+      return;
+    }
+    this._haveExistingInvites = haveInvites;
+    this._updateInvitesArea();
+  },
+  /**
+   * Details about the next unlock, or null if uninitialized or no loxId.
+   *
+   * @type {UnlockData?}
+   */
+  _nextUnlock: null,
+  /**
+   * Tracker id to ensure that the results from later calls to _updateNextUnlock
+   * take priority over earlier calls.
+   *
+   * @type {integer}
+   */
+  _nextUnlockCallId: 0,
+  /**
+   * Update the shown value asynchronously.
+   *
+   * @param {boolean} [unset=false] - Whether to set the _nextUnlock value to
+   *   null before waiting for the new value. I.e. ensure that the current value
+   *   will not be used.
+   */
+  async _updateNextUnlock(unset = false) {
+    // NOTE: We do not expect the integer to exceed the maximum integer.
+    this._nextUnlockCallId++;
+    const callId = this._nextUnlockCallId;
+    if (unset) {
+      this._nextUnlock = null;
+    }
+    const nextUnlock = this._loxId
+      ? await Lox.getNextUnlock(this._loxId)
+      : null;
+    if (callId !== this._nextUnlockCallId) {
+      // Replaced by another update.
+      // E.g. if the _loxId changed. Or if getNextUnlock triggered
+      // LoxTopics.UpdateNextUnlock.
       return;
     }
+    // Should be safe to trigger the update, even when the value hasn't changed.
+    this._nextUnlock = nextUnlock;
+    this._updateUnlockArea();
+  },
+  /**
+   * The list of events the user has not yet cleared, or null if uninitialized
+   * or no loxId.
+   *
+   * @type {EventData[]?}
+   */
+  _pendingEvents: null,
+  /**
+   * Update the shown value.
+   */
+  _updatePendingEvents() {
+    // Should be safe to trigger the update, even when the value hasn't changed.
+    this._pendingEvents = this._loxId ? Lox.getEventData(this._loxId) : null;
+    this._updateUnlockArea();
+  },
 
-    if (loxId !== this._loxId) {
-      // Replaced during await.
+  /**
+   * Update the display of the current or next unlock.
+   */
+  _updateUnlockArea() {
+    if (
+      !this._loxId ||
+      this._pendingEvents === null ||
+      this._remainingInvites === null ||
+      this._nextUnlock === null
+    ) {
+      // Uninitialized or no Lox source.
+      // NOTE: This area may already be hidden by the change in Lox source,
+      // but we clean up for the next non-empty id.
+      this._area.classList.remove("show-unlock-alert");
+      this._area.classList.remove("show-next-unlock");
       return;
     }
 
@@ -1428,6 +1518,7 @@ const gLoxStatus = {
     const alertHadFocus = this._unlockAlert.contains(document.activeElement);
     const detailsHadFocus = this._detailsArea.contains(document.activeElement);
 
+    const pendingEvents = this._pendingEvents;
     const showAlert = !!pendingEvents.length;
     this._area.classList.toggle("show-unlock-alert", showAlert);
     this._area.classList.toggle("show-next-unlock", !showAlert);
@@ -1479,7 +1570,7 @@ const gLoxStatus = {
       document.l10n.setAttributes(
         this._unlockAlertInviteItem,
         "tor-bridges-lox-new-invites",
-        { numInvites }
+        { numInvites: this._remainingInvites }
       );
       this._unlockAlert.classList.toggle(
         "lox-unlock-upgrade",
@@ -1494,7 +1585,7 @@ const gLoxStatus = {
       const numDays = Math.max(
         1,
         Math.ceil(
-          (new Date(nextUnlock.date).getTime() - Date.now()) /
+          (new Date(this._nextUnlock.date).getTime() - Date.now()) /
             (24 * 60 * 60 * 1000)
         )
       );
@@ -1505,9 +1596,9 @@ const gLoxStatus = {
       );
 
       // Gain 2 bridges from level 0 to 1. After that gain invites.
-      const bridgeGain = nextUnlock.nextLevel === 1;
-      const firstInvites = nextUnlock.nextLevel === 2;
-      const moreInvites = nextUnlock.nextLevel > 2;
+      const bridgeGain = this._nextUnlock.nextLevel === 1;
+      const firstInvites = this._nextUnlock.nextLevel === 2;
+      const moreInvites = this._nextUnlock.nextLevel > 2;
 
       this._detailsArea.classList.toggle("lox-next-gain-bridges", bridgeGain);
       this._detailsArea.classList.toggle(
@@ -1529,24 +1620,19 @@ const gLoxStatus = {
   /**
    * Update the invites area.
    */
-  _updateInvites() {
-    if (!this._loxId) {
-      return;
-    }
-
-    let remainingInvites;
-    let existingInvites;
-    // Fetch the latest events or details about the next unlock.
-    try {
-      remainingInvites = Lox.getRemainingInviteCount();
-      existingInvites = Lox.getInvites().length;
-    } catch (e) {
-      console.error("Failed get get remaining invites", e);
-      return;
+  _updateInvitesArea() {
+    let hasInvites;
+    if (
+      !this._loxId ||
+      this._remainingInvites === null ||
+      this._haveExistingInvites === null
+    ) {
+      // Not initialized yet.
+      hasInvites = false;
+    } else {
+      hasInvites = this._haveExistingInvites || !!this._remainingInvites;
     }
 
-    const hasInvites = !!existingInvites || !!remainingInvites;
-
     if (!hasInvites) {
       if (
         this._remainingInvitesEl.contains(document.activeElement) ||
@@ -1563,11 +1649,13 @@ const gLoxStatus = {
     // creating new ones.
     this._detailsArea.classList.toggle("lox-has-invites", hasInvites);
 
-    document.l10n.setAttributes(
-      this._remainingInvitesEl,
-      "tor-bridges-lox-remaining-invites",
-      { numInvites: remainingInvites }
-    );
+    if (hasInvites) {
+      document.l10n.setAttributes(
+        this._remainingInvitesEl,
+        "tor-bridges-lox-remaining-invites",
+        { numInvites: this._remainingInvites }
+      );
+    }
   },
 };
 


=====================================
browser/components/torpreferences/content/loxInviteDialog.js
=====================================
@@ -3,14 +3,14 @@
 const { TorSettings, TorSettingsTopics, TorBridgeSource } =
   ChromeUtils.importESModule("resource://gre/modules/TorSettings.sys.mjs");
 
-const { Lox, LoxErrors } = ChromeUtils.importESModule(
+const { Lox, LoxError, LoxTopics } = ChromeUtils.importESModule(
   "resource://gre/modules/Lox.sys.mjs"
 );
 
 /**
  * Fake Lox module
 
-const LoxErrors = {
+const LoxError = {
   LoxServerUnreachable: "LoxServerUnreachable",
   Other: "Other",
 };
@@ -36,7 +36,7 @@ const Lox = {
           return;
         }
         if (!this.remainingInvites) {
-          rej({ type: LoxErrors.Other });
+          rej({ type: LoxError.Other });
           return;
         }
         const invite = JSON.stringify({
@@ -104,7 +104,8 @@ const gLoxInvites = {
     // NOTE: TorSettings should already be initialized when this dialog is
     // opened.
     Services.obs.addObserver(this, TorSettingsTopics.SettingsChanged);
-    // TODO: Listen for new invites from Lox, when supported.
+    Services.obs.addObserver(this, LoxTopics.UpdateRemainingInvites);
+    Services.obs.addObserver(this, LoxTopics.NewInvite);
 
     // Set initial _loxId value. Can close this dialog.
     this._updateLoxId();
@@ -118,6 +119,8 @@ const gLoxInvites = {
    */
   uninit() {
     Services.obs.removeObserver(this, TorSettingsTopics.SettingsChanged);
+    Services.obs.removeObserver(this, LoxTopics.UpdateRemainingInvites);
+    Services.obs.removeObserver(this, LoxTopics.NewInvite);
   },
 
   observe(subject, topic, data) {
@@ -131,6 +134,12 @@ const gLoxInvites = {
           this._updateLoxId();
         }
         break;
+      case LoxTopics.UpdateRemainingInvites:
+        this._updateRemainingInvites();
+        break;
+      case LoxTopics.NewInvite:
+        this._updateExistingInvites();
+        break;
     }
   },
 
@@ -204,7 +213,7 @@ const gLoxInvites = {
    * Update the display of the remaining invites.
    */
   _updateRemainingInvites() {
-    this._remainingInvites = Lox.getRemainingInviteCount();
+    this._remainingInvites = Lox.getRemainingInviteCount(this._loxId);
 
     document.l10n.setAttributes(
       this._remainingInvitesEl,
@@ -254,7 +263,7 @@ const gLoxInvites = {
     this._connectingEl.focus();
 
     let lostFocus = false;
-    Lox.generateInvite()
+    Lox.generateInvite(this._loxId)
       .finally(() => {
         // Fetch whether the connecting label still has focus before we hide it.
         lostFocus = this._connectingEl.contains(document.activeElement);
@@ -275,15 +284,11 @@ const gLoxInvites = {
             // message.
             this._inviteListEl.focus();
           }
-
-          // TODO: When Lox sends out notifications, let the observer handle the
-          // change rather than calling _updateRemainingInvites directly.
-          this._updateRemainingInvites();
         },
         loxError => {
           console.error("Failed to generate an invite", loxError);
-          switch (loxError.type) {
-            case LoxErrors.LoxServerUnreachable:
+          switch (loxError instanceof LoxError ? loxError.code : null) {
+            case LoxError.LoxServerUnreachable:
               this._updateGenerateError("no-server");
               break;
             default:


=====================================
browser/components/torpreferences/content/provideBridgeDialog.js
=====================================
@@ -15,14 +15,14 @@ const { TorParsers } = ChromeUtils.importESModule(
   "resource://gre/modules/TorParsers.sys.mjs"
 );
 
-const { Lox, LoxErrors } = ChromeUtils.importESModule(
+const { Lox, LoxError } = ChromeUtils.importESModule(
   "resource://gre/modules/Lox.sys.mjs"
 );
 
 /*
  * Fake Lox module:
 
-const LoxErrors = {
+const LoxError = {
   BadInvite: "BadInvite",
   LoxServerUnreachable: "LoxServerUnreachable",
   Other: "Other",
@@ -30,9 +30,9 @@ const LoxErrors = {
 
 const Lox = {
   failError: null,
-  // failError: LoxErrors.BadInvite,
-  // failError: LoxErrors.LoxServerUnreachable,
-  // failError: LoxErrors.Other,
+  // failError: LoxError.BadInvite,
+  // failError: LoxError.LoxServerUnreachable,
+  // failError: LoxError.Other,
   redeemInvite(invite) {
     return new Promise((res, rej) => {
       setTimeout(() => {
@@ -281,13 +281,13 @@ const gProvideBridgeDialog = {
           },
           loxError => {
             console.error("Redeeming failed", loxError);
-            switch (loxError.type) {
-              case LoxErrors.BadInvite:
+            switch (loxError instanceof LoxError ? loxError.code : null) {
+              case LoxError.BadInvite:
                 // TODO: distinguish between a bad invite, an invite that has
                 // expired, and an invite that has already been redeemed.
                 this.updateError({ type: "bad-invite" });
                 break;
-              case LoxErrors.LoxServerUnreachable:
+              case LoxError.LoxServerUnreachable:
                 this.updateError({ type: "no-server" });
                 break;
               default:


=====================================
toolkit/components/lox/Lox.sys.mjs
=====================================
@@ -5,15 +5,32 @@ import {
 } from "resource://gre/modules/Timer.sys.mjs";
 
 const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => {
+  let { ConsoleAPI } = ChromeUtils.importESModule(
+    "resource://gre/modules/Console.sys.mjs"
+  );
+  return new ConsoleAPI({
+    maxLogLevel: "warn",
+    maxLogLevelPref: "lox.log_level",
+    prefix: "Lox",
+  });
+});
+
 ChromeUtils.defineESModuleGetters(lazy, {
   DomainFrontRequestBuilder:
     "resource://gre/modules/DomainFrontedRequests.sys.mjs",
+  DomainFrontRequestNetworkError:
+    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
+  DomainFrontRequestResponseError:
+    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
   TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
   TorConnectState: "resource://gre/modules/TorConnect.sys.mjs",
   TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
   TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
   TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
 });
+
 XPCOMUtils.defineLazyModuleGetters(lazy, {
   init: "resource://gre/modules/lox_wasm.jsm",
   open_invite: "resource://gre/modules/lox_wasm.jsm",
@@ -37,13 +54,22 @@ XPCOMUtils.defineLazyModuleGetters(lazy, {
   handle_blockage_migration: "resource://gre/modules/lox_wasm.jsm",
 });
 
-export const LoxErrors = Object.freeze({
-  BadInvite: "BadInvite",
-  MissingCredential: "MissingCredential",
-  LoxServerUnreachable: "LoxServerUnreachable",
-  NoInvitations: "NoInvitations",
-  InitError: "InitializationError",
-  NotInitialized: "NotInitialized",
+export const LoxTopics = Object.freeze({
+  // Whenever the bridges *might* have changed.
+  // getBridges only uses #credentials, so this will only fire when it changes.
+  UpdateBridges: "lox:update-bridges",
+  // Whenever we gain a new upgrade or blockage event, or clear events.
+  UpdateEvents: "lox:update-events",
+  // Whenever the next unlock *might* have changed.
+  // getNextUnlock uses #credentials and #constants, sow ill fire when either
+  // value changes.
+  UpdateNextUnlock: "lox:update-next-unlock",
+  // Whenever the remaining invites *might* have changed.
+  // getRemainingInviteCount only uses #credentials, so will only fire when it
+  // changes.
+  UpdateRemainingInvites: "lox:update-remaining-invites",
+  // Whenever we generate a new invite.
+  NewInvite: "lox:new-invite",
 });
 
 const LoxSettingsPrefs = Object.freeze({
@@ -56,10 +82,21 @@ const LoxSettingsPrefs = Object.freeze({
   constants: "lox.settings.constants",
 });
 
-class LoxError extends Error {
-  constructor(type) {
-    super("");
-    this.type = type;
+/**
+ * Error class for Lox.
+ */
+export class LoxError extends Error {
+  static BadInvite = "BadInvite";
+  static LoxServerUnreachable = "LoxServerUnreachable";
+
+  /**
+   * @param {string} message - The error message.
+   * @param {string?} [code] - The specific error type, if any.
+   */
+  constructor(message, code = null) {
+    super(message);
+    this.name = "LoxError";
+    this.code = code;
   }
 }
 
@@ -70,14 +107,65 @@ class LoxImpl {
   #encTablePromise = null;
   #constantsPromise = null;
   #domainFrontedRequests = null;
-  #invites = null;
+  /**
+   * The list of invites generated.
+   *
+   * @type {string[]}
+   */
+  #invites = [];
   #pubKeys = null;
   #encTable = null;
   #constants = null;
-  #credentials = null;
+  /**
+   * The latest credentials for a given lox id.
+   *
+   * @type {Object<string, string>}
+   */
+  #credentials = {};
+  /**
+   * The list of accumulated blockage or upgrade events.
+   *
+   * This can be cleared when the user acknowledges the events.
+   *
+   * @type {EventData[]}
+   */
   #events = [];
   #backgroundInterval = null;
 
+  /**
+   * The lox ID that is currently active.
+   *
+   * Stays in sync with TorSettings.bridges.lox_id. null when uninitialized.
+   *
+   * @type {string?}
+   */
+  #activeLoxId = null;
+
+  /**
+   * Update the active lox id.
+   */
+  #updateActiveLoxId() {
+    const loxId = lazy.TorSettings.bridges.lox_id;
+    if (loxId === this.#activeLoxId) {
+      return;
+    }
+    lazy.logger.debug(
+      `#activeLoxId switching from "${this.#activeLoxId}" to "${loxId}"`
+    );
+    if (this.#activeLoxId !== null) {
+      lazy.logger.debug(
+        `Clearing event data and invites for "${this.#activeLoxId}"`
+      );
+      // If not initializing clear the metadata for the old lox ID when it
+      // changes.
+      this.clearEventData(this.#activeLoxId);
+      // TODO: Do we want to keep invites? See tor-browser#42453
+      this.#invites = [];
+      this.#store();
+    }
+    this.#activeLoxId = loxId;
+  }
+
   observe(subject, topic, data) {
     switch (topic) {
       case lazy.TorSettingsTopics.SettingsChanged:
@@ -87,11 +175,8 @@ class LoxImpl {
           changes.includes("bridges.source") ||
           changes.includes("bridges.lox_id")
         ) {
-          // if lox_id has changed, clear event and invite queues
-          if (changes.includes("bridges.lox_id")) {
-            this.clearEventData();
-            this.clearInvites();
-          }
+          // The lox_id may have changed.
+          this.#updateActiveLoxId();
 
           // Only run background tasks if Lox is enabled
           if (this.#inuse) {
@@ -108,6 +193,8 @@ class LoxImpl {
         }
         break;
       case lazy.TorSettingsTopics.Ready:
+        // Set the initial #activeLoxId.
+        this.#updateActiveLoxId();
         // Run background tasks every 12 hours if Lox is enabled
         if (this.#inuse) {
           this.#backgroundInterval = setInterval(
@@ -119,36 +206,86 @@ class LoxImpl {
     }
   }
 
+  /**
+   * Assert that the module is initialized.
+   */
+  #assertInitialized() {
+    if (!this.#initialized) {
+      throw new LoxError("Not initialized");
+    }
+  }
+
   get #inuse() {
     return (
+      Boolean(this.#activeLoxId) &&
       lazy.TorSettings.bridges.enabled === true &&
-      lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox &&
-      lazy.TorSettings.bridges.lox_id
+      lazy.TorSettings.bridges.source === lazy.TorBridgeSource.Lox
     );
   }
 
+  /**
+   * Change some existing credentials for an ID to a new value.
+   *
+   * @param {string} loxId - The ID to change the credentials for.
+   * @param {string} newCredentials - The new credentials to set.
+   */
+  #changeCredentials(loxId, newCredentials) {
+    // FIXME: Several async methods want to update the credentials, but they
+    // might race and conflict with each. tor-browser#42492
+    if (!newCredentials) {
+      // Avoid overwriting and losing our current credentials.
+      throw new LoxError(`Empty credentials being set for ${loxId}`);
+    }
+    if (!this.#credentials[loxId]) {
+      // Unexpected, but we still want to save the value to storage.
+      lazy.logger.warn(`Lox ID ${loxId} is missing existing credentials`);
+    }
+
+    this.#credentials[loxId] = newCredentials;
+    this.#store();
+
+    // NOTE: In principle we could determine within this module whether the
+    // bridges, remaining invites, or next unlock changes in value when
+    // switching credentials.
+    // However, this logic can be done by the topic observers, as needed. In
+    // particular, TorSettings.bridges.bridge_strings has its own logic
+    // determining whether its value has changed.
+
+    // Let TorSettings know about possibly new bridges.
+    Services.obs.notifyObservers(null, LoxTopics.UpdateBridges);
+    // Let UI know about changes.
+    Services.obs.notifyObservers(null, LoxTopics.UpdateRemainingInvites);
+    Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
+  }
+
+  /**
+   * Fetch the latest credentials.
+   *
+   * @param {string} loxId - The ID to get the credentials for.
+   *
+   * @returns {string} - The credentials.
+   */
+  #getCredentials(loxId) {
+    const cred = loxId ? this.#credentials[loxId] : undefined;
+    if (!cred) {
+      throw new LoxError(`No credentials for ${loxId}`);
+    }
+    return cred;
+  }
+
   /**
    * Formats and returns bridges from the stored Lox credential.
    *
-   * @param {string} loxid The id string associated with a lox credential.
+   * @param {string} loxId The id string associated with a lox credential.
    *
    * @returns {string[]} An array of formatted bridge lines. The array is empty
    *   if there are no bridges.
    */
-  getBridges(loxid) {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    if (loxid === null) {
-      return [];
-    }
-    if (!this.#credentials[loxid]) {
-      // This lox id doesn't correspond to a stored credential
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  getBridges(loxId) {
+    this.#assertInitialized();
     // Note: this is messy now but can be mostly removed after we have
     // https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/46
-    let bridgelines = JSON.parse(this.#credentials[loxid]).bridgelines;
+    let bridgelines = JSON.parse(this.#getCredentials(loxId)).bridgelines;
     let bridges = [];
     for (const bridge of bridgelines) {
       let addr = bridge.addr;
@@ -219,18 +356,12 @@ class LoxImpl {
   }
 
   #load() {
-    if (this.#credentials === null) {
-      let cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, "");
-      this.#credentials = cred !== "" ? JSON.parse(cred) : {};
-      let invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, "");
-      if (invites !== "") {
-        this.#invites = JSON.parse(invites);
-      }
-      let events = Services.prefs.getStringPref(LoxSettingsPrefs.events, "");
-      if (events !== "") {
-        this.#events = JSON.parse(events);
-      }
-    }
+    const cred = Services.prefs.getStringPref(LoxSettingsPrefs.credentials, "");
+    this.#credentials = cred ? JSON.parse(cred) : {};
+    const invites = Services.prefs.getStringPref(LoxSettingsPrefs.invites, "");
+    this.#invites = invites ? JSON.parse(invites) : [];
+    const events = Services.prefs.getStringPref(LoxSettingsPrefs.events, "");
+    this.#events = events ? JSON.parse(events) : [];
     this.#pubKeys = Services.prefs.getStringPref(
       LoxSettingsPrefs.pubkeys,
       null
@@ -246,16 +377,21 @@ class LoxImpl {
   }
 
   async #getPubKeys() {
+    // FIXME: We are always refetching #pubKeys, #encTable and #constants once
+    // per session, but they may change more frequently. tor-browser#42502
     if (this.#pubKeyPromise === null) {
       this.#pubKeyPromise = this.#makeRequest("pubkeys", [])
         .then(pubKeys => {
           this.#pubKeys = JSON.stringify(pubKeys);
           this.#store();
         })
-        .catch(() => {
+        .catch(error => {
+          lazy.logger.debug("Failed to get pubkeys", error);
+          // Make the next call try again.
+          this.#pubKeyPromise = null;
           // We always try to update, but if that doesn't work fall back to stored data
           if (!this.#pubKeys) {
-            throw new LoxError(LoxErrors.LoxServerUnreachable);
+            throw error;
           }
         });
     }
@@ -269,10 +405,13 @@ class LoxImpl {
           this.#encTable = JSON.stringify(encTable);
           this.#store();
         })
-        .catch(() => {
+        .catch(error => {
+          lazy.logger.debug("Failed to get encTable", error);
+          // Make the next call try again.
+          this.#encTablePromise = null;
           // Try to update first, but if that doesn't work fall back to stored data
           if (!this.#encTable) {
-            throw new LoxError(LoxErrors.LoxServerUnreachable);
+            throw error;
           }
         });
     }
@@ -284,55 +423,96 @@ class LoxImpl {
       // Try to update first, but if that doesn't work fall back to stored data
       this.#constantsPromise = this.#makeRequest("constants", [])
         .then(constants => {
+          const prevValue = this.#constants;
           this.#constants = JSON.stringify(constants);
           this.#store();
+          if (prevValue !== this.#constants) {
+            Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
+          }
         })
-        .catch(() => {
+        .catch(error => {
+          lazy.logger.debug("Failed to get constants", error);
+          // Make the next call try again.
+          this.#constantsPromise = null;
           if (!this.#constants) {
-            throw new LoxError(LoxErrors.LoxServerUnreachable);
+            throw error;
           }
         });
     }
     await this.#constantsPromise;
   }
 
+  /**
+   * Parse a decimal string to a non-negative integer.
+   *
+   * @param {string} str - The string to parse.
+   * @returns {integer} - The integer.
+   */
+  static #parseNonNegativeInteger(str) {
+    if (typeof str !== "string" || !/^[0-9]+$/.test(str)) {
+      throw new LoxError(`Expected a non-negative decimal integer: "${str}"`);
+    }
+    return parseInt(str, 10);
+  }
+
+  /**
+   * Get the current lox trust level.
+   *
+   * @param {string} loxId - The ID to fetch the level for.
+   * @returns {integer} - The trust level.
+   */
+  #getLevel(loxId) {
+    return LoxImpl.#parseNonNegativeInteger(
+      lazy.get_trust_level(this.#getCredentials(loxId))
+    );
+  }
+
   /**
    * Check for blockages and attempt to perform a levelup
    *
    * If either blockages or a levelup happened, add an event to the event queue
    */
   async #backgroundTasks() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
+    this.#assertInitialized();
+    let addedEvent = false;
+    // Only run background tasks for the active lox ID.
+    const loxId = this.#activeLoxId;
+    if (!loxId) {
+      lazy.logger.warn("No loxId for the background task");
+      return;
     }
-    const loxid = lazy.TorSettings.bridges.lox_id;
     try {
-      const levelup = await this.#attemptUpgrade(loxid);
+      const levelup = await this.#attemptUpgrade(loxId);
       if (levelup) {
-        const level = lazy.get_trust_level(this.#credentials[loxid]);
+        const level = this.#getLevel(loxId);
         const newEvent = {
           type: "levelup",
           newlevel: level,
         };
         this.#events.push(newEvent);
         this.#store();
+        addedEvent = true;
       }
     } catch (err) {
-      console.log(err);
+      lazy.logger.error(err);
     }
     try {
-      const leveldown = await this.#blockageMigration(loxid);
+      const leveldown = await this.#blockageMigration(loxId);
       if (leveldown) {
-        let level = lazy.get_trust_level(this.#credentials[loxid]);
+        let level = this.#getLevel(loxId);
         const newEvent = {
           type: "blockage",
           newlevel: level,
         };
         this.#events.push(newEvent);
         this.#store();
+        addedEvent = true;
       }
     } catch (err) {
-      console.log(err);
+      lazy.logger.error(err);
+    }
+    if (addedEvent) {
+      Services.obs.notifyObservers(null, LoxTopics.UpdateEvents);
     }
   }
 
@@ -356,10 +536,8 @@ class LoxImpl {
     await lazy.init(this.#window);
     lazy.set_panic_hook();
     if (typeof lazy.open_invite !== "function") {
-      throw new LoxError(LoxErrors.InitError);
+      throw new LoxError("Initialization failed");
     }
-    this.#invites = [];
-    this.#events = [];
     this.#load();
     this.#initialized = true;
   }
@@ -376,14 +554,14 @@ class LoxImpl {
     }
     this.#initialized = false;
     this.#window = null;
-    this.#invites = null;
+    this.#invites = [];
     this.#pubKeys = null;
     this.#encTable = null;
     this.#constants = null;
     this.#pubKeyPromise = null;
     this.#encTablePromise = null;
     this.#constantsPromise = null;
-    this.#credentials = null;
+    this.#credentials = {};
     this.#events = [];
     if (this.#backgroundInterval) {
       clearInterval(this.#backgroundInterval);
@@ -398,13 +576,11 @@ class LoxImpl {
    * @returns {bool} Whether the value passed in was a Lox invitation.
    */
   validateInvitation(invite) {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
+    this.#assertInitialized();
     try {
       lazy.invitation_is_trusted(invite);
     } catch (err) {
-      console.log(err);
+      lazy.logger.error(err);
       return false;
     }
     return true;
@@ -413,11 +589,9 @@ class LoxImpl {
   // Note: This is only here for testing purposes. We're going to be using telegram
   // to issue open invitations for Lox bridges.
   async requestOpenInvite() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
+    this.#assertInitialized();
     let invite = await this.#makeRequest("invite", []);
-    console.log(invite);
+    lazy.logger.debug(invite);
     return invite;
   }
 
@@ -425,36 +599,37 @@ class LoxImpl {
    * Redeems a Lox invitation to obtain a credential and bridges.
    *
    * @param {string} invite A Lox invitation.
-   * @returns {string} The loxid of the associated credential on success.
+   * @returns {string} The loxId of the associated credential on success.
    */
   async redeemInvite(invite) {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
+    this.#assertInitialized();
     await this.#getPubKeys();
     let request = await lazy.open_invite(JSON.parse(invite).invite);
-    let id = this.#genLoxId();
-    let response;
-    try {
-      response = await this.#makeRequest(
-        "openreq",
-        JSON.parse(request).request
-      );
-    } catch {
-      throw new LoxError(LoxErrors.LoxServerUnreachable);
-    }
-    console.log("openreq response: ", response);
+    let response = await this.#makeRequest(
+      "openreq",
+      JSON.parse(request).request
+    );
+    lazy.logger.debug("openreq response: ", response);
     if (response.hasOwnProperty("error")) {
-      throw new LoxError(LoxErrors.BadInvite);
+      throw new LoxError(
+        `Error response to "openreq": ${response.error}`,
+        LoxError.BadInvite
+      );
     }
     let cred = lazy.handle_new_lox_credential(
       request,
       JSON.stringify(response),
       this.#pubKeys
     );
-    this.#credentials[id] = cred;
+    // Generate an id that is not already in the #credentials map.
+    let loxId;
+    do {
+      loxId = this.#genLoxId();
+    } while (Object.hasOwn(this.#credentials, loxId));
+    // Set new credentials.
+    this.#credentials[loxId] = cred;
     this.#store();
-    return id;
+    return loxId;
   }
 
   /**
@@ -463,10 +638,9 @@ class LoxImpl {
    * @returns {string[]} A list of all historical invites.
    */
   getInvites() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    return this.#invites;
+    this.#assertInitialized();
+    // Return a copy.
+    return structuredClone(this.#invites);
   }
 
   /**
@@ -477,212 +651,191 @@ class LoxImpl {
    *  - there is no saved Lox credential, or
    *  - the saved credential does not have any invitations available.
    *
+   * @param {string} loxId - The ID to generate an invite for.
    * @returns {string} A valid Lox invitation.
    */
-  async generateInvite() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    const loxid = lazy.TorSettings.bridges.lox_id;
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  async generateInvite(loxId) {
+    this.#assertInitialized();
     await this.#getPubKeys();
     await this.#getEncTable();
-    let level = lazy.get_trust_level(this.#credentials[loxid]);
+    let level = this.#getLevel(loxId);
     if (level < 1) {
-      throw new LoxError(LoxErrors.NoInvitations);
+      throw new LoxError(`Cannot generate invites at level ${level}`);
     }
     let request = lazy.issue_invite(
-      JSON.stringify(this.#credentials[loxid]),
+      JSON.stringify(this.#getCredentials(loxId)),
       this.#encTable,
       this.#pubKeys
     );
-    let response;
-    try {
-      response = await this.#makeRequest(
-        "issueinvite",
-        JSON.parse(request).request
-      );
-    } catch {
-      throw new LoxError(LoxErrors.LoxServerUnreachable);
-    }
+    let response = await this.#makeRequest(
+      "issueinvite",
+      JSON.parse(request).request
+    );
     if (response.hasOwnProperty("error")) {
-      console.log(response.error);
-      throw new LoxError(LoxErrors.NoInvitations);
+      lazy.logger.error(response.error);
+      throw new LoxError(`Error response to "issueinvite": ${response.error}`);
     } else {
-      this.#credentials[loxid] = response;
       const invite = lazy.prepare_invite(response);
       this.#invites.push(invite);
       // cap length of stored invites
       if (this.#invites.len > 50) {
         this.#invites.shift();
       }
-      return invite;
+      this.#store();
+      this.#changeCredentials(loxId, response);
+      Services.obs.notifyObservers(null, LoxTopics.NewInvite);
+      // Return a copy.
+      // Right now invite is just a string, but that might change in the future.
+      return structuredClone(invite);
     }
   }
 
   /**
    * Get the number of invites that a user has remaining.
    *
+   * @param {string} loxId - The ID to check.
    * @returns {int} The number of invites that can still be generated by a
    *   user's credential.
    */
-  getRemainingInviteCount() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    const loxid = lazy.TorSettings.bridges.lox_id;
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
-    return parseInt(lazy.get_invites_remaining(this.#credentials[loxid]));
+  getRemainingInviteCount(loxId) {
+    this.#assertInitialized();
+    return LoxImpl.#parseNonNegativeInteger(
+      lazy.get_invites_remaining(this.#getCredentials(loxId))
+    );
   }
 
-  async #blockageMigration(loxid) {
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  async #blockageMigration(loxId) {
     await this.#getPubKeys();
     let request;
     try {
-      request = lazy.check_blockage(this.#credentials[loxid], this.#pubKeys);
+      request = lazy.check_blockage(this.#getCredentials(loxId), this.#pubKeys);
     } catch {
-      console.log("Not ready for blockage migration");
+      lazy.logger.log("Not ready for blockage migration");
       return false;
     }
     let response = await this.#makeRequest("checkblockage", request);
     if (response.hasOwnProperty("error")) {
-      console.log(response.error);
-      throw new LoxError(LoxErrors.LoxServerUnreachable);
+      lazy.logger.error(response.error);
+      throw new LoxError(
+        `Error response to "checkblockage": ${response.error}`
+      );
     }
     const migrationCred = lazy.handle_check_blockage(
-      this.#credentials[loxid],
+      this.#getCredentials(loxId),
       JSON.stringify(response)
     );
     request = lazy.blockage_migration(
-      this.#credentials[loxid],
+      this.#getCredentials(loxId),
       migrationCred,
       this.#pubKeys
     );
     response = await this.#makeRequest("blockagemigration", request);
     if (response.hasOwnProperty("error")) {
-      console.log(response.error);
-      throw new LoxError(LoxErrors.LoxServerUnreachable);
+      lazy.logger.error(response.error);
+      throw new LoxError(
+        `Error response to "blockagemigration": ${response.error}`
+      );
     }
     const cred = lazy.handle_blockage_migration(
-      this.#credentials[loxid],
+      this.#getCredentials(loxId),
       JSON.stringify(response),
       this.#pubKeys
     );
-    this.#credentials[loxid] = cred;
-    this.#store();
+    this.#changeCredentials(loxId, cred);
     return true;
   }
 
   /** Attempts to upgrade the currently saved Lox credential.
    *  If an upgrade is available, save an event in the event list.
    *
-   *  @returns {boolean} whether a levelup event occured
+   *  @returns {boolean} Whether a levelup event occurred.
    */
-  async #attemptUpgrade(loxid) {
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  async #attemptUpgrade(loxId) {
     await this.#getPubKeys();
     await this.#getEncTable();
     await this.#getConstants();
-    let success = false;
-    let level = lazy.get_trust_level(this.#credentials[loxid]);
+    let level = this.#getLevel(loxId);
     if (level < 1) {
       // attempt trust promotion instead
-      try {
-        success = await this.#trustMigration();
-      } catch (err) {
-        console.log(err);
-        return false;
-      }
-    } else {
-      let request = lazy.level_up(
-        this.#credentials[loxid],
-        this.#encTable,
-        this.#pubKeys
-      );
-      const response = await this.#makeRequest("levelup", request);
-      if (response.hasOwnProperty("error")) {
-        console.log(response.error);
-        throw new LoxError(LoxErrors.LoxServerUnreachable);
-      }
-      const cred = lazy.handle_level_up(
-        request,
-        JSON.stringify(response),
-        this.#pubKeys
-      );
-      this.#credentials[loxid] = cred;
-      return true;
+      return this.#trustMigration(loxId);
+    }
+    let request = lazy.level_up(
+      this.#getCredentials(loxId),
+      this.#encTable,
+      this.#pubKeys
+    );
+    const response = await this.#makeRequest("levelup", request);
+    if (response.hasOwnProperty("error")) {
+      lazy.logger.error(response.error);
+      throw new LoxError(`Error response to "levelup": ${response.error}`);
     }
-    return success;
+    const cred = lazy.handle_level_up(
+      request,
+      JSON.stringify(response),
+      this.#pubKeys
+    );
+    this.#changeCredentials(loxId, cred);
+    return true;
   }
 
   /**
    * Attempt to migrate from an untrusted to a trusted Lox credential
    *
-   * @returns {Promise<bool>} A bool value indicated whether the credential
-   *    was successfully migrated.
+   * @param {string} loxId - The ID to use.
+   * @returns {boolean} Whether the credential was successfully migrated.
    */
-  async #trustMigration() {
-    const loxid = lazy.TorSettings.bridges.lox_id;
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  async #trustMigration(loxId) {
     await this.#getPubKeys();
     return new Promise((resolve, reject) => {
       let request = "";
       try {
-        request = lazy.trust_promotion(this.#credentials[loxid], this.#pubKeys);
+        request = lazy.trust_promotion(
+          this.#getCredentials(loxId),
+          this.#pubKeys
+        );
       } catch (err) {
-        console.log("Not ready to upgrade");
+        lazy.logger.debug("Not ready to upgrade");
         resolve(false);
       }
       this.#makeRequest("trustpromo", JSON.parse(request).request)
         .then(response => {
           if (response.hasOwnProperty("error")) {
+            lazy.logger.error("Error response from trustpromo", response.error);
             resolve(false);
           }
-          console.log("Got promotion cred");
-          console.log(response);
-          console.log(request);
+          lazy.logger.debug("Got promotion cred", response, request);
           let promoCred = lazy.handle_trust_promotion(
             request,
             JSON.stringify(response)
           );
-          console.log("Formatted promotion cred");
+          lazy.logger.debug("Formatted promotion cred");
           request = lazy.trust_migration(
-            this.#credentials[loxid],
+            this.#getCredentials(loxId),
             promoCred,
             this.#pubKeys
           );
-          console.log("Formatted migration request");
+          lazy.logger.debug("Formatted migration request");
           this.#makeRequest("trustmig", JSON.parse(request).request)
             .then(response => {
               if (response.hasOwnProperty("error")) {
+                lazy.logger.error(
+                  "Error response from trustmig",
+                  response.error
+                );
                 resolve(false);
               }
-              console.log("Got new credential");
+              lazy.logger.debug("Got new credential");
               let cred = lazy.handle_trust_migration(request, response);
-              this.#credentials[loxid] = cred;
-              this.#store();
+              this.#changeCredentials(loxId, cred);
               resolve(true);
             })
             .catch(err => {
-              console.log(err);
-              console.log("Failed trust migration");
+              lazy.logger.error("Failed trust migration", err);
               resolve(false);
             });
         })
         .catch(err => {
-          console.log(err);
-          console.log("Failed trust promotion");
+          lazy.logger.error("Failed trust promotion", err);
           resolve(false);
         });
     });
@@ -701,88 +854,105 @@ class LoxImpl {
   /**
    * Get a list of accumulated events.
    *
+   * @param {string} loxId - The ID to get events for.
    * @returns {EventData[]} A list of the accumulated, unacknowledged events
    *   associated with a user's credential.
    */
-  getEventData() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    const loxid = lazy.TorSettings.bridges.lox_id;
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
+  getEventData(loxId) {
+    this.#assertInitialized();
+    if (loxId !== this.#activeLoxId) {
+      lazy.logger.warn(
+        `No event data for loxId ${loxId} since it was replaced by ${
+          this.#activeLoxId
+        }`
+      );
+      return [];
     }
-    return this.#events;
+    // Return a copy.
+    return structuredClone(this.#events);
   }
 
   /**
    * Clears accumulated event data.
+   *
+   * Should be called whenever the user acknowledges the existing events.
+   *
+   * @param {string} loxId - The ID to clear events for.
    */
-  clearEventData() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
+  clearEventData(loxId) {
+    this.#assertInitialized();
+    if (loxId !== this.#activeLoxId) {
+      lazy.logger.warn(
+        `Not clearing event data for loxId ${loxId} since it was replaced by ${
+          this.#activeLoxId
+        }`
+      );
+      return;
     }
     this.#events = [];
     this.#store();
-  }
-
-  /**
-   * Clears accumulated invitations.
-   */
-  clearInvites() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    this.#invites = [];
-    this.#store();
+    Services.obs.notifyObservers(null, LoxTopics.UpdateEvents);
   }
 
   /**
    * @typedef {object} UnlockData
    *
-   * @property {string} date - The date-time for the next level up, formatted as YYYY-MM-DDTHH:mm:ssZ.
-   * @property {integer} nextLevel - The next level. Levels count from 0, so this will be 1 or greater.
-   *
+   * @property {string} date - The date-time for the next level up, formatted as
+   *   YYYY-MM-DDTHH:mm:ssZ.
+   * @property {integer} nextLevel - The next level. Levels count from 0, so
+   *   this will be 1 or greater.
    */
 
   /**
    * Get details about the next feature unlock.
    *
+   * NOTE: A call to this method may trigger LoxTopics.UpdateNextUnlock.
+   *
+   * @param {string} loxId - The ID to get the unlock for.
    * @returns {UnlockData} - Details about the next unlock.
    */
-  async getNextUnlock() {
-    if (!this.#initialized) {
-      throw new LoxError(LoxErrors.NotInitialized);
-    }
-    const loxid = lazy.TorSettings.bridges.lox_id;
-    if (!loxid || !this.#credentials[loxid]) {
-      throw new LoxError(LoxErrors.MissingCredential);
-    }
+  async getNextUnlock(loxId) {
+    this.#assertInitialized();
     await this.#getConstants();
-    let nextUnlocks = JSON.parse(
-      lazy.get_next_unlock(this.#constants, this.#credentials[loxid])
+    let nextUnlock = JSON.parse(
+      lazy.get_next_unlock(this.#constants, this.#getCredentials(loxId))
     );
-    const level = parseInt(lazy.get_trust_level(this.#credentials[loxid]));
-    const unlocks = {
-      date: nextUnlocks.trust_level_unlock_date,
+    const level = this.#getLevel(loxId);
+    return {
+      date: nextUnlock.trust_level_unlock_date,
       nextLevel: level + 1,
     };
-    return unlocks;
   }
 
   async #makeRequest(procedure, args) {
     // TODO: Customize to for Lox
-    const serviceUrl = "https://rdsys-frontend-01.torproject.org/lox";
+    const serviceUrl = "https://lox.torproject.org";
     const url = `${serviceUrl}/${procedure}`;
 
     if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) {
-      const request = await fetch(url, {
-        method: "POST",
-        headers: {
-          "Content-Type": "application/vnd.api+json",
-        },
-        body: JSON.stringify(args),
-      });
+      let request;
+      try {
+        request = await fetch(url, {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/vnd.api+json",
+          },
+          body: JSON.stringify(args),
+        });
+      } catch (error) {
+        lazy.logger.debug("fetch fail", url, args, error);
+        throw new LoxError(
+          `fetch "${procedure}" from Lox authority failed: ${error?.message}`,
+          LoxError.LoxServerUnreachable
+        );
+      }
+      if (!request.ok) {
+        lazy.logger.debug("fetch response", url, args, request);
+        // Do not treat as a LoxServerUnreachable type.
+        throw new LoxError(
+          `Lox authority responded to "${procedure}" with ${request.status}: ${request.statusText}`
+        );
+      }
       return request.json();
     }
 
@@ -803,7 +973,26 @@ class LoxImpl {
       });
     }
     const builder = await this.#domainFrontedRequests;
-    return builder.buildPostRequest(url, args);
+    try {
+      return await builder.buildPostRequest(url, args);
+    } catch (error) {
+      lazy.logger.debug("Domain front request fail", url, args, error);
+      if (error instanceof lazy.DomainFrontRequestNetworkError) {
+        throw new LoxError(
+          `Domain front fetch "${procedure}" from Lox authority failed: ${error?.message}`,
+          LoxError.LoxServerUnreachable
+        );
+      }
+      if (error instanceof lazy.DomainFrontRequestResponseError) {
+        // Do not treat as a LoxServerUnreachable type.
+        throw new LoxError(
+          `Lox authority responded to domain front "${procedure}" with ${error.status}: ${error.statusText}`
+        );
+      }
+      throw new LoxError(
+        `Domain front request for "${procedure}" from Lox authority failed: ${error?.message}`
+      );
+    }
   }
 }
 


=====================================
toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
=====================================
@@ -429,20 +429,6 @@ export const TorLauncherUtil = Object.freeze({
     return aStringName;
   },
 
-  getLocalizedStringForError(aNSResult) {
-    for (let prop in Cr) {
-      if (Cr[prop] === aNSResult) {
-        const key = "nsresult." + prop;
-        const rv = this.getLocalizedString(key);
-        if (rv !== key) {
-          return rv;
-        }
-        return prop; // As a fallback, return the NS_ERROR... name.
-      }
-    }
-    return undefined;
-  },
-
   getLocalizedBootstrapStatus(aStatusObj, aKeyword) {
     if (!aStatusObj || !aKeyword) {
       return "";


=====================================
toolkit/modules/DomainFrontedRequests.sys.mjs
=====================================
@@ -347,6 +347,31 @@ class MeekTransportAndroid {
   }
 }
 
+/**
+ * Corresponds to a Network error with the request.
+ */
+export class DomainFrontRequestNetworkError extends Error {
+  constructor(request, statusCode) {
+    super(`Error fetching ${request.name}: ${statusCode}`);
+    this.name = "DomainFrontRequestNetworkError";
+    this.statusCode = statusCode;
+  }
+}
+
+/**
+ * Corresponds to a non-ok response from the server.
+ */
+export class DomainFrontRequestResponseError extends Error {
+  constructor(request) {
+    super(
+      `Error response from ${request.name} server: ${request.responseStatus}`
+    );
+    this.name = "DomainFrontRequestResponseError";
+    this.status = request.responseStatus;
+    this.statusText = request.responseStatusText;
+  }
+}
+
 /**
  * Callback object to promisify the XPCOM request.
  */
@@ -379,12 +404,11 @@ class ResponseListener {
   onStopRequest(request, status) {
     try {
       if (!Components.isSuccessCode(status)) {
-        const errorMessage =
-          lazy.TorLauncherUtil.getLocalizedStringForError(status);
-        this.#reject(new Error(errorMessage));
+        // Assume this is a network error.
+        this.#reject(new DomainFrontRequestNetworkError(request, status));
       }
       if (request.responseStatus !== 200) {
-        this.#reject(new Error(request.responseStatusText));
+        this.#reject(new DomainFrontRequestResponseError(request));
       }
     } catch (err) {
       this.#reject(err);


=====================================
toolkit/modules/TorSettings.sys.mjs
=====================================
@@ -7,6 +7,7 @@ const lazy = {};
 ChromeUtils.defineESModuleGetters(lazy, {
   TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
   Lox: "resource://gre/modules/Lox.sys.mjs",
+  LoxTopics: "resource://gre/modules/Lox.sys.mjs",
   TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
   TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 });
@@ -330,7 +331,17 @@ class TorSettingsImpl {
           if (!val) {
             return;
           }
-          this.bridges.bridge_strings = lazy.Lox.getBridges(val);
+          let bridgeStrings;
+          try {
+            bridgeStrings = lazy.Lox.getBridges(val);
+          } catch (error) {
+            addError(`No bridges for lox_id ${val}: ${error?.message}`);
+            // Set as invalid, which will make the builtin_type "" and set the
+            // bridge_strings to be empty at the next #cleanupSettings.
+            this.bridges.source = TorBridgeSource.Invalid;
+            return;
+          }
+          this.bridges.bridge_strings = bridgeStrings;
         },
       },
     });
@@ -692,7 +703,7 @@ class TorSettingsImpl {
     try {
       await lazy.Lox.init();
     } catch (e) {
-      lazy.logger.error("Could not initialize Lox.", e.type);
+      lazy.logger.error("Could not initialize Lox.", e);
     }
 
     if (
@@ -711,6 +722,8 @@ class TorSettingsImpl {
       }
     }
 
+    Services.obs.addObserver(this, lazy.LoxTopics.UpdateBridges);
+
     lazy.logger.info("Ready");
   }
 
@@ -718,9 +731,28 @@ class TorSettingsImpl {
    * Unload or uninit our settings.
    */
   async uninit() {
+    Services.obs.removeObserver(this, lazy.LoxTopics.UpdateBridges);
     await lazy.Lox.uninit();
   }
 
+  observe(subject, topic, data) {
+    switch (topic) {
+      case lazy.LoxTopics.UpdateBridges:
+        if (this.bridges.lox_id) {
+          // Fetch the newest bridges.
+          this.bridges.bridge_strings = lazy.Lox.getBridges(
+            this.bridges.lox_id
+          );
+          // No need to save to prefs since bridge_strings is not stored for Lox
+          // source. But we do pass on the changes to TorProvider.
+          // FIXME: This can compete with TorConnect to reach TorProvider.
+          // tor-browser#42316
+          this.applySettings();
+        }
+        break;
+    }
+  }
+
   /**
    * Check whether the object has been successfully initialized, and throw if
    * it has not.
@@ -763,24 +795,32 @@ class TorSettingsImpl {
       TorSettingsPrefs.bridges.source,
       TorBridgeSource.Invalid
     );
-    this.bridges.lox_id = Services.prefs.getStringPref(
-      TorSettingsPrefs.bridges.lox_id,
-      ""
-    );
-    if (this.bridges.source == TorBridgeSource.BuiltIn) {
-      this.bridges.builtin_type = Services.prefs.getStringPref(
-        TorSettingsPrefs.bridges.builtin_type,
-        ""
-      );
-    } else {
-      const bridgeBranchPrefs = Services.prefs
-        .getBranch(TorSettingsPrefs.bridges.bridge_strings)
-        .getChildList("");
-      this.bridges.bridge_strings = Array.from(bridgeBranchPrefs, pref =>
-        Services.prefs.getStringPref(
-          `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
-        )
-      );
+    switch (this.bridges.source) {
+      case TorBridgeSource.BridgeDB:
+      case TorBridgeSource.UserProvided:
+        this.bridges.bridge_strings = Services.prefs
+          .getBranch(TorSettingsPrefs.bridges.bridge_strings)
+          .getChildList("")
+          .map(pref =>
+            Services.prefs.getStringPref(
+              `${TorSettingsPrefs.bridges.bridge_strings}${pref}`
+            )
+          );
+        break;
+      case TorBridgeSource.BuiltIn:
+        // bridge_strings is set via builtin_type.
+        this.bridges.builtin_type = Services.prefs.getStringPref(
+          TorSettingsPrefs.bridges.builtin_type,
+          ""
+        );
+        break;
+      case TorBridgeSource.Lox:
+        // bridge_strings is set via lox id.
+        this.bridges.lox_id = Services.prefs.getStringPref(
+          TorSettingsPrefs.bridges.lox_id,
+          ""
+        );
+        break;
     }
     /* Proxy */
     this.proxy.enabled = Services.prefs.getBoolPref(
@@ -866,7 +906,10 @@ class TorSettingsImpl {
       );
     });
     // write new ones
-    if (this.bridges.source !== TorBridgeSource.BuiltIn) {
+    if (
+      this.bridges.source !== TorBridgeSource.Lox &&
+      this.bridges.source !== TorBridgeSource.BuiltIn
+    ) {
       this.bridges.bridge_strings.forEach((string, index) => {
         Services.prefs.setStringPref(
           `${TorSettingsPrefs.bridges.bridge_strings}.${index}`,


=====================================
toolkit/torbutton/chrome/locale/en-US/torlauncher.properties
=====================================
@@ -55,7 +55,3 @@ torlauncher.bootstrapWarning.timeout=connection timeout
 torlauncher.bootstrapWarning.noroute=no route to host
 torlauncher.bootstrapWarning.ioerror=read/write error
 torlauncher.bootstrapWarning.pt_missing=missing pluggable transport
-
-torlauncher.nsresult.NS_ERROR_NET_RESET=The connection to the server was lost.
-torlauncher.nsresult.NS_ERROR_CONNECTION_REFUSED=Could not connect to the server.
-torlauncher.nsresult.NS_ERROR_PROXY_CONNECTION_REFUSED=Could not connect to the proxy.



View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/4255b354f2abcfc12e83107ebe19a324cc2430c1...9abd099f1d0bb0889c49e380fb3e2f427df59e23

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/4255b354f2abcfc12e83107ebe19a324cc2430c1...9abd099f1d0bb0889c49e380fb3e2f427df59e23
You're receiving this email because of your account on gitlab.torproject.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tor-commits/attachments/20240409/f7bb61c3/attachment-0001.htm>


More information about the tor-commits mailing list