Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,19 @@ turns the code into `error.message`, so callers must branch with
Keychain or Google Password Manager passkey is backup eligible, so this is the
default path, not an edge case.

`@seamless-auth/types` publishes no union for those codes as of 0.15.0, so
`PasskeyPolicyErrorCode` in `src/client/errors.ts` is a local copy of the API's
list. If the types package starts exporting one, switch to it so the
`Record<Code, true>` check catches upstream drift the way the OAuth one does.
`@seamless-auth/types` 0.16.0 publishes `WebAuthnErrorCode`, which covers every
machine code the API sends for WebAuthn across all of its operations, so
`PasskeyPolicyErrorCode` in `src/client/errors.ts` is derived from it rather than
kept as a local list. It is that union minus `prf_output_not_allowed`, a `400`
from login and step-up finish that reports a caller which failed to strip PRF
output, not a deployment refusing an authenticator.

Derive it by subtraction, not by listing the codes you want. A code added
upstream then lands in `PasskeyPolicyErrorCode`, and the
`Record<PasskeyPolicyErrorCode, true>` map stops compiling until someone either
handles it or excludes it deliberately. Listing the wanted names would drop a new
code on the floor and leave the map compiling, which is the silent drift the
union exists to prevent.

Before documenting new flow behavior, verify the route contract in `seamless-auth-server` or `seamless-auth-api`.

Expand Down
17 changes: 8 additions & 9 deletions src/client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,9 @@ export function getOAuthErrorCode(error: unknown): OAuthErrorCode | undefined {
* ceremony runs. The rest come from register/finish with a `403`, once the
* credential exists and can be inspected.
*/
export type PasskeyPolicyErrorCode = Extract<
export type PasskeyPolicyErrorCode = Exclude<
WebAuthnErrorCodeShape,
| 'attachment_not_allowed'
| 'synced_passkey_not_allowed'
| 'authenticator_not_allowed'
| 'prf_required'
'prf_output_not_allowed'
>;

/*
Expand All @@ -109,10 +106,12 @@ export type PasskeyPolicyErrorCode = Extract<
* refusing an authenticator. Reporting it as a policy refusal would point an
* integrator at their configuration for what is a bug in the caller.
*
* `Extract` ties these names to the upstream union: if one is renamed or dropped
* there, it resolves to `never` and the `Record` below stops compiling. As with
* the OAuth codes, the runtime list stays out of the browser bundle so Zod does
* not come with it.
* Subtracting that one name, rather than listing the four that are wanted, is
* what makes upstream additions visible: a code added to `WebAuthnErrorCode`
* lands in this type, and the `Record` below then fails to compile until it is
* either handled here or excluded on purpose. Listing the wanted names instead
* would silently ignore it. As with the OAuth codes, the runtime list stays out
* of the browser bundle so Zod does not come with it.
*/
const PASSKEY_POLICY_ERROR_CODES: Record<PasskeyPolicyErrorCode, true> = {
attachment_not_allowed: true,
Expand Down
Loading