diff --git a/docs/superpowers/specs/2026-04-22-logto-protected-api-mock-design.md b/docs/superpowers/specs/2026-04-22-logto-protected-api-mock-design.md new file mode 100644 index 0000000..e644186 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-logto-protected-api-mock-design.md @@ -0,0 +1,216 @@ +# Logto 受保护 API Mock 联调设计 + +## 背景 + +当前扩展已经具备基础的 Logto 登录能力: + +- popup 可触发登录和登出 +- background 可通过 `@logto/chrome-extension` 获取 access token +- 页面功能在未登录时会被 auth gate 拦住 + +但现阶段业务数据仍然来自页面站内接口,尚未真正验证“扩展拿到 Logto access token 后,请求受保护 API”这条链路。后续真实后端将由其他人提供,因此当前阶段的目标不是接入正式接口,而是先在本地完成一套可重复验证的模拟联调方案。 + +## 目标 + +- 新增一个专用于 Logto 受保护 API 的扩展客户端。 +- 扩展请求受保护 API 前,必须先通过 background 获取 access token。 +- 请求时自动附加 `Authorization: Bearer ` 请求头。 +- 提供一个本地 mock 受保护 API,用于验证扩展到后端的完整调用链路。 +- 提供自动化测试,分别覆盖: + - token 注入逻辑 + - mock API 授权成功/失败行为 + +## 非目标 + +- 不接入真实业务后端。 +- 不做真实 JWT 签名校验、JWKS 拉取或资源权限判定。 +- 不修改现有星图页面数据采集逻辑为正式后端模式。 +- 不在本阶段设计复杂的 token 刷新监控或重试策略。 + +## 方案对比 + +### 方案 A:只做本地 mock 后端 + +- 直接搭一个假接口,扩展请求它并观察返回。 +- 优点:最接近最终使用方式。 +- 缺点:如果联调失败,不容易快速判断问题出在 token 注入还是 mock 服务本身。 + +### 方案 B:只做代码级测试 + +- 不起本地服务,只用测试替身验证请求头是否带 token。 +- 优点:实现快,定位问题直接。 +- 缺点:无法证明完整链路可运行。 + +### 方案 C:先代码级测试,再接本地 mock 后端 + +- 先用单元测试锁定 token 注入行为,再起 mock 服务完成真实联调。 +- 优点:定位问题更快,同时保留完整链路验证。 +- 缺点:改动稍多于单一方案。 + +推荐采用方案 C。 + +## 架构设计 + +### 1. 扩展受保护 API 客户端 + +新增一个独立客户端模块,职责如下: + +- 向 background 发送 `auth:get-access-token` 消息 +- 读取返回的 access token +- 使用该 token 请求指定后端地址 +- 将接口成功、未授权、网络失败这三类结果转成明确错误 + +这个客户端不直接耦合星图 DOM,也不依赖 popup。它只负责“拿 token 并带 token 发请求”,这样后续替换正式后端时只需要调整接口地址和返回映射。 + +### 2. Background 认证桥接 + +现有 background 已支持 `auth:get-access-token`。本次不改变登录主流程,只把它当作唯一 token 来源: + +- content script 不直接接触 Logto SDK +- 所有受保护 API 请求都通过 background 提供 token + +这样可以保持认证逻辑集中,符合 MV3 扩展的边界约束。 + +### 3. 本地 mock 受保护 API + +新增一个轻量本地服务作为测试后端,建议职责保持极小: + +- 暴露固定测试 endpoint,例如 `/api/mock/protected` +- 检查请求头中是否存在 `Authorization` +- 如果请求头形如 `Bearer <非空字符串>`,返回固定假数据 +- 如果没有该头,返回 `401` + +本阶段不要求验证 token 是否来自真实 Logto,只验证“扩展是否按约定附带了 Bearer token”。 + +## 数据流 + +完整调用链路如下: + +1. 扩展中的业务入口调用受保护 API 客户端 +2. 客户端向 background 发送 `auth:get-access-token` +3. background 返回当前 access token +4. 客户端带上 `Authorization: Bearer ` 请求本地 mock API +5. mock API 校验请求头并返回假数据 +6. 客户端将结果回传给调用方 + +失败分支: + +- 如果 background 没返回合法 token,客户端直接报错,不发请求 +- 如果 mock API 返回 `401`,客户端将其识别为未授权错误 +- 如果请求超时或网络失败,客户端抛出网络错误 + +## 接口设计 + +### 扩展内客户端接口 + +建议客户端提供单一入口,例如: + +- `loadProtectedMockData()` + +内部行为: + +- 先取 token +- 再发 GET 请求 +- 返回结构化响应对象或抛出明确错误 + +后续替换真实后端时,可以保留同样入口,内部再切换到真实 endpoint。 + +### Mock API 返回 + +成功时返回固定 JSON,例如: + +```json +{ + "ok": true, + "source": "mock-protected-api", + "message": "authorized", + "receivedAuthHeader": "Bearer ..." +} +``` + +失败时返回: + +```json +{ + "ok": false, + "error": "unauthorized" +} +``` + +## 错误处理 + +### 无 token + +- 判定条件:background 未返回 `auth:token`,或 token 为空字符串 +- 处理方式:立即抛错,提示当前未登录或 token 不可用 + +### 未授权 + +- 判定条件:后端返回 `401` 或 `403` +- 处理方式:抛出授权失败错误,供上层展示“请重新登录” + +### 网络失败 + +- 判定条件:fetch 抛异常、连接失败、超时 +- 处理方式:抛出网络错误,不吞掉异常原因 + +## 实现边界 + +建议新增或调整如下模块: + +### `src/content/market` 下新增受保护 API 客户端 + +- 不替换现有页面接口客户端 +- 独立处理 mock 受保护 API 访问 +- 便于后续把“页面抓取模式”和“后端接口模式”并行保留 + +### `src/shared/auth-messages.ts` + +- 复用现有 `auth:get-access-token` +- 若现有消息结构足够,则不新增消息类型 + +### `scripts/` 或独立目录中的 mock 服务 + +- 提供本地测试服务启动脚本 +- 默认监听本地固定端口 +- 返回固定 JSON 结果 + +## 测试策略 + +### 单元测试 + +- 客户端请求前会先获取 access token +- 拿到 token 后,请求头中包含 `Authorization: Bearer ` +- token 缺失时不会发起 fetch +- 接口返回 `401` 时抛出未授权错误 +- 接口返回成功时正确解析 JSON + +### 联调测试 + +- 启动本地 mock 服务后,带 token 请求能成功 +- 不带 token 请求返回 `401` +- 扩展客户端能读到 mock 返回的假数据 + +## 手动验证 + +1. 构建并加载扩展 +2. 在 popup 中完成 Logto 登录 +3. 启动本地 mock API +4. 触发扩展中的受保护接口请求 +5. 确认 mock API 收到 `Authorization: Bearer ` +6. 确认扩展端收到成功响应 + +## 迁移到真实后端的路径 + +当真实后端可用时,仅需要替换以下内容: + +- mock API 基地址 +- 具体 endpoint 路径 +- 返回数据结构映射 +- 若真实后端要求额外 scope,则补充 `auth-config` 中的 scopes + +核心认证链路保持不变: + +- 仍由 background 提供 token +- 仍由独立客户端附带 `Bearer` 请求头 +- 仍按未授权和网络错误分别处理