Skip to content

An api-triggered flow can verify only an ObjectStack signature: a vendor webhook signed its own way (DocuSign Connect HMAC) cannot be admitted #29983

Open

Description

@objectstack-fleet

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Blocked-by: #30009

Seat note (2026-11-04T04:02Z): the contract half landed in PR #29995; the server verifier is #30009, whose PR closes this card.

Found by the hotclm app (objectstack-ai/hotclm#83, ruling 6617232764) on @objectstack/* 18.3.0.

Need. hotclm's F8 e-signature step uses a declared DocuSign openapi connector (18.3.0, #26702 / #26709). The status of a sent envelope should arrive by DocuSign Connect, which POSTs to a webhook URL and signs the raw body with X-DocuSign-Signature-1 (base64 HMAC-SHA256 under a Connect key). Other vendors do the same with their own header and encoding (Adobe Sign, Dropbox Sign, Stripe-style t=…,v1=…).

What the platform offers. An api-triggered flow is admitted only by the platform's own x-objectstack-signature (timestamped HMAC-SHA256 of <t>.<raw body> under the hook's secret; packages/spec/src/contracts/trigger-api-service.ts). "The signature is the only credential", and there is no way to declare a vendor's header, encoding or signed payload, so a vendor callback is refused 401 INVALID_SIGNATURE or must be admitted unsigned. hotclm therefore polls envelope status on a schedule (as the official connector-docusign app does), which delays activation and spends API quota.

Ask. A declarative verifier on an api trigger for third-party senders: header name, algorithm (HMAC-SHA256), encoding (hex / base64), what is signed (raw body, or <t>.<body>), and the key as a credentialRef, keeping the single-verifier rule. Alternatively, a connector-level inbound webhook that the openapi connector instance owns.

