/** * The wire contract: every backend route this repo names. * * A test that asserts a route NAME proves nothing unless it proves the name * RESOLVES. `expect(fetch).toHaveBeenCalledWith('/api/…')` passes happily * against a route the router 404s — that is how 4.0.96 shipped a POST against a * route the backend did not serve and locked every buyer out of every * password-protected event. * * This module is the single source of truth two gates read: * * route-answers-contract.test.ts (every PR, offline) * every API call site in src/ must name a route listed here, with a verb * and body shape that match. * * route-answers-probe.test.ts (CI job, hits the deployed backend) * every route listed here must be ANSWERED by the router. That is what * keeps this file from being fiction. * * ROUTER-404 vs RESOURCE-404 — the whole diagnosis. The backend replies to an * unrouted request with a 404 whose SHAPE depends on which host answered: the * public host names it back (`{"code":"NOT_FOUND","message":"Route not found: * POST /api/…"}`), the orders-v2 host answers a generic `{"code":"NOT_FOUND", * "message":"Not Found"}` (#808). A route that EXISTS but whose resource does * not replies 400 "Invalid event ID", or 404 "Order not found" / CART_NOT_FOUND, * or 401, or 200 — a RESOURCE-level answer. `isRouterNotFound()` below is the one * place that distinction is made, per-host. * * PROBE SAFETY. The probe test asks PRODUCTION. A probe recipe is only correct if * it CANNOT MUTATE — and that claim is worthless unless something checks it. It * is load-bearing, not decorative: `POST /orders/create` with an EMPTY body * answers 200 and creates a real order. * * So every mutating row (POST/PUT/DELETE) must declare `probe.safety`, and * route-answers-probe.test.ts asserts the declaration against what production * actually answers. That holds for EVERY mutating row in BOTH lists — an UNSERVED * row is not exempt. "It is a router-404, so no handler runs" is circular: it is * true until the day the row's whole purpose is fulfilled and the backend ships the * route. The probe sends the request BEFORE it can know whether the name resolved, * so on that day the POST lands on a live handler, and the safety claim is the only * thing asserting it bounced. Two shapes are legal, and only two: * * refused — the handler REJECTS the recipe (400 invalid id, 401, 404, 422) and * never reaches its write. Assert the status is one it declared. * * inert — the handler ACCEPTS the recipe but it targets a resource that * cannot exist, so it changes nothing, and the response SAYS SO. * `POST /orders/complete/:uuid` is a bulk update keyed on the UUID: * it answers 200 "Completed 0 reservations" for every UUID, refusing * nothing — there is no rejecting recipe to write. Assert the row * count in the body: the response itself proves zero rows moved. * * A row that cannot honestly claim either shape must not be probed at all. */ /** Host prefixes. `client.ts` prefixes any endpoint not starting with `http` * with PUBLIC — a bare `/orders/create` means `${PUBLIC}/orders/create`. */ export declare const PUBLIC = "/api/v2/public"; export declare const ORDERS_V2 = "/api/orders/v2/public"; export declare const ORDERS_V2_ROOT = "/api/orders/v2"; /** * The verbs this contract knows — runtime, not just a type. The source scanner in * route-answers-contract.test.ts checks a call site's `method:` against THIS list * and goes red on anything else. When the scanner carried its own regex * alternation instead, a `method: 'PATCH'` matched nothing, fell through to the * `'GET'` default and was checked against the GET row: green, silently, at a route * that serves no PATCH. One list, read by both. */ export declare const HTTP_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "DELETE"]; export type HttpMethod = (typeof HTTP_METHODS)[number]; /** * Whether a verb written at a call site is one this contract can reason about. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function isHttpMethod(verb: string): verb is HttpMethod; /** * Why a probe cannot mutate production — see PROBE SAFETY above. Required on * every POST/PUT/DELETE row and asserted live; the offline gate fails a mutating * row that omits it, so the safety claim can never again be author discipline. */ export type ProbeSafety = { kind: 'refused'; /** Statuses the handler answers when it refuses this recipe. Never 2xx. */ statuses: readonly number[]; } | { kind: 'inert'; /** The status the handler answers when it accepts this recipe. */ status: number; /** Must match the response body, and must prove nothing was written. */ proof: RegExp; /** Why accepting it changes nothing. */ why: string; }; export interface Probe { path: string; body?: unknown; headers?: Record; /** Required on POST/PUT/DELETE. Reads cannot mutate, so GET/HEAD omit it. */ safety?: ProbeSafety; } export interface BackendRoute { method: HttpMethod; /** Full path from the host root. Path params are written `:name`. */ path: string; /** Whether the route takes a request body. */ body: 'required' | 'none'; /** A request that reaches the router and cannot change anything behind it. */ probe: Probe; } /** * The verbs that can write. These are the probes that need a safety claim. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function isMutating(method: HttpMethod): boolean; /** * Turn a manifest pattern into a matcher: `:name` matches any single segment. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function patternMatches(pattern: string, concrete: string): boolean; /** * A path param value that is syntactically valid and cannot exist. * * Exported because production code needs it too: `testNetworkConnection()` asks a * SERVED route about this id to find out whether the router answers at all. Same * id, same reason, one definition — an id-that-cannot-exist written out twice is * two ids that can drift. */ export declare const NONEXISTENT_ID = "00000000-0000-0000-0000-000000000000"; /** * Routes the deployed backend ANSWERS. Verified by route-answers-probe.test.ts. * Adding a row here without the probe agreeing is exactly the lie this gate exists * to catch — the probe will fail the row. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare const SERVED_ROUTES: BackendRoute[]; export interface UnservedRoute extends Omit { /** Where the dead name is spoken. */ callSites: string[]; /** What the buyer sees because the name resolves to nothing. */ impact: string; issue: string; } /** * Routes this repo NAMES that the router does not serve — verified router-404. * * This is a debt ledger, not a sanctioned end-state. It exists so the gate can * land red-free while every dead name stays visible and non-regressible: the * contract test fails if a NEW call site names a route in neither list, and the * probe fails if a route listed here starts answering (fix the call site, then * move the row into SERVED_ROUTES). The list may only ever shrink. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare const UNSERVED_ROUTES: UnservedRoute[]; export interface RetiredRoute { method: HttpMethod; /** The dead name, exactly as the ledger row used to spell it. */ path: string; /** Who used to speak it, and what happened to them. */ wasNamedBy: string; issue: string; } /** * Dead names this repo has STOPPED speaking. The ledger's graveyard. * * UNSERVED_ROUTES says "may only ever shrink" — and until now nothing enforced * that. Two ways to put a dead name back were wide open: re-add the row, or write * a fresh call site and baseline it. Either one turns the debt ledger into a * silencer, and a silencer is how the 4.0.96 password outage shipped in the first * place. * * So a retired route is retired in both directions, asserted in * route-answers-contract.test.ts: * * - nothing in src/ may NAME it again (a new call site goes red, not baselined); * - it may not reappear in UNSERVED_ROUTES (you cannot re-open the debt); * - it may not appear in SERVED_ROUTES either — if the backend genuinely ships * the route one day, DELETE the row here in the same commit that adds the * served one with a probe recipe proving the router answers it. That is a * deliberate two-place edit, which is the whole point: coming back from the * dead should cost more than adding a line. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare const RETIRED_ROUTES: RetiredRoute[]; /** * The router-404 discriminator — the one place the distinction is made. * * TRUE = the name resolved to nothing (the bug), per the answering HOST's * unrouted-name shape. * FALSE = the router answered; whatever the handler then said (400 invalid id, * 404 "Order not found", 401, 501, 200) is a RESOURCE-level answer and * is none of this gate's business — OR the path belongs to a host this * manifest does not know, in which case no router-404 can be claimed and * `hostsToCanary` is what fails closed on it. * * Takes the PATH, not just the body: the orders-v2 and public hosts answer a * dead name with different bodies, so the body alone cannot be read without * knowing which host produced it (#808). * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function isRouterNotFound(status: number, bodyText: string, path: string): boolean; export interface CanaryHost { /** Host label, for the canary's failure message. */ label: string; /** A path under this host that no route serves — to probe its 404 shape. */ deadPath: (_nonce: string) => string; } /** * The distinct hosts a set of probe paths touches, each with a dead-route path * builder — so the canary can prove the discriminator works on EVERY host the * manifest probes, derived from the rows rather than hardcoded, so a host added * later cannot get a free pass (#808). * * FAILS CLOSED: a probe path that belongs to no known host throws here, because * the discriminator cannot tell a dead name from a live one on a host whose 404 * shape it has never been taught. A new host prefix must be added to HOSTS (with * its router-404 discriminator) before its rows can be trusted. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function hostsToCanary(paths: readonly string[]): CanaryHost[]; /** * HEAD is answered by the GET route — Express routes the two together. * @internal the manifest's readers are the route-answers contract and probe suites - nothing in the shipped widget reads it, which is the point of a manifest. */ export declare function effectiveMethod(method: HttpMethod): HttpMethod;