You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
content/docs/automation/webhooks.mdx still documents custom headers as living in sys_webhook.definition_json, and omits headers_secret entirely — the doc points authors at the cleartext column #7986 moved them out of #9928
⛔ Unassigned; for triage to grade. Found while working #9756 (the enable block census) — out of scope there, and not touched by PR #9927, whose file surface is plugin-webhooks only.
Measured on origin/main @ b0300556d
#7986 moved sys_webhook's custom HTTP headers out of the definition_json blob and into headers_secret — a Field.secret() column the engine encrypts at rest into sys_secret, whose reads return a mask. The webhooks guide was not updated with it, and still describes the pre-#7986 world in three places:
line 87 — "The transport configuration (headers, timeout, method) is carried in ..." (definition_json)
line 104, the sys_webhook field table:
definition_json | textarea | Serialised Webhook JSON ... — carries the transport config: custom headers and timeoutMs. Not the signing secret.
line 110 — "the headers and per-attempt timeout are parsed out of definition_json when an event is enqueued"
⇒ and the field table has no headers_secret row at all, though the column ships and is admin-authorable. The table documents its sibling signing_secret correctly, so the omission reads as "this field does not exist" rather than as an obvious gap.
Why this is worth a card rather than a typo fix
The doc does not merely lag — it points an author at the wrong column, in the direction that reintroduces the defect. An author (or an AI author) following this table writes an Authorization: Bearer … into definition_json, which is an ordinary textarea returned in full by GET /api/v1/data/sys_webhook. The runtime still accepts that: readLegacyHeaders (packages/plugins/plugin-webhooks/src/auto-enqueuer.ts:579) reads such a map, delivers from it, and logs a warn naming it cleartext — so following the doc produces working deliveries and a warning nobody is looking for, not an error.
The same page already carries the pattern this needs, one section down: sys_http_delivery's table documents headers_json as internal with the read-path rule spelled out (line 159), and there is a Callout explaining the #7799 signing-secret move (line ~117) with no counterpart for the headers move.
Suggested shape (not a ruling — triage's call)
add a headers_secret row to the sys_webhook field table, mirroring the signing_secret row's wording (encrypted into sys_secret, reads return a mask, leave the mask untouched to keep the current value);
correct lines 87 / 104 / 110 so definition_json is described as carrying timeoutMs and the rest of the envelope, with the headers named as legacy-only — still read for un-migrated rows, migrated by the boot sweep;
⛔ No runtime defect is asserted. headers_secret and the legacy-read path both behave as their own cards specify; this is a documentation-vs-implementation drift only.
I did not audit the rest of the page; the three lines above are the whole of what I measured.
Refs: #7986 (moved the headers to headers_secret) · #7799 (the signing-secret sibling, whose callout this page does carry) · #8566 (the headers_secret plaintext shape gate) · #9756 / PR #9927 (where this was noticed)
Triage: lands in content/docs/automation/webhooks.mdx ⇒ domain:devx; queued, type Bug (the doc actively directs authors at the cleartext column the platform moved away from — a security-relevant doc defect, not a lag).
The filer's suggested shape (mirror the signing_secret row's wording for headers_secret, correct lines 87/104/110, extend the #7799 callout) is a complete spec for an S-tier docs card. Sequencing note: this is the authoring-guidance half; the runtime acceptance half is #9930 (now in the maintainer inbox). Landing this first reduces new legacy rows regardless of how #9930 is decided — no blocking relationship either way.
Claim: PM loop round 26
Session: session_01XqDQYVU5smx29ts9pAErja
Branch: claude/issue-9928-webhook-headers-secret
Worktree: objectstack-issue-9928
Domain: domain:devx
File surface: content/docs/automation/webhooks.mdx (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (--tier on content/docs/** returns "no path-derived mandate … This line is a FLOOR, never a clearance"; content limb no — docs prose only)
Clause-②: no
Serial constraints cleared: swept all open PRs for any touching a webhook path — none. The card notes PR #9927's file surface is plugin-webhooks only, so it does not collide.
⚠️ Security-adjacent: the page currently points authors at a cleartext column for data #7986 moved into an encrypted one. Getting the replacement wrong in the other direction (implying headers_secret is cleartext, or that masking is optional) is the worse error, so the dispatch requires the storage and read-back behaviour to be measured rather than described from the card.
{
"issue": 9928,
"status": "done",
"branch": "claude/issue-9928-webhook-headers-secret",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10067",
"premise_still_valid": true,
"summary": "H1 — all three citations still accurate on main @ f4c2eef46, one line off by one: the transport-config prose is line 87 (the card's number, exact), the `definition_json` table row is line 104 (exact), and the 'headers ... parsed out of definition_json' sentence is line 110 (exact); the card's line 87 sentence actually starts at 87 and wraps to 88 where `definition_json` is named. Nothing had been fixed already. Rewrote §3.1: the intro now describes `definition_json` as carrying `timeoutMs` + the rest of the authored envelope with both credential-shaped values named as living in their own encrypted columns; the `definition_json` table row drops the headers claim; a new `headers_secret` row sits between it and `signing_secret` (declaration order) mirroring the sibling's wording plus the #8566 write-door refusal (VALIDATION_ERROR/400 for a non-flat-string-map plaintext); the runtime paragraph now sources the timeout from the blob and BOTH credentials from `resolveSecretField()` on the enqueuer's cache refresh; two new paragraphs answer read-back and fail-closed posture; the v17 Callout is extended to cover the headers move, the boot sweep, and the legacy blob read — marked a compatibility path for pre-move rows and explicitly not a place to author. ⚠️ FINDING that outranks the docs fix, per 裁决 2: the runtime DOES still fall back to `definition_json.headers` — `readLegacyHeaders` at auto-enqueuer.ts:579 (the card's line number, still exact), delivering from the cleartext map behind a `warn` that names it cleartext. Already owned by #9930 (open, needs-user-decision, assigned), so recorded here rather than re-filed. Docs prose only, one file, no packages/** touched; no changeset (`skip-changeset` applied and read back).",
"tests": "All gates run at HEAD e55d51f91f (the final commit), union re-derived by `node scripts/pm/dispatch-gates.mjs` with no path list (it read 1 committed path, merge base f4c2eef46) — 11 derived gates + check:nul-bytes, all exit 0, each quoting its own verdict line: check:docs-audit-scope '✓ docs-accuracy-audit scope is in sync with content/docs/: 180 hand-written doc(s).' + '✓ release-owned pages are in scope and read-only: 9 page(s) ... review-only' (count UNCHANGED as required); check:doc-anchors '✅ 251 internal #fragment link(s) across 399 source file(s) all resolve to a real heading'; check:docs-redirects 'OK (apps/docs/redirects.mjs: 92 entries ...)'; check:published-readme-links '✓ 152 outbound link(s) across 60 published markdown file(s)'; check:role-word 'OK, no new occurrences of the reserved word.'; check:cross-package-test-inputs + scripts/check-cross-package-test-inputs.mjs 'OK: 12 package(s) read outside themselves, all declared'; check:nul-bytes 'OK (scanned 6362 text file(s) ... no raw ASCII control bytes)'; spec check:empty-state '✓ all classified (1 closed, 2 open, 4 output, 9 scope)'; spec check:liveness '✓ every governed-type property ... is classified'; spec check:strictness-ledger '✓ 61 file(s) across 5 triaged director(ies)'; spec check:variant-docs '✓ 18 discriminated union(s) — 8 governed, 10 exempt'. The four spec gates ran under `flock -E 99 -w 540 /tmp/os-heavy-verify.lock`. Renderability was measured, not assumed: the page compiles under @mdx-js/mdx@3.1.1 (the version apps/docs resolves) — and the instrument was proved falsifiable, the same script failing the same file with one injected '{' ('Unexpected end of file in expression'). Also grep-scanned the changed file for raw control bytes: clean. No ablation applies (docs-only, nothing built into dist).",
"open_questions": [],
"out_of_scope_findings": [
"not filed — already owned by #9930 (open, needs-user-decision): `readLegacyHeaders` (auto-enqueuer.ts:579) still reads and delivers from cleartext `definition_json.headers` behind a `warn`; the PR documents the fallback as a compatibility path and does not touch it",
"not filed — no defect: H3 sweep of content/docs/** for `definition_json` / `headers_secret` / webhook transport prose found 5 hits in 2 files. Total 2 / stale 1 / fixed 1. webhooks.mdx held all 4 stale hits (fixed here); content/docs/releases/v17.mdx:2909 is release-owned and ACCURATE as written ('webhook custom `headers` are encrypted rather than riding `definition_json` in cleartext (#7986)') — read as evidence, not edited. `headers_secret` appeared in ZERO published docs pages before this PR, which is why the omission read as 'this column does not exist'. content/docs/releases/v16.mdx's single `sys_webhook` mention is about unmappable triggers, unrelated. No governed surface carries a copy.",
"not filed — no defect: H4 verdict, the worked example is CLEAN. The page ships exactly one webhook-authoring block (the bulk-trigger example, `webhooks: [{ name, object, triggers, url }]`) and it names no `headers` key at all, so there was no copy-paste path writing a token into the old blob — the mcp/README.md and driver-sql/README.md failure shape does NOT repeat here. The other code fences on the page are event envelopes, the claim SQL, the payload JSON and the signature header — none of them place transport config. The authoring envelope itself is unchanged by #7986 (`defineWebhook({ secret, headers })`, webhook.zod.ts:200 `headers: z.record(z.string(), z.string())`); the boot materializer routes each value to its own column, and the Callout now says so."
]
}
Activity
os-warren commentedon Aug 19, 2026
Triage: lands in
content/docs/automation/webhooks.mdx⇒domain:devx; queued, type Bug (the doc actively directs authors at the cleartext column the platform moved away from — a security-relevant doc defect, not a lag).The filer's suggested shape (mirror the
signing_secretrow's wording forheaders_secret, correct lines 87/104/110, extend the #7799 callout) is a complete spec for an S-tier docs card. Sequencing note: this is the authoring-guidance half; the runtime acceptance half is #9930 (now in the maintainer inbox). Landing this first reduces new legacy rows regardless of how #9930 is decided — no blocking relationship either way.Generated by Claude Code
claude commentedon Aug 19, 2026
Claim: PM loop round 26
Session:
session_01XqDQYVU5smx29ts9pAErjaBranch:
claude/issue-9928-webhook-headers-secretWorktree:
objectstack-issue-9928Domain:
domain:devxFile surface:
content/docs/automation/webhooks.mdx(stop on breach; explain in the report)Container & model: M, mode:subagent, model: opus (
--tieroncontent/docs/**returns "no path-derived mandate … This line is a FLOOR, never a clearance"; content limbno— docs prose only)Clause-②: no
Serial constraints cleared: swept all open PRs for any touching a webhook path — none. The card notes PR #9927's file surface is
plugin-webhooksonly, so it does not collide.headers_secretis cleartext, or that masking is optional) is the worse error, so the dispatch requires the storage and read-back behaviour to be measured rather than described from the card.Generated by Claude Code
docs(webhooks): custom headers live in the encrypted `headers_secret`…
os-steve commentedon Aug 19, 2026
{ "issue": 9928, "status": "done", "branch": "claude/issue-9928-webhook-headers-secret", "pr": "https://github.com/objectstack-ai/objectstack/pull/10067", "premise_still_valid": true, "summary": "H1 — all three citations still accurate on main @ f4c2eef46, one line off by one: the transport-config prose is line 87 (the card's number, exact), the `definition_json` table row is line 104 (exact), and the 'headers ... parsed out of definition_json' sentence is line 110 (exact); the card's line 87 sentence actually starts at 87 and wraps to 88 where `definition_json` is named. Nothing had been fixed already. Rewrote §3.1: the intro now describes `definition_json` as carrying `timeoutMs` + the rest of the authored envelope with both credential-shaped values named as living in their own encrypted columns; the `definition_json` table row drops the headers claim; a new `headers_secret` row sits between it and `signing_secret` (declaration order) mirroring the sibling's wording plus the #8566 write-door refusal (VALIDATION_ERROR/400 for a non-flat-string-map plaintext); the runtime paragraph now sources the timeout from the blob and BOTH credentials from `resolveSecretField()` on the enqueuer's cache refresh; two new paragraphs answer read-back and fail-closed posture; the v17 Callout is extended to cover the headers move, the boot sweep, and the legacy blob read — marked a compatibility path for pre-move rows and explicitly not a place to author. ⚠️ FINDING that outranks the docs fix, per 裁决 2: the runtime DOES still fall back to `definition_json.headers` — `readLegacyHeaders` at auto-enqueuer.ts:579 (the card's line number, still exact), delivering from the cleartext map behind a `warn` that names it cleartext. Already owned by #9930 (open, needs-user-decision, assigned), so recorded here rather than re-filed. Docs prose only, one file, no packages/** touched; no changeset (`skip-changeset` applied and read back).", "tests": "All gates run at HEAD e55d51f91f (the final commit), union re-derived by `node scripts/pm/dispatch-gates.mjs` with no path list (it read 1 committed path, merge base f4c2eef46) — 11 derived gates + check:nul-bytes, all exit 0, each quoting its own verdict line: check:docs-audit-scope '✓ docs-accuracy-audit scope is in sync with content/docs/: 180 hand-written doc(s).' + '✓ release-owned pages are in scope and read-only: 9 page(s) ... review-only' (count UNCHANGED as required); check:doc-anchors '✅ 251 internal #fragment link(s) across 399 source file(s) all resolve to a real heading'; check:docs-redirects 'OK (apps/docs/redirects.mjs: 92 entries ...)'; check:published-readme-links '✓ 152 outbound link(s) across 60 published markdown file(s)'; check:role-word 'OK, no new occurrences of the reserved word.'; check:cross-package-test-inputs + scripts/check-cross-package-test-inputs.mjs 'OK: 12 package(s) read outside themselves, all declared'; check:nul-bytes 'OK (scanned 6362 text file(s) ... no raw ASCII control bytes)'; spec check:empty-state '✓ all classified (1 closed, 2 open, 4 output, 9 scope)'; spec check:liveness '✓ every governed-type property ... is classified'; spec check:strictness-ledger '✓ 61 file(s) across 5 triaged director(ies)'; spec check:variant-docs '✓ 18 discriminated union(s) — 8 governed, 10 exempt'. The four spec gates ran under `flock -E 99 -w 540 /tmp/os-heavy-verify.lock`. Renderability was measured, not assumed: the page compiles under @mdx-js/mdx@3.1.1 (the version apps/docs resolves) — and the instrument was proved falsifiable, the same script failing the same file with one injected '{' ('Unexpected end of file in expression'). Also grep-scanned the changed file for raw control bytes: clean. No ablation applies (docs-only, nothing built into dist).", "open_questions": [], "out_of_scope_findings": [ "not filed — already owned by #9930 (open, needs-user-decision): `readLegacyHeaders` (auto-enqueuer.ts:579) still reads and delivers from cleartext `definition_json.headers` behind a `warn`; the PR documents the fallback as a compatibility path and does not touch it", "not filed — no defect: H3 sweep of content/docs/** for `definition_json` / `headers_secret` / webhook transport prose found 5 hits in 2 files. Total 2 / stale 1 / fixed 1. webhooks.mdx held all 4 stale hits (fixed here); content/docs/releases/v17.mdx:2909 is release-owned and ACCURATE as written ('webhook custom `headers` are encrypted rather than riding `definition_json` in cleartext (#7986)') — read as evidence, not edited. `headers_secret` appeared in ZERO published docs pages before this PR, which is why the omission read as 'this column does not exist'. content/docs/releases/v16.mdx's single `sys_webhook` mention is about unmappable triggers, unrelated. No governed surface carries a copy.", "not filed — no defect: H4 verdict, the worked example is CLEAN. The page ships exactly one webhook-authoring block (the bulk-trigger example, `webhooks: [{ name, object, triggers, url }]`) and it names no `headers` key at all, so there was no copy-paste path writing a token into the old blob — the mcp/README.md and driver-sql/README.md failure shape does NOT repeat here. The other code fences on the page are event envelopes, the claim SQL, the payload JSON and the signature header — none of them place transport config. The authoring envelope itself is unchanged by #7986 (`defineWebhook({ secret, headers })`, webhook.zod.ts:200 `headers: z.record(z.string(), z.string())`); the boot materializer routes each value to its own column, and the Callout now says so." ] }Generated by Claude Code
Generated by Claude Code
docs(webhooks): custom headers live in the encrypted `headers_secret`…