Repository navigation
feat(service-storage): 无 http-server 的 kernel 上把 /api/v1/storage/* 挂到宿主 dispatcher —— 多租户宿主上附件上传整条链路无人应答 #17354
Description
Activity
Closing as a duplicate of #15169 — the work described here landed two days before this card was filed. PM
session_01FGca3Gt5irhBoVcVwFfoVY, 2026-09-10T08:5xZ.dd2184a—feat(service-storage): mountStorageRoutes — one host door for kernels with no http-server service (#16741), merged 2026-09-08T05:56:03Z, closing #15169 (state_reason: completed, closed byos-zhuang2026-09-08T06:16:49Z).mountStorageRoutesis exported atpackages/services/service-storage/src/index.ts:30onorigin/main.It also answers, better, the two points this card flagged as unresolved:
- The download-authorization seam. This card asked who supplies
authorizeFileRead/resolveFileHolderon the no-http-serverpath and leaned "framework side". feat(service-storage): mountStorageRoutes — one host door for kernels with nohttp-serverservice #16741 binds all three seams (session resolver, ADR-0104 D3 download authorizer, tombstone holder predicate) inside the package, through the samecomposeStorageRoutesthe plugin's ownkernel:readymount calls — one composition, two callers. The host gets the door and never a handle on a gate;MountStorageRoutesOptionscarries wire knobs only, and naming one of the three option keys is a type error. - How a plugin reaches the dispatcher. This card framed the choice as (a) make the dispatcher resolvable as a service vs (b) a
runtime/src/domains/storage.ts, and recorded that (b) would invertruntime → service-storage. Both are moot: feat(service-storage): mountStorageRoutes — one host door for kernels with nohttp-serverservice #16741 took neither. It is a package-exportedmountStorageRoutes(hostHttpServer, kernel)the host calls directly — the same shape as the settings bridge, which its docblock names as the working precedent. NoregisterDomainHandler, no new dependency edge, no/storagedispatcher domain, so the dispatcher 的 /storage/upload 用 upload(file, {request}) 调用契约里的 upload(key, data, options?) —— 对任何实现都会 TypeError #4087 warning inroute-ledger.tsis not engaged at all.
How this card came to be filed. The PM checkout it was written from is pinned at
8b37a09(2026-09-08T03:59Z) and is 288 commits behindorigin/main; #16741 merged ~2 hours after that pin. Every source reading in this card is accurate for that tree and stale for today's. It surfaced on the mandatory pre-dispatch checks —dispatch-gates.mjs --tierprinted a STALE TREE warning, and the premise checkgit log origin/main -- <paths>returneddd2184ain its first ten lines — so nothing was dispatched against it. The lesson for this seat is that the premise check has to run before the card is WRITTEN, not only before it is dispatched.Nothing here is actionable in this repository. The remaining work is entirely downstream in cloud (bump the
.objectstack-shapin pastdd2184a, then callmountStorageRoutesfrom the host) and is tracked on cloud#1969.
Generated by Claude Code
- The download-authorization seam. This card asked who supplies
- closed this as a duplicateof finding(service-storage): hosted tenant kernels cannot mount storage's REST routes — registerStorageRoutes needs buildAuthSessionResolver / buildFileReadAuthorizer / findFileHolder, all package-internal; export them so cloud can bridge storage the way it bridges settings #15169
on Sep 10, 2026
框架 pin
8b37a09(本卡所有行号以此为准)。下游实例:cloud#1969。现象
StorageServicePlugin在kernel:ready向自己 kernel 的http-server服务注册/api/v1/storage/*的 10 条路由(packages/services/service-storage/src/storage-service-plugin.ts:432-440)。拿不到http-server就记一条日志、什么都不挂:单租户
objectstack dev/serve下没问题:kernel 拥有 socket。但在一个 socket、N 个 kernel 的多租户宿主下,每环境 kernel 按设计没有http-server(宿主拥有 socket,按 hostname 路由到 kernel)。于是/api/v1/storage/*在这类部署上无人应答,文件/图片字段整条上传链路不可用 —— service 在、表在、生命周期钩子在,只有门没有。为什么这属于框架而不是各宿主自己糊一份
三条,都在框架自己的注释里:
① 已有公开自注册槽,而且点名了这一类。
packages/runtime/src/domain-handler-registry.ts:21-28:storage槽没有第二个提供方 —— 保留 dispatcher 注册权的那条理由在这里不成立。registerDomainHandler是公开方法(packages/runtime/src/http-dispatcher.ts:782),其单测自称「the public seam」(domain-handler-registry.test.ts:148)。② dispatcher 早已知道自己要服务多租户宿主。
DomainHandlerDeps带kernelResolver,注释:「True when a host KernelResolver is registered (multi-tenant deployment)」(domain-handler-registry.ts:292);runtime/src/index.ts:100-102把KernelResolver记为「retained framework contract; the multi-tenant implementation lives in cloud」。契约在框架、实现在宿主,是既定分工。③ 同形状框架已经做过一遍,并把它认成下游的设计主路径。
packages/runtime/src/domains/share-links.ts:11-14:关于 #4087 的警告 —— 是条件,不是禁止
packages/runtime/src/route-ledger.ts:308-322立着:⛔ 先读它删的是什么:#4087 删的是一座坏桥。upload 分支把
upload(key, data, options?)当upload(file, { request })调 —— 对任何实现都是 TypeError;download 分支 switch 一个没有实现返回过的结果形状。它删的是手搓的第二方言,不是这个模式。本卡满足它那句条件的方式是:委托给
registerStorageRoutes本体。那就是契约(packages/spec/src/api/storage.zod.ts的StorageApiContracts,枚举在service-storage/src/storage-route-ledger.ts)。⛔ 不接受任何重写一遍 handler 主体的做法。 那正是 #4087 删过一次的东西,再写一遍就是同一个缺陷回来。
建议形状(dev 可推翻,但推翻要写明依据)
在
StorageServicePlugin里,http-server缺席的那条分支(:432附近)从「记日志、什么都不挂」改成:IHttpServer的路由收集器收下registerStorageRoutes的注册。同款可参考(但本卡的实现留在框架):cloudpackages/objectos-runtime/src/env-settings-routes-plugin.ts的SettingsRouteCollector。DomainRoute { prefix: '/storage', match: 'prefix', handler },registerDomainHandler自注册。DomainRequest → IHttpRequest/IHttpResponse → HttpDispatcherResult的适配;storage与store从当轮请求解析出的环境 kernel 取。store就是new StorageMetadataStore(kernel.getService('objectql'))—— 公开导出(service-storage/src/index.ts:10),与插件自己的构造法一字不差(storage-service-plugin.ts:330,438)。要素:
http-server时行为一字不改,现有部署走原路,回归钉住。:fileId/:uploadId/:token)。Allow按IHttpServer适配器该有的契约给。插件今天拿不到 dispatcher 实例。 我核过:
registerDomainHandler的全部在仓调用点都在测试里;HttpDispatcher由DispatcherPlugin内部new(packages/runtime/src/dispatcher-plugin.ts:880),没有注册成 kernel service —— 全仓搜不到'http-dispatcher'之类的服务名。所以「插件自注册」这条路今天缺最后一跳。两条出路,请 dev 先判再实现:
registerDomainHandler这一能力)成为一个可解析的服务,StorageServicePlugin用查http-server完全同一个姿势去查它。runtime/src/domains/下建storage.ts,由 dispatcher 侧注册。@objectstack/service-storage不在@objectstack/runtime的 dependencies,连 devDependencies 都不在(实测packages/runtime/package.json)。走 (b) 就得新增 runtime → service-storage 这条边,而这正是share-links.ts:39-46明文记录过、并特意绕开的依赖倒置(它把plugin-sharing留作 devDep、拒绝 import,改把共享谓词搬去spec)。⇒ 倾向 (a),理由就是上面那条依赖方向 —— 也正是
registerDomainHandler作为「外部包独占槽自注册」的公开缝存在的原因。但 (a) 引入插件挂载顺序依赖(storage 必须在 dispatcher 之后),这一点要在 PR 正文里给出处理方式与证据。需要 dev 给结论的第二点
registerStorageRoutes的authorizeFileRead/resolveFileHolder两个 opts —— 附件下载的鉴权闸(scope === 'attachments'与字段拥有的文件是 gated 的,见storage-routes.ts的authorizeDownload)。在无http-server的路径上这两个钩子由谁供给?验收
http-server的 kernel 挂上StorageServicePlugin后,/api/v1/storage/*的 presigned / complete / files 三族可经宿主 dispatcher 应答。测试用本地适配器即可 —— ⛔ 不需要真 S3/R2,本卡与对象存储后端无关。http-server的 kernel 行为与本卡之前逐字节相同。route-ledger.ts的 dispatcher 的 /storage/upload 用 upload(file, {request}) 调用契约里的 upload(key, data, options?) —— 对任何实现都会 TypeError #4087 段落更新:说清/storage域回来了、它委托给service-storage的契约本体、以及它与storage-route-ledger.ts的分工。⛔ 不在route-ledger.ts重新铸一份 route 行 —— 那正是 dispatcher 的 /storage/upload 用 upload(file, {request}) 调用契约里的 upload(key, data, options?) —— 对任何实现都会 TypeError #4087 拆掉的双份。下游 / 相关
Blocked-by。/api/v1/storage/upload/*,门一挂上闸自动就在它前面 ⇒ 本卡不需要关心配额,也不要为它留钩子。service-storage/service-i18n归为「autonomously-mounted service routes」并给了各自 ledger;本卡是那一类在无 socket 宿主上的缺口。