Repository navigation
Commit 0d34066
refactor(rest): computeExecCtx calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext (#24260)
Part of #23217
Clause-②: no
Ruling-ref: 6227928430
Slice 2 of #23217 under ruling record 6227928430 (letter A): REST's
door, `RestServer#computeExecCtx` in `packages/rest/src/rest-server.ts`,
calls the one `resolveRequestExecutionContext` that slice 1 landed in
`@objectstack/core` (`f5ef054a7`). #23217 remains open for three doors
(`service-settings`, `service-storage`, `plugin-hono-server`), each a
later slice. No export moves and there is no app-author surface.
## What changed
- **`computeExecCtx`** keeps what is this door's own and calls the
function once. 192 lines out, 90 in. What stays at the door, in its old
order: the environment and auth-service resolution with the `if
(!authService) return undefined` early return, the header normalisation
(append per array element), the engine through `wiredEngineOrLoud` (the
kernel branch still absorbs), the tenancy wiring (now a thunk: the
kernel's `getServiceAsync('tenancy')`, else `tenancyServiceProvider`,
else none), then `__kernel`, `__authEnvironmentId`, the `Server-Timing`
disclosure, the memo in `resolveExecCtx`, and the fail-closed `catch`
with its `apiKeyLifecycleRefusals` side channel.
- **REST's named parameters**, each measured against the hand-written
code on `f5ef054a7` before it was replaced (slice 1's handover,
confirmed row by row):
| parameter | REST passes | what the hand-written code did |
|:--|:--|:--|
| `anonymous` | `'fail-closed'` | `if (!authz.userId) return undefined`
and `assembleExecutionContext` |
| `authGate` | `'reread-when-active'` | a key: `authz.authGate`. A
session: re-read only while `isAuthGateActive()`; a throwing probe reads
as inactive; a failed re-read is
`AuthzStoreUnavailableError('auth_gate')` |
| `credentials` | `'api-key-and-session'` | `headers` and `getSession`
both handed to `resolveAuthzContext` |
| `sessionApi` | `'required'` | `if (!api?.getSession) return
undefined`, the API-key door included |
| `session` | `'request'` |
`api.getSession(inProcessSessionReadInput(headers))`, a throw read as no
session |
| `tenancy` | the thunk above | `classifyAdmissionTenancyPosture` on the
same two wirings |
| `localization` | `{ settings: () => provider ?
seamOrUndefined(provider(env)) : undefined }` | the same lookup, then
`resolveLocalizationContext` |
| `requestLocale` | `this.extractLocale(req)` | the same parse, at
assembly |
| `accessToken` | `'withhold'` | `accessToken: undefined` |
| `oauth` / `onAdmission` | `undefined` / `undefined` | none |
- **The readers move with the call.**
- `scripts/check-single-authz-resolver.mjs`: the rest row of
`DELEGATORS` now names `resolveRequestExecutionContext`. Left on
`resolveAuthzContext`, check (2) would pass on the five prose mentions
of it that remain in the file.
- `packages/core/src/security/authz-store-unavailable.test.ts`:
`rest-server.ts` no longer calls `resolveAuthzContext({`, so its ledger
row goes (the set-equality audit requires it). The scanner's positive
control now names the composition's file.
- `packages/rest/src/execctx-authz-input-seam-reachability.test.ts` §0
and `auth-gate-allowlist-fault-window.measurement.test.ts`'s structural
pin read source text inside `computeExecCtx`. They are re-aimed in
place, with the superseded pins quoted, as those files ask. Each still
holds its subject: both tenancy wirings reach the classification, the
tenancy region holds no `catch`, the door declares
`'reread-when-active'` and keeps no second copy of the gate decision,
and the call hands over no path. The decision's own window is pinned in
core's `request-execution-context.test.ts` (choice 2). The behavioural
drives in both files are unchanged and green.
-
`packages/runtime/src/security/request-execution-context.conformance.test.ts`
(ruling item 4) gains REST as a row. It drives
`RestServer.resolvePackageRouteExecutionContext` on the single-kernel
wiring and sets aside `__kernel` and `__authEnvironmentId` by name (both
asserted absent on this wiring). It has a new request that tells
`'resolved'` from `'reread-when-active'` (the gated session while no
gate is active) and a REST CONTROL case. Who a request resolves to is
now derived from the row's declared `sessionApi`: REST authenticates
nobody on a host with no auth service, while the runtime door admits the
key there.
- **The ruling's one ADR-0096 / ADR-0069 pin** is in
`packages/rest/src/rest-exec-ctx-principal-kind.test.ts`. It is a
six-row table driven through the real `GET /api/v1/data/:object` route
and read at the protocol:
- no credential gets `401` with exactly `ANONYMOUS_DENY_BODY`, and
`findData` is never called;
- a held session while a gate is active gets `403` with its code and
message, and the engine is never asked;
- the same session while no gate is active is admitted as `member1`,
with no `authGate` on the envelope;
- an API key beside the held cookie answers for its own owner;
- an active gate whose re-read fails gets `503 SERVICE_UNAVAILABLE`, and
the engine is never asked;
- CONTROL: an ungated session is admitted.
- **Changeset:** `patch` on `@objectstack/rest`
(`.changeset/23217-s2-rest-door.md`). The published `rest-server` code
changes, and the one ordering note below is a fact a reader of the
package can meet.
## Evidence (head `5acdd53a9`)
- **Merge of `main` at `fb6fb24e2`** (merge commit `5acdd53a9`; no
rebase, amend or force-push). One conflicted hunk: the import block of
`rest-server.ts` (about :220). Main (#23050 slice 13c) dropped the
`metaSaveRequestOptions` import, and this branch dropped
`AuthSessionApi` from the contracts type import. It was resolved to
main's two comment lines and `import type { ISecurityService } from
'@objectstack/spec/contracts';`. Neither symbol is used as code in the
merged file; the only mention left is that comment. The merged file
differs from main by exactly this slice's 284 changed lines, and the PR
still touches the same 8 files. Census counts in `rest-server.ts`, main
/ merge: `resolveExecCtx` 55 / 55, `enforceAuth` 40 / 40,
`seamOrUndefined` 11 / 11, `wiredEngineOrLoud` 11 / 11. Re-measured on
the merge, after rebuilding the closure:
- `@objectstack/rest` typecheck exit 0 (test layer 0 / 0);
- ESLint `--no-inline-config` on `rest-server.ts` exit 0;
- the REST full local project: `Test Files 295 passed (295)`, `Tests
6042 passed | 326 skipped (6368)`, which includes this slice's pins, the
censuses and the `meta-disabled-package-doors.test.ts` file main
brought;
- conformance `Tests 9 passed (9)` and the core ledger `Tests 39 passed
(39)`;
- gates: 87 derived, 86 run with exit 0, and
`check:dual-build-cjs-loads` NOT MEASURED (exit 3), reconciled `0
UNRUN`.
The readings below were taken on the pre-merge head `96f05115f`.
- **Before / after.** The pin was committed first, on the hand-written
door (`865e7d135`): `Tests 20 passed (20)`. On the converted door it is
green again, inside the full suite.
- **REST, full local project, under the verify lock:** `Test Files 294
passed (294)`, `Tests 6036 passed | 326 skipped (6362)`, `VERDICT
command-exit 0`.
- **Targeted REST families** (execctx, tenancy posture, API-key
lifecycle and account posture, auth gate, direct mounts, in-process
session read, memo, censuses), at `e6e916a00` (same code): `Test Files
21 passed (21)`, `Tests 330 passed (330)`.
- **Conformance:** `Tests 9 passed (9)` (two rows, six requests, two
CONTROLs). Core repo-project ledger: `Tests 39 passed (39)` (40 before,
minus the rest row).
- **Typecheck:** `@objectstack/rest` exit 0 (test layer 0 / 0),
`@objectstack/core` exit 0 (4 files / 4 errors held),
`@objectstack/runtime` exit 0 (27 files / 190 errors held). All three
debt ledgers are unchanged.
- **Ablation, one-off.** Direction declared before running: RED.
`authGate: 'reread-when-active'` became `'withhold'` via
`scripts/ablation-replace.mjs` (anchor 1 to 0, blob `4aec26e3a6c4` to
`8f9ccb644aee`, marker count 1). REST gate pins: `Tests 21 failed | 61
passed (82)` over 5 of 6 files. The red pins include the new pin's 403
and 503 rows, `#7280`'s end-to-end 403, both direct-mount HELD cases and
the OUTAGE case, seam §4's POSITIVE CONTROL and its 503, and the
key-posture pins. `rest-auth-gate.test.ts` stays green; it hand-builds
its contexts. Conformance: `Tests 2 failed | 7 passed (9)` (the session
row and the REST CONTROL). Restore: `blob == HEAD (4aec26e) and git
diff HEAD is empty`, marker 0. Observed direction = declared direction.
- **Census terms held:** `resolveExecCtx` stays at 55 and `enforceAuth`
at 40 in `rest-server.ts`. These are the bare-term counts
`execctx-consumer-census.test.ts` and the dogfood
`authz-probe-blind-spot.census.ts` pin; the dogfood one does not run on
PRs. `seamOrUndefined` stays at 11 and `wiredEngineOrLoud` at 10.
- **The composition's distinctive code lines** in `rest-server.ts`, base
to head: `resolveAuthzContext({` 1 to 0,
`classifyAdmissionTenancyPosture(` 2 to 0, `assembleExecutionContext(` 1
to 0, `resolveLocalizationContext(` 1 to 0, `inProcessSessionReadInput(`
2 to 0, `normalizeAuthGate(` 1 to 0. `isAuthGateActive(` goes 3 to 1,
and the one left is a line of the gate table in a comment.
- ESLint `--no-inline-config` over the 7 changed `.ts` / `.mjs` files:
exit 0, no output.
## Acceptance notes
- **One ordering moved, measured.** The door resolves the engine before
the composition asks the auth service for its session read; before, it
asked first. A scratch probe (not committed) drove
`resolvePackageRouteExecutionContext` on both trees. Five of seven
readings are identical. The two that differ are double faults on the
single-kernel wiring: the configured engine provider fails AND the auth
service exposes no session read (or its `getApi()` rejects). Those
answered `401` (no context) and now answer `503 SERVICE_UNAVAILABLE`,
the engine outage. With a working auth service the engine outage was
already `503`, so the new answer no longer depends on the auth service's
state. The kernel branch absorbs an engine failure, so it is unaffected.
Production compositions build the auth service from plugin-auth, whose
`getApi()` always yields a session read. Keeping the old order byte for
byte needs either a second copy of the session-API check at the door,
which letter A item 3 forbids, or a lazy `ql` input on the function,
which is a change to slice 1's export and outside this slice. The
changeset states the window.
- **`extractLocale(req)`** is now evaluated at the call, before the
reads, instead of at assembly. It is a pure parse of the headers and
`?locale` that cannot throw for a request that passed the header check,
so no answer moves.
- **Files outside the claim's named surface**, for the seat to declare:
`packages/rest/src/execctx-authz-input-seam-reachability.test.ts` and
`packages/rest/src/auth-gate-allowlist-fault-window.measurement.test.ts`.
Both are REST source-text pins on `computeExecCtx`'s body, re-aimed in
place.
- Local gates: 88 derived, 87 run, all exit 0;
`check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: it needs a
whole-tree build), so it is NOT MEASURED locally and CI runs it.
`test:repo` is CI's.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a422f04 commit 0d34066
8 files changed
Lines changed: 420 additions & 241 deletions
File tree
- .changeset
- packages
- core/src/security
- rest/src
- runtime/src/security
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
183 | 182 | | |
184 | 183 | | |
185 | | - | |
| 184 | + | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| |||
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
302 | | - | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
303 | 306 | | |
304 | 307 | | |
305 | 308 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
286 | 286 | | |
287 | 287 | | |
288 | 288 | | |
289 | | - | |
290 | | - | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
291 | 304 | | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
296 | 310 | | |
297 | 311 | | |
298 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
197 | | - | |
198 | | - | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
199 | 214 | | |
200 | 215 | | |
201 | 216 | | |
202 | | - | |
| 217 | + | |
203 | 218 | | |
204 | | - | |
205 | | - | |
| 219 | + | |
| 220 | + | |
206 | 221 | | |
207 | 222 | | |
208 | 223 | | |
| |||
212 | 227 | | |
213 | 228 | | |
214 | 229 | | |
215 | | - | |
| 230 | + | |
216 | 231 | | |
217 | 232 | | |
218 | 233 | | |
| |||
255 | 270 | | |
256 | 271 | | |
257 | 272 | | |
258 | | - | |
259 | | - | |
260 | | - | |
261 | | - | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
268 | | - | |
269 | | - | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
270 | 296 | | |
271 | 297 | | |
272 | 298 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| |||
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
130 | | - | |
| 130 | + | |
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
143 | | - | |
| 143 | + | |
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
154 | | - | |
| 153 | + | |
| 154 | + | |
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
0 commit comments