Skip to content

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

Closed

Description

@os-warren

⛔ 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;
  • consider extending the existing [security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799 callout to cover the headers move, since the two landed as siblings.

Not claimed

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)


Generated by Claude Code

Activity

  1. added
    documentationImprovements or additions to documentation
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Aug 19, 2026
  2. added theissue type on Aug 19, 2026
  3. os-warren commented on Aug 19, 2026

    @os-warren
    CollaboratorAuthor

    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.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 19, 2026
  5. claude commented on Aug 19, 2026

    @claude
    Contributor

    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.


    Generated by Claude Code

  6. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator
    {
      "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

  7. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationdomain:devxDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions