Files
skills/docs/api-stack/api/api-plugin-auth/src/error-codes/error-codes.md
2026-05-29 08:20:56 +01:00

3.7 KiB

error-codes

(no submodules)

Purpose

The error-codes module owns the stable AUTH.* code registry and the AuthError class used by every production failure path inside @suaveplan/api-plugin-auth. Consumers branch on the namespaced string literal (e.g. error.code === "AUTH.UNAUTHENTICATED") — the registry exists so the strings never drift across the plugin, its consumers, and the OpenAPI error responses surfaced by @suaveplan/api.

Features

  • 10 namespaced AUTH.* codes built via @suaveplan/error's defineCodes("AUTH", […]) — type-safe at compile time and stable at runtime.
  • AuthError extends SuaveplanError<AuthErrorCode> so instanceof AuthError narrows error.code to the union.
  • Codes carry HTTP status via context.statusCode — the @suaveplan/api error-handler reads that field and translates to the matching response code (401 / 403 / 500 / 503).

Usage: Basic

import { AUTH_CODES, AuthError } from "@suaveplan/api-plugin-auth/error-codes/error-codes";

throw new AuthError(AUTH_CODES.UNAUTHENTICATED, "Login required", {
    context: { statusCode: 401 },
});

Usage: Advanced

Pass context.required on a role-check failure so callers and the OpenAPI 403 response can show which roles satisfy the gate. Carry the underlying ZodError as cause on input-validation failures so the toJSON pipeline preserves it on the wire:

import { AUTH_CODES, AuthError } from "@suaveplan/api-plugin-auth/error-codes/error-codes";
import z from "zod";

const parsed = z.object({ email: z.string().email() }).safeParse(input);
if (!parsed.success) {
    throw new AuthError(AUTH_CODES.INVALID_INPUT, "Invalid sign-up body", {
        cause: parsed.error,
        context: { statusCode: 400, issues: parsed.error.issues },
    });
}

const principal = c.get("user");
if (!principal) {
    throw new AuthError(AUTH_CODES.UNAUTHENTICATED, "Login required", {
        context: { statusCode: 401 },
    });
}
const required = ["admin", "owner"] as const;
if (!principal.roles.some((r) => required.includes(r as never))) {
    throw new AuthError(AUTH_CODES.INSUFFICIENT_ROLE, "Forbidden", {
        context: { statusCode: 403, required: [...required] },
    });
}

API Reference

AUTH_CODES

Record<AuthCodeKey, "AUTH.<KEY>"> — frozen mapping built with defineCodes. Keys: UNAUTHENTICATED, INSUFFICIENT_ROLE, EMAIL_NOT_VERIFIED, SESSION_RESOLVE_FAILED, PRINCIPAL_MAPPING_FAILED, BETTER_AUTH_INIT_FAILED, INVALID_INPUT, ROUTE_PREFIX_COLLISION, SCHEMA_APPLY_FAILED, SERVICE_NOT_AVAILABLE.

AuthErrorCode

Discriminated string union, "AUTH.UNAUTHENTICATED" | … — the type of AUTH_CODES[key].

class AuthError extends SuaveplanError<AuthErrorCode>

  • new AuthError(code, message, options?) — code is an AuthErrorCode; options is the SuaveplanErrorOptions shape ({ context?, cause?, traceId?, clock? }). The constructor is public so any plugin module may throw directly.
  • error.authCode — convenience field that narrows to AuthErrorCode even when the value is treated as a plain SuaveplanError.

Implementation Notes

  • The full code list lives in this file. Adding a new code requires authoring a new OpenSpec change so the spec delta, registry, and call sites move together.
  • context.statusCode is non-normative on SuaveplanError (the field is read by @suaveplan/api's error-handler); the auth plugin always attaches it for codes that map cleanly to an HTTP response so the served error matches the route's OpenAPI schema.
  • SERVICE_NOT_AVAILABLE lists missing service names in context.missing so operators reading logs / span events can see exactly which ctx.getService(...) call returned undefined.