Initial Commit
This commit is contained in:
51
packages/identity/auth/webauthn/src/errors/error-codes.md
Normal file
51
packages/identity/auth/webauthn/src/errors/error-codes.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# WebAuthn error codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the frozen registry of every typed error code raised across the `@suaveplan/webauthn` surface. The codes mirror the W3C WebAuthn Level 3 failure classes (§5, §6, §7, §8, §10) and the CTAP 2.1 attestation-object failure points so consumers can `switch (err.code)` against a stable enum instead of pattern-matching on free-form messages. The registry is the single source of truth for the observability counter `webauthn.errors_total{code}` and for the DOMException mapper at the adapter boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `WEBAUTHN_CODES` registry built via `defineCodes("WEBAUTHN", [...])`.
|
||||
- Concrete `WebAuthnError` subclass of `SuaveplanError<WebAuthnErrorCode>`.
|
||||
- Literal union `WebAuthnErrorCode` pinned with `expectTypeOf` assertions in tests so any drift between the registry and the union fails typecheck.
|
||||
- Every code raised by at least one production path; codes that exist only to placate the conformance harness are forbidden (`AGENTS.md` §2c).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WEBAUTHN_CODES, WebAuthnError } from "@suaveplan/webauthn/errors";
|
||||
|
||||
throw new WebAuthnError(
|
||||
WEBAUTHN_CODES.CHALLENGE_MISMATCH,
|
||||
"error.webauthn.challenge-mismatch",
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { WebAuthnError, type WebAuthnErrorCode } from "@suaveplan/webauthn/errors";
|
||||
|
||||
function classify(err: unknown): WebAuthnErrorCode | "unknown" {
|
||||
if (err instanceof WebAuthnError) {
|
||||
return err.code;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WEBAUTHN_CODES` — frozen object mapping every code identifier (`NOT_SUPPORTED`, `USER_CANCELLED`, ...) to its dotted string (`"WEBAUTHN.NOT_SUPPORTED"`).
|
||||
- `WebAuthnErrorCode` — literal union of every value in `WEBAUTHN_CODES`.
|
||||
- `WebAuthnError` — extends `SuaveplanError<WebAuthnErrorCode>`; accepts `(code, messageKey, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Codes are intentionally `SCREAMING_SNAKE` so they survive minification.
|
||||
- The order in the `defineCodes` array is alphabetised within W3C section groupings: lifecycle (`NOT_SUPPORTED`, `USER_CANCELLED`, ...), validation (`MALFORMED_RESPONSE`, `CHALLENGE_MISMATCH`, ...), signature (`SIGNATURE_INVALID`, ...), counter (`SIGN_COUNTER_REGRESSION`), attestation (`ATTESTATION_FORMAT_UNSUPPORTED`), flag (`UV_NOT_PERFORMED`, `UP_NOT_PERFORMED`), cancellation (`ABORTED`, `TIMEOUT`), options validation (`INVALID_OPTIONS`).
|
||||
- `WEBAUTHN.ABORTED` and `WEBAUTHN.TIMEOUT` are kept distinct from `USER_CANCELLED` so the OTel `errors_total{code}` counter slices AbortSignal cancellation, authenticator-side timeout, and explicit user denial separately.
|
||||
@@ -0,0 +1,90 @@
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
expectTypeOf,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import { resetMetricsForTesting } from "../observability/metrics.js";
|
||||
import {
|
||||
WEBAUTHN_CODES,
|
||||
WebAuthnError,
|
||||
type WebAuthnErrorCode,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("WEBAUTHN_CODES registry", () => {
|
||||
beforeEach(() => {
|
||||
resetMetricsForTesting();
|
||||
});
|
||||
afterEach(() => {
|
||||
metrics.disable();
|
||||
resetMetricsForTesting();
|
||||
});
|
||||
it("exposes every documented code", { timeout: 5000 }, () => {
|
||||
const expected = [
|
||||
"NOT_SUPPORTED",
|
||||
"USER_CANCELLED",
|
||||
"INVALID_STATE",
|
||||
"SECURITY_ERROR",
|
||||
"NOT_ALLOWED",
|
||||
"MALFORMED_RESPONSE",
|
||||
"CHALLENGE_MISMATCH",
|
||||
"ORIGIN_MISMATCH",
|
||||
"RPID_HASH_MISMATCH",
|
||||
"SIGN_COUNTER_REGRESSION",
|
||||
"SIGNATURE_INVALID",
|
||||
"ATTESTATION_FORMAT_UNSUPPORTED",
|
||||
"UV_NOT_PERFORMED",
|
||||
"UP_NOT_PERFORMED",
|
||||
"ABORTED",
|
||||
"TIMEOUT",
|
||||
"INVALID_OPTIONS",
|
||||
];
|
||||
for (const key of expected) {
|
||||
expect(WEBAUTHN_CODES).toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
it("WebAuthnError preserves code, key, and cause", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("inner");
|
||||
const err = new WebAuthnError(
|
||||
WEBAUTHN_CODES.MALFORMED_RESPONSE,
|
||||
"error.x",
|
||||
{ cause }
|
||||
);
|
||||
expect(err.code).toBe("WEBAUTHN.MALFORMED_RESPONSE");
|
||||
expect(err.cause).toBe(cause);
|
||||
expect(err).toBeInstanceOf(WebAuthnError);
|
||||
});
|
||||
it("WebAuthnError uses default options when omitted", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new WebAuthnError(WEBAUTHN_CODES.ABORTED, "error.y");
|
||||
expect(err.code).toBe("WEBAUTHN.ABORTED");
|
||||
expect(err.cause).toBeUndefined();
|
||||
});
|
||||
it("WebAuthnErrorCode type unions every code", { timeout: 5000 }, () => {
|
||||
expectTypeOf<WebAuthnErrorCode>().toEqualTypeOf<
|
||||
| "WEBAUTHN.NOT_SUPPORTED"
|
||||
| "WEBAUTHN.USER_CANCELLED"
|
||||
| "WEBAUTHN.INVALID_STATE"
|
||||
| "WEBAUTHN.SECURITY_ERROR"
|
||||
| "WEBAUTHN.NOT_ALLOWED"
|
||||
| "WEBAUTHN.MALFORMED_RESPONSE"
|
||||
| "WEBAUTHN.CHALLENGE_MISMATCH"
|
||||
| "WEBAUTHN.ORIGIN_MISMATCH"
|
||||
| "WEBAUTHN.RPID_HASH_MISMATCH"
|
||||
| "WEBAUTHN.SIGN_COUNTER_REGRESSION"
|
||||
| "WEBAUTHN.SIGNATURE_INVALID"
|
||||
| "WEBAUTHN.ATTESTATION_FORMAT_UNSUPPORTED"
|
||||
| "WEBAUTHN.UV_NOT_PERFORMED"
|
||||
| "WEBAUTHN.UP_NOT_PERFORMED"
|
||||
| "WEBAUTHN.ABORTED"
|
||||
| "WEBAUTHN.TIMEOUT"
|
||||
| "WEBAUTHN.INVALID_OPTIONS"
|
||||
>();
|
||||
});
|
||||
});
|
||||
51
packages/identity/auth/webauthn/src/errors/error-codes.ts
Normal file
51
packages/identity/auth/webauthn/src/errors/error-codes.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { SuaveplanErrorOptions } from "@suaveplan/error";
|
||||
import { defineCodes, SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Frozen registry of typed error codes raised by `@suaveplan/webauthn`.
|
||||
*
|
||||
* Every code corresponds to a distinct W3C WebAuthn L3 / CTAP 2.1 failure
|
||||
* class. The literal union is pinned via `expectTypeOf` in tests so
|
||||
* accidental additions/removals fail typecheck. Every code is thrown by
|
||||
* at least one production path (or returned as the `code` field of a
|
||||
* discriminated-union `VerifyResult`).
|
||||
*/
|
||||
export const WEBAUTHN_CODES = defineCodes("WEBAUTHN", [
|
||||
"NOT_SUPPORTED",
|
||||
"USER_CANCELLED",
|
||||
"INVALID_STATE",
|
||||
"SECURITY_ERROR",
|
||||
"NOT_ALLOWED",
|
||||
"MALFORMED_RESPONSE",
|
||||
"CHALLENGE_MISMATCH",
|
||||
"ORIGIN_MISMATCH",
|
||||
"RPID_HASH_MISMATCH",
|
||||
"SIGN_COUNTER_REGRESSION",
|
||||
"SIGNATURE_INVALID",
|
||||
"ATTESTATION_FORMAT_UNSUPPORTED",
|
||||
"UV_NOT_PERFORMED",
|
||||
"UP_NOT_PERFORMED",
|
||||
"ABORTED",
|
||||
"TIMEOUT",
|
||||
"INVALID_OPTIONS",
|
||||
] as const);
|
||||
|
||||
export type WebAuthnErrorCode =
|
||||
(typeof WEBAUTHN_CODES)[keyof typeof WEBAUTHN_CODES];
|
||||
|
||||
const DEFAULT_OPTIONS: SuaveplanErrorOptions = {};
|
||||
|
||||
/**
|
||||
* Concrete error subclass. All thrown errors in this package are
|
||||
* instances of `WebAuthnError`; switching on `error.code` narrows to
|
||||
* `WebAuthnErrorCode`.
|
||||
*/
|
||||
export class WebAuthnError extends SuaveplanError<WebAuthnErrorCode> {
|
||||
public constructor(
|
||||
code: WebAuthnErrorCode,
|
||||
messageKey: string,
|
||||
options: SuaveplanErrorOptions = DEFAULT_OPTIONS
|
||||
) {
|
||||
super(code, messageKey, options);
|
||||
}
|
||||
}
|
||||
5
packages/identity/auth/webauthn/src/errors/index.ts
Normal file
5
packages/identity/auth/webauthn/src/errors/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
WEBAUTHN_CODES,
|
||||
WebAuthnError,
|
||||
type WebAuthnErrorCode,
|
||||
} from "./error-codes.js";
|
||||
Reference in New Issue
Block a user