Repository navigation
Commit 20949e7
Fixes #23142
Clause-②: no (narrowing) — it narrows the anonymous accept set to the
ADR-0056 D2 floor; no published schema or export moves.
## What changed
`packages/services/service-settings/src/settings-routes.ts#registerSettingsRoutes`
now mounts every route of the settings REST family through one door,
`mountFamilyRoute`. For each request the door:
1. resolves the caller with `contextFromRequest`, unchanged. An
authorization-store outage it raises still relays its own `503`.
2. asks the shared ADR-0056 D2 decision, `shouldDenyAnonymous` from
`@objectstack/core`. A caller whose context names no `userId` gets `401
UNAUTHENTICATED` (`ANONYMOUS_DENY_STATUS` / `ANONYMOUS_DENY_CODE` /
`ANONYMOUS_DENY_MESSAGE`), written through the family's existing
`sendError` envelope, before any settings logic runs.
3. only then runs the route's own work with that same context.
- **Mounted once for the family, not copied per handler.** Each route is
now a `{ serve, refuse }` pair: `serve` is the old `try` body and
`refuse` is the old `catch` arm, byte for byte. The four
`relayAuthzStoreOutage` calls collapse into the door's one.
- **A signed-in caller is served as before.** The per-namespace read and
write capabilities still decide what it may see and change.
- `isSystem` is not read: `SettingsContext` has no such field and
inbound HTTP never carries one. The request path is not passed either,
so a `basePath` moved under an allow-listed control-plane name inherits
no exemption.
- The 401 is written the way `service-datasource`'s admin family and
`@objectstack/rest`'s package family already write it: `sendError` with
the three shared constants.
- No new error code, no spec key, no export or option change.
This is the fourth door of the same shape. Ruling `6074960686` on #22146
settled the first three (#22430, #22431, #22432), and this change
follows the same seam: the shared decision, called before any of the
family's own logic.
PR #23137 (`domain:spec`, the anonymous sweep) is red on this family
alone; its everything-else case expects exactly this answer. That PR and
its files are not touched here.
## Producer, measured before the fix
On `origin/main` `02bec6ca86`: `git grep shouldDenyAnonymous` under
`packages/services/service-settings/src` gives 0 hits, and
`registerSettingsRoutes` gives its definition. The registrar's only
identity input is `contextFromRequest`; the plugin's resolver
(`verifiedContextFromRequest`) returns an `enforced` context with no
`userId` for a caller with no session, and the four routes handed that
straight to the service's capability checks. The landing is therefore
the registrar itself, as the card said.
The console reads the family only from signed-in views (the settings
pages and the administrator timezone prompt, which waits for a user and
an organization), and every in-repo dogfood caller sends a bearer token.
No anonymous consumer was found.
## Pins
New `settings-routes.anonymous-floor.pin.test.ts`:
- **Census read off the registration.** Every route is captured from a
mock HTTP server as the registrar registers it. Asserted: exactly 4
routes, equal to `SETTINGS_ROUTE_LEDGER`, and no middleware. A route
added later is driven by every case below the day it lands, and the
count turns red until this file is read.
- **Every captured route, five anonymous context shapes:** no resolver
wired; a resolver that resolved no principal, sync and async;
capabilities with no principal; the in-process `enforced: false` marker
with no principal. Plus a forged identity header. Each asserts `status`
401 and `error.code` `UNAUTHENTICATED` (with `success: false`, the
shared message and no `data`), against a service that records and fails
on any call, asserted untouched.
- **A `basePath` the shared allow-list would exempt** (non-vacuity
asserted with `isAuthGateAllowlisted`) is still refused on all four
routes.
- **Signed-in control per route:** `200`, and the service receives that
caller's own context object.
## Fixture triage: tests that leaned on an anonymous caller
The floor reads `userId` and nothing else, so each fixture without one
was re-judged:
- **The fixture modelled a signed-in caller and omitted the id.** It
gains one: the admin providers in `settings-routes.test.ts` and
`envelope.conformance.test.ts`, the read-only caller in
`settings-routes.test.ts`, the resolving controls in
`settings-routes.authz-outage-relay.test.ts`, and the fixtures in
`settings-crypto-fail-closed.test.ts` and
`localization-timezone-help.pin.test.ts`. An unknown-namespace write
case that used the anonymous default moves to a signed-in caller,
because an anonymous one no longer reaches the namespace lookup.
- **The pin asserted the anonymous answer this change retires.** It now
asserts the 401: the four anonymous cases in `settings-routes.test.ts`,
the anonymous read and write in `envelope.conformance.test.ts`, the
anonymous write through the plugin's own routes in
`config-change-audit.test.ts` (both ledgers still untouched), and the
six refused-key cases in `settings-admission-tenancy-posture.test.ts`. A
refused API key resolves to no principal, which is an anonymous caller;
the store-level "nothing lands" assertions are unchanged, and the file's
docblock records why.
- **Coverage the anonymous cases carried is re-homed.** The capability
filter and the `403 SETTINGS_FORBIDDEN` refusal are now driven by a
signed-in caller holding no capability: two new cases in
`settings-routes.test.ts` and one in the envelope sweep, where the
`SETTINGS_FORBIDDEN` details-slot case also moves to that caller.
## Ablation: the anonymous pins fail without the floor
The implementation was committed first. At HEAD `3bb73c0cd1` the floor
call was removed from the door with `scripts/ablation-replace.mjs`:
anchor 1 to 0, blob `c4f12f1dd2b9` to `6e12080a10ea`, and an on-disk
count of the floor call before / during / after of 1 / 0 / 1. The run
has a `trap` restore on EXIT, INT and TERM against an absolute path. The
whole package suite, mutated:
```text
Test Files 5 failed | 38 passed (43)
Tests 38 failed | 750 passed (788)
AssertionError: expected 500 to be 401 // Object.is equality
AssertionError: expected 403 to be 401 // Object.is equality
AssertionError: expected 200 to be 401 // Object.is equality
```
- 25 of the new file's 30 cases went red: every anonymous case (4 routes
× 5 shapes, 4 forged-header cases, the moved `basePath`). The census and
the 4 signed-in controls stayed green.
- The re-homed pins went red too: tenancy posture 6, settings-routes 4,
envelope 2, config-change audit 1.
- `500` is the untouchable service being reached.
- Restore was proven by bytes, not by an exit code: the blob after
restore equals HEAD's (`c4f12f1dd2b9`), and `git diff HEAD` is empty.
## Verification
- `@objectstack/service-settings` suite: base `02bec6ca86` gave 42 files
/ 755 tests passed. This branch gives 43 files / 788 tests passed.
`typecheck` (`tsc --noEmit`) exits 0, and `--listFiles` shows the new
pin file compiled (43 test files in the program).
- Dependency closure built first (`--filter
'@objectstack/service-settings^...' build`), then the package itself.
- Gates: re-derived with `scripts/pm/dispatch-gates.mjs --commands` from
this branch's own change set. That gives 65 commands, the same set as
the dispatch's reading. The results are in the report on the card.
- `check:dual-build-cjs-loads` printed PREREQUISITE NOT MET: it reads
every package's `dist/`, and this worktree built only the closure. **NOT
MEASURED**, and left to CI. Narrowed instead to the changed package:
`require` of its `dist/index.cjs` and `import` of `dist/index.js` both
load.
## Acceptance notes
- `content/docs/protocol/kernel/config-resolution.mdx` § HTTP Surface
describes the family's capability gating but not the anonymous floor in
front of it. It stays true, though incomplete. Not edited here; it is
outside the claimed file surface.
- The dogfood authorization conformance matrix draws its population from
the REST and runtime route ledgers only, so this family's floor is
ratcheted by its package-local pins and by PR #23137's live sweep, not
by that matrix.
- Not measured: whether a session held by an ADR-0069 auth policy (an
expired password, pending MFA) reaches this family through some host
middleware. The registrar does not consult that gate itself. Noted as an
inference, not a finding.
- `Clause-②` is copied verbatim from the claim, which carries no
direction arm although its own prose says the change narrows. The
changeset declares the narrowing as **BREAKING** (`minor`, launch-window
convention), with an ADR-0087 `not-required (no-migration-prescription)`
disposition, as the three sibling doors did.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e81cae0 commit 20949e7
10 files changed
Lines changed: 509 additions & 98 deletions
File tree
- .changeset
- packages/services/service-settings/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
547 | 547 | | |
548 | 548 | | |
549 | 549 | | |
550 | | - | |
| 550 | + | |
551 | 551 | | |
552 | 552 | | |
553 | 553 | | |
554 | | - | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
555 | 557 | | |
556 | 558 | | |
557 | 559 | | |
558 | 560 | | |
559 | 561 | | |
560 | | - | |
561 | | - | |
| 562 | + | |
| 563 | + | |
562 | 564 | | |
563 | 565 | | |
564 | 566 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
64 | | - | |
65 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
66 | 66 | | |
| 67 | + | |
| 68 | + | |
67 | 69 | | |
68 | 70 | | |
69 | 71 | | |
| |||
199 | 201 | | |
200 | 202 | | |
201 | 203 | | |
| 204 | + | |
| 205 | + | |
202 | 206 | | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
203 | 216 | | |
204 | 217 | | |
205 | 218 | | |
206 | | - | |
| 219 | + | |
207 | 220 | | |
208 | 221 | | |
209 | 222 | | |
| |||
230 | 243 | | |
231 | 244 | | |
232 | 245 | | |
233 | | - | |
234 | | - | |
| 246 | + | |
| 247 | + | |
235 | 248 | | |
236 | 249 | | |
237 | 250 | | |
| |||
338 | 351 | | |
339 | 352 | | |
340 | 353 | | |
341 | | - | |
| 354 | + | |
342 | 355 | | |
343 | 356 | | |
344 | 357 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
| 100 | + | |
100 | 101 | | |
101 | 102 | | |
102 | | - | |
| 103 | + | |
103 | 104 | | |
104 | 105 | | |
105 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
36 | 43 | | |
37 | 44 | | |
38 | 45 | | |
| |||
454 | 461 | | |
455 | 462 | | |
456 | 463 | | |
457 | | - | |
458 | | - | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
459 | 467 | | |
460 | 468 | | |
461 | 469 | | |
| |||
464 | 472 | | |
465 | 473 | | |
466 | 474 | | |
467 | | - | |
468 | | - | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
469 | 478 | | |
470 | 479 | | |
471 | 480 | | |
472 | | - | |
473 | | - | |
474 | | - | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
475 | 485 | | |
476 | 486 | | |
477 | 487 | | |
478 | | - | |
479 | | - | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
480 | 491 | | |
481 | 492 | | |
482 | 493 | | |
483 | 494 | | |
484 | 495 | | |
485 | | - | |
| 496 | + | |
| 497 | + | |
486 | 498 | | |
487 | 499 | | |
488 | 500 | | |
| |||
504 | 516 | | |
505 | 517 | | |
506 | 518 | | |
507 | | - | |
| 519 | + | |
| 520 | + | |
508 | 521 | | |
509 | 522 | | |
510 | 523 | | |
| |||
553 | 566 | | |
554 | 567 | | |
555 | 568 | | |
556 | | - | |
| 569 | + | |
| 570 | + | |
557 | 571 | | |
558 | 572 | | |
559 | 573 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
382 | 382 | | |
383 | 383 | | |
384 | 384 | | |
385 | | - | |
| 385 | + | |
| 386 | + | |
386 | 387 | | |
387 | 388 | | |
388 | 389 | | |
| |||
0 commit comments