|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * cli console route ledger — the audited disposition of every HTTP route this |
| 5 | + * package mounts on the host app's framework-native handle (#11882, in the |
| 6 | + * #3636 / #11863 pattern). |
| 7 | + * |
| 8 | + * WHY THIS EXISTS. Both plugin factories in `utils/console.ts` resolve the HTTP |
| 9 | + * server, take the Hono handle through `getRawApp()`, and register straight on |
| 10 | + * it. Those mounts are outside every ledger the platform had, and outside the |
| 11 | + * reach of the #7526 live-mount parity gate as well: that gate reads |
| 12 | + * `IHttpServer.getMountedRoutes()`, and "routes an adapter mounts on its |
| 13 | + * framework-native handle behind `getRawApp` are outside this table by |
| 14 | + * construction" — the contract's own words |
| 15 | + * (`packages/spec/src/contracts/http-server.ts`). |
| 16 | + * |
| 17 | + * ## WHY THIS LEDGER HAS A SIXTH DISPOSITION, AND WHY THAT IS NOT A DISTORTION |
| 18 | + * |
| 19 | + * This is the family #11882 singled out. The routing comment on that card |
| 20 | + * (2026-08-24) and the card body both flag it: these rows are **static-asset |
| 21 | + * serving, not API surface**. None of the five words the REST-ledger vocabulary |
| 22 | + * carries describes them truthfully, and the nearest one is actively |
| 23 | + * misleading: |
| 24 | + * |
| 25 | + * - `sdk` — false. No client method builds these URLs, and none should. |
| 26 | + * - `gap` — false, and it is the WRONG KIND of false: `gap` means |
| 27 | + * "should be in the SDK and is not", and it is ratcheted to |
| 28 | + * <= 0 across this programme. Filing a static file server as |
| 29 | + * a gap would assert that `@objectstack/client` ought to |
| 30 | + * grow a method for fetching `index.html`, and would reverse |
| 31 | + * a ratchet to say it. |
| 32 | + * - `server-only` — false. That word means an inbound integration door or a |
| 33 | + * loopback (webhooks, HMAC-token reads). These are the |
| 34 | + * opposite: outbound bytes to a browser. |
| 35 | + * - `public` — TRUE but insufficient, and this is the trap. These routes |
| 36 | + * ARE anonymous and browser-facing, so `public` is the |
| 37 | + * nearest allowed word — which is exactly why it is the |
| 38 | + * wrong one to reach for. It would file a static file |
| 39 | + * server alongside genuine anonymous API endpoints like |
| 40 | + * `GET /api/v1/runtime/config`, and a reader auditing the |
| 41 | + * platform's unauthenticated API surface would find four |
| 42 | + * rows here that are not API at all. The peer group is the |
| 43 | + * discriminator (`check-auth-mount-ledger.mjs`'s rule), and |
| 44 | + * these routes' peer group is a CDN, not an endpoint. |
| 45 | + * |
| 46 | + * `check-auth-mount-ledger.mjs` states the governing rule for precisely this |
| 47 | + * situation: *"IF YOU CANNOT DECIDE, DO NOT PICK THE NEAREST ALLOWED WORD."* |
| 48 | + * Here the disposition is not undecided — the card, the routing comment and the |
| 49 | + * source all agree on what these are — so this ledger says it in a word that is |
| 50 | + * true: `static-asset`. The precedent that a per-package ledger may extend the |
| 51 | + * vocabulary when the shared words are false is `plugin-auth`, whose |
| 52 | + * `AUTH_ROUTE_LEDGER` carries a sixth disposition of its own (`disabled`). |
| 53 | + * |
| 54 | + * The extension is deliberately CONTAINED: this type is package-local, so it |
| 55 | + * cannot widen the vocabulary any other ledger is read against, and the guard |
| 56 | + * asserts that `static-asset` is used ONLY for routes that serve bytes off |
| 57 | + * disk — a word that could be reached for by an API route would just be a new |
| 58 | + * parking space. |
| 59 | + * |
| 60 | + * SCOPE, re-derived on `origin/main` @ 2ba4329e rather than inherited from the |
| 61 | + * filing: four routes across two plugin factories, both in `utils/console.ts`, |
| 62 | + * which the guard confirms is the ONLY file in this package's 109 sources that |
| 63 | + * mounts a route at all. |
| 64 | + * |
| 65 | + * This module is package-internal: it is the guard's data, not public API, and |
| 66 | + * `@objectstack/cli` is a binary rather than a consumed library surface. It |
| 67 | + * must stay import-free. |
| 68 | + */ |
| 69 | + |
| 70 | +/** |
| 71 | + * Disposition of a single cli-mounted route. The five REST-ledger words, plus |
| 72 | + * `static-asset` — see the header for why the five cannot express this family. |
| 73 | + */ |
| 74 | +export type ConsoleRouteDisposition = |
| 75 | + /** Expressed by the SDK — `client` names the method (dotted path). */ |
| 76 | + | 'sdk' |
| 77 | + /** Should be in the SDK and is not — an open, acknowledged gap. */ |
| 78 | + | 'gap' |
| 79 | + /** Deliberately not SDK surface (inbound integration doors, loopbacks). */ |
| 80 | + | 'server-only' |
| 81 | + /** Public, unauthenticated browser-facing API route. */ |
| 82 | + | 'public' |
| 83 | + /** Server and client disagree on the shape — needs reconciliation. */ |
| 84 | + | 'mismatch' |
| 85 | + /** |
| 86 | + * Not API surface at all: serves bytes off disk (or redirects to something |
| 87 | + * that does). An SDK method here would be a category error, so this word |
| 88 | + * records a reviewed NON-question rather than a deferred one. |
| 89 | + */ |
| 90 | + | 'static-asset'; |
| 91 | + |
| 92 | +export interface ConsoleRouteLedgerEntry { |
| 93 | + /** `VERB /path` — the full wire path, verbatim as mounted. */ |
| 94 | + route: string; |
| 95 | + /** Registrar family (the plugin factory that mounts it). */ |
| 96 | + family: string; |
| 97 | + /** The `src/`-relative file whose mount call produced this row. */ |
| 98 | + mountedIn: string; |
| 99 | + disposition: ConsoleRouteDisposition; |
| 100 | + /** Dotted method path on `ObjectStackClient` — required when disposition is `sdk`. */ |
| 101 | + client?: string; |
| 102 | + /** |
| 103 | + * True when the mount is guarded by a condition rather than unconditional. |
| 104 | + * Recorded because the census reads SOURCE: it sees the mount call, not the |
| 105 | + * branch around it, and a row that silently implied "always mounted" would |
| 106 | + * overstate the surface. |
| 107 | + */ |
| 108 | + conditional?: string; |
| 109 | + /** One-line rationale. Required for every non-`sdk` disposition. */ |
| 110 | + note?: string; |
| 111 | +} |
| 112 | + |
| 113 | +export const CONSOLE_ROUTE_LEDGER: readonly ConsoleRouteLedgerEntry[] = [ |
| 114 | + // ── console SPA static serving (createConsoleStaticPlugin) ───────── |
| 115 | + { |
| 116 | + route: 'GET /', |
| 117 | + family: 'console-static', |
| 118 | + mountedIn: 'utils/console.ts', |
| 119 | + disposition: 'static-asset', |
| 120 | + conditional: 'options.rootRedirect !== false (default: mounted)', |
| 121 | + note: |
| 122 | + 'redirects the site root to `/_console/`. The Console is the default end-user surface, so claiming `/` is the ' |
| 123 | + + 'intended behaviour in both dev and production once the Console is mounted at all; `os serve` gates whether ' |
| 124 | + + 'it mounts via `--no-console` / `OS_DISABLE_CONSOLE=1`. Not API surface — a redirect to a static bundle. ' |
| 125 | + + 'CONDITIONAL, and the ledger says so because the census reads the mount call and cannot see the branch.', |
| 126 | + }, |
| 127 | + { |
| 128 | + route: 'GET /_console', |
| 129 | + family: 'console-static', |
| 130 | + mountedIn: 'utils/console.ts', |
| 131 | + disposition: 'static-asset', |
| 132 | + note: |
| 133 | + 'redirects the bare mount path to its trailing-slash form, the ordinary SPA convention — the Console is built ' |
| 134 | + + 'with `base: \'/_console/\'`, so relative asset URLs only resolve from the slashed path. Pure navigation ' |
| 135 | + + 'plumbing for a static bundle; there is nothing here for an SDK to express.', |
| 136 | + }, |
| 137 | + { |
| 138 | + route: 'GET /_console/*', |
| 139 | + family: 'console-static', |
| 140 | + mountedIn: 'utils/console.ts', |
| 141 | + disposition: 'static-asset', |
| 142 | + note: |
| 143 | + 'serves the pre-built Console SPA verbatim from `dist/`, with HTML entry points routed through base-tag ' |
| 144 | + + 'injection and an SPA fallback for client-side routes. Reads files off disk behind a path-traversal guard ' |
| 145 | + + '(any resolved path escaping `dist/` is refused 403). A file server, not an endpoint: its peer group is a ' |
| 146 | + + 'CDN origin, so no client method builds these URLs and none should.', |
| 147 | + }, |
| 148 | + |
| 149 | + // ── runtime asset serving (second factory in the same module) ────── |
| 150 | + { |
| 151 | + route: 'GET /runtime/assets/:filename', |
| 152 | + family: 'runtime-assets', |
| 153 | + mountedIn: 'utils/console.ts', |
| 154 | + disposition: 'static-asset', |
| 155 | + note: |
| 156 | + 'serves individual build assets off disk by filename, behind two guards: separators are stripped from the ' |
| 157 | + + 'parameter and any resolved path escaping the assets directory is refused 403. Sent with a one-hour ' |
| 158 | + + '`cache-control`, which is the tell that this is CDN-shaped rather than API-shaped. A distinct family from ' |
| 159 | + + '`console-static` because it is a separate plugin factory with its own dist root and its own mount guard.', |
| 160 | + }, |
| 161 | +]; |
0 commit comments