Repository navigation
spec: WebhookSchema marks secret and headers as credential fields; the redactor registry derives meta read-door redaction from the schema (ruling B+ on #24966) - #25036
Conversation
…ecret and headers (#24994) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…in both compositions (#24994) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…trumented lazy schema derives instead of throwing (#24994) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 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 7e2e81c7037d7ed348bd8bea9f2229eabbb474d7 && git checkout 7e2e81c7037d7ed348bd8bea9f2229eabbb474d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b3830ca223eaea7875babb155e47eebbcd0300f e86f32235fc03bc032d3b1aaf271e23feaad69b5 && git checkout -B drift-repro 3b3830ca223eaea7875babb155e47eebbcd0300f && git merge --no-ff e86f32235fc03bc032d3b1aaf271e23feaad69b5
node scripts/docs-audit/affected-docs.mjs --json 3b3830ca223eaea7875babb155e47eebbcd0300f
|
…hema-derived-credential-redactor
…ing in the launch window) Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #24994 (body, claim 6304417396, dev reports 6307031456 and 6307426041); #24966 and its ruling record 6304303975; PR #25036 body, file list, and the three-dot diff against Re-review of head ① Derived judgmentsThe marker (ruling item 1). The derivation, once, in the registry (ruling item 2, shape). Purity. Key question — removal versus substitution (card item 2 against item 5). The literal text: card item 2 "drops every marked field (not masked) and serves its cleared form (an empty string for a value, an empty map for a map), and names them in The ratchet (ruling item 3). Door pins. Both compositions boot (with and without plugin-webhooks, guarded by a composition pin); every door the card names is pinned (meta list in both spellings, item, layered view, published, stored-row data door and its history twin), plus the protocol's ② Semver levelChangeset at this head: Level, judged: the card and ruling 6304303975 wrote " ③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 49 runs on |
Closes #24994
Refs #24966
Clause-②: no (narrowing)
What the ruling binds (B+ on #24966, record 6304303975)
Quoted from the ruling record, items as recorded:
Disclosure: the maintainer's ruling on #9387 (
5323310017) applies. This PR carries location, rule and abstract reach only.What changed
packages/spec/src/automation/webhook.zod.ts):secretcarries.meta({ credential: 'value' })andheaderscarries.meta({ credential: 'map' }). Nothing else about the schema changes: no key is added, removed or re-typed. The marker key iscredentialand its vocabulary is closed:value(one credential, cleared form the empty string) andmap(a map whose entries cannot be told apart, so the whole map is the credential, cleared form the empty map).packages/spec/src/kernel/metadata-credential-fields.ts, new, not re-exported from thekernelbarrel): walks the type's declared object shape and each nested object shape below it (through optional, nullable, default, pipe and lazy wrappers), collects the marked fields, and builds a pure redactor. It is memoised per schema identity, so the walk runs once per schema, not per read.packages/spec/src/kernel/metadata-type-redaction.ts):getMetadataTypeRedactor(type)resolves explicit registration, then built-in, then the redactor derived fromgetMetadataTypeSchema(type).listMetadataTypeRedactorTypes()now includes the types whose schema derives one. The module note records the ruling and the precedence.packages/spec/src/kernel/metadata-credential-fields.test.ts): the ratchet, described below.packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts), in both compositions..changeset/24994-webhook-credential-marker.md:@objectstack/specminor, with a BREAKING banner and aspec!:headline. The declaration isClause-②: no (narrowing), and the ADR-0087 disposition isnot-required (no-migration-prescription). During the launch window a narrowing ships asminor, notpatch. That rule comes fromscripts/check-changeset-no-major.mjs(maintainer ruling 2026-09-04, batch [WIP] Add query enhancements and advanced validation features #35, on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294) and outranks the card'spatchworkload line. Everyno (narrowing)changeset on main is gradedminor, and this one now is too.The served form, read precisely
A marked field that holds credential material is removed from the served item and named in
redactedKeys. A stored cleared form (an empty-stringsecret, an emptyheadersmap) holds nothing, so it is served as written and not reported. This is how the governing precedents put it: theflowprojection's "Dropped, not masked", and the dev report's option A ("keys removed, not masked; serve the two cleared forms as written … so a reader tells 'cleared' from 'withheld'"). It is also what ruling item 5 of the card needs: the carry-forward keeps the stored values for a save that omits the two fields. If the cleared form were put in place of the withheld value, a save of the served body would be indistinguishable from a deliberate clear. A reviewer who reads card item 2 ("serves its cleared form") as "substitute the cleared form" should note that this reading was weighed and not taken, for that reason.The ratchet, and what "reads as a credential" means
listMetadataTypeSchemaTypes()pluslistUnregisteredKindSchemaTypes().webhookis a stack-collection kind bound inUNREGISTERED_KIND_SCHEMAS, not in the registered-kind list, so the first list alone would not read the one type this card is about.getMetadataTypeSchema('webhook')resolves through that map, and the derivation reads it from there.apithenkey; or when the first sentence of its description contains one of those words. Only fields that can hold a credential count: a string, a record, a union, an unknown. A number, boolean, enum or array cannot hold a secret, and without this limit token budgets would be flagged.datasource:external.credentialsRef. It is a reference into the secrets store, never the credential itself. A pin fails if an entry stops matching a field the rule flags, so the list cannot rot.mapmarker sits on a record; and no marker sits where the derivation cannot reach it (inside an array, a union or a record value). A probe test checks that the second pin can see such a marker.Unchanged (ruling item 5 of the card)
datasourceandflowkeep their explicit redactors, which take precedence. A unit test pins that an explicit registration wins over the derivation and that the datasource built-in still resolves.Door pins (both compositions: without plugin-webhooks, and with it mounted)
Each pin reads one door and asserts three things by rule: the read answers 200, it names the webhook (a positive control, so a refusal or an empty answer cannot pass), and it holds neither declared credential field. The doors:
Meta doors are read as an administrator and as a signed-in member. The data door is read as an administrator, the only caller who can reach it. The fixture's credentials are obviously-fake placeholders.
Verification (head
0cf6eed565)@objectstack/spec:pnpm --filter @objectstack/spec testgreen (run at441ce0128d; the later commit touches only the walker's definition read and adds one test). At0cf6eed565: the two touched filessrc/kernel/metadata-credential-fields.test.tsandsrc/kernel/metadata-type-redaction.test.tsgive 2 files and 39 tests passed, andtypecheckpassed. Afterspec build,check:generatedreports all 14 generated artifacts up to date, so no artifact regenerated. No public export was added: the new module is not re-exported from thekernelbarrel, andcheck:api-surfaceis green.@objectstack/rest:testgreen at0cf6eed565(307 files; 6157 passed, 326 skipped), andtypecheckpassed. An earlier head failedrest-exposure-gate-execution-read.test.ts, and the cause was in this PR. That test wraps the registeredobjectschema in a second Proxy. A_zodread through a Proxy over alazySchemaProxy throws a Proxy invariant error, so the walk read that way turned the instrumented type's meta reads into 500s. The walker now reads the public.def, and a unit test pins derivation through a doubly proxied schema.@objectstack/metadata-protocoland@objectstack/plugin-webhooks:testandtypecheckgreen (run at441ce0128d).@objectstack/dogfoodtypecheckis green.0cf6eed565: the new pin file pluswebhook-materializationandwall-declared-webhook-deployment-widegive 3 files and 24 tests passed.credentialmarkers onWebhookSchemawere rewritten to a non-marker key throughscripts/ablation-replace.mjs: anchor 1 to 0 for each, on-disk counts 0 and 0.@objectstack/specwas rebuilt, andscripts/ablation-dist-preflight.mjs --absentconfirmed both markers absent from all 232 built files. The pin file then read 12 failed and 3 passed. The 3 that stayed green are the two composition guards and the dispatcher-source pin, which do not depend on redaction. Restore: blob equal to HEAD andgit diff HEADempty, spec rebuilt, the preflight confirmed both markers present in 18 built files, and the pin file read 15 of 15 passed. A first attempt was refused by the replace tool before anything ran, because its replacement-count check rejected the anchor shape; nothing was mutated, and that attempt counts for nothing.scripts/pm/dispatch-gates.mjs --commands, 87 families): all 87 run, with the spec and changeset subset run again at0cf6eed565, and all exit 0 except one.check:type-check-debtwas run under the lock at0cf6eed565: the re-measure is OK, and no ledger entry is above its recorded number. The exception:check:dual-build-cjs-loadsis NOT MEASURED, because it needs every workspace package built and only the dogfood closure was built. That one is declared to CI.Fix round (head
e86f32235f)This round merged
origin/mainand re-graded the changeset tominor, adding the BREAKING banner. The changeset gates all exit 0:check-changeset-no-major(--base origin/mainand--self-test),check-adr-0087-registration(both),check-empty-changeset(both), andcheck:changeset-gate-self-tests. After them,pnpm --filter @objectstack/spec testpassed 703 files and 21388 tests (1 skipped, 1 todo), andtypecheckpassed. Both ran under the lock, with VERDICT command-exit 0. Thedispatch-gates --commandsderivation is unchanged from the previous head (same 87 families). The gates this round's delta touches also exit 0:check:nul-bytes,check:issue-citations,check:doc-authoring,check:pm-changeset-deadline-census, closing-keyword parity,check:objectui-changesetandcheck:closed-ceremony-text.Acceptance notes
.meta({ credential })is carried into the generated JSON Schema of a webhook (zod copies metadata into the JSON Schema node), so/meta/typesand the IDE schema now showcredential: 'value' | 'map'on the two properties. This is an annotation only: no gate artifact changed (check:generatedreports all 14 up to date).webhookmetadata type has no redactor, so its secret-bearing fields are not masked on the meta read doors (detail withheld under the disclosure ruling) #24966 measured that the MCP HTTP tools refuse the stored metadata objects, and that the stdio data bridge applies the same stored-row projection this registry drives. No MCP pin is added here.Generated by Claude Code