Skip to content

ANONYMOUS_DENY_BODY 自称是「每个 seam 都返回的唯一 401 body 形状」,但 dispatcher 侧五个 seam 返回的是另一种 wrapper #5632

Description

@baozhoutao

发现于 #5570(PR #5631)的实跑取证 —— 该 PR 首次把四个 HTTP 面的匿名 401 body 在同一个 showcase 进程里并排取了下来。基线:origin/main 7e58212d1

Filed unassigned,无标签,待分诊席分域。不在 PR #5631 内修(那是 test-only 补证据单)。

事实

packages/core/src/security/anonymous-deny.ts:42 的注释:

/** The single 401 body shape every seam returns: `{ error, message }`. */
export const ANONYMOUS_DENY_BODY = {
  error: ANONYMOUS_DENY_CODE,
  message: ANONYMOUS_DENY_MESSAGE,
} as const;

「every seam」是假的。实测(showcase 真启动,匿名,同一进程):

owner 401 body
GET /meta @objectstack/rest enforceAuth {"error":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}
GET /data/... 同上 同上
POST /actions/... runtime domains/actions.ts {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}
POST /automation/.../trigger runtime domains/automation.ts 同上
GET /automation 同上 同上
DELETE /automation/... 同上 同上

只有 @objectstack/rest 消费 ANONYMOUS_DENY_BODY(rest-server.ts:1961 res.status(ANONYMOUS_DENY_STATUS).json(ANONYMOUS_DENY_BODY))。dispatcher 侧的 seam —— domains/ai.tsdomains/meta.tsdomains/security.ts,以及 PR #5569 新加的 domains/actions.tsdomains/automation.ts —— 走的都是 deps.error(ANONYMOUS_DENY_MESSAGE, ANONYMOUS_DENY_STATUS, { code: ANONYMOUS_DENY_CODE }),即 dispatcher 自己的 wrapper。

语义层面三者一致(401 / UNAUTHENTICATED / 同一条 message),分歧只在 wrapper

为什么这不只是注释问题

  1. 消费者要写两条读法。body.error.code 的客户端在 /data 上拿到 undefined(那里 error 是 CODE 字符串本身);读 body.error 当字符串的客户端在 /actions 上拿到一个对象。fix(runtime): deny anonymous callers on /actions and /automation (#5519) #5569 自己的集成用例就写了 body?.error?.code ?? body?.error?.details?.code —— 一条 ?? 容忍链,正是 AI 生成的元数据/客户端代码最容易把错误藏进去的形状。
  2. 注释会误导下一个作者(尤其是 AI)。 一个 as const 常量,docstring 明说自己是「every seam」的唯一形状,读者没有理由去逐个 seam 复核。PR test(dogfood): cover /actions and /automation in the anonymous-deny proof artifact (#5570) #5631 是复核了才发现的。
  3. 这是安全语义的出口。声明与实际不符,正是 Prime Directive chore: version packages #10 的形状,只不过发生在注释层

与既有工作的关系

处置建议(未预设,留给分诊)

按代价从小到大:

  1. 只修注释 —— 把 docstring 从「every seam」窄化为「the REST seam's shape」,并指向 dispatcher 那半。代价最小,消除误导,但 ?? 容忍链留在原地。
  2. 让 dispatcher 侧也发扁平信封(或反过来让 rest 发 wrapper)—— 真正收敛到一种形状。属于 breaking wire change,应挂在 envelope-convergence 线上统一决策,不宜由本条单独定。
  3. 加一条 conformance 用例,把「每个 seam 的 401 body ∈ 已声明的两种形状之一,且 code/message 一致」钉住,让第三种方言出现时 CI 红。

倾向 1 + 3:1 立即止损(注释是唯一在说谎的东西),3 防止分歧从两种扩成三种;2 的时机取决于 envelope-convergence 线何时排期,不该由本条抢跑。

PR #5631 已在 showcase-anonymous-deny-surfaces.dogfood.test.ts 里把这两种形状各按自身声明显式断言(刻意不写 ?? 跨族容忍读),并单独钉住 code/message 的一致性 —— 相当于把 3 的一个切片先落在了匿名面上;但那只覆盖匿名 401,不覆盖全部 seam 的全部错误。

关联:#5570、PR #5631、PR #5569#5519、ADR-0112(D5 + 2026-07-30 修正案)、#3843#4007


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions