From f7f5db5d0322ee8002a2d1f7daaa3b9c482bf9df Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 8 Jul 2026 22:55:48 -0700 Subject: [PATCH 1/9] Define signals on PublicKeyCredentialFuture --- packages/browser/src/types/index.ts | 61 +++++++++++++++++++++++++++++ packages/server/src/types/index.ts | 61 +++++++++++++++++++++++++++++ packages/types/src/index.ts | 61 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/packages/browser/src/types/index.ts b/packages/browser/src/types/index.ts index e565d1c3..9c657a72 100644 --- a/packages/browser/src/types/index.ts +++ b/packages/browser/src/types/index.ts @@ -212,6 +212,12 @@ export interface PublicKeyCredentialFuture extends PublicKeyCredential { toJSON(): PublicKeyCredentialJSON; // See https://w3c.github.io/webauthn/#sctn-getClientCapabilities getClientCapabilities?(): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential + signalUnknownCredential(options: UnknownCredentialOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials + signalAllAcceptedCredentials(options: AllAcceptedCredentialsOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails + signalCurrentUserDetails(options: CurrentUserDetailsOptions): Promise; } /** @@ -287,3 +293,58 @@ export type PublicKeyCredentialClientCapabilities = { * https://github.com/denoland/std/blob/b5a5fe4f96b91c1fe8dba5cc0270092dd11d3287/bytes/_types.ts#L11 */ export type Uint8Array_ = ReturnType; + +/** + * Options for `PublicKeyCredential.signalUnknownCredential()`. This signal communicates that the + * credential that the user just tried to register, or to authenticate with, was not one that the + * Relying Party recognizes. The authenticator responsible for the credential can hide or delete + * the credential so that the user does not see it in the future as an option to sign in with. + * + * It is a good idea for a Relying Party to send this signal immediately after the use of an + * unrecognized credential. For example, after rejecting the output from `startRegistration()` due + * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from + * `startAuthentication()` because the user deleted the passkey from their RP-specific user + * settings. + * + * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. + */ +type UnknownCredentialOptions = { + rpId: string; + credentialId: Base64URLString; +}; + +/** + * Options for `PublicKeyCredential.signalAllAcceptedCredentials()`. This signal communicates the + * current list of passkeys the Relying Party will recognize for use by the **authenticated** user + * on the next login. Authenticators that have a passkey for (rpId + userId), but the passkey ID is + * not found in allAcceptedCredentialIds, may choose to hide or delete the passkey because it will + * not be accepted for use by the Relying Party. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication. + * + * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. + */ +type AllAcceptedCredentialsOptions = { + rpId: string; + userId: Base64URLString; + allAcceptedCredentialIds: Base64URLString[]; +}; + +/** + * Options for `PublicKeyCredential.signalCurrentUserDetails()`. This signal that communicates a + * change in the **authenticated** user's name and/or display name. This can help browsers and + * platforms display the most up-to-date information about the user during a passkey authentication + * instead of always showing whatever value was set at the time of registration. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication and immediately after the user name and/or display name is changed. + * + * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. + */ +type CurrentUserDetailsOptions = { + rpId: string; + userId: Base64URLString; + name: string; + displayName: string; +}; diff --git a/packages/server/src/types/index.ts b/packages/server/src/types/index.ts index e565d1c3..9c657a72 100644 --- a/packages/server/src/types/index.ts +++ b/packages/server/src/types/index.ts @@ -212,6 +212,12 @@ export interface PublicKeyCredentialFuture extends PublicKeyCredential { toJSON(): PublicKeyCredentialJSON; // See https://w3c.github.io/webauthn/#sctn-getClientCapabilities getClientCapabilities?(): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential + signalUnknownCredential(options: UnknownCredentialOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials + signalAllAcceptedCredentials(options: AllAcceptedCredentialsOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails + signalCurrentUserDetails(options: CurrentUserDetailsOptions): Promise; } /** @@ -287,3 +293,58 @@ export type PublicKeyCredentialClientCapabilities = { * https://github.com/denoland/std/blob/b5a5fe4f96b91c1fe8dba5cc0270092dd11d3287/bytes/_types.ts#L11 */ export type Uint8Array_ = ReturnType; + +/** + * Options for `PublicKeyCredential.signalUnknownCredential()`. This signal communicates that the + * credential that the user just tried to register, or to authenticate with, was not one that the + * Relying Party recognizes. The authenticator responsible for the credential can hide or delete + * the credential so that the user does not see it in the future as an option to sign in with. + * + * It is a good idea for a Relying Party to send this signal immediately after the use of an + * unrecognized credential. For example, after rejecting the output from `startRegistration()` due + * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from + * `startAuthentication()` because the user deleted the passkey from their RP-specific user + * settings. + * + * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. + */ +type UnknownCredentialOptions = { + rpId: string; + credentialId: Base64URLString; +}; + +/** + * Options for `PublicKeyCredential.signalAllAcceptedCredentials()`. This signal communicates the + * current list of passkeys the Relying Party will recognize for use by the **authenticated** user + * on the next login. Authenticators that have a passkey for (rpId + userId), but the passkey ID is + * not found in allAcceptedCredentialIds, may choose to hide or delete the passkey because it will + * not be accepted for use by the Relying Party. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication. + * + * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. + */ +type AllAcceptedCredentialsOptions = { + rpId: string; + userId: Base64URLString; + allAcceptedCredentialIds: Base64URLString[]; +}; + +/** + * Options for `PublicKeyCredential.signalCurrentUserDetails()`. This signal that communicates a + * change in the **authenticated** user's name and/or display name. This can help browsers and + * platforms display the most up-to-date information about the user during a passkey authentication + * instead of always showing whatever value was set at the time of registration. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication and immediately after the user name and/or display name is changed. + * + * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. + */ +type CurrentUserDetailsOptions = { + rpId: string; + userId: Base64URLString; + name: string; + displayName: string; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 12a5857d..9f4a8f15 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -202,6 +202,12 @@ export interface PublicKeyCredentialFuture extends PublicKeyCredential { toJSON(): PublicKeyCredentialJSON; // See https://w3c.github.io/webauthn/#sctn-getClientCapabilities getClientCapabilities?(): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential + signalUnknownCredential(options: UnknownCredentialOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials + signalAllAcceptedCredentials(options: AllAcceptedCredentialsOptions): Promise; + // See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails + signalCurrentUserDetails(options: CurrentUserDetailsOptions): Promise; } /** @@ -277,3 +283,58 @@ export type PublicKeyCredentialClientCapabilities = { * https://github.com/denoland/std/blob/b5a5fe4f96b91c1fe8dba5cc0270092dd11d3287/bytes/_types.ts#L11 */ export type Uint8Array_ = ReturnType; + +/** + * Options for `PublicKeyCredential.signalUnknownCredential()`. This signal communicates that the + * credential that the user just tried to register, or to authenticate with, was not one that the + * Relying Party recognizes. The authenticator responsible for the credential can hide or delete + * the credential so that the user does not see it in the future as an option to sign in with. + * + * It is a good idea for a Relying Party to send this signal immediately after the use of an + * unrecognized credential. For example, after rejecting the output from `startRegistration()` due + * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from + * `startAuthentication()` because the user deleted the passkey from their RP-specific user + * settings. + * + * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. + */ +type UnknownCredentialOptions = { + rpId: string; + credentialId: Base64URLString; +}; + +/** + * Options for `PublicKeyCredential.signalAllAcceptedCredentials()`. This signal communicates the + * current list of passkeys the Relying Party will recognize for use by the **authenticated** user + * on the next login. Authenticators that have a passkey for (rpId + userId), but the passkey ID is + * not found in allAcceptedCredentialIds, may choose to hide or delete the passkey because it will + * not be accepted for use by the Relying Party. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication. + * + * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. + */ +type AllAcceptedCredentialsOptions = { + rpId: string; + userId: Base64URLString; + allAcceptedCredentialIds: Base64URLString[]; +}; + +/** + * Options for `PublicKeyCredential.signalCurrentUserDetails()`. This signal that communicates a + * change in the **authenticated** user's name and/or display name. This can help browsers and + * platforms display the most up-to-date information about the user during a passkey authentication + * instead of always showing whatever value was set at the time of registration. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication and immediately after the user name and/or display name is changed. + * + * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. + */ +type CurrentUserDetailsOptions = { + rpId: string; + userId: Base64URLString; + name: string; + displayName: string; +}; From 45d9b2fd0edcae5a3be5f6e7cb580dbe5f064d1f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 8 Jul 2026 22:55:55 -0700 Subject: [PATCH 2/9] Begin defining new `sendSignal()` browser helper --- packages/browser/src/methods/sendSignal.ts | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 packages/browser/src/methods/sendSignal.ts diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts new file mode 100644 index 00000000..697945dd --- /dev/null +++ b/packages/browser/src/methods/sendSignal.ts @@ -0,0 +1,113 @@ +import type { Base64URLString, PublicKeyCredentialFuture } from '../types/index.ts'; + +/** + * Broadcast a passkey state change on the server to the browser to enlist the browser's help + * in propagating that change to the corresponding authenticator. This can help prevent phantom + * credentials from being offered for use, and enable new usernames to be displayed after a + * passkey's creation. + * + * Sending a signal **does not** guarantee that the signal will be received by the authenticator. + * Signals are a "fire and forget" type of broadcast that will have browsers making a best effort + * to propagate the signal to the relevant authenticator. See the descriptions of the various + * signal option types for guidance on how often a signal may need to be resent for maximum + * efficacy. + */ +export async function sendSignal( + opts: + | SignalAllAcceptedCredentialsOpts + | SignalCurrentUserDetailsOpts + | SignalUnknownCredentialOpts, +): Promise { + const { signalName } = opts; + + if (signalName === 'signalAllAcceptedCredentials') { + return _callSignalAllAcceptedCredentials(opts); + } else if (signalName) { + } + return new Promise((resolve, _) => { + resolve(undefined); + }); +} + +function _callSignalAllAcceptedCredentials( + opts: SignalAllAcceptedCredentialsOpts, +): Promise { + const globalPublicKeyCredential = globalThis + .PublicKeyCredential as unknown as PublicKeyCredentialFuture; + + if (typeof globalPublicKeyCredential.signalAllAcceptedCredentials !== 'function') { + throw new Error(''); + } + + return globalPublicKeyCredential.signalAllAcceptedCredentials({ + rpId: opts.rpID, + userId: opts.userId, + allAcceptedCredentialIds: opts.allAcceptedCredentialIds, + }); +} + +/** + * A signal that communicates the current list of passkeys the Relying Party will recognize for use + * by the **authenticated** user on the next login. Authenticators that have a passkey for + * (rpId + userId), but the passkey ID is not found in allAcceptedCredentialIds, may choose to hide + * or delete the passkey because it will not be accepted for use by the Relying Party. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication. + * + * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. + */ +type SignalAllAcceptedCredentialsOpts = { + signalName: 'signalAllAcceptedCredentials'; + /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + rpID: string; + /** The base64url-encoded value used for `userID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + userId: Base64URLString; + /** An array of base64url-encoded credential IDs for all credentials the user may use to authenticate */ + allAcceptedCredentialIds: Base64URLString[]; +}; + +/** + * A signal that communicates a change in the **authenticated** user's name and/or display name. + * This can help browsers and platforms display the most up-to-date information about the user + * during a passkey authentication instead of always showing whatever value was set at the time of + * registration. + * + * It is a good idea for a Relying Party to periodically send this signal, for example after every + * successful authentication and immediately after the user name and/or display name is changed. + * + * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. + */ +type SignalCurrentUserDetailsOpts = { + signalName: 'signalCurrentUserDetails'; + /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + rpID: string; + /** The base64url-encoded value used for `userID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + userId: Base64URLString; + /** The primary account name, like an email address, username, etc... */ + userName: string; + /** An optional, longer user identifier, like a full name, account differentiator, etc... Defaults to `""` */ + userDisplayName?: string; +}; + +/** + * A signal that communicates that the credential that the user just tried to register, or to + * authenticate with, was not one that the Relying Party recognizes. The authenticator responsible + * for the credential can hide or delete the credential so that the user does not see it in the + * future as an option to sign in with. + * + * It is a good idea for a Relying Party to send this signal immediately after the use of an + * unrecognized credential. For example, after rejecting the output from `startRegistration()` due + * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from + * `startAuthentication()` because the user deleted the passkey from their RP-specific user + * settings. + * + * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. + */ +type SignalUnknownCredentialOpts = { + signalName: 'signalUnknownCredential'; + /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + rpID: string; + /** The credential ID that the Relying Party didn't recognize for use */ + credentialID: Base64URLString; +}; From 708bada113ade25310406ed62a198528935885a0 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 8 Jul 2026 22:56:00 -0700 Subject: [PATCH 3/9] Commit to identifying signal errors too --- packages/browser/src/helpers/identifySignalError.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/browser/src/helpers/identifySignalError.ts diff --git a/packages/browser/src/helpers/identifySignalError.ts b/packages/browser/src/helpers/identifySignalError.ts new file mode 100644 index 00000000..f8673b06 --- /dev/null +++ b/packages/browser/src/helpers/identifySignalError.ts @@ -0,0 +1,4 @@ +/** + * TODO: Use `WebAuthnError` here too to try and stop the bleeding of how many `throw new Error()` are + * introduced. This logic is specific to the signal APIs. + */ From 6e560f11bb51b3488588a0f3cf5125ff0631403d Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 8 Jul 2026 22:58:26 -0700 Subject: [PATCH 4/9] Fix capitalization on wrapper types --- packages/browser/src/methods/sendSignal.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 697945dd..80683438 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -41,7 +41,7 @@ function _callSignalAllAcceptedCredentials( return globalPublicKeyCredential.signalAllAcceptedCredentials({ rpId: opts.rpID, - userId: opts.userId, + userId: opts.userID, allAcceptedCredentialIds: opts.allAcceptedCredentialIds, }); } @@ -62,7 +62,7 @@ type SignalAllAcceptedCredentialsOpts = { /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; /** The base64url-encoded value used for `userID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ - userId: Base64URLString; + userID: Base64URLString; /** An array of base64url-encoded credential IDs for all credentials the user may use to authenticate */ allAcceptedCredentialIds: Base64URLString[]; }; @@ -83,7 +83,7 @@ type SignalCurrentUserDetailsOpts = { /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; /** The base64url-encoded value used for `userID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ - userId: Base64URLString; + userID: Base64URLString; /** The primary account name, like an email address, username, etc... */ userName: string; /** An optional, longer user identifier, like a full name, account differentiator, etc... Defaults to `""` */ From 3734b81eb5ca3e9eed80cfcd4fabe2951c57551f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 9 Jul 2026 21:35:55 -0700 Subject: [PATCH 5/9] Rearrange opts types for consistency --- packages/browser/src/methods/sendSignal.ts | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 80683438..04da46f0 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -46,6 +46,28 @@ function _callSignalAllAcceptedCredentials( }); } +/** + * A signal that communicates that the credential that the user just tried to register, or to + * authenticate with, was not one that the Relying Party recognizes. The authenticator responsible + * for the credential can hide or delete the credential so that the user does not see it in the + * future as an option to sign in with. + * + * It is a good idea for a Relying Party to send this signal immediately after the use of an + * unrecognized credential. For example, after rejecting the output from `startRegistration()` due + * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from + * `startAuthentication()` because the user deleted the passkey from their RP-specific user + * settings. + * + * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. + */ +export type SignalUnknownCredentialOpts = { + signalName: 'signalUnknownCredential'; + /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ + rpID: string; + /** The credential ID that the Relying Party didn't recognize for use */ + credentialID: Base64URLString; +}; + /** * A signal that communicates the current list of passkeys the Relying Party will recognize for use * by the **authenticated** user on the next login. Authenticators that have a passkey for @@ -57,7 +79,7 @@ function _callSignalAllAcceptedCredentials( * * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. */ -type SignalAllAcceptedCredentialsOpts = { +export type SignalAllAcceptedCredentialsOpts = { signalName: 'signalAllAcceptedCredentials'; /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; @@ -78,7 +100,7 @@ type SignalAllAcceptedCredentialsOpts = { * * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. */ -type SignalCurrentUserDetailsOpts = { +export type SignalCurrentUserDetailsOpts = { signalName: 'signalCurrentUserDetails'; /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; @@ -89,25 +111,3 @@ type SignalCurrentUserDetailsOpts = { /** An optional, longer user identifier, like a full name, account differentiator, etc... Defaults to `""` */ userDisplayName?: string; }; - -/** - * A signal that communicates that the credential that the user just tried to register, or to - * authenticate with, was not one that the Relying Party recognizes. The authenticator responsible - * for the credential can hide or delete the credential so that the user does not see it in the - * future as an option to sign in with. - * - * It is a good idea for a Relying Party to send this signal immediately after the use of an - * unrecognized credential. For example, after rejecting the output from `startRegistration()` due - * to unsatisfied RP-specific authenticator registration policy; or after rejecting the output from - * `startAuthentication()` because the user deleted the passkey from their RP-specific user - * settings. - * - * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. - */ -type SignalUnknownCredentialOpts = { - signalName: 'signalUnknownCredential'; - /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ - rpID: string; - /** The credential ID that the Relying Party didn't recognize for use */ - credentialID: Base64URLString; -}; From 5397f1de1272a11e95effeef65671659f1ef4f82 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 9 Jul 2026 21:36:31 -0700 Subject: [PATCH 6/9] Finish wrapping signalAllAcceptedCredentials() --- packages/browser/src/methods/sendSignal.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 04da46f0..ed553615 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -29,6 +29,9 @@ export async function sendSignal( }); } +/** + * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() + */ function _callSignalAllAcceptedCredentials( opts: SignalAllAcceptedCredentialsOpts, ): Promise { @@ -36,7 +39,9 @@ function _callSignalAllAcceptedCredentials( .PublicKeyCredential as unknown as PublicKeyCredentialFuture; if (typeof globalPublicKeyCredential.signalAllAcceptedCredentials !== 'function') { - throw new Error(''); + throw new Error( + 'This browser does not support PublicKeyCredential.signalAllAcceptedCredentials()', + ); } return globalPublicKeyCredential.signalAllAcceptedCredentials({ From b0b9a07682c5725866cc3d469e090dbad0120f49 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 9 Jul 2026 22:28:43 -0700 Subject: [PATCH 7/9] Identify WebAuthn signal errors --- .../src/helpers/identifySignalError.ts | 81 ++++++++++++++++++- packages/browser/src/helpers/webAuthnError.ts | 1 + 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/helpers/identifySignalError.ts b/packages/browser/src/helpers/identifySignalError.ts index f8673b06..f5745745 100644 --- a/packages/browser/src/helpers/identifySignalError.ts +++ b/packages/browser/src/helpers/identifySignalError.ts @@ -1,4 +1,81 @@ +import { isValidDomain } from './isValidDomain.ts'; +import { WebAuthnError } from './webAuthnError.ts'; +import type { + SignalAllAcceptedCredentialsOpts, + SignalCurrentUserDetailsOpts, + SignalUnknownCredentialOpts, +} from '../methods/sendSignal.ts'; + /** - * TODO: Use `WebAuthnError` here too to try and stop the bleeding of how many `throw new Error()` are - * introduced. This logic is specific to the signal APIs. + * Attempt to intuit _why_ an error was raised after calling one of the WebAuthn Signal APIs */ +export function identifySignalError({ error, options }: { + error: Error; + options: + | SignalUnknownCredentialOpts + | SignalAllAcceptedCredentialsOpts + | SignalCurrentUserDetailsOpts; +}): WebAuthnError { + /** + * General Signal API error conditions + */ + if (error.name === 'SecurityError') { + const effectiveDomain = globalThis.location.hostname; + if (!isValidDomain(effectiveDomain)) { + // https://w3c.github.io/webauthn/#sctn-signal-methods-async-rp-id-validation (Step 1) + return new WebAuthnError({ + message: `${globalThis.location.hostname} is an invalid domain`, + code: 'ERROR_INVALID_DOMAIN', + cause: error, + }); + } + + // https://w3c.github.io/webauthn/#sctn-signal-methods-async-rp-id-validation (Step 3) + return new WebAuthnError({ + message: + `The browser does not support Related Origins to enable signals for RP ID ${options.rpID} on this domain`, + code: 'ERROR_INVALID_RP_ID', + cause: error, + }); + } + + /** + * Signal-specific error conditions + */ + if (options.signalName === 'signalUnknownCredential') { + if (error.name === 'TypeError') { + // https://w3c.github.io/webauthn/#sctn-signalUnknownCredential (Step 1) + return new WebAuthnError({ + message: 'credentialID is an invalid base64url string', + code: 'ERROR_SIGNAL_INVALID_ARGUMENT', + cause: error, + }); + } + } else if (options.signalName === 'signalAllAcceptedCredentials') { + if (error.name === 'TypeError') { + // https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials (Step 1) + // https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials (Step 2) + return new WebAuthnError({ + message: 'userID, or an entry in allAcceptedCredentialIDs, is an invalid base64url string', + code: 'ERROR_SIGNAL_INVALID_ARGUMENT', + cause: error, + }); + } + } else if (options.signalName === 'signalCurrentUserDetails') { + if (error.name === 'TypeError') { + // https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails + return new WebAuthnError({ + message: 'userID is an invalid base64url string', + code: 'ERROR_SIGNAL_INVALID_ARGUMENT', + cause: error, + }); + } + } + + // Consistently return a WebAuthnError, but point to the original error for more info + return new WebAuthnError({ + message: error.message, + code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', + cause: error, + }); +} diff --git a/packages/browser/src/helpers/webAuthnError.ts b/packages/browser/src/helpers/webAuthnError.ts index 2b0efa71..1c20d744 100644 --- a/packages/browser/src/helpers/webAuthnError.ts +++ b/packages/browser/src/helpers/webAuthnError.ts @@ -48,4 +48,5 @@ export type WebAuthnErrorCode = | 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED' | 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG' | 'ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE' + | 'ERROR_SIGNAL_INVALID_ARGUMENT' | 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY'; From c6c1d6d040eb8afc94838f3adff3e45ccba2c957 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 9 Jul 2026 22:29:54 -0700 Subject: [PATCH 8/9] Finish wrapping signals --- packages/browser/src/methods/sendSignal.ts | 66 ++++++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index ed553615..02af4d65 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -1,4 +1,5 @@ import type { Base64URLString, PublicKeyCredentialFuture } from '../types/index.ts'; +import { identifySignalError } from '../helpers/identifySignalError.ts'; /** * Broadcast a passkey state change on the server to the browser to enlist the browser's help @@ -12,7 +13,7 @@ import type { Base64URLString, PublicKeyCredentialFuture } from '../types/index. * signal option types for guidance on how often a signal may need to be resent for maximum * efficacy. */ -export async function sendSignal( +export function sendSignal( opts: | SignalAllAcceptedCredentialsOpts | SignalCurrentUserDetailsOpts @@ -20,21 +21,43 @@ export async function sendSignal( ): Promise { const { signalName } = opts; - if (signalName === 'signalAllAcceptedCredentials') { - return _callSignalAllAcceptedCredentials(opts); - } else if (signalName) { + try { + if (signalName === 'signalUnknownCredential') { + return _callSignalUnknownCredential(opts); + } else if (signalName === 'signalAllAcceptedCredentials') { + return _callSignalAllAcceptedCredentials(opts); + } else if (signalName === 'signalCurrentUserDetails') { + return _callSignalCurrentUserDetails(opts); + } + } catch (err) { + throw identifySignalError({ error: err as Error, options: opts }); } - return new Promise((resolve, _) => { - resolve(undefined); + + // @ts-ignore: this should never happen, but just in case + throw new Error(`Received unrecognized signalName "${opts.signalName}"`); +} + +/** + * Wrapper for PublicKeyCredential.signalUnknownCredential() + */ +function _callSignalUnknownCredential(opts: SignalUnknownCredentialOpts) { + const globalPublicKeyCredential = globalThis + .PublicKeyCredential as unknown as PublicKeyCredentialFuture; + + if (typeof globalPublicKeyCredential.signalUnknownCredential !== 'function') { + throw new Error('This browser does not support PublicKeyCredential.signalUnknownCredential()'); + } + + return globalPublicKeyCredential.signalUnknownCredential({ + rpId: opts.rpID, + credentialId: opts.credentialID, }); } /** * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() */ -function _callSignalAllAcceptedCredentials( - opts: SignalAllAcceptedCredentialsOpts, -): Promise { +function _callSignalAllAcceptedCredentials(opts: SignalAllAcceptedCredentialsOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -47,7 +70,28 @@ function _callSignalAllAcceptedCredentials( return globalPublicKeyCredential.signalAllAcceptedCredentials({ rpId: opts.rpID, userId: opts.userID, - allAcceptedCredentialIds: opts.allAcceptedCredentialIds, + allAcceptedCredentialIds: opts.allAcceptedCredentialIDs, + }); +} + +/** + * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() + */ +function _callSignalCurrentUserDetails(opts: SignalCurrentUserDetailsOpts) { + const globalPublicKeyCredential = globalThis + .PublicKeyCredential as unknown as PublicKeyCredentialFuture; + + if (typeof globalPublicKeyCredential.signalCurrentUserDetails !== 'function') { + throw new Error( + 'This browser does not support PublicKeyCredential.signalCurrentUserDetails()', + ); + } + + return globalPublicKeyCredential.signalCurrentUserDetails({ + rpId: opts.rpID, + userId: opts.userID, + name: opts.userName, + displayName: opts.userDisplayName ?? '', }); } @@ -91,7 +135,7 @@ export type SignalAllAcceptedCredentialsOpts = { /** The base64url-encoded value used for `userID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ userID: Base64URLString; /** An array of base64url-encoded credential IDs for all credentials the user may use to authenticate */ - allAcceptedCredentialIds: Base64URLString[]; + allAcceptedCredentialIDs: Base64URLString[]; }; /** From 32798432956396e8aad8e3ee5bb55be76d519296 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 9 Jul 2026 22:30:12 -0700 Subject: [PATCH 9/9] Export everything from sendSignal.ts --- packages/browser/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 354157a5..99e6dbec 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,5 +1,6 @@ export * from './methods/startRegistration.ts'; export * from './methods/startAuthentication.ts'; +export * from './methods/sendSignal.ts'; export * from './helpers/browserSupportsWebAuthn.ts'; export * from './helpers/browserSupportsPasskeys.ts'; export * from './helpers/platformAuthenticatorIsAvailable.ts';