# HTTP API 模块 ## 1. 定位 本模块冻结 cpa-ext 的 HTTP 边界:谁可以调用、使用哪些固定路径、请求/响应如何表达、错误是否稳定,以及 UI 如何只通过公开用例访问 Core。 API 不直接暴露 Repository 表,也不允许 UI 自行拼 SQL 语义。每个写接口都对应 [core.md](core.md) 中的应用用例,每个查询都经过权限化 facade。 当前宿主基线为 CLIProxyAPI `v7.2.130`: - 插件 Management routes 位于 `/v0/management/...`,由 CPA Management Key 保护; - plugin resource 位于 `/v0/resource/plugins//...`,宿主只支持未鉴权的精确 GET; - 插件声明的路径不能包含 `:`, `*` 或 `..`; - Management 和 resource handler 都会在进入插件前被宿主完整读入内存; - `home.enabled` 时两类插件路由均不可用; - Management JSON 的所有字符串目前会被宿主 HTML entity 转义,resource JSON 不会。 这些是设计约束,不是实现细节。 ## 2. API 表面 ### 2.1 管理 API 固定基路径: ```text /v0/management/plugins/cpa-ext/v1 ``` CPA 先执行 Management 鉴权,再把请求交给插件。cpa-ext 不创建第二套管理员密码或 session。第一版 CPA 只有一把 Management Key,因此所有 Management 调用都是全管理员权限,不能在文档中虚构细粒度 RBAC。 ### 2.2 Browser resources 固定基路径: ```text /v0/resource/plugins/cpa-ext ``` 用途分为: - 静态 UI shell 和构建产物; - 最小公开 readiness; - 由插件自行校验 downstream Key 的用户只读 JSON。 resource route 在宿主层**没有鉴权**。任何包含管理员数据、写操作、OAuth 原文或数据库备份的 handler 都不得注册为 ResourceRoute。 ### 2.3 模型调用 API 模型调用仍使用 CPA 原生 Codex/OpenAI Responses 路径。cpa-ext 通过 frontend auth、interceptor、scheduler 和 response/usage callback 参与,不另建代理入口。允许路径见 [access-routing.md](access-routing.md)。 ## 3. 通用 wire 约定 ### 3.1 JSON - Content-Type:`application/json; charset=utf-8`; - 字段名:`snake_case`; - 未知写入字段默认拒绝; - 空集合返回 `[]`/`{}`,不使用 `null`; - ID 是 opaque string,调用方不能解析前缀获得权限; - 时间是 UTC RFC3339Nano; - 时长使用整数毫秒或明确命名的秒数; - 百分比使用 `0..100` 并明确字段名,fraction 使用 `0..1`; - Token 只出现在管理员审计 DTO; - secrets、Authorization、Prompt、Response 和 raw Auth JSON 永不进入普通 DTO。 ### 3.2 金额 ```json { "currency": "USD", "micros": 1234567, "display": "$1.234567" } ``` `currency + micros` 是机器真值,`display` 由服务端统一 formatter 生成。用户看到的余额、额度、消费和账单只有金额,不返回 credits/token 余额。管理员可以在账单明细中额外看到 Token、费率和倍率。 ### 3.3 成功响应 单对象: ```json { "data": {}, "meta": { "request_id": "req_opaque", "revision": 12 } } ``` 列表: ```json { "data": [], "meta": { "request_id": "req_opaque", "next_cursor": "opaque_cursor", "has_more": false } } ``` 创建 Credential/轮换 secret 的第一次成功响应额外包含 `secret`,并强制 `Cache-Control: no-store`。明文只展示一次。 ### 3.4 错误响应 ```json { "error": { "code": "billing_quota_exhausted", "message": "Amount balance is exhausted.", "request_id": "req_opaque", "retryable": false, "details": { "field": "billing_account_id" } } } ``` `message` 可本地化且不得作为程序判断依据;`code` 稳定。`details` 只包含安全字段、约束和资源 ID,不回显 secret 或原始数据库错误。 状态映射: | HTTP | 语义 | | ---: | --- | | 400 | DTO/字段/游标无效 | | 401 | 用户 Key 无效;当前 CPA frontend auth 也会把认证依赖故障折叠为 401 | | 403 | 已认证但无权限/入口不允许 | | 404 | 资源不存在或为防枚举而隐藏 | | 409 | revision、状态或幂等冲突 | | 412 | `expected_revision` 不匹配 | | 413 | 请求体过大;真正上限必须先由反向代理/CPA HTTP 层执行 | | 422 | 语法正确但领域规则不可满足 | | 429 | 金额额度、并发或速率限制 | | 500 | 未分类内部缺陷,响应脱敏 | | 503 | 数据库、账本、价格或必需 runtime 不健康 | ### 3.5 版本与并发控制 - 路径版本为 `v1`; - 响应头返回 `X-CPA-Ext-API-Version: 1`; - 每个可写对象返回 `revision`; - PATCH/PUT/状态动作必须携带 `expected_revision`; - 版本不匹配返回 `412 revision_mismatch` 和当前 revision; - 不允许 last-write-wins 覆盖套餐、价格、路由和账户状态。 ### 3.6 幂等 以下写操作必须携带 `Idempotency-Key`: - provision/签发/轮换 Credential; - 金额 adjustment; - 价格发布/回滚; - quota 批量刷新; - backup/rebuild 等 maintenance command。 相同 key + 相同 payload 返回同一业务结果;相同 key + 不同 payload 返回 `409 idempotency_conflict`。 Credential 明文不持久化,因此签发/轮换响应重放只保证不会再创建第二个 secret:首次响应有 `secret`,后续幂等重放返回同一 Credential 但 `secret=null`、`secret_available=false`。如果调用方丢失首次响应,只能执行轮换。 ## 4. 管理 API 路由 CPA 只允许精确路径,所以记录 ID 放 query/body,不使用 `/credentials/:id`。 ### 4.1 系统与诊断 | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/status` | 版本、runtime topology、schema/capabilities、数据库与统计状态 | | GET | `/readiness` | 计费依赖、HMAC keyring、active price、gateway 前置条件 | | GET | `/diagnostics` | 脱敏故障、lag、unpriced/unmeasured/persistence gap | `readiness` 只能证明插件自身依赖。部署网关还必须验证 CPA 中插件 active、Home 关闭、exclusive/sentinel 流量探针成立,不能只信一个自报 JSON。 ### 4.2 下游账户、Credential 与套餐 | Method | Path suffix | 用途 | | --- | --- | --- | | POST | `/provision` | 原子创建 BillingAccount、绑定 Plan/Route 并签发第一把 Key | | GET | `/billing-accounts` | 列表或 `?id=` 详情 | | POST | `/billing-accounts` | 创建空账户 | | PATCH | `/billing-accounts` | 修改 label/status/并发等安全字段 | | GET | `/credentials` | 列表或 `?id=` 详情,不返回明文/digest | | POST | `/credentials` | 为现有账户签发 Key | | PATCH | `/credentials` | label、有效期、模型/tier 范围 | | POST | `/credentials/rotate` | 创建新 SecretVersion 并按策略失效旧版本 | | POST | `/credentials/revoke` | 撤销 Credential/SecretVersion | | GET | `/plans` | 计划列表或详情 | | POST | `/plans` | 创建计划 | | PATCH | `/plans` | 修改计划,必须预览影响 | | DELETE | `/plans` | retire 未使用计划;历史引用不删除 | | GET | `/route-policies` | 查询账户/credential 路由策略 | | PUT | `/route-policies` | 原子替换一条策略及绑定 | `POST /provision` 是 UI 的主路径,避免业务层出现“账户创建成功但 Key/Plan/Route 只完成一半”。其事务边界以 [core.md](core.md) 为准。 ### 4.3 金额账本 | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/ledger` | 按 account、时间、kind 稳定分页 | | POST | `/ledger/adjustments` | 管理员充值、扣减、refund/纠错 | | GET | `/billing-cycles` | 周期、额度、消费、余额投影 | Adjustment 必须包含 `billing_account_id`、Money、kind、reason、`expected_revision` 和 `Idempotency-Key`。API 不提供“直接改 balance”端点。 ### 4.4 价格 | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/pricing/active` | active version 与模型政策 | | GET | `/pricing/versions` | 版本历史/详情 | | GET | `/pricing/candidates` | 候选、来源和冲突 | | POST | `/pricing/candidates/refresh` | 拉取外部 candidate,不发布 | | POST | `/pricing/preview` | 校验 draft、diff、golden cases | | POST | `/pricing/publish` | 显式发布不可变版本 | | POST | `/pricing/rollback` | 以历史内容创建并发布新版本 | 没有“直接 PUT 当前价格”的捷径。所有变更走 draft/preview/publish,详见 [pricing.md](pricing.md)。 ### 4.5 上游账户和池 | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/upstream-accounts` | 目录、状态、priority、bindability | | POST | `/upstream-accounts/sync` | 触发 host.auth.list reconciliation | | POST | `/upstream-accounts/reconcile` | 人工确认 Auth replacement/merge 拒绝 | | GET | `/upstream-quotas` | 最新 quota snapshot | | POST | `/upstream-quotas/refresh` | 单个/有界批量按需刷新 | | GET | `/upstream-pools` | 池列表/详情 | | POST | `/upstream-pools` | 创建池 | | PATCH | `/upstream-pools` | 修改名称/成员/revision | | DELETE | `/upstream-pools` | retire 无引用池 | Quota refresh 请求只接受内部 `upstream_account_id[]`,不接受 URL、Header、token 或任意 provider payload。 ### 4.6 统计、请求与审计 | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/statistics/overview` | 金额/请求/健康总览 | | GET | `/statistics/activity` | 时间序列 | | GET | `/statistics/analysis` | 维度分析 | | GET | `/requests` | Request 列表或 `?request_id=` 详情 | | GET | `/executions` | Execution/重试明细 | | GET | `/audit-events` | 管理动作与安全事件 | 图表接口返回服务端聚合。请求详情默认不含 Prompt/Response;CPA request log 访问走单独显式授权、短期 token、审计和脱敏流程。 ### 4.7 Maintenance | Method | Path suffix | 用途 | | --- | --- | --- | | GET | `/maintenance/status` | checkpoint、备份、完整性和 runner 状态 | | POST | `/maintenance/backup` | 创建 SQLite online backup | | GET | `/maintenance/backups` | 脱敏备份清单,不直接返回任意文件路径 | | POST | `/maintenance/integrity-check` | 启动有界检查 | | POST | `/maintenance/rebuild` | 从指定 event_seq 重建投影 | 在线 API 不提供 restore、删除数据库或任意路径下载。恢复属于 [operations.md](operations.md) 的离线、排空流程。 ## 5. 用户只读 API 固定 ResourceRoute: | Method | Path | 用途 | | --- | --- | --- | | GET | `/v0/resource/plugins/cpa-ext/v1/me` | 当前 Credential、共享金额账户和周期摘要 | | GET | `/v0/resource/plugins/cpa-ext/v1/me/requests` | 当前 Credential 的逐请求金额记录 | | GET | `/v0/resource/plugins/cpa-ext/v1/me/activity` | 当前 Credential/账户金额趋势 | 鉴权: ```http Authorization: Bearer ``` 规则: - Key 不得放 query、fragment、cookie、URL 或 localStorage; - 插件自行 HMAC lookup,并按 Credential scope 构造查询; - 共享 BillingAccount 的余额可以显示,但默认请求列表只显示当前 Credential 产生的记录; - 请求参数中的 account/credential ID 不能扩大 scope; - 响应 `Cache-Control: no-store`、`Pragma: no-cache`、`Vary: Authorization`; - 错误不区分不存在、撤销、过期等可枚举细节; - 用户接口只返回 Money、模型展示名、时间和安全状态,不返回 Token、上游账户、价格规则或管理员诊断。 当前 resource 只有 GET,且没有 HttpOnly session/CSRF 模型。需要用户写操作、稳定登录或团队账号时必须增加 sidecar/public API 或扩展 CPA authenticated user plugin routes。 ## 6. UI resources 至少注册: ```text GET /v0/resource/plugins/cpa-ext/ui GET /v0/resource/plugins/cpa-ext/assets/<每个确定文件名> GET /v0/resource/plugins/cpa-ext/ready ``` 宿主不支持 wildcard,因此每个构建产物在 `management.register` 中逐个声明。前端使用相对 base、HashRouter、不注册 service worker。 `/ready` 只返回: ```json {"ready":true,"version":"0.1.0"} ``` 它不包含数据库路径、price 内容、账户数、错误详情或 secret。外部 gateway 可以把 404/非 200 视为插件缺失,但仍需执行部署级旁路探针。 ## 7. 查询、分页和导出 - 默认 `limit=50`,最大 `200`; - 使用 `(occurred_at, stable_id)` 编码的 opaque cursor,不使用大 offset; - cursor 绑定 endpoint、scope、排序和过滤条件,修改条件后 cursor 无效; - 默认时间范围 24 小时,普通交互最大 90 天; - 过滤字段使用 allowlist,未知字段返回 400; - 排序字段固定,禁止任意 SQL column; - 大导出使用受控异步任务/sidecar,不能在动态库 handler 中一次加载全表; - CSV 需要防 `=`, `+`, `-`, `@` 公式注入,并记录导出审计。 ## 8. 输入与资源限制 插件 handler 内执行: - 方法和精确路径二次校验; - Content-Type 校验; - strict JSON + 单文档 EOF; - 字段长度、集合数量、时间范围和分页上限; - 响应行数/字节预算; - context cancellation; - 不持锁执行 SQL、host callback 或网络。 但 CPA 当前在调用 handler 前执行 `io.ReadAll`,所以这些校验无法保护宿主免受超大 body。生产反向代理/CPA HTTP 层必须先限制: - Management body 建议不超过 1 MiB; - Resource GET 不接收 body; - Header/query 总大小由入口代理限制; - 请求读取超时和并发连接数在入口层限制。 ## 9. 当前 CPA Management JSON 转义限制 `internal/pluginhost/management.go` 会对看起来像 JSON 的响应递归执行 `html.EscapeString`。因此 `A & B ` 在 wire 上会变成 HTML entity;这不是标准 JSON API 应有的语义。 过渡规则: - cpa-ext Management JSON 响应头标记 `X-CPA-Ext-String-Encoding: html-entity-v1`; - 内置 UI 对**响应字符串**最多兼容 decode 一次,再通过 React text node 渲染; - 禁止 `dangerouslySetInnerHTML`; - IDs、error codes、cursor 只使用不受影响的安全字符; - 写 DTO 不能盲目回传未 decode 的读 DTO; - 测试覆盖 `A & B ` 往返和已含 entity 文本; - 在 CPA 停止改写 JSON 或 API 移到 sidecar 前,不将 Management API 宣称为无损的通用第三方 API。 Resource JSON 不经过该 escape 路径,仍然必须使用普通 JSON encoder 和文本渲染防 XSS。 ## 10. 安全头与浏览器策略 HTML: - `Content-Security-Policy`,默认 `default-src 'self'`,按实际构建最小放开; - `X-Content-Type-Options: nosniff`; - `Referrer-Policy: no-referrer`; - `Cache-Control: no-store`(HTML shell); - 不使用 inline script,或固定构建 hash; - 不信任 iframe parent 消息,除非校验明确 origin。 敏感 JSON: - `Cache-Control: no-store`; - 默认不启用 CORS; - 不在错误响应反射 Origin/Header/Body; - 不把 Management Key 或 downstream Key 写入前端持久存储。 完整要求见 [security.md](security.md)。 ## 11. 审计 以下写操作必须形成审计事件: - provision、签发、轮换、撤销; - 账户/套餐/路由/池修改; - 金额 adjustment/refund; - 价格发布/回滚; - 上游 Auth reconciliation; - quota reset credits 等有副作用 provider 操作; - backup、rebuild、导出; - 安全设置和热配置拒绝。 CPA 当前只有共享 Management Key,Actor 至少记录 management principal fingerprint、来源 IP 的受信代理结果、request ID、reason 和变更前后 revision。调用方自报 `actor` 只能作为 label,除非受信 gateway 已验证并覆盖该 header。 ## 12. 如何参考现有项目 ### 12.1 CLIProxyAPI - `sdk/pluginapi/types.go`:ManagementRequest/Response、Route/Resource; - `internal/pluginhost/management.go`:精确路径、未鉴权 resource、body ReadAll、JSON escape; - `internal/api/server_management.go`:Management middleware、Home 限制和 route mounting; - `examples/plugin/management-api/`:ABI/RPC 形状。 ### 12.2 `cpa-plugin-key-billing` - `internal/plugin/management.go`:精确 route 表、query/body 传 ID、严格 DTO; - `internal/plugin/admin.go`:价格、计划、Key 操作用例; - `internal/plugin/ui.html`:Management 页面调用方式。 吸收其固定路径和 handler 分派;不要继承错误响应泄露内部文本、浏览器持久化 Management Key 或 UI 直接承担业务原子性的做法。 ### 12.3 `cpa-usage-keeper` - `internal/api/router.go`:管理员/Key viewer 分离、no-store 与静态资源; - `internal/api/usage_*`:分页、范围和统计投影; - `internal/api/pricing*.go`、`quota.go`:领域 API; - `internal/api/request_limits.go`、`errors.go`:入口限制和脱敏错误; - API security tests:session、rate limit、secret redaction、CSP、request log token。 Keeper 是独立 HTTP 服务,Gin wildcard、middleware session 和后台任务接口不能假设在 CPA plugin route 中同样可用。 ## 13. 验收标准 - 所有注册 route 都是宿主允许的精确路径且无冲突; - Management 数据必须通过 CPA Management 鉴权; - resource 静态页不含敏感数据,用户 JSON 独立校验 downstream Key; - 用户不能通过 ID/query 越权读取其他 Credential/账户; - 所有金额 DTO 使用 currency + micros + display; - 用户 DTO 不出现 Token、价格规则和上游账户; - 写接口 strict decode、revision check、审计和必要幂等全部生效; - secret 只在首次签发/轮换响应显示,重放不创建第二把 Key; - 超大 body 在反向代理层被拒绝,不能依赖插件收到后再拒绝; - Management JSON entity 转义兼容测试通过,且限制被明确标记; - 列表 cursor 在并发插入下无重复/漏页; - panic/DB 失败返回脱敏 5xx,不泄露 SQL、路径或 secret; - Home 模式返回明确不支持,UI 不显示空数据假象; - API、Core、UI 的字段和错误码保持同一份实现定义。