Repository navigation
refactor(service-storage): the storage download door calls the one request-to-ExecutionContext composition, resolveRequestExecutionContext - #24364
Conversation
…the download route (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…-ExecutionContext composition (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 48 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e0387d6eae7150be77ab6f60b997bfe7442a2424 && git checkout e0387d6eae7150be77ab6f60b997bfe7442a2424
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a734e088899c8a50d8dbdefdd252e83b63a1308 e2891bfbc1a5b05fce87cef426be860f7500c623 && git checkout -B drift-repro 3a734e088899c8a50d8dbdefdd252e83b63a1308 && git merge --no-ff e2891bfbc1a5b05fce87cef426be860f7500c623
node scripts/docs-audit/affected-docs.mjs --json 3a734e088899c8a50d8dbdefdd252e83b63a1308
|
席位核对 — REWORK (patch round 1: one docblock table, no behaviour)
Accepted as it stands:
Item 3 (the family gate reader), the seat's decision:
Owed in this PR:
Redaction (done by the seat in this act): the reproduction of the gate gap had reached GitHub in this PR's body and in your report. Both were replaced with a pointer to #24366, under the maintainer's disclosure ruling (2026-08-18, #9387). ⛔ Do not reintroduce it. Before pushing: read this head's checks ( Generated by Claude Code |
… download door's 'resolved' gate and carried bearer (WIP) Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT ·
Generated by Claude Code |
Part of #23217
Clause-②: no
Ruling-ref: 6227928430
Basis for the
Clause-②line: the ruling's "later slicesClause-②: no". The storage door's outward answers are unchanged (a 12-row ADR-0096 / ADR-0069 pin and a 47-wiring fault probe read the same on both trees), and no export moves in@objectstack/service-storage. The function's export landed in slice 1.Slice 4 of #23217 under ruling record 6227928430 (letter A): the storage door, the download authorizer
buildFileReadAuthorizerinpackages/services/service-storage/src/storage-service-plugin.ts, calls the oneresolveRequestExecutionContextthat slice 1 landed in@objectstack/core.@objectstack/core's function is not touched (patch round 1 corrects two rows of its module docblock table, comment text only): every difference of the download door is one of the function's existing named parameters, so letter B does not arise for it. The card stays open: slice 5 (plugin-hono-server) remains, and the storage family's ADR-0069 gate reader is an item-3 question (below), not converted.What changed
The door
buildFileReadAuthorizerkeeps its door facts and calls the function once. Each parameter was measured on the hand-written door (origin/main68a090fb9, door blobff2307ad9) before converting:anonymous'fail-closed'userIdanswered'unauthenticated'(401AUTH_REQUIRED); unchangedauthGate'resolved'authGateis the posture the admitting read decided. The family door's gate reader runs first, so this door is not a second gate;'reread-when-active'would read the session a third time and add a 503 arm this door never hadcredentials'api-key-and-session'x-api-keyis honouredsessionApi'optional'session'request'tenancytenancyServiceResolver(registry)localization,requestLocale'withhold',undefinedaccessToken'carry'session.accessTokenfor a hook)oauth,onAdmissionundefinedDoor facts that stay at the door: the auth-service lookup (once per binding, as the gate reader does it), the header normalisation, the tenancy wiring, the owner shortcut, the owner-record and attachment-parent reads, and the fail-closed
catchthat re-raises the two brands.The tenancy wiring was split as settings' was:
tenancyServiceResolver(registry)is the door's way to the service, handed to the function; the gate reader'sresolveAdmissionTenancyPosturenow classifies through the same resolver. The privateGateAuthServicetype became@objectstack/core'sRequestAuthService, the slice of the auth service the function reads.What the door hands on
The context that reaches
readableFieldOwners(the owner-record read and afileAccessDelegateservice) and the attachment-parent reads is now the assembledExecutionContextenvelope instead of the rawResolvedAuthzContext. Measured on every resolved row of the probe: the key set gainsisSystem(false) andprincipalKind('human') and losescredential; every other key and value is the same. Readers measured onorigin/main: no source underpackages/objectql,plugin-security,plugin-sharingorplugin-approvalsreadscontext.credential; no reader comparesisSystemwithfalseorundefined;principalKindis read for'guest','agent','service'and'system'only (plus the security plugin's memo key, which is a cache key); the one file-access delegate (ApprovalService.authorizeFileRead) is typed forExecutionContextalready. No door-side projection is needed: the door itself reads onlyuserId.Readers moved in the same PR
packages/core/src/security/authz-store-unavailable.test.ts: the storage row ofTRANSPORT_LEDGERleft (the file no longer holdsresolveAuthzContext({), and the composition's comment names the fourth door.scripts/check-single-authz-resolver.mjs:DELEGATORSgained the storage row (resolveRequestExecutionContext).packages/runtime/src/security/request-execution-context.conformance.test.ts: aSTORAGE_ROWwith its own capture point (the read of the record that owns a private field-owned file, through the realGET /files/:fileId/urlon the booted plugin), and a storage CONTROL.DoorRowgainedansweredBeforeComposition: the storage door answers two requests before its composition runs (its family gate refuses the held session while a gate is active, and a host with no auth service binds no authorizer), so the row declares them, hands nothing on for them, and the CONTROL reads the answer the door gave (403PASSWORD_EXPIRED; 200 served unasked). The world fixture gained the owned file, the owner object, afindOne, and an auth service without a session read; the storagesessionApiis told apart there.Requestgainedheld.packages/runtime/package.json:@objectstack/service-storageas a devDependency (no workspace cycle: its 18-package closure, devDependencies included, never reaches@objectstack/runtime;check:workspace-manifest-cyclesexit 0),pnpm-lock.yamlregenerated bypnpm install(+3 lines), and an anchored test-only alias inpackages/runtime/vitest.config.ts(check:test-source-alias).scripts/slot-lookup-baseline.json: not touched.buildGetSession's any-typed lookup is still reached by its two other readers, andcheck:slot-lookupholds at 5 for the file.The pin (ruling item 2)
packages/services/service-storage/src/storage-routes.request-context-door.pin.test.tsboots the real plugin, drivesGET /files/:fileId/urlfor a private field-owned file, and reads what reached the service (which context the owner-record read received, and whether the adapter minted a capability). 12 rows: four ADR-0096 refusals (no credential, an unknown cookie, a failed session read with no gate active, an ex-member's key beside a cookie), four ADR-0069 rows (a held session 403, a failed re-read under an active gate 503, a held owner's key 403, a key beside a held cookie answers for its owner), the held session served with no gate active carrying the admitting read's posture, two CONTROLs (an ungated session carrying its bearer; a key on a host whose auth service has no session read) and the wiring row (no auth service: served unasked).Evidence
All on a shared container; lock seconds are shared-box readings.
d2888fa3d, door blobff2307ad9):Tests 13 passed (13)(the pin's 12 plus the one-off probe), and on the converted door (0d76d89eb, door blob1bdd64a15):Tests 13 passed (13).getApirejecting, its session read throwing, its gate probe throwing, its posture computation or key policy throwing, and its registry lookup throwing only after boot; held sessions and held owners' keys with the gate active and inactive; revoked, refused and org-less keys, alone and beside cookies).diffof the before and after readings: IDENTICAL on all 47 answers (status, code, who the owner read was asked as, capability minted). The only difference is the context's key set described above, plus one row: with the auth service's registry lookup throwing after boot and a held owner's key under an inactive gate, the context now carries the owner'sauthGate, because the door asks the auth service it bound for the posture where it used to look it up again per request; the verdict and the answer are unchanged (200, read as the key's owner).f24d8a103throughscripts/ablation-replace.mjsinside the verify lock (both suites read the door fromsrc: relative imports in the package, the runtime alias): leg 1authGate: 'resolved',to'withhold'(markerABLATION-23217-s4-gate, on disk marker 1 / anchor 0): pinTests 1 failed | 11 passed (12), conformanceTests 2 failed | 9 passed (11). Leg 2accessToken: 'carry',to'withhold'(markerABLATION-23217-s4-bearer): pinTests 2 failed | 10 passed (12), conformanceTests 2 failed | 9 passed (11). Each restore: blob equals HEAD (1bdd64a15) andgit diff HEADempty, marker 0. Observed direction equals declared.Tests (HEAD
f24d8a103, after mergingorigin/maindd72a047f)@objectstack/service-storage:Test Files 60 passed (60),Tests 1041 passed (1041);typecheckexit 0 (test layer 0 / 0 / 0 held), andtsc -p tsconfig.test.json --listFilesincludes the new pin.@objectstack/runtime: the conformance test,Test Files 1 passed (1),Tests 11 passed (11)(6 requests, the identity test, 4 CONTROLs);typecheckexit 0 (test layer 27 files / 190 errors / 68 signatures held, none in the conformance file). The rest of the runtime suite was not run locally: no other runtime test imports@objectstack/service-storage(only comments and strings name it), the alias is anchored, and CI's Test Core runs it.@objectstack/core: the repo-project ledgersrc/security/authz-store-unavailable.test.ts,Tests 37 passed (37)(38 before, minus the storage row);typecheckexit 0 (test layer 4 / 4 / 4 held). Core's code is untouched; patch round 1 (e2891bfbc) edits two docblock table rows.turbo run build --filter=!@objectstack/docs --concurrency=2after the merge,76 successful, 76 total.Gates (HEAD
f24d8a103)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 102 commands; each ran with its exit code captured before any pipe. 101 exited 0 on the first pass;pnpm check:type-check-debtexited 3 (PREREQUISITE NOT MET: its closure build did not get a turn on the verify lock), and was re-run under the lock on the same head: exit 0 (none above its recorded number).--ran:102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Among themcheck:authz-resolver,check:slot-lookup(100 unswept site(s) in 25 file(s), none new),check:test-source-alias,check:cross-package-test-inputs,check:workspace-manifest-cycles,check:dual-build-cjs-loads(exit 0 on the built tree),check:dts-closure,check:published-files,check:type-check-coverage,check:pm-dispatch-gates.dispatch-gates.mjswith the changed paths, no--commands): all 11 families run, all exit 0:check:init-service-contract,check:live-db-isolation,check:meta-type-normalized,check:optional-error-sink,check:resume-authority-declared,check:route-envelope,check:runner-env-posture,check:settings-bind-window,check:startup-registry-verdict,check:verify-stand-in,check:wildcard-fallthrough. Alsocheck:adr-anchorsandcheck:adr-symbol-anchorsexit 0, andnode scripts/check-platform-checklist.mjs --changed-from origin/mainexit 0.node scripts/check-single-authz-resolver.mjsexit 0 and--self-testexit 0.pnpm exec eslint --no-inline-configover the six changed.ts/.mjsfiles: exit 0, no output.Item-3 question, not converted: the storage family's ADR-0069 gate reader
buildAuthGateSessionReaderruns before every storage route (mountFamilyRoute). It re-composes the function's gate decision beside it: the key arm (resolveApiKeyAdmissionwith the door's posture, a held key answers its owner's posture) and, whileisAuthGateActive(), a session re-read whose failure isAuthzStoreUnavailableError('auth_gate'). That isdecideAuthGate's'reread-when-active'arm plusresolveAuthzContext's key arm, so it is a second copy of the gate decision. The one function cannot carry it as an existing parameter without changing answers: the probe asked the function, with storage's parameters and'reread-when-active', what the gate reader decides on the same 45 applicable wirings, and they differ on 14 (the function resolves the whole principal even with no gate active, so a tenancy, permission-store or posture outage becomes a 503 on every storage route; an active gate whose re-read fails is a 503 at the gate and a pass in the function, whose first read swallows the failure; the key arm differs too, detail withheld with #24366). So it was not converted. The seat's decision (review6252129939): it stays as it is on this card; the gap is #24366, where a parity pin or a gate-only mode is decided. The card's acceptance (git grep -cof the composition's distinctive lines reads 0 in the five files) therefore does not yet read 0 forstorage-service-plugin.ts:resolveApiKeyAdmission(andclassifyAdmissionTenancyPosture(remain there, in the gate reader.Acceptance notes
6252129939,e2891bfbc):@objectstack/core's module docblock table (request-execution-context.ts) named the storage family door underauthGate: 'reread-when-active'and only the dispatcher underaccessToken: 'carry', which this slice made false. Both rows now name the storage download door ('resolved','carry'), with one clause that the family's gate reader stays hand-written (security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling) #24366). Comment text only; the file is declared on [PM seat] domain:engine — ⏳ vacant #6367.mainand on this PR's head.Generated by Claude Code