From f7f5db5d0322ee8002a2d1f7daaa3b9c482bf9df Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 8 Jul 2026 22:55:48 -0700 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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'; From 8abaf16778e3e9a119733554bd4c9f3961244cfc Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Mon, 13 Jul 2026 07:27:06 -0700 Subject: [PATCH 10/21] Update ordering to match rest of code --- packages/browser/src/methods/sendSignal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 02af4d65..ca0afda7 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -15,9 +15,9 @@ import { identifySignalError } from '../helpers/identifySignalError.ts'; */ export function sendSignal( opts: + | SignalUnknownCredentialOpts | SignalAllAcceptedCredentialsOpts - | SignalCurrentUserDetailsOpts - | SignalUnknownCredentialOpts, + | SignalCurrentUserDetailsOpts, ): Promise { const { signalName } = opts; From 02f834716a03e4787408fde1ab2ef445ad997142 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 21:53:44 -0700 Subject: [PATCH 11/21] Shorten signalNames --- packages/browser/src/helpers/identifySignalError.ts | 6 +++--- packages/browser/src/methods/sendSignal.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/browser/src/helpers/identifySignalError.ts b/packages/browser/src/helpers/identifySignalError.ts index f5745745..d212fcc8 100644 --- a/packages/browser/src/helpers/identifySignalError.ts +++ b/packages/browser/src/helpers/identifySignalError.ts @@ -42,7 +42,7 @@ export function identifySignalError({ error, options }: { /** * Signal-specific error conditions */ - if (options.signalName === 'signalUnknownCredential') { + if (options.signalName === 'unknownCredential') { if (error.name === 'TypeError') { // https://w3c.github.io/webauthn/#sctn-signalUnknownCredential (Step 1) return new WebAuthnError({ @@ -51,7 +51,7 @@ export function identifySignalError({ error, options }: { cause: error, }); } - } else if (options.signalName === 'signalAllAcceptedCredentials') { + } else if (options.signalName === 'allAcceptedCredentials') { if (error.name === 'TypeError') { // https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials (Step 1) // https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials (Step 2) @@ -61,7 +61,7 @@ export function identifySignalError({ error, options }: { cause: error, }); } - } else if (options.signalName === 'signalCurrentUserDetails') { + } else if (options.signalName === 'currentUserDetails') { if (error.name === 'TypeError') { // https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails return new WebAuthnError({ diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index ca0afda7..b57afdc0 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -22,11 +22,11 @@ export function sendSignal( const { signalName } = opts; try { - if (signalName === 'signalUnknownCredential') { + if (signalName === 'unknownCredential') { return _callSignalUnknownCredential(opts); - } else if (signalName === 'signalAllAcceptedCredentials') { + } else if (signalName === 'allAcceptedCredentials') { return _callSignalAllAcceptedCredentials(opts); - } else if (signalName === 'signalCurrentUserDetails') { + } else if (signalName === 'currentUserDetails') { return _callSignalCurrentUserDetails(opts); } } catch (err) { @@ -110,7 +110,7 @@ function _callSignalCurrentUserDetails(opts: SignalCurrentUserDetailsOpts) { * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. */ export type SignalUnknownCredentialOpts = { - signalName: 'signalUnknownCredential'; + signalName: 'unknownCredential'; /** 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 */ @@ -129,7 +129,7 @@ export type SignalUnknownCredentialOpts = { * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. */ export type SignalAllAcceptedCredentialsOpts = { - signalName: 'signalAllAcceptedCredentials'; + signalName: 'allAcceptedCredentials'; /** 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()` */ @@ -150,7 +150,7 @@ export type SignalAllAcceptedCredentialsOpts = { * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. */ export type SignalCurrentUserDetailsOpts = { - signalName: 'signalCurrentUserDetails'; + signalName: 'currentUserDetails'; /** 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()` */ From ed67386d094a71f45395c0446bf0f249100796f5 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:10:33 -0700 Subject: [PATCH 12/21] Rename sendSignal opts types --- .../browser/src/helpers/identifySignalError.ts | 12 ++++++------ packages/browser/src/methods/sendSignal.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/browser/src/helpers/identifySignalError.ts b/packages/browser/src/helpers/identifySignalError.ts index d212fcc8..8e7daaa5 100644 --- a/packages/browser/src/helpers/identifySignalError.ts +++ b/packages/browser/src/helpers/identifySignalError.ts @@ -1,9 +1,9 @@ import { isValidDomain } from './isValidDomain.ts'; import { WebAuthnError } from './webAuthnError.ts'; import type { - SignalAllAcceptedCredentialsOpts, - SignalCurrentUserDetailsOpts, - SignalUnknownCredentialOpts, + SendSignalAllAcceptedCredentialsOpts, + SendSignalCurrentUserDetailsOpts, + SendSignalUnknownCredentialOpts, } from '../methods/sendSignal.ts'; /** @@ -12,9 +12,9 @@ import type { export function identifySignalError({ error, options }: { error: Error; options: - | SignalUnknownCredentialOpts - | SignalAllAcceptedCredentialsOpts - | SignalCurrentUserDetailsOpts; + | SendSignalUnknownCredentialOpts + | SendSignalAllAcceptedCredentialsOpts + | SendSignalCurrentUserDetailsOpts; }): WebAuthnError { /** * General Signal API error conditions diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index b57afdc0..c0023304 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -15,9 +15,9 @@ import { identifySignalError } from '../helpers/identifySignalError.ts'; */ export function sendSignal( opts: - | SignalUnknownCredentialOpts - | SignalAllAcceptedCredentialsOpts - | SignalCurrentUserDetailsOpts, + | SendSignalUnknownCredentialOpts + | SendSignalAllAcceptedCredentialsOpts + | SendSignalCurrentUserDetailsOpts, ): Promise { const { signalName } = opts; @@ -40,7 +40,7 @@ export function sendSignal( /** * Wrapper for PublicKeyCredential.signalUnknownCredential() */ -function _callSignalUnknownCredential(opts: SignalUnknownCredentialOpts) { +function _callSignalUnknownCredential(opts: SendSignalUnknownCredentialOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -57,7 +57,7 @@ function _callSignalUnknownCredential(opts: SignalUnknownCredentialOpts) { /** * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() */ -function _callSignalAllAcceptedCredentials(opts: SignalAllAcceptedCredentialsOpts) { +function _callSignalAllAcceptedCredentials(opts: SendSignalAllAcceptedCredentialsOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -77,7 +77,7 @@ function _callSignalAllAcceptedCredentials(opts: SignalAllAcceptedCredentialsOpt /** * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() */ -function _callSignalCurrentUserDetails(opts: SignalCurrentUserDetailsOpts) { +function _callSignalCurrentUserDetails(opts: SendSignalCurrentUserDetailsOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -109,7 +109,7 @@ function _callSignalCurrentUserDetails(opts: SignalCurrentUserDetailsOpts) { * * See https://w3c.github.io/webauthn/#sctn-signalUnknownCredential for more info. */ -export type SignalUnknownCredentialOpts = { +export type SendSignalUnknownCredentialOpts = { signalName: 'unknownCredential'; /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; @@ -128,7 +128,7 @@ export type SignalUnknownCredentialOpts = { * * See https://w3c.github.io/webauthn/#sctn-signalAllAcceptedCredentials for more info. */ -export type SignalAllAcceptedCredentialsOpts = { +export type SendSignalAllAcceptedCredentialsOpts = { signalName: 'allAcceptedCredentials'; /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; @@ -149,7 +149,7 @@ export type SignalAllAcceptedCredentialsOpts = { * * See https://w3c.github.io/webauthn/#sctn-signalCurrentUserDetails for more info. */ -export type SignalCurrentUserDetailsOpts = { +export type SendSignalCurrentUserDetailsOpts = { signalName: 'currentUserDetails'; /** The same value used for `rpID` when calling \@simplewebauthn/server's `generateRegistrationOptions()` */ rpID: string; From 9a03bfd3a0181e331d7a90f686e9f5e75fd6d874 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:11:09 -0700 Subject: [PATCH 13/21] Ignore a linting rule because I am the captain now --- packages/browser/deno.json | 7 +++++++ packages/browser/src/methods/sendSignal.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/browser/deno.json b/packages/browser/deno.json index 88d1beb0..b2a3dc7d 100644 --- a/packages/browser/deno.json +++ b/packages/browser/deno.json @@ -44,5 +44,12 @@ "./src/**/__jest__", "./npm" ] + }, + "lint": { + "rules": { + "exclude": [ + "require-await" + ] + } } } diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index c0023304..30aa1461 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -13,7 +13,7 @@ import { identifySignalError } from '../helpers/identifySignalError.ts'; * signal option types for guidance on how often a signal may need to be resent for maximum * efficacy. */ -export function sendSignal( +export async function sendSignal( opts: | SendSignalUnknownCredentialOpts | SendSignalAllAcceptedCredentialsOpts From 71c010bb38797108019ce756c5ce3d4e449bd73f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:51:06 -0700 Subject: [PATCH 14/21] Refine some signal error messaging --- packages/browser/src/helpers/identifySignalError.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/helpers/identifySignalError.ts b/packages/browser/src/helpers/identifySignalError.ts index 8e7daaa5..ad027510 100644 --- a/packages/browser/src/helpers/identifySignalError.ts +++ b/packages/browser/src/helpers/identifySignalError.ts @@ -24,7 +24,7 @@ export function identifySignalError({ error, options }: { 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`, + message: `"${globalThis.location.hostname}" is an invalid domain`, code: 'ERROR_INVALID_DOMAIN', cause: error, }); @@ -33,7 +33,7 @@ export function identifySignalError({ error, options }: { // 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`, + `The browser does not support Related Origins to enable signals for RP ID "${options.rpID}" on domain "${globalThis.location.hostname}"`, code: 'ERROR_INVALID_RP_ID', cause: error, }); From 30549d634ccef9181ffc59caf200913ecb748cfe Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:51:23 -0700 Subject: [PATCH 15/21] Fix signal error identification --- packages/browser/src/methods/sendSignal.ts | 70 +++++++++++++--------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 30aa1461..77e9f541 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -21,16 +21,12 @@ export async function sendSignal( ): Promise { const { signalName } = opts; - try { - if (signalName === 'unknownCredential') { - return _callSignalUnknownCredential(opts); - } else if (signalName === 'allAcceptedCredentials') { - return _callSignalAllAcceptedCredentials(opts); - } else if (signalName === 'currentUserDetails') { - return _callSignalCurrentUserDetails(opts); - } - } catch (err) { - throw identifySignalError({ error: err as Error, options: opts }); + if (signalName === 'unknownCredential') { + return _callSignalUnknownCredential(opts); + } else if (signalName === 'allAcceptedCredentials') { + return _callSignalAllAcceptedCredentials(opts); + } else if (signalName === 'currentUserDetails') { + return _callSignalCurrentUserDetails(opts); } // @ts-ignore: this should never happen, but just in case @@ -40,7 +36,7 @@ export async function sendSignal( /** * Wrapper for PublicKeyCredential.signalUnknownCredential() */ -function _callSignalUnknownCredential(opts: SendSignalUnknownCredentialOpts) { +async function _callSignalUnknownCredential(opts: SendSignalUnknownCredentialOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -48,16 +44,22 @@ function _callSignalUnknownCredential(opts: SendSignalUnknownCredentialOpts) { throw new Error('This browser does not support PublicKeyCredential.signalUnknownCredential()'); } - return globalPublicKeyCredential.signalUnknownCredential({ - rpId: opts.rpID, - credentialId: opts.credentialID, - }); + try { + await globalPublicKeyCredential.signalUnknownCredential({ + rpId: opts.rpID, + credentialId: opts.credentialID, + }); + } catch (err) { + throw identifySignalError({ error: err as Error, options: opts }); + } + + return undefined; } /** * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() */ -function _callSignalAllAcceptedCredentials(opts: SendSignalAllAcceptedCredentialsOpts) { +async function _callSignalAllAcceptedCredentials(opts: SendSignalAllAcceptedCredentialsOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -67,17 +69,23 @@ function _callSignalAllAcceptedCredentials(opts: SendSignalAllAcceptedCredential ); } - return globalPublicKeyCredential.signalAllAcceptedCredentials({ - rpId: opts.rpID, - userId: opts.userID, - allAcceptedCredentialIds: opts.allAcceptedCredentialIDs, - }); + try { + await globalPublicKeyCredential.signalAllAcceptedCredentials({ + rpId: opts.rpID, + userId: opts.userID, + allAcceptedCredentialIds: opts.allAcceptedCredentialIDs, + }); + } catch (err) { + throw identifySignalError({ error: err as Error, options: opts }); + } + + return undefined; } /** * Wrapper for PublicKeyCredential.signalAllAcceptedCredentials() */ -function _callSignalCurrentUserDetails(opts: SendSignalCurrentUserDetailsOpts) { +async function _callSignalCurrentUserDetails(opts: SendSignalCurrentUserDetailsOpts) { const globalPublicKeyCredential = globalThis .PublicKeyCredential as unknown as PublicKeyCredentialFuture; @@ -87,12 +95,18 @@ function _callSignalCurrentUserDetails(opts: SendSignalCurrentUserDetailsOpts) { ); } - return globalPublicKeyCredential.signalCurrentUserDetails({ - rpId: opts.rpID, - userId: opts.userID, - name: opts.userName, - displayName: opts.userDisplayName ?? '', - }); + try { + await globalPublicKeyCredential.signalCurrentUserDetails({ + rpId: opts.rpID, + userId: opts.userID, + name: opts.userName, + displayName: opts.userDisplayName ?? '', + }); + } catch (err) { + throw identifySignalError({ error: err as Error, options: opts }); + } + + return undefined; } /** From 9d73df0ea0db02136475df949810910fd0413b95 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:51:32 -0700 Subject: [PATCH 16/21] Add sendSignal tests --- .../helpers/__jest__/generateCustomError.ts | 1 + .../browser/src/methods/sendSignal.test.ts | 288 ++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 packages/browser/src/methods/sendSignal.test.ts diff --git a/packages/browser/src/helpers/__jest__/generateCustomError.ts b/packages/browser/src/helpers/__jest__/generateCustomError.ts index 25609fad..734e681c 100644 --- a/packages/browser/src/helpers/__jest__/generateCustomError.ts +++ b/packages/browser/src/helpers/__jest__/generateCustomError.ts @@ -8,6 +8,7 @@ type WebAuthnErrorName = | 'NotAllowedError' | 'NotSupportedError' | 'SecurityError' + | 'TypeError' | 'UnknownError'; export function generateCustomError( diff --git a/packages/browser/src/methods/sendSignal.test.ts b/packages/browser/src/methods/sendSignal.test.ts new file mode 100644 index 00000000..d994270a --- /dev/null +++ b/packages/browser/src/methods/sendSignal.test.ts @@ -0,0 +1,288 @@ +/// +import { + assert, + assertEquals, + assertExists, + assertInstanceOf, + assertRejects, + assertStringIncludes, +} from '@std/assert'; +import { assertSpyCall, assertSpyCalls, type Spy, spy, stub } from '@std/testing/mock'; +import { afterEach, beforeEach, describe, it } from '@std/testing/bdd'; + +import { generateCustomError } from '../helpers/__jest__/generateCustomError.ts'; +import type { Base64URLString } from '../types/index.ts'; +import { WebAuthnError } from '../helpers/webAuthnError.ts'; +import { + sendSignal, + type SendSignalAllAcceptedCredentialsOpts, + type SendSignalCurrentUserDetailsOpts, + type SendSignalUnknownCredentialOpts, +} from './sendSignal.ts'; + +const credentialID: Base64URLString = 'NYtMO7dYULX2NcXrpzp5ig'; +const rpID = 'simplewebauthn.dev'; +const userID = 'uHqO6EqtKu7UIG074emo5w'; +const userName = 'SimpleWebAuthn'; +const userDisplayName = 'SimpleWebAuthn (Browser)'; + +describe('Method: sendSignal()', () => { + describe('Signal: unknownCredential', () => { + let signalUnknownCredentialSpy: Spy; + const signalName: SendSignalUnknownCredentialOpts['signalName'] = 'unknownCredential'; + + beforeEach(() => { + signalUnknownCredentialSpy = spy(); + + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalUnknownCredential + globalThis.PublicKeyCredential.signalUnknownCredential = signalUnknownCredentialSpy; + }); + + it('should call PublicKeyCredential.signalUnknownCredential', async () => { + const returned = await sendSignal({ signalName, rpID, credentialID }); + + assertSpyCalls(signalUnknownCredentialSpy, 1); + assertSpyCall(signalUnknownCredentialSpy, 0, { + args: [{ rpId: rpID, credentialId: credentialID }], + }); + + assertEquals(returned, undefined); + }); + + it('should reject when signal is unsupported', async () => { + // @ts-ignore: Intentionally deleting this + delete globalThis.PublicKeyCredential.signalUnknownCredential; + + await assertRejects(() => sendSignal({ signalName, rpID, credentialID })); + }); + + it('should identify incorrectly Base64URL-encoded credential ID', async () => { + const TypeError = generateCustomError('TypeError'); + signalUnknownCredentialSpy = spy(async () => { + throw TypeError; + }); + + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalUnknownCredential + globalThis.PublicKeyCredential.signalUnknownCredential = signalUnknownCredentialSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName, rpID, credentialID }), + WebAuthnError, + 'invalid base64url string', + ); + + assertEquals(rejected.name, 'TypeError'); + assertEquals(rejected.code, 'ERROR_SIGNAL_INVALID_ARGUMENT'); + assertEquals(rejected.cause, TypeError); + }); + }); + + describe('Signal: allAcceptedCredentials', () => { + let signalAllAcceptedCredentialsSpy: Spy; + const signalName: SendSignalAllAcceptedCredentialsOpts['signalName'] = 'allAcceptedCredentials'; + + beforeEach(() => { + signalAllAcceptedCredentialsSpy = spy(); + + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalAllAcceptedCredentials + globalThis.PublicKeyCredential.signalAllAcceptedCredentials = signalAllAcceptedCredentialsSpy; + }); + + it('should call PublicKeyCredential.signalAllAcceptedCredentials', async () => { + const returned = await sendSignal({ + signalName, + rpID, + userID, + allAcceptedCredentialIDs: [credentialID], + }); + + assertSpyCalls(signalAllAcceptedCredentialsSpy, 1); + assertSpyCall(signalAllAcceptedCredentialsSpy, 0, { + args: [{ rpId: rpID, userId: userID, allAcceptedCredentialIds: [credentialID] }], + }); + + assertEquals(returned, undefined); + }); + + it('should reject when signal is unsupported', async () => { + // @ts-ignore: Intentionally deleting this + delete globalThis.PublicKeyCredential.signalAllAcceptedCredentials; + + await assertRejects(() => + sendSignal({ signalName, rpID, userID, allAcceptedCredentialIDs: [credentialID] }) + ); + }); + + it('should identify incorrectly Base64URL-encoded userID or credential ID', async () => { + const TypeError = generateCustomError('TypeError'); + signalAllAcceptedCredentialsSpy = spy(async () => { + throw TypeError; + }); + + // @ts-ignore: Set up signalAllAcceptedCredentials + globalThis.PublicKeyCredential.signalAllAcceptedCredentials = signalAllAcceptedCredentialsSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName, rpID, userID, allAcceptedCredentialIDs: [credentialID] }), + WebAuthnError, + 'invalid base64url string', + ); + + assertEquals(rejected.name, 'TypeError'); + assertEquals(rejected.code, 'ERROR_SIGNAL_INVALID_ARGUMENT'); + assertEquals(rejected.cause, TypeError); + }); + }); + + describe('Signal: currentUserDetails', () => { + let signalCurrentUserDetailsSpy: Spy; + const signalName: SendSignalCurrentUserDetailsOpts['signalName'] = 'currentUserDetails'; + + beforeEach(() => { + signalCurrentUserDetailsSpy = spy(); + + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalCurrentUserDetails + globalThis.PublicKeyCredential.signalCurrentUserDetails = signalCurrentUserDetailsSpy; + }); + + it('should call PublicKeyCredential.signalCurrentUserDetails', async () => { + const returned = await sendSignal({ signalName, rpID, userID, userName, userDisplayName }); + + assertSpyCalls(signalCurrentUserDetailsSpy, 1); + assertSpyCall(signalCurrentUserDetailsSpy, 0, { + args: [{ rpId: rpID, userId: userID, name: userName, displayName: userDisplayName }], + }); + + assertEquals(returned, undefined); + }); + + it('should reject when signal is unsupported', async () => { + // @ts-ignore: Intentionally deleting this + delete globalThis.PublicKeyCredential.signalCurrentUserDetails; + + await assertRejects(() => + sendSignal({ signalName, rpID, userID, userName, userDisplayName }) + ); + }); + + it('should default to empty displayName when omitted', async () => { + const returned = await sendSignal({ signalName, rpID, userID, userName }); + + assertSpyCalls(signalCurrentUserDetailsSpy, 1); + assertSpyCall(signalCurrentUserDetailsSpy, 0, { + args: [{ rpId: rpID, userId: userID, name: userName, displayName: '' }], + }); + + assertEquals(returned, undefined); + }); + + it('should identify incorrectly Base64URL-encoded userID', async () => { + const TypeError = generateCustomError('TypeError'); + signalCurrentUserDetailsSpy = spy(async () => { + throw TypeError; + }); + + // @ts-ignore: Set up signalCurrentUserDetails + globalThis.PublicKeyCredential.signalCurrentUserDetails = signalCurrentUserDetailsSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName, rpID, userID, userName }), + WebAuthnError, + 'invalid base64url string', + ); + + assertEquals(rejected.name, 'TypeError'); + assertEquals(rejected.code, 'ERROR_SIGNAL_INVALID_ARGUMENT'); + assertEquals(rejected.cause, TypeError); + }); + }); + + it('should identify invalid RP ID for domain when sending signal', async () => { + /** + * I'm just testing one of the signals for now, this error is not specific to any of them + */ + const SecurityError = generateCustomError('SecurityError'); + const signalUnknownCredentialSpy = spy(async () => { + throw SecurityError; + }); + + // @ts-ignore: Setting up globalThis.location.hostname + globalThis.location = { hostname: 'localhost2' } as unknown; + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalUnknownCredential + globalThis.PublicKeyCredential.signalUnknownCredential = signalUnknownCredentialSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName: 'unknownCredential', rpID, credentialID }), + WebAuthnError, + 'invalid domain', + ); + + assertEquals(rejected.name, 'SecurityError'); + assertEquals(rejected.code, 'ERROR_INVALID_DOMAIN'); + assertEquals(rejected.cause, SecurityError); + }); + + it('should identify missing Related Origins support', async () => { + /** + * I'm just testing one of the signals for now, this error is not specific to any of them + */ + const SecurityError = generateCustomError('SecurityError'); + const signalUnknownCredentialSpy = spy(async () => { + throw SecurityError; + }); + + // @ts-ignore: Setting up globalThis.location.hostname + globalThis.location = { hostname: 'localhost' } as unknown; + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalUnknownCredential + globalThis.PublicKeyCredential.signalUnknownCredential = signalUnknownCredentialSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName: 'unknownCredential', rpID, credentialID }), + WebAuthnError, + 'does not support Related Origins', + ); + + assertEquals(rejected.name, 'SecurityError'); + assertEquals(rejected.code, 'ERROR_INVALID_RP_ID'); + assertEquals(rejected.cause, SecurityError); + }); + + it('should default to passing through original error', async () => { + /** + * I'm just testing one of the signals for now, this error is not specific to any of them + */ + // This error isn't one expected to be raised by a signal + const ConstraintError = generateCustomError('ConstraintError'); + const signalUnknownCredentialSpy = spy(async () => { + throw ConstraintError; + }); + + // @ts-ignore: Setting up globalThis.location.hostname + globalThis.location = { hostname: 'localhost' } as unknown; + // @ts-ignore: Set up PublicKeyCredential + globalThis.PublicKeyCredential = () => {}; + // @ts-ignore: Set up signalUnknownCredential + globalThis.PublicKeyCredential.signalUnknownCredential = signalUnknownCredentialSpy; + + const rejected = await assertRejects( + () => sendSignal({ signalName: 'unknownCredential', rpID, credentialID }), + WebAuthnError, + ); + + assertEquals(rejected.name, 'ConstraintError'); + assertEquals(rejected.code, 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY'); + assertEquals(rejected.cause, ConstraintError); + }); +}); From ebb72075aa2632870f858e5c916e78b54a97ea56 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:52:58 -0700 Subject: [PATCH 17/21] Refine test messages --- packages/browser/src/methods/sendSignal.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.test.ts b/packages/browser/src/methods/sendSignal.test.ts index d994270a..4182604f 100644 --- a/packages/browser/src/methods/sendSignal.test.ts +++ b/packages/browser/src/methods/sendSignal.test.ts @@ -205,7 +205,7 @@ describe('Method: sendSignal()', () => { }); }); - it('should identify invalid RP ID for domain when sending signal', async () => { + it('should identify invalid RP ID for domain when sending any signal', async () => { /** * I'm just testing one of the signals for now, this error is not specific to any of them */ @@ -232,7 +232,7 @@ describe('Method: sendSignal()', () => { assertEquals(rejected.cause, SecurityError); }); - it('should identify missing Related Origins support', async () => { + it('should identify missing Related Origins support when sending any signal', async () => { /** * I'm just testing one of the signals for now, this error is not specific to any of them */ @@ -259,7 +259,7 @@ describe('Method: sendSignal()', () => { assertEquals(rejected.cause, SecurityError); }); - it('should default to passing through original error', async () => { + it('should default to passing through original error when sending any signal', async () => { /** * I'm just testing one of the signals for now, this error is not specific to any of them */ From 9665c257ff6de77e79ef712b1a96c08b621dd038 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:54:12 -0700 Subject: [PATCH 18/21] Clarify some ts-ignore definitions --- packages/browser/src/methods/sendSignal.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.test.ts b/packages/browser/src/methods/sendSignal.test.ts index 4182604f..6fdfece6 100644 --- a/packages/browser/src/methods/sendSignal.test.ts +++ b/packages/browser/src/methods/sendSignal.test.ts @@ -214,7 +214,7 @@ describe('Method: sendSignal()', () => { throw SecurityError; }); - // @ts-ignore: Setting up globalThis.location.hostname + // @ts-ignore: Setting up globalThis.location.hostname to be an invalid domain globalThis.location = { hostname: 'localhost2' } as unknown; // @ts-ignore: Set up PublicKeyCredential globalThis.PublicKeyCredential = () => {}; @@ -241,7 +241,7 @@ describe('Method: sendSignal()', () => { throw SecurityError; }); - // @ts-ignore: Setting up globalThis.location.hostname + // @ts-ignore: Setting up globalThis.location.hostname to be a valid domain globalThis.location = { hostname: 'localhost' } as unknown; // @ts-ignore: Set up PublicKeyCredential globalThis.PublicKeyCredential = () => {}; @@ -269,8 +269,6 @@ describe('Method: sendSignal()', () => { throw ConstraintError; }); - // @ts-ignore: Setting up globalThis.location.hostname - globalThis.location = { hostname: 'localhost' } as unknown; // @ts-ignore: Set up PublicKeyCredential globalThis.PublicKeyCredential = () => {}; // @ts-ignore: Set up signalUnknownCredential From 77d985be1ec8f4cd99115b6eaddf449acb1e534a Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:55:14 -0700 Subject: [PATCH 19/21] Clean up test imports --- packages/browser/src/methods/sendSignal.test.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.test.ts b/packages/browser/src/methods/sendSignal.test.ts index 6fdfece6..c0db969a 100644 --- a/packages/browser/src/methods/sendSignal.test.ts +++ b/packages/browser/src/methods/sendSignal.test.ts @@ -1,14 +1,6 @@ -/// -import { - assert, - assertEquals, - assertExists, - assertInstanceOf, - assertRejects, - assertStringIncludes, -} from '@std/assert'; -import { assertSpyCall, assertSpyCalls, type Spy, spy, stub } from '@std/testing/mock'; -import { afterEach, beforeEach, describe, it } from '@std/testing/bdd'; +import { assertEquals, assertRejects } from '@std/assert'; +import { assertSpyCall, assertSpyCalls, type Spy, spy } from '@std/testing/mock'; +import { beforeEach, describe, it } from '@std/testing/bdd'; import { generateCustomError } from '../helpers/__jest__/generateCustomError.ts'; import type { Base64URLString } from '../types/index.ts'; From 5c0ff9ae8fdc6bbcf1fec5a15e2d87c295c550b3 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 29 Jul 2026 22:58:50 -0700 Subject: [PATCH 20/21] Add package export test for sendSignal --- packages/browser/src/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts index 19e52be9..93fc04bd 100644 --- a/packages/browser/src/index.test.ts +++ b/packages/browser/src/index.test.ts @@ -42,3 +42,7 @@ Deno.test('should export method `getBrowserCapabilities`', () => { Deno.test('should export method `browserSupportsPasskeys`', () => { assert(index.browserSupportsPasskeys); }); + +Deno.test('should export method `sendSignal`', () => { + assert(index.sendSignal); +}); From f08090a83e75b1dcc8ac91d45af0b046692f61d1 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 31 Jul 2026 20:07:01 -0700 Subject: [PATCH 21/21] Migrate sendSignal arg types into types/ --- packages/browser/src/methods/sendSignal.ts | 79 ++++------------------ packages/browser/src/types/index.ts | 72 ++++++++++++++++++++ packages/server/src/types/index.ts | 72 ++++++++++++++++++++ packages/types/src/index.ts | 72 ++++++++++++++++++++ 4 files changed, 228 insertions(+), 67 deletions(-) diff --git a/packages/browser/src/methods/sendSignal.ts b/packages/browser/src/methods/sendSignal.ts index 77e9f541..1b24aaa1 100644 --- a/packages/browser/src/methods/sendSignal.ts +++ b/packages/browser/src/methods/sendSignal.ts @@ -1,6 +1,17 @@ -import type { Base64URLString, PublicKeyCredentialFuture } from '../types/index.ts'; +import type { + PublicKeyCredentialFuture, + SendSignalAllAcceptedCredentialsOpts, + SendSignalCurrentUserDetailsOpts, + SendSignalUnknownCredentialOpts, +} from '../types/index.ts'; import { identifySignalError } from '../helpers/identifySignalError.ts'; +export type { + SendSignalAllAcceptedCredentialsOpts, + SendSignalCurrentUserDetailsOpts, + SendSignalUnknownCredentialOpts, +} 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 @@ -108,69 +119,3 @@ async function _callSignalCurrentUserDetails(opts: SendSignalCurrentUserDetailsO return undefined; } - -/** - * 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 SendSignalUnknownCredentialOpts = { - signalName: 'unknownCredential'; - /** 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 - * (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. - */ -export type SendSignalAllAcceptedCredentialsOpts = { - signalName: 'allAcceptedCredentials'; - /** 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. - */ -export type SendSignalCurrentUserDetailsOpts = { - signalName: 'currentUserDetails'; - /** 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; -}; diff --git a/packages/browser/src/types/index.ts b/packages/browser/src/types/index.ts index 9c657a72..2600c438 100644 --- a/packages/browser/src/types/index.ts +++ b/packages/browser/src/types/index.ts @@ -348,3 +348,75 @@ type CurrentUserDetailsOptions = { name: string; displayName: string; }; + +/** + * Below are types for @simplewebauthn/browser's `sendSignal()` method. Shared out of here so that an RP + * might use these same types in @simplewebauthn/server to type an API return value that can be + * passed into `sendSignal()` + */ + +/** + * 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 SendSignalUnknownCredentialOpts = { + signalName: 'unknownCredential'; + /** 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 + * (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. + */ +export type SendSignalAllAcceptedCredentialsOpts = { + signalName: 'allAcceptedCredentials'; + /** 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. + */ +export type SendSignalCurrentUserDetailsOpts = { + signalName: 'currentUserDetails'; + /** 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; +}; diff --git a/packages/server/src/types/index.ts b/packages/server/src/types/index.ts index 9c657a72..2600c438 100644 --- a/packages/server/src/types/index.ts +++ b/packages/server/src/types/index.ts @@ -348,3 +348,75 @@ type CurrentUserDetailsOptions = { name: string; displayName: string; }; + +/** + * Below are types for @simplewebauthn/browser's `sendSignal()` method. Shared out of here so that an RP + * might use these same types in @simplewebauthn/server to type an API return value that can be + * passed into `sendSignal()` + */ + +/** + * 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 SendSignalUnknownCredentialOpts = { + signalName: 'unknownCredential'; + /** 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 + * (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. + */ +export type SendSignalAllAcceptedCredentialsOpts = { + signalName: 'allAcceptedCredentials'; + /** 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. + */ +export type SendSignalCurrentUserDetailsOpts = { + signalName: 'currentUserDetails'; + /** 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; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9f4a8f15..e2402855 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -338,3 +338,75 @@ type CurrentUserDetailsOptions = { name: string; displayName: string; }; + +/** + * Below are types for @simplewebauthn/browser's `sendSignal()` method. Shared out of here so that an RP + * might use these same types in @simplewebauthn/server to type an API return value that can be + * passed into `sendSignal()` + */ + +/** + * 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 SendSignalUnknownCredentialOpts = { + signalName: 'unknownCredential'; + /** 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 + * (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. + */ +export type SendSignalAllAcceptedCredentialsOpts = { + signalName: 'allAcceptedCredentials'; + /** 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. + */ +export type SendSignalCurrentUserDetailsOpts = { + signalName: 'currentUserDetails'; + /** 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; +};