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

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions