Files
suaveplan/openspec/archive/2026-05-26-api-host-reserved-prefix/specs/api-host/spec.md
2026-05-27 23:29:53 +01:00

3.7 KiB

Capability Spec Delta: api-host — reserved-prefix contract

Package: @suaveplan/api-host Status: PROPOSED

ADDED Requirements

Requirement: Publishes the canonical reserved-prefix list

@suaveplan/api-host SHALL export HOST_RESERVED_PREFIXES as a frozen ReadonlyArray<string> of every URL path prefix the host (and any first-party engine that has shipped) reserves. The list MUST be normalised (lowercase, leading-slash, no trailing slash) and deduped. Downstream adapters that mount their own routes MUST consult this list (directly or via isReservedPrefix) before mounting and refuse to install on collision.

Scenario: list contains every always-reserved host route

  • WHEN an adapter reads HOST_RESERVED_PREFIXES
  • THEN it contains /_host and /internal (always-reserved); /admin, /graphql, /ws, /sse are included once their respective host modules ship

Scenario: list is frozen

  • WHEN consumer code attempts HOST_RESERVED_PREFIXES.push("/foo")
  • THEN the assignment throws TypeError

Verified by: src/reserved-prefix/reserved-prefix.test.ts::"HOST_RESERVED_PREFIXES contains always-reserved prefixes" AND src/reserved-prefix/reserved-prefix.test.ts::"HOST_RESERVED_PREFIXES is frozen"


Requirement: Exposes a predicate + assertion helper

The package SHALL export isReservedPrefix(prefix: string): boolean and assertPrefixAvailable(prefix: string, codeFactory: (collision: string) => Error): void. The predicate MUST treat input prefixes case-insensitively after normalisation and MUST match both exact prefixes (/internal reserves /internal) and child paths (/internal/livez is also reserved). The assertion MUST throw the error produced by codeFactory(collision) when reserved, returning void otherwise — letting each adapter surface its own code namespace (AUTH_BA_*, RBAC_*, …) without coupling.

Scenario: predicate matches exact + child paths

  • WHEN isReservedPrefix("/internal") and isReservedPrefix("/internal/livez") are called
  • THEN both return true

Scenario: predicate normalises case and trailing slash

  • WHEN isReservedPrefix("/Internal/") is called
  • THEN it returns true

Scenario: assertion throws the factory-supplied error on collision

  • WHEN assertPrefixAvailable("/_host", () => new MyError("X")) is called
  • THEN the call throws MyError("X")

Scenario: assertion is a no-op when free

  • WHEN assertPrefixAvailable("/auth", factory) is called and /auth is not in the list
  • THEN the call returns void and factory is NOT invoked

Verified by: src/reserved-prefix/reserved-prefix.test.ts::"isReservedPrefix matches exact and child paths" AND src/reserved-prefix/reserved-prefix.test.ts::"isReservedPrefix normalises case and trailing slash" AND src/reserved-prefix/reserved-prefix.test.ts::"assertPrefixAvailable throws supplied error on collision" AND src/reserved-prefix/reserved-prefix.test.ts::"assertPrefixAvailable is no-op when prefix is free"


Requirement: Published via ./reserved-prefix subpath

The contract MUST be importable from @suaveplan/api-host/reserved-prefix so adapters can pull only the predicate surface without bundling the full host. The subpath MUST forward types and runtime export of the same set documented in HOST_RESERVED_PREFIXES, isReservedPrefix, assertPrefixAvailable, normalisePrefix.

Scenario: subpath import works in TypeScript and at runtime

  • WHEN import { isReservedPrefix } from "@suaveplan/api-host/reserved-prefix" is used
  • THEN the symbol resolves AND TypeScript types it as (prefix: string) => boolean

Verified by: src/reserved-prefix/reserved-prefix.test.ts::"subpath export resolves and types are forwarded"