first commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# reserved-prefix
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```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<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.
|
||||
Reference in New Issue
Block a user