Dedupe: no issue mentions vendor callback / webhook signature verification for api triggers.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Nov 3, 2026
  2. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 · domain:engine → domain:spec (contract-first) · pm:queue kept · 2026-11-03T22:53Z

    ⛔ Not a claim.

    • Read fresh. An api-triggered flow is admitted only by the platform's own x-objectstack-signature, so a vendor webhook signed its own way (DocuSign Connect, base64 HMAC-SHA256 of the raw body) cannot be admitted. hotclm's e-signature status step needs it.
    • The ask is a declarative verifier on an api trigger: header, algorithm, encoding, what is signed, and a credentialRef key. That is new protocol surface, so p1 under 「协议新增定 p1」, above the p2 filed. It goes contract-first to domain:spec, and the server verifier follows as its own slice.
    • Human floor (auth boundary): the verifier admits external callers. It keeps the single-verifier rule, a constant-time compare and a timestamp/replay bound where the sender offers one, and ⛔ never inlines the key. The maintainer reviews the contract. A security finding found on the way goes to security#1, not to a comment.
    • domain:engine is retired; the live label here is domain:spec.

    Generated by Claude Code

  3. added
    priority:p1High: required for production / M2
    and removed
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Nov 3, 2026
  4. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Claim: PM loop round 11 (p1)
    Session: session_01WGufzfgvGexKn9vLHsy89K
    Account: os-support-ai
    Branch: the card thread's own claude/project-thread-* branch, named in the dev's hand-back and restated in a later claim (a measured platform exception to claude/issue-29983-…)
    Worktree: objectstack-issue-29983
    Domain: domain:spec
    Seat: domain:spec#3
    File surface: the contract half only. The api trigger's config in packages/spec/src/automation/ (beside config.secretRef, api-trigger-secret-ref.zod.ts), packages/spec/src/contracts/trigger-api-service.ts (the verifier contract text), spec tests, and the generated ledgers the key moves. The server verifier is a separate domain:server slice, filed after this contract lands.
    Container & model: M, mode:thread, model: TIER_DEFAULT, effort: xhigh (an auth boundary: the verifier admits external callers)
    Clause-②: yes (widening: a new declarative verifier on the api trigger)
    Metadata-protocol: added
    Responsibility: n/a — capability gap, not a regression of a landed PR
    Thread-read: 6617546776
    Premise: holds. At origin/main 9049be26, an api flow is admitted only by x-objectstack-signature (contracts/trigger-api-service.ts ≈:57, "The signature is the only credential" ≈:100), and its secret is named by config.secretRef. No key declares a vendor header, encoding or signed payload.
    Human floor (triage 6617546776): an auth boundary. The single-verifier rule is kept, the compare is constant-time, a timestamp/replay bound applies where the sender offers one, and the key is never inlined. The maintainer reviews the contract, so the PR is handed to the maintainer, not auto-merged. A security finding goes to security#1.
    Serial constraints cleared: none in flight on the api trigger config or trigger-api-service.ts.

  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Claim: PM loop round 11 (p1), restated with the branch
    Session: session_01WGufzfgvGexKn9vLHsy89K
    Account: os-support-ai
    Branch: claude/project-thread-5mb6lr (card thread, executor session_01WGufzggC8errMtPRYBUmoY; the thread can push only its own branch, a measured platform exception to claude/issue-29983-…)
    Worktree: objectstack-issue-29983
    Domain: domain:spec
    Seat: domain:spec#3
    File surface: the contract half only. The api trigger's config in packages/spec/src/automation/ (beside config.secretRef, api-trigger-secret-ref.zod.ts), packages/spec/src/contracts/trigger-api-service.ts (the verifier contract text), spec tests, and the generated ledgers the key moves. The server verifier is a separate domain:server slice, filed after this contract lands.
    Container & model: M, mode:thread, model: TIER_DEFAULT, effort: xhigh (an auth boundary: the verifier admits external callers)
    Clause-②: yes (widening: a new declarative verifier on the api trigger)
    Metadata-protocol: added
    Responsibility: n/a — capability gap, not a regression of a landed PR
    Thread-read: 6618402070
    Premise: holds. At origin/main 9049be26, an api flow is admitted only by x-objectstack-signature (contracts/trigger-api-service.ts ≈:57, "The signature is the only credential" ≈:100), and its secret is named by config.secretRef. No key declares a vendor header, encoding or signed payload.
    Human floor (triage 6617546776): an auth boundary. The single-verifier rule is kept, the compare is constant-time, a timestamp/replay bound applies where the sender offers one, and the key is never inlined. The maintainer reviews the contract, so the PR is handed to the maintainer, not auto-merged. A security finding goes to security#1.
    Serial constraints cleared: none in flight on the api trigger config or trigger-api-service.ts.

  6. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzggC8errMtPRYBUmoY

    os-dev-report

    {
      "card": 29983,
      "status": "pr_open",
      "pr": 29995,
      "draft": true,
      "closing_keyword": "Part of #29983",
      "branch": "claude/project-thread-5mb6lr",
      "head_sha": "db0c0f86b017b8d0400c2571277e46fc38a40b89",
      "merge_base": "9049be26a",
      "declarations": { "clause_2": "yes (widening)", "metadata_protocol": "added", "bump": "@objectstack/spec minor", "adr_0087": "not-required (no-migration-prescription)" },
      "gates": { "derived": 107, "run": 107, "exit_nonzero": 0, "not_measured": 0, "reconcile": "dispatch-gates --ran exit 0" },
      "tests": {
        "new_file": "packages/spec/src/automation/api-trigger-signature.test.ts: 22 passed",
        "spec_test": "809 files, 24299 passed, 1 todo, exit 0",
        "spec_typecheck": "exit 0",
        "spec_test_repo": "89 files, 1524 passed, exit 0",
        "check_changeset_no_major_with_event": "exit 0"
      },
      "files_changed": [".changeset/29983-api-trigger-signature.md", "content/docs/references/automation/api-trigger-secret-ref.mdx", "content/docs/references/automation/api-trigger-signature.mdx", "content/docs/references/automation/index.mdx", "content/docs/references/automation/meta.json", "content/docs/references/index.mdx", "docs/audits/2026-07-unknown-key-strictness-ledger.counts/automation.md", "docs/audits/2026-07-unknown-key-strictness-ledger.md", "packages/spec/api-surface/automation.json", "packages/spec/authorable-surface/automation.json", "packages/spec/declaration-map/automation.json", "packages/spec/export-origins/automation.json", "packages/spec/json-schema.manifest/automation.json", "packages/spec/llms.txt", "packages/spec/src/automation/api-trigger-secret-ref.zod.ts", "packages/spec/src/automation/api-trigger-signature.test.ts", "packages/spec/src/automation/api-trigger-signature.zod.ts", "packages/spec/src/automation/flow.zod.ts", "packages/spec/src/automation/index.ts", "packages/spec/src/contracts/trigger-api-service.ts", "packages/spec/src/type-alias-convention.pin.test.ts", "packages/spec/variant-docs.json", "skills/objectstack-automation/references/_index.md"],
      "line_budget": { "additions": 935, "deletions": 19, "hand_written_src_lines": "api-trigger-signature.zod.ts +372, its test +236, trigger-api-service.ts +49/-5, flow.zod.ts +9" },
      "fork_reported": false,
      "deviations": [
        "liveness planned+authorWarn row not added: flow nodes config is an open record, no row is reachable under it (measured, PR Acceptance notes)",
        "metadata-form reconciliation not moved: api start node config is not offered in a platform-objects form",
        "no assignees, no reviewer request, no PR subscription from this thread: os-dev rule 2, the seat holds the PR's people and its watch"
      ],
      "writes": [
        "git push claude/project-thread-5mb6lr to db0c0f86",
        "relay pr_create fw-20261104T005719Z-f1869b, run 41750617604 success, read-back identical, PR 29995",
        "relay comment: this os-dev-report via post-stamped"
      ],
      "mcp_calls": [
        "github issue_read get 29983",
        "github issue_read get_comments 29983",
        "github pull_request_read get 29995"
      ],
      "security_findings": "none filed",
      "out_of_scope_findings": []
    }

    Stamp: 2026-11-04T00:58Z

  7. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Seat ACCEPT for the contract half, PR #29995 at head bb371a5c3103db11490cb2cc4c39c378b588a755: #29995 (comment). The contract review passed after two fix rounds (record 6620688559). The PR is Tier H, so the maintainer merges it. This card stays open: the domain:server verifier slice is filed from the PR body's checklist once #29995 lands.

    Posted 2026-11-04T02:25Z.

  8. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Landed (contract half): PR #29995.

    It merged through the queue at 2026-11-04T04:00Z. @objectstack/spec now declares config.signature on an api start node, with two arms (body, timestamp.body), the key only through config.secretRef, eight reserved header names refused in the schema itself, and apiTriggerVerifierOf answering platform, declared or unusable. ITriggerApiService states the verifier's obligations. Nothing serves the key yet, so a hook that declares it still answers every vendor post with 401 INVALID_SIGNATURE (fail closed).

    Remaining: the server verifier, filed as #30009 (domain:server, p1, from the PR's checklist). Its PR closes this card. This card moves to pm:blocked with Blocked-by: #30009 in its body. The Skills-docs: follow-up (three skills lines and a flows.mdx section) rides #30009, because those lines are correct until the server serves the key.

    Posted 2026-11-04T04:04Z.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpm:blockedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions