Skip to content

Commit ea00591

Browse files
os-trumpclaude
andauthored
feat(cloud-connection,metadata,cli): ledger the six raw-app route mounters and guard them (#12152)
* feat(cloud-connection,metadata,cli): ledger the six raw-app route mounters and guard them Six registrars across three packages mount HTTP routes on the host Hono app's framework-native handle (`http-server` -> `getRawApp()`), so their routes sit outside the dispatcher ledger, outside `RestServer.getRoutes()`, and outside `IHttpServer.getMountedRoutes()` -- the last one by the contract's own words, "routes an adapter mounts on its framework-native handle behind `getRawApp` are outside this table by construction". None carried a reviewed disposition anywhere. Adds three per-package ledgers in the #3636 / #11863 pattern plus a guard for each. All six files are new; no existing file is touched and no route behaviour changes. The guards read package SOURCE rather than driving plugin lifecycles: every one of these registrars mounts from inside a `kernel:ready` hook behind multi-service resolutions that return quietly when a service is absent, so a lifecycle drive would fail OPEN -- observing zero mounts while every accounting assertion passed vacuously. That is the completed-census defect these ledgers exist to remove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa * build(cli): exclude the console route ledger from the shipped tsc program `packages/cli` compiles its whole `include` program rather than an entry graph, so the #11882 route ledger -- a review record read only by its own conformance test, which the test globs already exclude -- was emitted into the published tarball as ~10KB of permanently dead module. Its two sibling ledgers needed no such line only because `tsup` bundles from `src/index.ts` and never reached them; this makes cli's end state match theirs. Safe against a future import, measured rather than assumed: `exclude` filters the `include` glob but does NOT remove a file that an included file imports -- TypeScript still pulls such a file in through the module graph and emits it. So this line can under-exclude, never dangle. The ledger remains in the package's `tsconfig.json` typecheck program; only the build config drops it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6274a1a commit ea00591

7 files changed

Lines changed: 1999 additions & 1 deletion

packages/cli/src/utils/console-route-ledger.conformance.test.ts

Lines changed: 413 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
];

packages/cli/tsconfig.build.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,29 @@
2525
// `packages/cli/.objectstack` residue survived a fix to the source: the run
2626
// was still executing the pre-fix compiled duplicate. It also means a source
2727
// test could be edited to pass while its stale twin asserted the old thing.
28-
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/__tests__/**"]
28+
//
29+
// `console-route-ledger.ts` joins them for the same reason one step over
30+
// (#11882). It is a REVIEW RECORD, not runtime code: the audited disposition
31+
// of every route this package mounts on the host Hono app, read only by
32+
// `console-route-ledger.conformance.test.ts` — which is itself already
33+
// excluded by the test globs above. Nothing in the shipped CLI imports it, so
34+
// without this line `tsc` emitted ~10KB of permanently dead module into the
35+
// published tarball. This package compiles its whole `include` program rather
36+
// than an entry graph, which is why its two sibling ledgers
37+
// (`cloud-connection`, `metadata`) needed no such line: `tsup` bundles from
38+
// `src/index.ts` and never reached them. The exclusion makes this package's
39+
// END STATE match theirs.
40+
//
41+
// Safe against a future import, and that is a measured property rather than a
42+
// hope: `exclude` filters the `include` glob, it does NOT remove a file that
43+
// an included file imports — TypeScript still pulls such a file in through
44+
// the module graph and emits it. So if shipped code ever imports this ledger,
45+
// it comes back into the build automatically; this line can under-exclude,
46+
// never dangle.
47+
"exclude": [
48+
"src/**/*.test.ts",
49+
"src/**/*.spec.ts",
50+
"src/**/__tests__/**",
51+
"src/utils/console-route-ledger.ts"
52+
]
2953
}

0 commit comments

Comments
 (0)