From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../satchel/integrations/FirefoxRelay.sys.mjs | 647 +++++++++++++++++++++ .../satchel/integrations/FirefoxRelayTelemetry.mjs | 73 +++ .../satchel/integrations/FirefoxRelayUtils.sys.mjs | 23 + .../satchel/integrations/WebAuthnFeature.sys.mjs | 129 ++++ 4 files changed, 872 insertions(+) create mode 100644 toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs create mode 100644 toolkit/components/satchel/integrations/FirefoxRelayTelemetry.mjs create mode 100644 toolkit/components/satchel/integrations/FirefoxRelayUtils.sys.mjs create mode 100644 toolkit/components/satchel/integrations/WebAuthnFeature.sys.mjs (limited to 'toolkit/components/satchel/integrations') diff --git a/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs b/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs new file mode 100644 index 0000000000..8f88373763 --- /dev/null +++ b/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs @@ -0,0 +1,647 @@ +/* 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/. */ + +import { FirefoxRelayTelemetry } from "resource://gre/modules/FirefoxRelayTelemetry.mjs"; +import { + LoginHelper, + OptInFeature, + ParentAutocompleteOption, +} from "resource://gre/modules/LoginHelper.sys.mjs"; +import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs"; +import { showConfirmation } from "resource://gre/modules/FillHelpers.sys.mjs"; + +const lazy = {}; + +// Static configuration +const gConfig = (function () { + const baseUrl = Services.prefs.getStringPref( + "signon.firefoxRelay.base_url", + undefined + ); + return { + scope: ["profile", "https://identity.mozilla.com/apps/relay"], + addressesUrl: baseUrl + `relayaddresses/`, + acceptTermsUrl: baseUrl + `terms-accepted-user/`, + profilesUrl: baseUrl + `profiles/`, + learnMoreURL: Services.urlFormatter.formatURLPref( + "signon.firefoxRelay.learn_more_url" + ), + manageURL: Services.urlFormatter.formatURLPref( + "signon.firefoxRelay.manage_url" + ), + relayFeaturePref: "signon.firefoxRelay.feature", + termsOfServiceUrl: Services.urlFormatter.formatURLPref( + "signon.firefoxRelay.terms_of_service_url" + ), + privacyPolicyUrl: Services.urlFormatter.formatURLPref( + "signon.firefoxRelay.privacy_policy_url" + ), + }; +})(); + +ChromeUtils.defineLazyGetter(lazy, "log", () => + LoginHelper.createLogger("FirefoxRelay") +); +ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => + ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton() +); +ChromeUtils.defineLazyGetter(lazy, "strings", function () { + return new Localization([ + "branding/brand.ftl", + "browser/firefoxRelay.ftl", + "toolkit/branding/accounts.ftl", + "toolkit/branding/brandings.ftl", + ]); +}); + +if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) { + throw new Error("FirefoxRelay.sys.mjs should only run in the parent process"); +} + +// Using 418 to avoid conflict with other standard http error code +const AUTH_TOKEN_ERROR_CODE = 418; + +let gFlowId; + +async function getRelayTokenAsync() { + try { + return await lazy.fxAccounts.getOAuthToken({ scope: gConfig.scope }); + } catch (e) { + console.error(`There was an error getting the user's token: ${e.message}`); + return undefined; + } +} + +async function hasFirefoxAccountAsync() { + if (!lazy.fxAccounts.constructor.config.isProductionConfig()) { + return false; + } + return lazy.fxAccounts.hasLocalSession(); +} + +async function fetchWithReauth( + browser, + createRequest, + canGetFreshOAuthToken = true +) { + const relayToken = await getRelayTokenAsync(); + if (!relayToken) { + if (browser) { + await showErrorAsync(browser, "firefox-relay-must-login-to-account"); + } + return undefined; + } + + const headers = new Headers({ + Authorization: `Bearer ${relayToken}`, + Accept: "application/json", + "Accept-Language": Services.locale.requestedLocales, + "Content-Type": "application/json", + }); + + const request = createRequest(headers); + const response = await fetch(request); + + if (canGetFreshOAuthToken && response.status == 401) { + await lazy.fxAccounts.removeCachedOAuthToken({ token: relayToken }); + return fetchWithReauth(browser, createRequest, false); + } + return response; +} + +async function getReusableMasksAsync(browser, _origin) { + const response = await fetchWithReauth( + browser, + headers => + new Request(gConfig.addressesUrl, { + method: "GET", + headers, + }) + ); + + if (!response) { + // fetchWithReauth only returns undefined if login / obtaining a token failed. + // Otherwise, it will return a response object. + return [undefined, AUTH_TOKEN_ERROR_CODE]; + } + + if (response.ok) { + return [await response.json(), response.status]; + } + + lazy.log.error( + `failed to find reusable Relay masks: ${response.status}:${response.statusText}` + ); + await showErrorAsync(browser, "firefox-relay-get-reusable-masks-failed", { + status: response.status, + }); + + return [undefined, response.status]; +} + +/** + * Show localized notification. + * + * @param {*} browser + * @param {*} messageId from browser/firefoxRelay.ftl + * @param {object} messageArgs + */ +async function showErrorAsync(browser, messageId, messageArgs) { + const { PopupNotifications } = browser.ownerGlobal.wrappedJSObject; + const [message] = await lazy.strings.formatValues([ + { id: messageId, args: messageArgs }, + ]); + PopupNotifications.show( + browser, + "relay-integration-error", + message, + "password-notification-icon", + null, + null, + { + autofocus: true, + removeOnDismissal: true, + popupIconURL: "chrome://browser/content/logos/relay.svg", + learnMoreURL: gConfig.learnMoreURL, + } + ); +} + +function customizeNotificationHeader(notification) { + if (!notification) { + return; + } + const document = notification.owner.panel.ownerDocument; + const description = document.querySelector( + `description[popupid=${notification.id}]` + ); + const headerTemplate = document.getElementById("firefox-relay-header"); + description.replaceChildren(headerTemplate.firstChild.cloneNode(true)); +} + +async function formatMessages(...ids) { + for (let i in ids) { + if (typeof ids[i] == "string") { + ids[i] = { id: ids[i] }; + } + } + + const messages = await lazy.strings.formatMessages(ids); + return messages.map(message => { + if (message.attributes) { + return message.attributes.reduce( + (result, { name, value }) => ({ ...result, [name]: value }), + {} + ); + } + return message.value; + }); +} + +async function showReusableMasksAsync(browser, origin, error) { + const [reusableMasks, status] = await getReusableMasksAsync(browser, origin); + if (!reusableMasks) { + FirefoxRelayTelemetry.recordRelayReusePanelEvent("shown", gFlowId, status); + return null; + } + + let fillUsername; + const fillUsernamePromise = new Promise(resolve => (fillUsername = resolve)); + const [getUnlimitedMasksStrings] = await formatMessages( + "firefox-relay-get-unlimited-masks" + ); + const getUnlimitedMasks = { + label: getUnlimitedMasksStrings.label, + accessKey: getUnlimitedMasksStrings.accesskey, + dismiss: true, + async callback() { + FirefoxRelayTelemetry.recordRelayReusePanelEvent( + "get_unlimited_masks", + gFlowId + ); + browser.ownerGlobal.openWebLinkIn(gConfig.manageURL, "tab"); + }, + }; + + let notification; + + function getReusableMasksList() { + return notification?.owner.panel.getElementsByClassName( + "reusable-relay-masks" + )[0]; + } + + function notificationShown() { + if (!notification) { + return; + } + + customizeNotificationHeader(notification); + + notification.owner.panel.getElementsByClassName( + "error-message" + )[0].textContent = error.detail || ""; + + // rebuild "reuse mask" buttons list + const list = getReusableMasksList(); + list.innerHTML = ""; + + const document = list.ownerDocument; + const fragment = document.createDocumentFragment(); + reusableMasks + .filter(mask => mask.enabled) + .forEach(mask => { + const button = document.createElement("button"); + + const maskFullAddress = document.createElement("span"); + maskFullAddress.textContent = mask.full_address; + button.appendChild(maskFullAddress); + + const maskDescription = document.createElement("span"); + maskDescription.textContent = + mask.description || mask.generated_for || mask.used_on; + button.appendChild(maskDescription); + + button.addEventListener( + "click", + () => { + notification.remove(); + lazy.log.info("Reusing Relay mask"); + fillUsername(mask.full_address); + showConfirmation( + browser, + "confirmation-hint-firefox-relay-mask-reused" + ); + FirefoxRelayTelemetry.recordRelayReusePanelEvent( + "reuse_mask", + gFlowId + ); + }, + { once: true } + ); + fragment.appendChild(button); + }); + list.appendChild(fragment); + } + + function notificationRemoved() { + const list = getReusableMasksList(); + list.innerHTML = ""; + } + + function onNotificationEvent(event) { + switch (event) { + case "removed": + notificationRemoved(); + break; + case "shown": + notificationShown(); + FirefoxRelayTelemetry.recordRelayReusePanelEvent("shown", gFlowId); + break; + } + } + + const { PopupNotifications } = browser.ownerGlobal.wrappedJSObject; + notification = PopupNotifications.show( + browser, + "relay-integration-reuse-masks", + "", // content is provided after popup shown + "password-notification-icon", + getUnlimitedMasks, + [], + { + autofocus: true, + removeOnDismissal: true, + eventCallback: onNotificationEvent, + } + ); + + return fillUsernamePromise; +} + +async function generateUsernameAsync(browser, origin) { + const body = JSON.stringify({ + enabled: true, + description: origin.substr(0, 64), + generated_for: origin.substr(0, 255), + used_on: origin, + }); + + const response = await fetchWithReauth( + browser, + headers => + new Request(gConfig.addressesUrl, { + method: "POST", + headers, + body, + }) + ); + + if (!response) { + FirefoxRelayTelemetry.recordRelayUsernameFilledEvent( + "shown", + gFlowId, + AUTH_TOKEN_ERROR_CODE + ); + return undefined; + } + + if (response.ok) { + lazy.log.info(`generated Relay mask`); + const result = await response.json(); + showConfirmation(browser, "confirmation-hint-firefox-relay-mask-created"); + return result.full_address; + } + + if (response.status == 403) { + const error = await response.json(); + if (error?.error_code == "free_tier_limit") { + FirefoxRelayTelemetry.recordRelayUsernameFilledEvent( + "shown", + gFlowId, + error?.error_code + ); + return showReusableMasksAsync(browser, origin, error); + } + } + + lazy.log.error( + `failed to generate Relay mask: ${response.status}:${response.statusText}` + ); + + await showErrorAsync(browser, "firefox-relay-mask-generation-failed", { + status: response.status, + }); + + FirefoxRelayTelemetry.recordRelayReusePanelEvent( + "shown", + gFlowId, + response.status + ); + + return undefined; +} + +function isSignup(scenarioName) { + return scenarioName == "SignUpFormScenario"; +} + +class RelayOffered { + async *autocompleteItemsAsync(_origin, scenarioName, hasInput) { + if ( + !hasInput && + isSignup(scenarioName) && + (await hasFirefoxAccountAsync()) && + !Services.prefs.prefIsLocked("signon.firefoxRelay.feature") + ) { + const [title, subtitle] = await formatMessages( + "firefox-relay-opt-in-title-1", + "firefox-relay-opt-in-subtitle-1" + ); + yield new ParentAutocompleteOption( + "chrome://browser/content/logos/relay.svg", + title, + subtitle, + "PasswordManager:offerRelayIntegration", + { + telemetry: { + flowId: gFlowId, + scenarioName, + }, + } + ); + FirefoxRelayTelemetry.recordRelayOfferedEvent( + "shown", + gFlowId, + scenarioName + ); + } + } + + async notifyServerTermsAcceptedAsync(browser) { + const response = await fetchWithReauth( + browser, + headers => + new Request(gConfig.acceptTermsUrl, { + method: "POST", + headers, + }) + ); + + if (!response?.ok) { + lazy.log.error( + `failed to notify server that terms are accepted : ${response?.status}:${response?.statusText}` + ); + + let error; + try { + error = await response?.json(); + } catch {} + await showErrorAsync(browser, "firefox-relay-mask-generation-failed", { + status: error?.detail || response.status, + }); + return false; + } + + return true; + } + + async offerRelayIntegration(feature, browser, origin) { + const fxaUser = await lazy.fxAccounts.getSignedInUser(); + + if (!fxaUser) { + return null; + } + const { PopupNotifications } = browser.ownerGlobal.wrappedJSObject; + let fillUsername; + const fillUsernamePromise = new Promise( + resolve => (fillUsername = resolve) + ); + const [enableStrings, disableStrings, postponeStrings] = + await formatMessages( + "firefox-relay-opt-in-confirmation-enable-button", + "firefox-relay-opt-in-confirmation-disable", + "firefox-relay-opt-in-confirmation-postpone" + ); + const enableIntegration = { + label: enableStrings.label, + accessKey: enableStrings.accesskey, + dismiss: true, + callback: async () => { + lazy.log.info("user opted in to Firefox Relay integration"); + // Capture the flowId here since async operations might take some time to resolve + // and by then gFlowId might have another value + const flowId = gFlowId; + if (await this.notifyServerTermsAcceptedAsync(browser)) { + feature.markAsEnabled(); + FirefoxRelayTelemetry.recordRelayOptInPanelEvent("enabled", flowId); + fillUsername(await generateUsernameAsync(browser, origin)); + } + }, + }; + const postpone = { + label: postponeStrings.label, + accessKey: postponeStrings.accesskey, + dismiss: true, + callback() { + lazy.log.info( + "user decided not to decide about Firefox Relay integration" + ); + feature.markAsOffered(); + FirefoxRelayTelemetry.recordRelayOptInPanelEvent("postponed", gFlowId); + }, + }; + const disableIntegration = { + label: disableStrings.label, + accessKey: disableStrings.accesskey, + dismiss: true, + callback() { + lazy.log.info("user opted out from Firefox Relay integration"); + feature.markAsDisabled(); + FirefoxRelayTelemetry.recordRelayOptInPanelEvent("disabled", gFlowId); + }, + }; + let notification; + feature.markAsOffered(); + notification = PopupNotifications.show( + browser, + "relay-integration-offer", + "", // content is provided after popup shown + "password-notification-icon", + enableIntegration, + [postpone, disableIntegration], + { + autofocus: true, + removeOnDismissal: true, + learnMoreURL: gConfig.learnMoreURL, + eventCallback: event => { + switch (event) { + case "shown": + customizeNotificationHeader(notification); + const document = notification.owner.panel.ownerDocument; + const tosLink = document.getElementById( + "firefox-relay-offer-tos-url" + ); + tosLink.href = gConfig.termsOfServiceUrl; + const privacyLink = document.getElementById( + "firefox-relay-offer-privacy-url" + ); + privacyLink.href = gConfig.privacyPolicyUrl; + const content = document.querySelector( + `popupnotification[id=${notification.id}-notification] popupnotificationcontent` + ); + const line3 = content.querySelector( + "[id=firefox-relay-offer-what-relay-provides]" + ); + document.l10n.setAttributes( + line3, + "firefox-relay-offer-what-relay-provides", + { + useremail: fxaUser.email, + } + ); + FirefoxRelayTelemetry.recordRelayOptInPanelEvent( + "shown", + gFlowId + ); + break; + } + }, + } + ); + getRelayTokenAsync(); + return fillUsernamePromise; + } +} + +class RelayEnabled { + async *autocompleteItemsAsync(origin, scenarioName, hasInput) { + if ( + !hasInput && + isSignup(scenarioName) && + (await hasFirefoxAccountAsync()) + ) { + const [title] = await formatMessages("firefox-relay-use-mask-title"); + yield new ParentAutocompleteOption( + "chrome://browser/content/logos/relay.svg", + title, + "", // when the user has opted-in, there is no subtitle content + "PasswordManager:generateRelayUsername", + { + telemetry: { + flowId: gFlowId, + }, + } + ); + FirefoxRelayTelemetry.recordRelayUsernameFilledEvent("shown", gFlowId); + } + } + + async generateUsername(browser, origin) { + return generateUsernameAsync(browser, origin); + } +} + +class RelayDisabled {} + +class RelayFeature extends OptInFeature { + constructor() { + super(RelayOffered, RelayEnabled, RelayDisabled, gConfig.relayFeaturePref); + Services.telemetry.setEventRecordingEnabled("relay_integration", true); + // Update the config when the signon.firefoxRelay.base_url pref is changed. + // This is added mainly for tests. + Services.prefs.addObserver( + "signon.firefoxRelay.base_url", + this.updateConfig + ); + } + + get learnMoreUrl() { + return gConfig.learnMoreURL; + } + + updateConfig() { + const newBaseUrl = Services.prefs.getStringPref( + "signon.firefoxRelay.base_url" + ); + gConfig.addressesUrl = newBaseUrl + `relayaddresses/`; + gConfig.profilesUrl = newBaseUrl + `profiles/`; + gConfig.acceptTermsUrl = newBaseUrl + `terms-accepted-user/`; + } + + async autocompleteItemsAsync({ origin, scenarioName, hasInput }) { + const result = []; + + // Generate a flowID to unique identify a series of user action. FlowId + // allows us to link users' interaction on different UI component (Ex. autocomplete, notification) + // We can use flowID to build the Funnel Diagram + // This value need to always be regenerated in the entry point of an user + // action so we overwrite the previous one. + gFlowId = TelemetryUtils.generateUUID(); + + if (this.implementation.autocompleteItemsAsync) { + for await (const item of this.implementation.autocompleteItemsAsync( + origin, + scenarioName, + hasInput + )) { + result.push(item); + } + } + + return result; + } + + async generateUsername(browser, origin) { + return this.implementation.generateUsername?.(browser, origin); + } + + async offerRelayIntegration(browser, origin) { + return this.implementation.offerRelayIntegration?.(this, browser, origin); + } +} + +export const FirefoxRelay = new RelayFeature(); diff --git a/toolkit/components/satchel/integrations/FirefoxRelayTelemetry.mjs b/toolkit/components/satchel/integrations/FirefoxRelayTelemetry.mjs new file mode 100644 index 0000000000..c03f48ba0f --- /dev/null +++ b/toolkit/components/satchel/integrations/FirefoxRelayTelemetry.mjs @@ -0,0 +1,73 @@ +/* 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/. */ + +export const FirefoxRelayTelemetry = { + recordRelayIntegrationTelemetryEvent( + eventObject, + eventMethod, + eventFlowId, + eventExtras + ) { + Services.telemetry.recordEvent( + "relay_integration", + eventMethod, + eventObject, + eventFlowId ?? "", + eventExtras ?? {} + ); + }, + + recordRelayPrefEvent(eventMethod, eventFlowId, eventExtras) { + this.recordRelayIntegrationTelemetryEvent( + "pref_change", + eventMethod, + eventFlowId, + eventExtras + ); + }, + + recordRelayOfferedEvent(eventMethod, eventFlowId, scenarioName) { + return this.recordRelayIntegrationTelemetryEvent( + "offer_relay", + eventMethod, + eventFlowId, + { + scenario: scenarioName, + } + ); + }, + + recordRelayUsernameFilledEvent(eventMethod, eventFlowId, errorCode = 0) { + return this.recordRelayIntegrationTelemetryEvent( + "fill_username", + eventMethod, + eventFlowId, + { + error_code: errorCode + "", + } + ); + }, + + recordRelayReusePanelEvent(eventMethod, eventFlowId, errorCode = 0) { + return this.recordRelayIntegrationTelemetryEvent( + "reuse_panel", + eventMethod, + eventFlowId, + { + error_code: errorCode + "", + } + ); + }, + + recordRelayOptInPanelEvent(eventMethod, eventFlowId, eventExtras) { + return this.recordRelayIntegrationTelemetryEvent( + "opt_in_panel", + eventMethod, + eventFlowId, + eventExtras + ); + }, +}; + +export default FirefoxRelayTelemetry; diff --git a/toolkit/components/satchel/integrations/FirefoxRelayUtils.sys.mjs b/toolkit/components/satchel/integrations/FirefoxRelayUtils.sys.mjs new file mode 100644 index 0000000000..0dfb2a969d --- /dev/null +++ b/toolkit/components/satchel/integrations/FirefoxRelayUtils.sys.mjs @@ -0,0 +1,23 @@ +/* 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/. */ + +import { LoginHelper } from "resource://gre/modules/LoginHelper.sys.mjs"; + +export const FirefoxRelayUtils = { + isRelayInterestedField(input) { + return ( + FirefoxRelayUtils.relayIsAvailableOrEnabled && + (LoginHelper.isInferredEmailField(input) || + LoginHelper.isInferredUsernameField(input)) + ); + }, + + relayIsAvailableOrEnabled() { + const value = Services.prefs.getStringPref( + "signon.firefoxRelay.feature", + undefined + ); + return ["available", "offered", "enabled"].includes(value); + }, +}; diff --git a/toolkit/components/satchel/integrations/WebAuthnFeature.sys.mjs b/toolkit/components/satchel/integrations/WebAuthnFeature.sys.mjs new file mode 100644 index 0000000000..b5805d5a93 --- /dev/null +++ b/toolkit/components/satchel/integrations/WebAuthnFeature.sys.mjs @@ -0,0 +1,129 @@ +/* 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/. */ + +import { + LoginHelper, + ParentAutocompleteOption, +} from "resource://gre/modules/LoginHelper.sys.mjs"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "webauthnService", + "@mozilla.org/webauthn/service;1", + "nsIWebAuthnService" +); + +ChromeUtils.defineLazyGetter( + lazy, + "strings", + () => new Localization(["browser/webauthnDialog.ftl"]) +); +ChromeUtils.defineLazyGetter(lazy, "log", () => + LoginHelper.createLogger("WebAuthnFeature") +); + +if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) { + throw new Error( + "PasskeySupport.sys.mjs should only run in the parent process" + ); +} + +class WebAuthnSupport { + async *#getAutocompleteItemsAsync(browsingContextId, formOrigin) { + let transactionId = lazy.webauthnService.hasPendingConditionalGet( + browsingContextId, + formOrigin + ); + if (transactionId == 0) { + // No pending transaction + return; + } + let credentials = lazy.webauthnService.getAutoFillEntries(transactionId); + + let labels = credentials.map(x => ({ + id: "webauthn-specific-passkey-label", + args: { domain: x.rpId }, + })); + if (!credentials.length) { + labels.push({ id: "webauthn-a-passkey-label" }); + } else { + labels.push({ id: "webauthn-another-passkey-label" }); + } + const formattedLabels = await lazy.strings.formatValues(labels); + for (let i = 0; i < credentials.length; i++) { + yield new ParentAutocompleteOption( + "chrome://browser/content/logos/passkey.svg", + credentials[i].userName, + formattedLabels[i], + "PasswordManager:promptForAuthenticator", + { + selection: { + transactionId, + credentialId: credentials[i].credentialId, + }, + } + ); + } + // `getAutoFillEntries` may not return all of the credentials on the device + // (in particular it will not include credentials with a protection policy + // that forbids silent discovery), so we include a catch-all entry in the + // list. If the user selects this entry, the WebAuthn transaction will + // proceed using the modal UI. + yield new ParentAutocompleteOption( + "chrome://browser/content/logos/passkey.svg", + formattedLabels[formattedLabels.length - 1], + "", + "PasswordManager:promptForAuthenticator", + { + selection: { + transactionId, + }, + } + ); + } + + /** + * + * @param {int} browsingContextId the browsing context ID associated with this request + * @param {string} formOrigin + * @param {string} scenarioName can be "SignUpFormScenario" or undefined + * @param {string} isWebAuthn indicates whether "webauthn" was included in the input's autocomplete value + * @returns {ParentAutocompleteOption} the optional WebAuthn autocomplete item + */ + async autocompleteItemsAsync( + browsingContextId, + formOrigin, + scenarioName, + isWebAuthn + ) { + const result = []; + if (scenarioName !== "SignUpFormScenario" || isWebAuthn) { + for await (const item of this.#getAutocompleteItemsAsync( + browsingContextId, + formOrigin + )) { + result.push(item); + } + } + return result; + } + + async promptForAuthenticator(browser, selection) { + lazy.log.info("Prompting to authenticate with relying party."); + if (selection.credentialId) { + lazy.webauthnService.selectAutoFillEntry( + selection.transactionId, + selection.credentialId + ); + } else { + lazy.webauthnService.resumeConditionalGet(selection.transactionId); + } + } +} + +export const WebAuthnFeature = new WebAuthnSupport(); -- cgit v1.2.3