Skip to content

plugin-auth 的终结式 catch-all 吞掉 /api/v1/auth/* 下别人的路由 —— console 权限层目前靠 kernel.use() 顺序才活着 #4088

Description

@os-zhuang

按 Prime Directive #10 记录,#4073 / #4079(拆分 registerStandardEndpoints)实施中发现。

现象

plugin-auth 在自己的 kernel:ready hook 里挂一个覆盖整个 auth 命名空间的 catch-all(auth-plugin.ts 的 registerAuthRoutes):

rawApp.all(`${basePath}/*`, async (c) => {
    const response = await this.authManager!.handleRequest(c.req.raw);
    // …日志 / jwks cache-control…
    return response;                  // ← 终结式:从不 next()
});

basePath 默认 /api/v1/auth。这个 handler 从不 next() —— 它无条件返回 better-auth 的响应,包括 better-auth 不认识那条路径时返回的 404。

而 plugin-hono-server 在它自己的 kernel:ready hook 里挂 /api/v1/auth/me/permissions 与 /api/v1/auth/me/localization(#4079 后这两条已改为无条件注册)。两个 hook 都在 kernel:ready 阶段,Hono 对同一路径按注册顺序依次执行 handler,先返回 Response 的赢。

于是这两条端点能不能工作,完全取决于 kernel.use() 的顺序:

顺序 结果
HonoServerPlugin 先(今天 os serve 的顺序) hono 的具体路由先注册 → 正常应答
AuthPlugin 先 catch-all 先注册 → better-auth 不认识 /me/permissions → 404

为什么值得单独处理

坏掉的时候是静默的,而且砸在最要命的路径上:

  • objectui console 的整个权限层读 /auth/me/permissions(MePermissionsProvider 的 DEFAULT_ENDPOINT)—— 404 意味着 FLS / apiOperations 全线失去服务端答案;
  • apps/console/src/AppContent.tsx 读 /auth/me/localization 取区域默认值;
  • core/security/auth-gate.ts 把 /me/localization 列为"被门禁拦住的用户必须仍能访问"。

而且没有任何东西保护它:#4079 新增的测试只钉住了 hono 插件内部的注册顺序(/auth/me/* 在 CRUD 块之前),跨插件的 hono-先于-auth 没有任何断言、也没有任何机制保证。

这与 #2567(匿名拒绝取决于谁先注册 /data)、#4018(discovery 被顺序遮蔽)是同一类问题:不变量靠加载顺序侥幸成立,而非被强制。前两个都是发现后单独收口的。

更一般地说:任何插件想在 /api/v1/auth/* 下挂路由,今天都会被这个 catch-all 吞掉,除非它恰好注册得更早。这是命名空间所有权的问题,不只是这三条端点的问题。

建议修法

让 catch-all 在 better-auth 不拥有该路径时落穿,而不是把 404 当作最终答案:

rawApp.all(`${basePath}/*`, async (c, next) => {
    const response = await this.authManager!.handleRequest(c.req.raw);
    if (response.status === 404) {
        await next();                     // 让后注册的具体路由有机会应答
        if (!c.finalized) return response; // 没人接 → 保持 better-auth 原本的 404
        return;                           // 有人接了
    }
    // …现有的 >=500 日志 / jwks cache-control…
    return response;
});

要点:

  • 只对 404 落穿。 401/403 是 better-auth 的真实回答,不是"不认识这条路径"。
  • c.finalized 兜底(Hono 4.12.31 有这个属性),所以真正无人认领的 auth 路径线上形状不变 —— 仍是 better-auth 原本那个 404,而不是平台的 notFound。
  • 优先级方向正确:plugin-auth 拥有这个命名空间,所以 better-auth 实现了的路径仍然先赢;别人只能捡它不要的。同时结果与注册顺序无关。
  • 代价是这三条 /me/* 每次请求会多走一次 better-auth 的未知路径查找(应无副作用)。

不建议改成"按枚举只挂 better-auth 真实拥有的路径":#3656 的 route-ledger 明确依赖"catch-all 会自动发布上游新增的端点"这一性质,改成枚举会引入一份需要长期同步的表 —— 正是 #4018 / #4073 一路在消除的那种重复供给税。

保护

auth-plugin.test.ts 已有用 mock rawApp 驱动 kernel:ready 注册的先例,换成真实 Hono app 即可测到真代码:catch-all 先注册、具体路由后注册,断言后者仍能应答;并断言无人认领的路径仍拿到 better-auth 原本的 404、401/403 不会被落穿。

关联:#4073、#4079、#2567、#4018、#3656(auth route ledger)、ADR-0076 D11(单一 owner)。

Activity

  1. os-zhuang commented on Jul 30, 2026

    @os-zhuang
    ContributorAuthor

    实施中订正两点 —— 上面正文里那段建议补丁照抄是不工作的:

    1. c.finalized 不是可用判据。 链末尾无人匹配时,Hono 会运行 notFound handler,它会设置响应并把 finalized 翻成 true —— 所以它分不清"有人接手"和"没人接手"。实测(Hono 4.12.31):await next() 之后 finalized === true、c.res.status === 404、body 是 404 Not Found。

    改用状态码判据:下游给出非 404 才算有人接手。代价是下游路由自己那份 404 body 会被 better-auth 的 404 取代(status 两者相同),而这个前缀下今天没有任何路由回 404 —— /auth/me/* 对匿名调用者是 200 + authenticated: false。

    2. 不能 return response,必须写 c.res = response。 plugin-auth 自己在 catch-all 之前挂了一个 IP 门禁 rawApp.use(${basePath}/*, …)(ADR-0069 D5),链里因此有中间件;而 Hono 的 compose 只在 c.finalized 为 false 时才把 handler 返回的 Response 赋给 c.res。notFound 已经把它翻真,所以 return response 会被静默丢弃,调用方拿到 Hono 的 404 Not Found 文本而不是 better-auth 的 JSON。直接赋值 c.res 不受这个条件限制。

    三个变体的实测对比(同一条无人认领的路径):

    写法 结果
    return response(链中有 use() 中间件) 404 "404 Not Found" ← 丢了
    c.res = response 404 {"code":"NOT_FOUND"} ← 正确
    c.res = response,但下游有 200 路由 200 {"ok":true} ← 未被覆盖

    顺带核实了这个形状在别处是否复发(结论:os serve 路径上没有第二处):


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions