diff --git a/package-lock.json b/package-lock.json index 6533824..c4db83a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@seamless-auth/react", - "version": "0.7.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamless-auth/react", - "version": "0.7.0", + "version": "0.9.0", "license": "AGPL-3.0-only", "dependencies": { - "@seamless-auth/types": "^0.5.0", + "@seamless-auth/types": "^0.16.0", "@simplewebauthn/browser": "^13.1.0", "eslint-plugin-license-header": "^0.9.0", "libphonenumber-js": "^1.12.7", @@ -3066,9 +3066,9 @@ "license": "MIT" }, "node_modules/@seamless-auth/types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.5.0.tgz", - "integrity": "sha512-T566YQu8FqUmqgte0A0iJpjTDXUKq/d/7uvLlXGXKtdl52Edo0r3iafL3kx4OM0cRJM63tBGe9s4MTjRUth6yA==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.16.0.tgz", + "integrity": "sha512-7W8w850S5B+6nekeIMG9Cmmouo6ABnoQ7iUpO62GG08HGx2Ur5j14pFB2CFs0MQWIQss5mcMkNPSY2NfB0UoNA==", "license": "AGPL-3.0-only", "dependencies": { "zod": "^4.3.6" diff --git a/package.json b/package.json index 53be7d1..833efc4 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "typescript-eslint": "^8.46.1" }, "dependencies": { - "@seamless-auth/types": "^0.5.0", + "@seamless-auth/types": "^0.16.0", "@simplewebauthn/browser": "^13.1.0", "eslint-plugin-license-header": "^0.9.0", "libphonenumber-js": "^1.12.7", diff --git a/src/client/errors.ts b/src/client/errors.ts index 6e6510c..c66a411 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -4,7 +4,10 @@ * See LICENSE file in the project root for full license information */ -import type { OAuthErrorCode as OAuthErrorCodeShape } from '@seamless-auth/types'; +import type { + OAuthErrorCode as OAuthErrorCodeShape, + WebAuthnErrorCode as WebAuthnErrorCodeShape, +} from '@seamless-auth/types'; /** * Error carrying the auth server's response detail, so callers can map known @@ -86,17 +89,23 @@ export function getOAuthErrorCode(error: unknown): OAuthErrorCode | undefined { * Machine-readable codes `POST /webAuthn/register/finish` answers `403` with * when a deployment refuses an otherwise valid credential on policy grounds. */ -export type PasskeyPolicyErrorCode = - | 'synced_passkey_not_allowed' - | 'authenticator_not_allowed' - | 'prf_required'; +export type PasskeyPolicyErrorCode = Extract< + WebAuthnErrorCodeShape, + 'synced_passkey_not_allowed' | 'authenticator_not_allowed' | 'prf_required' +>; /* - * Unlike the OAuth codes, `@seamless-auth/types` publishes no union for these - * (checked against 0.15.0), so this list is a copy of the API's rather than a - * check against it and will not fail to compile if the API adds a code. Drift - * therefore degrades to generic messaging instead of breaking; the `Record` - * still keeps the list and the union in step with each other. + * `WebAuthnErrorCode` covers every WebAuthn code the API sends, across all of + * its operations, so it is deliberately narrowed rather than used whole. The + * two it leaves out belong to other calls and other statuses: + * `attachment_not_allowed` is a `400` from register/start, and + * `prf_output_not_allowed` a `400` from login and step-up finish. Reporting + * either as a registration policy refusal would be wrong. + * + * `Extract` still ties the three 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. */ const PASSKEY_POLICY_ERROR_CODES: Record = { synced_passkey_not_allowed: true, diff --git a/tests/errors.test.ts b/tests/errors.test.ts index 42f67f6..e25f8c8 100644 --- a/tests/errors.test.ts +++ b/tests/errors.test.ts @@ -8,6 +8,7 @@ import { getOAuthErrorCode, getPasskeyPolicyErrorCode, getWebAuthnErrorDetail, + type PasskeyPolicyErrorCode, SeamlessAuthError, toSeamlessAuthError, } from '@/client/errors'; @@ -168,7 +169,7 @@ describe('getOAuthErrorCode', () => { }); describe('getPasskeyPolicyErrorCode', () => { - const policyCodes = [ + const policyCodes: PasskeyPolicyErrorCode[] = [ 'synced_passkey_not_allowed', 'authenticator_not_allowed', 'prf_required', @@ -189,6 +190,18 @@ describe('getPasskeyPolicyErrorCode', () => { expect(getPasskeyPolicyErrorCode(error)).toBe(code); }); + // These are WebAuthn codes from other operations: `attachment_not_allowed` is + // a 400 from register/start, `prf_output_not_allowed` a 400 from login and + // step-up finish. Neither is a registration policy refusal. + it.each(['attachment_not_allowed', 'prf_output_not_allowed'])( + 'ignores %s, which is not a registration policy refusal', + code => { + const error = new SeamlessAuthError(code, 400, { error: code }); + + expect(getPasskeyPolicyErrorCode(error)).toBeUndefined(); + } + ); + it('reads a real refusal built from the API response', async () => { const error = await toSeamlessAuthError( responseWith(403, async () => ({ error: 'synced_passkey_not_allowed' })),