Repository navigation
Description
⚠️ 本 issue 已订正(见下方评论)。 初版标题与建议是"零非测试消费者,默认翻false" —— 错的:我当时查的是"谁传这个选项",没查"谁依赖这个默认值"。os serve正是靠默认值拿到这个面,且其中三个端点没有第二个提供者。照初版执行会打断 console。下文已按证据重写。
按 Prime Directive #10 记录,#4018(静态 discovery 按注册计算,#4063 已合并)实施中发现。#4018 只收口了 discovery 那一块。
真实现象
HonoPluginOptions.registerStandardEndpoints(默认 true)用一个开关同时控制两类性质完全不同的路由:
| 路由 | 性质 | 谁还提供 |
|---|---|---|
POST/GET /api/v1/data/:object、GET /data/:object/:id(只有 C+R) |
重复供给 | @objectstack/rest 的 /data(完整 CRUD + 全部闸门),且因先注册先赢而实际生效 |
GET /api/v1/discovery、/.well-known/objectstack |
重复供给 | dispatcher / REST(#4018 后已显式 cede) |
GET /api/v1/auth/me/permissions |
独家供给 | 无 |
GET /api/v1/auth/me/localization |
独家供给 | 无 |
GET /api/v1/me/apps |
独家供给 | 无 |
三条独家供给的证据:
os serve靠默认值拿到它们 ——packages/cli/src/commands/serve.ts:1057是new HonoServerPlugin({ port }),不传这个选项。- console 硬依赖其中两条 —— objectui
packages/permissions/src/MePermissionsProvider.tsx:72的DEFAULT_ENDPOINT = '/api/v1/auth/me/permissions'(整套PermissionProvider/ObjectForm/RecordDetailView/ 相关列表的apiOperations都建在这份 payload 上);apps/console/src/AppContent.tsx:121取/api/v1/auth/me/localization。 - core 把它们当一等公民 ——
packages/core/src/security/auth-gate.ts:30的ALLOW_SUFFIXES含/me/apps、/me/localization,注释写明是"被门禁拦住的用户必须仍能访问,用以补救或引导补救 UI"。 - grep 确认
packages/rest、packages/runtime都不挂任何/me/*。
所以默认值不能直接翻 false —— 那会让 os serve 上的 console 权限层与本地化整个断掉。
仍然成立的部分
「重复供给」那一半的论据不变,而且是这个面反复付税的根源 —— 每条平台级不变量都要在这里再实现一遍,且每次都是事后补的:
- [security] Anonymous posture inconsistent across surfaces: dispatcher /graphql and raw hono /data bypass requireAuth (/meta now gated) #2567:raw
/data直连 ObjectQL,匿名拒绝原本只在 REST 先注册时才生效 —— 安全姿态取决于加载顺序,于是单独补了一遍shouldDenyAnonymous闸门; - discovery 广播「跨对象原子 batch」能力位(让客户端声明式协商,取代 404/405/501 运行时探测) #3298:
transactionalBatch要在这里单独如实申报false; - plugin-hono-server 的 registerStandardEndpoints 静态 discovery 硬编码整张路由表 —— 与 D12 "按注册计算" 的两个真 discovery 面不一致 #4018:discovery 的 D12 诚实性又补一遍。
修法:先拆,才谈退役
前置步骤(本 issue 的核心):把三条 /me/* 从 registerStandardEndpoints 门里挪出去,让这个开关名副其实地只覆盖重复供给。两条路线:
- A. 就地拆分 —— 在 plugin-hono-server 内部改为无条件注册(需把
registerDiscoveryAndCrudEndpoints里的resolveCtx/getObjectQL/denyAnonymous提成共享 helper)。小、安全、对os serve行为不变。 - B. 归还真正的 owner ——
/auth/me/*归 plugin-auth(它已经拥有/api/v1/auth/*),/me/apps归 REST 或 dispatcher。契合 ADR-0076 D11「单一 owner」,但跨插件,且/me/permissions带着约 195 行权限合并逻辑(含foldWildcardSuperUser)与 objectql/spec 依赖。
拆完之后才是原计划:默认值翻 false → 观察一个 release → 删除 registerDiscoveryAndCrudEndpoints 余下部分。
顺带发现:/auth/me/* 目前是靠加载顺序活着的
plugin-auth 在 kernel:ready 里注册 rawApp.all('${basePath}/*')(auth-plugin.ts:1817),终结式转发给 better-auth、不会 next();hono 的 /auth/me/permissions 也在 kernel:ready 注册。两者谁先,取决于 kernel.use() 顺序(今天是 HonoServerPlugin 先于 AuthPlugin,所以 hono 的具体路由先注册、赢下匹配)。一旦顺序调换,/auth/me/permissions 会落进 better-auth 的 catch-all 拿到 404。
这与 #2567、#4018 是同一类「靠顺序侥幸成立」的问题 —— 也是选 B 的最强论据:让 auth 拥有 /auth/* 下的全部路由,这个碰撞根本不存在。
顺带:三个死常量
hono-plugin.ts 的 DEFAULT_ENDPOINT_PRIORITY = 100 / CORE_ENDPOINT_PRIORITY = 950 / DISCOVERY_ENDPOINT_PRIORITY = 900 声明后全仓零引用。DISCOVERY_ENDPOINT_PRIORITY 尤其误导:它暗示存在一套 discovery 优先级机制,而真实机制是 Hono 的先注册先赢。
相邻的、可独立处理的缝
该面的 discovery payload 是 { version, apiName, routes, capabilities },不满足 DiscoverySchema(缺 name / environment / locale / services)。services 恰是 D12 定的 single source of truth(handlerReady),standalone 客户端因此读不到 D12 的那一半信息。走完退役路线后这条自动消失。
Activity
[-]plugin-hono-server 的 registerStandardEndpoints 便捷面:默认 true、零非测试消费者,且每条不变量都要在这里补第二遍 —— 建议退役[/-][+]plugin-hono-server 的 registerStandardEndpoints 把「重复供给」和「独家供给」绑在同一个开关上 —— 拆开是退役的前置条件[/+]os-zhuang commentedon Jul 30, 2026
订正记录(标题与正文已按下述证据重写)。
初版的核心论断是"
registerStandardEndpoints零非测试消费者 → 默认翻false"。这是错的,而且照它执行会打断生产:我当时 grep 的是"谁传这个选项"(结论没错:只有 4 个测试传),却没查"谁依赖这个默认值"。准备动手实施时查到的:
packages/cli/src/commands/serve.ts:1057——new HonoServerPlugin({ port }),不传该选项。os serve走的正是默认true。packages/rest、packages/runtime都不挂任何/me/*路由 ——/auth/me/permissions、/auth/me/localization、/me/apps只有这个块提供。packages/permissions/src/MePermissionsProvider.tsx:72的DEFAULT_ENDPOINT = '/api/v1/auth/me/permissions',apps/console/src/AppContent.tsx:121的/api/v1/auth/me/localization。packages/core/src/security/auth-gate.ts:30把/me/apps、/me/localization放进ALLOW_SUFFIXES,注释是"被门禁拦住的用户必须仍能访问"。即:这个开关把重复供给(
/dataC+R、discovery)和独家供给(三条/me/*)绑在了一起,而后者是 console 权限层与本地化的唯一来源。"退役"因此不是翻个默认值,前置条件是先把两类拆开。顺带查到一条相邻的隐患(已写进正文):
/auth/me/*落在 plugin-auth 的终结式 catch-allrawApp.all('/api/v1/auth/*')覆盖范围内,两者都在kernel:ready注册,谁赢取决于kernel.use()顺序。今天 HonoServerPlugin 先于 AuthPlugin 所以正常,但这是"靠顺序侥幸成立",与 #2567 / #4018 同类。Generated by Claude Code
fix(plugin-hono-server): stop gating the current-user endpoints behin…
os-zhuang commentedon Jul 30, 2026
前置步骤(路线 A:就地拆分)已随 #4079 合并(
43ff598)。三条 current-user 端点现在无条件注册,走自己的
kernel:readyhook 且排在被门控那块之前;registerStandardEndpoints从此只覆盖重复供给 —— 也就是它的名字和文档一直声称的那一半。顺带删了三个零引用的*_ENDPOINT_PRIORITY常量。CI 16 项全绿,含两个 Dogfood Regression Gate(对"
os serve挂哪些路由"的改动,这是最实的验证)。本 issue 保持开启,余下两步现在才是安全的:
registerStandardEndpoints默认值true→false(附 FROM → TO changeset;对os serve而言这一步只移除重复供给的/dataC+R 与被 cede 的 discovery);registerDiscoveryAndCrudEndpoints余下部分,届时那条DiscoverySchema不符合(缺name/environment/locale/services)自动消失。另外,实施中发现的那条加载顺序隐患(
/auth/me/*落在 plugin-auth 终结式 catch-all 覆盖范围内,靠kernel.use()顺序才赢下匹配)在本次拆分后依然存在 —— #4079 的测试只钉住了 hono 插件内部的注册顺序,跨插件的 hono-先于-auth 没有任何东西保证。它比余下两步更值得优先处理,建议单独立项。Generated by Claude Code
feat(plugin-hono-server): export the current-user endpoint registrar …
os-zhuang commentedon Jul 30, 2026
前置拆分已完成,但下一步(翻默认值)不能照计划走 —— 有实测证据
前置步骤已经做完了:#4144 采用了路线 A(就地拆分),三条
/me/*现在无条件注册,hono-plugin.ts:595的注释直接引了本 issue。所以按计划,下一步就是翻默认值。我去做这一步,先验证它是不是计划所说的 no-op。不是。 证据见 #4192。
实测矩阵
真实
HonoServerPlugin+ 真实 dispatcher + 真实createRestApiPlugin,真实监听端口,按serve.ts的注册顺序:GET /api/v1/data/account这个面的两半,性质完全不同
/discovery+/.well-known/objectstack—— 结论成立,可以退役。它们靠
registerDiscoveryEndpoints里的kernel.hasPlugin(rest|dispatcher)显式 cede(#4018)。这是一个与注册顺序无关的机制,所以无论 flag 开关,应答的都是 dispatcher 计算出的 payload。两个方向都验证过。/data/:object—— 结论不成立。它没有 cede 判断。issue 正文里对它的论断是「
@objectstack/rest的/data……因先注册先赢而实际生效」,hono-plugin.ts:657的注释也这么写("both of those register during pluginstart()— i.e. before thiskernel:readyhook — so they already shadowed this handler")。真实监听器上不是这样。翻掉默认值不会把这条路径交给 REST,而是让它变成 404。
这是同一类错误的第二次出现
本 issue 顶部已经订正过一次:
这次是同构的下一层:查的是"谁声称提供这条路径",没查"真实监听器上谁应答"。 cede 机制(显式、可验证)和先注册先赢(隐式、依赖生命周期顺序)看起来都像"被遮蔽了",但只有前者真的成立。
建议的修订路线
/discovery那半已经是显式 cede,实际上已经退役了 —— flag 对它没有影响。可以先把它从registerDiscoveryAndCrudEndpoints里摘出去,让这个 flag 名副其实地只剩/data(与本 issue 当初把/me/*摘出去是同一个动作)。/data的真问题:完整供给的 REST(有 metadata/protocol 服务、真实配置)到底挂不挂/api/v1/data/:object?test(runtime): 把「谁在服务 /data 与 /discovery」钉成事实,用证据挡住 #4073 的默认值翻转 #4192 的 harness 里 REST 只配了最小服务集,所以它证明的是**「flag 关掉后这些装配里没有东西服务/data」**,不是「REST 从不服务/data」。这个区别决定了下一步是"翻默认值"还是"先让 REST 真正接管"。/data的两条断言失败时会直接提示该回头重读本 issue。顺带确认仍然成立的部分
正文里"这个面反复付税"的论据没有被动摇 —— #2567、#3298、#4018 三次都是在这里再实现一遍平台级不变量。
/data那条 401 正是 #2567 补的闸门在生效。退役方向是对的,只是不能用"翻默认值"这一步走完。关联:#4192(本条的证据)、#4144(前置拆分,已合并)、#4018、#2567。
Generated by Claude Code
test(runtime): pin who serves /data and /discovery, blocking the #4073 …
os-zhuang commentedon Jul 31, 2026
撤回我上一条的阻塞结论 ——
/data那一半我搞错了我在上一条评论里说「
/data的前提不成立,翻默认值会让它变 404」,并用 #4192 把这个结论固化成了测试。那是错的。 订正见 #4260。错在哪
#4192 的 harness 用
createRestApiPlugin({})配一个 stub 的objectql服务。REST 的 CRUD 是从对象注册表生成的 —— 需要真实引擎(driver + 已注册对象)以及自己的api.api配置。供给不全时它什么都不挂。那个 404 是关于 harness 的事实,不是关于 REST 的。而且证据一直在仓库里:
packages/client/src/client.environment-scoping.test.ts就是用registerStandardEndpoints: false启动,断言GET /api/v1/data/task返回 200 由 REST 服务,注释写明这正是重点。我当时没找到它。按同样方式供给后
响应逐字节相同。 两半都是 duplicate supply,翻默认值对任何挂了 REST 或 dispatcher 的宿主都是 no-op —— 所有 composed 部署,含
os serve。还有一处细节值得记:
/data/task返回的 404 是OBJECT_NOT_FOUND,那是引擎的回答(路由通的);路由未命中会是{"error":"Not found"}。我上次把这两种 404 当成了同一件事。 新测试因此改为直接比对两个位置的响应,不再依赖状态码。所以这个 issue 现在的状态
/me/*移出 flagtrue→falseregisterDiscoveryAndCrudEndpoints翻之前唯一需要决定的事
对 composed 宿主是 no-op(已证)。但对只挂 HonoServerPlugin、既无 REST 也无 dispatcher 的裸宿主,翻默认值会真的拿走
/data与/discovery—— 而正文写得很清楚,"The flag exists for a bare host that mounts neither"。所以这不是技术问题而是 API 决策:裸宿主应该继续默认拿到这个便利面,还是应该显式 opt-in? 我没有替这个 issue 做这个决定 —— 需要一个 major/minor changeset 和明确的 FROM → TO。定了方向我可以直接实施。
三次答错同一个问题
这个 issue 现在有一条挺完整的记录,值得留着:
「谁在服务这条路径」始终是一个关于 composed 且 provisioned 的运行时的问题。 能 boot 的 harness ≠ 能回答这个问题的 harness。
Generated by Claude Code
test(runtime): correct the #4073 evidence — the flip is a no-op for a…
feat(plugin-hono-server): registerStandardEndpoints defaults to false…
os-zhuang commentedon Jul 31, 2026
第 1 步已落地:默认值翻
false(#4280,623e555)决策:裸宿主改为显式 opt-in —— 从平台长期合理性(D11 单一 owner、这个面三次重复付税 #2567/#3298/#4018、次等供给不该默认在场)与降低 agent 出错率(grep 真相 = 运行时真相、显式组合优于默认魔法、缺席要响亮)两个角度定的,权衡记录在 #4280。
落地内容:
true→false;flag 标@deprecated,JSDoc 写明终态与迁移路径;false测试的便车;os serve、objectstack dev均 composed(test(runtime): 订正 #4073 的证据 —— 翻默认值对 composed 宿主是 no-op(推翻我在 #4192 的结论) #4260 已证 no-op);三个测遗留面本身的套件改显式true;/me/*不受影响(feat(plugin-hono-server): 导出 current-user 端点注册面,裸适配器宿主也能供给这三条 (cloud#924) #4144);authentication.mdx的最小组装恰是裸宿主形状、同页又 fetch/api/v1/data/task—— 已按迁移路径改为挂createRestApiPlugin,而不是教读者废弃 flag。验证:plugin-hono-server 155/155(含新 pin)、runtime 挂 hono 套件 23/23、client 16/16、http-conformance 46/46、CI 17/17。
剩余步骤
/me/*出门)falseregisterDiscoveryAndCrudEndpoints+ flag + 三个死优先级常量Generated by Claude Code
docs(authorization): drop the retired raw-hono /data surface from the #…
feat(plugin-hono-server)!: delete the CRUD/discovery surface — the pl…
os-zhuang commentedon Jul 31, 2026
退役完成 —— #4296(
e5a4d26)已合并,本 issue 关闭HonoServerPlugin现在只拥有 socket、中间件和三条 current-user 端点。数据面与 discovery 各自单一 owner(ADR-0076 D11)。净 −678 行。四步全部落地
/me/*移出 flag(路线 A)false+ boot 提示 + pin 测试顺带清掉了正文里点名的三个死优先级常量,以及 discovery payload 不满足
DiscoverySchema那条缝(随退役自动消失)。「/auth/me/*靠加载顺序活着」那条也已由 #4088 独立修好(catch-all 改为可穿透)。三道门禁抓出了删除欠下的账
这部分比删除本身更值得记 —— 每一处都是被工具抓出来的,不是预先想到的:
HIGH_RISK—— 但 ratchet 自己的反向测试拿这一行当 fixture。若只删台账行,那条测试会空转通过,等于把刚刚抓到问题的检查拆掉引信。已改指一个存活的行。authorization.mdx—— 它把已删的面两次列为一条安全不变量的组成部分。不改的话,权限文档会承诺一道不存在的门。改法沿用该句已有的写法:那个面离开了矩阵,不变量本身没变,只是少了一扇要关的门。AGENTS.md
新增 §「Route & surface ownership」四条规则:一条路由一个 owner · 显式组合优于默认魔法 · 缺席要响亮 · 机器可读面不许撒谎。
外加一条验证方法,是这条线最贵的一课:「谁在服务这条路径」是关于 composed 且 provisioned 运行时的问题 —— 不是关于谁声明了它、不是关于注册顺序、也不是关于一个能启动的最小 harness。本 issue 被答错三次,每次正好对应绕过其中一条:
Generated by Claude Code
fix(core,hono): retire /graphql residue — stale surface list and iner…
fix(core,hono): retire /graphql residue — stale surface list and iner…