import { User } from '@stacksjs/orm'; import type { Insertable } from '@stacksjs/database'; import type { VerifiedRegistrationResponse } from '@stacksjs/ts-auth'; // Re-export WebAuthn types from ts-auth export type { VerifiedRegistrationResponse, VerifiedAuthenticationResponse, RegistrationCredential, AuthenticationCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, RegistrationOptions, AuthenticationOptions, } from '@stacksjs/ts-auth'; /** * Turn a user's stored passkeys into credential descriptors for the options a * server hands the browser. * * Both `GenerateRegistrationAction` (as `excludeCredentials`) and * `GenerateAuthenticationAction` (as `allowCredentials`) need the same three * facts: the stored base64url id, the `type: 'public-key'` the spec requires, * and the transports. Building it in one place is also where the * ArrayBuffer-vs-JSON boundary gets explained once rather than at each call. */ export declare function passkeyDescriptors(passkeys: readonly PasskeyAttribute[]): PublicKeyCredentialDescriptorJSON[]; export declare function getUserPasskeys(userId: number): Promise; export declare function getUserPasskey(userId: number, passkeyId: string): Promise; /** * Persist the post-verification authenticator counter and refresh the * passkey's last-used timestamp. WebAuthn's anti-cloning guarantee * depends on the relying party rejecting any authentication whose * `newCounter` is **not strictly greater** than the stored value — * authenticators monotonically increment their counter on every use, * so a counter that doesn't advance (or goes backwards) signals a * cloned or replayed credential. * * Returns `true` when the counter was updated successfully; `false` * when the new counter is not greater than the stored one (the * authentication MUST be rejected by the caller in that case). * stacksjs/stacks#1861 A-4. */ export declare function updatePasskeyCounter(userId: number, passkeyId: string, newCounter: number): Promise; export declare function setCurrentRegistrationOptions(user: UserModel, verified: VerifiedRegistrationResponse): Promise; /** * Persist a server-issued WebAuthn challenge for the given user + * purpose. Deletes any prior challenge for the same (user, purpose) * pair so a fresh `generateOptions` invalidates the previous one. * * The unique index on `(user_id, purpose)` enforces single-outstanding * at the DB layer; this delete makes the upsert safe even on installs * that ran an earlier auth:setup before the unique index existed. */ export declare function storeWebAuthnChallenge(userId: number, challenge: string | Uint8Array, purpose: WebAuthnChallengePurpose, ttlSeconds?: number): Promise; /** * Read + delete (single-use) a WebAuthn challenge for the given user * + purpose. Returns `null` when no outstanding challenge exists or * when the stored challenge has expired. * * Single-use semantics matter: a successful verify must invalidate * the challenge so a captured assertion can't be replayed even within * the TTL window. Callers MUST treat a `null` return as a verification * failure. */ export declare function consumeWebAuthnChallenge(userId: number, purpose: WebAuthnChallengePurpose): Promise; /** * A credential descriptor as it crosses the wire. * * `ts-auth`'s `PublicKeyCredentialRequestOptions` describes the shape the * BROWSER wants: `id` as an ArrayBuffer. A server handing those options to a * client has to send JSON, and an ArrayBuffer serializes to `{}` - so ids * travel as the base64url strings the passkey rows already store, and the * client turns them back into buffers before calling `navigator.credentials`. * * The passkey actions were already written against these names; they simply * had nowhere to import them from, so the whole file typechecked as `any`. */ export declare interface PublicKeyCredentialDescriptorJSON { id: string type: 'public-key' transports?: Array<'ble' | 'internal' | 'nfc' | 'usb' | 'hybrid'> } export declare interface PublicKeyCredentialRequestOptionsJSON { challenge: Uint8Array rpId?: string allowCredentials?: PublicKeyCredentialDescriptorJSON[] userVerification?: 'required' | 'preferred' | 'discouraged' timeout?: number } export declare interface PublicKeyCredentialCreationOptionsJSON { challenge: Uint8Array rp: { name: string, id?: string } user: { id: Uint8Array | string, name: string, displayName: string } pubKeyCredParams: Array<{ alg: number, type: 'public-key' }> timeout?: number attestation?: string authenticatorSelection?: { authenticatorAttachment?: 'platform' | 'cross-platform' requireResidentKey?: boolean residentKey?: 'discouraged' | 'preferred' | 'required' userVerification?: 'required' | 'preferred' | 'discouraged' } excludeCredentials?: PublicKeyCredentialDescriptorJSON[] } export declare interface PasskeyAttribute { id: string cred_public_key: string user_id: number webauthn_user_id: string counter: number credential_type: string device_type: string backup_eligible: boolean backup_status: boolean transports?: string created_at?: Date last_used_at: string } declare type UserModel = NonNullable>>; declare type PasskeyInsertable = Insertable; // ============================================================================= // WebAuthn challenge persistence (stacksjs/stacks#1866) // ============================================================================= // // WebAuthn relying parties MUST verify the assertion's challenge // against a server-issued nonce. Previously Stacks returned the // challenge in `generateOptions` and trusted the client to echo it // back on verify — so any attacker who captured the assertion AND the // challenge could replay the response. Persisting the challenge // server-side and consuming it on verify closes that gap. // // The default TTL is 5 minutes — long enough for slow biometric // flows, short enough to bound the replay window. Override per-call // via the `ttlSeconds` parameter. export type WebAuthnChallengePurpose = 'registration' | 'authentication'; // Re-export WebAuthn functions from ts-auth export { generateRegistrationOptions, generateAuthenticationOptions, verifyRegistrationResponse, verifyAuthenticationResponse, // Browser-side functions (for client use) startRegistration, startAuthentication, browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, platformAuthenticatorIsAvailable, } from '@stacksjs/ts-auth';