Skip to content

Commit 8323cad

Browse files
test(dogfood): the anonymous door sweep — ADR-0138 criterion 2 as an in-repo conformance test (#23137)
Part of #22146 Clause-②: no (a test; no published accept set or public surface changes) ## Summary The test half of ruling `6134095444`: ADR-0138 acceptance criterion 2 is evidenced by a test in the tree, not by a one-off sweep. This PR adds one dogfood suite and its classification table, nothing else: - `packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts` - `packages/qa/dogfood/test/anonymous-door-sweep.classification.ts` No production code changes, and nothing under `docs/adr/**`. The criterion-2 wording and criterion 4's two back-pointers are the next PR of the ruling, after this test is green on `main`. **The suite is red on this head. That is the result, not a defect in the test.** The everything-else class does not answer 401 throughout: one surface family outside D2's list answers an anonymous caller without the 401 floor (class-level result below). As the dispatch orders, the assertion stays as D2 states it. It is not weakened and not marked expected-to-fail, and the PM decides the next step. ## What the suite does 1. **Boot.** It boots the showcase the way a deployment serves it: `bootShowcase`, plus two extras. `MetadataPlugin` runs over a build-shaped artifact, because `createStandaloneStack` composes it and declared endpoints only route through it. `MCPServerPlugin` is added because `serve` mounts MCP by default. 2. **Population.** It reads the registered route table off the running server, from three sources: - the port, `getMountedRoutes()`; - the framework's own table, `getRawApp().routes`, which also holds the raw-app mounts; - the declared ADR-0121 endpoints the metadata service matches. Nothing in the population is listed by hand. The derivation is guarded by a floor per source, by asserting the port table is a subset of the framework table, by one anchor per source, and by checking that every declared endpoint resolves through `matchEndpoint` to itself. 3. **One unauthenticated request per route.** Middleware is the exception, because it serves no request of its own. 4. **A closed table.** Each answer is judged by the route's class: D2's six door classes, the surfaces outside the guest model by construction, middleware, and nothing else. - A route the table does not claim is in the everything-else class. It must answer 401 with an error envelope and nothing served, so a new anonymous door nobody classified turns the suite red. - A classification row that claims no route on the boot is red as stale. 5. **Every class on a real subject, admitted and refused.** A sweep whose probes name nothing can only observe the refusal half of a door. The subjects are the showcase's own or are created in-test while signed in: the public form, the shareable brief, the public book, an uploaded file. The showcase has no subject for three of the legs, so they are declared inline beside it: - an anonymous `object_operation` endpoint and an anonymous `flow` endpoint, over a flow with no data step; - an `org`-audience book; - two docs. The showcase's own two declared endpoints are `authRequired: true` and of the same two types, so they serve as the negative controls of classes 4 and 5. The assertions are class-level. A failure names the route pattern and the status class it answered, never a body. ## Classification, for the ADR amendment to name **Door classes (D2):** | class | routes | |:--|:--| | 1 · the public form doors | resolve and submit | | 2 · share links | the two token-addressed reads | | 3 · the public book and doc reads | no route of its own: it rides the shared `/meta` read routes, which the sweep holds to 401 for every other subject | | 4 · declared `authRequired: false` endpoints, `object_operation` | the declared endpoints | | 5 · declared `authRequired: false` endpoints, `flow` | the declared endpoints | | 6 · the `acl: 'public_read'` file download | both download routes | **Outside the guest model by construction.** Each row carries its reason in the table. - D2's own list: - the control-plane allowlist, by its own predicate `isAuthGateAllowlisted`, never a copy of its paths; - the ADR-0041 signed inbound hook, asserted 401 on a missing signature, as D2 states; - MCP, asserted 401, as D2 states. - The surfaces the ruling ordered classified explicitly: - the approvals plugin's token-addressed action pages (the ADR-0043 action token); - the OAuth and OpenID well-known documents; - `RuntimeConfigPlugin`'s bootstrap reads; - the MCP skill document read; - the dev-only metadata HMR stream, which this boot mounts because `bootStack` runs the development posture. - Two more that the population held and the round-5 list did not name: - the discovery documents, at the two spellings `isAuthGateAllowlisted` does not name literally (the REST root and the dispatcher's well-known document); - the local storage adapter's signed byte URLs, where the signed, expiring token an authorized upload or download mints is the credential. **Middleware:** the server-wide `*` registrations. ## Class-level result on this head | class | matches D2 | |:--|:--| | 1 · public form doors | yes | | 2 · share links | yes | | 3 · public book and doc reads | yes | | 4 · `authRequired: false` `object_operation` | yes (D3's empty state: the guest holds no grant, so it is denied) | | 5 · `authRequired: false` `flow` | yes (admitted at the door as the guest; the flow's output only) | | 6 · `acl: 'public_read'` file download | yes | | everything else | **no**: one family | The family, at function level only: the settings service's REST family (`registerSettingsRoutes` in `@objectstack/service-settings`, mounted by `SettingsServicePlugin`). It opens no handler with an anonymous floor, so it answers an anonymous caller from its own authorization logic rather than with 401. On this boot it served no settings value. The status class is still not the one D2 states. The finding is in the report on the card, for the seat to file. ## Proof the suite can fail Two ablations, each through `scripts/ablation-replace.mjs` (anchor hit, then restore proven by blob hash against `HEAD` and an empty `git diff HEAD`): - **A, the closed table.** One outside-family row (the runtime-config bootstrap reads) was deleted from the classification. - Mutated: the everything-else case named 4 → 6 routes, and every other case was unchanged. - Restored: blob `eb86b66c06` == `HEAD`. - **B, a product floor, through `dist/`.** One dispatcher domain's anonymous floor (`shouldDenyAnonymous` in the `/i18n` domain of `@objectstack/runtime`) was bypassed in source, and runtime was rebuilt. - Mutated: `ablation-dist-preflight` found the marker in 2 built files, and the everything-else case named 4 → 7 routes. - Restored: blob `d111be27d4` == `HEAD`, runtime rebuilt, the marker absent from all 6 built files, the tree clean, and the everything-else case back to the same 4. Both moved in the predicted direction: more diagnostics, not a different failure. ## Tests Run on head `bf3ba9957`, which merges `origin/main` `dfb3ba2b2`: - `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/anonymous-door-sweep.dogfood.test.ts test/authz-conformance.test.ts`: 74 passed, 1 failed. The failing case is the everything-else one, on the settings family. `authz-conformance` is green; it scans every `*.dogfood.test.ts` for row attribution, and the new file claims no row. - `pnpm --filter @objectstack/dogfood typecheck` (`tsc --noEmit`): exit 0. `tsc --listFilesOnly` lists both new files in the program. - eslint, narrowed to the 2 new files with `--no-inline-config --format json`: 0 errors, 0 warnings over 2 files. The narrowing is safe because `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project` or `projectService` in the printed config), so no untouched file's result can change. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 53 commands from the merge base. All 53 were run on this head and every one exits 0. The `--ran` reconcile prints `53 derived famil(ies) accounted for — 53 run, 0 NOT-MEASURED (a DERIVED zero — all 53 recorded an exit code and none of them is 3)`. Two of them first exited 3 (PREREQUISITE NOT MET): one needed a pinned commit the shallow clone lacked, and one needed 8 packages that had no `dist/`. Each was re-run once its prerequisite was met, and both exit 0. ## Acceptance notes - **Where it runs.** The suite lands in the `isolated` dogfood project by its file name; `packages/qa/dogfood/vitest.config.ts` is untouched, and PR #22962 edits it. It runs with the dogfood battery, which is part of `main`'s full battery. PR and queue runs carry the fast set, so this draft's own CI does not run it. Landing it while the settings family is open makes the dogfood battery red on `main`. - **Two codes for one refusal.** The everything-else 401s carry two codes: `UNAUTHENTICATED`, and `AUTH_REQUIRED` on the storage family's refusals. D2's text names `UNAUTHENTICATED`, while criterion 2 judges the status class and what is served. The suite asserts the status and the error envelope and does not pin the code. Noted for the ADR wording PR. - **What a failure prints.** A failure message names the route pattern and the status class. On `main` that text lands in CI logs. It never carries a body. - **No changeset.** The dogfood package is private, so this PR carries `skip-changeset`. ## Unparked (written by the seat, `domain:spec` seat 2) - **Was parked** in draft on #23142: the settings REST family answered anonymous callers without the 401 floor, and this suite's everything-else case was red on that family alone. - **Unlocked:** #23142 landed on `main` as `20949e7a7` (PR #23171). `main` was merged into this branch through the relay's base sync (head `87a0f748a`). - **Re-run on `87a0f748a`** (the dev's run through `os-verify-lock`): `Tests 75 passed (75)` across this suite and `authz-conformance`, and the dogfood typecheck exits 0. Classes 1 to 6 match D2, and the everything-else class answers 401 throughout (the suite's floor of more than 100 routes judged held). - The dogfood battery is not in PR or queue CI; it runs on `main`, where this suite now runs with it. - The criterion-2 wording PR on `docs/adr/**` (Tier H) follows once this suite is green on `main`. --- _Generated by [Claude Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
1 parent d4aaeaf commit 8323cad

2 files changed

Lines changed: 997 additions & 0 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// The CLOSED classification the anonymous door sweep judges every mounted route
4+
// by (ADR-0138 D2; the test half of ruling 6134095444 on #22146).
5+
//
6+
// ── What this table is ───────────────────────────────────────────────────────
7+
//
8+
// D2 says an unauthenticated request is served by exactly six door classes,
9+
// that a short list of surfaces is "outside the guest model by construction",
10+
// and that EVERYTHING ELSE answers 401. This module writes that sentence down as
11+
// data the sweep can execute:
12+
//
13+
// - `DOOR_CLASSES` the six classes, each with the routes that carry it
14+
// and the class-level answer D2 gives an anonymous
15+
// request that names no declared subject;
16+
// - `OUTSIDE_FAMILIES` the surfaces outside the guest model by
17+
// construction, each with the reason it is outside;
18+
// - `MIDDLEWARE` registrations that answer no request of their own;
19+
// - and NOTHING ELSE. A route none of the above claims is in the
20+
// everything-else class, and must answer 401.
21+
//
22+
// ⛔ That last line is the whole point. There is no "unclassified" bucket and no
23+
// tolerance list: a new anonymous door that nobody wrote down here lands in the
24+
// everything-else class and turns the sweep red, which is the "a new surface is
25+
// therefore 401 until this list names it" sentence of D2 made mechanical.
26+
// Naming a new door class is an amendment of ADR-0138, never an edit here alone.
27+
//
28+
// ── What a row may and may not say ───────────────────────────────────────────
29+
//
30+
// Rows are keyed by the pattern the router registered (`METHOD pattern`, `*` for
31+
// every verb), or by the platform's own predicate where one exists (the
32+
// control-plane allowlist is `isAuthGateAllowlisted` itself, never a copy of its
33+
// paths). Every row carries the reason it is where it is.
34+
//
35+
// The expectations are CLASS-LEVEL: one predicate per class, over the status
36+
// class and over whether anything was served. No row pins one route's own
37+
// answer, and no measured route-level reading belongs in this file (#22146:
38+
// classes, positions and functions only).
39+
//
40+
// ── Ordering ─────────────────────────────────────────────────────────────────
41+
//
42+
// First match wins, in this order: door classes, outside families, the
43+
// control-plane allowlist, middleware. The explicit outside families sit ahead
44+
// of the allowlist on purpose: two of them (the approval action page and the
45+
// signed inbound hook) are ALSO admitted by the allowlist predicate, and each
46+
// deserves its own reason and, for the hook, its own D2-stated answer.
47+
48+
import { isAuthGateAllowlisted } from '@objectstack/core';
49+
50+
/** One route of the swept population. */
51+
export interface SweptRoute {
52+
/** `GET`, `POST`, … or `ALL` for a framework registration on every verb. */
53+
readonly method: string;
54+
/** The registered pattern (`/api/v1/meta/:type`), or a declared endpoint's literal path. */
55+
readonly pattern: string;
56+
/**
57+
* Where the population read it from:
58+
* - `port` — `IHttpServer.getMountedRoutes()`, the adapter's record of every
59+
* route registered through the HTTP-server contract;
60+
* - `raw` — the framework's own route table only (a mount on the raw app
61+
* handle, outside the port by construction);
62+
* - `declared` — an ADR-0121 declared endpoint, served by the transport's
63+
* fallback seam and therefore in no route table at all.
64+
*/
65+
readonly source: 'port' | 'raw' | 'declared';
66+
/** For `declared`: the parsed declaration the metadata service matches. */
67+
readonly endpoint?: { readonly type?: string; readonly authRequired?: boolean };
68+
}
69+
70+
/** What one anonymous request got back, as the sweep records it. */
71+
export interface SweepAnswer {
72+
readonly status: number;
73+
readonly contentType: string;
74+
/** The parsed body when the answer was JSON, else `undefined`. */
75+
readonly json?: Record<string, unknown>;
76+
}
77+
78+
/** `undefined` when the answer is the class's answer, otherwise why it is not. */
79+
export type ClassExpectation = (answer: SweepAnswer) => string | undefined;
80+
81+
const isJson = (a: SweepAnswer): boolean => a.json !== undefined;
82+
83+
/** Nothing served: an error envelope, never a `data` payload or a success flag. */
84+
function servedNothing(a: SweepAnswer): string | undefined {
85+
if (!isJson(a)) return undefined;
86+
if (a.json!.success === true) return 'the body says success:true';
87+
if ('data' in a.json!) return 'the body carries a data payload';
88+
return undefined;
89+
}
90+
91+
/** The everything-else answer: 401, as an error envelope, with nothing served. */
92+
export const EVERYTHING_ELSE: ClassExpectation = (a) => {
93+
if (a.status !== 401) return `answered ${a.status}, not 401`;
94+
if (!isJson(a) || a.json!.error === undefined) return 'answered 401 without an error envelope';
95+
return servedNothing(a);
96+
};
97+
98+
/** A door that admits only by a declared subject, asked about none: a 4xx refusal, nothing served. */
99+
const REFUSED_WITHOUT_A_SUBJECT: ClassExpectation = (a) => {
100+
if (a.status < 400 || a.status >= 500) return `answered ${a.status}, not a 4xx refusal`;
101+
return servedNothing(a);
102+
};
103+
104+
const codeOf = (a: SweepAnswer): unknown => {
105+
const error = a.json?.error;
106+
return error && typeof error === 'object' ? (error as { code?: unknown }).code : a.json?.code;
107+
};
108+
109+
export type DoorClassId = 1 | 2 | 3 | 4 | 5 | 6;
110+
111+
export interface DoorClass {
112+
readonly id: DoorClassId;
113+
/** D2's own name for the class. */
114+
readonly name: string;
115+
/** D2's enforcer for it, by function. */
116+
readonly enforcedBy: string;
117+
/**
118+
* The routes that carry this class. `undefined` for the one class that has no
119+
* route of its own (class 3 rides the shared `/meta` read routes, whose every
120+
* other reading is everything-else); see `ridesOn`.
121+
*/
122+
readonly match?: (r: SweptRoute) => boolean;
123+
/** For a class with no route of its own: the routes it rides, all in the population. */
124+
readonly ridesOn?: readonly string[];
125+
/** The class-level answer to the sweep's request on one of its routes. */
126+
readonly expect?: ClassExpectation;
127+
}
128+
129+
const routeIs = (...keys: string[]) => {
130+
const set = new Set(keys);
131+
return (r: SweptRoute) => set.has(`${r.method} ${r.pattern}`);
132+
};
133+
134+
const declaredAnonymous = (type: string) => (r: SweptRoute) =>
135+
r.source === 'declared' && r.endpoint?.authRequired === false && r.endpoint?.type === type;
136+
137+
/** ADR-0138 D2's six door classes. */
138+
export const DOOR_CLASSES: readonly DoorClass[] = [
139+
{
140+
id: 1,
141+
name: 'the public form doors (resolve and submit)',
142+
enforcedBy: 'RestServer.registerFormEndpoints; the publicFormGrant branch of SecurityPlugin',
143+
match: routeIs('GET /api/v1/forms/:slug', 'POST /api/v1/forms/:slug/submit'),
144+
// Admitted by the form's own `sharing` declaration; a slug no form declares
145+
// is no door.
146+
expect: REFUSED_WITHOUT_A_SUBJECT,
147+
},
148+
{
149+
id: 2,
150+
name: 'share links',
151+
enforcedBy: 'registerShareLinkRoutes (plugin-sharing), the capability token',
152+
// The two token-addressed reads. The create / list / revoke routes beside
153+
// them manage links and need a signed-in caller: they are everything-else.
154+
match: routeIs('GET /api/v1/share-links/:token/resolve', 'GET /api/v1/share-links/:token/messages'),
155+
expect: REFUSED_WITHOUT_A_SUBJECT,
156+
},
157+
{
158+
id: 3,
159+
name: 'the public book and doc metadata reads',
160+
enforcedBy: 'isPublicAudienceRead (rest meta-item-read-gate), the ADR-0046 §6.7 audience gate',
161+
// No route of its own: the audience gate admits by the book's
162+
// `audience: 'public'` VALUE, on the same read routes every other metadata
163+
// read takes. Asked about any other subject those routes are
164+
// everything-else and answer 401, so the sweep holds them to 401 and the
165+
// class itself is asserted on its real subjects.
166+
ridesOn: ['GET /api/v1/meta/:type', 'GET /api/v1/meta/:type/:name', 'GET /api/v1/meta/book/:name/tree'],
167+
},
168+
{
169+
id: 4,
170+
name: "authRequired: false endpoints of type 'object_operation'",
171+
enforcedBy: 'resolveExecutionContext (the guest envelope); the CRUD gate',
172+
match: declaredAnonymous('object_operation'),
173+
// The guest envelope under the guest's grants (D3). No grant is bound to
174+
// the guest on this boot, so D3's empty state applies: deny all.
175+
expect: (a) => {
176+
if (a.status !== 403) return `answered ${a.status}, not D3's empty-state 403`;
177+
if (codeOf(a) !== 'PERMISSION_DENIED') return `answered 403 with code ${String(codeOf(a))}`;
178+
return servedNothing(a);
179+
},
180+
},
181+
{
182+
id: 5,
183+
name: "authRequired: false endpoints of type 'flow'",
184+
enforcedBy: 'resolveExecutionContext (the guest envelope), then @objectstack/service-automation',
185+
match: declaredAnonymous('flow'),
186+
// Admitted at the door as the guest; what the run answers is the flow's own
187+
// declared behaviour, asserted on the class's fixture, not here.
188+
expect: (a) => (a.status === 401 ? 'answered 401: the door did not admit the guest' : undefined),
189+
},
190+
{
191+
id: 6,
192+
name: "the acl: 'public_read' file download",
193+
enforcedBy: 'registerStorageRoutes, at the download gate authorizeDownload both download routes call',
194+
match: routeIs('GET /api/v1/storage/files/:fileId', 'GET /api/v1/storage/files/:fileId/url'),
195+
// Admitted by the file's own `acl: 'public_read'`; an id that names no such
196+
// file is no door.
197+
expect: REFUSED_WITHOUT_A_SUBJECT,
198+
},
199+
];
200+
201+
export type OutsideFamilyId =
202+
| 'signed-inbound-hook'
203+
| 'mcp'
204+
| 'mcp-skill-document'
205+
| 'approval-action-token'
206+
| 'storage-signed-url'
207+
| 'discovery'
208+
| 'oauth-discovery-metadata'
209+
| 'console-bootstrap'
210+
| 'dev-metadata-hmr'
211+
| 'control-plane-allowlist';
212+
213+
export interface OutsideFamily {
214+
readonly id: OutsideFamilyId;
215+
/** Why this surface is outside the guest model by construction. */
216+
readonly reason: string;
217+
readonly match: (r: SweptRoute, probePath: string) => boolean;
218+
/** Only where D2 itself states the anonymous answer. */
219+
readonly expect?: ClassExpectation;
220+
}
221+
222+
const statusIs401: ClassExpectation = (a) => (a.status === 401 ? servedNothing(a) : `answered ${a.status}, not 401`);
223+
224+
/**
225+
* The surfaces outside the guest model by construction. The first three are
226+
* D2's own list (the allowlist, the signed inbound-hook channel, MCP); the rest
227+
* are the token-addressed approval pages and the raw-app mounts ruling
228+
* 6134095444 ordered classified explicitly, each with its reason.
229+
*/
230+
export const OUTSIDE_FAMILIES: readonly OutsideFamily[] = [
231+
{
232+
id: 'signed-inbound-hook',
233+
reason:
234+
'ADR-0041 signed inbound hook (trigger-api verifySignature): the per-flow secret is the credential, and ' +
235+
'D2 states the answer to a missing or bad signature: 401',
236+
match: routeIs('POST /api/v1/automation/hooks/:flowName/:hookId'),
237+
expect: statusIs401,
238+
},
239+
{
240+
id: 'mcp',
241+
reason: 'MCP admits an OAuth token or an API key and never an anonymous caller (D2), so its answer is 401',
242+
match: (r) => r.pattern === '/api/v1/mcp',
243+
expect: statusIs401,
244+
},
245+
{
246+
id: 'mcp-skill-document',
247+
reason:
248+
'the portable Agent Skill document (ADR-0036 Amendment C, renderSkillMarkdown): generic instructions and the ' +
249+
"environment's MCP URL, never a tenant's schema or data; the verbs it does not serve answer for themselves",
250+
match: (r) => r.pattern === '/api/v1/mcp/skill',
251+
},
252+
{
253+
id: 'approval-action-token',
254+
reason:
255+
"ADR-0043 actionable-link pages (ApprovalsServicePlugin's peek and redeem): the single-use action token is " +
256+
'the whole credential, and a decision binds the approver that token names, never a guest; ' +
257+
'isAuthGateAllowlisted admits this exact route',
258+
match: routeIs('GET /api/v1/approvals/act', 'POST /api/v1/approvals/act'),
259+
},
260+
{
261+
id: 'storage-signed-url',
262+
reason:
263+
"the local storage adapter's signed byte URLs: the signed, expiring token an authorized upload or download " +
264+
"mints is the credential, the role a cloud store's presigned URL plays, and no principal is evaluated",
265+
match: routeIs('GET /api/v1/storage/_local/raw/:token', 'PUT /api/v1/storage/_local/raw/:token'),
266+
},
267+
{
268+
id: 'discovery',
269+
reason:
270+
"the discovery documents (the REST root and the dispatcher's well-known document): the allowlist's " +
271+
'discovery role, at the two spellings isAuthGateAllowlisted does not name literally',
272+
match: routeIs('GET /api/v1', 'GET /.well-known/objectstack'),
273+
},
274+
{
275+
id: 'oauth-discovery-metadata',
276+
reason:
277+
'the OAuth / OpenID discovery metadata (RFC 8414, RFC 9728) plugin-auth serves: public by standard, read by ' +
278+
'an MCP or OAuth client before it holds any token, and carrying no business data',
279+
match: (r) =>
280+
r.method === 'GET' &&
281+
(r.pattern.startsWith('/.well-known/oauth-authorization-server') ||
282+
r.pattern.startsWith('/.well-known/oauth-protected-resource') ||
283+
r.pattern === '/.well-known/openid-configuration'),
284+
},
285+
{
286+
id: 'console-bootstrap',
287+
reason:
288+
"RuntimeConfigPlugin's bootstrap reads: the console's pre-sign-in branding and runtime posture, no tenant data",
289+
match: routeIs('GET /api/v1/runtime/config', 'GET /api/v1/studio/runtime-config'),
290+
},
291+
{
292+
id: 'dev-metadata-hmr',
293+
reason:
294+
"MetadataPlugin's metadata HMR stream: a build-tool door mounted only under an explicit " +
295+
'NODE_ENV=development posture (isDevMetadataEndpointEnabled), never on a production deployment',
296+
match: routeIs('GET /api/v1/dev/metadata-events', 'POST /api/v1/dev/metadata-events'),
297+
},
298+
{
299+
id: 'control-plane-allowlist',
300+
reason:
301+
'the control-plane allowlist, by its own predicate (isAuthGateAllowlisted): sign-in and the auth surface, ' +
302+
'health, readiness, discovery and the current-user reads the ADR-0069 remediation screen needs',
303+
match: (r, probePath) => isAuthGateAllowlisted(probePath, r.method === 'ALL' ? 'GET' : r.method),
304+
},
305+
];
306+
307+
/** Registrations that run around other routes and answer no request of their own. */
308+
export const MIDDLEWARE: readonly { readonly key: string; readonly reason: string }[] = [
309+
{
310+
key: 'ALL /*',
311+
reason: 'the server-wide middleware (request context, CORS, error shaping): it wraps every route and serves none',
312+
},
313+
];
314+
315+
export type SweepClass =
316+
| { readonly kind: 'door'; readonly door: DoorClass }
317+
| { readonly kind: 'outside'; readonly family: OutsideFamily }
318+
| { readonly kind: 'middleware' }
319+
| { readonly kind: 'everything-else' };
320+
321+
/** The one classification of a route. First match wins, in the order the header states. */
322+
export function classify(route: SweptRoute, probePath: string): SweepClass {
323+
for (const door of DOOR_CLASSES) if (door.match?.(route)) return { kind: 'door', door };
324+
for (const family of OUTSIDE_FAMILIES) if (family.match(route, probePath)) return { kind: 'outside', family };
325+
if (MIDDLEWARE.some((m) => m.key === `${route.method} ${route.pattern}`)) return { kind: 'middleware' };
326+
return { kind: 'everything-else' };
327+
}
328+
329+
/**
330+
* A concrete request path for a registered pattern: every `:param` becomes a
331+
* value no declaration can name, and a trailing `*` a segment under it.
332+
*/
333+
export function probePathOf(pattern: string): string {
334+
let n = 0;
335+
return pattern
336+
.split('/')
337+
.map((seg) => (seg.startsWith(':') ? `anon-sweep-${n++}` : seg === '*' ? 'anon-sweep-any' : seg))
338+
.join('/');
339+
}

0 commit comments

Comments
 (0)