tor-browser

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

commit 2c518d40a8da389566b749fb397430f43b54eb58
parent c7f62c9b514cc7ae291f9d91270142f658e0d1ff
Author: Alex Cottner <acottner@mozilla.com>
Date:   Mon, 29 Dec 2025 16:39:08 +0000

Bug 2007208 - Adding v2 service support for remote-settings client r=leplatrem

Adding v2 service support for remote-settings client.
Removing quotes around _expected timestamps for remote-settings service calls.
This will match up with what the v2 reader endpoints are expecting.
v1 reader endpoints are being made forwards compatible.
Updated remote-settings constants to allow release to switch between v1 and v2.
Updated tests.

Differential Revision: https://phabricator.services.mozilla.com/D277258

Diffstat:
Mservices/settings/RemoteSettingsClient.sys.mjs | 2+-
Mservices/settings/SyncHistory.sys.mjs | 15+++++++--------
Mservices/settings/Utils.sys.mjs | 25+++++++++++++------------
Mservices/settings/remote-settings.sys.mjs | 21++++++++-------------
Mservices/settings/test/unit/test_remote_settings.js | 16++++++++--------
Mservices/settings/test/unit/test_remote_settings_older_than_local.js | 2+-
Mservices/settings/test/unit/test_remote_settings_poll.js | 14+++++++-------
Mservices/settings/test/unit/test_remote_settings_release_prefs.js | 37+++++++++++++++++++++++++++++++------
Mservices/settings/test/unit/test_remote_settings_signatures.js | 14+++++++-------
Mservices/settings/test/unit/test_remote_settings_sync_history.js | 20++++++++++----------
Mtoolkit/modules/AppConstants.sys.mjs | 6+++---
Mtools/@types/subs/AppConstants.sys.d.mts | 2+-
12 files changed, 97 insertions(+), 77 deletions(-)

