feat(passkey): expose the policy code behind a refused passkey registration - #133
Merged
Conversation
…ration The auth API can refuse a verified credential on policy grounds, answering 403 with a body whose `error` is a machine code rather than a sentence. The adapter forwards that body verbatim and `extractMessage` turns the code into `error.message`, so an app rendering the message shows a user `synced_passkey_not_allowed`, and a caller has nothing to branch on except the literal. `getPasskeyPolicyErrorCode()` reads that code, following the existing `getOAuthErrorCode()` shape: unrecognized values return undefined so a refusal from a newer API keeps generic messaging, and a code nested under `details` is found too, for a proxy that derives its own top-level `error` string. `prf_required` comes from the same endpoint and had the same problem, so it is covered as well. This is the default path rather than an edge case: `syncedPasskeys` defaults to `block` and every iCloud Keychain or Google Password Manager passkey is backup eligible, so README and AGENTS.md now name the codes and the default. Refs #132
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #132.
The problem
POST /webAuthn/register/finishcan refuse a credential that passedverification, on the deployment's authenticator policy, answering
403with abody whose
erroris a machine code rather than a sentence:synced_passkey_not_allowedauthenticator_policy.syncedPasskeysisblockand the credential is backup eligibleauthenticator_not_allowedaaguidDenyList, or absent from a non-emptyaaguidAllowListprf_requiredThe adapter forwards that body verbatim (
readPassthroughFailurereturnserrorBody), andextractMessageinsrc/client/errors.tsreads{ error: string }straight into the message. SoregisterPasskey()resolves witherror.message === 'synced_passkey_not_allowed'and an app rendering themessage puts that code in front of a user, with nothing to branch on but the
literal.
This is the default path, not an edge case:
syncedPasskeysdefaults toblock, and every iCloud Keychain or Google Password Manager passkey is backupeligible.
The change
getPasskeyPolicyErrorCode(error)returns'synced_passkey_not_allowed' | 'authenticator_not_allowed' | 'prf_required' | undefined, following the existinggetOAuthErrorCode()shape:undefined, so a refusal from a newer API keepsthe caller's generic messaging
detailsis found too, for a proxy that derives its ownhuman-readable top-level
errorand keeps the upstream body underneath. Anunrecognized top-level value falls through to the nested one rather than
ending the lookup, which is the difference from
getOAuthErrorCode(): therethe code lives under
code, here it is the whole oferror, so the top levelis occupied by the derived string in exactly the reshaped case
src/index.tsalongside thePasskeyPolicyErrorCodetypeREADME documents the codes, the
syncedPasskeysdefault, and that only the APIcan relax it. AGENTS.md records the same under the backend endpoint notes.
Decision worth flagging
@seamless-auth/typespublishes no union for these codes (checked against0.15.0, the version the API itself is on; this repo is on 0.5.0). The union only
exists as
AuthenticatorRefusalinside the API'sauthenticatorPolicyService,and
prf_requiredis a bare literal in its controller. SoPasskeyPolicyErrorCodeis defined locally, and the
Record<Code, true>check keeps the list and theunion in step with each other but cannot catch upstream drift the way the OAuth
one does. Drift degrades to generic messaging rather than breaking. The
alternative, adding the union to
@seamless-auth/types, needs a version bumpand a coordinated release across the API and both SDKs, so it is not done here.
A note in AGENTS.md says to switch to the published union if one appears.
Checks
npm run typecheck: cleannpm run lint: cleannpm run format:check: all matched files use Prettier code stylenpm test -- --runInBand: 32 suites, 303 tests passed.src/client/errors.tsat 100% statements and lines
npm run build: succeeded, anddist/index.d.tscarries both new exportsNew tests in
tests/errors.test.tscover each known code, a code nested underdetails, an unknown code at both levels, a generic verification failure, amissing or non-object body and
details, and a non-SeamlessAuthErrorinput.