Initial Commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Host Reserved-Prefix Contract
|
||||
|
||||
**Package**: `@suaveplan/api-host`
|
||||
**Status**: PROPOSED
|
||||
**Type**: capability-extension
|
||||
|
||||
## Why
|
||||
|
||||
Stage 6.3 (`api-auth-better-auth`) needs to refuse mounting under host-reserved path prefixes (`/_host`, `/internal`, `/graphql`, etc.) and surface `AUTH_BA_ROUTE_PREFIX_COLLISION`. There is no canonical list — `api-host` uses `/internal/livez` and `/internal/readyz` internally, and Stage 7 will add `/graphql`, but no contract exists for a downstream adapter to query "is this prefix reserved?". Hard-coding the list inside each adapter would drift the moment any host route changes.
|
||||
|
||||
This change adds the reserved-prefix contract to `@suaveplan/api-host`:
|
||||
|
||||
- `HOST_RESERVED_PREFIXES` — frozen `ReadonlyArray<string>` of all prefixes the host (and its first-party engines) reserve.
|
||||
- `isReservedPrefix(prefix: string): boolean` — predicate consumers call before mounting.
|
||||
- `assertPrefixAvailable(prefix, codeFn)` — throws via the consumer-supplied code when reserved.
|
||||
- `./reserved-prefix` subpath export so adapters import without pulling the full host bundle.
|
||||
|
||||
## What changes
|
||||
|
||||
- New feature folder `packages/api-stack/api/api-host/src/reserved-prefix/` with `reserved-prefix.ts`, `reserved-prefix.types.ts`, `reserved-prefix.md`, `reserved-prefix.test.ts`, `index.ts`.
|
||||
- `package.json` gains `./reserved-prefix` subpath export.
|
||||
- Capability spec gains the ADDED requirements below.
|
||||
- No breaking changes; pure addition.
|
||||
|
||||
## Reserved list (initial)
|
||||
|
||||
`/_host`, `/internal`, `/admin` (when admin-api mounted), `/graphql` (when GraphQL engine mounted), `/ws` (when WS engine mounted), `/sse` (when SSE engine mounted). Engine-conditional entries are returned only when the engine slot is filled in the registry; the static list returned by `HOST_RESERVED_PREFIXES` is the always-reserved subset.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Dynamic engine-conditional resolution beyond the static list. A follow-up (`api-host-reserved-prefix-dynamic`) will land when Stage 7 ships GraphQL/WS/SSE engines.
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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"`
|
||||
@@ -0,0 +1,29 @@
|
||||
# Tasks — Host Reserved-Prefix Contract
|
||||
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Add `src/reserved-prefix/reserved-prefix.types.ts` with `ReservedPrefix` branded type and `ReservedPrefixError<TCode>` shape.
|
||||
- [x] 1.2 Add `src/reserved-prefix/reserved-prefix.ts` exporting:
|
||||
- `HOST_RESERVED_PREFIXES: ReadonlyArray<string>` (frozen, deduped, lowercase, slash-normalised).
|
||||
- `isReservedPrefix(prefix: string): boolean`.
|
||||
- `normalisePrefix(prefix: string): string`.
|
||||
- `assertPrefixAvailable<TCode extends string>(prefix: string, codeFactory: (collision: string) => Error): void`.
|
||||
- [x] 1.3 Add sibling `reserved-prefix.md` (≥200 words, Purpose/Features/Usage:Basic/Usage:Advanced/API Reference/Implementation Notes).
|
||||
- [x] 1.4 Add `src/reserved-prefix/index.ts` barrel (named re-exports only — rule 10).
|
||||
- [x] 1.5 Wire `./reserved-prefix` subpath into `package.json` exports.
|
||||
- [x] 1.6 Re-export from top-level barrel `src/index.ts`.
|
||||
|
||||
## 2. Tests
|
||||
|
||||
- [x] 2.1 Unit: `reserved-prefix.test.ts` — coverage of every predicate path (exact match, normalised match, child path, sibling path, unrelated prefix). 100% istanbul on touched modules.
|
||||
- [x] 2.2 Conformance scenarios: prefix normalisation (`/Internal` ↔ `/internal`, trailing slash, missing leading slash), assertion throws when reserved, no-op when free.
|
||||
|
||||
## 3. Pre-publish gate
|
||||
|
||||
- [x] 3.1 `bunx turbo lint typecheck test --filter=@suaveplan/api-host` green.
|
||||
- [x] 3.2 `bunx turbo test:coverage --filter=@suaveplan/api-host` 100% on touched modules.
|
||||
|
||||
## 4. Archive
|
||||
|
||||
- [x] 4.1 Fold delta into `openspec/specs/api-host/spec.md`.
|
||||
- [x] 4.2 Move folder to `openspec/archive/2026-05-26-api-host-reserved-prefix/`.
|
||||
Reference in New Issue
Block a user