Repository navigation
fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals,objectql)!: the audit ledger honours internal, and the credential-class census is declared internal - #21301
Conversation
…pprovals,objectql): honour the internal declaration at the ledger mirror and declare the credential-class census internal The compliance ledger's CRUD mirror omits fields declared internal from both snapshot sides, through objectql's existing flag-keyed collector, beside the type-keyed mask it already applied. The credential-class fields of the identity, share-link and approval-token objects are declared internal, and every consumer that reads one back off a row gets it through the engine's privileged accessor: plugin-auth's readback table (signing key, one-time verification value, second factor, SSO protocol blobs, OAuth client secret), the adapter's consume verb, the legacy SSO secret migration, and the share-link service's redemption, probe and list reads. WIP: pins, changesets and gates follow. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…sumer that reads a declared column back - plugin-audit: create, update and delete ledger rows about each census platform object record a non-credential control column and none of the object's internal columns, on both snapshot sides and in the activity row; an update of only an internal column still writes its row. - plugin-auth: JWT signing on the first, a later and a fresh-manager request with a key row minted before the declaration; password reset end to end with single-use enforcement. - plugin-sharing: password-protected redemption refuses without and with a wrong password and opens with the right one, through the service and the route; the creator's list and the resolve route keep the token. - plugin-approvals: the actionable-link lookup resolves on a real engine with the digest stripped from reads. - dogfood: the admin's ledger by-id and query doors serve none of an API key's or a share link's internal columns. - platform-objects: the one-time credential object's title reads no internal column; the English bundle follows its description. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…real engine (enrolment TOTP, single-use backup code) Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…tial client authenticates to introspection on a real engine) Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…ensus is declared internal, with the rotation notice Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…ontract does Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…egistered declaration, per row, failing closed only where the strip ran Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
- plugin-auth: the readback pin is an ordinary unit-tier file, not a nightly-tier one. - plugin-audit: alias the identity subpath to source, so the census declarations judged are the checkout's; the fake driver applies the caller's bound after the filter. - plugin-approvals: the generic read's options are typed, not erased. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 7 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 52 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 09db1fac87eb082beb83ecd6c565914f0f8062ca && git checkout 09db1fac87eb082beb83ecd6c565914f0f8062ca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 222ecc27f9050857d4620e7010d8b85f9d35e378 1704f949d93f62417872e3404349787b41b24390 && git checkout -B drift-repro 222ecc27f9050857d4620e7010d8b85f9d35e378 && git merge --no-ff 1704f949d93f62417872e3404349787b41b24390
node scripts/docs-audit/affected-docs.mjs --json 222ecc27f9050857d4620e7010d8b85f9d35e378
|
…ns through the path that proves them, not the stripped generic read - dogfood two-factor backup-code reveal: the "storage holds no plaintext" leg reads the stored column at driver level. The engine's read path omits an internal column for every caller, so its guard saw "nothing stored" for a column that holds a value. Both assertions are unchanged. - plugin-security private credential row scope: the two controls prove row reach by id, assert the generic read serves no credential column, and prove the value through the engine's privileged accessor, the path the auth adapter's re-attach uses. The by-identifier lookup still finds exactly the member's row. The row-scope assertions are unchanged. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21197 (body and all eleven comments, among them the ruling 5942811916, the re-claim 5942885323, the amendments 5943017963 and 5943028352, the os-dev-report 5944632767), the #21118 notes 5942902099 and 5943032006, PR #21301 (body, 28-file list, diff — byte-equal to the local ① Derived judgmentsLedger mirror ( objectql re-export — right, pure and additive. Two names added to Census declarations — right, none retyped to Readback — right where the diff reaches, wrong at one consumer it does not reach.
Lookup-only columns — right, with the caveat above. Titles, formulas, highlights — right. Pins and A1–A10 — right as claimed, on real engines. plugin-audit: real ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…an internal column, and the dispatcher's share-link probe reads the password hash through it - objectql: `readInternalColumn` (secret-fields.ts, exported from both entries) recovers one internal column for rows a consumer already holds. Stripped-versus-unset is read off the registered declaration the strip keys on, recovery goes through the privileged accessor, and it fails closed where the strip ran and the value cannot be recovered. Unit-pinned on a real engine. - plugin-sharing: its share-link helper delegates to it, with no second copy. - runtime: the share-link resolve probe recovers the password hash through it, so a protected link answers password-required rather than the unknown-link shape. Pinned on a real engine. - changeset: objectql minor, naming both new exports; runtime patch. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…ny field and refuses what it does not implement Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…d of erasing them Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Re-review of the fix round after FAIL 5944831998 (head ① Derived judgmentsThe missed consumer, The new helper
The two test fixes — neither weakens an assertion.
The dev's repo-wide classification of generic readers — not among the readable inputs; spot-checked instead. No comment on the card (twelve) or on the PR (two) carries that classification at the read time, and the PR body is at the previous head. Else in the delta. Commits 5c10c7a and 1704f94 are test-only (the fake driver refuses operators it does not implement; typed engine options instead of erasure). The changeset edit is ② below. The PR body was NOT refreshed for the fix round: it still declares ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #21197. This PR lands the ruled mechanism (C2) for the whole credential-class census, with its pins. It leaves one ruled execution parameter open: the upgrade-time invalidation of outstanding one-time credentials (see "What this PR does not do").
Clause-②: no (narrowing)
Executes ruling record 5942811916 (letter C2, Q2 A; the maintainer's 「同意」), re-claim 5942885323. Classes and positions only.
What changed
1. The ledger mirror honours
internal(plugin-audit,audit-writers.ts).ledgerViewnow omits every field the object declaresinternal: true, through objectql's existing flag-keyed collectorcollectInternalReadFields. It runs beside the type-keyed mask (collectMaskedReadFields) and after it, the engine's own order. No third predicate.new_value, both halves of an update diff, deleteold_value, the activity row's metadata, and the label source.internalfield still writes its row, with neither value.@objectstack/objectqlre-exportscollectInternalReadFieldsfrom its main and/coreentries. This is a pure re-export with no logic, and the lean-entry closure gate is green.2. The credential-class census is declared
internal. No field is retyped tosecret.platform-objects:plugin-sharing: the share link's token and password hash.plugin-approvals: the action-token digest.internal, and now honoured by the ledger: the API-key digest and the email headers column.internalfields, so a title built from an internal column would re-serve it.3. Every consumer that reads a declared column back gets it through the engine's privileged accessor (
resolveInternalField), never through the generic path.plugin-authinternal-field-readback.ts: readback rows for the signing key (JWT signing), the verification value (reset, OTP, magic link, OIDC code redemption), both second-factor columns, both SSO blobs, and the OAuth client secret.plugin-authobjectql-adapter.ts:consumeOnere-attaches the consumed row's internal columns, which password reset reads;findOne/findManyre-attach BEFORE the [security]sys_sso_provider.oidc_configcarries the OIDCclientSecretin a cleartext JSON textarea, by its own field description #8009 client-secret injection, which parses the now-internal OIDC blob.plugin-authsso-client-secret.ts: the legacy cleartext-secret migration reads the OIDC blob throughrecoverInternalFieldsForSystemRead.plugin-sharing:readShareLinkInternalColumn); the helper refuses rather than answer "unset" wherever the registered declaration says the strip ran;Lookup-only columns need no readback: the OAuth token columns, the SCIM digest and the approval-token digest are only ever filtered by.
Fix round (head
1704f949d9, after contract review FAIL 5944831998)Section added by the seat (
domain:engine#1), from the dev's fix-round report and the re-review PASS 5946347621. Claim amendment: 5946149793.packages/runtime/src/domains/share-links.ts, the share-link resolve probe, read the stored hash off a generic read. Once the column becameinternal, a password-protected link answered the unknown-link shape. It now recovers the hash throughobjectql's new helper, fail-closed: a recovery it cannot make throws and answers an error, never an open link. It is pinned on a real engine: unknown token 404; protected without a password 401NEEDS_PASSWORD; wrong password 401WRONG_PASSWORD; right password 200. Reverting the recovery turns the pin red.objectqlsecret-fields.tsgainsreadInternalColumn, exported from both entries. It decides stripped versus unset by the same collector the engine's strip uses, recovers throughresolveInternalField, and fails closed.plugin-sharing's helper now delegates to it, so there is no second copy of the rule.plugin-securityprivate-credential row-scope controls keep their row-scope assertions verbatim and prove the value through the privileged read. Filtering on the now-internallookup column still finds the row.cliintegration control that fails identically on cleanmainin the dev's container and passes in CI.1704f949d9: every required context green.internalcolumn acrosspackages/**andapps/**source was classified (151 hits): fixed, lookup-only, reads non-internal columns only, or not a reader. The re-review spot-checked it at the head.Part of #21197.What this PR does not do
sys_migrationreceipt and spares replay tombstones. Wiring it into the auth plugin's boot was refused in the authoring session by its permission policy, and it was not routed around. The seat holds the design. The changeset does not claim this invalidation.secret. The CLI dev-mode secret fallback is out of scope, as ruled.Pins
Each pin is described by what it asserts.
plugin-auditaudit-internal-field-omission.test.ts, over the real platform-object definitions (aliased to source): for every census platform object, the declaredinternalset equals the census. Ledger create, update and delete rows, and the activity rows, carry none of those fields on either snapshot side. A non-credential control column is recorded on every side it changed. An internal-only update still writes its row.plugin-authinternal-credential-readback.test.ts, on a real engine with SQLite and a real auth manager:plugin-sharingshare-link-internal-columns.test.ts, on a real engine:plugin-approvalsapproval-token-internal-hash.integration.test.ts: peek, redeem and replay resolve on a real engine with the digest stripped from reads.dogfoodaudit-log-internal-fields.dogfood.test.ts, on a real showcase boot with the audit plugin: the admin's ledger by-id and query doors serve none of an API key's or a password-protected share link's internal columns, and the control column is served.Reverse verification
Each ablation was run from the committed state through
scripts/ablation-replace.mjs, and each restore was proven: the blob equals HEAD andgit diff HEADis empty.dist/: plugin-audit rebuilt, andablation-dist-preflightconfirms the limb is absent. The dogfood pin goes red, 3 of 3. Restored, rebuilt, preflight confirms it present again, and the pin is green.consumeOnere-attach removed: reset answers 400.findOnereturned to the old order (inject before re-attach): three cases of the SSO at-rest suite go red.Verification, first round (head
03a77fb1b6, after merging main; the fix round's readings are under "Fix round" above)dispatch-gates --commandsderives 73 families. All 73 exit 0 on this head, and--ranreconciles 73 derived, 73 run, 0 not measured, with every exit code recorded.check:tenant-chokepointwas also run, exit 0.pnpm lint. ESLint over the new test files exits 0.Notes for review
minorfor the five packages whose served surface narrows (declared breaking,Clause-②: no (narrowing), ADR-0087 dispositionnot-required (no-migration-prescription), accepted bycheck:adr-0087-registration).minorfor@objectstack/objectql, which adds exports:collectInternalReadFieldsre-exported, and the newreadInternalColumnhelper with its parameter typeInternalColumnSource.patchfor@objectstack/runtime, for the share-link probe fix. (Corrected by the seat for the fix round; the first round shippedobjectqlaspatch.) Thefixedgroup moves every package together in any case.#21259(serial neighbour, the MySQL key-table timestamp write) landed on main before this branch's merge of main. No overlap.Acceptance notes
These are observations, not filed:
internalfield, and the read path evaluates formulas before the strip. The one in-tree producer, the one-time verification object's title, is fixed here. Carrier: none.password_protectedprojection for listed share links, and the list route has never served it. Carrier: none.internalcolumn stays allowed, as the flag's declaration states. Carrier: none.Generated by Claude Code