# 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` 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 ```ts 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: ```ts 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` — 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.