diff --git a/services/settings/RemoteSettingsClient.sys.mjs b/services/settings/RemoteSettingsClient.sys.mjs @@ -1153,7 +1153,7 @@ export class RemoteSettingsClient extends EventEmitter { const hasLocalData = localTimestamp !== null; const { retry = false } = options; // On retry, we fully re-fetch the collection (no `?_since`). - const since = retry || !hasLocalData ? undefined : `"${localTimestamp}"`; + const since = retry || !hasLocalData ? undefined : localTimestamp; // Define an executor that will verify the signature of the local data. const verifySignatureLocalData = (resolve, reject) => { diff --git a/services/settings/SyncHistory.sys.mjs b/services/settings/SyncHistory.sys.mjs @@ -30,20 +30,19 @@ export class SyncHistory { } /** - * Store the synchronization status. The ETag is converted and stored as + * Store the synchronization status. The timestamp is converted and stored as * a millisecond epoch timestamp. * The entries with the oldest timestamps will be deleted to maintain the * history size under the configured maximum. * - * @param {string} etag the ETag value from the server (eg. `"1647961052593"`) + * @param {int} timestamp the timestamp value from the server (eg. 1647961052593) * @param {string} status the synchronization status (eg. `"success"`) * @param {object} infos optional additional information to keep track of */ - async store(etag, status, infos = {}) { + async store(timestamp, status, infos = {}) { const rkv = await this.#init(); - const timestamp = parseInt(etag.replace('"', ""), 10); - if (Number.isNaN(timestamp)) { - throw new Error(`Invalid ETag value ${etag}`); + if (!Number.isInteger(timestamp)) { + throw new Error(`Invalid timestamp value ${timestamp}`); } const key = `v1-${this.source}\t${timestamp}`; const value = { timestamp, status, infos }; @@ -51,8 +50,8 @@ export class SyncHistory { // Trim old entries. const allEntries = await this.list(); for (let i = this.size; i < allEntries.length; i++) { - let { timestamp } = allEntries[i]; - await rkv.delete(`v1-${this.source}\t${timestamp}`); + let { timestamp: entryTimestamp } = allEntries[i]; + await rkv.delete(`v1-${this.source}\t${entryTimestamp}`); } } diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs @@ -50,7 +50,7 @@ ChromeUtils.defineLazyGetter(lazy, "isRunningTests", () => { // Overriding the server URL is normally disabled on Beta and Release channels, // except under some conditions. -ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => { +ChromeUtils.defineLazyGetter(lazy, "allowServerURL", () => { if (!AppConstants.RELEASE_OR_BETA) { // Always allow to override the server URL on Nightly/DevEdition. return true; @@ -65,13 +65,14 @@ ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => { return true; } - if (lazy.gServerURL != AppConstants.REMOTE_SETTINGS_SERVER_URL) { - log.warn("Ignoring preference override of remote settings server"); - log.warn( - "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment" - ); + if (AppConstants.REMOTE_SETTINGS_SERVER_URLS.includes(lazy.gServerURL)) { + return true; } + log.warn("Ignoring preference override of remote settings server"); + log.warn( + "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment" + ); return false; }); @@ -79,7 +80,7 @@ XPCOMUtils.defineLazyPreferenceGetter( lazy, "gServerURL", "services.settings.server", - AppConstants.REMOTE_SETTINGS_SERVER_URL + AppConstants.REMOTE_SETTINGS_SERVER_URLS ); XPCOMUtils.defineLazyPreferenceGetter( @@ -97,9 +98,9 @@ const _cdnURLs = {}; export var Utils = { get SERVER_URL() { - return lazy.allowServerURLOverride + return lazy.allowServerURL ? lazy.gServerURL - : AppConstants.REMOTE_SETTINGS_SERVER_URL; + : AppConstants.REMOTE_SETTINGS_SERVER_URLS[0]; }, CHANGES_PATH: "/buckets/monitor/collections/changes/changeset", @@ -139,7 +140,7 @@ export var Utils = { get LOAD_DUMPS() { // Load dumps only if pulling data from the production server, or in tests. return ( - this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL || + AppConstants.REMOTE_SETTINGS_SERVER_URLS.includes(this.SERVER_URL) || lazy.isRunningTests ); }, @@ -147,7 +148,7 @@ export var Utils = { get PREVIEW_MODE() { // We want to offer the ability to set preview mode via a preference // for consumers who want to pull from the preview bucket on startup. - if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURLOverride) { + if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURL) { return lazy.gPreviewEnabled; } return !!this._previewModeEnabled; @@ -490,7 +491,7 @@ export var Utils = { return { changes, - currentEtag: `"${timestamp}"`, + timestamp, serverTimeMillis, backoffSeconds, ageSeconds, diff --git a/services/settings/remote-settings.sys.mjs b/services/settings/remote-settings.sys.mjs @@ -482,13 +482,8 @@ function remoteSettingsFunction() { throw new Error(`Polling for changes failed: ${e.message}.`); } - const { - serverTimeMillis, - changes, - currentEtag, - backoffSeconds, - ageSeconds, - } = pollResult; + const { serverTimeMillis, changes, timestamp, backoffSeconds, ageSeconds } = + pollResult; // Report age of server data in Telemetry. pollTelemetryArgs = { age: ageSeconds, ...pollTelemetryArgs }; @@ -565,7 +560,7 @@ function remoteSettingsFunction() { const syncTelemetryArgs = { source: TELEMETRY_SOURCE_SYNC, duration: durationMilliseconds, - timestamp: `${currentEtag}`, + timestamp, trigger, }; @@ -579,7 +574,7 @@ function remoteSettingsFunction() { ); // Keep track of sync failure in history. await lazy.gSyncHistory - .store(currentEtag, status, { + .store(timestamp, status, { expectedTimestamp, errorName: firstError.name, }) @@ -611,7 +606,7 @@ function remoteSettingsFunction() { } // Save current Etag for next poll. - lazy.gPrefs.setStringPref(PREF_SETTINGS_LAST_ETAG, currentEtag); + lazy.gPrefs.setStringPref(PREF_SETTINGS_LAST_ETAG, timestamp); // Report the global synchronization success. const status = lazy.UptakeTelemetry.STATUS.SUCCESS; @@ -622,7 +617,7 @@ function remoteSettingsFunction() { ); // Keep track of sync success in history. await lazy.gSyncHistory - .store(currentEtag, status) + .store(timestamp, status) .catch(error => console.error(error)); lazy.console.info( @@ -662,7 +657,7 @@ function remoteSettingsFunction() { if (!localOnly) { // Make sure we fetch the latest server info, use a random cache bust value. const randomCacheBust = 99990000 + Math.floor(Math.random() * 9999); - ({ changes, currentEtag: serverTimestamp } = + ({ changes, timestamp: serverTimestamp } = await lazy.Utils.fetchLatestChanges(lazy.Utils.SERVER_URL, { expected: randomCacheBust, })); @@ -795,7 +790,7 @@ export var remoteSettingsBroadcastHandler = { ); return RemoteSettings.pollChanges({ - expectedTimestamp: version.replace('"', ""), + expectedTimestamp: version.replaceAll('"', ""), trigger: isStartup ? "startup" : "broadcast", }); }, diff --git a/services/settings/test/unit/test_remote_settings.js b/services/settings/test/unit/test_remote_settings.js @@ -810,7 +810,7 @@ add_task(async function test_inspect_method() { equal(mainBucket, "main"); equal(serverURL, `http://localhost:${server.identity.primaryPort}/v1`); equal(defaultSigner, rsSigner); - equal(serverTimestamp, '"5000"'); + equal(serverTimestamp, "5000"); // A collection is listed in .inspect() if it has local data or if there // is a JSON dump for it. @@ -1527,7 +1527,7 @@ wNuvFqc= ], }, }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=%223000%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=3000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1558,7 +1558,7 @@ wNuvFqc= ], }, }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=%224000%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=4000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1581,7 +1581,7 @@ wNuvFqc= ], }, }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=%229999%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=9999": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1596,7 +1596,7 @@ wNuvFqc= error: "Service Unavailable", }, }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=%2210000%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=10000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1608,7 +1608,7 @@ wNuvFqc= status: { status: 200, statusText: "OK" }, responseBody: "<invalid json", }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=%2211000%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=11000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1695,7 +1695,7 @@ wNuvFqc= ], }, }, - "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=%223000%22": + "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=3000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", @@ -1795,7 +1795,7 @@ wNuvFqc= ], }, }, - "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=%222000%22": + "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=2000": { sampleHeaders: [ "Access-Control-Allow-Origin: *", diff --git a/services/settings/test/unit/test_remote_settings_older_than_local.js b/services/settings/test/unit/test_remote_settings_older_than_local.js @@ -41,7 +41,7 @@ add_setup(() => { ], }, // Client fetches with _expected=222&_since=333 - "/v1/buckets/main/collections/some-cid/changeset?_expected=222&_since=%22333%22": + "/v1/buckets/main/collections/some-cid/changeset?_expected=222&_since=333": { timestamp: 222, metadata: { diff --git a/services/settings/test/unit/test_remote_settings_poll.js b/services/settings/test/unit/test_remote_settings_poll.js @@ -73,7 +73,7 @@ add_task(async function test_an_event_is_sent_on_start() { server.registerPathHandler(CHANGES_PATH, (request, response) => { response.write(JSON.stringify({ timestamp: 42, changes: [] })); response.setHeader("Content-Type", "application/json; charset=UTF-8"); - response.setHeader("ETag", '"42"'); + response.setHeader("ETag", "42"); response.setHeader("Date", new Date().toUTCString()); response.setStatusLine(null, 200, "OK"); }); @@ -173,7 +173,7 @@ add_task(async function test_check_success() { Assert.ok(maybeSyncCalled, "maybeSync was called"); Assert.ok(notificationObserved, "a notification should have been observed"); // Last timestamp was saved. An ETag header value is a quoted string. - Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"1100"'); + Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), "1100"); // check the last_update is updated Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000); @@ -238,7 +238,7 @@ add_task(async function test_update_timer_interface() { }); // Everything went fine. - Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"42"'); + Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), "42"); Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000); }); add_task(clear_state); @@ -253,7 +253,7 @@ add_task(async function test_check_up_to_date() { const serverTime = 4000; server.registerPathHandler(CHANGES_PATH, serveChangesEntries(serverTime, [])); - Services.prefs.setStringPref(PREF_LAST_ETAG, '"1100"'); + Services.prefs.setStringPref(PREF_LAST_ETAG, "1100"); // Ensure that the remote-settings:changes-poll-end notification is sent. let notificationObserved = false; @@ -414,7 +414,7 @@ add_task(async function test_age_of_data_is_reported_in_uptake_status() { source: TELEMETRY_SOURCE_SYNC, duration: () => true, trigger: "manual", - timestamp: `"${recordsTimestamp}"`, + timestamp: `${recordsTimestamp}`, }, ], ], @@ -493,7 +493,7 @@ add_task(async function test_success_with_partial_list() { collection: "poll-test-collection", }, ]; - if (request.queryString.includes(`_since=${encodeURIComponent('"42"')}`)) { + if (request.queryString.includes(`_since=42`)) { response.write( JSON.stringify({ timestamp: 43, @@ -1308,7 +1308,7 @@ add_task( timestamp: 42, }) ); - response.setHeader("ETag", '"42"'); + response.setHeader("ETag", "42"); response.setStatusLine(null, 200, "OK"); response.setHeader("Content-Type", "application/json; charset=UTF-8"); response.setHeader("Date", new Date().toUTCString()); diff --git a/services/settings/test/unit/test_remote_settings_release_prefs.js b/services/settings/test/unit/test_remote_settings_release_prefs.js @@ -57,7 +57,7 @@ add_task( Assert.equal( Utils.SERVER_URL, - AppConstants.REMOTE_SETTINGS_SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0], "Server url pref was not read in release" ); } @@ -65,6 +65,31 @@ add_task( add_task( { + skip_if: () => !AppConstants.RELEASE_OR_BETA, + }, + async function test_server_url_can_be_changed_to_another_valid_option() { + Services.prefs.setStringPref( + "services.settings.server", + AppConstants.REMOTE_SETTINGS_SERVER_URLS[1] + ); + + const Utils = getNewUtils(); + + Assert.equal( + Utils.SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[1], + "Server url pref was read as second option in release" + ); + + Services.prefs.setStringPref( + "services.settings.server", + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0] + ); + } +); + +add_task( + { skip_if: () => AppConstants.RELEASE_OR_BETA, }, async function test_server_url_cannot_be_toggled_in_dev_nightly() { @@ -77,7 +102,7 @@ add_task( Assert.notEqual( Utils.SERVER_URL, - AppConstants.REMOTE_SETTINGS_SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0], "Server url pref was read in nightly/dev" ); } @@ -126,7 +151,7 @@ add_task( Assert.equal( Utils.SERVER_URL, - AppConstants.REMOTE_SETTINGS_SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0], "Server url pref was not read" ); Assert.ok(Utils.LOAD_DUMPS, "Dumps will always be loaded"); @@ -147,7 +172,7 @@ add_task( Assert.notEqual( Utils.SERVER_URL, - AppConstants.REMOTE_SETTINGS_SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0], "Server url pref was read" ); Assert.ok(!Utils.LOAD_DUMPS, "Dumps are not loaded if server is not prod"); @@ -167,7 +192,7 @@ add_task( Assert.notEqual( Utils.SERVER_URL, - AppConstants.REMOTE_SETTINGS_SERVER_URL, + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0], "Server url pref was read" ); } @@ -241,7 +266,7 @@ add_task( Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1"); Services.prefs.setStringPref( "services.settings.server", - AppConstants.REMOTE_SETTINGS_SERVER_URL + AppConstants.REMOTE_SETTINGS_SERVER_URLS[0] ); const Utils = getNewUtils(); diff --git a/services/settings/test/unit/test_remote_settings_signatures.js b/services/settings/test/unit/test_remote_settings_signatures.js @@ -441,7 +441,7 @@ add_task(async function test_check_synchronization_with_signatures() { }; const twoItemsResponses = { - "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=%221000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=1000": [RESPONSE_TWO_ADDED], }; registerHandlers(twoItemsResponses); @@ -482,7 +482,7 @@ add_task(async function test_check_synchronization_with_signatures() { }; const oneAddedOneRemovedResponses = { - "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=%223000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=3000": [RESPONSE_ONE_ADDED_ONE_REMOVED], }; registerHandlers(oneAddedOneRemovedResponses); @@ -520,7 +520,7 @@ add_task(async function test_check_synchronization_with_signatures() { }; const noOpResponses = { - "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=%224000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=4000": [RESPONSE_EMPTY_NO_UPDATE], }; registerHandlers(noOpResponses); @@ -581,7 +581,7 @@ add_task(async function test_check_synchronization_with_signatures() { // The first collection state is the three item collection (since // there was sync with no updates before) - but, since the signature is wrong, // another request will be made... - "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000": [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG], // Subsequent signature returned is a valid one for the three item // collection. @@ -639,7 +639,7 @@ add_task(async function test_check_synchronization_with_signatures() { const badSigGoodOldResponses = { // The first collection state is the current state (since there's no update // - but, since the signature is wrong, another request will be made) - "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000": [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG], // The next request is for the full collection. This will be // checked against the valid signature and last_modified times will be @@ -691,7 +691,7 @@ add_task(async function test_check_synchronization_with_signatures() { }; const badLocalContentGoodSigResponses = { - "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%223900%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=3900": [RESPONSE_COMPLETE_BAD_SIG], "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [ RESPONSE_COMPLETE_INITIAL, @@ -824,7 +824,7 @@ add_task(async function test_check_synchronization_with_signatures() { }), }; const allBadSigResponses = { - "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=%224000%22": + "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=4000": [RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000], "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [ RESPONSE_ONLY_RECORD4_BAD_SIG, diff --git a/services/settings/test/unit/test_remote_settings_sync_history.js b/services/settings/test/unit/test_remote_settings_sync_history.js @@ -7,9 +7,9 @@ add_task(clear_state); add_task(async function test_entries_are_stored_by_source() { const history = new SyncHistory(); - await history.store("42", "success", { pi: "3.14" }); + await history.store(42, "success", { pi: "3.14" }); // Check that history is isolated by source. - await new SyncHistory("main/cfr").store("88", "error"); + await new SyncHistory("main/cfr").store(88, "error"); const l = await history.list(); @@ -29,15 +29,15 @@ add_task( const history = new SyncHistory("settings-sync", { size: 3 }); const anotherHistory = await new SyncHistory("main/cfr"); - await history.store("42", "success"); - await history.store("41", "sync_error"); - await history.store("43", "up_to_date"); + await history.store(42, "success"); + await history.store(41, "sync_error"); + await history.store(43, "up_to_date"); let l = await history.list(); Assert.equal(l.length, 3); - await history.store("44", "success"); - await anotherHistory.store("44", "success"); + await history.store(44, "success"); + await anotherHistory.store(44, "success"); l = await history.list(); Assert.equal(l.length, 3); @@ -51,9 +51,9 @@ add_task(clear_state); add_task(async function test_entries_are_sorted_by_timestamp_desc() { const history = new SyncHistory("settings-sync"); - await history.store("42", "success"); - await history.store("41", "sync_error"); - await history.store("44", "up_to_date"); + await history.store(42, "success"); + await history.store(41, "sync_error"); + await history.store(44, "up_to_date"); const l = await history.list(); diff --git a/toolkit/modules/AppConstants.sys.mjs b/toolkit/modules/AppConstants.sys.mjs @@ -207,11 +207,11 @@ export var AppConstants = Object.freeze({ ENABLE_WEBDRIVER: @ENABLE_WEBDRIVER_BOOL@, - REMOTE_SETTINGS_SERVER_URL: + REMOTE_SETTINGS_SERVER_URLS: #ifdef MOZ_THUNDERBIRD - "https://thunderbird-settings.thunderbird.net/v1", + [ "https://thunderbird-settings.thunderbird.net/v1" ], #else - "https://firefox.settings.services.mozilla.com/v1", + [ "https://firefox.settings.services.mozilla.com/v1", "https://firefox.settings.services.mozilla.com/v2" ], #endif REMOTE_SETTINGS_VERIFY_SIGNATURE: diff --git a/tools/@types/subs/AppConstants.sys.d.mts b/tools/@types/subs/AppConstants.sys.d.mts @@ -154,7 +154,7 @@ export const AppConstants: Readonly<{ ENABLE_WEBDRIVER: boolean; // #ifdef !MOZ_THUNDERBIRD - REMOTE_SETTINGS_SERVER_URL: "https://firefox.settings.services.mozilla.com/v1"; + REMOTE_SETTINGS_SERVER_URLS: [ "https://firefox.settings.services.mozilla.com/v1" ]; // #ifdef !MOZ_THUNDERBIRD REMOTE_SETTINGS_VERIFY_SIGNATURE: boolean;