feat(auth): structured error codes + conditional passkey UI

- Add AuthErrorCode union and typed twoFactorRedirect/retryAfter fields on AuthResult so the frontend can branch on stable codes instead of locale-dependent error strings.
- Extend signInWithPasskey with an optional { conditional } flag, threaded through to @simplewebauthn/browser via useBrowserAutofill, so hosts can opt into WebAuthn Conditional UI (passkey suggestions inline in the email autofill dropdown).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-08 12:40:51 +02:00
parent 3c91691d26
commit ff7dc5d875
4 changed files with 35 additions and 7 deletions

View file

@ -436,9 +436,14 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
},
/**
* Sign in with a passkey
* Sign in with a passkey.
*
* Pass `{ conditional: true }` to use the WebAuthn Conditional UI flow,
* where the browser surfaces passkeys directly inside the email autofill
* dropdown instead of opening a modal. The host MUST verify
* `PublicKeyCredential.isConditionalMediationAvailable()` first.
*/
async signInWithPasskey(): Promise<AuthResult> {
async signInWithPasskey(options: { conditional?: boolean } = {}): Promise<AuthResult> {
try {
const { startAuthentication } = await import('@simplewebauthn/browser');
const storage = getStorageAdapter();
@ -454,10 +459,13 @@ export function createAuthService(config: AuthServiceConfig): AuthServiceInterfa
return { success: false, error: err.message || 'Failed to get authentication options' };
}
const { options, challengeId } = await optionsRes.json();
const { options: webauthnOptions, challengeId } = await optionsRes.json();
// Step 2: Authenticate via browser WebAuthn API
const credential = await startAuthentication({ optionsJSON: options });
const credential = await startAuthentication({
optionsJSON: webauthnOptions,
useBrowserAutofill: options.conditional === true,
});
// Step 3: Send credential to server for verification
const verifyRes = await fetch(`${baseUrl}${endpoints.passkeyAuthVerify}`, {

View file

@ -211,7 +211,7 @@ export interface AuthServiceInterface {
// Passkeys
isPasskeyAvailable(): boolean;
registerPasskey(friendlyName?: string): Promise<AuthResult>;
signInWithPasskey(): Promise<AuthResult>;
signInWithPasskey(options?: { conditional?: boolean }): Promise<AuthResult>;
listPasskeys(): Promise<any[]>;
deletePasskey(passkeyId: string): Promise<AuthResult>;
renamePasskey(passkeyId: string, friendlyName: string): Promise<AuthResult>;