From 7493ca0e90991087a0ca466a89780bb06fed1741 Mon Sep 17 00:00:00 2001 From: MagentaManifold <17zhaomingyuan@gmail.com> Date: Wed, 25 Feb 2026 15:36:35 -0500 Subject: [PATCH] feat(passkey): create server side helper functions Because: * we need helper functions for passkeys This commit: * creates helpers wrapping @simplewebauthn/server library functions Closes FXA-13056 --- libs/accounts/passkey/README.md | 4 + libs/accounts/passkey/src/index.ts | 1 + .../passkey/src/lib/passkey.config.ts | 29 +- .../passkey/src/lib/webauthn-adapter.spec.ts | 405 ++++++++++++++++++ .../passkey/src/lib/webauthn-adapter.ts | 240 +++++++++++ package.json | 1 + yarn.lock | 211 ++++++++- 7 files changed, 882 insertions(+), 9 deletions(-) create mode 100644 libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts create mode 100644 libs/accounts/passkey/src/lib/webauthn-adapter.ts diff --git a/libs/accounts/passkey/README.md b/libs/accounts/passkey/README.md index 2e1ed0c99d6..59e4163cf99 100644 --- a/libs/accounts/passkey/README.md +++ b/libs/accounts/passkey/README.md @@ -54,6 +54,10 @@ This library follows the layered architecture pattern used across `libs/accounts - Validated with class-validator decorators - Loaded from Convict config in consuming applications +6. **WebAuthn Adapter** (`webauthn-adapter.ts`) + - Thin wrapper around `@simplewebauthn/server` v13. + - Transforms between repository data structures and library format + ### Pattern: No Module Export Unlike `libs/shared/nestjs/*`, this library **does not export a NestJS module**. This is intentional: diff --git a/libs/accounts/passkey/src/index.ts b/libs/accounts/passkey/src/index.ts index d05ffd19b0b..2e601b32c35 100644 --- a/libs/accounts/passkey/src/index.ts +++ b/libs/accounts/passkey/src/index.ts @@ -25,3 +25,4 @@ export * from './lib/passkey.manager'; export * from './lib/passkey.repository'; export * from './lib/passkey.errors'; export * from './lib/passkey.config'; +export * from './lib/webauthn-adapter'; diff --git a/libs/accounts/passkey/src/lib/passkey.config.ts b/libs/accounts/passkey/src/lib/passkey.config.ts index 59677314f58..5e14502038c 100644 --- a/libs/accounts/passkey/src/lib/passkey.config.ts +++ b/libs/accounts/passkey/src/lib/passkey.config.ts @@ -2,7 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { IsArray, IsBoolean, IsNumber, IsString } from 'class-validator'; +import { + IsArray, + IsBoolean, + IsIn, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import type { + AuthenticatorAttachment, + ResidentKeyRequirement, + UserVerificationRequirement, +} from '@simplewebauthn/server'; /** * Configuration for passkey (WebAuthn) functionality. @@ -59,8 +71,9 @@ export class PasskeyConfig { * - 'discouraged': User verification should not occur * @example 'required' */ - @IsString() - public userVerification?: 'required' | 'preferred' | 'discouraged'; + @IsOptional() + @IsIn(['required', 'preferred', 'discouraged']) + public userVerification?: UserVerificationRequirement; /** * Resident key (discoverable credential) requirement. @@ -72,8 +85,9 @@ export class PasskeyConfig { * - 'discouraged': Non-discoverable credential preferred * @example 'required' */ - @IsString() - public residentKey?: 'required' | 'preferred' | 'discouraged'; + @IsOptional() + @IsIn(['required', 'preferred', 'discouraged']) + public residentKey?: ResidentKeyRequirement; /** * Authenticator attachment preference. @@ -81,6 +95,7 @@ export class PasskeyConfig { * - 'cross-platform': Roaming authenticators (USB security keys) * - undefined: No preference (allow any) */ - @IsString() - public authenticatorAttachment?: string; + @IsOptional() + @IsIn(['platform', 'cross-platform']) + public authenticatorAttachment?: AuthenticatorAttachment; } diff --git a/libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts b/libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts new file mode 100644 index 00000000000..1e3bff3c712 --- /dev/null +++ b/libs/accounts/passkey/src/lib/webauthn-adapter.spec.ts @@ -0,0 +1,405 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { + RegistrationResponseJSON, + AuthenticationResponseJSON, +} from '@simplewebauthn/server'; +import { + generateRegistrationOptions, + verifyRegistrationResponse, + generateAuthenticationOptions, + verifyAuthenticationResponse, + uuidToBuffer, +} from './webauthn-adapter'; +import { PasskeyConfig } from './passkey.config'; + +jest.mock('@simplewebauthn/server', () => ({ + generateRegistrationOptions: jest.fn(), + verifyRegistrationResponse: jest.fn(), + generateAuthenticationOptions: jest.fn(), + verifyAuthenticationResponse: jest.fn(), +})); + +const libMocks = jest.requireMock('@simplewebauthn/server') as { + generateRegistrationOptions: jest.MockedFunction< + (...args: unknown[]) => Promise + >; + verifyRegistrationResponse: jest.MockedFunction< + (...args: unknown[]) => Promise + >; + generateAuthenticationOptions: jest.MockedFunction< + (...args: unknown[]) => Promise + >; + verifyAuthenticationResponse: jest.MockedFunction< + (...args: unknown[]) => Promise + >; +}; + +function mockConfig(overrides: Partial = {}): PasskeyConfig { + return Object.assign(new PasskeyConfig(), { + rpId: 'accounts.firefox.com', + rpName: 'Mozilla Accounts', + allowedOrigins: ['https://accounts.firefox.com'], + userVerification: 'required', + residentKey: 'preferred', + ...overrides, + }); +} + +function mockRegistrationResponse(): RegistrationResponseJSON { + return { + id: 'aGVsbG93b3JsZA', + rawId: 'aGVsbG93b3JsZA', + response: { + clientDataJSON: 'e30', + attestationObject: 'e30', + }, + type: 'public-key', + clientExtensionResults: {}, + }; +} + +function mockAuthenticationResponse(): AuthenticationResponseJSON { + return { + id: 'aGVsbG93b3JsZA', + rawId: 'aGVsbG93b3JsZA', + response: { + clientDataJSON: 'e30', + authenticatorData: 'e30', + signature: 'e30', + }, + type: 'public-key', + clientExtensionResults: {}, + }; +} + +describe('uuidToBuffer', () => { + it('converts a standard AAGUID UUID string to a 16-byte Buffer', () => { + const result = uuidToBuffer('adce0002-35bc-c60a-648b-0b25f1f05503'); + + expect(result).toBeInstanceOf(Buffer); + expect(result.length).toBe(16); + expect(result[0]).toBe(0xad); + expect(result[1]).toBe(0xce); + expect(result[2]).toBe(0x00); + expect(result[3]).toBe(0x02); + }); + + it('converts the all-zeros AAGUID to a 16-byte zero Buffer', () => { + const result = uuidToBuffer('00000000-0000-0000-0000-000000000000'); + + expect(result).toBeInstanceOf(Buffer); + expect(result.length).toBe(16); + expect(result.equals(Buffer.alloc(16, 0))).toBe(true); + }); + + it('strips hyphens correctly regardless of position', () => { + const uuid = 'f1d0f1d0-f1d0-f1d0-f1d0-f1d0f1d0f1d0'; + const result = uuidToBuffer(uuid); + + expect(result.length).toBe(16); + for (let i = 0; i < 16; i++) { + expect(result[i]).toBe(i % 2 === 0 ? 0xf1 : 0xd0); + } + }); +}); + +describe('generateRegistrationOptions', () => { + beforeEach(() => jest.clearAllMocks()); + + it('maps config and input fields to the correct library options', async () => { + libMocks.generateRegistrationOptions.mockResolvedValue({ challenge: 'c' }); + const uid = Buffer.from([0xde, 0xad, 0xbe, 0xef]); + + await generateRegistrationOptions( + mockConfig({ + residentKey: 'required', + authenticatorAttachment: 'platform', + }), + { + uid, + email: 'alice@example.com', + challenge: 'test-challenge', + } + ); + + expect(libMocks.generateRegistrationOptions).toHaveBeenCalledWith( + expect.objectContaining({ + rpName: 'Mozilla Accounts', + rpID: 'accounts.firefox.com', + userName: 'alice@example.com', + userID: uid, + challenge: 'test-challenge', + authenticatorSelection: expect.objectContaining({ + userVerification: 'required', + residentKey: 'required', + authenticatorAttachment: 'platform', + }), + }) + ); + }); + + it('returns the library result unchanged', async () => { + const fakeResult = { challenge: 'xyz' }; + libMocks.generateRegistrationOptions.mockResolvedValue(fakeResult); + + const result = await generateRegistrationOptions(mockConfig(), { + uid: Buffer.alloc(16), + email: 'user@example.com', + challenge: 'xyz', + }); + + expect(result).toBe(fakeResult); + }); +}); + +describe('verifyRegistrationResponse', () => { + beforeEach(() => jest.clearAllMocks()); + + const baseInput = { + response: mockRegistrationResponse(), + challenge: 'test-challenge', + }; + + const successLibResult = { + verified: true, + registrationInfo: { + credential: { + id: 'aGVsbG93b3JsZA', + publicKey: new Uint8Array([0x04, 0xab, 0xcd, 0xef]), + counter: 0, + transports: ['internal'], + }, + aaguid: 'adce0002-35bc-c60a-648b-0b25f1f05503', + credentialDeviceType: 'multiDevice', + credentialBackedUp: true, + }, + }; + + it('returns { verified: true, data } when the library succeeds', async () => { + libMocks.verifyRegistrationResponse.mockResolvedValue(successLibResult); + + const result = await verifyRegistrationResponse(mockConfig(), baseInput); + + expect(result.verified).toBe(true); + if (!result.verified) throw new Error('narrowing'); + + expect(result.data.credentialId).toBeInstanceOf(Buffer); + expect(result.data.publicKey).toBeInstanceOf(Buffer); + expect(result.data.signCount).toBe(0); + expect(result.data.transports).toEqual(['internal']); + expect(result.data.aaguid).toBeInstanceOf(Buffer); + expect(result.data.aaguid.length).toBe(16); + expect(result.data.backupEligible).toBe(true); + expect(result.data.backupState).toBe(true); + }); + + it('returns { verified: false } when the library returns verified=false', async () => { + libMocks.verifyRegistrationResponse.mockResolvedValue({ verified: false }); + + const result = await verifyRegistrationResponse(mockConfig(), baseInput); + + expect(result.verified).toBe(false); + expect((result as { data?: unknown }).data).toBeUndefined(); + }); + + it('passes config and input options to the library', async () => { + libMocks.verifyRegistrationResponse.mockResolvedValue(successLibResult); + const config = mockConfig({ + allowedOrigins: ['https://accounts.firefox.com', 'https://other.example'], + }); + + await verifyRegistrationResponse(config, { + response: mockRegistrationResponse(), + challenge: 'expected-challenge-xyz', + }); + + expect(libMocks.verifyRegistrationResponse).toHaveBeenCalledWith( + expect.objectContaining({ + expectedChallenge: 'expected-challenge-xyz', + expectedOrigin: [ + 'https://accounts.firefox.com', + 'https://other.example', + ], + }) + ); + }); + + it('decodes the aaguid UUID into a 16-byte Buffer', async () => { + libMocks.verifyRegistrationResponse.mockResolvedValue(successLibResult); + + const result = await verifyRegistrationResponse(mockConfig(), baseInput); + + expect(result.data?.aaguid[0]).toBe(0xad); + }); +}); + +describe('generateAuthenticationOptions', () => { + beforeEach(() => jest.clearAllMocks()); + + it('passes config and input options to the library', async () => { + libMocks.generateAuthenticationOptions.mockResolvedValue({}); + + await generateAuthenticationOptions( + mockConfig({ userVerification: 'discouraged' }), + { + challenge: 'random-challenge-abc', + allowCredentials: [], + } + ); + + expect(libMocks.generateAuthenticationOptions).toHaveBeenCalledWith( + expect.objectContaining({ + rpID: 'accounts.firefox.com', + challenge: 'random-challenge-abc', + userVerification: 'discouraged', + }) + ); + }); + + it('passes undefined for allowCredentials when the array is empty (discoverable flow)', async () => { + libMocks.generateAuthenticationOptions.mockResolvedValue({}); + + await generateAuthenticationOptions(mockConfig(), { + challenge: 'ch', + allowCredentials: [], + }); + + expect(libMocks.generateAuthenticationOptions).toHaveBeenCalledWith( + expect.objectContaining({ allowCredentials: undefined }) + ); + }); + + it('converts Buffer credential IDs to base64url ids', async () => { + libMocks.generateAuthenticationOptions.mockResolvedValue({}); + const credId = Buffer.from('helloworld'); + + await generateAuthenticationOptions(mockConfig(), { + challenge: 'ch', + allowCredentials: [credId], + }); + + expect(libMocks.generateAuthenticationOptions).toHaveBeenCalledWith( + expect.objectContaining({ + allowCredentials: [ + expect.objectContaining({ id: credId.toString('base64url') }), + ], + }) + ); + }); + + it('returns the library result unchanged', async () => { + const fakeResult = { challenge: 'q1w2e3' }; + libMocks.generateAuthenticationOptions.mockResolvedValue(fakeResult); + + const result = await generateAuthenticationOptions(mockConfig(), { + challenge: 'q1w2e3', + allowCredentials: [], + }); + + expect(result).toBe(fakeResult); + }); +}); + +describe('verifyAuthenticationResponse', () => { + beforeEach(() => jest.clearAllMocks()); + + function makeInput() { + return { + response: mockAuthenticationResponse(), + challenge: 'auth-challenge', + credentialId: Buffer.from('aGVsbG93b3JsZA', 'base64url'), + publicKey: Buffer.from([0x04, 0xab, 0xcd, 0xef]), + signCount: 5, + }; + } + + const successLibResult = { + verified: true, + authenticationInfo: { + credentialID: 'aGVsbG93b3JsZA', + newCounter: 42, + credentialBackedUp: true, + credentialDeviceType: 'multiDevice', + userVerified: true, + rpID: 'accounts.firefox.com', + origin: 'https://accounts.firefox.com', + }, + }; + + it('returns { verified: true, data } when the library succeeds', async () => { + libMocks.verifyAuthenticationResponse.mockResolvedValue(successLibResult); + + const result = await verifyAuthenticationResponse( + mockConfig(), + makeInput() + ); + + expect(result.verified).toBe(true); + expect(result.data?.newSignCount).toBe(42); + expect(result.data?.backupState).toBe(true); + }); + + it('returns { verified: false } with no data when library returns verified=false', async () => { + libMocks.verifyAuthenticationResponse.mockResolvedValue({ + verified: false, + authenticationInfo: { + ...successLibResult.authenticationInfo, + newCounter: 0, + credentialBackedUp: false, + }, + }); + + const result = await verifyAuthenticationResponse( + mockConfig(), + makeInput() + ); + + expect(result.verified).toBe(false); + expect(result.data).toBeUndefined(); + }); + + it('reflects credentialBackedUp=false in data.backupState', async () => { + libMocks.verifyAuthenticationResponse.mockResolvedValue({ + ...successLibResult, + authenticationInfo: { + ...successLibResult.authenticationInfo, + credentialBackedUp: false, + }, + }); + + const result = await verifyAuthenticationResponse( + mockConfig(), + makeInput() + ); + + expect(result.data?.backupState).toBe(false); + }); + + it('passes config and input options to the library', async () => { + libMocks.verifyAuthenticationResponse.mockResolvedValue(successLibResult); + const credPublicKey = Buffer.from([0xde, 0xad, 0xbe, 0xef]); + + await verifyAuthenticationResponse(mockConfig(), { + response: mockAuthenticationResponse(), + challenge: 'specific-challenge-999', + credentialId: Buffer.from('dGVzdA', 'base64url'), + publicKey: credPublicKey, + signCount: 10, + }); + + expect(libMocks.verifyAuthenticationResponse).toHaveBeenCalledWith( + expect.objectContaining({ + expectedChallenge: 'specific-challenge-999', + expectedOrigin: ['https://accounts.firefox.com'], + credential: expect.objectContaining({ + id: Buffer.from('dGVzdA', 'base64url').toString('base64url'), + publicKey: credPublicKey, + counter: 10, + }), + }) + ); + }); +}); diff --git a/libs/accounts/passkey/src/lib/webauthn-adapter.ts b/libs/accounts/passkey/src/lib/webauthn-adapter.ts new file mode 100644 index 00000000000..8cd504c74dc --- /dev/null +++ b/libs/accounts/passkey/src/lib/webauthn-adapter.ts @@ -0,0 +1,240 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + generateRegistrationOptions as libGenerateRegistrationOptions, + verifyRegistrationResponse as libVerifyRegistrationResponse, + generateAuthenticationOptions as libGenerateAuthenticationOptions, + verifyAuthenticationResponse as libVerifyAuthenticationResponse, +} from '@simplewebauthn/server'; +import type { + RegistrationResponseJSON, + AuthenticationResponseJSON, + WebAuthnCredential, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + AuthenticatorTransportFuture, +} from '@simplewebauthn/server'; + +import { PasskeyConfig } from './passkey.config'; + +export interface RegistrationOptionsInput { + /** User's uid as a Buffer, used as the WebAuthn userID. */ + uid: Buffer; + /** User's email address, used as the WebAuthn userName. */ + email: string; + /** Challenge from ChallengeManager. */ + challenge: string; +} + +/** + * Generate WebAuthn registration (attestation) options. + * + * @param config - PasskeyConfig instance + * @param input - Per-request inputs + */ +export async function generateRegistrationOptions( + config: PasskeyConfig, + input: RegistrationOptionsInput +): Promise { + return libGenerateRegistrationOptions({ + rpName: config.rpName, + rpID: config.rpId, + userName: input.email, + userID: input.uid, + challenge: input.challenge, + authenticatorSelection: { + residentKey: config.residentKey, + userVerification: config.userVerification, + authenticatorAttachment: config.authenticatorAttachment, + }, + }); +} + +export interface VerifyRegistrationInput { + /** The raw registration response from the browser. */ + response: RegistrationResponseJSON; + /** Challenge that was issued for this registration. */ + challenge: string; +} + +export interface VerifiedRegistrationData { + credentialId: Buffer; + publicKey: Buffer; + signCount: number; + transports: AuthenticatorTransportFuture[]; + aaguid: Buffer; + backupEligible: boolean; + backupState: boolean; +} + +export type RegistrationVerificationResult = + | { verified: false; data?: never } + | { verified: true; data: VerifiedRegistrationData }; + +/** + * Verify a WebAuthn registration (attestation) response. + * + * @param config - PasskeyConfig instance + * @param input - Per-request inputs + * @returns a discriminated union indicating verification success or failure; + * `data` contains extracted credential info on success. + * @throws wrapped library function throws `Error` on invalid input. + * See source code for possible errors: https://github.com/MasterKale/SimpleWebAuthn/blob/320757144749c742e58ab3bb181087f9fbcac074/packages/server/src/registration/verifyRegistrationResponse.ts + */ +export async function verifyRegistrationResponse( + config: PasskeyConfig, + input: VerifyRegistrationInput +): Promise { + const { verified, registrationInfo } = await libVerifyRegistrationResponse({ + response: input.response, + expectedChallenge: input.challenge, + expectedOrigin: config.allowedOrigins, + expectedRPID: config.rpId, + }); + + if (!verified) { + return { verified: false }; + } + + const { credential, aaguid, credentialDeviceType, credentialBackedUp } = + registrationInfo; + + return { + verified: true, + data: { + credentialId: Buffer.from(credential.id, 'base64url'), + publicKey: Buffer.from(credential.publicKey), + signCount: credential.counter, + transports: credential.transports ?? [], + aaguid: uuidToBuffer(aaguid), + backupEligible: credentialDeviceType === 'multiDevice', + backupState: credentialBackedUp, + }, + }; +} + +/** + * Per-request inputs for generating WebAuthn authentication options. + * `rpID` and `userVerification` are supplied by PasskeyConfig. + */ +export interface AuthenticationOptionsInput { + /** Challenge from ChallengeManager. */ + challenge: string; + /** + * Credential IDs to restrict authentication to. + * Pass the user's stored credential IDs for known-user flows. + * Pass an empty array for usernameless/discoverable flows. + */ + allowCredentials: Buffer[]; +} + +/** + * Generate WebAuthn authentication (assertion) options. + * + * @param config - PasskeyConfig instance + * @param input - Per-request inputs + */ +export async function generateAuthenticationOptions( + config: PasskeyConfig, + input: AuthenticationOptionsInput +): Promise { + return libGenerateAuthenticationOptions({ + rpID: config.rpId, + userVerification: config.userVerification, + challenge: input.challenge, + allowCredentials: + input.allowCredentials.length > 0 + ? input.allowCredentials.map((id) => ({ id: id.toString('base64url') })) + : undefined, + }); +} + +export interface VerifyAuthenticationInput { + /** The raw authentication response from the browser. */ + response: AuthenticationResponseJSON; + /** Challenge that was issued for this authentication. */ + challenge: string; + credentialId: Buffer; + publicKey: Buffer; + signCount: number; +} + +/** + * Extracted data from a successful authentication verification. + * Use `newSignCount` and `backupState` to update the stored passkey record. + */ +export interface VerifiedAuthenticationData { + /** New sign count after successful authentication. */ + newSignCount: number; + /** Current backup state of the credential. */ + backupState: boolean; +} + +export type AuthenticationVerificationResult = + | { verified: false; data?: never } + | { verified: true; data: VerifiedAuthenticationData }; + +/** + * Verify a WebAuthn authentication (assertion) response. + * + * NOTE: the wrapped library function follows the spec strictly and throws an + * `Error` when signCount is non-zero and non-incrementing. This might reject + * existing authenticators following older spec versions where incrementing + * signCount was optional. Caller should catch and handle potential errors from + * the library, in addition to checking the `verified` boolean in the result. + * We should log signCount errors, to decide whether to relax this requirement + * in the future for better compatibility (by custom implementations or switching + * to a different library). + * + * @param config - PasskeyConfig instance + * @param input - Per-request inputs + * @returns a discriminated union indicating verification success or failure; + * `data` contains extracted new signCount and backupState on success. + * @throws wrapped library function throws `Error` on invalid input. + * See source code for possible errors: https://github.com/MasterKale/SimpleWebAuthn/blob/320757144749c742e58ab3bb181087f9fbcac074/packages/server/src/authentication/verifyAuthenticationResponse.ts + */ +export async function verifyAuthenticationResponse( + config: PasskeyConfig, + input: VerifyAuthenticationInput +): Promise { + const credential: WebAuthnCredential = { + id: input.credentialId.toString('base64url'), + publicKey: input.publicKey, + counter: input.signCount, + }; + + const { verified, authenticationInfo } = + await libVerifyAuthenticationResponse({ + response: input.response, + expectedChallenge: input.challenge, + expectedOrigin: config.allowedOrigins, + expectedRPID: config.rpId, + credential, + }); + + if (!verified) { + return { verified: false }; + } + + return { + verified: true, + data: { + newSignCount: authenticationInfo.newCounter, + backupState: authenticationInfo.credentialBackedUp, + }, + }; +} + +/** + * Convert a UUID string to a 16-byte Buffer. + * + * SimpleWebAuthn returns `aaguid` as a lower-case hyphenated UUID string. + * This strips the hyphens and parses the result to a Buffer. + * + * @param uuid - Lower-case hyphenated UUID string + */ +export function uuidToBuffer(uuid: string): Buffer { + return Buffer.from(uuid.replace(/-/g, ''), 'hex'); +} diff --git a/package.json b/package.json index d37d1daffc3..c8bb0415c5d 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@sentry/nextjs": "^10.33.0", "@sentry/node": "^10.33.0", "@sentry/opentelemetry": "^10.33.0", + "@simplewebauthn/server": "^13.2.2", "@smithy/smithy-client": "^4.5.0", "@stripe/react-stripe-js": "^2.7.1", "@stripe/stripe-js": "^4.9.0", diff --git a/yarn.lock b/yarn.lock index 24c3a5d3ce8..9ba48a60c15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8171,6 +8171,13 @@ __metadata: languageName: node linkType: hard +"@hexagon/base64@npm:^1.1.27": + version: 1.1.28 + resolution: "@hexagon/base64@npm:1.1.28" + checksum: 10c0/f90876938cda7c369444f9abcf268a9d93fe26269ae9a16a95fa1f294a5e8f9d172a77905fac205d315b4538ab4da90c866ba73cc035cc0fcf5a6062bb6691ca + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -9501,6 +9508,13 @@ __metadata: languageName: node linkType: hard +"@levischuck/tiny-cbor@npm:^0.2.2": + version: 0.2.11 + resolution: "@levischuck/tiny-cbor@npm:0.2.11" + checksum: 10c0/b34b4b2df5d601f0b260f2cae012168439e6128963959a716ea264586d621d4c411a120219a6abd8c96482de6f11cb34e77365fb6d9f61d99a89c1db5f43ddf3 + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.0.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -13218,6 +13232,108 @@ __metadata: languageName: node linkType: hard +"@peculiar/asn1-android@npm:^2.3.10": + version: 2.6.0 + resolution: "@peculiar/asn1-android@npm:2.6.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/065ae51e284c7a548be113d6fc5fab24cedfb8bc5ec7a210b9866f23e1ec4469a22975adf926837047e4733769a1a6003d5208a7881ab769ef9e545d3a98adf1 + languageName: node + linkType: hard + +"@peculiar/asn1-cms@npm:^2.6.0, @peculiar/asn1-cms@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-cms@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-x509-attr": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/682e952fb35dec229bf54ecaff58bdf56281c1d718b5bcc2da103d67b5be302452c6275300c9f9fce1ed02f03792dab3ebe98c903117e0a5b0d9e5d861356280 + languageName: node + linkType: hard + +"@peculiar/asn1-csr@npm:^2.6.0": + version: 2.6.1 + resolution: "@peculiar/asn1-csr@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/5ea1ef27bf3879c793acb0b370b9fc1cb45df244b4706cecf075e45b58d19d65e612f4777eb12aa37f2037c1c725e96543ab9caf41d5a92378c5069071deae1f + languageName: node + linkType: hard + +"@peculiar/asn1-ecc@npm:^2.3.8, @peculiar/asn1-ecc@npm:^2.6.0": + version: 2.6.1 + resolution: "@peculiar/asn1-ecc@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/7804600f12a8993085232839ea020f51a329a195ce991ebbce077668d9ee1e57301bf97d5ef9657bd81717888f36f51f7aed3a9eee59fe4345c55d04eff89927 + languageName: node + linkType: hard + +"@peculiar/asn1-pfx@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-pfx@npm:2.6.1" + dependencies: + "@peculiar/asn1-cms": "npm:^2.6.1" + "@peculiar/asn1-pkcs8": "npm:^2.6.1" + "@peculiar/asn1-rsa": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.6.0" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/69c86ed339b945f7c77173da06207af71553a5b033cc1f2bde262085e7b5870543f358a29efd8981ca7247ec7f1c5d722a014cc0979679045909cb13e2ca527e + languageName: node + linkType: hard + +"@peculiar/asn1-pkcs8@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-pkcs8@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/d712dc79ab877152f20c1772cbe065f5beb2a20e3dcae7892cc72f3227a1d3f7ae8eecba8bc29cf2b77cfdd8a01b0660f5390a416ca78ca7147f0e3c13d4d755 + languageName: node + linkType: hard + +"@peculiar/asn1-pkcs9@npm:^2.6.0": + version: 2.6.1 + resolution: "@peculiar/asn1-pkcs9@npm:2.6.1" + dependencies: + "@peculiar/asn1-cms": "npm:^2.6.1" + "@peculiar/asn1-pfx": "npm:^2.6.1" + "@peculiar/asn1-pkcs8": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-x509-attr": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/4a2f815bbeee3f65aea391d5e2287a19701d757d2781b3ecfd908a67028f2752796bd22f8ba3eb486911fcc34b52b0f7c1ff3e3b7d7f04ef58767be9ddbc851d + languageName: node + linkType: hard + +"@peculiar/asn1-rsa@npm:^2.3.8, @peculiar/asn1-rsa@npm:^2.6.0, @peculiar/asn1-rsa@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-rsa@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/4d7c71c5bddf7be3b0270c4d95b8274a392185cad4939a7a837d9c4c612601fee1a1ccabe414383b26629fb2013608e60a58ecd665c371617c1f177431a88ff2 + languageName: node + linkType: hard + "@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8": version: 2.3.15 resolution: "@peculiar/asn1-schema@npm:2.3.15" @@ -13229,6 +13345,41 @@ __metadata: languageName: node linkType: hard +"@peculiar/asn1-schema@npm:^2.6.0": + version: 2.6.0 + resolution: "@peculiar/asn1-schema@npm:2.6.0" + dependencies: + asn1js: "npm:^3.0.6" + pvtsutils: "npm:^1.3.6" + tslib: "npm:^2.8.1" + checksum: 10c0/8c283b10a2e4aca4cb20d242cde773c9a798ea15a6c221d1474ef483e182d48195aeb5dde3f7b518f236eceb7808ae4438539d41a3aa9ed6d20aa4d36a21a0c2 + languageName: node + linkType: hard + +"@peculiar/asn1-x509-attr@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-x509-attr@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + asn1js: "npm:^3.0.6" + tslib: "npm:^2.8.1" + checksum: 10c0/de8634ec12ef34b430e5a458151e856f954e15fe9e08d056dca51db6962e849a951820ab66d291e2452799576c44221b40087b9350dc3728d3770a46fcdeffc5 + languageName: node + linkType: hard + +"@peculiar/asn1-x509@npm:^2.3.8, @peculiar/asn1-x509@npm:^2.6.0, @peculiar/asn1-x509@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-x509@npm:2.6.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.6.0" + asn1js: "npm:^3.0.6" + pvtsutils: "npm:^1.3.6" + tslib: "npm:^2.8.1" + checksum: 10c0/2e73a0ce6521eeb2d876e0b52e9fae2de4e2d183be5fba77d5fae9b7724de446d02c0b4e5fb04d4fedb50eed0de842f29f4d7cf2e998eaed6a2d2952f5c52d2c + languageName: node + linkType: hard + "@peculiar/json-schema@npm:^1.1.12": version: 1.1.12 resolution: "@peculiar/json-schema@npm:1.1.12" @@ -13251,6 +13402,25 @@ __metadata: languageName: node linkType: hard +"@peculiar/x509@npm:^1.13.0": + version: 1.14.3 + resolution: "@peculiar/x509@npm:1.14.3" + dependencies: + "@peculiar/asn1-cms": "npm:^2.6.0" + "@peculiar/asn1-csr": "npm:^2.6.0" + "@peculiar/asn1-ecc": "npm:^2.6.0" + "@peculiar/asn1-pkcs9": "npm:^2.6.0" + "@peculiar/asn1-rsa": "npm:^2.6.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.0" + pvtsutils: "npm:^1.3.6" + reflect-metadata: "npm:^0.2.2" + tslib: "npm:^2.8.1" + tsyringe: "npm:^4.10.0" + checksum: 10c0/949231ca9daf84534bfe255f28a856df497302fed294d227c6a28e50f5cfb67ed1d91afe6db787b88294ce042295243dbcb44455fe2efa5ed07428a74392eec9 + languageName: node + linkType: hard + "@phenomnomnominal/tsquery@npm:~5.0.1": version: 5.0.1 resolution: "@phenomnomnominal/tsquery@npm:5.0.1" @@ -15795,6 +15965,22 @@ __metadata: languageName: node linkType: hard +"@simplewebauthn/server@npm:^13.2.2": + version: 13.2.2 + resolution: "@simplewebauthn/server@npm:13.2.2" + dependencies: + "@hexagon/base64": "npm:^1.1.27" + "@levischuck/tiny-cbor": "npm:^0.2.2" + "@peculiar/asn1-android": "npm:^2.3.10" + "@peculiar/asn1-ecc": "npm:^2.3.8" + "@peculiar/asn1-rsa": "npm:^2.3.8" + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/asn1-x509": "npm:^2.3.8" + "@peculiar/x509": "npm:^1.13.0" + checksum: 10c0/4dbcf25fe9b33ae37add96f3c8be3489eee8cf4126f9dab0f6637c4f6c1fabc9ac3b2d337452be3e6c9908616322eb07c7f782d316c39c6f6a9567f01c6241c6 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.24.1": version: 0.24.51 resolution: "@sinclair/typebox@npm:0.24.51" @@ -26365,6 +26551,17 @@ __metadata: languageName: node linkType: hard +"asn1js@npm:^3.0.6": + version: 3.0.7 + resolution: "asn1js@npm:3.0.7" + dependencies: + pvtsutils: "npm:^1.3.6" + pvutils: "npm:^1.1.3" + tslib: "npm:^2.8.1" + checksum: 10c0/7e79795edf1bcc86532c4084aa7c8c0ebc57f7dd6f964ad6de956abf617329722f6964b7af3a5d1c4554dd61b4b148ae1580e63e3ec2e70e7fba34f6df072b29 + languageName: node + linkType: hard + "assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": version: 1.0.0 resolution: "assert-plus@npm:1.0.0" @@ -36706,6 +36903,7 @@ __metadata: "@sentry/nextjs": "npm:^10.33.0" "@sentry/node": "npm:^10.33.0" "@sentry/opentelemetry": "npm:^10.33.0" + "@simplewebauthn/server": "npm:^13.2.2" "@smithy/smithy-client": "npm:^4.5.0" "@storybook/addon-essentials": "npm:7.6.17" "@storybook/addon-styling": "npm:1.3.7" @@ -52532,7 +52730,7 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:^0.2.0, reflect-metadata@npm:^0.2.1": +"reflect-metadata@npm:^0.2.0, reflect-metadata@npm:^0.2.1, reflect-metadata@npm:^0.2.2": version: 0.2.2 resolution: "reflect-metadata@npm:0.2.2" checksum: 10c0/1cd93a15ea291e420204955544637c264c216e7aac527470e393d54b4bb075f10a17e60d8168ec96600c7e0b9fcc0cb0bb6e91c3fbf5b0d8c9056f04e6ac1ec2 @@ -58132,7 +58330,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 @@ -58164,6 +58362,15 @@ __metadata: languageName: node linkType: hard +"tsyringe@npm:^4.10.0": + version: 4.10.0 + resolution: "tsyringe@npm:4.10.0" + dependencies: + tslib: "npm:^1.9.3" + checksum: 10c0/918594b4dfac97beb8be2c041c6ec45f078ef3768ed4edfe35ae2c709ab503e2e6b454b2b37e692c658572d1972a428fbfdbc0a2b42fee727a83c1c685fbe5e1 + languageName: node + linkType: hard + "tty-browserify@npm:0.0.0": version: 0.0.0 resolution: "tty-browserify@npm:0.0.0"