import * as Base64 from '../core/Base64.js' import * as Cbor from '../core/Cbor.js' import type * as Errors from '../core/Errors.js' import type * as Hex from '../core/Hex.js' import type { Compute } from '../core/internal/types.js' import * as PublicKey from '../core/PublicKey.js' import { base64UrlOptions, bufferSourceToBytes, bytesToArrayBuffer, responseKeys, } from './internal/utils.js' import type * as Types from './Types.js' /** A WebAuthn-flavored P256 credential. */ export type Credential = { attestationObject: serialized extends true ? string : ArrayBuffer clientDataJSON: serialized extends true ? string : ArrayBuffer id: string publicKey: serialized extends true ? Hex.Hex : PublicKey.PublicKey raw: Types.PublicKeyCredential } /** Metadata for a WebAuthn P256 signature. */ export type SignMetadata = Compute<{ authenticatorData: Hex.Hex challengeIndex?: number | undefined clientDataJSON: string typeIndex?: number | undefined userVerificationRequired?: boolean | undefined }> /** * Serializes a credential into a JSON-serializable * format. * * @example * ```ts twoslash * import { Registration, Credential } from 'ox/webauthn' * * const credential = await Registration.create({ name: 'Example' }) * * const serialized = Credential.serialize(credential) // [!code focus] * * // `serialized` is JSON-serializable — send it to a server, store it, etc. * const json = JSON.stringify(serialized) * ``` * * @param credential - The credential to serialize. * @returns The serialized credential. */ export function serialize(credential: Credential): Credential { const { attestationObject, clientDataJSON, id, publicKey, raw } = credential const response = {} as Record for (const key of responseKeys) { const r = raw.response as unknown as Record let value = r[key] // Some properties (e.g. `authenticatorData`) are only accessible via // getter methods (e.g. `getAuthenticatorData()`) in certain browsers // and passkey providers. if (!(value instanceof ArrayBuffer)) { const getter = `get${key[0]!.toUpperCase()}${key.slice(1)}` as keyof typeof r const fn = r[getter] if (typeof fn === 'function') value = fn.call(r) } if (value instanceof ArrayBuffer) response[key] = Base64.fromBytes(new Uint8Array(value), base64UrlOptions) } // Some browsers/passkey providers (e.g. Firefox + 1Password) don't expose // `authenticatorData` on the response object. Fall back to extracting it // from the CBOR-encoded `attestationObject` which always contains `authData`. if (!response.authenticatorData) { const attestation = Cbor.decode<{ authData: Uint8Array }>( new Uint8Array(attestationObject), ) if (attestation.authData) response.authenticatorData = Base64.fromBytes( attestation.authData, base64UrlOptions, ) } return { attestationObject: Base64.fromBytes( new Uint8Array(attestationObject), base64UrlOptions, ), clientDataJSON: Base64.fromBytes( new Uint8Array(clientDataJSON), base64UrlOptions, ), id, publicKey: PublicKey.toHex(publicKey), raw: { id: raw.id, type: raw.type, authenticatorAttachment: raw.authenticatorAttachment, rawId: Base64.fromBytes(bufferSourceToBytes(raw.rawId), base64UrlOptions), response: response as unknown as Types.AuthenticatorResponse, }, } } export declare namespace serialize { type ErrorType = | Base64.fromBytes.ErrorType | PublicKey.toHex.ErrorType | Errors.GlobalErrorType } /** * Deserializes a serialized credential. * * @example * ```ts twoslash * import { Credential } from 'ox/webauthn' * * const credential = Credential.deserialize({ // [!code focus] * attestationObject: 'o2NmbXRkbm9uZQ...', // [!code focus] * clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0', // [!code focus] * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * publicKey: '0x04ab891400140fc4f8e941ce0ff90e419de9470acaca613bbd717a4775435031a7d884318e919fd3b3e5a631d866d8a380b44063e70f0c381ee16e0652f7f97554', // [!code focus] * raw: { // [!code focus] * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * type: 'public-key', // [!code focus] * authenticatorAttachment: 'platform', // [!code focus] * rawId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus] * response: { // [!code focus] * clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0', // [!code focus] * }, // [!code focus] * }, // [!code focus] * }) // [!code focus] * ``` * * @param credential - The serialized credential. * @returns The deserialized credential. */ export function deserialize(credential: Credential): Credential { const { attestationObject, clientDataJSON, id, publicKey, raw } = credential const response = Object.create(null) as Record for (const key of responseKeys) { const value = (raw.response as unknown as Record)[key] if (value) response[key] = bytesToArrayBuffer(Base64.toBytes(value)) } return { attestationObject: bytesToArrayBuffer(Base64.toBytes(attestationObject)), clientDataJSON: bytesToArrayBuffer(Base64.toBytes(clientDataJSON)), id, publicKey: PublicKey.from(publicKey), raw: { id: raw.id, type: raw.type, authenticatorAttachment: raw.authenticatorAttachment, rawId: bytesToArrayBuffer(Base64.toBytes(raw.rawId)), response: response as unknown as Types.AuthenticatorResponse, getClientExtensionResults: () => ({}), }, } } export declare namespace deserialize { type ErrorType = | Base64.toBytes.ErrorType | PublicKey.from.ErrorType | Errors.GlobalErrorType }