openxiangda-skill-kit 2.0.0-alpha.19 → 2.0.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/architecture/admin-shell-v2.md +479 -71
- package/docs/architecture/authorization-consistency-v2.md +419 -0
- package/docs/architecture/environment-configuration-kernel-v2.md +288 -0
- package/docs/architecture/implementation-roadmap.md +77 -0
- package/docs/architecture/local-development-v2.md +134 -0
- package/docs/architecture/native-configuration-projection-v2.md +488 -0
- package/docs/architecture/native-kernel-inventory-v2.md +196 -0
- package/docs/architecture/repository-and-release.md +1 -0
- package/docs/architecture/tenant-public-origin-implementation-blueprint.md +484 -0
- package/docs/architecture/tenant-public-origin-v2.md +236 -0
- package/docs/architecture/verification-orchestration-v2.md +24 -0
- package/docs/backend.md +16 -7
- package/docs/data-authz.md +27 -2
- package/docs/delivery.md +2 -2
- package/docs/design/admin-shell-dashboard-v2.png +0 -0
- package/docs/design/admin-standard-pages-v2.png +0 -0
- package/docs/frontend.md +32 -11
- package/docs/getting-started.md +20 -14
- package/docs/index.md +2 -0
- package/docs/llms.txt +7 -0
- package/docs/reference/cli.md +17 -2
- package/docs/reference/mcp.md +1 -0
- package/docs/school-contact-relations.md +117 -0
- package/docs/workflow-events.md +23 -1
- package/package.json +2 -2
- package/skills/manifest.json +6 -6
- package/skills/openxiangda-v2/SKILL.md +13 -0
- package/skills/openxiangda-v2-backend/SKILL.md +7 -1
- package/skills/openxiangda-v2-data-authz/SKILL.md +7 -1
- package/skills/openxiangda-v2-frontend/SKILL.md +39 -3
- package/skills/openxiangda-v2-workflow-events/SKILL.md +22 -5
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# 租户公共 Origin v2
|
|
2
|
+
|
|
3
|
+
状态:已设计待确认;2026-08-13 经生产表结构与完整调用链审计后修订
|
|
4
|
+
|
|
5
|
+
## 1. 问题证据
|
|
6
|
+
|
|
7
|
+
当前平台有多个调用方分别实现“域名找到租户”:SSO 使用 `endsWith` / `includes`,游客登录和钉钉登录还有各自的近似逻辑。它们会把请求输入同时当成租户选择依据和安全信任依据,无法证明 `school.example.com.evil.test`、重叠后缀或重复配置不会解析到错误租户。
|
|
8
|
+
|
|
9
|
+
2026-08-13 对源码和 prod-1 的只读核验证明:
|
|
10
|
+
|
|
11
|
+
- K3s 内只有一套平台服务,入口 Nginx 将多个租户虚拟主机转发到同一个 NodePort;多 Pod、多应用服务器并不意味着一个租户需要多个公共 Origin。
|
|
12
|
+
- 当前唯一启用 CAS 的租户可由 HTTPS 访问,但数据库默认域名仍声明为 HTTP。若 B0-R 直接按数据库值严格校验,该租户会明确失败;若运行时忽略 scheme 兼容,则会破坏 OAuth/CAS 的同源安全边界。
|
|
13
|
+
- `yida.wisejob.cn` 与 `zyyh.wisejob.cn` 当前被同一个 HTTPS server block 接受,但平台只声明前者为租户主域名;同一虚拟主机还承载非平台路径,不能粗暴重定向整台主机。
|
|
14
|
+
- 生产 `default_configs` 只有主键、`configType` 和 `isActive` 三个索引。源码中声明在未注册派生类上的复合 `@Index` 没有成为数据库约束;10 条 DOMAIN 记录当前没有重复只是数据现状。
|
|
15
|
+
- DOMAIN DTO 接受任意 `configData`,两个通用配置控制器都能创建、更新、启停和删除;当前更新是先查后存,没有结构校验、数据库唯一约束或 revision CAS。
|
|
16
|
+
- `getBaseUrl()` 不只服务 SSO,还生成 CAS、钉钉、App Auth、OpenAPI/verification 回调和消息链接;`getDomainConfig()` 还被用于 Cookie。只替换 SSO 会制造新的双事实源。
|
|
17
|
+
|
|
18
|
+
因此,B0-R 之前新增独立的 **B0-O 租户公共 Origin 收敛**。B0-O 解决公共入口的所有权、存储和精确解析,B0-R 只解决认证完成后的 return target;Cookie 安全另属 B0-C。三者不能混成一个 URL 工具函数或一次不可回滚发布。
|
|
19
|
+
|
|
20
|
+
## 2. 架构决定
|
|
21
|
+
|
|
22
|
+
### 2.1 所有者与唯一事实源
|
|
23
|
+
|
|
24
|
+
Platform Server 新增唯一的 `TenantPublicOriginService`。SSO、游客登录、钉钉登录、CAS、App Auth、OpenAPI/verification 回调、消息链接和以后所有需要租户公共入口的模块都只能调用该服务,不再自行查询 `default_configs`、拼装 `{ protocol, domain, port }` 或实现字符串匹配。
|
|
25
|
+
|
|
26
|
+
长期唯一事实源是专用的版本化 registry。上一版设计曾计划继续复用 `default_configs` JSONB;生产索引和写入口审计已否定该决定。通用 `any` 配置不能可靠表达结构、全局唯一、revision CAS 和审计;给 JSONB 继续叠表达式索引会让认证边界永久依赖隐式格式。
|
|
27
|
+
|
|
28
|
+
registry 共九张表:下列七张负责版本、控制指针、legacy 逐行处置、主机名占用、验证与幂等,后文另外定义追加审计表和全局迁移 control 表。
|
|
29
|
+
|
|
30
|
+
| 表 | 约束与语义 |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `tenant_public_origin_versions` | 不可变历史;以 UUID 标识,保存稳定 tenant UUID/code 快照、规范化 scheme/hostname/port/origin_key、创建者和时间。tenant UUID 不直接 FK 到租户,使已 decommission/删除租户的配置正文仍可审计 |
|
|
33
|
+
| `tenant_public_origin_heads` | 每租户一行,`tenant_id` 主键/FK 指向 `tenants.id ON DELETE RESTRICT`;保存单租户 `source_state`、迁移 action/目标 origin/legacy 与依赖摘要、`active_version_id`、`staged_version_id`、`previous_active_version_id`、`rollback_until` 和单调 `revision`。三个版本引用都使用 `(tenant_id, version_id)` 复合 FK,防止串租户;所有 freeze/stage/activate/rollback/decommission 命令都做 head revision CAS |
|
|
34
|
+
| `tenant_public_origin_legacy_dispositions` | 每条 legacy DOMAIN config id 一行;保存原始 row digest、关联 tenant UUID/code 快照(orphan 可空)、`migrate/decommission/retire-orphan` action、计划项摘要和 `planned/retired` 状态。它是 finalize 证明每条旧记录有明确去向的数据库事实,不用 audit 或外部 JSON 反推当前状态 |
|
|
35
|
+
| `tenant_public_hostname_claims` | 规范化 `hostname` 为主键;记录该 hostname 当前归属的稳定租户 UUID、租户 code 快照、`owned/retirement/decommission-hold` 状态和释放时间。active/staged/previous 对同一 hostname 共享一条 owned claim;跨 scheme/port 仍只允许一个租户拥有该 hostname |
|
|
36
|
+
| `tenant_public_origin_challenges` | ops CLI 为当前 staged version 临时签发;只保存随机 nonce 的 hash、head revision 和短 TTL。公开 challenge 端点按 opaque id + header 校验,不按 Host 找租户,不返回租户信息 |
|
|
37
|
+
| `tenant_public_origin_verifications` | ops CLI 在外部检查全部成功并消费 challenge 后插入的短期回执;不可修改的证据主体绑定 tenant UUID、staged version、head revision、该租户 state digest、部署 release、网关声明摘要、逐项检查结果、验证者和过期时间,只允许一次性从未消费变成 consumed;浏览器管理 API 不能创建或伪造。它不绑定全平台 digest,避免无关租户变化使回执失效 |
|
|
38
|
+
| `tenant_public_origin_operations` | 每个成功逻辑命令一个不可变幂等 receipt;operation UUID 唯一并绑定 command type/digest、安全结果摘要和操作者。同 ID 同 digest 稳定 replay,不同 digest 冲突 |
|
|
39
|
+
|
|
40
|
+
版本字段约束为:scheme 仅 `http|https`;hostname 是 WHATWG URL 规范化后的 ASCII 小写值,不含尾点、方括号、path/query/userinfo;port 为 `NULL` 或 `1..65535`,默认端口必须归一为 `NULL`;`origin_key` 是与这些列一致的数据库生成值,IPv6 只在生成 origin 时加方括号。`(tenant_id, version_id)` 唯一键供 head 复合 FK 使用。版本记录创建后不可 UPDATE/DELETE,普通管理和 decommission 都只改变 head/claim 生命周期。
|
|
41
|
+
|
|
42
|
+
同一 expand migration 建立追加式 `tenant_public_origin_audit_events`,记录 import/stage/verify/activate/rollback/decommission/release-claim 的 operation id、稳定 event sequence、前后版本、head revision、操作者、requestId 和时间。operation receipt、head/claim 变更、revision CAS 与全部审计必须在同一个事务提交;审计不保存认证 ticket、code、Cookie、Secret 或完整敏感 query。审计表保留租户 UUID/code 快照而不级联删除,保证租户删除后仍可追责。
|
|
43
|
+
|
|
44
|
+
数据库和服务共同保证:
|
|
45
|
+
|
|
46
|
+
1. 每个租户至多一个 active 和一个 staged origin;运行时只读取 active。需要浏览器入口的租户必须恰好一个 active。
|
|
47
|
+
2. scheme、hostname、port 经统一纯函数规范化;默认端口消除,IPv6 只在序列化 URL 时加方括号。
|
|
48
|
+
3. hostname claim 跨 active/staged/previous-active 全局唯一;同一 hostname 不允许因 scheme 或 port 不同分给两个租户。一个租户可以在版本切换中继续使用自己已 claim 的 hostname。
|
|
49
|
+
4. head 更新执行 revision CAS;影响行数不是 1 即返回 409,禁止后写覆盖先写。
|
|
50
|
+
5. stage 只创建不可变版本和 claim,不影响运行时;activate 必须携带目标 staged version、head revision 和与该版本绑定的验证回执。
|
|
51
|
+
6. 每个租户只保留一个上一 active 版本,默认回滚窗口 7 天、平台可配置但不得超过 30 天。下一次激活只在更旧 previous hostname 已不被 active/staged/retirement 引用时原子释放其 claim,再把当前 active 变成新的 previous,避免 claim 无界增长。
|
|
52
|
+
7. 停用采用 `decommission` 命令;依赖 origin 的 SSO/外部登录必须先停用。租户删除必须先 decommission,hostname 转成默认 30 天的 `decommission-hold`,即使租户行删除也不能立刻被另一租户接管;到期由有界清理任务释放并写审计。
|
|
53
|
+
8. hostname 变化后旧 hostname 至少保留到当前已签发 refresh token 的最大 TTL 到期。平台在旧入口提供无业务数据的 retirement 响应来清理旧 Cookie 后跳 canonical;hold 期间禁止该 hostname 分配给其他租户。
|
|
54
|
+
9. 一个租户存在未到期 hostname retirement hold 时,不允许再次激活第三个新 hostname;同 hostname 的 scheme/port 修订仍可进行。回滚到 head 当前 previous hostname 是唯一例外:事务把旧 canonical claim 从 retirement 恢复为 owned,并把刚下线的 canonical 变成新的 retirement/previous,仍保持至多一个旧 host。这样 claim 数量有界,也不会让 hold 阻断合法回滚。
|
|
55
|
+
|
|
56
|
+
### 2.2 Legacy 兼容但不双写
|
|
57
|
+
|
|
58
|
+
现有 `ConfigType.DOMAIN + configName=default` 只作为迁移输入。切换为 registry 后:
|
|
59
|
+
|
|
60
|
+
- `default_configs` 的 DOMAIN 行成为冻结历史快照,不再接受写入,绝不长期双写。
|
|
61
|
+
- 旧读取 API 由 `TenantPublicOriginService` 动态投影成旧 `{ protocol, domain, port }` 形状,兼容仍需读取的 1.x 客户端;投影不是第二份存储。
|
|
62
|
+
- 旧通用 DOMAIN 写 API 返回 409 `DOMAIN_CONFIG_MOVED`,响应给出新的平台管理入口,不静默改写新表。
|
|
63
|
+
- 平台管理端使用专用、强类型、带 head revision 的 stage/activate/rollback API;不再让管理员编辑 JSON,也不允许保存即生效。
|
|
64
|
+
- `DefaultConfigService.getDomainConfig/getBaseUrl` 在兼容期只做废弃适配并委托新服务;所有源码调用者逐项迁移后删除其域名职责。
|
|
65
|
+
|
|
66
|
+
### 2.3 Canonical origin 与入口别名
|
|
67
|
+
|
|
68
|
+
首期每租户只认一个 canonical origin,不保存普通入口别名。额外域名上的**平台入口与认证路径**必须在最外层受控网关使用固定目标 308 重定向到 canonical origin,并保留普通 path/query;别名不得把这些请求直接代理到平台。若同一虚拟主机还承载其他非平台服务,重定向只作用于明确的平台 location,不接管整台主机。
|
|
69
|
+
|
|
70
|
+
处于 `retirement` hold 的旧 hostname 是唯一例外,但它也不是第二个业务入口。网关只把该 host 的平台 location 导向无会话、无数据权限的 `AuthCookieRetirementEndpoint`:响应在旧 host 上清除已知两代 Cookie,设置 `Cache-Control: no-store`,再用固定目标 302 跳到 canonical 根入口。它不执行 refresh、SSO callback 或业务路由,也不携带原 query、ticket、code/state 到新 host;非平台 location 仍不受影响。hold 到期且旧名使用指标归零后才能移除此端点并改为普通 308/下线。这样既能清理 host-only Cookie,也不会把旧域名保留成第二个认证面。
|
|
71
|
+
|
|
72
|
+
未来若确有“同一租户多个不可重定向入口”的业务证据,应单独设计带全局唯一约束、用途、启停状态、revision 和审计的 alias registry;不能重新启用后缀、前缀或 `includes` 匹配。
|
|
73
|
+
|
|
74
|
+
### 2.4 解析与写入契约
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
getCanonicalOrigin(tenantId: string): Promise<CanonicalTenantOrigin>
|
|
78
|
+
resolveTenantByPublicOrigin(candidate: string): Promise<ResolvedTenantOrigin>
|
|
79
|
+
stageCanonicalOrigin(command: StageTenantPublicOriginCommand): Promise<StagedTenantOrigin>
|
|
80
|
+
activateCanonicalOrigin(command: ActivateTenantPublicOriginCommand): Promise<CanonicalTenantOrigin>
|
|
81
|
+
rollbackCanonicalOrigin(command: RollbackTenantPublicOriginCommand): Promise<CanonicalTenantOrigin>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- 新版浏览器提交 `window.location.origin`,服务端规范化后按完整 `origin_key` 精确查询。
|
|
85
|
+
- 兼容现有 1.x 的 SSO status 请求可以暂时提交 hostname;服务端按全局 hostname claim 精确解析。hostname 模式只用于发现租户,不能成为回调 allowlist;同一 hostname 多租户/多端口模式不再支持。
|
|
86
|
+
- `Host`、`X-Forwarded-Host` 和 query 参数都是不可信候选输入。即使网关转发这些头,也不能覆盖 registry。
|
|
87
|
+
- CAS `redirectUri` 由 canonical origin 约束;发起和回调阶段分别再次校验精确 origin,不能因旧 hostname 或别名放宽 scheme/host/port。
|
|
88
|
+
- 找不到、歧义、配置无效或数据库不可用均 fail closed。SSO status 返回 disabled/明确配置错误;认证发起返回 4xx/5xx 安全错误,不回退到任意租户,也不导航到原始目标。
|
|
89
|
+
- 消息/通知链接生成也不使用请求 Host 或过期缓存;无法取得 origin 时进入可重试失败,不能发送指向错误租户的链接。
|
|
90
|
+
- 首期浏览器管理写只允许当前租户 `PLATFORM_ADMIN`;应用管理员和普通租户角色不能进入。stage 提交完整目标值、`operationId` 和 `expectedHeadRevision`,activate 还必须提交 `stagedVersionId + verificationReceiptId`;禁止 PATCH 一个字符串后由服务猜测剩余字段。
|
|
91
|
+
- stage 不改变 active,也不把 challenge nonce 暴露给浏览器。ops CLI 在 verify 开始时生成至少 256 bit 随机、短期、一次性的 staged-origin challenge,只把 hash 写库,随后主动通过 staged HTTPS URL 的固定 challenge 路径发送 nonce header,并在客户端校验 DNS、证书 hostname/有效期/链、响应状态和 platform release。平台端只按 opaque challenge ID 和 nonce hash 验证,不从 Host 推导租户、不返回租户信息。适配器再检查固定 308/平台路径和 SSO provider callback;全部成功后在消费 challenge 的事务中插入不可变短期回执。activate 锁定 head 和回执,重新校验该租户 state digest、部署 release 与网关声明摘要,再一次性消费并切 active;过期或已消费回执返回 409。
|
|
92
|
+
- rollback 不是直接改列,而是把仍处于 rollback-hold 的旧版本重新 stage,再经过与普通激活相同的验证。这样回滚不会把已经失效的 DNS/TLS 配置盲目恢复上线。
|
|
93
|
+
- 每个租户迁移期的首次激活只接受处置计划覆盖的当前生产入口;该租户进入 registry 后、B0-C 完成前,API 禁止激活 hostname 变化,只允许同 hostname 的 scheme/port 修订。这样首次数据迁移不会顺带变成用户会话域名迁移。
|
|
94
|
+
|
|
95
|
+
### 2.5 调用者迁移矩阵
|
|
96
|
+
|
|
97
|
+
O1 必须维护源码扫描门禁:除 `TenantPublicOriginService`、legacy 投影适配器和迁移诊断外,生产代码不得再引用 `ConfigType.DOMAIN`、`getDomainConfig()` 或 `getBaseUrl()`。已审计调用者及目标语义如下:
|
|
98
|
+
|
|
99
|
+
| 当前调用者 | 当前用途 | O1/O3 目标与失败语义 |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `SSOConfigService`、`DingTalkLoginService`、`GuestService` | hostname→tenant,当前全量扫描/模糊匹配 | 统一调用 `resolveTenantByPublicOrigin/Hostname`;未知或冲突 fail closed,不回退任意租户 |
|
|
102
|
+
| `CASAuthService`、`DingTalkAuthService`、`AppAuthService` | 构造外部认证 callback/service | 读取 active canonical origin;缺失/数据库故障拒绝发起认证,不使用请求 Host |
|
|
103
|
+
| `OpenXiangdaVerificationService`、`OpenApiFrontendLoginService` | 生成登录/验证回调,并可能偏好 request origin | 安全回调只使用 active origin;相同 origin 的请求信息只能用于一致性诊断,不能覆盖 registry;缺失即失败 |
|
|
104
|
+
| `ScriptInterpreterService` | 向 App Function 运行时注入 `publicOrigin` | 注入 active origin 和 revision;生产缺失时 Function 调用明确失败,不回退环境 base URL;本地 profile 的 dev origin 是独立显式配置 |
|
|
105
|
+
| `MessageEngineService` | 生成业务详情链接 | 读取 active origin;缺失进入可重试消息失败,不发送相对或错误租户链接 |
|
|
106
|
+
| `AuthService`、`JwtMiddleware`、传统 refresh、`AppAuthService`、CLI auth、Application API gateway、`AuthImpersonationControlService` | 多处直接读写旧认证 Cookie 名与 Domain | B0-O 只提供结构化 active origin;B0-C 全部迁到唯一 `AuthCookieService`,版本化名称、v2 优先和双代清理由该服务所有 |
|
|
107
|
+
| `DefaultConfigService` 与两个 default-config controller | 通用 DOMAIN 读写及进程缓存 | O1 legacy 期仍按 phase 读取;O3 后旧读动态投影,旧写 409;删除缓存和域名职责 |
|
|
108
|
+
| 平台 `SystemConfigManagement` | JSON 文本编辑、启停/删除 DOMAIN | 移除 DOMAIN 选项;新增专用 Origin 页面展示 active/staged/previous、验证证据、revision 冲突和明确激活/回滚 |
|
|
109
|
+
|
|
110
|
+
发布门禁还要扫描平台前端 API 路径和测试 fixture,确保没有通过通用配置接口继续写 DOMAIN。新增调用者必须明确属于“租户发现、认证回调、普通链接、运行时注入或 Cookie”之一;不得绕过服务直接读表。
|
|
111
|
+
|
|
112
|
+
### 2.6 多实例、缓存与资源边界
|
|
113
|
+
|
|
114
|
+
公共 Origin 是低频认证控制面数据,正确性优先于进程内缓存。首期按 head 的稳定租户 UUID、claim 的 hostname 主键和版本 origin 索引做数据库精确查询;通过 `tenants.code -> tenants.id -> active_version` 联合查询保持现有服务参数兼容。禁止每次加载所有租户后在 Node 内扫描,也禁止各 Pod 维护无共享版本的 allowlist。任意数量的 API Pod 对同一数据库快照必须得到相同结果。
|
|
115
|
+
|
|
116
|
+
输入 UTF-8 上限 8 KiB,只接受单个 origin/hostname;不做 DNS 查询、通配符展开或外部网络访问。攻击输入必须在查询前拒绝,查询计划需要证明索引命中。未来只有在性能证据充分时才能增加以 `tenant_id + revision` / `origin_key + revision` 为键、带共享 revision 的读穿缓存;数据库异常时安全调用方不得用过期值继续认证。
|
|
117
|
+
|
|
118
|
+
### 2.7 反向代理与 preflight 契约
|
|
119
|
+
|
|
120
|
+
标准入口需要:
|
|
121
|
+
|
|
122
|
+
- 将平台 NodePort/Service 只暴露给受控网关;
|
|
123
|
+
- 转发原始 `Host` 与可信的 `X-Forwarded-Proto`,但 Platform Server 只在配置好的内部代理范围解释 forwarded headers;
|
|
124
|
+
- 普通非 canonical 别名的平台入口/认证 location 使用固定目标 308;retirement host 只接入无业务能力的 Cookie 清理端点;同主机其他业务保持原路由;
|
|
125
|
+
- HTTP→HTTPS 不使用任意请求 Host 拼目标,而使用该 server block 的固定 canonical host;
|
|
126
|
+
- 三台服务器、两个应用节点或多个 Pod 共享数据库 registry 和同一 release,不按实例注册回调。
|
|
127
|
+
|
|
128
|
+
Preflight 分成两个证据源,不能让数据库命令假装理解所有网关:
|
|
129
|
+
|
|
130
|
+
1. Platform Server 的只读 JSON 命令验证 legacy/registry 结构、规范化、重复、SSO 依赖、head/claim、迁移 phase 和查询计划;由目标后端镜像提供。
|
|
131
|
+
2. 部署适配器读取实际受控入口(Kubernetes Ingress、标准 Nginx 渲染结果或显式运维清单),探测 staged origin 的 TLS/HTTP 跳转/平台路径并与 registry 比较;hostname 迁移时还要证明旧 host 只暴露 no-store retirement 响应且不会转发敏感 query。适配器无法证明时输出 `unverified`,允许 O0/O1 观察,但阻止验证回执生成和 O2/O3/日常 activate。
|
|
132
|
+
|
|
133
|
+
启用 SSO 时还必须证明 provider 使用的精确 callback/service 已就绪。能查询配置的 provider 使用适配器自动验证;无法查询 redirect allowlist 的 CAS/OAuth/OIDC 提供商允许平台运维人员提交**精确 provider、回调摘要、变更单引用和短期有效期**的风险证明,单独写审计并在 activate 时消费。它不是“跳过检查”:缺少自动证据或显式风险证明都不能生成完整回执。未来接入 provider API 后应移除对应人工证明路径。
|
|
134
|
+
|
|
135
|
+
运维清单只是待验证的部署声明,不成为认证事实源。prod-1 可使用 host Nginx 适配器;私有化三机环境使用其实际 Ingress/Nginx 适配器,不复制业务代码。
|
|
136
|
+
|
|
137
|
+
### 2.8 授权边界
|
|
138
|
+
|
|
139
|
+
当前 `AuthorizeMiddleware` 对未注册权限点的写请求默认放行,因此专用 API 使用三层授权,不能只依赖前端菜单或路径匹配:
|
|
140
|
+
|
|
141
|
+
1. 现有 `@Permission` Guard 在进入 controller 前抛错,而全局 error filter 会把它转成 HTTP 200;`PermissionService` 还会让 runtime principal 默认通过。首期 Origin API 不把这套机制伪装成保护层。
|
|
142
|
+
2. 服务层要求真实用户主体,显式拒绝 guest/runtime/application principal,从会话固定当前 tenant,再 `assertPlatformAdmin()`;普通应用管理员和应用角色不通过。controller 在方法内统一映射真实 HTTP status,body/query 不能指定另一个租户。
|
|
143
|
+
3. 跨租户 O0/O2/O3 预检、导入和 phase 切换不开放普通 HTTP,也不复用应用 runtime principal。它们由随 Platform Server 不可变镜像发布的运维 CLI 在 `server-deploy.sh` / K3s Job 中执行,使用既有受控数据库 Secret;必须携带 operation id、operator、change reference、release 和 requestId,每租户结果写控制面审计。
|
|
144
|
+
|
|
145
|
+
首期浏览器管理 API 只允许当前租户 `PLATFORM_ADMIN`,不做平台权限委派。未来需要委派时应先收敛平台级 seed 的唯一来源、Guard 的真实 HTTP 错误语义和 runtime principal bypass,再设计 `platform:tenant-public-origin:*` 权限与授权审计,不在本次迁移中默认开放。
|
|
146
|
+
|
|
147
|
+
数据库 FK、hostname claim、head revision CAS 和 phase lock 是最后一致性边界,不能被 `PLATFORM_ADMIN` bypass。`TenantsService.deleteTenant()` 必须在服务层先执行 decommission 检查,数据库 head→tenant FK 使用 `ON DELETE RESTRICT` 防止其他写入口硬删绕过;完成 decommission 后删除 head,才允许删除租户。immutable versions、hostname claim 和 audit 都以无租户 FK 的 UUID/code 快照保留,claim 到期由有界任务释放,版本/审计按平台审计保留策略归档而不由普通删除接口清除。
|
|
148
|
+
|
|
149
|
+
稳定 UUID 只解决 Origin registry 自身的所有权,不代表现有平台已经支持租户 code 重命名。当前大量业务表仍把 `tenants.code` 当作反规范化租户键,直接修改会使 SSO、角色、数据和应用记录分裂。B0-O1 因此必须给 `TenantsService.updateTenant()` 增加 `TENANT_CODE_IMMUTABLE` 栅栏:显示名称等资料仍可修改,code 变更一律拒绝;未来若需要重命名,应另建覆盖所有依赖表、外部凭据、缓存和回滚的专用事务,不在 Origin 改造中假装已经解决。
|
|
150
|
+
|
|
151
|
+
### 2.9 Cookie 是独立 B0-C 契约
|
|
152
|
+
|
|
153
|
+
现有 `AuthService` 同时把 DOMAIN JSON 用作 Cookie Domain,并固定 `secure: false`。这与公共 Origin 相邻,但不是同一能力。B0-O 不得在迁移表时顺手改变 Cookie,否则域名切换失败和登录态迁移失败会落入同一个回滚单元。
|
|
154
|
+
|
|
155
|
+
B0-C 在 registry 稳定后单独实施,并新增唯一 `AuthCookieService`,但不把所有协议 Cookie 塞进一个大服务。它只拥有平台共享会话 Cookie(access、refresh 和身份冒用控制)的名称、读取、写入、双代兼容与清理;`AuthService`、`JwtMiddleware`、传统 refresh controller、App Auth refresh、CLI browser auth、Application API gateway 和冒用身份控制不能再直接访问这些名称。App Auth/OAuth 的短期 verifier/nonce 仍由协议模块拥有,只复用无状态 `AuthCookiePolicyResolver` 取得 secure/host-only/sameSite/path 属性;公开访问、对象存储等非登录 Cookie 不在 B0-C 范围。发布门禁分别扫描“共享会话名的直接访问”和“协议 Cookie 自行推导 secure/domain”,避免中央服务变成新的协议耦合点。
|
|
156
|
+
|
|
157
|
+
- HTTPS canonical origin 的认证 Cookie 必须 `Secure`;HTTP 部署必须在部署清单显式选择 `insecure-http` 策略,Cookie 不带 `Secure` 并产生持续高危诊断。策略只来自受控部署配置,不从请求 Host/forwarded header 猜测;声明协议与 registry/外部探测不一致时 preflight 阻止 C1。
|
|
158
|
+
- v2 共享会话 Cookie 一律 host-only、`Path=/`,不设置 `Domain`,首期不支持跨子域共享。HTTPS 使用 `__Host-ox_access_v2`、`__Host-ox_refresh_v2` 等 `__Host-` 名称;显式 insecure HTTP 使用无前缀 `ox_access_v2`、`ox_refresh_v2`。未来若有真实跨子域共享需求,必须单独设计 scope registry、威胁模型和迁移,不能从 hostname 推导父域或给当前服务加一个任意 Domain 字符串。
|
|
159
|
+
- 新 Cookie 使用版本化名称,避免浏览器对同名 host-only/Domain Cookie 的发送顺序产生歧义。兼容期读 v2 优先、旧名只作受限 fallback;登录只签发 v2 并按 C0 生成的 legacy scope manifest 清除已知旧 Domain/host-only scope,登出同时清除两代名称。manifest 包含当前配置和运维声明的最近历史 hostname/domain/path,绑定摘要并进入发布证据;平台不能声称能清理一个未声明、不可访问的历史 host。
|
|
160
|
+
- 旧 refresh token TTL 当前为 30 天,因此兼容窗口不得短于已签发旧 refresh token 的最大剩余 TTL。窗口结束后停止接受旧名,而不是无限兼容。
|
|
161
|
+
- B0-C 单独回归密码、游客、钉钉、CAS、角色切换、退出和代表性 1.x;失败可回滚 Cookie 策略而不回滚 registry。
|
|
162
|
+
|
|
163
|
+
B0-C 也按兼容状态机发布:C0 把所有共享会话 Cookie 调用迁到 `AuthCookieService` 但仍只用旧名,同时冻结 legacy scope manifest;C1 所有服务实例升级为“v2 优先、旧名 fallback”后才切 writer 为 v2-only;C2 等待至少一个旧 refresh 最大 TTL 并确认旧名使用指标归零后删除 fallback。C1 以后日常回滚只能回到能读取 v2 的 C1 兼容版本,不能回到 C0/更早镜像;因此“独立回滚”是指不回滚 Origin registry,不承诺任意历史镜像仍能读取新 Cookie。
|
|
164
|
+
|
|
165
|
+
## 3. 多实例迁移、写栅栏与回滚
|
|
166
|
+
|
|
167
|
+
普通滚动发布不能安全切换:旧 Pod 仍写 JSONB,新 Pod 若写 registry 会形成永久漂移;但把所有租户一次性冻结又会把单租户配置问题放大为全平台故障。expand migration 因此采用“两级状态机”:单行 `tenant_public_origin_control.phase` 只表示平台总体处于 `legacy`、`migrating` 或 `registry`,每个 `tenant_public_origin_heads.source_state` 独立表示该租户处于 `legacy`、`frozen`、`registry` 或 `decommissioned`。禁止用进程缓存协调迁移。
|
|
168
|
+
|
|
169
|
+
数据库锁顺序是协议的一部分:global start/finalize 对 control 行取 `FOR UPDATE`;租户创建/删除、DOMAIN 写与单租户 Origin 命令先对 control 取 `FOR SHARE`,再对目标 head 取 `FOR UPDATE` 并做 revision CAS;涉及多个 hostname claim 时按规范化 hostname 升序加锁。`FOR SHARE` 允许不同租户并行,但会让 finalize 等待全部在途租户事务结束。任何事务都不得在持锁期间执行 DNS/TLS/provider 网络调用;verify 分为签发 challenge、事务外探测、写 verification 三段,最终写入重新检查 head revision/state digest。
|
|
170
|
+
|
|
171
|
+
- 全局 `legacy`:公共入口读取和旧管理写仍以 DOMAIN JSON 为准;新服务只做规范化与 observe,不长期双写。
|
|
172
|
+
- 全局 `migrating`:所有可服务实例已经理解两级状态。每个租户独立迁移:`legacy` 租户继续从旧配置读写;`frozen` 租户仍读旧配置但 DOMAIN 写返回 423;`registry` 租户只读写 registry,旧 DOMAIN 写返回 409;`decommissioned` 租户没有公共入口,相关认证 fail closed。
|
|
173
|
+
- 全局 `registry`:所有现存租户均已是 `registry` 或 `decommissioned`,运行时不再执行 legacy 解析;以后新租户直接创建 registry head,但在激活 public origin 前保持 provisioning 状态。
|
|
174
|
+
|
|
175
|
+
进入 `migrating` 前,部署 CLI 必须证明所有可服务实例都运行理解两级栅栏的兼容版本;Kubernetes 适配器检查所有可服务 Pod 的 release/capability,Docker/三机适配器检查声明的每个上游实例,readiness 本身不能证明版本一致。开始迁移的事务锁定 control,导入经验证的全覆盖计划,并为每个现存租户建立 head/source state,但不冻结任何租户。
|
|
176
|
+
|
|
177
|
+
O0 工具同时扫描全部现存租户与全部 legacy DOMAIN 行,生成事实 inventory、逐租户 legacy/依赖 digest、孤儿行摘要和全量 inventory digest,但绝不替操作者决定去留。运维人员依据 inventory/变更单为每个租户显式选择 `migrate` 或 `decommission`,并为无法关联现存租户的 legacy 行选择 `retire-orphan`;validator 再证明计划全覆盖:
|
|
178
|
+
|
|
179
|
+
- `migrate` 必须指定稳定租户 UUID、目标 staged origin 和后续验证责任;不能由 migration 猜 scheme 或域名。
|
|
180
|
+
- `decommission` 必须证明没有启用 SSO、外部回调、App Auth、OpenAPI/verification、消息链接或仍受流量的平台注册入口;有依赖时不能选。
|
|
181
|
+
- `retire-orphan` 只能用于确实无法关联稳定租户 UUID 的旧行;保存配置 id/code/configData 摘要和处置审计,不创建 head/version/claim。若仍有请求、回调或链接依赖该行,必须先人工关联/清理,validator 不允许直接退休。
|
|
182
|
+
- 计划开始时必须覆盖所有现存租户和每条 legacy 行;同一租户只有一个主结论,但其计划项要列出全部重复 legacy config id 并明确唯一目标值。迁移期仍允许其他 `legacy` 租户发生正常写入;因此冻结某个租户前只重算并校验该租户 digest。若已变化,必须显式更新该计划项和计划摘要,不能让无关租户变化使整批迁移永久失效。
|
|
183
|
+
|
|
184
|
+
在 global migrating 且 tenant source=legacy 时,旧 DOMAIN create/update/delete/toggle 仍写 legacy,但必须在同一事务把修改前后能关联到的所有 tenant head 按 UUID 排序加锁,清空其 planned digests、增加 head revision,并把被删除或改属的 disposition 标为 `superseded`;它不创建/更新 registry version 或 claim,因此不是双写。无法关联的新增/变更行成为未处置 orphan,只阻止最终 finalize。显式 update-plan 才能重新建立 current disposition 和计划摘要。tenant frozen 后所有旧 DOMAIN 写均为 423,避免 staged 与旧值再次分叉。
|
|
185
|
+
|
|
186
|
+
单租户迁移严格执行:`legacy -> frozen` 在一个事务中校验该租户计划/digest 并建立 staged/claim;冻结后修正网关、staged 和 provider 配置,再取得绑定 staged version/head revision 的短期验证回执;`frozen -> registry` 消费回执并只切换该租户事实源。任一步失败都只影响该租户,其他租户继续服务。尚未激活时可执行 `frozen -> legacy`,释放未引用 claim 并保留不可变版本/审计;一旦进入 tenant `registry`,就不再回退到模糊 legacy 解析,业务回滚必须重新 stage/verify/activate previous version。
|
|
187
|
+
|
|
188
|
+
迁移期间创建租户与 finalize 使用同一 control 行锁串行化:新租户自动得到 `source_state=legacy` 并进入待处置清单;删除仍须先 decommission。最终 `migrating -> registry` 事务只有在不存在缺失 head、`legacy/frozen` 租户、未处置/未审计的 orphan 或重复 DOMAIN 行、过期计划项时才成功。进入全局 registry 后,新租户直接为 `source_state=registry` 且 active 为空,所有依赖 origin 的能力 fail closed,直到完成正常 stage/verify/activate。
|
|
189
|
+
|
|
190
|
+
外部网关无法与数据库原子提交,因此每个租户的 verify/activate 必须持有该租户迁移锁,回执使用短 TTL;全局 finalize 不复用外部网络回执,也不批量切租户 active,只证明全部单租户切换已经完成。这是真实的一致性边界,不能写成“完全原子”。
|
|
191
|
+
|
|
192
|
+
普通平台后端发布只负责 O1 expand/兼容镜像,绝不自动开始迁移、冻结租户、激活或 finalize。O2/O3 使用独立显式命令和 receipt,避免一次日常小版本发布顺带切换认证事实源。逐文件和命令合同见 [租户公共 Origin v2 实施蓝图](./tenant-public-origin-implementation-blueprint.md)。
|
|
193
|
+
|
|
194
|
+
迁移开始后日常回滚只能到理解 `migrating` 与单租户 source state 的 O1 兼容版本;不能直接回到不认识新表的旧镜像。已激活租户不支持自动反投影到 legacy;越级灾难处置需要单独设计并演练,不属于常规发布回滚。
|
|
195
|
+
|
|
196
|
+
| 阶段 | 行为 | 发布条件 | 回滚单元 |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| B0-O0 诊断 | 目标镜像提供只读 JSON inventory/digest 和 plan validator,部署层补充真实网关检查;操作者显式编写全覆盖处置计划 | 列出结构、重复、规范化、scheme、全部依赖、legacy digest、逐行 migrate/decommission 和网关未验证项;工具未替人猜处置 | 删除诊断接线,无运行时影响 |
|
|
199
|
+
| B0-O1 expand/兼容 | 建 versions/heads/claims/verifications/audit/control;所有调用方接中央服务但 phase=`legacy`,记录差异;专用 UI 先只读 | 所有服务实例版本一致;代表性 1.x、SSO、游客、钉钉、App Auth、OpenAPI、消息无未知差异 | 回滚到 O1 前镜像;phase 仍 legacy,无事实源切换 |
|
|
200
|
+
| B0-O2 逐租户迁移 | 验证全覆盖计划后进入 global migrating;逐租户校验 digest、freeze、stage、网关/provider 修正、verify、activate 或 decommission | 每次只冻结目标租户;该租户事实源恰好一个;失败不影响其他租户;所有 receipt/claim/audit 原子一致 | 未激活租户可 abort 回 legacy;已激活租户只走 registry 版本回滚,不恢复模糊匹配 |
|
|
201
|
+
| B0-O3 全局收口 | 在所有租户均 registry/decommissioned 后原子 finalize global registry;旧读取动态投影,运行时删除 legacy 分支前先观察 | 双 Pod/三机、多虚拟主机、攻击向量、查询计划、完整 1.x/2.0 回归通过;无缺失 head/legacy/frozen/陈旧计划项 | 只回滚到理解 registry 的兼容版本;finalize 不批量切 active,因此不制造全平台业务回滚 |
|
|
202
|
+
| B0-C | Cookie Secure/host-only 迁移 | registry 稳定;双 scope 清理和全登录方式回归通过 | 不改 registry;C0 可回前版,C1 后只回到能读取 v2 Cookie 的兼容版 |
|
|
203
|
+
| B0-R | 收紧通用 callback 与 CAS return target | B0-O3 稳定且 preflight 仍全绿 | 前端、CAS 服务端分别独立提交和回滚 |
|
|
204
|
+
|
|
205
|
+
回滚不得恢复 `endsWith` / `includes` 或长期双写。某租户异常时可单独停用其 SSO 或修正入口,不能让全平台回到不确定租户解析。
|
|
206
|
+
|
|
207
|
+
## 4. 可证伪验收
|
|
208
|
+
|
|
209
|
+
- `school.example.com.evil.test`、父域、子域、大小写、尾点、Unicode/Punycode、userinfo、协议相对、反斜杠和控制字符不能命中其他租户。
|
|
210
|
+
- 两租户 claim 相同 hostname 时事务失败,即使 scheme/port 不同;同租户可 stage 同 hostname 的 HTTPS/端口变更。
|
|
211
|
+
- 两个管理员用相同 head revision 并发 stage/activate 只能一个成功,另一个得到 409;版本、head/claim 和同 revision 审计事件原子可见。
|
|
212
|
+
- HTTPS 页面配成 HTTP canonical origin 时 preflight 失败,CAS 不忽略 scheme;网关无法验证时为 `unverified`,不能进入 O2/O3。
|
|
213
|
+
- 普通别名的平台入口/认证请求在网关 308,平台日志中没有该别名认证请求;retirement host 只能得到清 Cookie/no-store/固定根入口跳转,ticket/code/query 不被转发;同主机非平台路径不受影响。
|
|
214
|
+
- 两个 Platform Server Pod 对同一输入返回相同 tenant/origin;配置提交后不等待本地 TTL。
|
|
215
|
+
- 混合版本实例存在时禁止进入 migrating;迁移期只冻结目标租户,其 legacy 写失败、staged 写不改变运行时、登录读取继续,其他租户读写不受影响;切换事务故障后该租户仍完整停留 frozen。
|
|
216
|
+
- staged 未验证、回执过期/已消费、目标租户 state digest/部署 release/网关声明摘要改变或网关处于 `unverified` 时 activate 必须失败;其他租户变化不会使该回执失效,active origin 保持不变。
|
|
217
|
+
- challenge 不能用于读取租户信息或建立会话;伪造 Host、重放 nonce、用浏览器主体写回执、TLS hostname 不符和 provider 证据缺失都不能得到可激活回执。
|
|
218
|
+
- 普通更新租户 code 返回 `TENANT_CODE_IMMUTABLE`,不会制造 Origin 与旧 code 依赖表分裂;未 decommission/删除 head 不能删除租户。删除后 hostname 继续被 decommission-hold 占用,version/audit 保留 UUID/code 快照,claim 清理到期后才能释放。
|
|
219
|
+
- 连续激活后每租户最多一个 previous active;旧 previous claim 被原子释放并审计。过期 previous 不能直接回滚,必须重新 stage/verify。
|
|
220
|
+
- registry 模式下旧 DOMAIN 读取返回动态兼容投影,旧写返回 409;legacy 行不随新写变化,证明没有长期双写。
|
|
221
|
+
- legacy 处置计划初始覆盖率为 100%,同时覆盖全部现存租户、重复行和 orphan 行;任意漏项、重复结论、未列全的重复 config id、带依赖的 decommission/retire-orphan 都阻止开始迁移;单租户 digest 变化只阻止该租户 freeze,显式刷新计划项后才能继续;只要仍有 legacy/frozen/缺失 head 或未审计 legacy 行就阻止全局 finalize。
|
|
222
|
+
- 代表性 1.x View、平台管理端、流程设计器、CLI browser auth、游客/钉钉、CAS、App Auth、OpenAPI/verification 和消息链接正向回归。
|
|
223
|
+
- 查询计划使用预期索引;超长输入查询前拒绝;日志不记录 ticket、code、Secret 或完整敏感 query。
|
|
224
|
+
- HTTPS v2 Cookie 带 Secure、`__Host-` 前缀且 host-only;共享会话消费者 v2 优先,协议 verifier 只复用 policy resolver;C1 切 writer 前没有不理解 v2 的可服务实例;C0 legacy scope manifest 摘要稳定,C2 前旧名 fallback 指标归零且已覆盖旧 refresh 最大 TTL。
|
|
225
|
+
- HTTP 部署没有显式 `insecure-http` 策略或声明协议与实际入口不一致时不能进入 C1;诊断不得因请求 Host 自动降级 Cookie 安全属性。
|
|
226
|
+
- hostname 变化后旧 host 在 retirement hold 期间清理旧 Cookie 并跳 canonical;hold 未到期不能转给其他租户,也不能激活第三个 hostname;回滚到当前 previous 时 claims 原子交换且仍只有一个 retirement host。
|
|
227
|
+
- B0-R 的跨域、双重编码和错误链路测试在 B0-O3 之后继续通过。
|
|
228
|
+
|
|
229
|
+
## 5. 明确不做
|
|
230
|
+
|
|
231
|
+
- 不在首期建设多 canonical origin、应用级自定义域名或任意外部 redirect URI。
|
|
232
|
+
- 不把 Nginx 配置变成租户身份数据库,也不让应用后端解析租户域名。
|
|
233
|
+
- 不长期双写 `default_configs` 与 registry,不用本地缓存协调迁移。
|
|
234
|
+
- 不把 Cookie policy 塞进 Origin 表,也不因 prod-1 配置错误放宽安全合同。
|
|
235
|
+
- 不在 B0-C 首期支持跨子域共享 Cookie,也不把公开访问/存储 Cookie 迁进共享会话服务。
|
|
236
|
+
- 不顺带重写第三方 OAuth state、账号绑定、App Auth 或 2.0 Client Credentials;它们保持各自协议和发布边界。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# OpenXiangda 2.0 验证编排边界
|
|
2
|
+
|
|
3
|
+
## 问题证据
|
|
4
|
+
|
|
5
|
+
仓库级 `turbo run check test build` 同时调度了模板根包
|
|
6
|
+
`openxiangda-application` 和它的 `@app/*` 子包。模板根包为了让生成后的独立应用可用,
|
|
7
|
+
其 `build/check/test` 又会递归运行同一批子包。两个 `@app/web build` 并发操作同一个
|
|
8
|
+
`dist`,出现构建已经输出文件、随后校验却收到 `ENOENT` 的竞态。
|
|
9
|
+
|
|
10
|
+
## 能力所有者与不变量
|
|
11
|
+
|
|
12
|
+
- 仓库级任务图唯一由 Turbo 所有;每个真实包的每种任务只允许一个写者。
|
|
13
|
+
- `openxiangda-application` 只是在生成应用后使用的聚合入口,不是仓库级验证单元。
|
|
14
|
+
- 模板根包继续保留递归脚本,确保复制成独立项目后仍可直接执行
|
|
15
|
+
`pnpm build/check/test`。
|
|
16
|
+
- 仓库验证排除聚合包,但仍直接并行验证 `@app/server`、`@app/web`、
|
|
17
|
+
`@app/domain` 和 `@app/contracts`,不降低覆盖范围。
|
|
18
|
+
|
|
19
|
+
## 失败、回退与验证
|
|
20
|
+
|
|
21
|
+
`scripts/verify-workspace-orchestration.mjs` 在每次 `verify` 和
|
|
22
|
+
`verify:affected` 前检查以上边界。若聚合包改名、仓库脚本遗漏排除规则或生成应用失去
|
|
23
|
+
聚合脚本,验证会在启动昂贵任务前失败。该修改只影响 2.0 仓库本地/CI 编排,不改变
|
|
24
|
+
应用运行时、平台服务或生产部署;回退只需恢复仓库级脚本。
|
package/docs/backend.md
CHANGED
|
@@ -16,13 +16,13 @@ src/health liveness/readiness
|
|
|
16
16
|
|
|
17
17
|
## 身份和接口
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
调用者原始 token 只到平台网关,不进入应用容器。网关根据当前 Native Head 换取最长 60 秒的 invocation token,并用最长 30 秒的 Ed25519 assertion 绑定目标应用、环境、AppVersion、DeploymentRun、请求方法、路径、查询和正文摘要。`openxiangda-nest` 的全局 transport guard 在业务控制器前校验 assertion 与运行容器身份,再向平台实时确认 RoleSession/Application Principal;NodePort、cluster DNS 或反向代理直连因缺少 assertion 被拒绝。应用开发者不配置网关密钥,也不手写这段校验。
|
|
20
20
|
|
|
21
|
-
用户身份必须绑定 RoleSession;应用身份绑定 tenant、app、environment、client、credentialVersion 和 scopes
|
|
21
|
+
用户身份必须绑定 RoleSession;应用身份绑定 tenant、app、environment、client、credentialVersion 和 scopes。业务 controller 使用生成的 `appOperations` 和 `@OpenXiangdaOperation()` 同时绑定不可变 operation contract 与 capability;`OpenXiangdaAuthzGuard` 对服务身份要求 `app:invoke` 和完全匹配的 operation capability。`@RequireCapability()` 只保留给官方 SDK 内部与非业务适配层,应用 controller 不应散写 capability 字符串。应用身份可以调用授权的 App API/Data API,但 Workflow 客户端拒绝没有用户 RoleSession 的上下文。
|
|
22
22
|
|
|
23
|
-
Application 2.0
|
|
23
|
+
Application 2.0 的远程运行环境只有 `preproduction`、`production` 两条固定通道,`environmentKey` 是 Principal、RoleSession、事件、Workflow、App API 和运行容器共同使用的稳定身份键,并与通道名一致;`environmentId` 是 Native registry 中必须参与授权和防串联校验的稳定 UUID。`local` 是 CLI 管理的 loopback 运行模式,不是 environmentKey,也不创建远程 OAuth、Secret、Head 或 RoleSession。需要同类多环境时必须升级发布契约,不能把 UUID 或任意字符串临时当成 `environmentKey`。
|
|
24
24
|
|
|
25
|
-
Worker、Scheduler 或事件消费者没有浏览器请求上下文时,注入 `OpenXiangdaApplicationDataApiService`。每个已部署后端的应用环境都有一个平台托管的 workload OAuth2 client;平台自动创建或复用客户端,并通过 Kubernetes Secret 注入 `OPENXIANGDA_OAUTH_CLIENT_ID`、`OPENXIANGDA_OAUTH_CLIENT_SECRET` 和 `OPENXIANGDA_OAUTH_SCOPES`,这些保留变量不写入 `backend.secrets
|
|
25
|
+
Worker、Scheduler 或事件消费者没有浏览器请求上下文时,注入 `OpenXiangdaApplicationDataApiService`。每个已部署后端的应用环境都有一个平台托管的 workload OAuth2 client;平台自动创建或复用客户端,并通过 Kubernetes Secret 注入 `OPENXIANGDA_OAUTH_CLIENT_ID`、`OPENXIANGDA_OAUTH_CLIENT_SECRET` 和 `OPENXIANGDA_OAUTH_SCOPES`,这些保留变量不写入 `backend.secrets`。本地开发时 CLI 自动创建 workspace runtime client,并只向 NestJS 进程注入同名变量;前端进程、生成 manifest、日志和 AppPackage 都不包含原始 Secret。
|
|
26
26
|
|
|
27
27
|
运行凭据的状态只包含 clientId、当前版本、hint、宽限期和待激活轮换元数据,不返回 Secret。`openxiangda oauth runtime rotate --environment <key> --idempotency-key <key>` 使用当前版本 CAS 暂存新凭据,并自动对当前激活 AppVersion 创建一次同版本滚动部署;部署准备阶段才把待激活凭据提升为当前凭据。旧 Pod 使用的上一版本在指定宽限期内仍可换 token,因此轮换不依赖同时重启全部副本。失败或重复请求不会生成第二份凭据,运维重试必须复用幂等键。
|
|
28
28
|
|
|
@@ -33,6 +33,13 @@ OpenXiangdaModule.forRoot({
|
|
|
33
33
|
appCode: process.env.OPENXIANGDA_APP_CODE!,
|
|
34
34
|
platformBaseUrl: process.env.OPENXIANGDA_PLATFORM_BASE_URL!,
|
|
35
35
|
environmentKey: process.env.OPENXIANGDA_ENVIRONMENT_KEY!,
|
|
36
|
+
environmentId: process.env.OPENXIANGDA_ENVIRONMENT_ID!,
|
|
37
|
+
appVersionId: process.env.OPENXIANGDA_APP_VERSION_ID!,
|
|
38
|
+
deploymentRunId: process.env.OPENXIANGDA_DEPLOYMENT_RUN_ID!,
|
|
39
|
+
environmentHeadRevision: Number(
|
|
40
|
+
process.env.OPENXIANGDA_ENVIRONMENT_HEAD_REVISION!
|
|
41
|
+
),
|
|
42
|
+
backendRevisionId: process.env.OPENXIANGDA_BACKEND_REVISION_ID!,
|
|
36
43
|
version: process.env.OPENXIANGDA_APP_VERSION!,
|
|
37
44
|
oauthClient: {
|
|
38
45
|
clientId: process.env.OPENXIANGDA_OAUTH_CLIENT_ID!,
|
|
@@ -48,6 +55,8 @@ OpenXiangdaModule.forRoot({
|
|
|
48
55
|
|
|
49
56
|
详情使用单记录接口,统计报表使用受限聚合接口,审计使用业务记录审计接口。三者都沿用调用上下文中的 RoleSession 和字段策略。附件先调用 initiate 获得短期签名上传计划,上传对象后调用 complete 校验,再把返回的 `DataFileRef` 随普通 create/update 或受限事务保存;不要把对象存储密钥、对象名或签名 URL作为业务字段。
|
|
50
57
|
|
|
58
|
+
家校应用读取监护人、学生和班级关系时,使用 Nest SDK 的 RoleSession 绑定接口,并由应用自己的 App API 向页面提供业务结果;浏览器不得直连平台接口。返回值同时提供平台用户 ID、手机号、钉钉 userid 和姓名。完整接入方法见[家校通讯录关系](./school-contact-relations.md)。
|
|
59
|
+
|
|
51
60
|
## 运行与 Secret
|
|
52
61
|
|
|
53
62
|
容器必须提供 liveness/readiness,支持优雅退出和结构化日志。事件签名密钥、工作流 provider 密钥及外部系统凭证由平台生成 Kubernetes Secret,并通过 `valueFrom.secretKeyRef` 注入;不会写入 AppPackage 或返回给浏览器。
|
|
@@ -75,14 +84,14 @@ backend: {
|
|
|
75
84
|
```bash
|
|
76
85
|
OPENXIANGDA_SECRET_VALUE='...' \
|
|
77
86
|
openxiangda secret create dingtalk-client-secret \
|
|
78
|
-
--environment
|
|
87
|
+
--environment preproduction \
|
|
79
88
|
--from-env OPENXIANGDA_SECRET_VALUE
|
|
80
89
|
|
|
81
|
-
openxiangda secret list --environment
|
|
90
|
+
openxiangda secret list --environment preproduction
|
|
82
91
|
|
|
83
92
|
OPENXIANGDA_SECRET_VALUE='...' \
|
|
84
93
|
openxiangda secret rotate dingtalk-client-secret \
|
|
85
|
-
--environment
|
|
94
|
+
--environment preproduction \
|
|
86
95
|
--revision 1 \
|
|
87
96
|
--from-env OPENXIANGDA_SECRET_VALUE \
|
|
88
97
|
--idempotency-key secret-rotation-20260812
|
package/docs/data-authz.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
2.0 权限由 RBAC 与上下文数据策略共同组成:角色回答“可以做什么”,策略回答“在当前角色下可以对哪些数据和字段做”。
|
|
4
4
|
|
|
5
|
+
家校关系使用相同的活动角色边界。默认授予 `app:<appCode>:school-contact:read`,范围为不限制的 `all`;需要收紧时改授 `self:read` 或 `class:read`,范围由平台在查询中强制执行,页面不能靠客户端过滤。见[家校通讯录关系](./school-contact-relations.md)。
|
|
6
|
+
|
|
5
7
|
## 请求模型
|
|
6
8
|
|
|
7
9
|
```text
|
|
@@ -70,7 +72,30 @@ authz: {
|
|
|
70
72
|
|
|
71
73
|
`roleCodes` 把一条数据规则限定到当前 `RoleSession` 选中的角色。同一用户同时是申请人、学院管理员时,切换到学院管理员后,`applicant` 的 `current_user` 规则不再参与 OR 计算。多角色应用的角色特有规则必须声明 `roleCodes`;未声明时表示该规则对所有活动角色通用。编译器会拒绝空数组、重复或未声明的角色代码。
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
角色成员、学院范围授权和“用户—仪器”关系授权属于环境运行数据,不进入不可变应用包。它们通过 Native 管理 API 维护,部署新版本不会删除。所有写操作要求 UUID `operationId`、目标资源 revision 和活动管理 RoleSession;平台在数据库事务内完成权限复核、幂等回执、CAS、相关会话撤销与审计。
|
|
76
|
+
|
|
77
|
+
新应用由 `openxiangda app provision` 为创建者建立平台保留的 `platform.app-super-admin` grant。它不是应用包中的普通角色,不能被业务流程代理或通过多个角色合并获得。重复执行 provision 会复用同一个 grant,可用于补齐早期创建但尚未初始化 Native 管理员的 2.0 应用。
|
|
78
|
+
|
|
79
|
+
CLI 提供标准成员、关系授权和最高管理员生命周期,不需要修改数据库:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
openxiangda authz membership list --environment preproduction
|
|
83
|
+
openxiangda authz membership grant \
|
|
84
|
+
--environment preproduction \
|
|
85
|
+
--user user-123 \
|
|
86
|
+
--role college_admin \
|
|
87
|
+
--scope college=college-a,college-b
|
|
88
|
+
|
|
89
|
+
openxiangda authz relationship grant \
|
|
90
|
+
--environment preproduction \
|
|
91
|
+
--subject-type role_membership \
|
|
92
|
+
--subject-key <membership-id> \
|
|
93
|
+
--relation instrument_manager \
|
|
94
|
+
--resource instrument \
|
|
95
|
+
--resource-id <instrument-id>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`update` 和 `revoke` 命令必须携带列表返回的 `revision`。CLI 默认生成 UUID 操作 ID;跨进程重试时可显式传入同一个 `--operation-id`,平台只会重放完全相同的请求。预发与生产成员关系彼此独立,不随 AppVersion 晋级复制。
|
|
74
99
|
|
|
75
100
|
## Directory v2
|
|
76
101
|
|
|
@@ -86,7 +111,7 @@ authz: {
|
|
|
86
111
|
|
|
87
112
|
## 管理员
|
|
88
113
|
|
|
89
|
-
应用管理员保留 bypass
|
|
114
|
+
应用管理员保留 bypass。它由平台保留的 super-admin grant 实现,不是应用配置中的 `app_admin` 角色,也不是散落在查询代码中的特判;所有绕过业务数据策略的写操作都记录审计原因、操作者和关联 ID。
|
|
90
115
|
|
|
91
116
|
## 防护
|
|
92
117
|
|
package/docs/delivery.md
CHANGED
|
@@ -48,11 +48,11 @@ Changesets 管理各个 `openxiangda-*` 包、CLI、MCP 与 skill-kit 的独立
|
|
|
48
48
|
|
|
49
49
|
发布入口只打包一次,并在 Git 私有目录写入绑定源码 `HEAD`、registry、包版本、字节数、SHA-256 与 npm SHA-512 integrity 的工件清单。全新应用、独立 reference app 与正式 npm publish 必须消费同一批 `.tgz`;任何字节或清单变化都会在写 registry 前失败。部分发布中断后可以从 receipt 继续:已经发布且 integrity 一致的包被跳过,不一致则停止。npm 在逐包写入时可能先推进预发布 tag;receipt 只接受这一个可恢复中间态,并在所有包确认后统一收敛最终 dist-tag 和 Git tag。
|
|
50
50
|
|
|
51
|
-
`pnpm verify:local`
|
|
51
|
+
`pnpm verify:local` 是日常可重复执行的全量验收入口。涉及本地 PostgreSQL 生命周期时,候选 tarball 只在 monorepo 外的新应用中运行一次 Chromium:生命周期门禁先通过受所有权校验的 reset 建立确定性数据库,再让同一浏览器套件同时经过 React、本地平台、NestJS 与 PostgreSQL;不会先跑 UI-only 再重复浏览器工作,也不会清理开发者现有应用的数据。正式候选在版本提交推送后由 `pnpm release:plan` 比较 npm 与本地 tarball,并比较候选与上一发布版本;漏升版或待消费 Changeset 会在昂贵测试前直接失败。`pnpm release:publish` 冻结一次候选工件,执行机器生成的增量计划,并在成功后发布同一批 tarball;候选依赖闭包永远完成 check/test/build,每个候选 tarball 都在 monorepo 外安装进全新生成应用并完成 generate/check/test/build。浏览器相关变化提升到真实 Chromium E2E,核心应用 SDK 变化增加持久 reference app,Skill/文档变化增加相应门禁。`pnpm verify:release` 提供相同规则的无发布演练,不能与正式发布组合成必须重复执行的双门禁。未知变化 fail-closed 到完整矩阵;`pnpm verify:release:full` 保留周期性全量审计。
|
|
52
52
|
|
|
53
53
|
Playwright 浏览器使用官方缓存目录;`playwright install chromium` 已安装对应版本时是无操作。发包门禁不会额外运行 1.x 测试,也不会为每个包重复浏览器验收,而是在最终独立新应用上只运行一次完整用户路径。
|
|
54
54
|
|
|
55
|
-
当计划要求 reference app 时,工具仓只把本次候选包发布到一次性、仅绑定 `127.0.0.1` 的 registry。每个候选包按精确包名注册为本地权威源且禁止回源,防止同版本公网包抢先占位或旧包被静默安装;未变化的包和普通依赖才继续从 npm
|
|
55
|
+
当计划要求 reference app 时,工具仓只把本次候选包发布到一次性、仅绑定 `127.0.0.1` 的 registry。每个候选包按精确包名注册为本地权威源且禁止回源,防止同版本公网包抢先占位或旧包被静默安装;未变化的包和普通依赖才继续从 npm 代理取得。验收副本不复用 reference worktree 的 lockfile,而是在临时目录从本轮 registry 生成一次 scratch-only lock,避免尚未物化新版本时相同预发布版本的旧 integrity 触发无意义重试;源仓锁文件不会被隐式修改。持久独立 reference app 再执行安装、契约生成、类型检查、单测、真实 NestJS 身份/Data API 进程验收和生产构建。`pnpm reference:install:from-build` 可在未公开发包时通过同一 registry 协议刷新 reference worktree 的本地依赖,避免 `file:`/`link:` 破坏独立性。
|
|
56
56
|
|
|
57
57
|
增量门禁先用 Turbo 完成候选包及其依赖闭包的 check/test/build;全量模式完成整个 workspace。后续生成契约、tarball 黑盒、技能校验和文档构建复用已验证的 `dist`。`distribution:smoke`、`skills:check`、`docs:build` 仍保留可独立执行的自包含入口。
|
|
58
58
|
|
|
Binary file
|
|
Binary file
|
package/docs/frontend.md
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
# 前端
|
|
2
2
|
|
|
3
|
-
`openxiangda-admin` 提供标准 B
|
|
3
|
+
`openxiangda-admin` 提供标准 B 端应用外壳:响应式布局、多级菜单、路由、面包屑、固定与可关闭标签、有界页面保活、个人中心、权限点、稳定角色会话、标准数据管理页、工作流任务页面和应用自定义操作注册。
|
|
4
|
+
|
|
5
|
+
## 视觉基线与标准页面族
|
|
6
|
+
|
|
7
|
+
OpenXiangda 2.0 的产品界面按内部业务系统设计:可信、克制、适合长期高频使用,设计参数固定为低变化、低动效和中等信息密度。默认使用浅色主题、单一应用强调色和统一圆角层级;颜色用于主操作、选择和真实语义状态,不使用装饰渐变、玻璃效果或无意义动画。界面必须同时提供与最终内容同构的 skeleton、可行动空态、页面内错误与重试,以及键盘可达的显式操作。
|
|
8
|
+
|
|
9
|
+
Ant Design 6.4.2 是唯一运行时设计系统。ProComponents 作为成熟交互参考,但在其未声明兼容 Ant Design 6 时不引入 `@ant-design/pro-components`,避免第二套表单、查询和表格状态。OpenXiangda 自己的标准组件保持以下页面顺序和职责:
|
|
10
|
+
|
|
11
|
+
- 首页工作台:统一页头后按“关键指标、快捷入口、业务趋势/分布”组织。指标和图表只读服务端受限聚合;应用只声明业务名称、聚合请求和跳转目标。图表使用按需加载的 ECharts React 适配层,不伪造统计值。
|
|
12
|
+
- 数据管理页:`PageHeader -> SearchPanel -> TableToolbar -> Table -> Pagination`。默认服务端分页和排序,支持搜索折叠、列显隐、密度、跨页选择、批量业务动作以及独立新建/详情/编辑路由;页面不下载全量数据再过滤。
|
|
13
|
+
- 标准表单页:标题、业务说明和主操作固定在页头,字段按业务分组进入有限宽度内容区;标签始终位于控件上方,帮助文案和字段错误分别位于控件下方。离开、刷新、关闭标签和切换身份都接入统一脏状态协议。
|
|
14
|
+
- 流程提交页:桌面端使用业务表单与审批预览双栏,窄屏降为单栏。业务数据先由 Data API/App API 保存并获得 revision,随后才能预览和发起;字段、主部门或审批人答案变化会使旧预览失效。
|
|
15
|
+
- 数据详情页:以业务字段为主,操作和审计为辅;支持独立 URL、面包屑、标签和字段权限。不可读字段不占位,空值使用统一文本,不把 JSON 协议值直接暴露给用户。
|
|
16
|
+
- 流程详情页:依次展示流程标题与业务摘要、允许的当前操作、业务字段、审批参与者和不可变流转记录。按钮、字段策略和操作表单由后端 Surface 协议驱动;前端不复制流程可操作性规则。
|
|
17
|
+
- 流程工作中心:分类、表格和真实服务端分页处于同一内容表面;默认每页 20 条,显示总数并允许 10/20/50/100 切换。分类、页码或身份变化后旧响应不得覆盖当前页面。
|
|
18
|
+
|
|
19
|
+
真实组件页面和浏览器截图是设计验收的事实来源。图片生成只用于应用确实需要的品牌插图、空态插画或非结构化视觉素材,不用生成图替代可运行的表格、表单、流程和响应式交互稿。
|
|
4
20
|
|
|
5
21
|
## 页面组成
|
|
6
22
|
|
|
7
|
-
-
|
|
23
|
+
- 菜单与路由使用同一份生成合同。`defineAdminRouteManifest` 在模块加载时把合同与 React renderer 闭合,缺页面、重复路径、父子漂移、动态详情固定标签或缺 fallback 会让开发启动和生产构建共同失败。路由可用 `capability` 简写,或用 `access.allOf/anyOf` 表达组合能力;子路由继承祖先约束,菜单与直接 URL 使用同一批量授权结果。前端按 RoleSession 缓存和去重,服务端仍是最终授权边界。
|
|
8
24
|
- Data API 与 App API 使用类型化客户端,不在组件中拼接平台 URL。
|
|
9
|
-
- 当前角色是显式会话状态。角色选项同时展示业务数据范围,解决同一用户拥有多个同名角色绑定时的歧义。切换角色后重新获取 capability
|
|
10
|
-
-
|
|
11
|
-
-
|
|
25
|
+
- 当前角色是显式会话状态。角色选项同时展示业务数据范围,解决同一用户拥有多个同名角色绑定时的歧义。切换角色后重新获取 capability、数据范围与待办,并恢复该角色自己的标签;不会把多个角色权限隐式合并。RoleSession 在到期前自动续期,浏览器从休眠恢复时再次校验;短暂网络故障会保留最后一个可用会话并提供就地重试,不会把整个应用切换成错误页。
|
|
26
|
+
- `tabPersistence` 只决定刷新后是否恢复标签,`keepAlive` 只决定切换后是否保留 React 实例。静态路由默认 `session + none`,动态详情默认 `none + none`;列表和工作台可显式使用 `keepAlive:"memory"`。标签最多 12 个,页面实例最多 6 个并按 LRU 淘汰,离开前台 30 分钟的实例自动卸载。会话数据按 `identityScope + manifest fingerprint` 隔离且最多 16 KiB,不保存权限、业务响应或表单值。业务路由使用独立错误边界,局部渲染错误不会导致整个应用白屏。
|
|
27
|
+
- `DirtyStateRegistry` 统一保护标准全页表单、列表内编辑弹窗、流程提交选择和节点可编辑业务字段。菜单/History 导航、标签关闭/批量关闭/刷新、LRU/超时、角色切换和退出使用同一确认协议;取消后原实例与字段值保持不变,保存或确认放弃后才卸载。浏览器刷新仅使用原生 `beforeunload`,草稿不会写入前端持久存储。自定义页面可用 `useAdminDirtyState` 注册草稿、用同步 `useAdminBeforeDispose` 释放订阅/worker/object URL;后者不能保存业务数据或阻止撤权、identity/manifest 安全失效。
|
|
28
|
+
- 标准数据管理模块把搜索字段转换成 Data API filter,并在服务端处理分页、排序、修订冲突和错误展示;搜索区折叠、列显隐、密度、跨页选择和批量业务动作是统一扩展点。分页大小、表格密度、可见列与搜索展开状态按应用/角色绑定/资源保存;异步查询会中止旧请求并按 generation 丢弃无法中止的旧响应,失败时提供就地重试。
|
|
29
|
+
- `useDataWorkbenchController` 是 `DataListPage` 复用的无头读取状态机。普通资源页直接使用标准列表;只有自定义表面或组合视图才从 `openxiangda-admin/data` 使用 controller,并传入稳定的身份 scope、资源、配置版本、行标识器和类型化 `load` 适配器。它沿用同一个 DataQuery,不在浏览器筛选全量数据,不保存业务响应;identity epoch 改变时同步清空旧 page、错误与选择。控制面客户端暂不接收 `AbortSignal` 时,generation 仍保证旧响应不能覆盖新身份或新查询。
|
|
30
|
+
- `PageHeader`、`PageSection`、标准搜索卡片和数据表面构成统一页面骨架;列表默认采用“标题与主操作 → 搜索 → 工具条与表格 → 分页”的稳定顺序。应用只声明资源、字段与业务动作,不再自行复制布局 CSS。`OpenXiangdaAdminProvider` 统一提供 Ant Design 主题与简体中文 locale,页面不得再各自覆盖分页、日期、空态或无障碍标签。
|
|
12
31
|
- `department`、`user` 和 `relation` 可直接用于列表搜索与编辑表单。`DepartmentText` / `UserText` 把持久化 ID 批量解析成可读名称,按应用、RoleSession 和类型隔离缓存,解析失败时安全回退到原始 ID。
|
|
13
32
|
- 标准数据编辑表单从 DataResource 生成类型化基础控件,应用只需声明业务标签、枚举选项、帮助信息和少量覆盖;JSON、日期时间和保存错误由框架统一处理。字段读写权限统一来自 DataResource 的 fieldPolicies:不可读字段不会出现在列表、搜索、导出和表单中,只读字段不能提交修改,应用管理员明确 bypass。
|
|
14
33
|
- 标准数据列表默认提供“查看”入口和行详情抽屉。详情抽屉复用 Data API 单记录接口,按字段策略展示业务字段,并以时间线显示当前记录的持久业务审计;操作者名称通过 Directory v2 解析,加载、空状态、失败和重试都有统一交互。
|
|
34
|
+
- 需要独立 URL、面包屑和标签的业务使用 `DataFormPage` / `DataRecordPage`;列表通过 `onCreate/onDetail/onEdit` 导航到生成合同中的静态或动态子路由。轻量操作仍可保留 Modal/Drawer。创建、修改或删除成功只增加当前资源的内存失效版本,缓存列表和详情据此重新查询;标签恢复不会保存记录响应、权限结果或表单草稿。
|
|
15
35
|
- `file` 字段使用平台托管附件组件:浏览器按签名计划直传对象存储,完成校验后只把稳定 `DataFileRef` 写入业务记录;查看和下载仍通过 Data API 执行行权限及字段权限。应用不能拼对象存储地址或保存临时签名 URL。
|
|
16
36
|
- 数据导出沿用当前服务端 filter/order 并按权限裁剪字段;CSV 防公式注入并限制最大导出量。导入先本地解析与类型预检,再以最多 100 个操作的受限事务批次提交,每批使用稳定幂等键,支持失败后安全重试。
|
|
17
|
-
- `DashboardPage`、`MetricCard`、`DataCountMetric`、`DataAggregateChart`、`DataDistributionChart`、`DataTrendChart` 和 `DashboardSection` 提供 RoleSession 约束的标准概览页。图表使用服务端受限聚合,ECharts 运行时按需加载;指标查询丢弃过期响应并提供独立错误恢复,不要求每个应用重复搭建卡片、图表与加载状态。
|
|
37
|
+
- `DashboardPage`、`MetricCard`、`WorkflowCountMetric`、`DataCountMetric`、`DashboardQuickActions`、`DataAggregateChart`、`DataDistributionChart`、`DataTrendChart` 和 `DashboardSection` 提供 RoleSession 约束的标准概览页。图表使用服务端受限聚合,ECharts 运行时按需加载;指标查询丢弃过期响应并提供独立错误恢复,不要求每个应用重复搭建卡片、图表与加载状态。
|
|
18
38
|
- `ApplicationOperationsPage` 提供应用管理员运行视图:平台契约、当前身份、后端 readiness/version、不可变 AppVersion 最近部署、事件重试/死信和 `eventId/requestId/traceId` 关联信息集中展示;事件订阅与定时事件可按 revision 暂停/启用,重试或死信可显式创建新投递并保留原记录。各探针独立失败,不会因为一个依赖不可用而把整页变成白屏,诊断快照可一键复制。
|
|
19
39
|
- `ApplicationCredentialsPage` 提供标准的应用管理员安全控制面:外部 OAuth2 Client 生命周期与审计、平台托管后端运行时凭据的分阶段轮换、环境级只写应用 Secret 生命周期与审计、Workflow 审批人 Provider 密钥轮换。浏览器只保存当前表单状态;外部 Client Secret 仅在成功响应后一次性展示,关闭即清除;应用 Secret、运行时凭据和 Provider 密钥永不回显或写入快照、日志与持久化缓存。
|
|
20
|
-
- 生产应用从 `openxiangda-admin/core`、`/data`、`/dashboard`、`/operations`、`/credentials`、`/workflow` 子路径导入,并用 `React.lazy`
|
|
21
|
-
- 工作流页面按后端返回的 actions、fieldPolicy 和 presentation
|
|
22
|
-
-
|
|
40
|
+
- 生产应用从 `openxiangda-admin/core`、`/data`、`/dashboard`、`/operations`、`/credentials`、`/workflow` 子路径导入,并用 `React.lazy` 声明业务路由。个人中心领域区块通过 `definePersonalCenterSections` 显式装配;Workflow 代理只从 `/workflow/personal-center` 导入。官方模板的构建门禁要求首页、数据页、运维页、凭据页和流程页保持独立异步 chunk,同时限制单 chunk 与首屏 JavaScript 预算,并用 Core-only 构建证明无 Workflow 应用不会携带代理代码。
|
|
41
|
+
- 工作流页面按后端返回的 actions、fieldPolicy 和 presentation 渲染按钮与字段。`WorkflowSubmissionPage` 并列展示业务表单与审批预览,应用只实现 `saveBusiness`:用 Data API 或 NestJS App API 保存后返回 `businessKey + dataRef(revision) + facts`。提交页、任务页和实例页用可选 `workflowName` 展示业务名称,稳定 `workflowCode` 仍用于协议请求,不把内部 code 当作主要产品文案。流程详情使用可读的流程名称、当前环节和流程状态;`nodeId` 与实例/任务 version 继续服务于路由、并发和命令协议,不作为普通用户的主要信息。业务字段、主部门或审批人答案发生变化后,旧 preparation token 立即失效;业务保存与实例发起各自使用稳定幂等键,网络重试不会重复创建。标准流程详情页分离业务字段、流程摘要、当前操作、审批参与者和流转审计。业务记录由 Data API/App API 加载,`edit_required` 在操作前验证,变更以 revision 乐观锁先保存后执行流程命令;Workflow Kernel 不保存业务字段,业务状态的后续投影由 App API 或流程事件消费者负责。
|
|
42
|
+
- 流程工作中心使用统一 `PageHeader` 装配发起与刷新操作;应用只用 `workflowLabels` 提供业务流程名称和 `onOpen` 导航。Kernel 返回同一数据库快照中的 `total/limit/offset` 和确定性排序结果;Admin 只把页码转换为 offset。分类、页码或身份切换会使旧请求失效,错误在页面内重试,状态、时间、空态和“处理/查看”入口由 Admin 统一呈现,不做客户端搜索或第二套查询规则。
|
|
43
|
+
- 个人中心 Core 固定展示资料、环境、当前身份、数据范围、刷新和角色切换。领域区块复用路由的 `capability` / `access` 表达式并 fail closed;Workflow 模块可选贡献长期代理,选择代理人后必须再选择其具体应用角色绑定和数据范围。代理始终绑定发起人的当前稳定身份,不合并其他角色权限。
|
|
23
44
|
|
|
24
45
|
## 自定义操作
|
|
25
46
|
|
|
@@ -27,9 +48,9 @@
|
|
|
27
48
|
|
|
28
49
|
## 质量门禁
|
|
29
50
|
|
|
30
|
-
页面测试至少覆盖路由授权、角色切换、服务端拒绝、修订冲突和关键工作流动作。官方模板还用 Playwright 在真实 Chromium
|
|
51
|
+
页面测试至少覆盖路由授权、角色切换、服务端拒绝、修订冲突和关键工作流动作。官方模板还用 Playwright 在真实 Chromium 中验收桌面布局、移动端抽屉、权限裁剪导航、搜索/列设置/密度、个人中心、身份切换、数据编辑、静态标签恢复、动态标签不恢复以及身份初始化失败后的重试恢复。不要把后端规则复制成只能在浏览器生效的判断。
|
|
31
52
|
|
|
32
|
-
Admin
|
|
53
|
+
Admin 框架的持久缓存只保存界面偏好和逻辑路由,不缓存 Token、权限判定或业务数据。资源级 mutation epoch 仅存在于当前页面生命周期,用于让保活页面重新读取服务端事实。页面可以通过 `persistPreferences={false}` 完全关闭列表偏好的读取与写入。
|
|
33
54
|
|
|
34
55
|
## 发布前本地验收
|
|
35
56
|
|