chore: initialize standalone business plugin repository
Business Plugins CI / check (plugin-admin) (push) Successful in 3m13s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m41s

This commit is contained in:
Qiufeng
2026-08-27 23:36:08 +08:00
commit 5feae3ad41
59 changed files with 8950 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# Business Plugin V1 验收矩阵
| ID | 类别 | 验收项 | 预期证据 | 状态 |
|---|---|---|---|---|
| AUTH-01 | 鉴权 | Core 管理员登录控制面 | `plugins/plugin-admin/main_test.go:TestAdminLoginDoesNotExposeCoreTokens`;本地浏览器登录 | passed |
| AUTH-02 | 鉴权 | Core 2FA 登录 | challenge 一次性消费,成功创建会话 | passed |
| AUTH-03 | 鉴权 | 普通用户登录和 API | `plugins/plugin-admin/main_test.go:TestOrdinaryCoreUserIsRejected` | passed |
| AUTH-04 | 会话 | 过期、撤销、登出和刷新 | `plugins/plugin-admin/main_test.go:TestRefreshRevalidatesAdminRole` | passed |
| AUTH-05 | CSRF | 所有写请求 | `plugins/plugin-admin/main_test.go:TestMutationRequiresCSRFAndIdempotency` | passed |
| SEC-01 | 秘密 | 浏览器、URL、HTML、JS、LocalStorage、下载、日志 | 登录/配置测试断言 token 和 secret 不回显;浏览器 DOM 未出现 Core token | passed |
| SEC-02 | 出站 | Core URL、重定向、代理和 SSRF | `TestHealthProbeRejectsRedirectAndRequiresReadiness`;loopback URL 校验 | passed |
| SEC-03 | 脱敏 | Core 响应和错误 | token/password/secret/cookie 不出现在响应和日志 | passed |
| MAN-01 | 清单 | 未知字段、尾随 JSON、路径跳转 | `plugins/plugin-admin/internal/manifest/manifest_test.go`;包上传 smoke | passed |
| MAN-02 | 签名 | Ed25519、key ID、哈希 | `manifest_test.go:TestSignatureAndKeyID`;生产不受信发布者路径 | passed |
| MAN-03 | 兼容 | Core baseline、tested versions、capability | `manifest_test.go:TestCompatibility`;上传卡片显示 compatible | passed |
| LIFE-01 | 安装 | staging、原子切换、失败回滚 | `main_test.go:TestPackageInspectionAndAtomicInstall`;真实上传后 active revision 可见 | passed |
| LIFE-02 | 启停 | enable/disable/drain | 停用路径有 SIGTERM + drain 超时逻辑;进程组清理和外部服务不误停 | passed |
| LIFE-03 | 升级 | 新 revision 健康后切换 | 失败升级保留 active;模式切换不继承端点;成功提交后才切换进程 | passed |
| LIFE-04 | 卸载 | 先停用再卸载 | 先提交注册表删除,成功后再清理插件资源,不删除 Core 数据 | passed |
| MENU-01 | 菜单 | preview/apply 自有 `custom_menu_items` | `main_test.go:TestMenuPreviewAndApplyPreserveOtherMenuItems` | passed |
| MENU-02 | 嵌入 | iframe 和新窗口 | 本地控制面三视口登录/刷新;插件提供独立登录和新窗口入口 | passed |
| API-01 | allowlist | 未声明路径和查询参数 | `allowedCorePath` 单元路径门禁;业务插件自身 allowlist 测试 | passed |
| API-02 | Core 错误 | 401/403/409/429/5xx | 失败关闭、刷新一次、错误脱敏和请求 ID 传播 | passed |
| UI-01 | 响应式 | 425px、900px、1440px | 本地 Browser 验收:三个视口 `scrollWidth == innerWidth`,插件卡片可见 | passed |
| OPS-01 | 健康 | healthz/readyz、版本和 request ID | 控制面 HTTP smoke + 插件清单检查;健康/就绪响应含版本 | passed |
| OPS-02 | 权限 | 低权限账号、secret 文件和网络 | systemd 示例使用低权限账号、禁止提权、限制读写目录 | passed |
| REG-01 | 重建 | 清空 projection/cache | 控制面注册表可从磁盘恢复;健康 command/external 插件启动时重探 | passed |
| REG-02 | 兼容 | Core 升级/降级和旧插件 | 未测试版本保持 disabled,启动恢复再次检查 baseline | passed |
## 命令门禁
控制面和每个业务插件至少执行:
```sh
go test ./... -count=1
go vet ./...
node --check <all-ui-scripts>
production build
manifest verification
git diff --check
```
浏览器验收必须保存三种视口截图、网络敏感字段扫描结果、iframe/新窗口登录结果、刷新恢复、停用、升级和回滚证据。Mock Core 只能证明契约;具备测试环境时必须追加真实 Core 登录、2FA、权限、分页和错误联调。
## 本轮证据
- `plugins/plugin-admin` 和 `plugins/subscription-admin`:`go test -race ./...`、`go vet ./...`、`node --check ui/app.js` 均通过。
- `plugins/subscription-admin/package.sh` 生成的 `.s2plugin` 已通过 `unzip -t`,并通过控制面真实上传接口进入 `disabled` 状态。
- 本地浏览器登录后,控制面首页显示“已登记插件”与订阅插件卡片;425、900、1440 视口均无横向溢出。
- 仍需部署环境追加:真实生产签名密钥、跨实例共享会话、真实 Core iframe 刷新和跨节点升级演练;这些属于部署级验证,不改变本地 V1 控制面契约。
截图证据保存在 `.playwright-cli/plugin-admin-v1-final/`、`.playwright-cli/plugin-admin-v1-sensitive/`、`.playwright-cli/subscription-admin-v1-final/` 和 `.playwright-cli/subscription-admin-v1-sensitive/`,每组包含 425px、900px、1440px 三种视口。
## 本地生命周期硬化证据
- `plugins/plugin-admin/main_test.go:TestRecoverExternalPluginAfterRestart` 验证外部服务重启后重新探测并保持健康。
- `plugins/plugin-admin/main_test.go:TestRecoverCommandPluginAfterRestart` 验证托管 command 插件重启后重新分配端口、启动进程组并探测健康/就绪。
- `plugins/plugin-admin/main_test.go:TestPluginLockSerializesLifecycleMutations` 验证同一插件生命周期互斥。
- `plugins/plugin-admin/main_test.go:TestIdempotencyKeyRejectsDifferentOperationHash` 和 `TestIdempotencyKeyReplaysSameBodyAndRetainsFailedOperation` 验证服务端请求体指纹、失败终态保留与冲突拒绝。
- `plugins/plugin-admin/main_test.go:TestUpgradeDoesNotCarryEndpointAcrossServiceModes` 验证 command/external 模式不继承错误端点。
- `go test -race ./... -count=1`、`go vet ./...`、全部 UI/测试脚本 `node --check`、包构建、`manifestcheck`、`unzip -t` 和 `git diff --check` 已通过。
+73
View File
@@ -0,0 +1,73 @@
# Business Plugin V1 架构与流程图
## 拓扑
```mermaid
flowchart TD
B[管理员浏览器] --> C[Core custom_menu_items]
C --> I[Core /custom/:id sandbox iframe]
I --> P[反向代理 /extensions/:plugin-id/]
P --> M[Plugin Control Plane]
M --> S[独立业务插件服务]
S --> A[Typed Core API Adapter]
A --> K[Core Auth/Admin API]
K --> D[Core 权威账本与审计]
M --> R[Plugin Registry / Revisions / Audit]
M --> H[Supervisor + healthz/readyz]
```
## 登录时序
```mermaid
sequenceDiagram
participant B as Browser
participant P as Plugin BFF
participant C as Core
B->>P: POST /login
P->>C: POST /api/v1/auth/login
C-->>P: access/refresh 或 2FA challenge
P->>C: POST /api/v1/auth/login/2fa (按需)
P->>C: GET /api/v1/auth/me
C-->>P: role=admin
P-->>B: HttpOnly plugin session + CSRF token
B->>P: GET /api/plugins
P->>C: Bearer Core JWT + X-Request-ID
P-->>B: 脱敏业务数据
```
## 生命周期
```mermaid
stateDiagram-v2
[*] --> discovered
discovered --> verified: manifest/signature/hash pass
verified --> installed: atomic staging
installed --> disabled
disabled --> starting: enable
starting --> healthy: health + contract pass
starting --> error: timeout/failure
healthy --> draining: disable/upgrade
draining --> disabled
healthy --> upgrading: new revision
upgrading --> healthy: new revision pass
upgrading --> rollback_pending: new revision fail
rollback_pending --> healthy: old revision restored
verified --> incompatible: version/capability mismatch
```
## 数据边界
```text
Core authoritative data
user / admin role / balance / plan / subscription / order / usage / billing / audit
▲
│ typed HTTPS API, server-side token only
▼
Plugin projection and UI
cache / filters / display index / plugin audit reference
▲
│ controlled by
▼
Plugin control plane
manifest / signature / revision / health / config / menu ownership / session
```
+38
View File
@@ -0,0 +1,38 @@
# Business Plugin V1 边界
## Core 负责
- 用户身份、密码、2FA、TokenVersion、会话撤销和管理员角色;
- 余额、余额流水、套餐、订阅、订单、用量、配额、计费和退款;
- 网关鉴权、请求路由、原子预留/结算、幂等和核心审计;
- Core 数据库 schema、迁移和事务;
- 现有 `.s2plugin` transport ABI 及 OpenAI OAuth 生命周期;
- `custom_menu_items` 的最终校验和页面可见性。
## 控制面负责
- 业务插件清单、签名、公钥、包哈希和 Core 兼容性;
- 插件安装目录、revision、active 指针和旧版本保留;
- 独立服务进程的启停、drain、健康、升级和回滚;
- 插件配置加密、会话、CSRF、权限和控制面审计;
- 由插件声明的管理员菜单 preview/apply;
- 只允许服务端调用的 Core API Adapter。
## 业务插件负责
- 自己的业务 UI、HTTP API、BFF 和领域逻辑;
- 自己声明的 Core API allowlist、字段映射、分页和错误展示;
- 可删除、可重建的只读 projection;
- 自己的健康端点、版本报告和配置校验;
- 不影响 Core 的独立发布和回滚。
## 明确禁止
- 插件前端持有 Core JWT、refresh token、Admin Key 或服务 secret;
- 插件直连 Core PostgreSQL、Redis、宿主文件目录或内部 Go 包;
- 插件自行判断余额、权限、配额、计费或网关放行;
- 用多个 Admin API 拼接一个本应由 Core 原子事务完成的购买/扣款/续费;
- 把业务插件声明成 `openai.oauth.outbound_transport.v1`;
- 通过 iframe URL、LocalStorage、查询参数或日志传递认证凭据;
- 由插件卸载流程删除 Core 账本、订阅或审计数据;
- 在 V1 承诺无感 SSO、远程任意下载、OS 沙箱或跨插件 RPC。
+46
View File
@@ -0,0 +1,46 @@
# Business Plugin V1 开发指南
## 目录模板
```text
my-business-plugin/
├── cmd/plugin/main.go
├── internal/auth/
├── internal/coreadapter/
├── internal/manifest/
├── internal/domain/
├── ui/index.html
├── ui/assets/
├── business-plugin-manifest.v1.json
├── deploy/
├── test/contract/
└── README.md
```
入口服务只负责加载配置、启动 HTTP server 和健康端点。鉴权、Core API、领域逻辑、manifest 校验和 UI 不要全部写在入口文件。
## 开发顺序
1. 先复制 manifest 模板,声明唯一 `plugin_id`、capability、Core baseline、服务健康路径、UI 入口和精确 allowlist。
2. 实现 Core Adapter 的 typed methods;禁止接受浏览器传入的任意 URL。
3. 实现管理员登录、2FA、HttpOnly session、CSRF、refresh budget、登出撤销和日志脱敏。
4. 实现领域 API 和 UI;浏览器只调用插件 BFF,不读取 Core token 或宿主存储。
5. 提供 `healthz`、`readyz`、版本和 request ID;失败时保持 fail-closed。
6. 由控制面执行签名、哈希、兼容性、启停、升级和回滚;插件服务不自行覆盖其他插件资源。
7. 用 deployment-managed `custom_menu_items` preview/apply 注入管理员入口。
## UI 约束
业务插件页面遵循 `docs/FRONTEND_DESIGN_GUIDELINES.md` 和 `UI.MD` 的控制高度、无衬线数字、响应式表格、无卡片嵌套和安全错误展示要求。iframe 与新窗口都必须可用;独立 origin 按部署配置 Cookie `SameSite=None; Secure`,同源反代优先使用 `Lax`。
## 测试最小集
- manifest 未知字段、尾随数据、路径穿越、签名和哈希;
- 管理员、普通用户、2FA、撤销、刷新、登出和 CSRF;
- Core API allowlist、查询过滤、401 单次 refresh、429/5xx 策略;
- secret 不出浏览器/日志/错误;
- health/readiness、启停、drain、升级、回滚和卸载;
- iframe、新窗口、刷新恢复、425/900/1440px 截图和横向溢出;
- 清空插件投影后从 Core 重建。
订阅插件的套餐、余额、订阅实例、同档多实例、单独续费和余额事务规则只写在订阅领域文档,不复制到本通用指南。
+212
View File
@@ -0,0 +1,212 @@
# Sub2API 独立业务插件框架 V1
状态:Accepted Contract / V1 参考实现已完成本地验收
本文定义与 Sub2API Core 解耦的通用业务插件框架。业务插件是独立服务、独立端口、独立版本和独立 UI;它可以通过现有管理员自定义菜单嵌入 Core,也可以在新窗口运行。订阅管理只是一个可选业务插件,不能成为框架后台、Core 热路径或插件生命周期的固定组成部分。
## 1. 目标
V1 需要提供一个独立的插件控制面,负责:
- 展示已登记、已安装和可升级的业务插件;
- 校验包清单、发布者签名、文件哈希和 Core 兼容范围;
- 安装、启用、停用、健康检查、升级、回滚、卸载和配置插件;
- 保存插件版本、服务地址、运行状态、菜单声明和操作审计;
- 通过 Core 现有管理员鉴权复用操作者身份;
- 将已启用插件的管理员菜单注入 `custom_menu_items`;
- 让每个业务插件仅通过自己的 BFF 调用 Core 明确允许的 API。
V1 不改变 Core Go/Vue、数据库迁移、现有鉴权、前端路由或 `.s2plugin` transport ABI。控制面自己的注册表、安装目录、进程和配置存储属于独立服务;它不连接 Core PostgreSQL、Redis 或宿主业务表。
## 2. 与现有插件的关系
| 类型 | 现有 `.s2plugin` transport | Business Plugin V1 |
|---|---|---|
| 运行方式 | Core 子进程 + gRPC | 独立服务 + HTTP/BFF |
| 能力 | `openai.oauth.outbound_transport.v1` | 由清单声明的业务能力 |
| 生命周期 | Core `PluginManager` | 独立 Plugin Control Plane |
| UI | 配置 iframe + UI Bridge | 业务后台/用户工具页面 |
| 数据边界 | Core 负责账号转发与计费 | Core 负责权威业务数据,插件只读/投影 |
| 菜单 | Core 固定插件管理页 | `custom_menu_items` 管理员入口 |
现有 `.s2plugin` 的 `TransportPlugin`、清单 schema 和 capability 校验保持不变。业务插件不能仅通过声明一个新 capability 假装获得 HTTP 路由、数据库、支付或订阅权限。
## 3. V1 拓扑
```text
管理员浏览器
│ Core 登录后的管理员菜单
▼
Core /custom/<menu-id>
│ sandbox iframe 或新窗口
▼
反向代理 /extensions/<plugin-id>/
▼
Business Plugin Control Plane
├─ 插件目录、签名、版本和状态
├─ 独立进程 supervisor
├─ 管理员会话和审计
└─ 插件 BFF / Core API Adapter
│ 仅发送服务端 Bearer Core JWT + X-Request-ID
▼
Sub2API Core 现有鉴权、Admin API 和领域账本
```
控制面可以托管插件进程,也可以把进程交给 systemd、容器或 Kubernetes;无论采用哪种 supervisor,控制面都必须能读取健康状态并保留旧版本回滚点。停用时先撤销自有菜单,再将托管进程置于有界终止流程(SIGTERM,最多等待 10 秒后强制结束);反向代理负责停止新请求,外部服务只做健康探测并由其部署者负责停机。
## 4. 角色和权限
- `plugin_admin`:安装、启停、升级、回滚、卸载、配置和菜单注入。
- `plugin_operator`:查看状态、日志摘要和健康诊断,不改变包或凭据。
- `plugin_readonly`:只读查看已登记插件。
V1 的控制面只允许 Core `role=admin` 登录。插件不创建第二套 Core 用户表;插件会话只保存 `plugin_id`、`admin_user_id`、角色、会话版本和过期时间。UI 隐藏按钮不等于授权,控制面和 Core API 均须重新校验权限。
## 5. 管理面契约
控制面内部 API 的最小集合:
```text
GET /healthz
POST /login
POST /login/2fa
POST /logout
GET /api/me
GET /api/plugins
GET /api/plugins/{id}
POST /api/plugins/{id}/install
POST /api/plugins/{id}/enable
POST /api/plugins/{id}/disable
POST /api/plugins/{id}/upgrade
POST /api/plugins/{id}/rollback
POST /api/plugins/{id}/uninstall
GET /api/plugins/{id}/config
PUT /api/plugins/{id}/config
GET /api/audit
POST /api/menu-items/preview
POST /api/menu-items/apply
```
所有写请求要求 CSRF、操作者会话和幂等键。幂等指纹由服务端根据方法、路径、查询和请求体计算,失败操作也保留终态,重复请求不会重新执行。安装、启用、升级、回滚和卸载必须返回 operation ID,并可通过插件详情查询最终状态。控制面不把上述路径注册到 Core,也不声称 Core 已存在 `/api/v1/plugin-host/*`。
## 6. 插件生命周期
```text
discovered -> verified -> installed -> disabled -> starting -> healthy
│ │
│ └── error
└── incompatible
healthy -> draining -> disabled
healthy -> upgrading -> healthy
healthy -> rollback_pending -> healthy
```
- `discovered`:目录或清单被发现,尚未验签。
- `verified`:清单、签名、哈希和兼容性通过。
- `installed`:版本包已安全写入 staging 并原子切换。
- `disabled`:已安装但不接收业务请求,菜单默认隐藏。
- `starting`:进程启动、端口和健康检查进行中。
- `healthy`:健康端点、就绪端点和(若响应提供)版本检查通过。
- `draining`:菜单已撤销,托管进程正在执行有界终止;在途请求由插件进程或反向代理按部署约定处理。
- `upgrading` / `rollback_pending`:新旧版本并存检查,只有健康版本成为 active。
- `error`:进程、健康、配置或 Core 合约失败,默认 fail-closed。
- `incompatible`:当前 Core baseline 或协议不满足清单要求。
已启用版本不能被原地覆盖。升级先安装新 revision、执行健康和契约检查,再原子更新 active revision;至少保留一个可回滚 revision。卸载只能作用于停用插件,不删除 Core 数据。控制面重启时重新校验 `healthy`/`enabled` 插件:托管 command 重新启动 active revision,外部服务重新探测 `service_url`;探测失败统一标记 `error` 并清空不可用端点。`backend.command` 与外部 `service_url` 是互斥运行模式,升级不会继承不属于新模式的旧端点。
## 7. 安装和包安全
安装包必须包含清单和 UI;若插件由控制面托管进程,再额外包含清单声明的服务文件:
```text
manifest.json
signature.json
ui/index.html
ui/assets/...
```
外部服务模式允许省略 `service/` 文件,但清单不得声明
`backend.command`,且启用前必须在控制面配置经过校验的 `service_url`。托管进程模式
必须声明 `backend.command`,并将该路径及二进制哈希放入包内。
控制面必须拒绝绝对路径、父目录跳转、重复条目、符号链接、未声明文件、超大文件和不匹配哈希。签名使用 Ed25519,签名覆盖 `manifest.json` 原始字节;清单中的 SHA-256 覆盖服务文件和 UI。发布者私钥不进入仓库、包、服务器或日志。
安装使用临时目录和原子 rename;失败不得破坏 active revision。包来源默认是管理员上传或受控本地目录,V1 不自动从互联网下载任意包。
## 8. Core API Adapter
每个插件清单声明精确的 `method + path` allowlist。控制面或插件 BFF 只能调用这些路径:
```http
POST /api/v1/auth/login
POST /api/v1/auth/login/2fa
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
GET /api/v1/settings/public
GET /api/v1/admin/<declared-read-endpoint>
```
`<declared-read-endpoint>` 只是文档占位符,实际清单必须列出具体路径、查询参数、分页上限和响应字段。浏览器永远不接触 Core JWT、refresh token 或 Admin Key;所有出站请求由服务端添加 `Authorization: Bearer ...` 和统一 `X-Request-ID`。
Core 返回 `401` 时,一次用户请求最多 refresh 一次;并发 refresh 必须按插件会话串行化。`403` 不重试,`429` 按 `Retry-After` 有上限退避,`5xx` 只重试明确幂等操作。响应按 DTO 或敏感键规则脱敏,不能把 token、密码、Cookie、secret 或完整凭据透传给 UI。
## 9. 会话和嵌入
插件登录调用 Core 现有 `/auth/login`、按需 `/auth/login/2fa`,再调用 `/auth/me` 校验管理员角色。Core token 只存插件服务端会话,浏览器只持有 HttpOnly、Secure、SameSite Cookie 和插件 CSRF token。
Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core `localStorage` 登录态。因此 V1 必须同时提供新窗口入口;iframe 首屏显示插件登录页是已知行为。真正无感 SSO 需要 V1.1 的一次性 code/state 或受控 `postMessage` 交接,不得把 JWT 放在 URL。
菜单注入使用 Core 现有 `custom_menu_items`:
```json
{
"id": "DOMAIN_PLUGIN_ID",
"label": "DOMAIN_PLUGIN_LABEL",
"url": "https://CORE_ORIGIN/extensions/DOMAIN_PLUGIN_ID/",
"visibility": "admin",
"sort_order": 200
}
```
控制面只能创建和更新自己声明的 ID,保留其他管理员菜单;应用前展示 diff 并记录审计。停用或卸载时先隐藏/移除自己拥有的菜单项,再停止进程。
## 10. 数据归属
Core 始终是用户身份、余额、订阅、订单、用量、权限、计费和审计的权威来源。控制面只保存插件包、revision、状态、服务配置、菜单声明、会话和操作索引。业务插件可以保存可删除、可重建的 projection,但 projection 不得作为 Core 网关放行、扣费或权限判断依据。
## 11. 安全和可运维性
- 监听地址默认 loopback,生产通过 HTTPS 反向代理暴露。
- 插件进程使用独立低权限账号/容器、最小文件权限和出站网络 allowlist。
- 配置 secret 使用服务端加密存储或 secret manager,UI 只显示 configured/rotatable,不回显原值。
- 日志只记录插件 ID、revision、操作者、operation ID、资源 ID、request ID、状态和耗时。
- 进程崩溃、健康失败、Core 不兼容或签名错误均 fail-closed,不静默切换到未验证版本。
- 停用、升级、回滚和卸载必须可重复执行;Core 数据不随插件卸载删除。
## 12. 分阶段实施
### Phase 0:契约冻结
冻结 manifest、签名、revision、控制面 API、状态机、反代路径、菜单字段、会话属性和 Core API allowlist。
### Phase 1:通用控制面
实现管理员登录、插件目录、包校验、安装/启停、健康检查、配置加密、审计、菜单 preview/apply、systemd/container 适配和回滚骨架。
### Phase 2:业务插件适配
提供 `DOMAIN_PLUGIN_ID` 级别的 SDK/模板和契约测试。订阅管理作为首个独立业务插件接入,只实现自身领域页面和 Core 只读 API,不改变控制面。
### Phase 3:生产增强
评审多实例共享状态、短时 Plugin Access Token、无感 SSO、Core Host Adapter、远程 registry、灰度升级和跨节点 drain;这些能力需要单独版本和安全评审。
## 13. 非目标
- 不把业务插件注册为现有 OpenAI OAuth transport。
- 不把任意业务路由、数据库、支付、余额扣款或每请求计费放进控制面。
- 不创建 Core 用户表、插件版账本或绕过 Core 鉴权。
- 不通过 URL、iframe、LocalStorage、HTML、日志或下载文件传递凭据。
- 不承诺独立进程是操作系统级沙箱。
+78
View File
@@ -0,0 +1,78 @@
# Business Plugin Manifest V1
状态:Accepted Contract
业务插件清单由独立控制面读取和校验,Core 当前不会读取它。清单只声明插件身份、版本、能力、服务、UI、兼容性、发布者和 Core API 权限,不授予数据库、路由或 secret 权限。
## 1. 最小清单
```json
{
"schema_version": 1,
"plugin_id": "DOMAIN_PLUGIN_ID",
"name": "DOMAIN_PLUGIN_NAME",
"version": "1.0.0",
"core_api_baseline": "sub2api-0.1.183",
"tested_core_versions": ["0.1.183"],
"capabilities": ["DOMAIN_CAPABILITY_V1"],
"backend": {
"listen_env": "PLUGIN_PORT",
"health_path": "/healthz",
"readiness_path": "/readyz"
},
"ui": {
"entrypoint": "/admin/",
"menu": {
"id": "DOMAIN_PLUGIN_ID",
"label": "DOMAIN_PLUGIN_LABEL",
"visibility": "admin",
"sort_order": 200
}
},
"publisher": {
"key_id": "PUBLISHER_KEY_ID"
},
"core_api_allowlist": [
"POST /api/v1/auth/login",
"POST /api/v1/auth/login/2fa",
"POST /api/v1/auth/refresh",
"POST /api/v1/auth/logout",
"GET /api/v1/auth/me",
"GET /api/v1/settings/public",
"GET /api/v1/admin/DOMAIN_READ_ENDPOINT"
]
}
```
## 2. 字段规则
- `plugin_id`:小写、稳定、全局唯一;不得包含 `/`、空格或路径跳转。
- `version`:插件自身 SemVer,与 Core 版本和发行标签分离。
- `core_api_baseline`:插件编译和契约测试所针对的 Core 版本。
- `tested_core_versions`:只填写真实执行过契约测试的版本。
- `capabilities`:一个或多个业务能力 ID;控制面不为未实现的能力自动创建路由。
- `backend.health_path`、`readiness_path`:只能是插件服务根下的绝对 HTTP 路径(例如 `/healthz`、`/readyz`),不得包含主机、查询、片段或路径跳转。
- `ui.entrypoint`:只能指向包内 UI 资源。
- `ui.menu`:管理员菜单声明;控制面必须校验 ID 与插件 ID 绑定,`visibility` 只能是 `admin`。
- `publisher.key_id`:必须匹配受信任发布者配置。
- `core_api_allowlist`:方法和路径必须逐项列出,不允许通配符、任意 URL 或未声明查询参数。
## 3. 签名和哈希
```json
{
"algorithm": "ed25519",
"key_id": "PUBLISHER_KEY_ID",
"signature": "BASE64_SIGNATURE"
}
```
签名覆盖 `manifest.json` 原始字节。包内每个服务文件和 UI 文件的 SHA-256 由清单声明。验证器必须拒绝尾随 JSON、重复字段、未知字段、无效 Base64、大小写错误的哈希和不匹配的 key ID。
## 4. 兼容性门禁
1. 清单 schema 版本不匹配:`incompatible`。
2. Core 不在 `requires` 范围:`incompatible`。
3. Core 在范围内但未列入 `tested_core_versions`:安装后保持 disabled,要求管理员确认。
4. 业务能力、服务协议或 UI 版本不支持:禁止启用。
5. 升级只产生新 revision;旧 active revision 在新版本健康和契约测试通过前保持可用。
+73
View File
@@ -0,0 +1,73 @@
# Business Plugin V1 实现边界与验收
状态:控制面入口已实现;业务插件按包独立接入
## 1. 唯一入口
`plugins/plugin-admin` 是通用 Business Plugin V1 控制面。它的首页和默认菜单只能表达“插件管理”,不表达任何具体业务域,也不默认打开订阅、支付或其他业务页面。
控制面负责:
- 展示已登记、已安装和可升级的插件包;
- 校验清单、签名、文件哈希和 Core 兼容性;
- 安装、启用、停用、升级、回滚、卸载和配置;
- 显示运行状态、健康检查结果和操作审计;
- 对插件声明的管理员菜单执行预览和应用。
控制面不负责:
- 订阅商品、余额、订单、支付、配额或请求计费;
- 任何业务插件自己的页面和领域数据;
- Core 数据库、Core 用户表或 `.s2plugin` transport ABI。
## 2. 安装后注入流程
```text
管理员登录 plugin-admin
|
v
插件目录 -> 上传/选择业务插件包
|
v
清单 + 签名 + 哈希 + Core 兼容性校验
|
v
安装到独立 revision,初始为 disabled
|
v
启用 -> 启动独立服务端口 -> healthz/readyz/版本检查
|
v
菜单预览 -> 管理员确认 -> 应用 custom_menu_items
|
v
Core 管理员菜单出现该插件自己的入口
```
每个插件使用自己的 `plugin_id`、版本、端口、服务进程、UI 和菜单 ID。停用或卸载插件时,只移除该插件自己声明的菜单项,不触碰其他插件或 Core 数据。
## 3. 订阅插件的位置
`plugins/subscription-admin` 是第一个业务插件样例,而不是控制面。它只有在管理员通过 `plugin-admin` 安装、启用并应用菜单后才出现。卸载订阅插件只删除插件资源和自身投影,不删除 Core 的套餐、订阅、余额、订单、用量或审计。
订阅插件的 V1 只读取 Core 现有管理员 API;余额购买、续费、撤销和退款写操作必须等待版本化 Core 原子接口,不得把多个 Admin API 拼成一次购买。
## 4. 登录与安全边界
- 控制面和业务插件均复用 Core 管理员登录及 2FA,不创建插件用户表;普通 Core 用户统一拒绝。
- 浏览器只持有插件自己的 HttpOnly 会话和 CSRF token;Core access/refresh token、Admin Key 只存在插件服务端。
- 插件后端通过精确 allowlist 调用 Core API,不提供任意 URL 代理,不连接 Core PostgreSQL/Redis。
- 插件包必须签名;未签名包仅限开发环境 loopback 测试。
## 5. 功能验收最小条件
1. 打开 `plugin-admin` 首屏看到插件目录,而不是订阅页面。
2. 未安装订阅包时,目录可以为空,左侧不会出现订阅菜单。
3. 安装并启用一个包后,详情页显示该包的状态、版本和健康结果。
4. 菜单预览只新增该包自己的菜单项;应用后 Core 管理员菜单才出现该入口。
5. 停用或卸载后入口消失,其他菜单保持不变。
6. 425px、900px、1440px 三种视口均无横向溢出、遮挡或凭据泄漏。
## 6. 后续插件模板
新增业务插件只需提供独立清单、服务、UI、Core API allowlist 和菜单声明,并遵守本文件的安装生命周期。插件管理控制面不因新增业务域而增加订阅、支付或其他领域分支。
+393
View File
@@ -0,0 +1,393 @@
# Sub2API 独立业务插件框架 V1 RFC
状态:V1 通用插件控制面参考实现已落库;订阅业务插件只读适配与 Core Host Adapter/写操作仍为 Draft
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 `plugins/plugin-admin`,它负责插件清单、签名、安装、启停、升级、回滚、卸载、配置、审计和菜单注入;控制面本身不是订阅后台。每个业务插件(包括独立的 `plugins/subscription-admin`)作为可选的独立服务运行在自己的端口,通过控制面安装后再由部署层反向代理和现有“管理员可见自定义菜单”嵌入 Sub2API 页面。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
V1 已实现范围以 `plugins/plugin-admin` 控制面和本文“当前实现范围”章节为准;本文中的订阅业务插件只是首个适配样例。Core Host Adapter、短时 Plugin Access Token、无感 SSO 和余额写操作仍是后续版本设计,不代表当前 Core 已提供这些接口。
本文不是现有 `.s2plugin` 协议的直接改版。现有 `.s2plugin` 继续只负责 `openai.oauth.outbound_transport.v1`。本 RFC 的 V1 是部署级业务插件约定,不向当前 Core 增加新的路由、数据表或业务 RPC。
## 0. 约束与可行性边界
本版本必须同时满足以下约束:
- 不修改 Sub2API 的 Go、Vue、数据库迁移和现有鉴权实现;
- 不在插件内建立 Core 用户表,管理员身份以 Core 现有账号和角色为准;
- 插件后端独立监听 `PLUGIN_PORT`,由 Nginx/Caddy/Traefik 等部署层转发;
- 通过 Core 已有 `custom_menu_items` 配置添加 `visibility=admin` 的 iframe 菜单入口;
- 插件只在服务端调用 Core 现有 API,浏览器不持有 `x-api-key` 或 Core JWT;
- 第一阶段只搭框架、登录、权限、健康检查、嵌入和只读联调,不实现订阅购买写操作。
现有自定义菜单可以完成“把插件页面显示在 Sub2API 管理页面内”,但现有 iframe 使用 sandbox,且 Core 前端 JWT 保存在 `localStorage`,不会自动注入跨端口 iframe。因此在完全不改 Core 的前提下,V1 的登录方式是:插件登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 `role=admin` 后只保留短时插件会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表,但不是无感知的当前页面会话共享。
如果以后要求“已登录 Core 后打开 iframe 立即无感登录”,需要一个很小的 Core 一次性登录交接接口或前端 `postMessage` 适配;这属于 V1.1,不应通过 URL 明文传递 JWT。反向代理只改变网络路径,不改变这一认证边界。
## 1. 决策摘要
### 1.1 推荐拓扑
```text
管理员浏览器
│ Core 页面中的 admin 自定义菜单
▼
Core `/custom/PLUGIN_ID`
│ 现有 sandbox iframe;只加载插件 UI,不共享 Core 存储
▼
反向代理 `/extensions/PLUGIN_ID/*`
│ 转发到独立服务 `127.0.0.1:PLUGIN_PORT`
▼
业务插件后台
├─ `/login` 接收管理员登录请求
├─ 服务端调用 Core `/api/v1/auth/login`(需要时调用 `/login/2fa`)
├─ 调用 Core `/api/v1/auth/me`,确认 `role=admin`
├─ 建立插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
└─ 通过 Bearer Core JWT 调用现有 Core Admin API
▼
Sub2API Core 现有认证、Admin API 和订阅/余额账本
```
插件后台是独立服务,不以高权限子进程形式嵌入核心,也不直接连接核心数据库。V1 通过部署层完成端口映射,通过 Core 现有登录/2FA、`/auth/me` 和 Admin API 完成功能联调;浏览器永远不接触 Admin Key,也不接触 Core JWT。插件后端不建立用户表,只维护短期会话(单实例可使用内存,多实例可使用插件自己的 Redis/会话存储)。这里的前提是插件属于受信任的内部服务:登录密码会在一次请求中经过插件后端再转交 Core,但不落库、不写日志;若要求插件进程也完全接触不到密码,需要另行引入 Core SSO/OIDC。
如果部署环境只允许服务到服务调用,也可以把 Core Admin API Key 放在插件后端 secret 中作为临时 BFF 凭据;这时所有命令都以该 Key 对应的管理员身份执行,属于单一服务身份模式,不等同于当前浏览器管理员的 SSO,也不替代 V1 的管理员登录方案。
### 1.2 V1 的默认范围
- 插件拥有独立后台和独立发布版本。
- 只允许管理员登录;普通用户访问插件后台一律拒绝。
- 首版支持管理员登录、权限校验、健康检查、嵌入和只读订阅数据展示。
- 余额购买命令暂不实现,待框架验收后再复用现有订阅逻辑设计原子入口。
- 现有 `.s2plugin` 传输插件 ABI、路由和生命周期保持不变。
- 不实现插件任意注册核心路由、任意执行 SQL、任意读取宿主 Cookie、任意注入 Vue 路由或任意修改 Core 菜单组件。
## 2. 为什么不复用现有 `.s2plugin`
现有插件协议的能力是 `GetInfo`、`Health`、配置读写、配置测试和 `Forward` HTTP 流。它的清单只接受 `openai.oauth.outbound_transport.v1`,UI 也只是无管理员 Token 的配置 iframe。
因此它不适合安全承载以下业务:
- 管理员登录和角色授权;
- 订阅商品、余额购买和订单审计;
- 核心数据库事务或迁移;
- 用户页面、菜单、任意 HTTP 路由和支付/订阅回调。
如果把这些能力硬塞入现有协议,就会同时改变进程 ABI、权限模型、数据库边界和前端路由,反而扩大与上游的冲突面。V1 采用“外部业务插件 + Core 现有 API 适配器”作为清晰的新边界;需要 Core 新增接口的部分另列为 V1.1。
## 3. 术语和边界
| 术语 | 定义 |
|---|---|
| Core | Sub2API 主服务,拥有用户、余额、Group、Key、订阅和用量账本。 |
| Business Plugin | 独立部署的后台服务,提供一个业务域的管理 UI 和 BFF。 |
| Plugin UI | 由 Business Plugin 提供的页面,只调用自己的后端。 |
| Plugin Backend | Business Plugin 的服务端,保存插件配置、会话和操作幂等记录。 |
| Core API Adapter | V1 插件后端对 Core 现有 REST API 的服务端客户端,只允许访问明确的认证、管理员和只读业务端点。 |
| Future Host Adapter | V1.1 以后、需要修改 Core 才能提供的版本化业务 API;不属于本次无 Core 改动的实现范围。 |
| Plugin Session Credential | 插件自己的 HttpOnly 会话标识,不等于 Core JWT 或全局 Admin Key。 |
| Capability | 插件声明的业务能力,例如 `subscription.admin.v1`。 |
| Projection | 插件保存的只读或可重建副本,不是核心账本的权威数据。 |
## 4. 认证与权限模型
### 4.1 管理员登录
V1 采用“插件会话 + Core 现有登录”的两层模型,不创建插件用户表:
1. 浏览器打开插件 `/login`,只向插件后端提交 Core 管理员凭据和必要的 2FA 信息。
2. 插件后端服务端调用 Core `POST /api/v1/auth/login`;需要二次验证时继续调用 `POST /api/v1/auth/login/2fa`。
3. 插件后端用返回的 Core access token 调用 `GET /api/v1/auth/me`,确认用户状态正常且 `role=admin`。
4. 插件后端建立自己的短时 HttpOnly 会话;Core access/refresh token 只保存在插件服务端的会话存储中,不回传浏览器。
5. 插件业务请求只携带插件会话 Cookie,插件后端再用对应管理员的 Core Bearer token 调用允许的 Core API。
```text
浏览器 -> Plugin /login(插件会话 Cookie 尚未建立)
Plugin -> Core /api/v1/auth/login
Plugin -> Core /api/v1/auth/login/2fa(按 Core 返回的要求)
Plugin -> Core /api/v1/auth/me(确认 role=admin)
Plugin <- 建立 HttpOnly 插件会话
浏览器 -> Plugin /admin/*(只带插件会话 Cookie)
Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
```
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,插件不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示插件登录页;由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,插件必须提供“新窗口打开插件”入口作为稳定登录路径。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
### 4.2 权限判定
- 默认拒绝所有普通用户、未登录用户和过期会话。
- 插件会话只包含 `plugin_id`、`admin_user_id`、角色、权限版本、签发时间和过期时间。
- V1 只定义 `plugin_admin` 角色;后续可增加 `subscription_operator`、`subscription_readonly`。
- 插件后端每次命令都携带对应 Core 管理员的 Bearer token,不使用“系统管理员”固定身份代替实际操作者(仅使用临时 Admin Key 的过渡模式除外)。
- Core 对每个现有 Admin API 操作再次做管理员角色、会话撤销和资源级授权,不把插件 UI 的按钮隐藏当作授权依据。
- 所有写操作要求 CSRF 防护、审计记录和幂等键;敏感操作可要求 step-up。
### 4.3 会话要求
- 会话 Cookie 必须 `HttpOnly`、`Secure`、`SameSite=Lax` 或更严格。
- 生产环境只允许 HTTPS;反向代理必须正确传递原始 Host 和协议。
- 登录使用短时一次性 state,绑定浏览器会话并防重放。
- 默认会话有效期 30 分钟,滑动续期上限 8 小时;登出立即撤销服务端会话。
- 独立 origin 嵌入时按浏览器策略使用 `SameSite=None; Secure`,同源反代优先使用 `Lax`;必须在目标浏览器验证刷新、退出和第三方 Cookie 行为。
- 插件 UI 不把 Core JWT、Admin Key 或服务凭据写入 LocalStorage、URL、HTML、日志或错误提示。
## 5. Admin Key 与服务凭据安全
### 5.1 绝对禁止的做法
- 不把全局 `x-api-key` 注入浏览器。
- 不把 Admin Key 放进插件静态 JS、HTML、iframe、URL query、下载文件或前端 sourcemap。
- 不让插件 UI 直接请求 Core Admin API。
- 不把 Admin Key 写入普通业务日志、请求追踪、错误响应、数据库明文或备份导出。
- 不让插件直接连接 Core PostgreSQL、Redis 或宿主文件目录。
### 5.2 V1 凭据分级
| 环境 | 凭据 | 用途 | 约束 |
|---|---|---|---|
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到插件登录页,不写入代码、配置和日志。 |
| 测试 | Core 返回的 access/refresh token | 建立插件服务端会话 | 只存插件服务端会话存储,短 TTL,测试 Core 与测试管理员专用。 |
| 生产 | Core 返回的 access/refresh token | 代表实际登录的 Core 管理员调用现有 Admin API | 服务端加密保存或内存保存,按 Core TokenVersion/撤销结果失效;不回传浏览器。 |
V1 不要求新增 Plugin Access Token 服务,也不要求修改 Core。`ADMIN_API_KEY` 只作为服务到服务 BFF 的应急/测试凭据:它必须只存在插件后端 secret manager、受限环境变量或权限为 `0600` 的 secret 文件中,并由 Core Admin API 的 endpoint allowlist 限制。使用 Admin Key 时所有请求都以同一个管理员身份执行,审计粒度是服务身份级别,不得作为插件登录态下发给浏览器。
V1.1 如需在不保存 Core refresh token 的情况下运行,再设计 Core 签发的短时、限 scope Plugin Access Token;在 Core 提供该能力前,文档只把它视为未来接口。
### 5.3 凭据生命周期
1. 插件后端接收管理员登录请求,但不保存密码。
2. 插件后端调用 Core 登录/2FA,保存返回 token 到服务端会话,并绑定 `admin_user_id`。
3. 每次 Core 请求都使用 TLS 和 Core Bearer token;Core 继续校验签名、TokenVersion、会话绑定和角色。
4. access token 过期时只使用对应 refresh token 调用 Core `/auth/refresh`;刷新失败就销毁插件会话并要求重新登录。
5. 登出、Core 管理员撤销会话、停用插件或发现泄露时,立即删除插件会话;Admin Key 过渡模式由运维轮换并撤销。
### 5.4 服务端防护
- Core 现有 Admin API 由自身 `adminAuth`、JWT 会话和管理员角色保护;插件后端再使用出站 allowlist,只能访问事先批准的 Core API 路径。
- 插件后端不接受任意 URL 转发,也不把 Core API 代理能力暴露给插件 UI。
- 请求设置超时、重试上限和熔断;禁止在超时后盲目重放非幂等命令。
- 日志对 `Authorization`、`x-api-key`、Cookie、session、余额和个人信息做结构化脱敏。
- 记录 `plugin_id`、操作者、scope、资源 ID、幂等键和结果,不记录 secret 原文。
- 生产插件运行在独立低权限账号或容器中,限制文件、网络、系统调用和环境变量。
## 6. Core API Adapter V1(使用现有接口)
本节描述在“不修改 Sub2API”前提下插件可以使用的最小集成面。它不是 Core 已注册的插件协议,而是插件后端对现有 HTTP API 的严格 allowlist;实现前应根据目标版本在插件配置中冻结路径和响应字段。
### 6.1 认证头
```http
Authorization: Bearer CORE_ACCESS_TOKEN
X-Request-Id: UNIQUE_REQUEST_ID
```
`CORE_ACCESS_TOKEN` 只允许由插件后端的会话适配器发送。插件浏览器、静态资源和 iframe 消息中不得出现该 token。
### 6.2 V1 允许的接口类别
```text
POST /api/v1/auth/login
POST /api/v1/auth/login/2fa
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
GET /api/v1/settings/public
GET /api/v1/admin/payment/plans
GET /api/v1/admin/subscriptions
GET /api/v1/admin/subscriptions/{id}
GET /api/v1/admin/users/{id}
GET /api/v1/admin/users/{id}/subscriptions
```
实际插件先只开放只读管理员接口;不得把路径中的 `<read-only-endpoint>` 当作通配符,部署配置必须列出具体路径、方法和分页上限。现有 Core 没有 `/api/v1/plugin-host/*` 路由,V1 不调用或宣称该路径已存在。
### 6.3 V1 写操作边界
V1 框架和第一版订阅插件不实现余额扣款、订阅续期或撤销写操作。现有 Admin API 的多个调用不应拼成一笔购买,因为这样无法保证余额、订单、订阅和审计的单事务一致性。
未来要支持写操作,必须先在 Core 中增加版本化 Host Adapter 或等价的原子命令接口(需要 Core 代码、路由、审计和幂等存储变更),再由插件接入;这属于 V1.1,不是本轮“零 Core 改动”的交付物。
### 6.4 错误和重试
| HTTP | 含义 | 插件行为 |
|---|---|---|
| `401` | 凭据无效或已撤销 | 停止重试,提示重新注册/轮换。 |
| `403` | 管理员角色、会话或资源权限不足 | 不重试,记录操作者和资源。 |
| `409` | 幂等键冲突或业务状态冲突 | 查询 operation 状态后展示最终结果。 |
| `422` | 参数或余额业务校验失败 | 展示可读错误,不重试。 |
| `429` | Core 限流 | 按 `Retry-After` 有上限地重试。 |
| `5xx` | Core 暂时故障 | 只对明确幂等命令重试,指数退避。 |
## 7. 插件注册、清单与生命周期
### 7.1 与 `.s2plugin` 分离
Business Plugin V1 不直接套用现有 `manifest.schema.json`。建议新增独立的业务插件清单,例如 `business-plugin-manifest.v1.json`:
```json
{
"schema_version": 1,
"plugin_id": "example.subscription",
"name": "Example Subscription Admin",
"version": "0.1.0",
"core_api_baseline": "sub2api-0.1.183",
"capabilities": ["subscription.admin.v1"],
"tested_core_versions": ["0.1.183"],
"backend": { "health_path": "/healthz" },
"ui": { "entrypoint": "/admin" },
"publisher": { "key_id": "publisher-key-id" }
}
```
清单只声明能力和兼容范围,不授予数据库、路由或 secret 权限。V1 由部署脚本/反向代理保存清单、校验签名和允许的 Core API 路径;当前 Core 不会读取该清单,也没有业务插件注册表。插件发布包/容器镜像仍应签名,但签名验证属于部署门禁,不应写成现有 Core 能力。
### 7.2 状态机
```text
registered -> disabled -> enabled -> draining -> disabled
│ │ │
└────── incompatible └── error
```
- `registered`:部署层已登记清单和发布者,但未启用。
- `disabled`:服务存在但不接收业务请求。
- `enabled`:健康检查通过,允许插件后端调用列入 allowlist 的 Core API。
- `draining`:停止接收新命令,等待进行中的幂等命令完成。
- `error`:健康检查或 Core API 合约校验失败,默认 fail-closed。
- `incompatible`:插件清单或 Core API 版本不兼容,不允许启用。
### 7.3 升级和回滚
- 插件版本独立于 `backend/cmd/server/VERSION` 和 `frontend/package.json`。
- 升级前先执行健康检查、现有 Core API contract test 和插件自身数据备份检查。
- 新版本不兼容时保持旧版本运行,不自动覆盖正在启用的实例。
- 停用时等待进行中的命令完成;超过 drain 超时则拒绝新命令并标记待恢复。
- 插件卸载不删除 Core 订阅、余额和审计数据。
## 8. 数据归属
### 8.1 Core 权威数据
- 用户身份和管理员授权;
- 余额账本;
- 套餐价格、额度、覆盖 Group 和购买快照;
- 用户订阅状态、期限、配额和 reserved;
- 余额扣减、订阅创建/续期、撤销和审计;
- 请求计费、额度预留、结算和幂等。
### 8.2 Plugin 可拥有的数据
- 插件管理员会话和本地角色映射;
- 插件 UI 偏好、筛选条件和缓存;
- Core API operation 的本地查询索引;
- 供应商或业务侧的非权威展示配置。
插件数据库中的订阅副本必须可删除、可重建、带来源版本和更新时间。它不作为网关放行请求的依据。
## 9. 前端和菜单集成
V1 通过现有的管理员自定义菜单嵌入,不修改 Core 前端路由:
```text
Core 设置 -> custom_menu_items
id: example.subscription
visibility: admin
url: https://PLUGIN_ORIGIN/extensions/example.subscription/
Core `/custom/example.subscription`
└── sandbox iframe -> 反向代理 -> `127.0.0.1:PLUGIN_PORT`
```
`visibility=admin` 只负责隐藏普通账号的菜单入口,插件后端仍必须独立鉴权。现有 iframe 的 sandbox、跨端口 origin 和 Core JWT `localStorage` 使其不会自动共享 Core 登录态;因此 iframe 首屏显示插件登录页是 V1 的预期行为。插件也应提供“新窗口打开”,便于登录后保持自身会话。
生产部署建议把插件外部地址挂在与 Core 相同的 HTTPS 站点下,由 Nginx/Caddy 按路径反代到独立端口;这只减少浏览器跨域问题,不改变插件必须登录和服务端调用 Core 的事实。若使用独立 origin,必须在 Core CORS 中精确加入该 origin,禁止 `*`,并仅允许必要的 `Authorization` 请求头。
插件页面只调用自己的 `/plugin-api/*`,由插件后端调用 Core 现有认证和管理员 API。V1 不允许插件动态注入主应用 Vue 路由、修改核心菜单组件或覆盖全局 CSS;主应用只提供一个受权限控制的“业务插件”入口,插件内部菜单由插件自己管理。
## 10. 威胁模型与处置
| 威胁 | V1 处置 |
|---|---|
| XSS 窃取 Admin Key | 浏览器永远没有 Admin Key;Cookie HttpOnly;CSP 和输出编码。 |
| 插件前端伪造管理员命令 | 插件后端重新验证插件会话;Core 重新验证 Bearer token、管理员角色、操作者和资源权限。 |
| 插件后端日志泄露 secret | 统一日志脱敏,禁止记录 Authorization 和完整请求。 |
| 插件服务被攻破 | 限制 Token scope、TTL、出站地址和 Core API;立即撤销凭据。 |
| 重放余额购买 | Idempotency-Key + Core 事务记录 + 请求时间/nonce。 |
| CSRF | SameSite Cookie、CSRF token、Origin/Referer 校验。 |
| SSRF | 插件出站 allowlist;Core 现有 API 不接受任意 URL。 |
| 普通用户进入后台 | 插件登录调用 Core `/auth/me` 并要求 `role=admin`;所有插件路由默认 deny。 |
| 多实例状态漂移 | Core 是权威;插件状态通过健康检查和配置版本对齐。 |
| 插件卸载误删订阅 | 插件没有删除 Core 账本的权限,卸载只撤销凭据。 |
必须明确:独立插件进程不是操作系统级沙箱,签名只证明发布者和完整性,不证明代码无漏洞。生产部署仍需要低权限运行、网络隔离和可撤销凭据。
## 11. 测试门禁
### 11.1 Core API Adapter 合约测试
- Core 登录、`/login/2fa`、`/auth/me`、access/refresh 过期和撤销;
- 普通用户、停用用户、无效会话和跨插件会话均返回 `403`;
- 只读 Admin API 的字段脱敏、分页上限和资源权限;
- Core 超时、`401/403/429/5xx`、刷新 token 和重新登录;
- V1 明确没有 `/api/v1/plugin-host/*`,测试不能把未来接口当成现有接口。
### 11.2 插件后端测试
- Core 登录代理、2FA、会话过期、登出和 CSRF;
- Core access/refresh token 不进入响应、页面、日志和异常堆栈;
- Core `401/403/429/5xx` 的处理和刷新失败后的重新登录;
- 只允许配置中列出的 Core API endpoint;
- 插件服务重启后会话失效或从插件自己的会话存储恢复,不能依赖 Core 用户表。
### 11.3 浏览器和部署测试
- 管理员可登录,普通用户无法登录或访问任何后台 API;
- 不同屏幕下页面无横向泄露和敏感字段;
- 浏览器 DevTools 的请求、下载和页面源中没有 Admin Key;
- HTTPS、反向代理、容器低权限、secret 文件权限和日志脱敏;
- 停用、升级、回滚、凭据撤销后所有写操作按预期失败。
## 12. 分阶段实施计划(不含本次代码)
### Phase 0:冻结部署契约
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务);
- 冻结插件端口、反向代理路径、`custom_menu_items` 字段和健康检查;
- 冻结允许调用的现有 Core API 路径、字段、分页和错误处理;
- 明确插件登录通过 Core `/auth/login`/`/login/2fa`,不创建用户表;
- 建立 Core token 会话销毁、Admin Key 过渡和日志脱敏方案。
### Phase 1:框架最小实现
- 独立服务目录、清单、发布者签名和部署兼容性检查;
- Plugin Backend 管理员登录和会话;
- Core access/refresh token 服务端会话适配器;
- 插件健康检查、启停、操作审计和同源入口;
- Core API allowlist、contract test 与本地示例插件。
### Phase 2:订阅插件试验
- 只读套餐、用户余额和订阅列表;
- 管理员操作页面、查询缓存和审计;
- 使用测试 Core 和测试账户,不连接生产余额;
- 余额购买、续费和撤销暂不实现,等待 Core 原子接口冻结。
### Phase 3:生产准备
- 未来 Core Host Adapter/短时 Plugin Access Token(若确认需要 Core 改动);
- step-up、密钥轮换和撤销;
- 多实例、备份恢复、升级回滚和故障演练;
- 通过安全、契约、浏览器和并发门禁后再启用。
## 13. 待确认事项
以下事项在写代码前必须确定:
1. V1 的订阅插件只允许管理员操作;普通用户购买页不纳入本框架首版。
2. 管理员登录是否按本文通过 Core `/auth/login` 和 `/login/2fa` 完成;确认不创建插件用户表。
3. 插件采用独立服务端口 + 同源反向代理,还是独立 origin;需确定生产网络拓扑。
4. V1 是否允许测试环境使用服务端专用 Admin Key;生产默认不使用,除非接受单一管理员审计语义。
5. 插件是否允许保存只读缓存;无论选择何种缓存,Core 必须始终是权威来源。
6. V1.1 是否需要真正无感 SSO 和 Core Host Adapter;若需要,单独立项修改 Core。
在这些问题确认前,不应开始实现插件协议、数据库表或前端页面。
+20
View File
@@ -0,0 +1,20 @@
# Business Plugins
这里存放独立业务插件的领域文档和适配说明。
## 框架入口
- [Business Plugin Framework V1](BUSINESS_PLUGIN_FRAMEWORK_V1.md)
- [Manifest V1](BUSINESS_PLUGIN_MANIFEST_V1.md)
- [Boundaries](BUSINESS_PLUGIN_BOUNDARIES.md)
- [Architecture](BUSINESS_PLUGIN_ARCHITECTURE.md)
- [Acceptance](BUSINESS_PLUGIN_ACCEPTANCE.md)
- [Development](BUSINESS_PLUGIN_DEVELOPMENT.md)
## 领域插件
- `subscription-admin`:独立管理员只读订阅插件。它是框架的第一个领域样例,不是通用插件后台,也不负责安装或管理其他插件。实现与运行方式见 [`../plugins/subscription-admin/README.md`](../plugins/subscription-admin/README.md)。
## 现有 `.s2plugin`
现有 OpenAI OAuth transport 插件不属于本仓库;Business Plugin V1 与 Core 的 transport 插件使用不同的包、运行时和生命周期协议。
+25
View File
@@ -0,0 +1,25 @@
# Repository Scope
本仓库只承载独立业务插件及其版本化契约,不承载 Sub2API Core。
## 依赖关系
```text
Sub2API Core(官方仓库)
^
| 公开 HTTP API / 管理员鉴权 / custom_menu_items
|
独立插件服务(本仓库)
```
插件不得依赖 Core 的 `internal` 包、Ent 生成代码、PostgreSQL、Redis 或
Core 前端源码。每个插件拥有自己的版本、端口、服务进程、UI、配置和发布
产物,并在清单中声明兼容的 Core 基线。
## 仓库边界
- Core 的用户、余额、订单、订阅、配额、计费和用量数据仍由 Core 管理。
- 插件只通过服务端 allowlist 调用 Core API。
- 浏览器只访问插件自己的会话 API,不接触 Core JWT 或 Admin Key。
- 插件菜单通过 `custom_menu_items` 注入;停用或卸载只移除插件自己的菜单。
- 订阅管理是可选领域插件,不是通用插件控制面的一部分。
+254
View File
@@ -0,0 +1,254 @@
# 余额订阅业务插件 V1 RFC
状态:V1 只读试验实现已落库;余额购买、续费、撤销仍为 V1.1 Draft
本文定义基于 [`PLUGIN_FRAMEWORK_V1_RFC.md`](./PLUGIN_FRAMEWORK_V1_RFC.md) 的第一个业务插件试验,对应实现为 `plugins/subscription-admin`。插件是独立端口的管理员后台,复用 Sub2API Core 的管理员鉴权,不创建 Core 用户表,也不直接连接 Core 数据库。当前实现已完成框架、管理员会话、只读套餐/余额/订阅/审计联调;余额购买、续费和撤销写操作后置到 Core 原子接口冻结之后。
## 1. 目标和范围
### 1.1 V1 目标
- 提供独立的管理员订阅后台,普通账号拒绝登录和访问;
- 插件登录调用 Core 现有 `/api/v1/auth/login`、`/api/v1/auth/login/2fa` 和 `/api/v1/auth/me`,不维护第二套用户密码;
- 展示 Core 中的套餐、用户余额和订阅实例状态;
- 通过现有 `custom_menu_items` 的 `visibility=admin` 入口嵌入 Core 页面,也支持新窗口打开;
- 插件可独立升级、停用和回滚,不影响 Core 网关、余额账本和已有订阅;
- 记录未来订阅写操作所需的业务语义、幂等和审计契约,但本阶段不执行扣款。
### 1.2 V1 非目标
- 不接入支付宝、微信、Stripe 或其他外部支付渠道;
- 不允许普通用户登录插件后台或在插件内自助购买;
- 不把余额、套餐、订阅额度复制成插件自己的权威账本;
- 不把每次网关请求改成调用插件 RPC;
- 不通过多个现有 Admin API 调用拼接一次购买;
- 不在本次设计阶段修改 Core 代码、数据库迁移、现有前端路由或 `.s2plugin` ABI。
## 2. 业务语义(供后续 Core 原子接口使用)
订阅插件以后需要保持当前业务语义,写操作必须由 Core 在单事务内完成:
1. **支持多个同档位**:同一用户可以拥有多个相同套餐/档位的订阅实例。每个实例有独立的 `subscription_id`、期限、配额、用量和审计记录;实例数量受套餐的上限字段约束。
2. **支持单独续费**:续费请求必须指定 `subscription_id`,只延长目标实例的期限或按 Core 规则生成续费记录,不影响同用户的其他同档位实例。
3. **不可由用户取消**:插件和用户端不提供“取消订阅”入口。管理员撤销属于单独的受控操作,需 Core 权限、原因、二次确认和审计;它不等同于用户取消。
4. **购买时快照**:价格、货币、有效期、额度、Group 覆盖和实例规则以购买时版本写入 Core 快照,后续编辑套餐不静默改写已购买实例。
5. **余额付款**:余额扣减、订阅创建/续费、余额流水、幂等记录和审计必须在 Core 同一个事务边界内完成。
这些语义是插件的业务约束,不代表当前 Core 已经提供了对应的业务插件 API。第一阶段只验证读取现有订阅数据,写入契约单独评审。
## 3. 推荐拓扑
```text
Core 管理后台
└─ custom_menu_items (visibility=admin)
└─ sandbox iframe / 新窗口
└─ 反向代理 -> Plugin `127.0.0.1:PLUGIN_PORT`
├─ Plugin /login -> Core /api/v1/auth/login (+ /login/2fa)
├─ Plugin /auth/me -> 只允许 role=admin
├─ HttpOnly 插件会话(不建 Core 用户表)
└─ 服务端 Bearer Core JWT -> 现有 Core Admin API(V1 只读)
```
插件 UI 只访问自己的 BFF;Core JWT、refresh token 和 Admin API Key 只在插件服务端会话或 secret 中出现。iframe 不会自动继承 Core `localStorage` 登录态,因此 V1 首屏显示插件登录页属于预期行为。sandbox 或第三方 Cookie 策略可能导致嵌入会话刷新后失效,插件必须提供新窗口登录路径;登录密码只在一次转发请求中经过插件后端,不落库、不写日志。
## 4. 权威边界
### 4.1 必须留在 Core
- 用户身份、管理员权限和资源授权;
- 用户余额和余额流水;
- 套餐价格、货币、有效期、额度、包含的 Group、实例模式和上限;
- 购买时的套餐快照;
- 订阅实例状态、期限、配额、reserved 和用量;
- 余额扣减、订阅创建/续费、撤销、退款/补偿;
- 额度预留、实际结算、幂等、防超卖、缓存失效和网关放行判定。
### 4.2 插件可以负责
- 管理员登录代理、插件会话和插件内角色;
- 套餐、用户和订阅的分页筛选与展示;
- 只读缓存、操作结果页和管理员审计视图;
- 未来写操作的确认表单,但提交必须调用 Core 原子命令;
- 插件 UI 的版本和发布。
插件的本地副本可删除、可重建、带来源版本和更新时间,不能作为网关授权或扣费依据。
## 5. V1 现有 Core API 适配
V1 不调用尚不存在的 `/api/v1/plugin-host/*`。插件后端通过严格 allowlist 调用 Core 已有接口,具体路径按部署的 Core 版本冻结:
```http
POST /api/v1/auth/login
POST /api/v1/auth/login/2fa
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
GET /api/v1/settings/public
GET /api/v1/admin/payment/plans
GET /api/v1/admin/subscriptions
GET /api/v1/admin/subscriptions/{id}
GET /api/v1/admin/users/{id}
GET /api/v1/admin/users/{id}/subscriptions
```
请求头只由插件后端添加:
```http
Authorization: Bearer CORE_ACCESS_TOKEN
X-Request-Id: UNIQUE_REQUEST_ID
```
列表接口必须设置分页、字段最小化、超时和审计。插件配置列出具体方法和路径,不能使用任意 URL 或宽泛通配符。Core 返回 `401` 时,插件先尝试一次 refresh;refresh 失败就销毁插件会话并要求重新登录。
## 6. 未来写操作契约(V1.1,当前不实现)
现有 Core Admin API 的 `assign`、`extend`、`revoke` 等操作属于 Core 管理面,当前不把它们组合成余额购买流程。要在插件内支持余额购买,Core 需要增加版本化原子命令或等价 Host Adapter,并在 Core 内完成余额、订单/购买记录、订阅和审计的一致性事务。
### 6.1 余额购买请求示例
```http
POST /api/v1/plugin-host/v1/subscription-purchases/balance
Authorization: Bearer PLUGIN_ACCESS_TOKEN
Idempotency-Key: subscription-purchase-UNIQUE_ID
Content-Type: application/json
{
"user_id": 123,
"plan_id": 12,
"expected_plan_version": 4,
"expected_price": "19.00",
"currency": "USD",
"request_id": "UNIQUE_REQUEST_ID"
}
```
该路径是未来接口示例,当前 Core 没有实现。Core 必须重新读取当前套餐和余额,`expected_*` 只用于发现界面陈旧;价格以字符串 decimal 处理,不使用二进制浮点数。
### 6.2 Core 事务要求
未来 Core 原子命令至少需要:
1. 校验操作者为管理员并通过资源级授权;
2. 锁定用户余额、套餐购买上限和目标订阅资源;
3. 校验套餐可售、Group 覆盖、余额和实例上限;
4. 写入购买快照;
5. 扣余额并写余额流水;
6. 创建新实例或仅续费指定 `subscription_id`;
7. 写唯一 operation、幂等记录和管理员审计;
8. 事务提交后失效相关缓存。
任一步失败都回滚整笔命令。插件不接触 SQL transaction,也不执行失败后的自行补偿。
### 6.3 操作状态和重试
```text
accepted -> processing -> completed
└── failed
```
同一 `Idempotency-Key` 重试返回第一次操作结果,不重复扣款。网络超时后插件查询 operation 状态,不再次创建购买。`401/403` 不重试;`429` 按 `Retry-After` 有上限退避;只有明确幂等命令才允许对 `5xx` 重试。
## 7. 同档位实例和续费规则
未来实现必须覆盖以下测试矩阵:
| 场景 | 预期结果 |
|---|---|
| 同一用户购买两个相同档位 | 产生两个独立实例,各自有期限、配额和 `subscription_id`。 |
| 续费实例 A | 只改变实例 A;实例 B 的期限和配额保持不变。 |
| 达到实例上限 | 返回稳定业务错误,不扣余额、不创建半成品订阅。 |
| 用户尝试取消 | 插件没有取消入口;Core 用户接口也不提供用户自助取消语义。 |
| 管理员撤销 | 走单独的受控 Core 操作,要求原因、审计和明确的退款/补偿规则。 |
| 两个管理员并发购买 | 由 Core 锁和幂等保证不超卖;失败方查询最终状态。 |
## 8. 套餐快照和变更
推荐后续 Core 设计提供不可变套餐版本或购买快照,至少包含价格、货币、有效期、三类额度、覆盖 Group、实例模式和上限。当前 schema 的运行时 Group 覆盖和套餐字段存在可变性,不能在本 RFC 中宣称已经提供完整快照保证。
套餐下架只影响新购买;已有订阅如何处理由 Core 现有授权和计费规则决定。若要迁移已有实例,需要单独的预览、影响数量、确认 token、幂等键和审计命令。
## 9. 管理员 UI V1
首版只读页面建议顺序:
1. 概览:Core 连接状态、插件版本、最后同步时间和健康状态;
2. 套餐:价格、货币、有效期、额度、覆盖 Group 和可售状态;
3. 用户订阅:按用户 ID、脱敏名称、订阅状态、实例档位和到期时间查询;
4. 订阅详情:展示单个 `subscription_id` 的期限、窗口额度、用量和来源版本;
5. 操作记录:展示操作者、Core request ID、结果和时间;
6. 设置:Core 地址、allowlist、会话/凭据状态和健康检查;secret 只允许轮换,不允许回显。
余额购买、单独续费和管理员撤销在 V1 只显示“未启用”状态,不渲染可提交按钮,避免误触发现有非原子接口。
## 10. 安全验收
- 页面源码、网络请求、下载文件和浏览器存储中没有 Admin Key 或 Core JWT;
- 普通用户使用 Core JWT 登录插件返回 `403`,直接访问插件 API 也返回 `403`;
- Core access/refresh token 不进入插件响应、页面、日志和异常堆栈;
- 插件后端只访问配置中的 Core API 路径,禁止 SSRF 和任意 URL 代理;
- 插件服务停用后,已有订阅仍按 Core 原有网关鉴权和计费逻辑运行;
- 清空插件本地缓存后,可从 Core 重建只读列表,不影响核心余额和订阅;
- 用户端没有取消订阅入口,管理员撤销(未来启用时)必须有原因、审计和幂等键。
## 11. 测试计划
### 11.1 插件后端
- 管理员登录、2FA、普通用户拒绝、会话过期、登出和 CSRF;
- Core token refresh、撤销和 Core `401/403/429/5xx` 处理;
- 套餐/余额/订阅查询的分页、字段脱敏、超时和缓存重建;
- allowlist 拒绝未声明路径、任意 URL 和跨插件会话;
- 多实例会话存储和服务重启后的会话策略。
### 11.2 未来 Core 写操作
- 两个同档位实例、目标实例单独续费和实例上限;
- 用户取消入口不存在,管理员撤销的审计和补偿规则;
- 余额不足、套餐下架、价格版本冲突、并发购买和幂等重试;
- 事务失败时余额、订单、订阅和审计均保持原状;
- 购买快照内容不可变、缓存只在提交后失效。
### 11.3 浏览器和部署
- 管理员可在 iframe 和新窗口完成登录;普通用户菜单不可见且 API 拒绝;
- 425px、900px、1440px 下无横向溢出、遮挡或敏感字段泄露;
- DevTools 请求、下载、页面源和日志中没有 secret;
- 反向代理、HTTPS、低权限运行、停用、升级和回滚流程可恢复。
## 12. 分阶段实施计划
### Phase 0:框架契约
- 冻结插件端口、反向代理路径、`custom_menu_items` 字段和健康检查;
- 冻结 Core 登录/2FA、管理员角色判断和现有只读 API allowlist;
- 确认会话存储、Core token 生命周期、日志脱敏和 secret 轮换;
- 不改 Core 代码、迁移、现有插件 ABI 或前端路由。
### Phase 1:插件框架最小实现
- 独立服务目录、清单、签名和部署兼容性检查;
- Core 登录代理、管理员角色校验、HttpOnly 插件会话和 CSRF;
- Core API BFF、健康检查、审计和只读示例页;
- 本地 contract test、浏览器验收和反向代理样例。
### Phase 2:订阅只读插件
- 套餐、用户余额、订阅实例和操作记录只读查询;
- 同档位实例/单独续费/不可取消语义的 UI 展示与测试数据;
- 只接入测试 Core,不连接生产余额。
### Phase 3:单独立项的 Core 写能力
- 评审并实现 Core 原子余额购买/续费/撤销接口;
- 增加 Core 幂等存储、审计和购买快照后,再接入插件写操作;
- 通过并发、事务、浏览器和回滚门禁后才启用生产 scope。
## 13. 实施前检查清单
- [ ] 插件只允许 Core `role=admin` 登录,且不创建用户表。
- [ ] 独立端口、反向代理路径和 `visibility=admin` 菜单入口已确定。
- [ ] iframe 登录页和新窗口登录页均可用,已知晓 V1 不提供无感 SSO。
- [ ] 只读 Core API allowlist、分页、字段脱敏和缓存策略已冻结。
- [ ] 同档位多实例、单独续费、用户不可取消和管理员撤销规则已确认。
- [ ] 余额购买写操作明确等待 Core 原子接口,不使用现有多个接口拼接。
- [ ] 测试 Core、测试管理员和测试订阅数据已准备,生产余额尚未接入。