Skip to content

Commit f47f7f2

Browse files
refactor(plugin-security): move SecurityPlugin's leaf readers into security-plugin-object-inputs.ts (#25223 cut 1) (#25290)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMYdF2uchcinMphEKW4sYxd)_ Part of #25223 Clause-②: no Metadata-protocol: none Ruling-ref: 6348891160 Cut 1 of the `security-plugin.ts` split. The bodies of the 16 leaf readers of `SecurityPlugin` move, unedited, to the new module `packages/plugins/plugin-security/src/security-plugin-object-inputs.ts`. These are the per-object inputs every gate layer reads: the row-level security policy collection with its ADR-0105 D3 admission and the authored-tenant-policy warning, the unresolved-posture probes, the field-level masks and required-permission folds, and the field-name tables. The one module-level declaration only those bodies read, `isDelegatedPrincipalLeg`, moves with them, and so do the four imports only they used. It is move-only, and the card stays open for the later cuts. The seam plan for the series is in the claim comment on #25223 (6349703046); each later cut is re-measured on the merged file first. Branch: `claude/project-thread-3fzya4`, the harness-pinned branch for this session; it names no issue, so the claim comment is the identity record. The PR also carries the one-sentence rider this card owes: a docblock note in `controlled-by-parent-unwritable-master-write.test.ts` saying why the raw registration door alone pins the resolution (ruling 6345869077 item 6, owed by the services seat's ACCEPT 6348830068 on PR #25256). Comment only. ## The published `.d.ts` does not change Under the series bar (ruling 6348891160 item 1), measured on base `72fad922` and head `a7cd7c62`: - Raw sha256 of `dist/index.d.ts` and `dist/index.d.mts`, base and head, all four the same: `61c36a53c30dfccdd4a62280b922bfd1328c83e02253266c469f809c0a78679c`. The raw diff is 0 lines, so no union order moved either. - Normalised sha256, all four the same: `942a52dc2afc950fdc2f89a720b08d4a8c584250f940db05d099c98f813c2359`. - The normaliser, run from `packages/plugins/plugin-security` (it sorts the members of each string-literal union and changes nothing else): ```sh node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum ``` - Positive controls on a copy of the head `dist/index.d.ts`, line 2410 (`private collectRLSPolicies;`): - Made public (`collectRLSPolicies;`): raw `05654fc4ea1aa556ce256aa3bee1f94c6ec9b5ae1a240c7744e6fe7225d8cd43`, normalised `9280d1c333e070fad6635d3adbb9dad308a9ad49a3e3d30cdf90002947f3f738`. - Renamed (`private collectRLSPoliciesRenamed;`): raw `f20c387525fecc3b2c65bd6113a3c83cfe0388d4defabfc0a5a0fb2149c449db`, normalised `312949d507e1943df1f357c43a6a85d97fe8205aefea057ded1bf17866943ef7`. - Both differ from the real hashes, so the comparison does see a visibility change and a rename. - Export tables: the ESM and CJS bundles each export the same 63 names at base and head, loaded at runtime rather than parsed. The sorted name list hashes `f607fd765d6b277e08b3b46ccb16d1e2d133ec55658ffe40155aef25e1bf54f0` on all four. - The bundle JS bytes differ (`index.js` `aba80183` to `b52cd4f1`, `index.mjs` `aca41ade` to `cd55afa9`), because code moved within them. No changeset: `skip-changeset`, per ruling 6348891160 item 4. ## What moved Base line numbers are each method's signature at `72fad922`. The last column is the members the body reads, which is exactly what its host carries. | method | base lines | moved function async | host members | |---|---|---|---| | `collectRLSPolicies` | `:10366`-`:10400` | no | `admitCollectedPolicy`, `ownershipFloorAlternates`, `ql`, `rlsCompiler` | | `admitCollectedPolicy` | `:10419`-`:10427` | no | `orgScopingEnabled`, `warnAuthoredTenantPolicyOnce` | | `warnAuthoredTenantPolicyOnce` | `:10437`-`:10450` | no | `logger`, `warnedAuthoredTenantPolicies` | | `resolveUnresolvedCause` | `:10579`-`:10582` | yes | `probeMetadataOutage`, `probeUnpublishedDraft` | | `probeMetadataOutage` | `:10628`-`:10639` | yes | `metadata` | | `probeUnpublishedDraft` | `:10673`-`:10684` | yes | `ql` | | `computePartialMaskRules` | `:10708`-`:10727` | no | `permissionEvaluator` | | `computeQueryGuardFieldPerms` | `:10753`-`:10773` | no | `computePartialMaskRules`, `foldFieldRequiredPermissions`, `permissionEvaluator` | | `computeForbiddenFieldWrites` | `:10788`-`:10804` | no | `fieldMasker`, `foldFieldRequiredPermissions`, `permissionEvaluator` | | `computeReadPartialMaskRules` | `:10826`-`:10840` | no | `computePartialMaskRules` | | `maskOperationResult` | `:10856`-`:10885` | no | `computeReadPartialMaskRules`, `fieldMasker`, `foldFieldRequiredPermissions`, `permissionEvaluator` | | `foldFieldRequiredPermissions` | `:10887`-`:10902` | no | `permissionEvaluator` | | `getObjectFieldNames` | `:10908`-`:10924` | yes | `fieldNamesCache`, `loadObjectFieldNames` | | `writeCheckFieldOptions` | `:10943`-`:10952` | yes | `metadata`, `ql` | | `loadObjectFieldNames` | `:10954`-`:11015` | yes | `datetimeFieldNamesCache`, `numberComparandFieldsCache`, `tenancyDisabledCache` | | `extractTargetField` | `:11035`-`:11053` | no | none (called directly) | Plus `isDelegatedPrincipalLeg` (base `:471`-`:487`, with its docblock), read only by `collectRLSPolicies`. Left in the class on purpose: `getObjectSecurityMeta`, which sits among these methods, because `packages/spec/liveness/object.json` anchors it next to `normalizeRequiredPermissions` and that file belongs to another lane. ## The shape (#23050 shape B, as #25222 and #25224 apply it) - The class keeps each private method with its name, signature, docblock and position, because tsup emits private members with their JSDoc into the `.d.ts`. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25223 cut 1]`. - `ObjectInputsHost` types every member as the class member itself (`SecurityPlugin['m']`, an indexed access that works on private members). A renamed or retyped member is a compile error. There is no cast, and no member becomes public or protected. The module imports `SecurityPlugin` type-only, so there is no runtime import cycle. - Each moved function types its `this` parameter as the `Pick` of `ObjectInputsHost` over exactly the members its own body reads, and each stub annotates its host literal with the same `Pick`. A missing or extra member is a compile error, and the closures get their parameter types from it. - Methods go in as late-bound closures that call `this.m(...a)` on the plugin at call time. A spy or test double set on the plugin is still the one the body reaches, and the moved bodies that call one another do so through the plugin, as before. Every `this.m(` call in the module goes to one of the eight methods a host carries this way; no moved body calls a function-valued field, so no receiver changes. - Each field was classified on its own declaration, one rule for all. A field declared `readonly` (the four caches, `ownershipFloorAlternates`, `warnedAuthoredTenantPolicies`) goes by value, because it is never reassigned and the Map or Set is the same object. Every other field (`ql`, `metadata`, `logger`, `rlsCompiler`, `permissionEvaluator`, `fieldMasker`) and the `orgScopingEnabled` accessor goes as a getter, so each read happens when the body makes it, after any `await`, as before. `start()` assigns `ql`, `metadata` and `logger` after construction, and tests replace `permissionEvaluator` on a constructed plugin. - The host declares every field `readonly`, so a moved body cannot assign a member through it; the typecheck passes, so none does. - Six methods lose `async`: `resolveUnresolvedCause`, `probeMetadataOutage`, `probeUnpublishedDraft`, `getObjectFieldNames`, `writeCheckFieldOptions` and `loadObjectFieldNames`. Each returns the moved async function's own promise. The stub cannot throw before it returns that promise: building the host reads only `readonly` fields and defines getters without running them, and calling an async function never throws synchronously. So a caller awaits a promise that settles exactly as the old method's did. - `extractTargetField` reads no member, so its method calls it directly. - The moved bodies keep saying `this.`, so the moved lines are unedited. Nothing re-exports the module, so the package entry does not move. ## Proof of move-only - **Bodies, by AST token**: for all 16 methods, the moved function's body has the same TypeScript token sequence as the base method body, and each of its 234 lines is the base line with two leading spaces removed. Parameters, return types and `async` match. A negative control, one token changed in one body, fails the check. - **Class side**: each method keeps its docblock and signature, with `async` dropped on the six above. `isDelegatedPrincipalLeg` is verbatim (17 lines) in the module and absent from `security-plugin.ts`. - **git's own view** (`git diff --color-moved=plain --color-moved-ws=allow-indentation-change 72fad92 HEAD` over the two source files): 255 of 262 deleted lines and 318 of 621 added lines are detected as moved. - Deleted, not moved (7): the six `private async` signature lines, which lost `async`, and the `withOwnershipFloorAlternates,` name taken out of a multi-name import. - Added, not moved: the stub bodies, the import of the new module with its comment, and in the module the license header, the module docblock, the imports, `ObjectSecurityMeta` (a type alias over `getObjectSecurityMeta`'s return type, so `security-plugin.ts` needs no new export), `ObjectInputsHost`, one pointer docblock per function and the rewritten signature heads. The rest are lines git does not pair with a moved block, such as blanks and closing braces. - **Size**: 890 changed lines (+628 / -262, 3 files), under the 3000-line human-merge threshold. `security-plugin.ts` goes from 11,054 to 10,929 lines; it shrinks only by the bodies because every docblock stays for the `.d.ts`. ## Readers No test, script, gate, baseline or docs anchor reads the moved lines by path, so nothing is re-pointed. - The symbol-anchor gates still resolve every anchor into this file: `pnpm check:adr-anchors`, `check-adr-symbol-anchors` (2383 anchors), `check-scripts-symbol-anchors`, `check-spec-docblock-symbol-anchors`, `pnpm --filter @objectstack/spec check:liveness`, and `check-platform-checklist --changed-from origin/main` (6 anchors judged, 0 lost). All exit 0. The ADR anchor for this file names `resolveCbpRelation`, `getMetadataReadableFields`, `claimSeedOwnershipOnSettle` and `readSeededRecordIds`, none of which moved. - Line citations: no `security-plugin.ts:N` citation in the repo points into a moved range (base `:471`-`:487` and `:10366`-`:11053`). Base lines `:56`-`:470` move by between -1 and +19 (the import block changed), lines from `:10373` on move up, and lines `:489`-`:10372` keep their numbers. Three citations land in the shifted head range: `CHANGELOG.md` (`:303`, release history), `docs/launch-readiness.md` (`:309`) and `packages/spec/liveness/object.json` (`:132`, inside a note that records it as an already-repointed stale cite). At base all three already point at unrelated lines, so this cut does not make any of them wrong. - Census gates run on base and head, compared. These have identical output: `check-platform-object-tenancy-census`, `check-position-name-fold-loaders`, `check-registry-log-declared`, `check-rest-log-spy-declared`, `check-system-context-census`, `check-tenant-audit-census`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:driver-memory-census`, `check:issue-citations` and `check:query-options-erasure`. Only the scanned counts move, by the one new file: `check-undeclared-dep-imports` (3284 to 3285 files, 2817 to 2820 specifiers), `check:logger-receiver-detach` (3600 to 3601 files) and `check:org-identifier` (3626 to 3627 files). ## Acceptance notes - Nothing outside this cut is touched: no other section of the class, no behaviour, no other lane's file. - The test file gains comment lines only. ## Gates All at head `a7cd7c62`, on base `72fad922`. Between that base and the current `origin/main` (`1eb1afae`), no file under `packages/plugins/plugin-security/` changed. - `pnpm --filter @objectstack/plugin-security test` (`vitest run --maxWorkers=3`, under the verify lock): Test Files 210 passed (210), Tests 4379 passed, 45 skipped (4424). Base `72fad922`, in a separate worktree: the same counts. VERDICT command-exit 0 on both. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0 (`tsc --noEmit`, the scripts tsconfig, and `check:test-typecheck` with 0 files / 0 errors in the debt ledger). - `eslint --no-inline-config` over the three touched files: exit 0. - `pnpm --filter @objectstack/plugin-security build`: exit 0, which produced the `.d.ts` and bundles measured above. - `dispatch-gates --commands`: 58 commands, 890 changed lines, under the threshold. `--ran` reconciliation: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 58 recorded an exit code and none is 3). Per command (exit code): - `node scripts/check-ci-filter-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` exit 0 - `node scripts/check-comment-mask-adoption.mjs` exit 0 - `node scripts/check-comment-mask-adoption.mjs --self-test` exit 0 - `node scripts/check-comment-mask-corpus.mjs` exit 0 - `node scripts/check-dts-emitted.mjs --self-test` exit 0 - `node scripts/check-issue-citations.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs --self-test` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs --self-test` exit 0 - `node scripts/check-plugin-teardown-shape.mjs` exit 0 - `node scripts/check-plugin-teardown-shape.mjs --self-test` exit 0 (the first run exited 3: this shallow clone lacked the pinned fixture commit; it passed after `git fetch --depth=1` of that commit) - `node scripts/check-position-name-fold-loaders.mjs` exit 0 - `node scripts/check-position-name-fold-loaders.mjs --self-test` exit 0 - `node scripts/check-registry-log-declared.mjs` exit 0 - `node scripts/check-registry-log-declared.mjs --self-test` exit 0 - `node scripts/check-rest-log-spy-declared.mjs` exit 0 - `node scripts/check-rest-log-spy-declared.mjs --self-test` exit 0 - `node scripts/check-system-context-census.mjs` exit 0 - `node scripts/check-system-context-census.mjs --self-test` exit 0 - `node scripts/check-tenant-audit-census.mjs` exit 0 - `node scripts/check-tenant-audit-census.mjs --self-test` exit 0 - `node scripts/check-undeclared-dep-imports.mjs` exit 0 - `node scripts/check-undeclared-dep-imports.mjs --self-test` exit 0 - `node scripts/docs-audit/check-affected-docs.mjs` exit 0 - `node scripts/docs-audit/check-drift-comment.mjs` exit 0 - `pnpm --filter @objectstack/spec run check:duration-unit-keys` exit 0 - `pnpm check:closed-ceremony-text` exit 0 - `pnpm check:cross-package-test-inputs` exit 0 - `pnpm check:doc-authoring` exit 0 - `pnpm check:driver-memory-census` exit 0 - `pnpm check:dts-closure` exit 0 - `pnpm check:dual-build-cjs-loads` exit 0 (the first run exited 3, PREREQUISITE NOT MET, before a full build; after a full `pnpm exec turbo run build` under the verify lock (77 of 77 tasks), it passed: 112 entry points across 69 packages load) - `pnpm check:engine-double-contract` exit 0 - `pnpm check:error-status-conformance` exit 0 - `pnpm check:gitlink-declared` exit 0 - `pnpm check:i18n` exit 0 (the same: exit 3 before the full build, exit 0 after it) - `pnpm check:i18n-stale-fill` exit 0 - `pnpm check:issue-citations` exit 0 - `pnpm check:lean-entry-closure` exit 0 - `pnpm check:logger-receiver-detach` exit 0 - `pnpm check:nul-bytes` exit 0 - `pnpm check:objectql-double-limit` exit 0 - `pnpm check:org-identifier` exit 0 - `pnpm check:page-declaration-shape` exit 0 - `pnpm check:published-files` exit 0 - `pnpm check:query-options-erasure` exit 0 - `pnpm check:refd-timer-probe` exit 0 - `pnpm check:slot-lookup` exit 0 - `pnpm check:sourcemap-no-sources-content` exit 0 - `pnpm check:test-source-alias` exit 0 - `pnpm check:tier-file-adoption` exit 0 - `pnpm check:type-check-coverage` exit 0 - `pnpm check:type-check-debt` exit 0 - `pnpm check:watch-hint-literal` exit 0 - `pnpm check:where-matcher` exit 0 --- _Generated by [Claude Code](https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b62f959 commit f47f7f2

3 files changed

Lines changed: 628 additions & 262 deletions

File tree

‎packages/plugins/plugin-security/src/controlled-by-parent-unwritable-master-write.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
* - every insert, single or batch, whether or not its body names a master;
1515
* - every update whose body carries the reference key, a master or `null`.
1616
*
17+
* Why the raw door alone pins it (ruling 6345869077 item 6): the three doors
18+
* that skip the parse — `defineStack` with `strict: false`, plugin code's raw
19+
* `registerApp` / `registerObject`, and an 18.0.0-era `sys_metadata` row the
20+
* boot registers under `[metadata_spec_invalid]` — all end in the one
21+
* `SchemaRegistry` entry the gate's resolver reads through `ql.getSchema()`,
22+
* so a declaration from any of them reaches the gate in the same form.
23+
*
1724
* One rule for both flags, for a member and for a platform admin alike, on an
1825
* ordinary object and on a `sys_`-named one (which the engine treats
1926
* differently on insert — the rule does not). What stays as it was:

0 commit comments

Comments
 (0)