尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenStatus 服务端架构解析:Hono on Deno 的四面路由、API Key 双层权限与 MCP 作用域门控

OpenStatus 服务端架构解析:Hono on Deno 的四面路由、API Key 双层权限与 MCP 作用域门控 OpenStatus 服务端架构解析Hono on Deno 的四面路由、API Key 双层权限与 MCP 作用域门控【免费下载链接】openstatus Status page with uptime monitoring API monitoring as code 项目地址: https://gitcode.com/GitHub_Trending/op/openstatusOpenStatus 的 API 服务端apps/server采用 Hono 框架运行在 Deno 之上在apps/server/src/routes/下划分出v1公开 REST、rpcConnectRPC、mcp与slack四个路由面。本文以 apps/server/AGENTS.md 为骨架结合源码与测试完整解析这套服务端的设计约定API Key 的read/write双层权限如何落地、路由配置为何要一次解析并注入、MCP 工具如何按作用域注册并对只读 Key 隐藏写工具。读完后你将掌握在 OpenStatus 中新增端点、迁移服务层、接入 Slack 路由与扩展 MCP 工具时的全部规范与原理。一、服务端总览Hono on Deno 与四个路由面apps/server是一个典型的Hono 跑在 Deno 上的 API 服务。入口在 apps/server/src/index.ts由 apps/server/src/serve.ts 通过Deno.serve({ port: 3000 }, app.fetch)启动端口固定为 3000见 serve.ts。应用在根路由上挂载了如下表面见 index.ts路由前缀表面说明/v1公开 REST面向第三方 API 的历史版本 REST 接口使用 OpenAPIHono 构建支持x-openstatus-key认证见 v1/index.ts/ConnectRPCConnectRPC通过mountRpcRoutes(app)挂载承载各领域 handlermonitor、notification、status-report 等/mcpMCP ServerModel Context Protocol 服务Streamable HTTP 传输供 AI 客户端调用见 index.ts/slackSlack 应用Slack 命令、事件、交互与 OAuth 回调见 index.ts此外还有/public公开状态页接口、/上的 OAuth 授权服务器RFC 8414/9728与健康检查路由。从源码结构看四个主要路由面共享同一套认证与权限基础设施——这正是本文后半部分要展开的核心。二、API Key 权限体系read / write 双层强制AGENTS.md 开篇即强调两层机制共同强制read/write权限缺一不可。第一层在服务层service level第二层在传输层transport level。2.1 服务层门控requireScope(ctx, write)凡是经由openstatus/services路由的服务动词第一行必须是requireScope(ctx, write)或对应的read。这是真正意义上的闸门。实现位于 packages/services/src/auth/require-scope.ts。核心逻辑只有apiKey与mcp两类 actor 会被检查对应ServiceContext.actor类型见 packages/services/src/context.tsuserDashboard 会话与system、slack、webhook、subscriberactor 是no-op——它们已经通过了各自的信任边界member 角色权限是独立项目权限不匹配时抛ForbiddenError并输出一条含keyId、userId、workspaceId、所需与持有 scope 的告警日志便于泄漏 Key 的应急溯源空 scopes 列表fail-closed任何权限都不放行。源码注释还给出一个重要约定把requireScope放在写动词的第一行、withTransaction之前——权限检查不应打开一个事务再回滚而且该检查没有数据库依赖。与之配套的纯函数匹配器是 packages/services/src/auth/matches-scope.ts其权限层级为* ⊇ write ⊇ read即持有write的 Key 天然满足read要求*超级管理员满足一切任何无法识别的 scope 字符串一律不匹配fail-closed。该匹配器前向兼容未来引入monitor.write之类的资源级 scope 时只需原地扩展无需改动require-scope.ts。packages/services/src/auth/tests/require-scope.test.ts 用 12 个用例覆盖了全部行为只读 Key 请求write抛错、请求read放行、*通吃、mcp actor 与 apiKey 同等约束、user/system/slack/webhook/subscriber 全为 no-op、空 scopes fail-closed。2.2 传输层门控requireWriteScope()与 V1 的历史债第二层是 apps/server/src/libs/middlewares/require-scope.ts 中的requireWriteScope()中间件它被挂载在 V1 路由器上、紧随authMiddleware之后见 v1/index.tsapi.use(/*, authMiddleware); // V1 路由使用内联 Drizzle 查询而非 openstatus/services // 因此服务层 requireScope 不会执行此中间件补上缺口 // 迁移完成后仍作为纵深防御保留。 api.use(/*, requireWriteScope());为什么 V1 需要传输层检查AGENTS.md 说得非常清楚V1 先于 services 约定存在至今仍直接内联 Drizzle 查询服务层的requireScope会完全跳过它的写处理器。该中间件按 HTTP 方法映射 scopeGET/HEAD视为 read其余方法一律视为 write。这种映射对 V1 是精确的因为每个 V1 写路由都是POST/PUT/PATCH/DELETE每个读路由都是GET/HEAD。尤其要注意POST /v1/check按需探测也属于 write——按计划文档的写规则定义POST 即为写。文件头注释还特别警告如果给 V1 新增一个通过GET完成变更的路由会静默绕过此中间件。实现细节值得逐一推敲对GET/HEAD直接next()不查 Key若apiKey缺失则放行把 401 的判定留给上游authMiddleware避免本应是 401 的请求被误判为 403const scopes apiKey.scopes ?? []是双保险即使authMiddleware部分填充了apiKey也会 fail-closed无 scopes 即无写权限而不是在.includes上崩溃只有同时不持有write与*时才抛 403API key lacks write scope。2.3 scope 从哪来validateKey与三类凭证authMiddlewareapps/server/src/libs/middlewares/auth.ts调用validateKey解析凭证并得到 scopes。从 auth.ts 可以归纳出 scope 的完整来源凭证类型触发条件scopes说明OAuth Access Token以oat_为 keyId 的授权来自 grant本地 OAuth 流程走通的关键审计归因到 grant自定义 Keyos_前缀先查本地 DB行的scopes列验证哈希、检查过期、best-effort 更新lastUsedAtUnkey Keyos_前缀但本地 DB 无记录[write]历史遗留姿态Unkey 时代 minted 的 Key 保持完整工作区访问权超级管理员sa_前缀且等于SUPER_ADMIN_TOKEN[*]*仅内部可用任何公开 API 都无法设置开发环境非 production[*]本地测试镜像超级管理员避免被 scope 检查误锁其中c.set(apiKey, ...)总是填充apiKey缺 keyId 时回退到工作区级占位符ws:id并打告警保证下游适配器可以无差别访问。2.4 迁移约束oxlint 禁止直连数据库AGENTS.md 指出新端点应调用服务动词而不是直接查询 Drizzle并且oxlint.config.ts已对已迁移域名的 handler 禁用openstatus/db与drizzle-orm的导入这个名单每个 PR 增长一个域名。oxlint.config.ts 中的no-restricted-imports规则印证了这一点packages/api/src/router/下的 statusReport、maintenance、incident、monitor、page 等以及apps/server/src/routes/rpc/handlers/下的 health、status-report、maintenance、notification 和slack/interactions.ts都在禁用名单内报错信息统一为 Use openstatus/services instead。同时用excludeFiles放行测试文件与少数仍直读 DB 的实现如 notification 的limits.ts、converters.ts并注释说明了豁免原因。三、路由配置模式一次解析注入传递AGENTS.md 的第二条约定是配置只解析一次并传入路由标杆示例是 apps/server/src/routes/slack/index.ts 的createSlackRoute(config)export function createSlackRoute(config: SlackConfig) { const slack new HonoSlackEnv(); slack.use(*, async (c, next) { c.set(slackConfig, config); if (!config.signingSecret || !config.aiGatewayApiKey) { return c.json({ error: Slack agent not configured }, 503); } await next(); }); // GET /install、GET /oauth/callback、POST /events|/interactions|/commands return slack; } // 生产环境模块作用域一次解析 export const slackRoute createSlackRoute(slackConfigFromEnv());slackConfigFromEnv()定义在 apps/server/src/routes/slack/config.ts把SLACK_SIGNING_SECRET、SLACK_CLIENT_ID、SLACK_CLIENT_SECRET、SLACK_REDIRECT_URI、AI_GATEWAY_API_KEY等环境变量一次性收敛为SlackConfig对象dashboardUrl还会按NODE_ENV区分生产https://app.openstatus.dev与本地http://localhost:3000。SlackConfig的全部字段可见 config.ts。这套依赖注入式设计的动机来自一个真实的测试痛点deno test --parallel下worker 共享同一进程环境各测试文件并发修改process.env会互相覆盖。因此 handler 一律从请求上下文读取配置而不是在请求时读取env——测试可以构造一个显式配置的路由彻底避开全局环境变量污染。生产则退化为模块加载时解析一次的简单形式。四、MCP按作用域注册工具只读 Key 看不到写工具4.1 路由与传输MCP 表面挂在/mcp使用hono/mcp的StreamableHTTPTransport见 apps/server/src/routes/mcp/index.ts。它同时是 RFC 9728 的 OAuth 保护资源每次请求都经authMiddleware匿名请求返回带WWW-Authenticate的 401——这个 401 不是失败而是发现机制MCP 客户端先无凭证initialize靠 401 挑战获知授权服务器地址进而走 OAuth 握手如果匿名应答握手客户端会永远停留在已连接但未认证的状态既学不到 OAuth 的存在也看不到任何工具。每个请求构建一个全新的McpServerapps/server/src/routes/mcp/server.ts工具注册时通过闭包捕获本次请求的ServiceContext从而在结构上保证工作区隔离。工具列表为静态listChanged: false资源是公开文档。4.2 核心机制registerScopedToolAGENTS.md 的第三条约定MCP 工具声明scope: read | write并通过registerScopedTool注册这样只读 Key 在tools/list里永远不会看到写工具用普通方式注册的工具则无论 Key 是什么都会泄露。实现位于 apps/server/src/routes/mcp/tools/register-scoped.ts。要点每个工具定义多了一个必填字段scope见 register-scoped.ts注册前用matchesScope(actorScopes(ctx), def.scope)过滤不满足直接返回undefined工具根本不会注册进 server——过滤是UX底层服务动词的requireScope抛ForbiddenError才是正确性actorScopes对非 Key actor防御纵深返回[*]注册时校验scope与annotations.readOnlyHint的一致性若写工具声明readOnlyHint: true会在注册阶段直接抛错防止对 MCP 客户端Claude Desktop、Cursor 等谎报可安全调用返回RegisteredTool | undefined调用方可以维护Mapname, RegisteredTool供测试/调试。apps/server/src/routes/mcp/tools/register-scoped.test.ts 的 5 个用例覆盖满足作用域时注册、只读时返回undefined、readOnlyHint与 scope 冲突双向抛错、省略 hint 时不触发校验。4.3 实际工具定义与工具族工具定义存放在packages/services/src/agent-tools/以 maintenance.ts 中的create_maintenance为例export const createMaintenanceTool: AgentTool... { name: create_maintenance, description: Schedule a maintenance window on a status page. PUBLIC, AUDIT-LOGGED, AND POTENTIALLY NOTIFIES SUBSCRIBERS — irreversible side effects. ..., scope: write, destructive: true, inputSchema: CreateMaintenanceInputShape, outputSchema: CreateMaintenanceOutput, approval: { extraFlags: [{ id: notify, label: Notify subscribers }], applyFlags: (input, flags) ({ ...input, notify: flags.notify ?? false }), summarize: (input) ({ title: Schedule Maintenance: ${input.title}, ... }), verb: scheduled, }, async run({ ctx, input }) { /* 调用 openstatus/services 的 createMaintenance */ }, };注意几个与权限强相关的设计副作用型工具发通知、不可逆操作都标记destructive: true并带approval摘要与确认 flagrun内部调用createMaintenance服务动词——服务层的requireScope(ctx, write)会再次把关形成注册过滤UX 服务层检查正确性的双保险。各工具族通过registerRegistryTools见 apps/server/src/routes/mcp/tools/monitor.ts统一接入当前注册域包括 page、status-report、maintenance、monitor、notification、private-location、audit、content见 server.ts。从agent-tools的 scope 声明分布monitor 族 6 个全为readmaintenance 与 status-report 各含write可以推断只读 Key 的tools/list将只看到查询类工具而创建维护窗口、发布状态报告等写工具会被整体隐藏。五、把约定变成习惯新增端点时的检查清单综合 AGENTS.md 与源码在 OpenStatus 服务端新增端点或扩展工具时应遵循以下顺序走服务层不走 Drizzle新端点调用openstatus/services的服务动词若域名尚未迁移先迁移并在 oxlint.config.ts 的no-restricted-imports名单中登记该文件。写动词第一行调用requireScope(ctx, write)放在withTransaction之前读动词如需要同样显式声明read。V1 新增路由需自证方法语义写操作必须使用POST/PUT/PATCH/DELETE因为传输层的requireWriteScope()以方法映射 scope用GET做变更会静默绕过。路由配置一次解析并注入仿照createSlackRoute(config)slackConfigFromEnv()让 handler 从c.get(slackConfig)之类上下文读取配置保证deno test --parallel下可测。MCP 工具一律registerScopedTool声明scope保持readOnlyHint与 scope 一致副作用工具标注destructive并提供approval摘要切勿用 SDK 裸registerTool注册否则会向只读 Key 泄露写工具。六、小结OpenStatus 的apps/server用一套小而清晰的约定同时管理了四个路由面的认证、授权与可测试性服务层requireScope与传输层requireWriteScope双闸互补前者覆盖已迁移域后者兜底仍内联 Drizzle 的 V1createSlackRoute模式确立了配置一次解析、上下文注入的测试友好范式registerScopedTool把 MCP 的tools/list变成权限的忠实投影。这三条约定从 apps/server/AGENTS.md 出发、在源码与测试中逐行落地是阅读和扩展 OpenStatus 服务端最值得先掌握的地图。【免费下载链接】openstatus Status page with uptime monitoring API monitoring as code 项目地址: https://gitcode.com/GitHub_Trending/op/openstatus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表