Repository navigation
fix(service-messaging,objectql): stop serving sys_http_delivery.headers_json on the generic read path — internal: true + a batch privileged accessor (#8118) - #8348
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
…eaders_json as a readable column (#8118) The page's own rule — 'reading a delivery row tells you what was sent, not how to forge it' — was false while headers_json served Authorization headers verbatim over the data API, and this PR is what makes it true. Mark the row internal in the field table and state that the guarantee now covers the authored headers, recovered only by the dispatcher's privileged read at claim time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
…n description (#8118) The internal:true marking on sys_http_delivery.headers_json added a field description; an object-definition change regenerates the package's i18n bundles, and the four service-messaging bundles were not regenerated with it (check:i18n DRIFTED (4)). Merge-mode regeneration — the new help key arrives filled with source text in all four locales, no existing translation overwritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
…bjectstack-ai#8363) * fix(pm): derive check:i18n by owning-package walk in dispatch-gates `check:i18n` was invisible to the derivation in BOTH halves of the output. The path half cannot reach it: check-i18n-bundles.mjs discovers its targets at runtime by walking packages/ for files named i18n-extract.config.ts, so its source names no population path to match. Worse than silent — it does carry eleven path-ish literals (CLI prerequisite, stale-dist checks), so it also never lands in the "repo-wide / undetermined" bucket. The convention half did not know the kind. Measured on PR objectstack-ai#8348: an object-definition edit under packages/services/service-messaging/src/objects/ regenerates that package's four bundles, and the derivation named the gate nowhere at all. Adds a second CHANGE_KIND_GATES entry that repeats the gate's own walk (same skip set, same filename-plus-/scripts/ test) and matches any input path inside an owning package. The whole package counts, config file included — narrowing to src/objects/** would under-cover, since extraction reads whatever each package's config enumerates. Lists nothing: the KIND is written down, never its population, so a tenth package growing a bundle is matched by the next run. Owner population verified at parity with the gate's own nine. Self-test 28 -> 47 cases, pinning both directions and the new entry's STALE branch. Fixes objectstack-ai#8352 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01139NJ9Wg5pFeZi1Zh8WLg6 * test(pm): anchor the i18n gate-name pins on exact rendered delimiters Found by reverse-verifying the entry above: renaming its gate to `check:i18n-renamed-probe` made the live run print the STALE line exactly as designed, and the self-test stayed green at 47/47 — `includes('pnpm check:i18n')` is satisfied by every name that merely STARTS with it, so a prefix-preserving rename is invisible to a substring pin. That is precisely the rot the STALE branch exists to report. Anchors both pins on the rendered delimiters (`- pnpm x —`, `⚠ x: STALE`) so the name must match exactly. Re-verified: the same rename now fails 2 cases instead of 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01139NJ9Wg5pFeZi1Zh8WLg6 --------- Co-authored-by: Claude <noreply@anthropic.com>
…on on the generic read path — internal: true + a privileged readback seam (objectstack-ai#8705) * fix(platform-objects,plugin-email): stop serving sys_email.headers_json on the generic read path internal: true on the column plus a privileged readback seam on the delivery paths, adopting the remedy objectstack-ai#8118 ruled and PR objectstack-ai#8348 landed. Refs objectstack-ai#8149 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH * test(plugin-email): pin the redaction/wire pair on a stub driver; add changeset + i18n Drops the driver-memory devDependency in favour of the stub-driver pattern objectql/src/internal-fields.test.ts already pins the flag against, so the package adds no new workspace dep, no vitest alias config and no entry to the shrink-only unaliased-import ledger. The at-rest pin now scans raw storage instead of reading back through the privileged accessor. Refs objectstack-ai#8149 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX1qcBzfwZb5wkRrJTNbhH --------- Co-authored-by: Claude <noreply@anthropic.com>
…ts that decided them (objectstack-ai#20757) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the eleventh stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/plugins/plugin-email/src/**` and nothing else. By the seat's census at the claim (`5902547086`), it is the largest package in the lane that no in-flight work holds. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 10 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as `9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR objectstack-ai#20737 as `4dfff176b`, PR objectstack-ai#20742 as `697845d19`). That is **16 sites on 16 lines in 8 files, covering 4 numbers**: - 7 census sites (every census site this package has); - 9 sites in test comments, which the census defers. Three of them carry `objectstack-ai#13190`, a dead number that stands only in test files here, so the census never judged it; it was read on its own (404); - no site the gate's grammar cannot see (the package has none that is dead, see Acceptance notes). Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what was decided: **4 distinct shas**. No number in this package has an ADR or ruling record of its own (a grep of `docs/adr/` and `scripts/adr-anchors/` finds only ADR-0131 naming `objectstack-ai#11741`, as evidence in its D7, not as the record of that decision; nothing else under `docs/` names the four), so every anchor is a commit, per ruling C's order. No number was dropped. Only comments changed. Every touched source file keeps its line count (16 lines out, 16 in, over 8 files), so no line citation into these files moves. Every one of the 16 changed lines carried a dead citation; there is no reflow line. No code token moves (see the guard below). **No citation number is added.** The added lines carry no tracker number at all. Over the whole diff, added minus removed is negative for the four dead numbers and zero for every other number, and no number is new to the diff. No PR number is the citation on an added line: the two `PR objectstack-ai#8675` spellings became that pull request's squash commit. 10 dead sites are left on purpose, all of them `describe` / `it` titles (see the list below). One more file: a `patch` changeset for `@objectstack/plugin-email`, because the rewritten prose ships (see Changeset below). ## Census: `plugin-email`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/plugins/plugin-email/`. Each run counts as a reading only because its board frontier equals the newest issue or pull-request number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | plugin-email sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `97005aed0`, run 2026-09-30T02:00:45Z to 02:04:02Z | enumerated, 186 pages, frontier objectstack-ai#20748 (newest objectstack-ai#20747 before, objectstack-ai#20748 after: a pull request opened at 02:03:20Z, inside the run) | 1,064 | **7** | 7 | 4 | 3 | | after | head `15a7d69a7`, run 02:11:19Z to 02:14:30Z | enumerated, 186 pages, frontier objectstack-ai#20753 (newest objectstack-ai#20753 before and after) | 1,057 | **0** | 0 | 0 | 0 | The before count matches the seat's census and A1 (7 sites: `objectstack-ai#13189` ×4, `objectstack-ai#11741` ×2, `objectstack-ai#8675` ×1). The before run's board moved during the run; its frontier equals the newest number at the run's end, which is A1's criterion (stage 7's precedent). The whole-repo drop is 7, exactly this diff's census sites. The `resolves` tally is 33,029 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. The after run was taken on `15a7d69a7`; the head `23283d394` adds only the changeset. No run was truncated or discarded: both enumerations read 186 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `plugin-email/src` (50 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when that census judged it on this board anywhere (its `--list` extraction, 37,072 rows) and did not report it. The eleven numbers the census never saw, because they stand only in test files or as the second half of a slash pair here, were read one by one on the issues endpoint: `objectstack-ai#13190` answers 404; `objectstack-ai#5169`, `objectstack-ai#5286`, `objectstack-ai#10619`, `objectstack-ai#16506`, `objectstack-ai#20374`, `objectstack-ai#5197` answer 200 as issues, and `objectstack-ai#8348`, `objectstack-ai#5191`, `objectstack-ai#5211`, `objectstack-ai#5232` as pull requests. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `97005aed0` | 360 | **26** | 7 | 9 | 0 | 10 | | after, `15a7d69a7` | 344 | **10** | 0 | 0 | 0 | 10 | Its src-comment column equals the census's 7, which is the control on the second instrument. The 323 live citations are the same in both readings, and the drop of 16 citations is exactly the rewritten sites. 11 extracted tokens are not tracker references at all and are not judged: the HTML entity `&objectstack-ai#39;` (6 sites in the template engine and its tests) and the fixture subjects `Invoice objectstack-ai#42` to `Invoice objectstack-ai#45` (5 sites). A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 371 occurrences and 26 dead before, 355 and 10 after. Beyond the gate's grammar it sees 11 tokens, none dead: the nine second numbers of the `#A/#B` lines (all live), the excused `Prime Directive objectstack-ai#12`, and the CSS colour `#2563eb`. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject, and `git blame` at the base puts every rewritten line in its anchor commit or in a later commit that descends from it (`merge-base --is-ancestor` exit 0 for all 16 line and anchor pairs). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#13189` | 13/4 | 8/5 | `33fbd3566` (PR objectstack-ai#13375): the SMTP port guard tests integrality (`Number.isInteger`), so a fractional port such as `587.5` is refused at construction, and the generated refusal sentence reads `(expected an integer 1-65535)`, the range still rendered from the constants. Its changeset headline names `objectstack-ai#13189`; its diff writes the integrality docblocks the rewritten lines sit in. New to the sweep | | `objectstack-ai#13190` | 5/1 | 3/2 | `56c5b1dbe` (PR objectstack-ai#13316): `smtpOptionsFromMailSettings` passes a present-but-unreadable `smtp_port` through to the guard instead of omitting it (which had silently fallen back to 587); absent and `''` still mean "not set", and no second refusal was added. Its changeset headline names `objectstack-ai#13190`; its diff writes the `objectstack-ai#13190` comment block itself. New to the sweep | | `objectstack-ai#11741` | 6/3 | 3/3 | `b706af987` (PR objectstack-ai#11839): `SendEmailInput` / `SendTemplateInput` gain an optional `organizationId`, which `plugin-email`'s writer stamps verbatim onto `sys_email.organization_id` (pass-through only, no resolution or fabrication), and `sendTemplate` forwards it as a producer of `send()`. Its message names `objectstack-ai#11741` as the card that commit closed; `git blame` puts all three rewritten lines in it. The `plugin-auth` stage's anchor for the same number | | `objectstack-ai#8675` | 2/2 | 2/0 | `c9f595083`: the squash commit of the pull request that was `objectstack-ai#8675` (its subject ends `(objectstack-ai#7987) (objectstack-ai#8675)`): `sys_account`'s OAuth token columns are declared `internal: true`. Its diff records the trap both lines describe: those columns are `required: false`, so inferring "key missing, therefore the strip ran" broke ordinary sign-in (16 red tests), which is why the readback carries the `absenceProvesStrip` discriminator. New to the sweep | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 4), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 4; control leg: stage 1's landing `422db788a` exit 0; the history is complete, `--is-shallow-repository` false, 15,155 commits). Each of the 4 numbers answers 404 on the issues endpoint, which serves pull requests too. Independently, the package's own shipped `CHANGELOG.md` pairs `b706af9`, `33fbd35` and `56c5b1d` with the same three decisions. ## Wordings to check - **Tag swaps in parentheses.** 「(objectstack-ai#13189)」 became 「(commit 33fbd35)」 at `transports/smtp-port-contract.ts:87` (a section heading), `:134` and `transports/smtp.ts:68`. - **Line openers.** 「objectstack-ai#11741 —」 became 「Commit b706af9 —」 at `email-service.ts:742` and `:1439`; 「objectstack-ai#13190 —」 became 「Commit 56c5b1d —」 at `transports/smtp.test.ts:221`; 「## objectstack-ai#13189 —」 became 「## Commit 33fbd35 —」 at `transports/smtp-port-contract.test.ts:34`. - **`email-service.test.ts:342`**, a section rule: 「── objectstack-ai#11741 —」 became 「── Commit b706af9 —」, and its trailing rule was shortened by 10 characters so the line keeps its width exactly. - **`internal-header-readback.ts:37`.** 「(PR objectstack-ai#8675 hit exactly this on `sys_account`'s optional」 became 「(Commit c9f5950 records exactly this on `sys_account`'s optional」: a commit does not "hit" a trap, it records one, and that commit's own diff is where the 16 red tests are recorded. - **`email-headers-internal.integration.test.ts:251`.** 「The regression PR objectstack-ai#8675 measured on a sibling card」 became 「The regression commit c9f5950 records from a sibling card」, the same reading. - **`transports/smtp-port-contract.test.ts:228`.** 「objectstack-ai#13189 is the card that SPENDS that」 became 「Commit 33fbd35 is the change that SPENDS that」, so the noun matches the anchor. - **`transports/smtp.ts:127`, `transports/smtp.test.ts:272`, `:276`, `:281`, `:283`.** The number became 「commit SHA」 in place (「until commit 33fbd35:」, 「The bucket commit 56c5b1d never had to name」, 「Commit 33fbd35 made the guard test」, 「Commit 56c5b1d's rule is that」, 「commit 33fbd35 changed which numbers」). ## The 10 sites left - **Test strings, 10 sites on 9 lines**, all `describe` / `it` titles, left as stages 1 to 10 left theirs: `email-service.test.ts:349` and `send-template.test.ts:63`, `:88` (`objectstack-ai#11741`); `transports/smtp-port-contract.test.ts:225`, `:309`, `:340` (`objectstack-ai#13189`); `transports/smtp.test.ts:230` (`objectstack-ai#13190`), `:271` (`objectstack-ai#13189`), `:293` (`objectstack-ai#13190` and `objectstack-ai#13189`). - No source string, operator log string, assertion message, quoted maintainer ruling or generated file in this package carries a dead number. - Outside `src`, the package's `CHANGELOG.md` names three of these numbers on 5 lines. It is release-owned and deliberately not edited here (see Acceptance notes). ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes (a `forEachChild` walk, so comments are trivia and JSDoc nodes are never visited), base `97005aed0` against head. String and template literals are therefore read in full. It ran over all 8 touched `.ts` files. - Real run: 7,035 base leaf tokens, **0 files with a token change** (exit 0). - Comment control in `email-service.ts` (「no resolution, no default, no fabrication」 to 「… no default and no fabrication」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `transports/smtp.ts` (`isValidSmtpPort(port)` given `as number`): DIFFER, 587 to 588 leaf tokens (exit 1). - Positive control, one digit changed inside a kept test title (`transports/smtp.test.ts:293`, `objectstack-ai#13189` to `objectstack-ai#13188`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs` (wrap mode) under a shell trap that restores by absolute path, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`1e99bd5e2bcb`, `46c13267611b`, `da5314910bc4`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/plugin-email` (`.changeset/20596-plugin-email-provenance-anchors.md`) is included. Its body is stage 10's, word for word, with the package name changed. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. After the build, `b706af987` appears twice in each of `dist/index.js` and `dist/index.mjs` (the two inline comments in `email-service.ts`, which the bundle keeps). `c9f595083` appears once in each of `dist/index.d.ts` and `dist/index.d.mts` (the `internal-header-readback.ts` docblock), and so does `33fbd3566` (the docblock on `SmtpTransportOptions.port`). `56c5b1dbe` reaches nothing (test files only). Positive controls, one unchanged line beside each shipped rewrite, land exactly where their neighbours do: 「context, so the input's organization is the one fact it may stamp:」 and 「caller's organization so the sys_email row it persists is stamped.」 once in each JS file; 「token columns: inheriting」 and the unchanged line just above the rewritten one in the `port` docblock once in each declaration file. A never-written negative phrase appears nowhere in `dist`. None of the 4 dead numbers is left in `dist`. ## Gates (head `23283d394`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run found no citation added against `97005aed0` (4 files read; test files are a deferred surface). - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `23283d394` derived 61 commands: all 55 derived at dispatch, plus `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. Each ran with its exit code captured before any pipe, and all 61 exit 0. `--ran`, fed each command with its exit code, reports 61 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/plugin-email test`: 31 files pass and 510 tests pass. `vitest list --filesOnly` names 31 files, all the tracked test files, the 4 touched ones included. - `pnpm --filter @objectstack/plugin-email typecheck` exits 0 (`tsc` on `tsconfig.json`, then `check:test-typecheck` on `tsconfig.test.json`: 0 files and 0 errors in its debt ledger). `tsc --listFiles` holds all 8 touched files in both programs, and the test program holds all 50 files under `src/`. - **Lint, as a proven narrowing:** eslint with inline config disabled, over the 8 touched `.ts` files, gives 8 files, 0 errors and 0 warnings. All 8 are in eslint's own population (`isPathIgnored` is false for each; a `dist` file, as the control, is ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 9 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked.** `CITATION_RE` refuses a hyphen after the digits and a `/` before the `#` (objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word 「option」. In this package: `#N-word` none, `#A/#B` 9 lines, `option #N` none, at the base and at the head, which is the claim's 0 / 9 / 0. Every second number on the 9 slash lines answers 200 (`objectstack-ai#5197` ×2, `objectstack-ai#5191`, `objectstack-ai#5211`, `objectstack-ai#5232` ×2, `objectstack-ai#5177`, `objectstack-ai#4251`, `objectstack-ai#5094`), so nothing there needed rewriting. - **ADR-0131 names `objectstack-ai#11741`.** Its D7 cites `objectstack-ai#11741` as the writer fact that keeps `sys_email` tenant data. That is evidence inside a later record, not the record of what `objectstack-ai#11741` decided, so it is not this stage's anchor, and `docs/adr/**` is a governed Tier H surface outside this card's stages. It joins the ADR-tree residue the seat already carries (ADR-0131's `objectstack-ai#14484`, stage 2). - **`CHANGELOG.md` is left.** `packages/plugins/plugin-email/CHANGELOG.md` names `objectstack-ai#11741`, `objectstack-ai#13189`, `objectstack-ai#13190` and `objectstack-ai#8675` on 5 lines. It is release-owned (AGENTS.md, Documentation Guardrails), a deferred surface of the citation gate, and ⛔ not part of this stage. - **「This card」 phrases are left.** 20 comment lines in 8 files of this package speak of 「this card」, 「the card」 or 「the two cards」. They carry no number and neither instrument sees them. Inside the `objectstack-ai#13189` test block, they still have the kept `(objectstack-ai#13189)` title as their referent; the one rewritten line that said 「the card」 now says 「the change」 (above). The rest are unchanged, as in stages 8 to 10. - **The census instrument did not truncate in this stage.** Both enumerations read 186 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13189` → `33fbd3566`; `objectstack-ai#13190` → `56c5b1dbe`; `objectstack-ai#8675` → `c9f595083`. `objectstack-ai#11741` → `b706af987` reuses the `plugin-auth` stage's anchor. - **Base.** The branch is on `main` at `97005aed0`. `main` has since moved two commits (`9c8f113c6`, `a6866da0c`). Their 14 files touch nothing under `plugin-email`, nor `scripts/check-issue-citations.mjs`, `.changeset/config.json` or the `doc-authoring-prose-id` baseline, and the three console-injection scripts they change are not among this diff's 61 derived families. So no merge was taken; the merge queue rebuilds on the merged generation. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #8118
Route 3, as ruled by triage (issue comment 5269775253, not reopenable): redact-on-read via
internal: trueonsys_http_delivery.headers_json, plus a purpose-built privileged accessor inpackages/objectql— theresolveSecretpattern that #7728 itself names as the remedy.What this does
packages/objectql—ObjectQL.resolveInternalField(object, recordIds, field): a batch, driver-level dereference of ONEinternal: truefield, returning a Map keyed by record id (null= row exists with nothing stored; absent = no such row). Refuses any field not declaredinternal: truewith the ADR-0112 envelope (INVALID_FIELD, status 400) — without that guard it would be a generic read-protection bypass (over ADR-0100's deliberately-maskedpasswordin particular). Batch-shaped because its consumer claims a batch per dispatcher tick: one driver read per claim batch, never per row (the per-row cost was one of route 1's rejection reasons).packages/services/service-messaging—sys_http_delivery.headers_jsonis declaredinternal: true, so the engine omits it from every generic read (list, get, explicit?select=headers_json) with no system carve-out (api-key-ui-lifecycle (secondary): thekeycolumn (SHA-256 hash) serializes over the data API, contradicting its own "never exposed to clients" description #7728's explicit design). The redaction sits at the ROW layer, so it covers the whole delivery population:source: 'webhook'rows (WebhookSchema-authored headers) ANDsource: 'flow'rows (per-run interpolated headers that never pass through WebhookSchema — the half every author-declared shape forgets).SqlHttpOutbox.claim()— the one reader that must put the map on the wire verbatim — recovers it through the accessor;list()/redeliver()return the redacted view.claim()throw — nothing goes out missing an authored header (missing headers are not self-announcing; the delivery would succeed while silently deviating from the authored config). The claimed rows revert topendingvia the claim TTL, and a healthy claimer recovers them headers-intact (pinned in test).content/docs/automation/webhooks.mdx— thesys_http_deliveryfield table stops advertisingheaders_jsonas a plain readable column, and the page's own rule ("reading a delivery row tells you what was sent, not how to forge it") now states explicitly that it covers the authored headers — this PR is what makes that sentence true. One page; no release-owned docs touched.Constraint evidence (all four binding constraints)
source: 'flow'shown covered: a dedicated integration case drives the exact input shapehttp-nodes.tsproduces (source: 'flow',refId= node id, per-run interpolated headers) end-to-end through the production enqueue → claim → send path: redacted on the data API, run-scoped bearer verbatim on the wire.sys_webhook.definition_json#7799 regression: the signing pin verifies the way a receiver verifies — recomputed HMAC over the actual wire body equals theX-Objectstack-Signatureheader, byte-identical across redelivery.sys_webhook.headers_secretnor theheadersPatchcompare-before-write; the enqueuer path is unchanged.sys_http_deliveryrow, while it still readsactive:true#8022 regression: no change to enqueue/re-arm or the error ledger; the redaction is read-side only.Field.secret()was measured and rejected on the card (orphansys_secretrow per delivery, boot-window fail-open, per-tick decrypt). A future card that changes the at-rest story flips that pin deliberately.Provenance — resumed work
Four commits (
c17e8677…d232665) were implemented by the previous dev on this card before an account-quota kill in the gate tail; this session inherited the branch, mergedorigin/main(89 commits, including #8327's rewrite of the projection filter inengine.ts— merged clean; the refusal is dotted-leg only, so the accessor's driver-level read and the?select=served-without-it behavior are unaffected), and re-ran all verification from scratch. Full inherited-vs-added accounting is in the dev report on #8118.Out of scope, deliberately
sys_email.headers_json([security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 ①-f, card [security]sys_email.headers_jsonstores custom headers cleartext — same shape assys_http_delivery; adopt whatever remedy #8118 lands, do not decide it twice #8149) is not addressed here — it is sequenced behind this card and adopts what lands here. [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 remains open.sys_secretretention (sys_secret: rotations that happened BEFORE #8030's fix left decryptable orphans on deployed instances, and nothing reaps them #8103) is untouched.Verification
pnpm --filter @objectstack/objectql test— 196 files / 3484 tests green (includes the new accessor pins).pnpm --filter @objectstack/service-messaging test— 19 files / 211 tests green (includes the 5-case at-rest integration suite).pnpm --filtertypecheck green on both packages; ESLint clean on all six changed source files.internal: trueturns exactly the 4 redaction/fail-closed cases red and the header-less-delivery case stays green; restored byte-identical from the commit.check-objectui-pin-freshreports the repo-wide pin staleness equally onmain— pre-existing release-lane state, zero lines of this diff.pnpm --filter @objectstack/lint run check:doc-formula-expressions(first run hit the documented unbuilt-dist prerequisiteERR_MODULE_NOT_FOUNDon@objectstack/formula; green after building that closure — a prerequisite, not a finding).Generated by Claude Code