Skip to content

fix(runtime): /share-links 的权限拒绝答 403 而不是 500 —— catch 走共享的 errorFromThrown (#6649) - #6718

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6649-sharelinks-status-passthrough
Aug 8, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-6649-sharelinks-status-passthrough

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6649

缺陷与前提复核

在 origin/main d6d1a50be 上实测复现,前提成立。

packages/runtime/src/domains/share-links.ts 的统一 catch 只读一条状态通道:

return sendErr(err?.status ?? 500, err?.code ?? 'INTERNAL', err?.message ?? '…');

ShareLinkService 自己抛的每个拒绝都带 status(makeError 同时设 status + code),所以 403 FORBIDDEN / 422 SHARING_NOT_ENABLED 一直是对的——但安全中间件的拒绝不出自那个 service。建链要做可见性读(svc.createLink 调 engine.find(object, { context })),当调用者的 permission set 对该对象没有 allowRead 时,CRUD 门 throw 的是 PermissionDeniedError { code = 'PERMISSION_DENIED'; statusCode = 403 }——完全没有 status 字段(packages/plugins/plugin-security/src/errors.ts;runtime 自己的镜像类 security/resolve-execution-context.ts 同形)。ShareLinkService 不接它,冒到这个 catch,err?.status 为 undefined,于是一个 403 类拒绝以 HTTP 500 出门,而 code 却如实读到 PERMISSION_DENIED。

信封自相矛盾,且这个矛盾对客户端是有后果的:5xx 对许多 SDK 与浏览器客户端是可重试的,于是一个永久性的授权答复被反复重试;按状态分支的调用者看到的是"服务器坏了",真相是"你无权读这条记录"。两种 tenancy 姿态都能复现。而且因为 registerShareLinkRoutes: false 让这个 domain 成为 cloud 每环境 kernel 上唯一的 share-link 表面,它在那里是主表面而非兜底。

前提测量(修前 / 修后)

用无 allowRead 的调用者 POST /api/v1/share-links:

HTTP status error.code
修前(origin/main) 500 PERMISSION_DENIED
修后 403 PERMISSION_DENIED

一处被证伪的子论断

issue 还称 5xx 路径上 looksLikeInternalErrorLeak 会把 [Security] Access denied… 消息脱敏掉。实测不成立:packages/types/src/error-leak.ts 的启发式只匹配 sqlite_ / sqlstate / 以 select |insert into |update |delete from 开头 / constraint failed / unique constraint / foreign key,安全消息一条都不命中。主论断(500 而非 403)成立,脱敏这半条不成立——照实记录,没有据此写断言。

方向:走共享映射器,而不是加长 ?? 链

catch 改为出口到 deps.errorFromThrown——dispatcher 已有的共享 thrown-error 映射器,/meta、/actions、/mcp 早已走它。它同时读 status 与 statusCode,并把 thrown error 的结构化 issues / fields 一并带出,而不是塌成一条 message。

选它而不是最小改法 err?.status ?? err?.statusCode ?? 500,理由是这张卡本身就是手写 catch 与共享映射器分叉的产物——再写一份手写副本,正是当初分叉的方式。errorFromThrown 已在 DomainHandlerDeps 契约上(domain-handler-registry.ts),所以本改动没有碰 http-dispatcher.ts。

顺带修掉的第二条通道:'INTERNAL' 这个兜底 code 从未为 @objectstack/runtime 在 ERROR_CODE_LEDGER 注册过(注册它的是 rest / service-storage / service-i18n / plugin-sharing;ledger 的分包行是 provenance,全局并集因此让 ApiErrorSchema 一直是绿的)。共享映射器把这个必填字段交给 standardErrorCodeForHttpStatus,拼出在册的 INTERNAL_ERROR(ADR-0112)——与其它每个 dispatcher 出口一致。

测试:复用 #6551 的架子,不另起第三套

扩展 PR #6647 落地的 packages/runtime/src/domains/share-links-enforcement-context.test.ts(真 handleShareLinksRequest + 真 ShareLinkService + 真 SecurityPlugin 中间件,只有存储是替身)。改动限于:把 permission-set 世界与 additive baseline 提成 bootSecurity / mintOnDispatcher 的带默认值参数(默认逐字等于 #6551 各用例原来的取值),加一个无 allowRead 的 permission set,追加一个 [#6649] describe 块。#6551 的 7 个用例正文一字未改,全程绿。

deps.errorFromThrown 在架子里不是手写替身,而是照 error-envelope.conformance.test.ts 的 makeDispatcher() 的做法,从一个真 HttpDispatcher 实例上借来的真方法——手写一份 e?.status ?? e?.statusCode ?? 500 会让用例绿在替身的规则上而不是生产的规则上,那正是本 issue 所属的错误类别。

每个用例都同时断言 status 与 code:修前 code 就已经是 PERMISSION_DENIED,所以只断言 code 的用例在缺陷上是绿的;只断言"不再是 500"又分不出"拒绝对了"和"拒绝错了"。信封另经 BaseResponseSchema / ApiErrorSchema / envelopeViolations 解析(ADR-0112 闭集)。

反向验证:先写预测,再跑

预测先于运行写入 scratchpad。把 share-links.ts 单独回滚到 origin/main、保留新用例后实测:

新用例 预测 实测
single 姿态,无 allowRead RED,status 500 ≠ 403 RED,expected 500 to be 403 ✓
group 姿态,无 allowRead RED,status 500 ≠ 403 RED,expected 500 to be 403 ✓
共享 catch:GET + DELETE,真 PermissionDeniedError RED,GET 迭代先失败 RED,GET status: expected 500 to be 403 ✓
两条通道都不带的 throw RED,code INTERNAL ≠ INTERNAL_ERROR RED,expected 'INTERNAL' to be 'INTERNAL_ERROR' ✓
带 status 422 + SHARING_NOT_ENABLED 的 throw 两个方向都绿,构造使然 两个方向都绿 ✓

预测 5 个新用例 RED 4 个,实测 RED 4 个,方向与失败通道逐条吻合。

照实排除:第 5 个用例两个方向都绿,不是 #6649 的回归 pin——status 通道旧链与共享映射器都在同一条首分支上读。保留它是为下一次改这个出口的人:若 status 通道被换成只读 statusCode,它会红。用例正文里就是这么写的,没有把它算进反向验证的红计数。

另:修前那次运行同时是全包基线——1701 个用例里只有我新加的 4 个红,#6551 的 7 个用例与其余 1690 个全绿。

门禁实测输出

pnpm --filter @objectstack/runtime test
  Test Files  114 passed (114)
       Tests  1701 passed (1701)

pnpm --filter @objectstack/runtime typecheck   → tsc --noEmit,无输出
pnpm lint                                       → eslint . --no-inline-config,无输出

check:route-envelope          exit=0
check:error-code-casing       exit=0
check:nul-bytes               OK (scanned 6236 tracked text file(s); no raw ASCII control bytes)
check:slot-lookup             exit=0
check:engine-double-contract  OK — 98 pinned, 133 in the DEBT ledger, 2 exempt
check:wildcard-fallthrough    exit=0
check:verify-stand-in         OK — 2 guarded, 5 exempt, 10 call sites, 0 asserted driver arguments
check:query-options-erasure   exit=0
check:type-check-debt         OK — 62/77 packages type-checked, 15 in DEBT (457 frozen raw errors)

DEBT / TEST_DEBT 未上升(457 与 main 同值)。

changeset

@objectstack/runtime: minor。判断依据是本仓已有先例——action-crash-vs-rejection.md("an action that CRASHED is a 500, not a 200")与 actions-global-key-and-failure-status.md 都是 wire 上的 HTTP 状态变化,都记 minor。本改动同类:拒绝从 500 变 403(对客户端是"可重试"变"不可重试"),且 codeless throw 的 error.code 从 INTERNAL 变 INTERNAL_ERROR,两者都是 SDK 可见的。

范围

只改 packages/runtime/src/domains/share-links.ts + 该测试文件 + changeset。读了 http-dispatcher.ts,未改——errorFromThrown 不需要任何改动即可服务这个调用点。content/docs/releases/ 未触碰。#5582 的 rest 侧 mapDataError 同族兄弟不在本 PR 范围内。


Generated by Claude Code

…n catch (#6649)

Route the domain's unified catch through the dispatcher's shared
`errorFromThrown` mapper, which reads `status` OR `statusCode`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 1:28pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 13:28
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant