feat(runtime): 端点策略键 —— authRequired / rateLimit / cacheTtl 接线(#5040 E4) - #5135
Merged
Conversation
…eTtl (#5091) Wire the three policy keys `ApiEndpointSchema` declares, in the order #5040 §3 fixes (rateLimit → authRequired → cacheTtl), reusing existing primitives only: - `authRequired` → `shouldDenyAnonymous` + the `ANONYMOUS_DENY_*` constants, so a declared endpoint answers the same 401 as `/meta`, `/ai` and `/security`. The key arrives materialized (schema default `true`), so there is no "omitted" state a consumer could read differently. - `rateLimit` → #5006's `deriveBucketConfig` / `resolveRateLimitKey` / `SharedTokenBucketLimiter` over the shared counter store, keyed `apiep:<name>:<principal|ip>` so the endpoint budget and the server-level budget are independent rather than one budget counted twice. Over limit answers the server limiter's own 429 body plus `Retry-After`. - `cacheTtl` → response-header semantics only (#5091 ruled out a server-side cache): `private, max-age=<ttl>` for a positive ttl, `no-store` for 0 or negative, nothing when absent, nothing + a warn on a non-GET endpoint. Metering runs BEFORE the auth gate on purpose: credential stuffing is anonymous traffic against an `authRequired` endpoint, and gating first would let a scanner make unlimited attempts none of which ever reach the meter. The dispatch step runs the chain between the match and its 501, and target execution can only land on the far side of the chain — the branch is unreachable without a policy context, so wiring an executor without wiring policies is not something a later change can do by forgetting. Structurally unreachable and zero live behavior change: a non-empty `apis:` is still rejected at publish until the #5040 E7 flip, and the step's answer without a policy context is byte-identical to today's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
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:
|
This was referenced Aug 4, 2026
…teral (#5091) 分隔符误写为 \x00:git 判文件为二进制,ESLint / check:nul-bytes 红。 替换为空格,registry 内部键行为等价。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
os-zhuang
marked this pull request as ready for review
August 4, 2026 05:58
os-zhuang
enabled auto-merge
August 4, 2026 05:58
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Aug 4, 2026
# Conflicts: # packages/runtime/src/error-envelope.conformance.test.ts
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 #5091
Part-of #5040(E 系列第 4 单)。前置 E2(
ecc61ab)/ E3(2649ccb)已在 main。做了什么
新增
packages/runtime/src/endpoint-policy.ts——ApiEndpointSchema三个策略键的唯一读取方;api-endpoint-step.ts在「匹配命中」与「答复」之间插入策略链。三键全部复用既有原语,零发明:authRequiredshouldDenyAnonymous+ANONYMOUS_DENY_*常量/meta、/ai、/security(#2567、#3963)rateLimitderiveBucketConfig/resolveRateLimitKey/SharedTokenBucketLimitercacheTtlCache-Control响应头,仅此而已链序:先限流、后鉴权(#5040 §3)
按设计原文执行,并把理由写进代码:最需要计量的流量(撞库、令牌喷洒、爬取)恰恰是会被答 401 的那批;先鉴权后限流等于让扫号者的每次尝试都绕开计量。因此被拒的请求确实消耗令牌 —— 这是预算的用途,不是它的漏洞。解析「谁在调用」与「据此设卡」是两件事:principal 先查一次(它给桶做键),再计量,再设卡,一次查询两处使用。
三键的具体裁断
authRequired:值到达时已由 schema 物化(缺省即true),执行器读不到「未声明」这个中间态 —— 漏写得到安全默认,authRequired: false是唯一开门方式且在 diff 里可 review。shouldDenyAnonymous:它的可选 path 参数会豁免控制面路径,而isAuthGateAllowlisted把任何含/auth/段的路径都算作控制面。若传 path,应用只要把端点声明成/api/v1/apps/{ns}/auth/callback就能豁免自己的authRequired: true—— 一个「声明期即可自助绕过平台默认拒绝」的口子。不传 = 声明路径根本够不到豁免。已配测试钉死。rateLimit:桶键apiep:{端点名}:{principal 或 ip}。命名空间前缀是两份预算互相独立的原因(server 级是全局底线,端点级是业务配额),共用键空间会让较小的那份被悄悄再砍一半。超限答与 server 级限流器逐字节相同的 429(同 message、同details、同派生 code)+Retry-After。armed 但不可用(maxRequests/windowMs≤ 0)→ fail closed,报错点名端点。cacheTtl:仅响应头语义(E4(#5040 执行器):策略键接线 ——authRequired/rateLimit/cacheTtl全部复用既有原语,零新执行语义 #5091 已裁掉服务端缓存 —— 缓存需要失效故事,给一个只有四个词的词表发明失效语义正是「运行时方言」的起点)。正值 →private, max-age={秒数};private是安全规则不是调优项,authRequired: false的端点同样适用(任何响应都可能是按主体裁剪过的,共享缓存永远不该存);0/负值 →no-store(作者写了0就是说了话,让它等价于沉默正是本程序要消灭的静默 no-op);缺省 → 不发头;非 GET → 不发头 + warn 点名(GET-only 见设计 §3.3,publish 门在 E7)。501 与头的关系
Cache-Control描述的是尚不存在的成功响应体(执行是 E5),让客户端缓存一个 501 比什么都不说更糟 —— 所以它留在策略裁决对象上(verdict.responseHeaders)供执行器接线时应用,并在单测里直接断言这一暴露;Retry-After则是拒绝本身的一部分,随答复走(AppEndpointStepAnswer.headers)。结构上防「接了执行、忘了策略」
执行落点在策略链之后的分支里,而该分支在没有策略上下文时不可达 —— 后续接线要加执行调用,没有别的地方可放。
未接的一段(留给 #5129,已在该单评论里交接)
按 R3 文件面切分,本单不碰
dispatcher-plugin.ts,因此派发步目前尚未拿到策略上下文(headers / remoteAddress / principal 解析 / limiter 注册表),answer.headers也还没被兜底器写出。这两处正是 #5129(E5b 接线收拢)的范围,交接契约已评论在该单。今日不构成任何缺口:没有策略上下文时的终态答复仍是 501 —— 没有任何请求会被未经策略地服务。结构性不可达 / 零现网行为变更
非空
apis:在 publish/validate 仍被硬拒(E7 翻转前);派发步在未获得策略上下文时的答复与此前逐字节相同 —— E3 的全部否定断言(未命中、前缀外、405 +Allow、裸 404)原样通过。验证
新测试:
endpoint-policy.test.ts(逐键 命中/未命中/边界:authRequiredtrue-无会话 / true-有会话 / false、/auth/自助豁免防线、限流 未到/到点/超限 +Retry-After值 + 端点间与调用者间桶隔离 + 转发头须trustProxy+ 计量先于鉴权 + 计数存储故障的有界 fail-open + 零预算 fail-closed、cacheTtl有/无/0/负/小数/非 GET)+api-endpoint-step.test.ts的链路接线用例(拒绝短路不出 501、429 带头、通过才到 501、501 不带Cache-Control)。endpoint-policy.ts已按 E3 加api-endpoint-step.ts的同样方式加入 error-envelope conformance 的 MODULES 扫描清单。🤖 Generated with Claude Code
https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd