@oj-module/cli 0.2.1 → 0.2.5

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 +147 -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 +46 -7
  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 +56 -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 +162 -0
  223. package/templates/web/src/system/__client/api.ts +382 -0
  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,207 @@
1
+ // 校验步骤(P1-6 sha256 fail-closed C2-29 + P2-3 Ed25519 验签,十阶段 stage 3 verifying)。
2
+ // 签名载体两形态同源(契约 v1.3 §2.9/§2.11):CDN 源 = Form B manifest.archive.signature;
3
+ // file 源 = detached 侧车 .sig(market install 端点上传时点注入同一结构)。
4
+ // 验签硬边界(G4):缺签名/验签不过/格式错一律拒装零落盘,无 solution_allow_unsigned 类开关。
5
+ // 结构校验序(全部前置,逐条短路):alg → key_id 命名空间 → public_key 64hex → value 标准 base64
6
+ // → 规范串 → 信任库比对 → verifyEd25519。未知 key_id → needsTrust(第五终态,staging 不清场)。
7
+ // 注入面(DIP):sha256Hex / verifyEd25519 / trustPath——原语不可得即拒装,同 sha256Hex 既有口径
8
+ // (oj 运行时私有 crypto 面;Node 测试侧 WebCrypto 无此面,两面皆缺 fail-closed 不静默跳过)。
9
+
10
+ import type { SigCode } from "../types";
11
+ import type { InstallContext } from "./preflight";
12
+ import { fsFace } from "../atomic";
13
+ import { createTrustStore, isValidKeyId } from "../trust";
14
+ import { archiveFileOf } from "./download";
15
+
16
+ export type Sha256Face = (data: Uint8Array) => string;
17
+ /** Ed25519 验签面(上游 crypto.verifyEd25519 同签名:hex/base64 32B 公钥 + UTF-8 msg + 标准 base64 签名) */
18
+ export type VerifyEd25519Face = (publicKey: string, msg: string, sigBase64: string) => boolean;
19
+
20
+ export interface VerifyContext extends InstallContext {
21
+ /** 暂存目录(与 download 同目录接力) */
22
+ staging: string
23
+ /** 注入式摘要面;缺省回落 globalThis.crypto.sha256Hex */
24
+ sha256Hex?: Sha256Face
25
+ /** 宿主信任库路径(.solutions/trusted-keys.yaml;缺省 = 验签能力未配置,fail-closed 拒装) */
26
+ trustPath?: string
27
+ /** Ed25519 验签面;缺省回落 globalThis.crypto.verifyEd25519 */
28
+ verifyEd25519?: VerifyEd25519Face
29
+ }
30
+
31
+ export interface StageResult {
32
+ stage: "verifying"
33
+ ok: boolean
34
+ message: string
35
+ retryable?: boolean
36
+ code?: SigCode
37
+ needsTrust?: boolean
38
+ trust?: { key_id: string, public_key: string, fingerprint: string }
39
+ }
40
+
41
+ const HEX64 = /^[0-9a-f]{64}$/;
42
+ /** 标准 base64(含 -/_ 即 base64url,一律 sig_format,禁归一) */
43
+ const STD_B64 = /^[A-Z0-9+/]+={0,2}$/i;
44
+ /** 规范串版本标记(契约 §2.11;不认识即 sig_format) */
45
+ const CANON = "v1";
46
+
47
+ interface Signature {
48
+ alg: unknown
49
+ key_id: unknown
50
+ public_key: unknown
51
+ value: unknown
52
+ canon?: unknown
53
+ }
54
+
55
+ /** 签名失败结果统一出口(六码:retryable 恒 false、零解压零落盘;unknown_key 附 needsTrust) */
56
+ function sigFail(code: SigCode, message: string, trust?: StageResult["trust"]): StageResult {
57
+ return { stage: "verifying", ok: false, retryable: false, code, message, ...(trust !== undefined ? { needsTrust: true, trust } : {}) };
58
+ }
59
+
60
+ function sha256HexFaceOf(ctx: VerifyContext): Sha256Face | undefined {
61
+ return ctx.sha256Hex ?? (globalThis as { crypto?: { sha256Hex?: Sha256Face } }).crypto?.sha256Hex;
62
+ }
63
+
64
+ function verifyEd25519FaceOf(ctx: VerifyContext): VerifyEd25519Face | undefined {
65
+ return ctx.verifyEd25519 ?? (globalThis as { crypto?: { verifyEd25519?: VerifyEd25519Face } }).crypto?.verifyEd25519;
66
+ }
67
+
68
+ /** 指纹 = sha256Hex(publicKey)(契约 §2.11;复用注入摘要面,面不可得 → null 仅展示降级) */
69
+ function fingerprintOf(shaFace: Sha256Face, publicKeyHex: string): string | null {
70
+ try {
71
+ const raw = new Uint8Array(32);
72
+ for (let i = 0; i < 32; i++)
73
+ raw[i] = Number.parseInt(publicKeyHex.slice(i * 2, i * 2 + 2), 16);
74
+ return shaFace(raw);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ export async function verify(ctx: VerifyContext): Promise<StageResult> {
82
+ const arch = archiveFileOf(ctx.manifest);
83
+ if (!arch.ok) {
84
+ return { stage: "verifying", ok: false, retryable: false, message: `校验失败:${arch.message}` };
85
+ }
86
+ const expected = arch.archive.sha256;
87
+ if (typeof expected !== "string" || expected === "") {
88
+ return {
89
+ stage: "verifying",
90
+ ok: false,
91
+ retryable: false,
92
+ message: "校验失败:archive.sha256 缺失(C2-29 fail-closed:Form A 占位无摘要,安装清单须为 Form B CDN 聚合真源)",
93
+ };
94
+ }
95
+ const dest = `${ctx.staging}/${arch.file}`;
96
+ const shaFace = sha256HexFaceOf(ctx);
97
+ if (typeof shaFace !== "function") {
98
+ return {
99
+ stage: "verifying",
100
+ ok: false,
101
+ retryable: false,
102
+ message: "校验失败:摘要能力不可用(sha256Hex 面未注入且运行时全局缺失)——C2-29 fail-closed 拒装,不静默跳过",
103
+ };
104
+ }
105
+ let bytes: Uint8Array;
106
+ try {
107
+ bytes = await fsFace().readFile(dest);
108
+ }
109
+ catch (e) {
110
+ return { stage: "verifying", ok: false, retryable: true, message: `校验失败:无法读取 ${dest}(${String(e)})——附录 A verifying:请重试下载` };
111
+ }
112
+ const actual = shaFace(bytes);
113
+ if (actual !== expected) {
114
+ return {
115
+ stage: "verifying",
116
+ ok: false,
117
+ retryable: true,
118
+ message: `校验失败:${arch.file} 摘要与索引不符(C2-29 fail-closed;附录 A verifying:分发包可能损坏,请重试下载)期望 ${expected},实得 ${actual}`,
119
+ };
120
+ }
121
+ // P2-6 同版本重装摘要比对(D11③ 族):sidecar 记录过本版本的安装摘要——重取包不同 =
122
+ // 版本内容被替换(CDN 同名版本换包即拒;发布方应升版本号)。升级(新版本)无此记录不比对。
123
+ const prev = ctx.state.read().solutions[ctx.name];
124
+ if (prev !== undefined && prev.version === ctx.version && prev.archive !== undefined && prev.archive.sha256 !== expected) {
125
+ return {
126
+ stage: "verifying",
127
+ ok: false,
128
+ retryable: false,
129
+ code: "tampered",
130
+ message: `校验失败:重取包摘要与已装记录不符(tampered)——同版本 ${ctx.name}@${ctx.version} 的包内容被替换,拒装。已装记录 ${prev.archive.sha256},本次重取 ${expected};请发布方升版本号后重试`,
131
+ };
132
+ }
133
+ // ── P2-3 验签(sha256 通过后;规范串基于已验字节,R10 语义)────────────────────────
134
+ return verifySignature(ctx, bytes, actual, shaFace);
135
+ }
136
+
137
+ /** 验签主体(sha256 已过):结构校验序 → 信任库 → verifyEd25519;全拒装路径零解压零落盘 */
138
+ async function verifySignature(ctx: VerifyContext, bytes: Uint8Array, sha256: string, shaFace: Sha256Face): Promise<StageResult> {
139
+ const raw = (ctx.manifest as { archive?: { signature?: unknown } } | null)?.archive?.signature;
140
+ if (raw === undefined || raw === null) {
141
+ return sigFail("sig_missing", "校验失败:分发包未签名(sig_missing;G4 硬 fail-closed——Form B 缺 signature 或 file 源缺同目录 .sig 侧车,无逃生门)。指引:请向发布方索取签名包,或见 devkit「签名与信任」");
142
+ }
143
+ if (typeof raw !== "object" || Array.isArray(raw)) {
144
+ return sigFail("sig_format", `校验失败:signature 结构非法(sig_format;应为 {alg,key_id,public_key,value,canon} 对象),实为 ${Array.isArray(raw) ? "array" : typeof raw}`);
145
+ }
146
+ const sig = raw as Signature;
147
+ // ① alg 恒 "Ed25519"(大小写敏感)
148
+ if (sig.alg !== "Ed25519") {
149
+ return sigFail("sig_alg", `校验失败:签名算法只支持 Ed25519(sig_alg),实为「${String(sig.alg)}」`);
150
+ }
151
+ // ② key_id 命名空间 <publisher>.<id>(C2-37:禁裸通用名,防抢注/碰撞)
152
+ if (typeof sig.key_id !== "string" || !isValidKeyId(sig.key_id)) {
153
+ return sigFail("sig_key", `校验失败:key_id 命名空间非法(sig_key;C2-37:<publisher>.<id> 全小写、禁裸通用名单段),实为「${String(sig.key_id)}」`);
154
+ }
155
+ // ③ public_key 严格 64hex(32B raw;禁 PEM/SPKI——上游 decode_key 不认)
156
+ if (typeof sig.public_key !== "string" || !HEX64.test(sig.public_key)) {
157
+ return sigFail("sig_format", `校验失败:public_key 必须是 64 hex(Ed25519 32B raw,sig_format;禁 PEM/SPKI),实为「${String(sig.public_key).slice(0, 24)}」`);
158
+ }
159
+ // ④ value 严格标准 base64(含 -/_ 即 base64url → sig_format,禁归一)
160
+ if (typeof sig.value !== "string" || !STD_B64.test(sig.value)) {
161
+ return sigFail("sig_format", "校验失败:签名 value 必须是标准 base64(sig_format;base64url 含 -/_ 不受理,不做归一)");
162
+ }
163
+ // ⑤ canon 版本标记
164
+ if (sig.canon !== undefined && sig.canon !== CANON) {
165
+ return sigFail("sig_format", `校验失败:规范串版本标记不认识(sig_format;本机支持 ${CANON}),实为「${String(sig.canon)}」`);
166
+ }
167
+ // ⑥ 验签原语可得性(与 sha256Hex 同口径:不可得 = 拒装,不静默跳过)
168
+ const face = verifyEd25519FaceOf(ctx);
169
+ if (typeof face !== "function") {
170
+ return sigFail("sig_format", "校验失败:验签能力不可用(verifyEd25519 面未注入且运行时全局缺失)——G4 fail-closed 拒装,不静默跳过");
171
+ }
172
+ // ⑦ 信任库比对(TOFU 三态)
173
+ if (ctx.trustPath === undefined || ctx.trustPath === "") {
174
+ return sigFail("sig_missing", "校验失败:宿主未配置信任库路径(trustPath 缺失)——验签能力未就绪,fail-closed 拒装");
175
+ }
176
+ const trustedKey = await createTrustStore(ctx.trustPath).get(sig.key_id);
177
+ const fingerprint = fingerprintOf(shaFace, sig.public_key);
178
+ if (trustedKey === null) {
179
+ // 未知 key_id:停 needs_trust(staging 不清场,确认后重试)
180
+ return sigFail(
181
+ "unknown_key",
182
+ `待信任确认:发布方「${sig.key_id}」不在宿主信任库(unknown_key)。请核对指纹后执行 ojm trust confirm --task <task_id>(或市场页确认),确认入库即可重试安装`,
183
+ { key_id: sig.key_id, public_key: sig.public_key, fingerprint: fingerprint ?? "" },
184
+ );
185
+ }
186
+ if (trustedKey !== sig.public_key) {
187
+ return sigFail("sig_key", `校验失败:key_id「${sig.key_id}」的包内公钥与信任库绑定不一致(sig_key;首次绑定不可改——疑抢注/换钥,请发布方轮换为新 key_id)`);
188
+ }
189
+ // ⑧ 规范串 + 验签(双层语义:抛错 = sig_format,false = tampered,同拒装但分码)
190
+ const canonical = `${ctx.name}@${ctx.version}:${sha256}`;
191
+ let verified: boolean;
192
+ try {
193
+ verified = face(sig.public_key, canonical, sig.value);
194
+ }
195
+ catch (e) {
196
+ // 上游格式错(verifyEd25519: 起头)在 isolate 内转 sig_format——抛错不得逃逸成阶段异常
197
+ return sigFail("sig_format", `校验失败:验签输入格式错(sig_format):${String(e)}`);
198
+ }
199
+ if (!verified) {
200
+ return sigFail("tampered", "校验失败:签名与包体不符(tampered;签名不符不落盘——请重新从可信源获取分发包与签名)");
201
+ }
202
+ return {
203
+ stage: "verifying",
204
+ ok: true,
205
+ message: `校验通过:sha256 与索引一致(C2-29)+ Ed25519 验签通过(发布方 ${sig.key_id},指纹 ${fingerprint ?? "n/a"},G4 已启用)`,
206
+ };
207
+ }
@@ -0,0 +1,119 @@
1
+ // 任务注册表(P1-10,R13/B9/R31):.install-tasks/<id>.json 任务状态文件——oj 进程内单写者
2
+ // (安装器),800ms 轮询消费面(市场页/CLI)。治理三件套:保留最近 10 条(超出删最旧,按
3
+ // startedAt 序);running 超 30min 无进展判 failed(启动/对账时点 sweep——持有者崩溃自愈);
4
+ // message 落盘前脱敏(URL query / Bearer / JWT / token= 形态;sha256 诊断摘要不脱——verify
5
+ // 失败的期望/实得摘要非秘密,脱掉即失去可诊断性)。写入一律经 atomicWriteText(D13 同款)。
6
+
7
+ import type { TaskRecord } from "./types";
8
+ import { atomicWriteText, fsFace } from "./atomic";
9
+
10
+ /** 任务文件保留条数(超出删最旧) */
11
+ export const TASK_KEEP = 10;
12
+
13
+ /** 当前任务记录 schema 版本(P2-10):写方盖章、读方回填;升版纪律见 TaskRecord.schemaVersion 注释 */
14
+ export const TASK_SCHEMA_VERSION = 1;
15
+
16
+ /** 旧记录兼容:无 schemaVersion 的记录回填 1(读方不炸;更高版本原样透传,由消费面降级提示) */
17
+ function normalizeTask(record: TaskRecord): TaskRecord {
18
+ return record.schemaVersion === undefined ? { ...record, schemaVersion: TASK_SCHEMA_VERSION } : record;
19
+ }
20
+
21
+ /** running 超时阈值:>30min 无进展判 failed(与 .lock stale 阈值同款口径) */
22
+ export const TASK_TIMEOUT_MS = 30 * 60_000;
23
+
24
+ /**
25
+ * message 脱敏(落盘前):URL query 整段、Bearer 授权头、JWT 三段形态、token/secret/password
26
+ * 等敏感参数赋值——四类 token 形态字符串。sha256/sha384 摘要不脱(公开产物的诊断摘要非秘密)。
27
+ */
28
+ export function redact(message: string): string {
29
+ return message
30
+ .replace(/\?[^"'\s)\]}]*/g, "?…")
31
+ .replace(/Bearer\s+\S+/gi, "Bearer …")
32
+ // PEM 私钥块(-----BEGIN ... PRIVATE KEY-----…-----END … PRIVATE KEY-----)整块脱敏
33
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/g, "-----BEGIN PRIVATE KEY-----…-----END PRIVATE KEY-----")
34
+ // JSON 中的 private_key / privateKey 字段值
35
+ .replace(/"private_?key"\s*:\s*"[^"]*"/gi, "\"private_key\":\"…\"")
36
+ // JWT(eyJ... 三段)
37
+ .replace(/eyJ[\w-]+(?:\.[\w-]+)+/g, "…")
38
+ .replace(/\b(token|secret|password|passwd|pwd|api_?key|signature)\s*[=:]\s*[^\s"',;)]+/gi, "$1=…");
39
+ }
40
+
41
+ /** 写任务文件:脱敏 → 盖章 schema 版本(写方语义,单写者恒为当前版本)→ 原子写 → 治理(保留最近 TASK_KEEP 条,删最旧;本次写入豁免) */
42
+ export async function writeTask(taskDir: string, record: TaskRecord): Promise<void> {
43
+ const clean: TaskRecord = {
44
+ ...record,
45
+ schemaVersion: TASK_SCHEMA_VERSION,
46
+ message: redact(record.message),
47
+ stages: record.stages.map(s => ({ ...s, message: redact(s.message) })),
48
+ ...(record.error === undefined ? {} : { error: { ...record.error, reason: redact(record.error.reason) } }),
49
+ };
50
+ await atomicWriteText(`${taskDir}/${record.id}.json`, `${JSON.stringify(clean, null, 2)}\n`);
51
+ const all = (await readTasks(taskDir))
52
+ .filter(t => t.id !== record.id)
53
+ .sort((a, b) => a.startedAt - b.startedAt);
54
+ const stale = all.slice(0, Math.max(0, all.length + 1 - TASK_KEEP));
55
+ for (const t of stale) {
56
+ try {
57
+ await fsFace().remove(`${taskDir}/${t.id}.json`);
58
+ }
59
+ catch {
60
+ // 单条清理失败不影响主流程(下次写入再收敛)
61
+ }
62
+ }
63
+ }
64
+
65
+ /** 读全部任务文件(损坏条目跳过不炸;目录不存在视同空表) */
66
+ export async function readTasks(taskDir: string): Promise<TaskRecord[]> {
67
+ let names: string[];
68
+ try {
69
+ names = await fsFace().readdir(taskDir);
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ const out: TaskRecord[] = [];
75
+ for (const name of names.filter(n => n.endsWith(".json"))) {
76
+ try {
77
+ out.push(normalizeTask(JSON.parse(new TextDecoder().decode(await fsFace().readFile(`${taskDir}/${name}`))) as TaskRecord));
78
+ }
79
+ catch {
80
+ // 单文件损坏跳过:任务面可观测性优先,不为坏文件阻塞对账/轮询
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** 单条查询(轮询消费面 getTask);id 不在白名单(lock/state 键同款字符集,Finding B 防穿越)、缺失或损坏返回 null */
87
+ export async function getTask(taskDir: string, id: string): Promise<TaskRecord | null> {
88
+ if (!/^[\w.-]+$/.test(id)) {
89
+ return null;
90
+ }
91
+ try {
92
+ return normalizeTask(JSON.parse(new TextDecoder().decode(await fsFace().readFile(`${taskDir}/${id}.json`))) as TaskRecord);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 超时清扫(启动/对账时点调用):running 且 startedAt 超 TASK_TIMEOUT_MS 判 failed
101
+ * (retryable——重跑即可续装),finishedAt 补齐。返回被改判的记录。
102
+ */
103
+ export async function sweepTimedOut(taskDir: string, now: number = Date.now()): Promise<TaskRecord[]> {
104
+ const flipped: TaskRecord[] = [];
105
+ for (const t of await readTasks(taskDir)) {
106
+ if (t.status !== "running" || now - t.startedAt <= TASK_TIMEOUT_MS) {
107
+ continue;
108
+ }
109
+ const fixed: TaskRecord = {
110
+ ...t,
111
+ status: "failed",
112
+ finishedAt: now,
113
+ error: { reason: `安装任务超时(超过 ${Math.round(TASK_TIMEOUT_MS / 60000)} 分钟无进展,持有者疑似崩溃)——R13/B9/R31 启动/对账判 failed;锁前失败可重跑,锁后失败请先对账(reconcile)`, retryable: true },
114
+ };
115
+ await writeTask(taskDir, fixed);
116
+ flipped.push(fixed);
117
+ }
118
+ return flipped;
119
+ }
@@ -0,0 +1,110 @@
1
+ // config.yaml 文本锚点编辑(R18):安装器改写用户 config.yaml 的唯一手段。
2
+ // 禁止 yaml 解析/stringify 回写——用户注释、缩进、空行、键序必须逐字保留,
3
+ // 因此只做文本级行操作:锚点是整行注释(如 `# ojm:mount`),块插入其下一行。
4
+ // 匹配口径:整行 trim 后相等才算已存在(子串出现不算,防误判幂等)。
5
+
6
+ export function hasAnchor(content: string, anchor: string): boolean {
7
+ const target = anchor.trim();
8
+ return content.split("\n").some(line => line.trim() === target);
9
+ }
10
+
11
+ export function insertAnchor(content: string, anchorComment: string, block: string): string {
12
+ const target = anchorComment.trim();
13
+ const lines = content.split("\n");
14
+ const index = lines.findIndex(line => line.trim() === target);
15
+ if (index === -1) {
16
+ throw new Error(`text-edit: config.yaml 中未找到锚点注释行「${anchorComment}」,无法插入内容块`);
17
+ }
18
+ // 幂等:锚点的下一行已与本块首行相同(上次插入的痕迹)则原样返回,
19
+ // 二次调用 no-op。判定只看紧邻一行(trim 后比较,兼容缩进继承);
20
+ // 用户在锚点与块之间手工加行后重跑会再插一次——不猜意图,宁重不漏。
21
+ const firstBlock = block.split("\n")[0].trim();
22
+ if (lines[index + 1] !== undefined && lines[index + 1].trim() === firstBlock) {
23
+ return content;
24
+ }
25
+ // 缩进继承锚点行:块的每个非空行前缀锚点行缩进,空行保持为空(不留尾随空白)
26
+ const indent = /^\s*/.exec(lines[index])?.[0] ?? "";
27
+ const blockLines = block.split("\n").map(line => (line === "" ? "" : indent + line));
28
+ lines.splice(index + 1, 0, ...blockLines);
29
+ return lines.join("\n");
30
+ }
31
+
32
+ /**
33
+ * D3 一次性注入集收口(P1-10,裁决 7 escape hatch;S1-1 实测定稿「安装无需改 mounts」——
34
+ * 注入集只剩 server.reload_api: true,R18)。纯文本判定,不引 yaml 解析:
35
+ * - 存在 reload_api 键:值 false → 拒绝(用户主动关闭热更端点,安装器不偷偷翻转语义值);
36
+ * 其余(true 等)→ no-op 幂等;
37
+ * - 键不存在 → 锚点 `# ojm:reload` 下插入 `reload_api: true`(insertAnchor 语义:缩进继承、
38
+ * 注释逐字保留;锚点缺失 = 可读报错不猜位置——锚点随模板 config.yaml 发布,P1-16 接线点)。
39
+ */
40
+ export function ensureReloadApi(content: string): { content: string, changed: boolean } {
41
+ const lines = content.split("\n");
42
+ const idx = lines.findIndex(l => /^\s*reload_api\s*:/.test(l));
43
+ if (idx !== -1) {
44
+ if (/^\s*reload_api\s*:\s*false\s*(?:#.*)?$/.test(lines[idx]!)) {
45
+ throw new Error("text-edit: config.yaml 显式 reload_api: false(用户关闭热更端点)——安装器拒绝偷偷翻转语义值,请人工确认后改回 true 再重试安装");
46
+ }
47
+ return { content, changed: false };
48
+ }
49
+ return { content: insertAnchor(content, "# ojm:reload", "reload_api: true"), changed: true };
50
+ }
51
+
52
+ /**
53
+ * 安装序号自增(B-11 reload 触发面;PRD E5 实测先例:vars 段语义值改动可热生效,而
54
+ * reloadConfig 的 unchanged 判定基于 config 文档 sha——不落盘变更则热更恒 unchanged)。
55
+ * R18 同纪律:纯文本行操作,注释/缩进/键序逐字保留,`ojm_install_seq: N` 整行自增为 N+1
56
+ * (行内尾注释原样保留)。键缺失/形状不符 = 可读报错不猜位置(键随模板 config.yaml 发布;
57
+ * 用户删行 = 显式退出热更触发,安装器拒绝静默安装)。
58
+ */
59
+ export function bumpInstallSeq(content: string): { content: string, seq: number } {
60
+ const lines = content.split("\n");
61
+ const idx = lines.findIndex(l => /^\s*ojm_install_seq\s*:\s*\d+\s*(?:#.*)?$/.test(l));
62
+ if (idx === -1) {
63
+ throw new Error("text-edit: config.yaml 缺 ojm_install_seq: N 行(vars 安装序号,模板 config.yaml 随发,勿删)——reload 热更无变更可落(unchanged 恒真),安装终止于 reloading 段");
64
+ }
65
+ const m = /^(\s*ojm_install_seq\s*:\s*)(\d+)/.exec(lines[idx]!)!;
66
+ const seq = Number(m[2]) + 1;
67
+ lines[idx] = `${m[1]}${seq}${lines[idx]!.slice(m[0].length)}`;
68
+ return { content: lines.join("\n"), seq };
69
+ }
70
+
71
+ /**
72
+ * C2-26 anonymous 运行时接线(P2-15③ 承载,观察项 6 收口):把方案的匿名端点追加进
73
+ * config.yaml `auth.anonymous_paths` 列表(oj 匿名模型 config 独占;热改属
74
+ * restart_pending——reload 段消息已明示,重启后生效)。
75
+ * 幂等:已存在(trim 相等)的路径不重复插入;`anonymous_paths:` 键缺失 = 可读报错
76
+ * (键随模板 config.yaml 发布,用户删键 = 显式退出匿名模型,安装器不猜位置重建)。
77
+ * paths 传方案相对形态(C2-26 anonymous 值,首段=apiSeg);本函数统一补前导 `/`。
78
+ */
79
+ export function appendAnonymousPaths(content: string, paths: string[]): { content: string, added: string[] } {
80
+ const lines = content.split("\n");
81
+ const idx = lines.findIndex(l => /^\s*anonymous_paths\s*:/.test(l));
82
+ if (idx === -1) {
83
+ throw new Error("text-edit: config.yaml 缺 auth.anonymous_paths 键(oj 匿名模型 config 独占,键随模板 config.yaml 发布)——方案匿名端点无法接线,请恢复该键后重试安装");
84
+ }
85
+ // 列表项收集:键行之后连续的 `- item` 行(缩进任意);空行/注释行跳过不中断
86
+ // (模板 config.yaml 列表中部有行内注释),遇首个其他内容行即止
87
+ const wanted = paths.map(p => (p.startsWith("/") ? p : `/${p}`));
88
+ const existing = new Set<string>();
89
+ let last = idx;
90
+ for (let i = idx + 1; i < lines.length; i++) {
91
+ const line = lines[i]!;
92
+ if (line.trim() === "" || /^\s*#/.test(line)) {
93
+ continue;
94
+ }
95
+ // 先剥行内注释再取项值(防 ReDoS:不写可交换量词的组合)
96
+ const stripped = line.replace(/#.*$/, "").trim();
97
+ if (!stripped.startsWith("- ")) {
98
+ break;
99
+ }
100
+ existing.add(stripped.slice(2).trim());
101
+ last = i;
102
+ }
103
+ const added = wanted.filter(p => !existing.has(p));
104
+ if (added.length === 0) {
105
+ return { content, added: [] };
106
+ }
107
+ const itemIndent = /^\s*-/.exec(lines[idx + 1] ?? "")?.[0]?.replace(/-.*/, "") ?? (`${/^\s*/.exec(lines[idx]!)?.[0] ?? ""} `);
108
+ lines.splice(last + 1, 0, ...added.map(p => `${itemIndent}- ${p}`));
109
+ return { content: lines.join("\n"), added };
110
+ }
@@ -0,0 +1,41 @@
1
+ // tgz 成员守卫(R1):zip-slip 防护的校验核,纯函数、无依赖。
2
+ // 输入契约:`tar -tvzf` 的逐行输出(P1-6 extract 步骤必须喂 verbose 形态;
3
+ // 非 verbose `tar -tzf` 是裸成员名,首字符会撞 l/h 误拒——勿换)。
4
+ // 条目类型取权限串首字符(l=symlink、h=hardlink);symlink 行成员名取 ` -> ` 左侧。
5
+ // 拒绝:绝对路径、任何 `..` 段、l/h 条目、空成员名、反斜杠路径(防御性);
6
+ // `./` 前缀视为相对合法(归一化后必须在包根内)。
7
+
8
+ function extractMemberName(line: string): string {
9
+ const arrow = line.indexOf(" -> ");
10
+ const entry = arrow === -1 ? line : line.slice(0, arrow);
11
+ const trimmed = entry.trim();
12
+ if (trimmed === "")
13
+ return "";
14
+ const fields = trimmed.split(/\s+/);
15
+ return fields[fields.length - 1];
16
+ }
17
+
18
+ export function validateTarListing(lines: string[]): { ok: true } | { ok: false, offender: string } {
19
+ for (const line of lines) {
20
+ const name = extractMemberName(line);
21
+ // 空成员名:退化行(空行/无名字段)一律拒绝,安全边界不做容错
22
+ if (name === "")
23
+ return { ok: false, offender: "" };
24
+ const type = line.trimStart()[0];
25
+ if (type === "l" || type === "h")
26
+ return { ok: false, offender: name };
27
+ // 成员名并非可靠来源:bsdtar 成员名可含空格,`../evil` 可藏在中段 token 里。
28
+ // 路径检查覆盖行的全部空白分隔 token(日期/时刻 token 不含 `..`,不误杀);
29
+ // 任一 token 以 `/` 开头即拒(封「空格名 `/etc x`」形态)。
30
+ // `C:/x` 这类盘符前缀不拒:POSIX 下是合法相对目录名、无逃逸力,反斜杠形态才拒。
31
+ for (const token of line.split(/\s+/)) {
32
+ if (token === "")
33
+ continue;
34
+ if (token.startsWith("/") || token.includes("\\"))
35
+ return { ok: false, offender: token };
36
+ if (token.split("/").includes(".."))
37
+ return { ok: false, offender: token };
38
+ }
39
+ }
40
+ return { ok: true };
41
+ }