fix(runtime): /share-links 的权限拒绝答 403 而不是 500 —— catch 走共享的 errorFromThrown (#6649) - #6718
Merged
os-project-manager merged 1 commit intoAug 8, 2026
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-project-manager
marked this pull request as ready for review
August 8, 2026 13:28
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-project-manager
enabled auto-merge
August 8, 2026 13:28
os-project-manager
deleted the
claude/issue-6649-sharelinks-status-passthrough
branch
August 8, 2026 14:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6649
缺陷与前提复核
在
origin/maind6d1a50be上实测复现,前提成立。packages/runtime/src/domains/share-links.ts的统一 catch 只读一条状态通道:ShareLinkService自己抛的每个拒绝都带status(makeError同时设status+code),所以 403FORBIDDEN/ 422SHARING_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:error.codeorigin/main)PERMISSION_DENIEDPERMISSION_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、保留新用例后实测:allowReadstatus500 ≠ 403expected 500 to be 403✓allowReadstatus500 ≠ 403expected 500 to be 403✓PermissionDeniedErrorGET status: expected 500 to be 403✓codeINTERNAL≠INTERNAL_ERRORexpected 'INTERNAL' to be 'INTERNAL_ERROR'✓status422 +SHARING_NOT_ENABLED的 throw预测 5 个新用例 RED 4 个,实测 RED 4 个,方向与失败通道逐条吻合。
照实排除:第 5 个用例两个方向都绿,不是 #6649 的回归 pin——
status通道旧链与共享映射器都在同一条首分支上读。保留它是为下一次改这个出口的人:若status通道被换成只读statusCode,它会红。用例正文里就是这么写的,没有把它算进反向验证的红计数。另:修前那次运行同时是全包基线——1701 个用例里只有我新加的 4 个红,#6551 的 7 个用例与其余 1690 个全绿。
门禁实测输出
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