Files
skills/docs/api-stack/api/api-host/src/reserved-prefix/reserved-prefix.md
2026-05-29 08:20:56 +01:00

4.8 KiB

reserved-prefix

(no submodules)

Purpose

The reserved-prefix contract publishes the canonical list of URL path prefixes that @suaveplan/api-host (and every first-party engine that ships under the host) considers reserved. Without it, downstream adapters such as @suaveplan/api-auth-better-auth, @suaveplan/api-plugin-rbac, or any user-authored plugin would hard-code a private snapshot of host-internal routes and silently drift the moment a new host module adds an endpoint.

This module is the single source of truth: it exposes the frozen HOST_RESERVED_PREFIXES array, the isReservedPrefix predicate, the normalisePrefix canonicalisation helper, and the assertPrefixAvailable assertion that throws a consumer-supplied error on collision. Adapters consult it before mounting their own routes and refuse to install — surfacing their own code namespace (AUTH_BA_ROUTE_PREFIX_COLLISION, RBAC_ROUTE_PREFIX_COLLISION, …) — when an operator-configured mount path collides with a host-reserved route.

Features

  • Frozen ReadonlyArray<string> of always-reserved prefixes (/_host, /internal).
  • Case-insensitive, trailing-slash-tolerant, leading-slash-tolerant prefix matching.
  • Exact-match AND child-path detection (/internal/livez is also reserved).
  • assertPrefixAvailable(prefix, factory) throws the factory-supplied error lazily — the factory is only invoked on collision so adapters can defer expensive context construction to the failure path.
  • Branded ReservedPrefix type tagging values that have been canonicalised through normalisePrefix, so type-aware pipelines can prove their input is in the canonical form without re-normalising at every hop.
  • Published as a ./reserved-prefix subpath export — adapters import only the predicate surface without pulling in the full host bundle.

Usage: Basic

import {
    HOST_RESERVED_PREFIXES,
    isReservedPrefix,
} from "@suaveplan/api-host/reserved-prefix";

if (isReservedPrefix("/internal/foo")) {
    throw new Error("cannot mount under a host-reserved prefix");
}

console.log(HOST_RESERVED_PREFIXES); // ["/_host", "/internal"]

Usage: Advanced

Adapter-side, the typical wiring uses assertPrefixAvailable with the adapter's own error code namespace:

import { assertPrefixAvailable } from "@suaveplan/api-host/reserved-prefix";
import { SuaveplanError } from "@suaveplan/error";
import { AUTH_BA_CODES } from "@suaveplan/api-auth-better-auth/codes";

export function mountBetterAuth(prefix: string): void {
    assertPrefixAvailable(prefix, (collision) =>
        new SuaveplanError(AUTH_BA_CODES.ROUTE_PREFIX_COLLISION, {
            messageKey: "error.auth_ba.route_prefix_collision",
            context: { requested: prefix, collision },
        })
    );
    // …safe to register routes under `prefix` now.
}

The normalisePrefix helper is also exported so adapters that store an operator-supplied mount prefix at configuration time can canonicalise it once and rely on the form for subsequent comparisons.

API Reference

  • HOST_RESERVED_PREFIXES: ReadonlyArray<string> — frozen, deduped, normalised list of always-reserved prefixes.
  • isReservedPrefix(prefix: string): boolean — true when prefix (after normalisation) equals or is a child path of any reserved entry.
  • normalisePrefix(prefix: string): string — lowercases, prepends /, collapses trailing slashes; empty input → /.
  • assertPrefixAvailable(prefix, codeFactory) — invokes codeFactory(normalisedCollision) and throws when reserved; no-op otherwise.
  • brandPrefix(prefix: string): ReservedPrefix — normalises and returns the value retyped as the ReservedPrefix brand.
  • type ReservedPrefix — branded string carrying the proof of canonicalisation.
  • type ReservedPrefixErrorFactory — (collision: string) => Error.

Implementation Notes

The list is initialised by feeding ["/_host", "/internal"] through normalisePrefix, then deduping via Set, then Object.freeze so the returned array refuses mutation at runtime (strict mode throws TypeError). Predicate matching is O(n) over the (currently 2-entry) reserved list; for child-path matching the implementation uses startsWith(${reserved}/) rather than startsWith(reserved) to avoid the /internal reserves /internals false positive.

Engine-conditional entries (/admin, /graphql, /ws, /sse) are intentionally NOT in the static list. They will land via a follow-up api-host-reserved-prefix-dynamic capability that inspects the active host registry — keeping the static contract immutable and runtime-deterministic. The branded ReservedPrefix type uses a unique symbol field; the brand never materialises at runtime, so JSON.stringify of a branded value is identical to its underlying string.