@oj-module/cli 0.2.2 → 0.2.6

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.
Files changed (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +189 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +1 -1
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +62 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
@@ -0,0 +1,228 @@
1
+ import type { FetchInitFace } from "../_shared/site-facade";
2
+ // 深检后端四项(P2-7,C2-42)+ SSRF 防护钉(R9 纪律:全程不触 exec,迁移账本走 db.query + journalHeadOf)。
3
+ // 职责(ISP):纯 diff 函数(diffPermRows/diffSettingRows/diffLedger/diffWebIntegrity)与 I/O 探针
4
+ // (verifyRouteReachability)分离;端点只编排取参 + 调四项 + 组装 checks(报告结构/persist/trust 项归 P2-8)。
5
+ // 设值永不进 checks(subject 只报 key/path/code;secret 同只报 key)——「看得见但不知怎么做」即不合格。
6
+ import { fsFace } from "./atomic";
7
+ import { createStateStore } from "./state";
8
+
9
+ export { sha384B64 } from "./sha384";
10
+
11
+ /** 深检项(契约 §2.13 item 结构) */
12
+ export interface VerifyItem {
13
+ id: string
14
+ severity: "error" | "warn" | "info"
15
+ subject: string
16
+ advise_key: string
17
+ }
18
+
19
+ /** 线格式路由行(与 solutions/api.ts 的 SolutionRouteRow 同构,避免循环依赖在此定义) */
20
+ export interface SolutionRouteRow {
21
+ path: string
22
+ module: string
23
+ category: "main" | "admin" | "hidden"
24
+ order: number
25
+ title_key: string | null
26
+ required_permission: string | null
27
+ enabled: boolean
28
+ }
29
+
30
+ function isObj(v: unknown): v is Record<string, unknown> {
31
+ return typeof v === "object" && v !== null && !Array.isArray(v);
32
+ }
33
+
34
+ /**
35
+ * SSRF 钉①:探测 URL 永不出站——拒绝 scheme:// 与协议相对 //,要求站点相对路径(以单 / 起);
36
+ * 绝对化用请求源(siteOrigin)补全,确保探针永不指向本站之外的主机。
37
+ */
38
+ export function probeUrlOf(routePath: string, origin: { proto: string, host: string }): string {
39
+ if (/:\/\//.test(routePath) || routePath.startsWith("//"))
40
+ throw Object.assign(new Error(`路由探测拒绝出站/协议相对 URL(SSRF 防护):${routePath}`), { status: 400 });
41
+ if (!routePath.startsWith("/"))
42
+ throw Object.assign(new Error(`路由探测路径须为站点相对路径(以 / 开头,SSRF 防护):${routePath}`), { status: 400 });
43
+ return `${origin.proto}://${origin.host}${routePath}`;
44
+ }
45
+
46
+ /** SSRF 钉③:探针不跟随重定向(手动处理,3xx 按异常对待) */
47
+ function noRedirectFetch(fetchFn: FetchInitFace): FetchInitFace {
48
+ return (url, init) => fetchFn(url, { ...init, redirect: "manual" } as Record<string, unknown>);
49
+ }
50
+
51
+ /**
52
+ * 路由可达性探针(I/O):逐路由探活;分类(契约 §2.13 + 计划 §3 P2-7):
53
+ * 2xx / 401(需登录仍可达)→ 不打项;
54
+ * 404 / 5xx → error(anomaly);
55
+ * 403 → 声明 `required_permission` = 未以目标身份验证、未校验(warn,非通过非失败);否则 error(被拦截)。
56
+ * 抛错(连不通)→ error(unreachable)。origin 为 null(非 HTTP 请求上下文)→ 跳过探针,打 info 项。
57
+ */
58
+ export async function verifyRouteReachability(
59
+ fetchFn: FetchInitFace,
60
+ routes: SolutionRouteRow[],
61
+ origin: { proto: string, host: string } | null,
62
+ ): Promise<VerifyItem[]> {
63
+ const items: VerifyItem[] = [];
64
+ if (origin === null) {
65
+ items.push({ id: "routes", severity: "info", subject: "*", advise_key: "lifecycle.verify.routes.skipped" });
66
+ return items;
67
+ }
68
+ const fetch = noRedirectFetch(fetchFn);
69
+ for (const r of routes) {
70
+ let status: number;
71
+ try {
72
+ const res = await fetch(probeUrlOf(r.path, origin), { headers: { "cache-control": "no-cache" } });
73
+ status = res.status;
74
+ }
75
+ catch (e) {
76
+ if (e instanceof Error && (e as { status?: number }).status != null)
77
+ throw e; // SSRF 拦截(400)原样上抛,端点收口为 400
78
+ items.push({ id: "routes", severity: "error", subject: r.path, advise_key: "lifecycle.verify.routes.unreachable" });
79
+ continue;
80
+ }
81
+ if (status === 404 || status >= 500) {
82
+ items.push({ id: "routes", severity: "error", subject: r.path, advise_key: "lifecycle.verify.routes.anomaly" });
83
+ }
84
+ else if (status >= 300 && status < 400) {
85
+ // 3xx 重定向:探针不跟随(SSRF 钉③,init.redirect="manual"),裸 3xx 即异常
86
+ items.push({ id: "routes", severity: "error", subject: r.path, advise_key: "lifecycle.verify.routes.redirect" });
87
+ }
88
+ else if (status === 403) {
89
+ if (r.required_permission != null)
90
+ items.push({ id: "routes", severity: "warn", subject: r.path, advise_key: "lifecycle.verify.routes.unauthenticated" });
91
+ else
92
+ items.push({ id: "routes", severity: "error", subject: r.path, advise_key: "lifecycle.verify.routes.blocked" });
93
+ }
94
+ // 2xx / 401 → 可达,不打项
95
+ }
96
+ return items;
97
+ }
98
+
99
+ /** RBAC 完整性(纯函数):路由声明权限点 ↔ sys_permission 实有;缺失即 error */
100
+ export function diffPermRows(declared: string[], actual: Set<string>): VerifyItem[] {
101
+ return declared
102
+ .filter(p => !actual.has(p))
103
+ .map(p => ({ id: "rbac", severity: "error" as const, subject: p, advise_key: "lifecycle.verify.rbac.missing_perm" }));
104
+ }
105
+
106
+ /** 设置完整性(纯函数):方案声明设置 key ↔ sys_setting 实有;缺失即 error(永不含值) */
107
+ export function diffSettingRows(declared: string[], actual: Set<string>): VerifyItem[] {
108
+ return declared
109
+ .filter(k => !actual.has(k))
110
+ .map(k => ({ id: "settings", severity: "error" as const, subject: k, advise_key: "lifecycle.verify.settings.missing_key" }));
111
+ }
112
+
113
+ /** 迁移账本(纯函数):声明迁移模块版本 ↔ `_oj_migrations` 实有 max(version);缺失/落后即 error */
114
+ export function diffLedger(declared: Array<{ module: string, version: number }>, applied: Map<string, number | null | undefined>): VerifyItem[] {
115
+ const items: VerifyItem[] = [];
116
+ for (const d of declared) {
117
+ const got = applied.get(d.module);
118
+ if (got == null)
119
+ items.push({ id: "ledger", severity: "error", subject: d.module, advise_key: "lifecycle.verify.ledger.not_applied" });
120
+ else if (got < d.version)
121
+ items.push({ id: "ledger", severity: "error", subject: d.module, advise_key: "lifecycle.verify.ledger.stale" });
122
+ }
123
+ return items;
124
+ }
125
+
126
+ /** manifest 声明设置 key 提取(不报值);groups[].items[].key 摊平 */
127
+ export function declaredSettingsOf(manifest: unknown): string[] {
128
+ const groups = isObj(manifest) ? manifest.settings : undefined;
129
+ if (!isObj(groups) || !Array.isArray(groups.groups))
130
+ return [];
131
+ const keys: string[] = [];
132
+ for (const g of groups.groups) {
133
+ if (!isObj(g) || !Array.isArray(g.items))
134
+ continue;
135
+ for (const it of g.items) {
136
+ if (isObj(it) && typeof it.key === "string")
137
+ keys.push(it.key);
138
+ }
139
+ }
140
+ return keys;
141
+ }
142
+
143
+ /** 迁移文件名 → 声明最高版本(module 由调用方给定;缺迁移文件返回空) */
144
+ export function declaredMigrationsOf(files: string[], module: string): Array<{ module: string, version: number }> {
145
+ const versions: number[] = [];
146
+ for (const f of files) {
147
+ const m = /^(\d+)__/.exec(f);
148
+ if (m)
149
+ versions.push(Number(m[1]));
150
+ }
151
+ if (versions.length === 0)
152
+ return [];
153
+ return [{ module, version: Math.max(...versions) }];
154
+ }
155
+
156
+ /** 读取已装方案 manifest(state 取版本 → 版本目录读盘);不可得返回 null(端点据 404 降级) */
157
+ export async function readSolutionManifest(env: SolutionsEnv, name: string): Promise<{ version: string, manifest: unknown } | null> {
158
+ const state = await createStateStore(env.statePath);
159
+ const entry = state.read().solutions[name];
160
+ if (!entry)
161
+ return null;
162
+ const version = entry.version;
163
+ if (typeof version !== "string")
164
+ return null;
165
+ try {
166
+ const yaml = (globalThis as { yaml?: { parse: (t: string) => Promise<unknown> } }).yaml;
167
+ if (!yaml?.parse)
168
+ return null;
169
+ const bytes = await fsFace().readFile(`${env.sharedRoot}/${name}-${version}/manifest.yaml`);
170
+ return { version, manifest: await yaml.parse(new TextDecoder().decode(bytes)) };
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /** web 入口条目(modules.json)形态(与 deployWeb 落地形状对齐;此处仅取深检所需字段) */
178
+ export interface ModuleEntryIntegrity {
179
+ name: string
180
+ entry?: string
181
+ integrity?: string
182
+ }
183
+
184
+ /**
185
+ * 深检 web 入口 integrity(P4-2,P3-10#2 收口):声明 entry 的 sha384(本地 dist 资产)↔
186
+ * modules.json 条目 integrity 比对;不符 = error(被篡改/损坏),缺失 integrity 声明 = info
187
+ * (未声明,非阻断),资产不存在 = info(web 入口缺失,非阻断)。SSRF 钉:只比对本地 dist
188
+ * 资产,绝不经 fetch 出站(与路由可达性探针严格隔离——复用 native 资产校验,不引入外联面)。
189
+ */
190
+ export async function diffWebIntegrity(
191
+ entries: ModuleEntryIntegrity[],
192
+ distRoot: string,
193
+ sha384: (data: Uint8Array) => string,
194
+ ): Promise<VerifyItem[]> {
195
+ const items: VerifyItem[] = [];
196
+ const me = entries.find(e => e.name != null && typeof e.entry === "string" && e.entry !== "");
197
+ if (me === undefined) {
198
+ items.push({ id: "web", severity: "info", subject: "*", advise_key: "lifecycle.verify.web.no_entry" });
199
+ return items;
200
+ }
201
+ if (typeof me.integrity !== "string" || !me.integrity.startsWith("sha384-")) {
202
+ items.push({ id: "web", severity: "info", subject: me.name ?? "*", advise_key: "lifecycle.verify.web.no_integrity" });
203
+ return items;
204
+ }
205
+ try {
206
+ const bytes = await fsFace().readFile(`${distRoot}/${me.entry}`);
207
+ const actual = `sha384-${sha384(new Uint8Array(bytes))}`;
208
+ if (actual !== me.integrity) {
209
+ items.push({ id: "web", severity: "error", subject: me.entry, advise_key: "lifecycle.verify.web.integrity_mismatch" });
210
+ }
211
+ }
212
+ catch {
213
+ // 资产缺失(web 入口未落位或路径变更)——info 非阻断(路由可达性已另测)
214
+ items.push({ id: "web", severity: "info", subject: me.entry, advise_key: "lifecycle.verify.web.asset_missing" });
215
+ }
216
+ return items;
217
+ }
218
+
219
+ /** 站点源(深检端点依赖;sharedRoot/statePath 为读面,fetch 可注入) */
220
+ export interface SolutionsEnv {
221
+ sharedRoot: string
222
+ statePath: string
223
+ modulesJsonPath?: string
224
+ apiPrefix?: string
225
+ /** 深检报告留存目录(默认随 statePath 同目录 verify-reports;P2-8) */
226
+ verifyReportsDir?: string
227
+ fetch?: FetchInitFace
228
+ }
@@ -0,0 +1,92 @@
1
+ import type { VerifyItem } from "./verify-checks";
2
+ // 深检报告结构 + 时间序留存(P2-8,C2-42):VerifyReport 定义、verify-reports 落盘、N=10 留存、
3
+ // 按 name 取最新。i18n 项本仓只下发待查 key 清单(advise),前端经 oj-market 解析;缺执行降级
4
+ // 「未校验」不阻断(不在此处解析文案)——编排见 solutions/api.ts。
5
+ // 设值永不进报告(checks 只报 id/path/code/key;secret 同只报 key)——与 verify-checks 同纪律。
6
+ import { atomicWriteText, fsFace } from "./atomic";
7
+
8
+ /** 深检报告(契约 §2.13:{ok, checks[], advise[]}) */
9
+ export interface VerifyReport {
10
+ /** 是否存在 error 级检查项(无 error = 整体通过;warn/info 不阻断) */
11
+ ok: boolean
12
+ /** 检查项(routes/rbac/settings/ledger/trust/i18n)——每项 id/severity/subject/advise_key */
13
+ checks: VerifyItem[]
14
+ /** 待查 i18n key 清单(去重排序):前端经 oj-market 解析文案;缺执行降级「未校验」不阻断 */
15
+ advise: string[]
16
+ }
17
+
18
+ /** 落盘文件形态(文件名=createdAt.json,便于时间序排序与按 name 检索) */
19
+ export interface VerifyReportFile {
20
+ createdAt: number
21
+ name: string
22
+ report: VerifyReport
23
+ }
24
+
25
+ // 报告留存条数(超出删最旧;与 tasks.ts TASK_KEEP 同口径「时间序保留 + 触发点」)
26
+ export const VERIFY_REPORTS_KEEP = 10;
27
+
28
+ /** 报告目录(默认随 statePath 同目录 verify-reports;与 .solutions 同根) */
29
+ export function verifyReportsDirOf(env: { statePath: string, verifyReportsDir?: string }): string {
30
+ if (env.verifyReportsDir !== undefined)
31
+ return env.verifyReportsDir;
32
+ const slash = env.statePath.lastIndexOf("/");
33
+ const base = slash === -1 ? "" : env.statePath.slice(0, slash);
34
+ return base === "" ? "verify-reports" : `${base}/verify-reports`;
35
+ }
36
+
37
+ /** 文件名(纯数字 .json)判定,避免误吞非报告文件 */
38
+ function isReportName(n: string): boolean {
39
+ return /^\d+\.json$/.test(n);
40
+ }
41
+
42
+ /** 持久化一份报告 + 触发留存 GC(保留最近 N=10,删最旧)。单条清理失败不影响主流程。 */
43
+ export async function persistReport(dir: string, name: string, report: VerifyReport, now: number = Date.now()): Promise<void> {
44
+ const file: VerifyReportFile = { createdAt: now, name, report };
45
+ await atomicWriteText(`${dir}/${now}.json`, `${JSON.stringify(file)}\n`);
46
+ // GC:按文件名数值序(=createdAt 序)保留最近 N=10,删最旧(第 11 份挤掉第 1 份)
47
+ let names: string[];
48
+ try {
49
+ names = await fsFace().readdir(dir);
50
+ }
51
+ catch {
52
+ return; // 目录尚不存在/不可读:本次写入已落盘,GC 下次收敛
53
+ }
54
+ const sorted = names.filter(isReportName).sort((a, b) => Number(a.slice(0, -5)) - Number(b.slice(0, -5)));
55
+ const stale = sorted.slice(0, Math.max(0, sorted.length - VERIFY_REPORTS_KEEP));
56
+ for (const n of stale) {
57
+ try {
58
+ await fsFace().remove(`${dir}/${n}`);
59
+ }
60
+ catch {
61
+ // 单条清理失败不影响主流程
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 取某方案最新报告(按 createdAt 最大);无/目录缺失/全部损坏 → null(不炸,端点据 404 降级)。
68
+ * 损坏文件跳过(可观测性优先,不为坏文件阻塞读取)。
69
+ */
70
+ export async function getLastReport(dir: string, name: string): Promise<VerifyReport | null> {
71
+ let names: string[];
72
+ try {
73
+ names = await fsFace().readdir(dir);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ let best: VerifyReportFile | null = null;
79
+ for (const n of names.filter(isReportName)) {
80
+ try {
81
+ const f = JSON.parse(new TextDecoder().decode(await fsFace().readFile(`${dir}/${n}`))) as VerifyReportFile;
82
+ if (f.name !== name)
83
+ continue;
84
+ if (best === null || f.createdAt > best.createdAt)
85
+ best = f;
86
+ }
87
+ catch {
88
+ // 损坏跳过
89
+ }
90
+ }
91
+ return best?.report ?? null;
92
+ }
@@ -64,8 +64,8 @@ function deny(): never {
64
64
  throw Object.assign(new Error("forbidden"), { status: 403 });
65
65
  }
66
66
 
67
- /** 当前请求的访问快照;未认证/禁用/改密悬挂即拒(403 不泄露账号存在性)。 */
68
- async function currentAccess(): Promise<AccessSnapshot> {
67
+ /** 当前请求的访问快照;未认证/禁用/改密悬挂即拒(403 不泄露账号存在性)。导出供各 handler 作「登录态即放行」判据(如操作日志写端)。 */
68
+ export async function currentAccess(): Promise<AccessSnapshot> {
69
69
  const uid = http.user?.id;
70
70
  if (uid == null)
71
71
  deny();
@@ -11,6 +11,8 @@ export interface SettingItem {
11
11
  value: string | null
12
12
  default_value: string | null
13
13
  label_key: string | null
14
+ /** 组标题 key(C2-22 组级 label_key 落库值,P2-15③;同组各行一致,组面取首个非空) */
15
+ group_label_key: string | null
14
16
  options: string | null
15
17
  secret: boolean
16
18
  update_time: number
@@ -18,6 +20,8 @@ export interface SettingItem {
18
20
 
19
21
  export interface SettingGroup {
20
22
  group: string
23
+ /** 组标题 key(组面收敛:同组任一行非空即用——P2-15③ DB 组标题回填消费面) */
24
+ label_key: string | null
21
25
  items: SettingItem[]
22
26
  }
23
27
 
@@ -36,13 +40,18 @@ export async function listSettings(): Promise<SettingGroup[]> {
36
40
  value: Number(r.secret) === 1 ? "***" : (r.value == null ? null : String(r.value)),
37
41
  default_value: r.default_value == null ? null : String(r.default_value),
38
42
  label_key: r.label_key == null ? null : String(r.label_key),
43
+ group_label_key: r.group_label_key == null ? null : String(r.group_label_key),
39
44
  options: r.options == null ? null : String(r.options),
40
45
  secret: Number(r.secret) === 1,
41
46
  update_time: Number(r.update_time),
42
47
  };
43
48
  (groups.get(item.group) ?? groups.set(item.group, []).get(item.group))!.push(item);
44
49
  }
45
- return [...groups.entries()].map(([group, items]) => ({ group, items }));
50
+ return [...groups.entries()].map(([group, items]) => ({
51
+ group,
52
+ label_key: items.find(x => x.group_label_key !== null)?.group_label_key ?? null,
53
+ items,
54
+ }));
46
55
  }
47
56
 
48
57
  export async function updateSetting(actorId: number, key: string, value: string): Promise<void> {
@@ -73,22 +73,29 @@ export async function upsertSolutionPerms(m: SolutionManifest): Promise<UpsertRe
73
73
  return { deprecated };
74
74
  }
75
75
 
76
+ /** 事务/非事务统一的 db 面(P3-3:可传入 db.tx 的事务句柄以纳入单一事务) */
77
+ interface DbLike {
78
+ query: (sql: string, params?: unknown[]) => Promise<unknown>
79
+ exec: (sql: string, params?: unknown[]) => Promise<unknown>
80
+ }
81
+
76
82
  /**
77
83
  * 卸载级联清理(AC2-6,唯一减法出口)——双维度:
78
84
  * ① 该 module 的角色行 + 这些角色的全部绑定 + 用户成员资格;
79
85
  * ② permission_code 属该 module 的绑定(含 custom 角色上的)+ 权限点行——
80
86
  * 漏维度②会让 custom 角色残留绑定在重装时静默复活旧授权。custom 角色本体保留(空绑定)。
81
87
  * AC2-7:业务表不属本出口,运维手工处理(Q4)。
88
+ * P3-3:可传入事务句柄 `d`(db.tx 的 t),使级联纳入卸载的单一 DB 事务;缺省回落全局 db(兼容既有调用面)。
82
89
  */
83
- export async function uninstallSolution(module: string): Promise<void> {
84
- const roles = await db.query("select name from sys_role where module = ?", [module]) as Array<{ name: string }>;
90
+ export async function uninstallSolution(module: string, d: DbLike = db): Promise<void> {
91
+ const roles = await d.query("select name from sys_role where module = ?", [module]) as Array<{ name: string }>;
85
92
  for (const r of roles) {
86
- await db.exec("delete from sys_role_permission where role_name = ?", [r.name]);
87
- await db.exec("delete from sys_user_role where role_name = ?", [r.name]);
93
+ await d.exec("delete from sys_role_permission where role_name = ?", [r.name]);
94
+ await d.exec("delete from sys_user_role where role_name = ?", [r.name]);
88
95
  }
89
- await db.exec("delete from sys_role where module = ?", [module]);
96
+ await d.exec("delete from sys_role where module = ?", [module]);
90
97
  // module 名是 slug([a-z0-9_-]),LIKE 前缀匹配的转义纪律同 query.ts(ESCAPE '!' 三方言同义)
91
98
  const prefix = `${module.replace(/[!%_]/g, c => `!${c}`)}.%`;
92
- await db.exec("delete from sys_role_permission where permission_code like ? escape '!'", [prefix]);
93
- await db.exec("delete from sys_permission where module = ?", [module]);
99
+ await d.exec("delete from sys_role_permission where permission_code like ? escape '!'", [prefix]);
100
+ await d.exec("delete from sys_permission where module = ?", [module]);
94
101
  }
@@ -0,0 +1,44 @@
1
+ // 站点编排面共享真源(P2-16 真机构建钉出:oj build 禁「模块内 import api.ts」——
2
+ // market/api.ts 是路由入口,verify-checks/solutions 复用的非路由原语必须落本模块)。
3
+ // 单一真源:market/api.ts re-export 保持既有引用面;深检/方案域直接 import 本模块。
4
+
5
+ import type { FetchFace } from "../_lifecycle/steps/download";
6
+
7
+ /** 带缓存指令的拉取面(FetchFace 的超集:oj 全局 fetch/测试 stub 天然满足,少参函数可赋多参形) */
8
+ export type FetchInitFace = (url: string, init?: { headers?: Record<string, string>, redirect?: string }) => Promise<{
9
+ ok: boolean
10
+ status: number
11
+ arrayBuffer: () => Promise<ArrayBuffer>
12
+ }>;
13
+
14
+ /** 站点源(SSRF 钉②:x-forwarded-proto 取首个、去空白)。非请求上下文(CLI/测试无 Host)→ null */
15
+ export function siteOrigin(): { proto: string, host: string } | null {
16
+ const h = (http as { headers?: Record<string, string | undefined> }).headers ?? {};
17
+ const host = h.host;
18
+ if (!host) {
19
+ return null;
20
+ }
21
+ const proto = String(h["x-forwarded-proto"] ?? "http").split(",")[0]!.trim() || "http";
22
+ return { proto, host };
23
+ }
24
+
25
+ /** 站点源补全(深检探针复用 siteOrigin;相对 URL → 绝对同源 URL) */
26
+ export function withSiteOrigin(fetch: FetchInitFace): FetchFace {
27
+ const o = siteOrigin();
28
+ if (o === null) {
29
+ return fetch as FetchFace;
30
+ }
31
+ return url => fetch(/^https?:\/\//i.test(url) ? url : `${o.proto}://${o.host}${url}`) as ReturnType<FetchFace>;
32
+ }
33
+
34
+ /** 站点迁移账本 head(oj journal 真源 `_oj_migrations`;表缺失/查询失败 → null = 跳过禁降版判定,不猜) */
35
+ export async function journalHeadOf(name: string): Promise<number | bigint | null> {
36
+ try {
37
+ const rows = await db.query("select max(version) as head from _oj_migrations where module = ?", [name]) as Array<{ head: unknown }>;
38
+ const head = rows[0]?.head;
39
+ return head == null ? null : Number(head);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
@@ -11,3 +11,4 @@ tables:
11
11
  - sys_role_permission
12
12
  - sys_user_role
13
13
  - sys_refresh_token
14
+ - market_history