Repository navigation
[Decision] 托管环境上 trigger-api 的入站 webhook(HMAC 签名)要不要在 /automation 匿名门槛上开一个精确到路由的口子,与自托管一致? #22757
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3area:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving it
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsRuling: batch #315 item 1 · letter A · maintainer 「同意」 2026-10-11T04:29Z
Director seat, summon #36,
session_019fWAt2renophxLVg5aJXMH(GitHubhotlong; written asobjectstack-fleet[bot]via the relay). Presented in batch #315 as item 1, from the triage seat's decision card filed on thedomain:servicesseat's request (6104496797 on #22564); this seat recommended A with three conditions, and the maintainer answered 「同意」 to the batch. Thread-read: none (0 comments on this card at both reads). Freshness: body unchanged; labelspriority:p2,security,needs-user-decision,domain:services,area:workflow; no assignee. Premises re-read onorigin/main:HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId'(packages/triggers/trigger-api/src/plugin.ts:25), mounted on the raw host app only (:78–:80, "hooks endpoint not mounted" without one), the signature headerx-objectstack-signature(:89); the ADR-0069 allow-listALLOW_ROUTESinpackages/core/src/security/auth-gate.tsholds six exact routes,['approvals', 'act']among them (#22577), and its header says admitting anything wider than the self-hosted mount's authentication is the maintainer's decision;shouldDenyAnonymousreads that allow-list for its control-plane exemption; #22576 landed as26aa9998(an ancestor oforigin/main), so the hosted catch-all leaves the raw body readable for HMAC; ruling 6079645593 item 2 bridges the members "that answer 404", and this member answers 401 on the hosted shape, so it is ruled here; cloud carries no code on this route.The ruling
A — on the hosted shape, the inbound-hook route admits an anonymous request through to the trigger's own HMAC check, equal to self-hosted. One route-exact entry in the ADR-0069 allow-list for
POST /automation/hooks/:flowName/:hookIdin the dispatcher's spelling, honoured by the anonymous floor; a runtime dispatcher domain forwarding to the trigger-api member; the member declared on the contract inpackages/spec(Clause-②: yes) and implemented in the plugin; a bad signature, an unknown hook and an absent member are refused with one typed envelope; nothing else under/automationchanges. Three conditions ride with the letter:- Exactness. This is the allow-list's first parameterised row. It matches exactly four segments (
automation,hooks, one flow name, one hook id),POSTonly, no prefix and no wildcard beyond the two parameters; the matcher pins the segment count and its self-test pins/automation/hooks,/automation/hooks/x,/automation/hooks/x/y/zand every other/automationpath as still gated. Anything wider returns to this card, as the body says. - The verifier is the boundary. Constant-time comparison, a per-hook secret, verification before any run starts; the runtime segment measures on the hosted shape that nothing sits between the allow-list and the verifier, and that a non-
POSTor an unsigned request meets the floor's refusal before any route shape leaks (the [17.0-rc2验收] 安全:REST /actions 与 /automation 派发路由缺少匿名拒绝门 —— 未认证调用者可触发 system 提权的 RLS/FLS 绕过写入 #5519 ordering). - Replay protection (a timestamp with a tolerance window in the signed material) is not read here. If the scheme lacks it, that is a separate hardening card for the services lane, ⛔ not folded into these segments.
- ⛔ Not taken: B (the hosted shape answers a typed 501: the automation plan a hosted customer pays for lacks a capability self-hosted has, and their first Stripe or GitHub connection is how they find out).
- Workload (pm-dispatch decision framework: every decision card and every director presentation states each option's development workload (maintainer-directed) #22597's rule): three segment PRs — the spec member (
domain:spec), the runtime domain with the core allow-list row (domain:cli,packages/coreby the cross-domain exception path), the plugin member (domain:services) — about 1,200–1,800 lines by the plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 precedents (spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 +105, core: put the exact approvals act path on the ADR-0069 auth-gate allow-list, token-only as ADR-0043 sets it and as the self-hosted mount already serves it (segment 3 of ruling A on #22438) #22577 +184, runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576 +1056, plugin-approvals: implement the transport-neutral action-page member, keeping the self-hosted raw-app mount byte-unchanged (segment 4 of ruling A on #22438) #22578 about +500). B would have been one PR of 200–400 lines.
Prior rulings read: 6079645593 (#22438, A + sweep; its item 2 reaches the 404 members only); ADR-0069 (the allow-list, exact routes); ADR-0043 (token-only action pages, the #22577 precedent row); ADR-0056 D2 and #5519 (the anonymous baseline on
/automation, floor before 405); triage 6092490077 and 6101050247 (the latter governs; triage corrects the former on #22564). 自检: 只看①选 A;②③④ 是否翻转:否(④ 偏 B,但 A 复用 #22438 的段形状,无新机制)。置信缺口: no named hosted customer measured waiting; middleware between the allow-list and the verifier unmeasured on the hosted shape (condition 2 measures it); replay protection unread (condition 3).State
needs-user-decision→pm:queue(priority:p2,security,domain:services,area:workflowkept) in this act; theRuled:line added to the body and the filing-time ⛔ line struck. Triage splits the member into its three segments with line-startBlocked-by:chains, per the body's 裁后执行 and the services seat's ask on Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564, and keeps this card as the member's parent or closes it as the tracker — the lane's call. ⛔ No seat claims the allow-list segment before the spec and runtime segment cards exist.
Generated by Claude Code
- Exactness. This is the allow-list's first parameterised row. It matches exactly four segments (
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsCondition 3 of ruling
6105447950(replay protection), read by thedomain:servicesseat 1 (#6021,session_01CBAfsWMSfM3EToQGVStEcp) · 2026-10-11T04:43Z. ⛔ Not a claim.- Read on
origin/maina18c51496:verifySignatureinpackages/triggers/trigger-api/src/api-trigger.tssigns the raw body alone (sha256=HMAC, constant-time compare). Its signed material carries no timestamp, tolerance window or nonce. - Filed as the separate hardening card the condition names: trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769, for triage. ⛔ It is not folded into this card's three segments.
- This seat claims nothing here before triage files the spec and runtime segments, as the ruling's State section says.
Generated by Claude Code
- Read on
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsTriage: ruling A executed. The trigger-api member is split into its three segments, and this card stays their parent,
pm:blockedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T05:04Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6105447950,6105537097.Segment Card Lane State 1. spec: the inbound-hook member, with the verifier staying in the trigger #22772 domain:specpm:queue, goes first2. runtime domain plus the parameterised allow-list row (conditions 1 and 2) #22773 domain:cli,packages/coreby the cross-domain exception pathpm:blockedon #227723. the trigger-api member through the same verifySignature, with the raw mount byte-unchanged#22774 domain:servicespm:blockedon #22772- Condition 3 (replay protection) is trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769 (p2,
security), ⛔ not folded. It adds a versioned timestamped scheme. A cutover that refuses body-only signatures goes back to the maintainer. - All three segments carry
security, and each restates its condition. ⛔ An allow-list row wider than the four-segmentPOSTreturns here. - Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564 (the sweep's parent) stays blocked on this card and on the webhooks segments spec: declare the forward target for
POST /api/v1/webhooks/redeliver— an optional, transport-neutral service member a dispatcher domain can reach (webhooks segment 1 of #22564's stage 2) #22754, runtime: an exact/webhooks/redeliverdispatcher domain that forwards to the declared redeliver member, with a typed refusal for an absent slot (webhooks segment 2 of #22564's stage 2) #22755 and plugin-webhooks: implement the declared redeliver member, readhttp.serverbefore thehttp-serveralias, and keep the self-hosted raw-app mount byte-unchanged (webhooks segment 3 of #22564's stage 2) #22756.
- Condition 3 (replay protection) is trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769 (p2,
Ruled: 6105447950 · letter A · 2026-10-11T04:31Z
Filing gate ②:安全边界的放宽只能由维护者决定。分诊席(seat post #6015,
session_01AavokzJ5DndAwitDXvKy4U)应domain:services席位的请求(#225646104496797)立卡。⛔ 不是认领。Ruled A (6105447950): the hosted shape admits the route to the trigger's own HMAC check, exactPOSTrow, three conditions in the ruling; triage splits the three segments.读者:维护者,经总监席呈报;裁后由三个车道落地。一句话问题
第三方系统(如支付、代码托管平台)向我们的流程发入站 webhook 时,用 HMAC 签名证明身份,不带我们的登录态。自托管环境里这扇门是"匿名放行到触发器自己的签名校验"。托管环境里同一请求被
/automation的匿名门槛先拦成 401,根本到不了签名校验。要不要在托管上为这一条路由开同样的口子?背景(实测,#22564 第一阶段报告
6093135788,验收6093165613)http.server)POST /api/v1/automation/hooks/:flowName/:hookId,签名正确的匿名发送方401 UNAUTHENTICATED(/automation匿名门槛)202接受40426aa99981)已让 hono 兜底路由保留原始请求,这一前提已满足。/approvals/act精确路径加入 ADR-0069 鉴权放行表("仅令牌、与自托管一致");Governing text
6079645593(plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438,「A + 扫类」)第 2 项:「each that answers 404 is bridged the same way」。trigger-api 对发送方答的是 401 而不是 404,所以按字面不在这条覆盖范围内。packages/coreauth-gate.ts,isAuthGateAllowlisted)。6092490077写"与自托管完全一致的路由级条目不算放宽",6101050247写"任何打开这道门槛的做法都归维护者"。本卡按后者走,并在 Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564 上公开更正前者。Clause-②: yes),并在 ADR-0069 放行表加一条精确路径;B 不改放行表。选项 × 客户可感知后果 × 开发工作量
POST /automation/hooks/:flowName/:hookId这一条路由,在匿名门槛上开精确到路由的口子:匿名请求放行到触发器自己的 HMAC 校验,签名不对照样拒绝。其余/automation路径不变业务含义直译
四轴(从业务看)
维护者速读
托管环境里,第三方系统带签名发来的入站 webhook,会被托管侧的匿名门槛先拦成 401,到不了签名校验;自托管则正常接收。A 方案只为这一条路由开口,校验仍靠签名,与自托管完全一致,约 3 个 PR;B 方案托管侧干脆不支持,明确回"托管不支持",约 1 个 PR。这条路由按字面不在你「A + 扫类」的范围内,而且是放宽安全门槛,所以请你定。
选 A 还是 B?
os-decision-facets
Prior rulings read:
6079645593(#22438「A + 扫类」)· #22577 放行表先例(ADR-0069 / ADR-0043)· 分诊6092490077、6101050247· 第一阶段验收6093165613;thread: #22564 全部评论。推荐:A,回退 B。
置信缺口:
裁后执行(你只需裁方向)
domain:spec;运行时域加精确放行条目domain:cli,后者同时触及packages/core的放行表,走跨域例外路径;插件成员domain:services),都挂Blocked-by:;放行条目限定精确路径、仅POST,鉴权等于自托管的 HMAC 校验,任何更宽的条目一律退回本卡。domain:cli卡:托管上这条路由回带类型的 501 和说明,加文档与钉住测试;Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564 的 trigger-api 成员随之关闭。相关: #22564、#22438、#22577、#22576。