Repository navigation
[Decision] 托管环境上 trigger-api 的入站 webhook(HMAC 签名)要不要在 /automation 匿名门槛上开一个精确到路由的口子,与自托管一致? #22757
Description
Ruled: 6105447950 · letter A · 2026-10-11T04:31Z
Filing gate ②:安全边界的放宽只能由维护者决定。分诊席(seat post #6015,session_01AavokzJ5DndAwitDXvKy4U)应 domain:services 席位的请求(#22564 6104496797)立卡。⛔ 不是认领。 Ruled A (6105447950): the hosted shape admits the route to the trigger's own HMAC check, exact POST row, 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 接受 |
| 同上,已登录调用方 | 404 |
由服务应答 |
- 原始请求体:托管侧原先拿不到原始字节,无法校验 HMAC。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(PR fix(runtime,hono): an exact /approvals/act dispatcher domain for kernels with no raw app, and a catch-all that leaves the raw request readable #22693,
26aa99981)已让 hono 兜底路由保留原始请求,这一前提已满足。 - 同类的另外两个成员:
- 审批邮件链接(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 落地,包括 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 把
/approvals/act精确路径加入 ADR-0069 鉴权放行表("仅令牌、与自托管一致"); - webhooks 重投(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 webhooks 段)不需要放行表条目。
- 审批邮件链接(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 落地,包括 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 把
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,所以按字面不在这条覆盖范围内。 - ADR-0069:鉴权门放行表(
packages/coreauth-gate.ts,isAuthGateAllowlisted)。 - ADR-0043:令牌式动作页(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 的先例)。
- 分诊此前两次表述不一致:
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 上公开更正前者。 - 协议声明:A 在 spec 增加一个可选成员(
Clause-②: yes),并在 ADR-0069 放行表加一条精确路径;B 不改放行表。
选项 × 客户可感知后果 × 开发工作量
| 选项 | 做什么 | 客户能感知的后果 | 开发工作量(按 #22438 已落地的同类段估) |
|---|---|---|---|
| A 与自托管一致 | 托管上只为 POST /automation/hooks/:flowName/:hookId 这一条路由,在匿名门槛上开精确到路由的口子:匿名请求放行到触发器自己的 HMAC 校验,签名不对照样拒绝。其余 /automation 路径不变 |
托管客户能直接接第三方入站 webhook,行为与自托管相同 | 3 段:spec 成员、运行时域加放行条目、插件成员。约 3 个 PR,1.2k–1.8k 行。先例:#22575 +105、#22577 +184、#22576(PR #22693)+1056、#22578 约 +500 |
| B 托管不开放 | 不改放行表。托管上这条路由改答带类型的"托管不支持"(501),文档写明入站签名 webhook 仅自托管可用 | 托管客户接不了第三方入站 webhook,只能借助外部中转 | 1 个 PR,约 200–400 行(类型化拒绝、文档、钉住测试) |
业务含义直译
- A: 大楼前台对快递员的规矩和分店一样:不用员工卡,但要核对快递单上的签名,签名不对就拒收。
- B: 总部前台一律不收没有员工卡的快递,分店照收。总部客户只能让快递先送到别处再转进来。
四轴(从业务看)
- ① 长期合理性: 入站签名 webhook 是自动化平台的基础能力。主流平台(如支付、代码托管平台的回调)都以签名而非会话鉴权。A 让托管与自托管一致,B 让同一能力在两种部署上表现不同。
- ② 实际业务拉动: 托管套餐在卖自动化;目前没有实测到具名客户在等这条路由。
- ③ 防 AI 犯错: B 下 AI 写的入站触发器在托管上会悄悄 401,除非类型化拒绝把原因说清楚。A 只有一种行为。
- ④ 创业阶段不扩散: B 最小;A 复用 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 的现成段形状,不发明新机制。
维护者速读
托管环境里,第三方系统带签名发来的入站 webhook,会被托管侧的匿名门槛先拦成 401,到不了签名校验;自托管则正常接收。A 方案只为这一条路由开口,校验仍靠签名,与自托管完全一致,约 3 个 PR;B 方案托管侧干脆不支持,明确回"托管不支持",约 1 个 PR。这条路由按字面不在你「A + 扫类」的范围内,而且是放宽安全门槛,所以请你定。
选 A 还是 B?
os-decision-facets
- ① 项目长远合理性:A 让托管与自托管一致,签名即鉴权;B 让同一能力因部署形态而不同。
- ② 实际业务拉动:托管自动化套餐;未实测到具名客户在等。
- ③ 防 AI 犯错:A 只有一种行为;B 需要类型化拒绝把原因说清,否则是静默 401。
- ④ 创业阶段不扩散:B 最小;A 复用 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 段形状,无新机制。
Prior rulings read: 6079645593(#22438「A + 扫类」)· #22577 放行表先例(ADR-0069 / ADR-0043)· 分诊 6092490077、6101050247 · 第一阶段验收 6093165613;thread: #22564 全部评论。
推荐:A,回退 B。
- 只看①选 A:签名鉴权的入站 webhook 是常见能力,托管与自托管应一致。
- ②③④ 是否翻转:否。④偏向 B,但 A 无新机制;③偏向 A。
- 自检:只看①选 A;②③④ 不翻转。
置信缺口:
- 是否有托管客户在等,未实测;
- 精确到路由的放行条目与签名校验之间是否还有其他中间件拦截,A 的运行时段需要实测。
裁后执行(你只需裁方向)
- 选 A: 分诊按 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 的形状立 3 张段卡(spec 成员
domain:spec;运行时域加精确放行条目domain:cli,后者同时触及packages/core的放行表,走跨域例外路径;插件成员domain:services),都挂Blocked-by:;放行条目限定精确路径、仅POST,鉴权等于自托管的 HMAC 校验,任何更宽的条目一律退回本卡。 - 选 B: 立 1 张
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 成员随之关闭。
Activity
objectstack-fleet commentedon Oct 11, 2026
Ruling: 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: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.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).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
objectstack-fleet commentedon Oct 11, 2026
Condition 3 of ruling
6105447950(replay protection), read by thedomain:servicesseat 1 (#6021,session_01CBAfsWMSfM3EToQGVStEcp) · 2026-10-11T04:43Z. ⛔ Not a claim.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.Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Triage: 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.domain:specpm:queue, goes firstdomain:cli,packages/coreby the cross-domain exception pathpm:blockedon #22772verifySignature, with the raw mount byte-unchangeddomain:servicespm:blockedon #22772security), ⛔ not folded. It adds a versioned timestamped scheme. A cutover that refuses body-only signatures goes back to the maintainer.security, and each restates its condition. ⛔ An allow-list row wider than the four-segmentPOSTreturns here.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.Blocked-by: #22772
Blocked-by: #22773
Blocked-by: #22774
objectstack-fleet commentedon Oct 11, 2026
Unlock scan: ruling A is executed. All three segments landed, so this decision closes
completedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T19:02Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6105447950,6105537097,6105672972.POST /automation/hooks/:flowName/:hookIdrow (conditions 1 and 2)fe58bbc60d)verifySignature75b6d6a048)completed. A cutover that refuses body-only signatures still goes back to the maintainer.POSTis a new decision, not a follow-up to this one.