--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1407,18 +1407,18 @@ pref("identity.mobilepromo.ios", "https:
// Migrate any existing Firefox Account data from the default profile to the
// Developer Edition profile.
#ifdef MOZ_DEV_EDITION
pref("identity.fxaccounts.migrateToDevEdition", true);
#else
pref("identity.fxaccounts.migrateToDevEdition", false);
#endif
-// If activated, send tab will use the new FxA messages backend.
-pref("identity.fxaccounts.messages.enabled", false);
+// If activated, send tab will use the new FxA commands backend.
+pref("identity.fxaccounts.commands.enabled", false);
// On GTK, we now default to showing the menubar only when alt is pressed:
#ifdef MOZ_WIDGET_GTK
pref("ui.key.menuAccessKeyFocuses", true);
#endif
#ifdef NIGHTLY_BUILD
pref("media.eme.vp9-in-mp4.enabled", true);
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -322,34 +322,49 @@ var gSync = {
async sendTabToDevice(url, clients, title) {
let devices;
try {
devices = await fxAccounts.getDeviceList();
} catch (e) {
console.error("Could not get the FxA device list", e);
devices = []; // We can still run in degraded mode.
}
- const toSendMessages = [];
+ const fxaCommandsDevices = [];
+ const oldSendTabClients = [];
for (const client of clients) {
const device = devices.find(d => d.id == client.fxaDeviceId);
- if (device && fxAccounts.messages.canReceiveSendTabMessages(device)) {
- toSendMessages.push(device);
+ if (!device) {
+ console.error(`Could not find associated FxA device for ${client.name}`);
+ continue;
+ } else if ((await fxAccounts.commands.sendTab.isDeviceCompatible(device))) {
+ fxaCommandsDevices.push(device);
} else {
- try {
- await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title);
- } catch (e) {
- console.error("Could not send tab to device", e);
- }
+ oldSendTabClients.push(client);
}
}
- if (toSendMessages.length) {
+ if (fxaCommandsDevices.length) {
+ console.log(`Sending a tab to ${fxaCommandsDevices.map(d => d.name).join(", ")} using FxA commands.`);
+ const report = await fxAccounts.commands.sendTab.send(fxaCommandsDevices, {url, title});
+ for (let {device, error} of report.failed) {
+ console.error(`Failed to send a tab with FxA commands for ${device.name}.
+ Falling back on the Sync back-end`, error);
+ const client = clients.find(c => c.fxaDeviceId == device.id);
+ if (!client) {
+ console.error(`Could not find associated Sync device for ${device.name}`);
+ continue;
+ }
+ oldSendTabClients.push(client);
+ }
+ }
+ for (let client of oldSendTabClients) {
try {
- await fxAccounts.messages.sendTab(toSendMessages, {url, title});
+ console.log(`Sending a tab to ${client.name} using Sync.`);
+ await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title);
} catch (e) {
- console.error("Could not send tab to device", e);
+ console.error("Could not send tab to device.", e);
}
}
},
populateSendTabToDevicesMenu(devicesPopup, url, title, createDeviceNodeFn) {
if (!createDeviceNodeFn) {
createDeviceNodeFn = (clientId, name, clientType, lastModified) => {
let eltName = name ? "menuitem" : "menuseparator";
@@ -586,17 +601,23 @@ var gSync = {
// via the various UI components.
doSync() {
if (!UIState.isReady()) {
return;
}
const state = UIState.get();
if (state.status == UIState.STATUS_SIGNED_IN) {
this.updateSyncStatus({ syncing: true });
- Services.tm.dispatchToMainThread(() => Weave.Service.sync());
+ Services.tm.dispatchToMainThread(() => {
+ // We are pretty confident that push helps us pick up all FxA commands,
+ // but some users might have issues with push, so let's unblock them
+ // by fetching the missed FxA commands on manual sync.
+ fxAccounts.commands.fetchMissedRemoteCommands();
+ Weave.Service.sync();
+ });
}
},
openPrefs(entryPoint = "syncbutton", origin = undefined) {
window.openPreferences("paneSync", { origin, urlParams: { entrypoint: entryPoint } });
},
openSyncedTabsPanel() {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -481,17 +481,17 @@ BrowserGlue.prototype = {
this._onVerifyLoginNotification(JSON.parse(data));
break;
case "fxaccounts:device_disconnected":
data = JSON.parse(data);
if (data.isLocalDevice) {
this._onDeviceDisconnected();
}
break;
- case "fxaccounts:messages:display-tabs":
+ case "fxaccounts:commands:open-uri":
case "weave:engine:clients:display-uris":
this._onDisplaySyncURIs(subject);
break;
case "session-save":
this._setPrefToSaveSession(true);
subject.QueryInterface(Ci.nsISupportsPRBool);
subject.data = true;
break;
@@ -636,17 +636,17 @@ BrowserGlue.prototype = {
os.addObserver(this, "browser-lastwindow-close-requested");
os.addObserver(this, "browser-lastwindow-close-granted");
}
os.addObserver(this, "weave:service:ready");
os.addObserver(this, "fxaccounts:onverified");
os.addObserver(this, "fxaccounts:device_connected");
os.addObserver(this, "fxaccounts:verify_login");
os.addObserver(this, "fxaccounts:device_disconnected");
- os.addObserver(this, "fxaccounts:messages:display-tabs");
+ os.addObserver(this, "fxaccounts:commands:open-uri");
os.addObserver(this, "weave:engine:clients:display-uris");
os.addObserver(this, "session-save");
os.addObserver(this, "places-init-complete");
os.addObserver(this, "distribution-customization-complete");
os.addObserver(this, "handle-xul-text-link");
os.addObserver(this, "profile-before-change");
os.addObserver(this, "keyword-search");
os.addObserver(this, "browser-search-engine-modified");
@@ -679,17 +679,17 @@ BrowserGlue.prototype = {
os.removeObserver(this, "browser-lastwindow-close-requested");
os.removeObserver(this, "browser-lastwindow-close-granted");
}
os.removeObserver(this, "weave:service:ready");
os.removeObserver(this, "fxaccounts:onverified");
os.removeObserver(this, "fxaccounts:device_connected");
os.removeObserver(this, "fxaccounts:verify_login");
os.removeObserver(this, "fxaccounts:device_disconnected");
- os.removeObserver(this, "fxaccounts:messages:display-tabs");
+ os.removeObserver(this, "fxaccounts:commands:open-uri");
os.removeObserver(this, "weave:engine:clients:display-uris");
os.removeObserver(this, "session-save");
if (this._bookmarksBackupIdleTime) {
this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
delete this._bookmarksBackupIdleTime;
}
if (this._lateTasksIdleObserver) {
this._idleService.removeIdleObserver(this._lateTasksIdleObserver, LATE_TASKS_IDLE_TIME_SEC);
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -21,34 +21,35 @@ ChromeUtils.defineModuleGetter(this, "Fx
"resource://gre/modules/FxAccountsConfig.jsm");
ChromeUtils.defineModuleGetter(this, "jwcrypto",
"resource://services-crypto/jwcrypto.jsm");
ChromeUtils.defineModuleGetter(this, "FxAccountsOAuthGrantClient",
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
-ChromeUtils.defineModuleGetter(this, "FxAccountsMessages",
- "resource://gre/modules/FxAccountsMessages.js");
+ChromeUtils.defineModuleGetter(this, "FxAccountsCommands",
+ "resource://gre/modules/FxAccountsCommands.js");
ChromeUtils.defineModuleGetter(this, "FxAccountsProfile",
"resource://gre/modules/FxAccountsProfile.jsm");
ChromeUtils.defineModuleGetter(this, "Utils",
"resource://services-sync/util.js");
XPCOMUtils.defineLazyPreferenceGetter(this, "FXA_ENABLED",
"identity.fxaccounts.enabled", true);
// All properties exposed by the public FxAccounts API.
var publicProperties = [
"_withCurrentAccountState", // fxaccounts package only!
"accountStatus",
"canGetKeys",
"checkVerificationStatus",
+ "commands",
"getAccountsClient",
"getAssertion",
"getDeviceId",
"getDeviceList",
"getKeys",
"getOAuthToken",
"getProfileCache",
"getPushSubscription",
@@ -56,17 +57,16 @@ var publicProperties = [
"getSignedInUserProfile",
"handleAccountDestroyed",
"handleDeviceDisconnection",
"handleEmailUpdated",
"hasLocalSession",
"invalidateCertificate",
"loadAndPoll",
"localtimeOffsetMsec",
- "messages",
"notifyDevices",
"now",
"removeCachedOAuthToken",
"resendVerificationEmail",
"resetCredentials",
"sessionStatus",
"setProfileCache",
"setSignedInUser",
@@ -407,32 +407,32 @@ FxAccountsInternal.prototype = {
this._profile = new FxAccountsProfile({
fxa: this,
profileServerUrl,
});
}
return this._profile;
},
- _messages: null,
- get messages() {
- if (!this._messages) {
- this._messages = new FxAccountsMessages(this);
+ _commands: null,
+ get commands() {
+ if (!this._commands) {
+ this._commands = new FxAccountsCommands(this);
}
- return this._messages;
+ return this._commands;
},
// A hook-point for tests who may want a mocked AccountState or mocked storage.
newAccountState(credentials) {
let storage = new FxAccountsStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
- // "Friend" classes of FxAccounts (e.g. FxAccountsMessages) know about the
+ // "Friend" classes of FxAccounts (e.g. FxAccountsCommands) know about the
// "current account state" system. This method allows them to read and write
// safely in it.
// Example of usage:
// fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
// const userData = await getUserData(['device']);
// ...
// await updateUserData({device: null});
// });
@@ -601,19 +601,19 @@ FxAccountsInternal.prototype = {
// We're telling the caller that this is durable now (although is that
// really something we should commit to? Why not let the write happen in
// the background? Already does for updateAccountData ;)
await currentAccountState.promiseInitialized;
// Starting point for polling if new user
if (!this.isUserEmailVerified(credentials)) {
this.startVerifiedCheck(credentials);
}
- await this.updateDeviceRegistration();
Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
await this.notifyObservers(ONLOGIN_NOTIFICATION);
+ await this.updateDeviceRegistration();
return currentAccountState.resolve();
},
/**
* Update account data for the currently signed in user.
*
* @param credentials
* The credentials object containing the fields to be updated.
@@ -701,27 +701,34 @@ FxAccountsInternal.prototype = {
device: {
id: data.deviceId,
registrationVersion: data.deviceRegistrationVersion
}
});
data = await this.currentAccountState.getUserAccountData();
}
const {device} = data;
- if (!device || !device.registrationVersion ||
- device.registrationVersion < this.DEVICE_REGISTRATION_VERSION) {
- // There is no device registered or the device registration is outdated.
- // Either way, we should register the device with FxA
- // before returning the id to the caller.
+ if ((await this.checkDeviceUpdateNeeded(device))) {
return this._registerOrUpdateDevice(data);
}
// Return the device id that we already registered with the server.
return device.id;
},
+ async checkDeviceUpdateNeeded(device) {
+ // There is no device registered or the device registration is outdated.
+ // Either way, we should register the device with FxA
+ // before returning the id to the caller.
+ const availableCommandsKeys = Object.keys((await this.availableCommands())).sort();
+ return !device || !device.registrationVersion ||
+ device.registrationVersion < this.DEVICE_REGISTRATION_VERSION ||
+ !device.registeredCommandsKeys ||
+ !CommonUtils.arrayEqual(device.registeredCommandsKeys, availableCommandsKeys);
+ },
+
async getDeviceList() {
const accountData = await this._getVerifiedAccountOrReject();
const devices = await this.fxAccountsClient.getDeviceList(accountData.sessionToken);
// Check if our push registration is still good.
const ourDevice = devices.find(device => device.isCurrentDevice);
if (ourDevice.pushEndpointExpired) {
await this.fxaPushService.unsubscribe();
@@ -762,18 +769,18 @@ FxAccountsInternal.prototype = {
log.debug("Polling aborted; Another user signing in");
clearTimeout(this.currentTimer);
this.currentTimer = 0;
}
if (this._profile) {
this._profile.tearDown();
this._profile = null;
}
- if (this._messages) {
- this._messages = null;
+ if (this._commands) {
+ this._commands = null;
}
// We "abort" the accountState and assume our caller is about to throw it
// away and replace it with a new one.
return this.currentAccountState.abort();
},
accountStatus: function accountStatus() {
return this.currentAccountState.getUserAccountData().then(data => {
@@ -1044,16 +1051,19 @@ FxAccountsInternal.prototype = {
DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", "));
}
await currentState.updateUserAccountData(updateData);
// We are now ready for business. This should only be invoked once
// per setSignedInUser(), regardless of whether we've rebooted since
// setSignedInUser() was called.
await this.notifyObservers(ONVERIFIED_NOTIFICATION);
+ // Some parts of the device registration depend on the Sync keys being available,
+ // so let's re-trigger it now that we have them.
+ await this.updateDeviceRegistration();
data = await currentState.getUserAccountData();
return currentState.resolve(data);
},
_deriveKeys(uid, kBbytes) {
return {
kSync: CommonUtils.bytesAsHex(this._deriveSyncKey(kBbytes)),
kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
@@ -1678,23 +1688,27 @@ FxAccountsInternal.prototype = {
});
},
// @returns Promise<Subscription>.
getPushSubscription() {
return this.fxaPushService.getSubscription();
},
- // Once FxA messages is stable, remove this, hardcode the capabilities,
- // and reset the device registration version.
- get deviceCapabilities() {
- if (Services.prefs.getBoolPref("identity.fxaccounts.messages.enabled", true)) {
- return [CAPABILITY_MESSAGES, CAPABILITY_MESSAGES_SENDTAB];
+ async availableCommands() {
+ if (!Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true)) {
+ return {};
}
- return [];
+ const sendTabKey = await this.commands.sendTab.getEncryptedKey();
+ if (!sendTabKey) { // This will happen if the account is not verified yet.
+ return {};
+ }
+ return {
+ [COMMAND_SENDTAB]: sendTabKey
+ };
},
// If you change what we send to the FxA servers during device registration,
// you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older
// devices to re-register when Firefox updates
async _registerOrUpdateDevice(signedInUser) {
const {sessionToken, device: currentDevice} = signedInUser;
if (!sessionToken) {
@@ -1711,34 +1725,38 @@ FxAccountsInternal.prototype = {
deviceOptions.pushCallback = subscription.endpoint;
let publicKey = subscription.getKey("p256dh");
let authKey = subscription.getKey("auth");
if (publicKey && authKey) {
deviceOptions.pushPublicKey = urlsafeBase64Encode(publicKey);
deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey);
}
}
- deviceOptions.capabilities = this.deviceCapabilities;
+ deviceOptions.availableCommands = await this.availableCommands();
+ const availableCommandsKeys = Object.keys(deviceOptions.availableCommands).sort();
let device;
if (currentDevice && currentDevice.id) {
log.debug("updating existing device details");
device = await this.fxAccountsClient.updateDevice(
sessionToken, currentDevice.id, deviceName, deviceOptions);
} else {
log.debug("registering new device details");
device = await this.fxAccountsClient.registerDevice(
sessionToken, deviceName, this._getDeviceType(), deviceOptions);
}
+ // Get the freshest device props before updating them.
+ let {device: deviceProps} = await this.getSignedInUser();
await this.currentAccountState.updateUserAccountData({
device: {
- ...currentDevice, // Copy the other properties (e.g. messagesIndex).
+ ...deviceProps, // Copy the other properties (e.g. handledCommands).
id: device.id,
- registrationVersion: this.DEVICE_REGISTRATION_VERSION
+ registrationVersion: this.DEVICE_REGISTRATION_VERSION,
+ registeredCommandsKeys: availableCommandsKeys,
}
});
return device.id;
} catch (error) {
return this._handleDeviceError(error, sessionToken);
}
},
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -376,16 +376,18 @@ this.FxAccountsClient.prototype = {
* @param sessionTokenHex
* Session token obtained from signIn
* @param name
* Device name
* @param type
* Device type (mobile|desktop)
* @param [options]
* Extra device options
+ * @param [options.availableCommands]
+ * Available commands for this device
* @param [options.pushCallback]
* `pushCallback` push endpoint callback
* @param [options.pushPublicKey]
* `pushPublicKey` push public key (URLSafe Base64 string)
* @param [options.pushAuthKey]
* `pushAuthKey` push auth secret (URLSafe Base64 string)
* @return Promise
* Resolves to an object:
@@ -404,16 +406,17 @@ this.FxAccountsClient.prototype = {
if (options.pushCallback) {
body.pushCallback = options.pushCallback;
}
if (options.pushPublicKey && options.pushAuthKey) {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
+ body.availableCommands = options.availableCommands;
return this._request(path, "POST", creds, body);
},
/**
* Sends a message to other devices. Must conform with the push payload schema:
* https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json
*
@@ -442,72 +445,72 @@ this.FxAccountsClient.prototype = {
if (excludedIds) {
body.excluded = excludedIds;
}
return this._request("/account/devices/notify", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
},
/**
- * Retrieves messages from our device's message box.
+ * Retrieves pending commands for our device.
*
- * @method getMessages
+ * @method getCommands
* @param sessionTokenHex - Session token obtained from signIn
* @param [index] - If specified, only messages received after the one who
* had that index will be retrieved.
* @param [limit] - Maximum number of messages to retrieve.
*/
- getMessages(sessionTokenHex, {index, limit}) {
+ getCommands(sessionTokenHex, {index, limit}) {
const params = new URLSearchParams();
if (index != undefined) {
params.set("index", index);
}
if (limit != undefined) {
params.set("limit", limit);
}
- const path = `/account/device/messages?${params.toString()}`;
+ const path = `/account/device/commands?${params.toString()}`;
return this._request(path, "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
},
/**
- * Stores a message in the recipient's message box.
+ * Invokes a command on another device.
*
- * @method sendMessage
+ * @method invokeCommand
* @param sessionTokenHex - Session token obtained from signIn
- * @param topic
- * @param to - Recipient device ID.
- * @param data
+ * @param command - Name of the command to invoke
+ * @param target - Recipient device ID.
+ * @param payload
* @return Promise
* Resolves to the request's response, (which should be an empty object)
*/
- sendMessage(sessionTokenHex, topic, to, data) {
+ invokeCommand(sessionTokenHex, command, target, payload) {
const body = {
- topic,
- to,
- data
+ command,
+ target,
+ payload
};
- return this._request("/account/devices/messages", "POST",
+ return this._request("/account/devices/invoke_command", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
},
/**
* Update the session or name for an existing device
*
* @method updateDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param id
* Device identifier
* @param name
* Device name
* @param [options]
* Extra device options
- * @param options.capabilities
- * Device capabilities
+ * @param [options.availableCommands]
+ * Available commands for this device
* @param [options.pushCallback]
* `pushCallback` push endpoint callback
* @param [options.pushPublicKey]
* `pushPublicKey` push public key (URLSafe Base64 string)
* @param [options.pushAuthKey]
* `pushAuthKey` push auth secret (URLSafe Base64 string)
* @return Promise
* Resolves to an object:
@@ -523,17 +526,17 @@ this.FxAccountsClient.prototype = {
let body = { id, name };
if (options.pushCallback) {
body.pushCallback = options.pushCallback;
}
if (options.pushPublicKey && options.pushAuthKey) {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
- body.capabilities = options.capabilities;
+ body.availableCommands = options.availableCommands;
return this._request(path, "POST", creds, body);
},
/**
* Get a list of currently registered devices
*
* @method getDeviceList
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsCommands.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["SendTab", "FxAccountsCommands"];
+
+ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+ChromeUtils.import("resource://gre/modules/Preferences.jsm");
+ChromeUtils.defineModuleGetter(this, "PushCrypto",
+ "resource://gre/modules/PushCrypto.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://services-common/observers.js");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BulkKeyBundle: "resource://services-sync/keys.js",
+ CommonUtils: "resource://services-common/utils.js",
+ CryptoUtils: "resource://services-crypto/utils.js",
+ CryptoWrapper: "resource://services-sync/record.js",
+});
+
+class FxAccountsCommands {
+ constructor(fxAccounts) {
+ this._fxAccounts = fxAccounts;
+ this.sendTab = new SendTab(this, fxAccounts);
+ }
+
+ async invoke(command, device, payload) {
+ const userData = await this._fxAccounts.getSignedInUser();
+ if (!userData) {
+ throw new Error("No user.");
+ }
+ const {sessionToken} = userData;
+ if (!sessionToken) {
+ throw new Error("_send called without a session token.");
+ }
+ const client = this._fxAccounts.getAccountsClient();
+ await client.invokeCommand(sessionToken, command, device.id, payload);
+ log.info(`Payload sent to device ${device.id}.`);
+ }
+
+ async consumeRemoteCommand(index) {
+ if (!Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true)) {
+ return false;
+ }
+ log.info(`Consuming command with index ${index}.`);
+ const {messages} = await this._fetchRemoteCommands(index, 1);
+ if (messages.length != 1) {
+ log.warn(`Should have retrieved 1 and only 1 message, got ${messages.length}.`);
+ }
+ return this._fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
+ const {device} = await getUserData(["device"]);
+ if (!device) {
+ throw new Error("No device registration.");
+ }
+ const handledCommands = (device.handledCommands || []).concat(messages.map(m => m.index));
+ await updateUserData({
+ device: {...device, handledCommands}
+ });
+ await this._handleCommands(messages);
+
+ // Once the handledCommands array length passes a threshold, check the
+ // potentially missed remote commands in order to clear it.
+ if (handledCommands.length > 20) {
+ await this.fetchMissedRemoteCommands();
+ }
+ });
+ }
+
+ fetchMissedRemoteCommands() {
+ if (!Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true)) {
+ return false;
+ }
+ log.info(`Consuming missed commands.`);
+ return this._fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
+ const {device} = await getUserData(["device"]);
+ if (!device) {
+ throw new Error("No device registration.");
+ }
+ const lastCommandIndex = device.lastCommandIndex || 0;
+ const handledCommands = device.handledCommands || [];
+ handledCommands.push(lastCommandIndex); // Because the server also returns this command.
+ const {index, messages} = await this._fetchRemoteCommands(lastCommandIndex);
+ const missedMessages = messages.filter(m => !handledCommands.includes(m.index));
+ await updateUserData({
+ device: {...device, lastCommandIndex: index, handledCommands: []}
+ });
+ if (missedMessages.length) {
+ log.info(`Handling ${missedMessages.length} missed messages`);
+ await this._handleCommands(missedMessages);
+ }
+ });
+ }
+
+ async _fetchRemoteCommands(index, limit = null) {
+ const userData = await this._fxAccounts.getSignedInUser();
+ if (!userData) {
+ throw new Error("No user.");
+ }
+ const {sessionToken} = userData;
+ if (!sessionToken) {
+ throw new Error("No session token.");
+ }
+ const client = this._fxAccounts.getAccountsClient();
+ const opts = {index};
+ if (limit != null) {
+ opts.limit = limit;
+ }
+ return client.getCommands(sessionToken, opts);
+ }
+
+ async _handleCommands(messages) {
+ const fxaDevices = await this._fxAccounts.getDeviceList();
+ for (const {data} of messages) {
+ let {command, payload, sender} = data;
+ if (sender) {
+ sender = fxaDevices.find(d => d.id == sender);
+ }
+ switch (command) {
+ case COMMAND_SENDTAB:
+ try {
+ await this.sendTab.handle(sender, payload);
+ } catch (e) {
+ log.error(`Error while handling incoming Send Tab payload.`, e);
+ }
+ default:
+ log.info(`Unknown command: ${command}.`);
+ }
+ }
+ }
+}
+
+/**
+ * Send Tab is built on top of FxA commands.
+ *
+ * Devices exchange keys wrapped in kSync between themselves (getEncryptedKey)
+ * during the device registration flow. The FxA server can theorically never
+ * retrieve the send tab keys since it doesn't know kSync.
+ */
+class SendTab {
+ constructor(commands, fxAccounts) {
+ this._commands = commands;
+ this._fxAccounts = fxAccounts;
+ }
+ /**
+ * @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()).
+ * @param {Object} tab
+ * @param {string} tab.url
+ * @param {string} tab.title
+ * @returns A report object, in the shape of
+ * {succeded: [Device], error: [{device: Device, error: Exception}]}
+ */
+ async send(to, tab) {
+ log.info(`Sending a tab to ${to.length} devices.`);
+ const encoder = new TextEncoder("utf8");
+ const data = {
+ entries: [{title: tab.title, url: tab.url}]
+ };
+ const bytes = encoder.encode(JSON.stringify(data));
+ const report = {
+ succeeded: [],
+ failed: [],
+ };
+ for (let device of to) {
+ try {
+ const encrypted = await this._encrypt(bytes, device);
+ const payload = {encrypted};
+ await this._commands.invoke(COMMAND_SENDTAB, device, payload); // FxA needs an object.
+ report.succeeded.push(device);
+ } catch (error) {
+ log.error("Error while invoking a send tab command.", error);
+ report.failed.push({device, error});
+ }
+ }
+ return report;
+ }
+
+ // Returns true if the target device is compatible with FxA Commands Send tab.
+ async isDeviceCompatible(device) {
+ if (!Services.prefs.getBoolPref("identity.fxaccounts.commands.enabled", true) ||
+ !device.availableCommands || !device.availableCommands[COMMAND_SENDTAB]) {
+ return false;
+ }
+ const {kid: theirKid} = JSON.parse(device.availableCommands[COMMAND_SENDTAB]);
+ const ourKid = await this._getKid();
+ return theirKid == ourKid;
+ }
+
+ // Handle incoming send tab payload, called by FxAccountsCommands.
+ async handle(sender, {encrypted}) {
+ if (!sender) {
+ log.warn("Incoming tab is from an unknown device (maybe disconnected?)");
+ }
+ const bytes = await this._decrypt(encrypted);
+ const decoder = new TextDecoder("utf8");
+ const data = JSON.parse(decoder.decode(bytes));
+ const current = data.hasOwnProperty("current") ? data.current :
+ data.entries.length - 1;
+ const tabSender = {
+ id: sender ? sender.id : "",
+ name: sender ? sender.name : ""
+ };
+ const {title, url: uri} = data.entries[current];
+ console.log(`Tab received with FxA commands: ${title} from ${tabSender.name}.`);
+ Observers.notify("fxaccounts:commands:open-uri", [{uri, title, sender: tabSender}]);
+ }
+
+ async _getKid() {
+ let {kXCS} = await this._fxAccounts.getKeys();
+ return kXCS;
+ }
+
+ async _encrypt(bytes, device) {
+ let bundle = device.availableCommands[COMMAND_SENDTAB];
+ if (!bundle) {
+ throw new Error(`Device ${device.id} does not have send tab keys.`);
+ }
+ const json = JSON.parse(bundle);
+ const wrapper = new CryptoWrapper();
+ wrapper.deserialize({payload: json});
+ const {kSync} = await this._fxAccounts.getKeys();
+ const syncKeyBundle = BulkKeyBundle.fromHexKey(kSync);
+ let {publicKey, authSecret} = await wrapper.decrypt(syncKeyBundle);
+ authSecret = urlsafeBase64Decode(authSecret);
+ publicKey = urlsafeBase64Decode(publicKey);
+
+ const {ciphertext: encrypted} = await PushCrypto.encrypt(bytes, publicKey, authSecret);
+ return urlsafeBase64Encode(encrypted);
+ }
+
+ async _getKeys() {
+ const {device} = await this._fxAccounts.getSignedInUser();
+ return device && device.sendTabKeys;
+ }
+
+ async _decrypt(ciphertext) {
+ let {privateKey, publicKey, authSecret} = await this._getKeys();
+ publicKey = urlsafeBase64Decode(publicKey);
+ authSecret = urlsafeBase64Decode(authSecret);
+ ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext));
+ return PushCrypto.decrypt(privateKey, publicKey, authSecret,
+ // The only Push encoding we support.
+ {encoding: "aes128gcm"}, ciphertext);
+ }
+
+ async _generateAndPersistKeys() {
+ let [publicKey, privateKey] = await PushCrypto.generateKeys();
+ publicKey = urlsafeBase64Encode(publicKey);
+ let authSecret = PushCrypto.generateAuthenticationSecret();
+ authSecret = urlsafeBase64Encode(authSecret);
+ const sendTabKeys = {
+ publicKey,
+ privateKey,
+ authSecret
+ };
+ await this._fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
+ const {device} = await getUserData();
+ await updateUserData({
+ device: {
+ ...device,
+ sendTabKeys,
+ }
+ });
+ });
+ return sendTabKeys;
+ }
+
+ async getEncryptedKey() {
+ let sendTabKeys = await this._getKeys();
+ if (!sendTabKeys) {
+ sendTabKeys = await this._generateAndPersistKeys();
+ }
+ // Strip the private key from the bundle to encrypt.
+ const keyToEncrypt = {
+ publicKey: sendTabKeys.publicKey,
+ authSecret: sendTabKeys.authSecret,
+ };
+ const {kSync} = await this._fxAccounts.getSignedInUser();
+ if (!kSync) {
+ return null;
+ }
+ const wrapper = new CryptoWrapper();
+ wrapper.cleartext = keyToEncrypt;
+ const keyBundle = BulkKeyBundle.fromHexKey(kSync);
+ await wrapper.encrypt(keyBundle);
+ const kid = await this._getKid();
+ return JSON.stringify({
+ kid,
+ IV: wrapper.IV,
+ hmac: wrapper.hmac,
+ ciphertext: wrapper.ciphertext,
+ });
+ }
+}
+
+function urlsafeBase64Encode(buffer) {
+ return ChromeUtils.base64URLEncode(new Uint8Array(buffer), {pad: false});
+}
+
+function urlsafeBase64Decode(str) {
+ return ChromeUtils.base64URLDecode(str, {padding: "reject"});
+}
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -63,24 +63,24 @@ exports.ONLOGOUT_NOTIFICATION = "fxaccou
exports.ON_DEVICE_CONNECTED_NOTIFICATION = "fxaccounts:device_connected";
exports.ON_DEVICE_DISCONNECTED_NOTIFICATION = "fxaccounts:device_disconnected";
exports.ON_PROFILE_UPDATED_NOTIFICATION = "fxaccounts:profile_updated"; // Push
exports.ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed";
exports.ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset";
exports.ON_ACCOUNT_DESTROYED_NOTIFICATION = "fxaccounts:account_destroyed";
exports.ON_COLLECTION_CHANGED_NOTIFICATION = "sync:collection_changed";
exports.ON_VERIFY_LOGIN_NOTIFICATION = "fxaccounts:verify_login";
+exports.ON_COMMAND_RECEIVED_NOTIFICATION = "fxaccounts:command_received";
exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; // WebChannel
exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange";
-exports.CAPABILITY_MESSAGES = "messages";
-exports.CAPABILITY_MESSAGES_SENDTAB = "messages.sendtab";
+exports.COMMAND_SENDTAB = "https://identity.mozilla.com/cmd/open-uri";
// UI Requests.
exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
// The OAuth client ID for Firefox Desktop
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
deleted file mode 100644
--- a/services/fxaccounts/FxAccountsMessages.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const EXPORTED_SYMBOLS = ["FxAccountsMessages", /* the rest are for testing only */
- "FxAccountsMessagesSender", "FxAccountsMessagesReceiver",
- "FxAccountsMessagesHandler"];
-
-ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
-ChromeUtils.import("resource://gre/modules/Preferences.jsm");
-ChromeUtils.defineModuleGetter(this, "PushCrypto",
- "resource://gre/modules/PushCrypto.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.import("resource://services-common/observers.js");
-
-const AES128GCM_ENCODING = "aes128gcm"; // The only Push encoding we support.
-const TOPICS = {
- SEND_TAB: "sendtab"
-};
-
-class FxAccountsMessages {
- constructor(fxAccounts, options = {}) {
- this.fxAccounts = fxAccounts;
- this.sender = options.sender || new FxAccountsMessagesSender(fxAccounts);
- this.receiver = options.receiver || new FxAccountsMessagesReceiver(fxAccounts);
- }
-
- _isDeviceMessagesAware(device) {
- return device.capabilities && device.capabilities.includes(CAPABILITY_MESSAGES);
- }
-
- canReceiveSendTabMessages(device) {
- return this._isDeviceMessagesAware(device) &&
- device.capabilities.includes(CAPABILITY_MESSAGES_SENDTAB);
- }
-
- consumeRemoteMessages() {
- if (!Services.prefs.getBoolPref("identity.fxaccounts.messages.enabled", true)) {
- return Promise.resolve();
- }
- return this.receiver.consumeRemoteMessages();
- }
-
- /**
- * @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()).
- * @param {Object} tab
- * @param {string} tab.url
- * @param {string} tab.title
- */
- async sendTab(to, tab) {
- log.info(`Sending a tab to ${to.length} devices.`);
- const ourDeviceId = await this.fxAccounts.getDeviceId();
- const payload = {
- topic: TOPICS.SEND_TAB,
- data: {
- from: ourDeviceId,
- entries: [{title: tab.title, url: tab.url}]
- }
- };
- return this.sender.send(TOPICS.SEND_TAB, to, payload);
- }
-}
-
-class FxAccountsMessagesSender {
- constructor(fxAccounts) {
- this.fxAccounts = fxAccounts;
- }
-
- async send(topic, to, data) {
- const userData = await this.fxAccounts.getSignedInUser();
- if (!userData) {
- throw new Error("No user.");
- }
- const {sessionToken} = userData;
- if (!sessionToken) {
- throw new Error("_send called without a session token.");
- }
- const encoder = new TextEncoder("utf8");
- const client = this.fxAccounts.getAccountsClient();
- for (const device of to) {
- try {
- const bytes = encoder.encode(JSON.stringify(data));
- const payload = await this._encrypt(bytes, device, encoder);
- await client.sendMessage(sessionToken, topic, device.id, payload);
- log.info(`Payload sent to device ${device.id}.`);
- } catch (e) {
- log.error(`Could not send data to device ${device.id}.`, e);
- }
- }
- }
-
- async _encrypt(bytes, device) {
- let {pushPublicKey, pushAuthKey} = device;
- if (!pushPublicKey || !pushAuthKey) {
- throw new Error(`Device ${device.id} does not have push keys.`);
- }
- pushPublicKey = ChromeUtils.base64URLDecode(pushPublicKey, {padding: "ignore"});
- pushAuthKey = ChromeUtils.base64URLDecode(pushAuthKey, {padding: "ignore"});
- const {ciphertext} = await PushCrypto.encrypt(bytes, pushPublicKey, pushAuthKey);
- return ChromeUtils.base64URLEncode(ciphertext, {pad: false});
- }
-}
-
-class FxAccountsMessagesReceiver {
- constructor(fxAccounts, options = {}) {
- this.fxAccounts = fxAccounts;
- this.handler = options.handler || new FxAccountsMessagesHandler(this.fxAccounts);
- }
-
- async consumeRemoteMessages() {
- log.info(`Consuming unread messages.`);
- const messages = await this._fetchMessages();
- if (!messages || !messages.length) {
- log.info(`No new messages.`);
- return;
- }
- const decoder = new TextDecoder("utf8");
- const keys = await this._getOwnKeys();
- const payloads = [];
- for (const {index, data} of messages) {
- try {
- const bytes = await this._decrypt(data, keys);
- const payload = JSON.parse(decoder.decode(bytes));
- payloads.push(payload);
- } catch (e) {
- log.error(`Could not unwrap message ${index}`, e);
- }
- }
- if (payloads.length) {
- await this.handler.handle(payloads);
- }
- }
-
- async _fetchMessages() {
- return this.fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
- const userData = await getUserData(["sessionToken", "device"]);
- if (!userData) {
- throw new Error("No user.");
- }
- const {sessionToken, device} = userData;
- if (!sessionToken) {
- throw new Error("No session token.");
- }
- if (!device) {
- throw new Error("No device registration.");
- }
- const opts = {};
- if (device.messagesIndex) {
- opts.index = device.messagesIndex;
- }
- const client = this.fxAccounts.getAccountsClient();
- log.info(`Fetching unread messages with ${JSON.stringify(opts)}.`);
- const {index: newIndex, messages} = await client.getMessages(sessionToken, opts);
- await updateUserData({
- device: {...device, messagesIndex: newIndex}
- });
- return messages;
- });
- }
-
- async _getOwnKeys() {
- const subscription = await this.fxAccounts.getPushSubscription();
- return {
- pushPrivateKey: subscription.p256dhPrivateKey,
- pushPublicKey: new Uint8Array(subscription.getKey("p256dh")),
- pushAuthKey: new Uint8Array(subscription.getKey("auth"))
- };
- }
-
- async _decrypt(ciphertext, {pushPrivateKey, pushPublicKey, pushAuthKey}) {
- ciphertext = ChromeUtils.base64URLDecode(ciphertext, {padding: "reject"});
- return PushCrypto.decrypt(pushPrivateKey, pushPublicKey,
- pushAuthKey,
- {encoding: AES128GCM_ENCODING},
- ciphertext);
- }
-}
-
-class FxAccountsMessagesHandler {
- constructor(fxAccounts) {
- this.fxAccounts = fxAccounts;
- }
-
- async handle(payloads) {
- const sendTabPayloads = [];
- for (const payload of payloads) {
- switch (payload.topic) {
- case TOPICS.SEND_TAB:
- sendTabPayloads.push(payload.data);
- default:
- log.info(`Unknown messages topic: ${payload.topic}.`);
- }
- }
-
- // Only one type of payload so far!
- if (sendTabPayloads.length) {
- await this._handleSendTabPayloads(sendTabPayloads);
- }
- }
-
- async _handleSendTabPayloads(payloads) {
- const toDisplay = [];
- const fxaDevices = await this.fxAccounts.getDeviceList();
- for (const payload of payloads) {
- const current = payload.hasOwnProperty("current") ? payload.current :
- payload.entries.length - 1;
- const device = fxaDevices.find(d => d.id == payload.from);
- if (!device) {
- log.warn("Incoming tab is from an unknown device (maybe disconnected?)");
- }
- const sender = {
- id: device ? device.id : "",
- name: device ? device.name : ""
- };
- const {title, url: uri} = payload.entries[current];
- toDisplay.push({uri, title, sender});
- }
-
- Observers.notify("fxaccounts:messages:display-tabs", toDisplay);
- }
-}
-
--- a/services/fxaccounts/FxAccountsPush.js
+++ b/services/fxaccounts/FxAccountsPush.js
@@ -2,16 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://services-sync/util.js");
ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
/**
* FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
*
* @param [options]
* Object, custom options that used for testing
* @constructor
*/
function FxAccountsPushService(options = {}) {
@@ -153,23 +155,21 @@ FxAccountsPushService.prototype = {
this.log.trace("FxAccountsPushService _onPushMessage");
if (!message.data) {
// Use the empty signal to check the verification state of the account right away
this.log.debug("empty push message - checking account status");
this.fxAccounts.checkVerificationStatus();
return;
}
let payload = message.data.json();
- if (payload.topic) {
- this.log.debug(`received messages tickle with topic ${payload.topic}`);
- this.fxAccounts.messages.consumeRemoteMessages();
- return;
- }
this.log.debug(`push command: ${payload.command}`);
switch (payload.command) {
+ case ON_COMMAND_RECEIVED_NOTIFICATION:
+ this.fxAccounts.commands.consumeRemoteCommand(payload.data.index);
+ break;
case ON_DEVICE_CONNECTED_NOTIFICATION:
Services.obs.notifyObservers(null, ON_DEVICE_CONNECTED_NOTIFICATION, payload.data.deviceName);
break;
case ON_DEVICE_DISCONNECTED_NOTIFICATION:
this.fxAccounts.handleDeviceDisconnection(payload.data.id);
return;
case ON_PROFILE_UPDATED_NOTIFICATION:
// We already have a "profile updated" notification sent via WebChannel,
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -411,17 +411,17 @@ this.FxAccountsWebChannelHelpers.prototy
try {
return Services.prefs.getBoolPref(`services.sync.engine.${engineName}.available`);
} catch (e) {
return false;
}
});
},
- changePassword(credentials) {
+ async changePassword(credentials) {
// If |credentials| has fields that aren't handled by accounts storage,
// updateUserAccountData will throw - mainly to prevent errors in code
// that hard-codes field names.
// However, in this case the field names aren't really in our control.
// We *could* still insist the server know what fields names are valid,
// but that makes life difficult for the server when Firefox adds new
// features (ie, new fields) - forcing the server to track a map of
// versions to supported field names doesn't buy us much.
@@ -431,18 +431,25 @@ this.FxAccountsWebChannelHelpers.prototy
};
for (let name of Object.keys(credentials)) {
if (name == "email" || name == "uid" || FxAccountsStorageManagerCanStoreField(name)) {
newCredentials[name] = credentials[name];
} else {
log.info("changePassword ignoring unsupported field", name);
}
}
- return this._fxAccounts.updateUserAccountData(newCredentials)
- .then(() => this._fxAccounts.updateDeviceRegistration());
+ await this._fxAccounts.updateUserAccountData(newCredentials);
+ // Force the keys derivation, to be able to register a send-tab command
+ // in updateDeviceRegistration.
+ try {
+ await this._fxAccounts.getKeys();
+ } catch (e) {
+ log.error("getKeys errored", e);
+ }
+ await this._fxAccounts.updateDeviceRegistration();
},
/**
* Get the hash of account name of the previously signed in account
*/
getPreviousAccountNameHashPref() {
try {
return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -19,17 +19,17 @@ EXTRA_COMPONENTS += [
'FxAccountsComponents.manifest',
'FxAccountsPush.js',
]
EXTRA_JS_MODULES += [
'Credentials.jsm',
'FxAccounts.jsm',
'FxAccountsClient.jsm',
+ 'FxAccountsCommands.js',
'FxAccountsCommon.js',
'FxAccountsConfig.jsm',
- 'FxAccountsMessages.js',
'FxAccountsOAuthGrantClient.jsm',
'FxAccountsProfile.jsm',
'FxAccountsProfileClient.jsm',
'FxAccountsStorage.jsm',
'FxAccountsWebChannel.jsm',
]
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -97,16 +97,19 @@ function MockFxAccounts(device = {}) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
_getDeviceName() {
return device.name || "mock device name";
},
+ async availableCommands() {
+ return {};
+ },
fxAccountsClient: new MockFxAccountsClient(device),
fxaPushService: {
registerPushEndpoint() {
return new Promise((resolve) => {
resolve({
endpoint: "http://mochi.test:8888",
getKey(type) {
return ChromeUtils.base64URLDecode(
@@ -185,16 +188,17 @@ add_task(async function test_updateDevic
const deviceId = "my device id";
const deviceName = "phil's device";
const credentials = getTestUser("pb");
const fxa = new MockFxAccounts({ name: deviceName });
await fxa.internal.setSignedInUser(credentials);
await fxa.updateUserAccountData({uid: credentials.uid, device: {
id: deviceId,
+ registeredCommandsKeys: [],
registrationVersion: 1 // < 42
}});
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
@@ -242,16 +246,17 @@ add_task(async function test_updateDevic
const deviceType = "bar";
const currentDeviceId = "my device id";
const credentials = getTestUser("baz");
const fxa = new MockFxAccounts({ name: deviceName });
await fxa.internal.setSignedInUser(credentials);
await fxa.updateUserAccountData({uid: credentials.uid, device: {
id: currentDeviceId,
+ registeredCommandsKeys: [],
registrationVersion: 1 // < 42
}});
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
@@ -306,16 +311,17 @@ add_task(async function test_updateDevic
const currentDeviceId = "my device id";
const conflictingDeviceId = "conflicting device id";
const credentials = getTestUser("baz");
const fxa = new MockFxAccounts({ name: deviceName });
await fxa.internal.setSignedInUser(credentials);
await fxa.updateUserAccountData({uid: credentials.uid, device: {
id: currentDeviceId,
+ registeredCommandsKeys: [],
registrationVersion: 1 // < 42
}});
const spy = {
registerDevice: { count: 0, args: [] },
updateDevice: { count: 0, args: [], times: [] },
getDeviceList: { count: 0, args: [] }
};
@@ -448,17 +454,17 @@ add_task(async function test_getDeviceId
add_task(async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
const fxa = new MockFxAccounts();
await fxa.internal.setSignedInUser(credentials);
const spy = { count: 0, args: [] };
fxa.internal.currentAccountState.getUserAccountData =
- () => Promise.resolve({ device: {id: "my id", registrationVersion: 0}});
+ () => Promise.resolve({ device: {id: "my id", registrationVersion: 0, registeredCommandsKeys: []}});
fxa.internal._registerOrUpdateDevice = function() {
spy.count += 1;
spy.args.push(arguments);
return Promise.resolve("wibble");
};
const result = await fxa.internal.getDeviceId();
@@ -471,17 +477,17 @@ add_task(async function test_getDeviceId
add_task(async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() {
const credentials = getTestUser("foo");
credentials.verified = true;
const fxa = new MockFxAccounts();
await fxa.internal.setSignedInUser(credentials);
const spy = { count: 0 };
fxa.internal.currentAccountState.getUserAccountData =
- () => Promise.resolve({ device: {id: "foo's device id", registrationVersion: DEVICE_REGISTRATION_VERSION}});
+ async () => ({ device: {id: "foo's device id", registrationVersion: DEVICE_REGISTRATION_VERSION, registeredCommandsKeys: []}});
fxa.internal._registerOrUpdateDevice = function() {
spy.count += 1;
return Promise.resolve("bar");
};
const result = await fxa.internal.getDeviceId();
Assert.equal(spy.count, 0);
@@ -522,29 +528,30 @@ add_task(async function test_migration_t
accountData.deviceId = "mydeviceid";
accountData.deviceRegistrationVersion = DEVICE_REGISTRATION_VERSION;
const result = await fxa.internal.getDeviceId();
Assert.equal(result, "mydeviceid");
const state = fxa.internal.currentAccountState;
const data = await state.getUserAccountData();
- Assert.deepEqual(data.device, {id: "mydeviceid", registrationVersion: DEVICE_REGISTRATION_VERSION});
+ Assert.deepEqual(data.device, {id: "mydeviceid", registrationVersion: DEVICE_REGISTRATION_VERSION, registeredCommandsKeys: []});
Assert.ok(!data.deviceId);
Assert.ok(!data.deviceRegistrationVersion);
});
add_task(async function test_devicelist_pushendpointexpired() {
const deviceId = "mydeviceid";
const credentials = getTestUser("baz");
credentials.verified = true;
const fxa = new MockFxAccounts();
await fxa.internal.setSignedInUser(credentials);
await fxa.updateUserAccountData({uid: credentials.uid, device: {
id: deviceId,
+ registeredCommandsKeys: [],
registrationVersion: 1 // < 42
}});
const spy = {
updateDevice: { count: 0, args: [] },
getDeviceList: { count: 0, args: [] }
};
const client = fxa.internal.fxAccountsClient;
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_commands.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://testing-common/Assert.jsm");
+ChromeUtils.import("resource://gre/modules/FxAccountsCommands.js");
+
+add_task(async function test_sendtab_isDeviceCompatible() {
+ const fxAccounts = {
+ getKeys() {
+ return {
+ kXCS: "abcd"
+ };
+ }
+ };
+ const sendTab = new SendTab(null, fxAccounts);
+ let device = {name: "My device"};
+ Assert.ok(!(await sendTab.isDeviceCompatible(device)));
+ device = {name: "My device", availableCommands: {}};
+ Assert.ok(!(await sendTab.isDeviceCompatible(device)));
+ device = {name: "My device", availableCommands: {
+ "https://identity.mozilla.com/cmd/open-uri": JSON.stringify({
+ kid: "dcba"
+ })
+ }};
+ Assert.ok(!(await sendTab.isDeviceCompatible(device)));
+ device = {name: "My device", availableCommands: {
+ "https://identity.mozilla.com/cmd/open-uri": JSON.stringify({
+ kid: "abcd"
+ })
+ }};
+ Assert.ok((await sendTab.isDeviceCompatible(device)));
+});
+
+add_task(async function test_sendtab_send() {
+ const commands = {
+ invoke: sinon.spy((cmd, device, payload) => {
+ if (device.name == "Device 1") {
+ throw new Error("Invoke error!");
+ }
+ Assert.equal(payload.encrypted, "encryptedpayload");
+ })
+ };
+ const sendTab = new SendTab(commands, null);
+ sendTab._encrypt = (bytes, device) => {
+ if (device.name == "Device 2") {
+ throw new Error("Encrypt error!");
+ }
+ return "encryptedpayload";
+ };
+ const to = [
+ {name: "Device 1"},
+ {name: "Device 2"},
+ {name: "Device 3"},
+ ];
+ const tab = {title: "Foo", url: "https://foo.bar/"};
+ const report = await sendTab.send(to, tab);
+ Assert.equal(report.succeeded.length, 1);
+ Assert.equal(report.failed.length, 2);
+ Assert.equal(report.succeeded[0].name, "Device 3");
+ Assert.equal(report.failed[0].device.name, "Device 1");
+ Assert.equal(report.failed[0].error.message, "Invoke error!");
+ Assert.equal(report.failed[1].device.name, "Device 2");
+ Assert.equal(report.failed[1].error.message, "Encrypt error!");
+ Assert.ok(commands.invoke.calledTwice);
+});
+
+add_task(async function test_commands_fetchMissedRemoteCommands() {
+ const accountState = {
+ data: {
+ device: {
+ handledCommands: [8, 9, 10, 11],
+ lastCommandIndex: 11,
+ }
+ }
+ };
+ const fxAccounts = {
+ async _withCurrentAccountState(cb) {
+ const get = () => accountState.data;
+ const set = (val) => { accountState.data = val; };
+ await cb(get, set);
+ }
+ };
+ const commands = new FxAccountsCommands(fxAccounts);
+ commands._fetchRemoteCommands = () => {
+ return {
+ index: 12,
+ messages: [
+ {
+ index: 11,
+ data: {}
+ },
+ {
+ index: 12,
+ data: {}
+ }
+ ]
+ };
+ };
+ commands._handleCommands = sinon.spy();
+ await commands.fetchMissedRemoteCommands();
+
+ Assert.equal(accountState.data.device.handledCommands.length, 0);
+ Assert.equal(accountState.data.device.lastCommandIndex, 12);
+ const callArgs = commands._handleCommands.args[0][0];
+ Assert.equal(callArgs[0].index, 12);
+});
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -43,16 +43,17 @@ function createFxAccounts() {
async registerDevice() {
return { id: "deviceAAAAAA" };
},
async recoveryEmailStatus() {
return { verified: true };
},
async signOut() {},
},
+ updateDeviceRegistration() {},
_getDeviceName() {
return "mock device name";
},
observerPreloads: [],
fxaPushService: {
async registerPushEndpoint() {
return {
endpoint: "http://mochi.test:8888",
deleted file mode 100644
--- a/services/fxaccounts/tests/xpcshell/test_messages.js
+++ /dev/null
@@ -1,210 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-ChromeUtils.import("resource://testing-common/Assert.jsm");
-ChromeUtils.import("resource://gre/modules/FxAccountsMessages.js");
-
-add_task(async function test_sendTab() {
- const fxAccounts = {
- async getDeviceId() {
- return "my-device-id";
- }
- };
- const sender = {
- send: sinon.spy()
- };
- const fxAccountsMessages = new FxAccountsMessages(fxAccounts, {sender});
-
- const to = [{
- id: "deviceid-1",
- pushPublicKey: "pubkey-1",
- pushAuthKey: "authkey-1"
- }];
- const tab = {url: "https://foo.com", title: "Foo"};
- await fxAccountsMessages.sendTab(to, tab);
- Assert.ok(sender.send.calledOnce);
- Assert.equal(sender.send.args[0][0], "sendtab");
- Assert.deepEqual(sender.send.args[0][1], to);
- Assert.deepEqual(sender.send.args[0][2], {
- topic: "sendtab",
- data: {
- from: "my-device-id",
- entries: [{title: "Foo", url: "https://foo.com"}]
- }
- });
-});
-
-add_task(async function test_consumeRemoteMessages() {
- const fxAccounts = {};
- const receiver = {
- consumeRemoteMessages: sinon.spy()
- };
- const fxAccountsMessages = new FxAccountsMessages(fxAccounts, {receiver});
- fxAccountsMessages.consumeRemoteMessages();
- Assert.ok(receiver.consumeRemoteMessages.calledOnce);
-});
-
-add_task(async function test_canReceiveSendTabMessages() {
- const fxAccounts = {};
- const messages = new FxAccountsMessages(fxAccounts);
- Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1"}));
- Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: []}));
- Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: ["messages"]}));
- Assert.ok(messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: ["messages", "messages.sendtab"]}));
-});
-
-add_task(async function test_sender_send() {
- const sandbox = sinon.sandbox.create();
- const fxaClient = {
- sendMessage: sinon.spy()
- };
- const sessionToken = "toktok";
- const fxAccounts = {
- async getSignedInUser() {
- return {sessionToken};
- },
- getAccountsClient() {
- return fxaClient;
- }
- };
- const sender = new FxAccountsMessagesSender(fxAccounts);
- sandbox.stub(sender, "_encrypt").callsFake((_, device) => {
- if (device.pushPublicKey == "pubkey-1") {
- return "encrypted-text-1";
- }
- return "encrypted-text-2";
- });
-
- const topic = "mytopic";
- const to = [{
- id: "deviceid-1",
- pushPublicKey: "pubkey-1",
- pushAuthKey: "authkey-1"
- }, {
- id: "deviceid-2",
- pushPublicKey: "pubkey-2",
- pushAuthKey: "authkey-2"
- }];
- const payload = {foo: "bar"};
-
- await sender.send(topic, to, payload);
-
- Assert.ok(fxaClient.sendMessage.calledTwice);
- const checkCallArgs = (callNum, deviceId, encrypted) => {
- Assert.equal(fxaClient.sendMessage.args[callNum][0], sessionToken);
- Assert.equal(fxaClient.sendMessage.args[callNum][1], topic);
- Assert.equal(fxaClient.sendMessage.args[callNum][2], deviceId);
- Assert.equal(fxaClient.sendMessage.args[callNum][3], encrypted);
- };
- checkCallArgs(0, "deviceid-1", "encrypted-text-1");
- checkCallArgs(1, "deviceid-2", "encrypted-text-2");
- sandbox.restore();
-});
-
-add_task(async function test_receiver_consumeRemoteMessages() {
- const fxaClient = {
- getMessages: sinon.spy(async () => {
- return {
- index: "idx-2",
- messages: [{
- index: "idx-1",
- data: "#giberish#"
- }, {
- index: "idx-2",
- data: "#encrypted#"
- }]
- };
- })
- };
- const fxAccounts = {
- accountState: {sessionToken: "toktok", device: {}},
- _withCurrentAccountState(fun) {
- const get = () => this.accountState;
- const update = (obj) => { this.accountState = {...this.accountState, ...obj}; };
- return fun(get, update);
- },
- getAccountsClient() {
- return fxaClient;
- }
- };
- const sandbox = sinon.sandbox.create();
- const messagesHandler = {
- handle: sinon.spy()
- };
- const receiver = new FxAccountsMessagesReceiver(fxAccounts, {
- handler: messagesHandler
- });
- sandbox.stub(receiver, "_getOwnKeys").callsFake(async () => {});
- sandbox.stub(receiver, "_decrypt").callsFake((ciphertext) => {
- if (ciphertext == "#encrypted#") {
- return new TextEncoder("utf-8").encode(JSON.stringify({"foo": "bar"}));
- }
- throw new Error("Boom!");
- });
-
- await receiver.consumeRemoteMessages();
-
- Assert.ok(fxaClient.getMessages.calledOnce);
- Assert.equal(fxaClient.getMessages.args[0][0], "toktok");
- Assert.deepEqual(fxaClient.getMessages.args[0][1], {});
- Assert.ok(messagesHandler.handle.calledOnce);
- Assert.deepEqual(messagesHandler.handle.args[0][0], [{"foo": "bar"}]);
- fxaClient.getMessages.reset();
-
- await receiver.consumeRemoteMessages();
-
- Assert.ok(fxaClient.getMessages.calledOnce);
- Assert.equal(fxaClient.getMessages.args[0][0], "toktok");
- Assert.deepEqual(fxaClient.getMessages.args[0][1], {"index": "idx-2"});
-
- sandbox.restore();
-});
-
-add_task(async function test_handler_handle_sendtab() {
- const fxAccounts = {
- async getDeviceList() {
- return [{id: "1234a", name: "My Computer"}];
- }
- };
- const handler = new FxAccountsMessagesHandler(fxAccounts);
- const payloads = [{
- topic: "sendtab",
- data: {
- from: "1234a",
- current: 0,
- entries: [{title: "Foo", url: "https://foo.com"},
- {title: "Bar", url: "https://bar.com"}]
- }
- }, {
- topic: "sendtab",
- data: {
- from: "unknown_device",
- entries: [{title: "Foo2", url: "https://foo2.com"},
- {title: "Bar2", url: "https://bar2.com"}]
- }
- }, {
- topic: "unknowntopic",
- data: {foo: "bar"}
- }];
- const notificationPromise = promiseObserver("fxaccounts:messages:display-tabs");
- await handler.handle(payloads);
- const {subject} = await notificationPromise;
- const toDisplay = subject.wrappedJSObject.object;
- const expected = [
- {uri: "https://foo.com", title: "Foo", sender: {id: "1234a", name: "My Computer"}},
- {uri: "https://bar2.com", title: "Bar2", sender: {id: "", name: ""}}
- ];
- Assert.deepEqual(toDisplay, expected);
-});
-
-function promiseObserver(aTopic) {
- return new Promise(resolve => {
- Services.obs.addObserver(function onNotification(subject, topic, data) {
- Services.obs.removeObserver(onNotification, topic);
- resolve({subject, data});
- }, aTopic);
- });
-}
-
--- a/services/fxaccounts/tests/xpcshell/test_push_service.js
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -404,32 +404,35 @@ add_test(function observePushTopicPasswo
pushService._onPasswordChanged = function() {
run_next_test();
};
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
-add_task(async function messagesTickle() {
+add_task(async function commandReceived() {
let msg = {
data: {
json: () => ({
- topic: "sendtab"
+ command: "fxaccounts:command_received",
+ data: {
+ url: "https://api.accounts.firefox.com/auth/v1/account/device/commands?index=42&limit=1"
+ }
})
},
QueryInterface() {
return this;
}
};
let fxAccountsMock = {};
const promiseConsumeRemoteMessagesCalled = new Promise(res => {
- fxAccountsMock.messages = {
- consumeRemoteMessages() {
+ fxAccountsMock.commands = {
+ consumeRemoteCommand() {
res();
}
};
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: fxAccountsMock,
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -4,19 +4,19 @@ skip-if = (toolkit == 'android' || appna
support-files =
!/services/common/tests/unit/head_helpers.js
!/services/common/tests/unit/head_http.js
[test_accounts.js]
[test_accounts_config.js]
[test_accounts_device_registration.js]
[test_client.js]
+[test_commands.js]
[test_credentials.js]
[test_loginmgr_storage.js]
-[test_messages.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]
[test_oauth_token_storage.js]
[test_profile_client.js]
[test_push_service.js]
[test_web_channel.js]
[test_profile.js]
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -528,21 +528,16 @@ SyncScheduler.prototype = {
return;
}
if (!Async.isAppReady()) {
this._log.debug("Not initiating sync: app is shutting down");
return;
}
Services.tm.dispatchToMainThread(() => {
- // Terrible hack below: we do the fxa messages polling in the sync
- // scheduler to get free post-wake/link-state etc detection.
- fxAccounts.messages.consumeRemoteMessages().catch(e => {
- this._log.error("Error while polling for FxA messages.", e);
- });
this.service.sync({engines, why});
});
},
/**
* Set a timer for the next sync
*/
scheduleNextSync(interval, {engines = null, why = null} = {}) {
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -70,18 +70,18 @@
"forms.jsm": ["FormData"],
"FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
"FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
"FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"],
"FrameScriptManager.jsm": ["getNewLoaderID"],
"fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
- "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "CAPABILITY_MESSAGES", "CAPABILITY_MESSAGES_SENDTAB", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
- "FxAccountsMessages.js": ["FxAccountsMessages", "FxAccountsMessagesSender", "FxAccountsMessagesReceiver", "FxAccountsMessagesHandler"],
+ "FxAccountsCommands.js": ["SendTab", "FxAccountsCommands"],
+ "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_COMMAND_RECEIVED_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "COMMAND_SENDTAB", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],
"FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],
"FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"Geometry.jsm": ["Point", "Rect"],