refactor(core,plugin-audit,service-storage,plugin-reports): give the __ operation-private-key convention a single owner (#7284) - #7352
Conversation
…`__` operation-private-key convention a single owner (#7284) `withoutOperationPrivateKeys` and its `OPERATION_PRIVATE_KEY_PREFIX` had been hand-copied into three packages — plugin-audit's comment access hooks (#7141), service-storage's attachment access hooks (#7145) and plugin-reports' report service (#7204). All three were byte-equivalent in behaviour; their doc blocks had already diverged in prose. The rule now lives once in `@objectstack/core` (`security/operation-private-keys.ts`), beside `assemble-execution-context.ts` — that file owns where an ExecutionContext is BUILT at a transport entry point, this one owns where it is stripped before being forwarded to a question it was not resolved for. Home chosen by dependency measurement: all three consumers already depend on `@objectstack/core`, none depends on `plugin-security` (the producer, and the most honest owner, but a string-prefix filter does not justify three new dependency edges onto a plugin), and `@objectstack/spec` is fenced by Prime Directive #2. Core is the only zero-new-edge candidate. The reasoning moved with the code rather than being thinned; each consumer keeps only its own local half and points at the shared home. Pins: `operation-private-keys.test.ts` asserts the rule's own behaviour, which no package-level test had asserted directly, and `operation-private-keys.pin.test.ts` turns red if a fourth file declares its own copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017AD2nx7MRuje3kLqoLBHPM
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 23 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also reference the affected code. These are read-only:
|
Gate runs (local, on the pushed head)Gates enumerated fresh off Changeset family — the decision in the PR body, checked against the gates that judge it:
Export-surface family — the §4 question, answered: they required nothing of this PR. Every workflow invocation of these four is
Security- and packaging-adjacent gates that could plausibly read a new file under
Build / lint / tests
Anti-copy pin, reverse-verified. Planting a fourth copy in Generated by Claude Code |
Closes #7284.
withoutOperationPrivateKeys— the rule that a consumer forwarding a caller's execution envelope to a question about a different object must first drop the__-prefixed keys plugin-security stamped for the operation in flight — existed in three unexported, hand-copied copies. This gives it one owner.The fact, re-verified on this base
Confirmed at
f3f855a(triage measured at62b6a2f), located by content rather than by line number:packages/plugins/plugin-audit/src/comment-access-hooks.tspackages/services/service-storage/src/attachment-access-hooks.tspackages/plugins/plugin-reports/src/report-service.tsSweep for a fourth: none.
withoutOperationPrivateKeys/OPERATION_PRIVATE_KEY_PREFIXacross all.ts/.tsx/.mdoutsidenode_modulesreturns exactly those three files. A second sweep for hand-rolled near-misses under other names (startsWith('__'),__readScope/__writeScopehandling) found only the producer side —plugin-security,plugin-sharing— which stamps and reads these keys rather than stripping them, and is unaffected.Where the single owner lives, and why
Measured from the three consumers'
package.jsondependency blocks:@objectstack/core@objectstack/specplugin-security(the producer)Inside core the home is
src/security/operation-private-keys.ts, deliberately besideassemble-execution-context.ts. Those two own opposite ends of one lifecycle: #6216 made that file the single place anExecutionContextis built at a transport entry point; this is the single place one is stripped back down before being forwarded. Exported fromsrc/security/index.ts, whichsrc/index.tsalready re-exports wholesale.⛔ Nothing lands in
packages/objectql/src/engine.ts(#7095),packages/core/src/qa/runner.ts(#7256), orpackages/spec/**/packages/metadatatest files (#7223).packages/specis untouched — no cross-seat declaration needed.The reasoning moved with the code
Not three thinner copies. The shared home carries the full argument, generalised out of the three divergent prose versions: what a
__key is and which ones exist today (the ADR-0057 D1 depths, the ADR-0090 D10 delegator halves, the engine's__expandRead/__referentialFieldClearprivilege markers); why a consumer must drop them (one object's widening applied to another object's question — including the concrete survival path, that plugin-security only overwrites__readScopewhen it actually resolves permission sets for the new object); why dropping is safe in the one direction that matters; why by prefix and never by a name list, plus the ⛔ corollary for whoever next changes the middleware; and why the fresh copy is load-bearing in both directions.Each consumer keeps only its genuinely local half — which object its gates ask about (
sys_comment,sys_attachment,report.object_name) — and points at the shared home. The[#7141]/[#7145]/[#7204]provenance markers and the surrounding#6206/#6523envelope-forwarding arguments at the call sites are untouched.Pins
operation-private-keys.test.ts— the rule's own behaviour, which no package-level test had ever asserted directly (each package tested only its own gates' effect): prefix-dropping, that it strips rather than projects (every principal field survives — the defect half of the five-field projections these call sites replaced), the fresh copy even on a clean envelope, the deliberate shallow-copy boundary, and that a key the middleware has not stamped yet is dropped by prefix alone.operation-private-keys.pin.test.ts— the anti-fourth-copy pin. Scans every.ts/.tsxunderpackages/for a declaration (not a mention) of either symbol outside the home, and fails with a message naming the import to use. Two anti-vacuity guards travel with it (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690's "a check that can't see its input has verified nothing"): the home must really declare both symbols, and the walker must demonstrably reach all three former-copy files.Scope is deliberately narrow — it detects redeclaration, not "a consumer that should have used the helper and didn't". The latter is the more interesting question and is not decidable by scanning; a regex ambitious enough would be a false-red generator, and an inert or noisy gate reads as a gate that is watching. Redeclaration is exactly how all three copies got here.
Reverse-verified: planting a fourth copy in
plugin-auditturns it red naming that file; removing it turns it green.Changeset
Real patch bumps for the four packages, not an empty changeset and not
skip-changeset, read offcheck-empty-changeset.mjs's own ledger. That gate's text is one-directional: an empty changeset produces no CHANGELOG entry (measured, #5471), buys nothing the label doesn't, and uniquely is a real input tochangesets/actionthat can trigger #4898 — the failure mode that silently stalled 17.0.0-rc.2. This PR changes published source in four packages, so a real bump is both honest and the route that produces an actual CHANGELOG entry.Nothing breaking, so no ADR-0087 conversion.
Gates
Enumerated fresh off
origin/main— 64check:*inlint.yml, 70 across all workflows.The export-surface family (
check:export-origins,check:api-surface,check:exported-any,check:dual-source-exports) is--filter @objectstack/specin every workflow invocation — it reads spec's surface, not core's. Adding an export to@objectstack/coretherefore required nothing of them. Run results in a follow-up comment.Verification
@objectstack/core— 738 tests green, including the 11 new onesplugin-audit160 ·service-storage352 ·plugin-reports68 — all green, unchangedGenerated by Claude Code