Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View 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.

View File

@@ -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"
>();
});
});

View 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);
}
}

View File

@@ -0,0 +1,5 @@
export {
WEBAUTHN_CODES,
WebAuthnError,
type WebAuthnErrorCode,
} from "./error-codes.js";