@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,182 @@
1
+ // 部署步骤(P1-9,十阶段 stage deploying;R18/R30 前半):解压 staging 内 web/ 产物树真复制到
2
+ // 站点 web 模块目录(<dist>/modules/<name>/<version>/…,布局与 packages/cli/src/build.ts 产物形态、
3
+ // shell-dist 静态服务约定一致),随后读-改-原子写 upsert modules.json。
4
+ // 站点路径全部 ctx 注入(stagingDir/siteModulesDir/modulesJsonPath)——安装器不硬编码站点布局;
5
+ // fs 依赖同 atomic.ts(globalThis.fs 面,测试注入 node 适配)。
6
+ // D13:modules.json 一切写入经 atomicWriteText——读者(并发 fetch)永不见半截 JSON(AC-P1-12);
7
+ // 损坏清单拒绝 upsert(避免覆盖丢失他模块条目),宁可 fail-closed。
8
+ // R30:entry/css 路径 POSIX 分隔符序列化(Windows 侧反斜杠统一归一)。
9
+ // R10:integrity 由调用方(P1-10 对验签后包内容重算)传入,本步不计算;缺省回落 manifest.web.integrity
10
+ // (构建器计算值)——integrity 必须基于包内容,防 manifest 与产物错配。
11
+ // entry 浏览器加载验证不在安装器范围(Node import 必假绿——设计 §4.1):宿主 error boundary 兜底
12
+ // + P2 深检可见,本步只保证落位与清单一致。
13
+
14
+ import type { FsFace } from "../atomic";
15
+ import type { InstallContext } from "./preflight";
16
+ import { atomicWriteText, fsFace, isMissingPath } from "../atomic";
17
+
18
+ /**
19
+ * modules.json 模块条目(对齐 shell 消费面 packages/cli/shell/src/manifest.ts 透传注释与
20
+ * shell/src/preload.ts HostModule:name/entry/enabled/dependencies/peerRuntime/css 必透传)。
21
+ * 构建器其余字段(version/chunks)经索引签名原样透传,upsert 不裁剪。
22
+ */
23
+ export interface ModuleEntry {
24
+ name: string
25
+ entry: string
26
+ enabled: boolean
27
+ dependencies?: string[]
28
+ peerRuntime?: string
29
+ css?: string[]
30
+ integrity?: string
31
+ [extra: string]: unknown
32
+ }
33
+
34
+ /** R30:POSIX 分隔符序列化——反斜杠/正斜杠混合路径统一为 / */
35
+ export function posixPath(p: string): string {
36
+ return p.split(/[\\/]/).join("/");
37
+ }
38
+
39
+ function isObj(v: unknown): v is Record<string, unknown> {
40
+ return typeof v === "object" && v !== null && !Array.isArray(v);
41
+ }
42
+
43
+ /** manifest api.deps 的模块依赖键(排除 _platform——平台非 web 模块依赖,P2-15② extras) */
44
+ function apiDepsOf(m: Record<string, unknown>): string[] {
45
+ const deps = isObj(m.api) && isObj((m.api as Record<string, unknown>).deps) ? (m.api as Record<string, unknown>).deps as Record<string, unknown> : {};
46
+ return Object.keys(deps).filter(k => k !== "_platform").sort();
47
+ }
48
+
49
+ /** 归一化条目:entry/css 路径 POSIX 化;其余字段(含构建器 extras)原样透传 */
50
+ function normalizeEntry(entry: ModuleEntry): ModuleEntry {
51
+ const out: ModuleEntry = { ...entry, entry: posixPath(entry.entry) };
52
+ if (Array.isArray(entry.css)) {
53
+ out.css = entry.css.map(c => posixPath(String(c)));
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * modules.json 读-modify-原子写 upsert(D13):同名条目整体替换(位置保持)、其余条目原样保留;
60
+ * 清单不存在视同空表创建;清单损坏(非合法 JSON / 非数组)拒绝写入并上抛——绝不覆盖。
61
+ */
62
+ export async function upsertModulesJson(entry: ModuleEntry, opts: { modulesJsonPath: string }): Promise<void> {
63
+ const f = fsFace();
64
+ let list: ModuleEntry[] = [];
65
+ try {
66
+ list = JSON.parse(new TextDecoder().decode(await f.readFile(opts.modulesJsonPath)));
67
+ }
68
+ catch (e) {
69
+ if (isMissingPath(e)) {
70
+ list = [];
71
+ }
72
+ else if (e instanceof SyntaxError || !Array.isArray(list)) {
73
+ throw new Error(`modules.json 不是合法 JSON,拒绝 upsert(避免覆盖丢失既有条目,请人工修复后重试):${String(e)}`);
74
+ }
75
+ else {
76
+ throw e;
77
+ }
78
+ }
79
+ if (!Array.isArray(list)) {
80
+ throw new TypeError("modules.json 顶层不是数组,拒绝 upsert(避免覆盖丢失既有条目)");
81
+ }
82
+ const normalized = normalizeEntry(entry);
83
+ const idx = list.findIndex(e => isObj(e) && e.name === normalized.name);
84
+ if (idx === -1) {
85
+ list.push(normalized);
86
+ }
87
+ else {
88
+ list[idx] = normalized;
89
+ }
90
+ await atomicWriteText(opts.modulesJsonPath, `${JSON.stringify(list, null, 2)}\n`);
91
+ }
92
+
93
+ export interface DeployWebContext extends InstallContext {
94
+ /** 解压暂存目录(web/ 产物树所在;与 download/verify/extract 同目录接力) */
95
+ stagingDir: string
96
+ /** 站点 web 模块目录(<dist>/modules,注入不硬编码) */
97
+ siteModulesDir: string
98
+ /** 站点 modules.json 路径(与 siteModulesDir 同属 <dist>) */
99
+ modulesJsonPath: string
100
+ /** P1-10 对验签后包内容重算的 sha384(sha384-<base64>,R10);缺省回落 manifest.web.integrity */
101
+ integrity?: string
102
+ }
103
+
104
+ export interface StageResult {
105
+ stage: "deploying"
106
+ ok: boolean
107
+ message: string
108
+ retryable?: boolean
109
+ }
110
+
111
+ /** 递归复制目录树(readdir 成功即按目录递归、抛错即按文件复制——FsFace 无 stat 面的最小判别);迁移服务目录物化复用(B-11) */
112
+ export async function copyTree(src: string, dest: string, f: FsFace): Promise<void> {
113
+ const names = await f.readdir(src);
114
+ await f.mkdir(dest, { recursive: true });
115
+ for (const name of names) {
116
+ const s = `${src}/${name}`;
117
+ const d = `${dest}/${name}`;
118
+ try {
119
+ await copyTree(s, d, f);
120
+ }
121
+ catch {
122
+ await f.copyFile(s, d);
123
+ }
124
+ }
125
+ }
126
+
127
+ export async function deployWeb(ctx: DeployWebContext): Promise<StageResult> {
128
+ const m = isObj(ctx.manifest) ? ctx.manifest : {};
129
+ const web = m.web;
130
+ if (!isObj(web)) {
131
+ return { stage: "deploying", ok: true, message: "部署完成:manifest 无 web 段(api-only 方案),零复制零清单变更" };
132
+ }
133
+ if (typeof web.entry !== "string" || web.entry === "") {
134
+ return { stage: "deploying", ok: false, retryable: false, message: "部署失败:web 段存在但 entry 缺失(C2-27:web 段存在时必填)——契约破坏,重试不自愈" };
135
+ }
136
+ const f = fsFace();
137
+ // 包内 web 树即 modules 子树(C2-27 路径空间 = modules/<name>/<version>/…):
138
+ // staging/web/modules → 站点 <dist>/modules 一层对齐,不整树套复制(避免 modules/modules 双层嵌套)。
139
+ try {
140
+ await copyTree(`${ctx.stagingDir}/web/modules`, ctx.siteModulesDir, f);
141
+ }
142
+ catch (e) {
143
+ return {
144
+ stage: "deploying",
145
+ ok: false,
146
+ retryable: false,
147
+ message: `部署失败:staging web 产物树(${ctx.stagingDir}/web/modules)不可复制(${String(e)})——web.entry 已声明而包内 web/modules 缺失属包结构违约(C2-27),重试不自愈`,
148
+ };
149
+ }
150
+ const integrity = typeof ctx.integrity === "string" && ctx.integrity !== ""
151
+ ? ctx.integrity
152
+ : typeof web.integrity === "string" ? web.integrity : undefined;
153
+ const entry: ModuleEntry = {
154
+ name: ctx.name,
155
+ entry: posixPath(web.entry),
156
+ enabled: true,
157
+ // P2-15②(G7 defer-P2 承载):完整条目 extras——version/dependencies/chunks 补齐
158
+ // (deployWeb 全新生成 entry,构建器条目不透传的缺口;宿主 HostModule 全字段消费面)
159
+ version: ctx.version,
160
+ ...(apiDepsOf(m).length > 0 ? { dependencies: apiDepsOf(m) } : {}),
161
+ chunks: [], // 单 entry 产物无懒加载 chunk(宿主 collectPreloads 消费 `?? []`,字段形状齐备)
162
+ ...(Array.isArray(web.css) ? { css: web.css.map(c => posixPath(String(c))) } : {}),
163
+ ...(integrity !== undefined ? { integrity } : {}),
164
+ ...(typeof m.peerRuntime === "string" ? { peerRuntime: m.peerRuntime } : {}),
165
+ };
166
+ try {
167
+ await upsertModulesJson(entry, { modulesJsonPath: ctx.modulesJsonPath });
168
+ }
169
+ catch (e) {
170
+ return {
171
+ stage: "deploying",
172
+ ok: false,
173
+ retryable: false,
174
+ message: `部署失败:modules.json upsert 被拒(${String(e)})——文件已复制、清单未被半写(D13 原子写保证读者无感)`,
175
+ };
176
+ }
177
+ return {
178
+ stage: "deploying",
179
+ ok: true,
180
+ message: `部署完成:staging web/ → ${ctx.siteModulesDir}/,modules.json 已 upsert ${ctx.name}(entry ${entry.entry}${integrity ? `;integrity ${integrity}` : ""};D13 原子写,读者永不见半截 JSON)`,
181
+ };
182
+ }
@@ -0,0 +1,90 @@
1
+ // 下载步骤(P1-6,十阶段 stage 2 downloading):S1-2.3 裁决形态——isolate 内 fetch → fs.writeFile。
2
+ // 落盘位 = <staging>/<archive.file>(.staging/<name>-<version>.tgz),与 verify/extract 同目录接力。
3
+ // 失败语义走 StageResult(附录 A downloading:retryable ✓——网络类失败可重试);契约身份缺失
4
+ // (C2-29,重试不自愈)retryable=false。fetch 面经 ctx 注入(DIP,生产 = 平台 isolate 全局 fetch),
5
+ // fs 同 atomic(globalThis.fs 面)。
6
+
7
+ import type { InstallContext } from "./preflight";
8
+ import { fsFace } from "../atomic";
9
+
10
+ /** 最小 fetch 面(结构化:Node fetch/Response 天然满足;测试注入 stub 不真跑网络) */
11
+ export type FetchFace = (url: string) => Promise<{
12
+ ok: boolean
13
+ status: number
14
+ arrayBuffer: () => Promise<ArrayBuffer>
15
+ }>;
16
+
17
+ export interface DownloadContext extends InstallContext {
18
+ /** 分发包完整 URL(安装器由市场源 + archive.file 拼装;本步不拼 URL) */
19
+ archiveUrl: string
20
+ /** 暂存目录(.staging/);与 verify/extract 同目录接力(D11 锁前失败清场重跑) */
21
+ staging: string
22
+ /** 注入式 fetch 面 */
23
+ fetch: FetchFace
24
+ }
25
+
26
+ export interface StageResult {
27
+ stage: "downloading"
28
+ ok: boolean
29
+ message: string
30
+ retryable?: boolean
31
+ }
32
+
33
+ /**
34
+ * 三步共用:从 manifest 提取 archive 身份(C2-29 安全形态守卫,verify/extract 复用)。
35
+ * file 仅接受 `<name>-<version>.tgz` 安全形态:无路径分隔符(穿越结构性不可能)、
36
+ * 无前导点、无 `..` 段——这是信任边界(archive 来自分发源),宁可过严不可放行。
37
+ */
38
+ export function archiveFileOf(manifest: unknown): { ok: true, file: string, archive: Record<string, unknown> } | { ok: false, message: string } {
39
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
40
+ return { ok: false, message: "C2-29:manifest 不是键值对象,archive 身份不可得" };
41
+ }
42
+ const archive = (manifest as Record<string, unknown>).archive;
43
+ if (typeof archive !== "object" || archive === null || Array.isArray(archive)) {
44
+ return { ok: false, message: "C2-29:archive 段缺失或不是键值对象(Form B 真源须含 file/sha256/size)" };
45
+ }
46
+ const file = archive.file;
47
+ if (typeof file !== "string" || !/^[A-Z0-9][\w.-]*$/i.test(file) || file.includes("..")) {
48
+ return { ok: false, message: `C2-29:archive.file「${String(file)}」缺失或不符安全形态 ^<name>-<version>.tgz$(禁路径分隔符/前导点/.. 穿越段)` };
49
+ }
50
+ return { ok: true, file, archive };
51
+ }
52
+
53
+ export async function download(ctx: DownloadContext): Promise<StageResult> {
54
+ const arch = archiveFileOf(ctx.manifest);
55
+ if (!arch.ok) {
56
+ return { stage: "downloading", ok: false, retryable: false, message: `下载失败:${arch.message}` };
57
+ }
58
+ // P2-11 索引投毒加固:archive.file 收紧 <name>-<version>.tgz 逐字(禁异名/异版本/路径拼接;
59
+ // 此刻 name/version 已过 preflight C2-01/C2-02,均无路径分隔符,逐字等值即排除穿越形态)
60
+ const expected = `${ctx.name}-${ctx.version}.tgz`;
61
+ if (arch.file !== expected) {
62
+ return { stage: "downloading", ok: false, retryable: false, message: `下载失败:archive.file「${arch.file}」须逐字为 <name>-<version>.tgz(「${expected}」,P2-11 索引投毒加固)` };
63
+ }
64
+ const dest = `${ctx.staging}/${arch.file}`;
65
+ try {
66
+ const res = await ctx.fetch(ctx.archiveUrl);
67
+ if (!res.ok) {
68
+ return {
69
+ stage: "downloading",
70
+ ok: false,
71
+ retryable: true,
72
+ message: `下载失败:分发源返回 HTTP ${res.status}(${ctx.archiveUrl})——附录 A downloading:请检查网络与市场地址后重试`,
73
+ };
74
+ }
75
+ const bytes = new Uint8Array(await res.arrayBuffer());
76
+ // P2-11 索引投毒加固:下载后立即比 archive.size(声明在场且为有限数才比对;
77
+ // 内容级真伪仍由 verify 段 sha256 fail-closed 把关,size 是低成本前置快筛)
78
+ const declared = arch.archive.size;
79
+ if (typeof declared === "number" && Number.isFinite(declared) && declared !== bytes.byteLength) {
80
+ return { stage: "downloading", ok: false, retryable: false, message: `下载失败:分发包 ${bytes.byteLength} 字节与索引声明 archive.size ${declared} 不符——P2-11 索引投毒加固(size 立即比对)` };
81
+ }
82
+ const f = fsFace();
83
+ await f.mkdir(ctx.staging, { recursive: true });
84
+ await f.writeFile(dest, bytes);
85
+ return { stage: "downloading", ok: true, message: `下载完成:${arch.file}(${bytes.byteLength} 字节)→ ${dest}` };
86
+ }
87
+ catch (e) {
88
+ return { stage: "downloading", ok: false, retryable: true, message: `下载失败:${String(e)}——附录 A downloading(S1-2.3 fetch→fs.writeFile 范式)` };
89
+ }
90
+ }
@@ -0,0 +1,239 @@
1
+ // 解压步骤(P1-6,十阶段 stage 4 extracting):R1 zip-slip 防护的 list→validate→extract 三步范式
2
+ // (S1-2.2 裁决:tar 会忠实重建 symlink/hardlink,先列后解是安全必需而非优化)。
3
+ // listing 必须来自 -tvzf verbose 形态(消费方铁律①:非 verbose 裸名首字符撞 l/h 误拒,勿换 -tzf);
4
+ // 喂 validateTarListing 前先 trim 尾部空行再 split(铁律②:尾随空行=空成员名必拒)。
5
+ // 任一恶意成员 → 全拒且零解压零落盘(附录 A extracting「安装已终止且零落盘」);校验通过才
6
+ // `tar -xzf -C staging`,解压到 staging 就位即止——版本目录 rename 由 P1-10 落锁段接管。
7
+ // argv 与平台 exec 白名单逐字对齐(S1-2 基座 yaml),不得添加未白名单旗标——安全性由
8
+ // validate 段 + `-C` 限定目标目录承担,而非 tar 旗标。exec 面经 ctx 注入(DIP,测试 stub)。
9
+
10
+ import type { InstallContext } from "./preflight";
11
+ import { fsFace } from "../atomic";
12
+ import { validateTarListing } from "../tgz-guard";
13
+ import { archiveFileOf } from "./download";
14
+
15
+ export interface ExecResult {
16
+ code: number
17
+ stdout: string
18
+ stderr: string
19
+ }
20
+
21
+ export interface ExecFace {
22
+ /** opts 透传平台 exec.run(timeoutMs 等;0.1.78 共用 opts——缺省 30s 会杀长命令) */
23
+ run: (argv: string[], opts?: { timeoutMs?: number }) => Promise<ExecResult>
24
+ /**
25
+ * oj 0.1.78 流式形态(P2-13 feature-detect 存在性;缺失回落 exec.run——stage 粒度,进度不增量刷)。
26
+ * stdout/stderr 为 AsyncIterable<Uint8Array>(原始字节,UTF-8 切块自管);exit() 等待退出;
27
+ * 超时/超限不抛错只 code=137(v0.1.79 订正)。消费面:migrate 段进度流(h.kill() 主动取消归 P3,G2)。
28
+ */
29
+ spawn?: (argv: string[], opts?: { timeoutMs?: number }) => Promise<ExecSpawnHandle>
30
+ }
31
+
32
+ /** exec.spawn 句柄(devkit api-manual「exec 流式形态 v0.1.78」;devkit global.d.ts 未收录,此处按契约镜像) */
33
+ export interface ExecSpawnHandle {
34
+ stdout: AsyncIterable<Uint8Array>
35
+ stderr: AsyncIterable<Uint8Array>
36
+ exit: () => Promise<{ code: number }>
37
+ kill: () => void
38
+ }
39
+
40
+ export interface ExtractContext extends InstallContext {
41
+ /** 暂存目录(下载/校验/解压三步同目录接力;亦为解压 -C 目标) */
42
+ staging: string
43
+ /** 注入式 exec 面(平台 exec.run;测试 stub 不真跑 tar) */
44
+ exec: ExecFace
45
+ }
46
+
47
+ export interface StageResult {
48
+ stage: "extracting"
49
+ ok: boolean
50
+ message: string
51
+ retryable?: boolean
52
+ }
53
+
54
+ function isObj(v: unknown): v is Record<string, unknown> {
55
+ return typeof v === "object" && v !== null && !Array.isArray(v);
56
+ }
57
+
58
+ /** tar 列表命令(逐字断言):-tvzf verbose 形态是 validateTarListing 的输入契约(铁律①) */
59
+ export function buildListCmd(file: string): string[] {
60
+ return ["tar", "-tvzf", file];
61
+ }
62
+
63
+ /** tar 解压命令(逐字断言):-C 限定 staging;对齐 S1-2 白名单 `tar -xzf ${path:archive} -C ${path:dest}` */
64
+ export function buildExtractCmd(file: string, staging: string): string[] {
65
+ return ["tar", "-xzf", file, "-C", staging];
66
+ }
67
+
68
+ export async function extract(ctx: ExtractContext): Promise<StageResult> {
69
+ const arch = archiveFileOf(ctx.manifest);
70
+ if (!arch.ok) {
71
+ return { stage: "extracting", ok: false, retryable: false, message: `解压失败:${arch.message}` };
72
+ }
73
+ const file = `${ctx.staging}/${arch.file}`;
74
+ try {
75
+ // 第一步 list:verbose listing 是安全校验的唯一输入(R1)
76
+ const listed = await ctx.exec.run(buildListCmd(file));
77
+ if (listed.code !== 0) {
78
+ return {
79
+ stage: "extracting",
80
+ ok: false,
81
+ retryable: true,
82
+ message: `解压失败:tar 列表失败(exit ${listed.code}):${(listed.stderr || listed.stdout).trim()}(R1 三步范式第一步;附录 A extracting)`,
83
+ };
84
+ }
85
+ // 铁律②:tar 输出恒带尾随换行(可能多个空行),先 trim 尾部空行再 split
86
+ const lines = listed.stdout.replace(/\n+$/, "").split("\n");
87
+ // 第二步 validate:任一越界/链接成员即全拒,零解压
88
+ const v = validateTarListing(lines);
89
+ if (!v.ok) {
90
+ return {
91
+ stage: "extracting",
92
+ ok: false,
93
+ retryable: true,
94
+ message: `解压失败:包内容未通过安全校验(R1:offender「${v.offender}」为越界/链接成员)——零解压零落盘,附录 A extracting:若反复出现请勿使用该分发包`,
95
+ };
96
+ }
97
+ // 第三步 extract:校验通过才落盘;-C staging 就位即止,rename 由 P1-10 落锁段接管
98
+ const extracted = await ctx.exec.run(buildExtractCmd(file, ctx.staging));
99
+ if (extracted.code !== 0) {
100
+ return {
101
+ stage: "extracting",
102
+ ok: false,
103
+ retryable: true,
104
+ message: `解压失败:tar 解压失败(exit ${extracted.code}):${(extracted.stderr || extracted.stdout).trim()}(R1 三步范式第三步;附录 A extracting)`,
105
+ };
106
+ }
107
+ await flattenBundleRoot(ctx.staging);
108
+ const c31 = await checkApiSegment(ctx);
109
+ if (!c31.ok) {
110
+ return { stage: "extracting", ok: false, retryable: false, message: c31.message };
111
+ }
112
+ return { stage: "extracting", ok: true, message: `解压完成:${lines.length} 个成员 → ${ctx.staging}/(单根已归平,§2.9 布局消费点;C2-31 复核通过;目录 rename 由 P1-10 落锁段接管)` };
113
+ }
114
+ catch (e) {
115
+ return { stage: "extracting", ok: false, retryable: true, message: `解压失败:${String(e)}——附录 A extracting(S1-2 exec.run tar 三步范式)` };
116
+ }
117
+ }
118
+
119
+ /** oj 原生模块清单七键白名单(OJ-X 红线:manifest.rs 形态 name/desc/version/config/db/tables/deps) */
120
+ const NATIVE_KEYS = new Set(["name", "desc", "version", "config", "db", "tables", "deps"]);
121
+
122
+ /**
123
+ * C2-31 安装期复核纯函数(双端 DRY 于夹具,preflight 同纪律;构建侧同形实现在
124
+ * packages/cli/src/bundle.ts checkModuleManifest):包内 oj 原生清单 ↔ C2 Form A 逐项钉住——
125
+ * name/version 三方一致、desc === summary(构建期映射真源;summary 缺省/多字节/空白边界)、
126
+ * 七键白名单(白名单外键 = OJ-X 红线,oj 不拒但契约拒)。只验不写:安装期改写 desc 等于
127
+ * 掩盖上游映射被篡改,必须拒装(附录 B-11 裁决备选的落地形态:bundle 侧 ojm build --bundle
128
+ * 已写原生清单,安装期复核即映射消费点)。
129
+ */
130
+ export function checkModuleManifest(
131
+ c2: unknown,
132
+ native: unknown,
133
+ name: string,
134
+ version: string,
135
+ ): { ok: true } | { ok: false, message: string } {
136
+ if (!isObj(native)) {
137
+ return { ok: false, message: `包内原生清单不是键值对象(实为 ${native === null ? "null" : Array.isArray(native) ? "数组" : typeof native})` };
138
+ }
139
+ const summary = isObj(c2) && typeof c2.summary === "string" ? c2.summary : undefined;
140
+ if (summary === undefined || summary.trim() === "") {
141
+ return { ok: false, message: "C2 manifest summary 缺省或空白——desc 映射无真源(C2-31/C2-04)" };
142
+ }
143
+ const desc = native.desc;
144
+ if (typeof desc !== "string" || desc.trim() === "") {
145
+ return { ok: false, message: "包内原生清单 desc 缺失或空白(oj Manifest 必填字段,serve 装配即失败)——C2-31" };
146
+ }
147
+ if (desc !== summary) {
148
+ return { ok: false, message: "包内原生清单 desc 与 C2 summary 不一致(构建期映射被篡改或上游不一致,拒装)——C2-31" };
149
+ }
150
+ if (native.name !== name) {
151
+ return { ok: false, message: `包内原生清单 name「${String(native.name)}」与安装身份「${name}」不一致——C2-31` };
152
+ }
153
+ if (native.version !== version) {
154
+ return { ok: false, message: `包内原生清单 version「${String(native.version)}」与安装身份「${version}」不一致——C2-31` };
155
+ }
156
+ const extra = Object.keys(native).filter(k => !NATIVE_KEYS.has(k));
157
+ if (extra.length > 0) {
158
+ return { ok: false, message: `包内原生清单含白名单外键「${extra.join("、")}」(七键 = name/desc/version/config/db/tables/deps,OJ-X 红线)——C2-31` };
159
+ }
160
+ return { ok: true };
161
+ }
162
+
163
+ /**
164
+ * C2-31 安装期复核接线(extracting 段收口):manifest 声明 api 段而包内 api/<name>-<version>/
165
+ * 缺失 = 结构违约;包内 api 模块目录在则必复核其原生清单(web-only 包无 api 段 → 跳过)。
166
+ * yaml 解析走 oj 全局(market/api.ts 同款),解析失败按复核不通过处理(fail-closed)。
167
+ */
168
+ async function checkApiSegment(ctx: ExtractContext): Promise<{ ok: true } | { ok: false, message: string }> {
169
+ const modDir = `${ctx.staging}/api/${ctx.name}-${ctx.version}`;
170
+ let inner: string[];
171
+ try {
172
+ inner = await fsFace().readdir(modDir);
173
+ }
174
+ catch {
175
+ const declared = isObj(ctx.manifest) && ctx.manifest.api !== undefined;
176
+ if (!declared) {
177
+ return { ok: true }; // web-only 方案:无 api 段,migrate 段同步跳过
178
+ }
179
+ return { ok: false, message: `解压复核失败:manifest 声明 api 段而包内 api/${ctx.name}-${ctx.version}/ 缺失(§2.9 结构违约)——C2-31` };
180
+ }
181
+ if (!inner.includes("manifest.yaml")) {
182
+ return { ok: false, message: `解压复核失败:api/${ctx.name}-${ctx.version}/ 缺 manifest.yaml(oj 原生清单,oj build 产物必带)——C2-31` };
183
+ }
184
+ const g = globalThis as { yaml?: { parse: (text: string) => Promise<unknown> } };
185
+ let native: unknown;
186
+ try {
187
+ native = await g.yaml!.parse(new TextDecoder().decode(await fsFace().readFile(`${modDir}/manifest.yaml`)));
188
+ }
189
+ catch (e) {
190
+ return { ok: false, message: `解压复核失败:api/${ctx.name}-${ctx.version}/manifest.yaml 不可解析(${String(e)})——C2-31` };
191
+ }
192
+ return checkModuleManifest(ctx.manifest, native, ctx.name, ctx.version);
193
+ }
194
+
195
+ /**
196
+ * 单根归平(契约 §2.9 布局消费点,P1-13 真机冒烟实测缺此步——附录 B-10):`ojm build --bundle`
197
+ * 产物是 `<name>-<version>/` 单根 tgz,`tar -xzf -C staging` 后内容嵌在一层目录里,而安装器
198
+ * 全链消费扁平 staging(verifyTarget/deployWeb 读 `${staging}/manifest.yaml`,lockCommit 把
199
+ * staging 整体 rename 成版本目录)。归平规则:顶层「以 manifest.yaml 为首层标记」的目录 = 包根,
200
+ * 内容上提一层;标记不匹配(api/、web/、archive 文件)一律不动——不猜布局,宁可下游可读失败。
201
+ * 幂等:flat 布局无匹配目录即空操作;market file 源预解包(manifestOfUpload)与本步骤同调此函数,
202
+ * 二次解压重建的单根目录再次被归平。
203
+ */
204
+ export async function flattenBundleRoot(staging: string): Promise<void> {
205
+ const f = fsFace();
206
+ let names: string[];
207
+ try {
208
+ names = await f.readdir(staging);
209
+ }
210
+ catch {
211
+ return; // staging 不可列(不存在等)交由下游可读失败
212
+ }
213
+ for (const name of names) {
214
+ const root = `${staging}/${name}`;
215
+ let inner: string[];
216
+ try {
217
+ inner = await f.readdir(root); // readdir 失败 = 普通文件(archive 等),跳过
218
+ }
219
+ catch {
220
+ continue;
221
+ }
222
+ if (!inner.includes("manifest.yaml")) {
223
+ continue;
224
+ }
225
+ for (const item of inner) {
226
+ const dest = `${staging}/${item}`;
227
+ try {
228
+ await f.rename(`${root}/${item}`, dest);
229
+ }
230
+ catch {
231
+ // 目标已在(market file 源预解包 manifestOfUpload 先归平过同一 staging,重装/重试
232
+ // 时上次产物尚在——附录 B-10):staging 是安装器私有无价值暂存,先清后搬
233
+ await f.remove(dest, { recursive: true, force: true });
234
+ await f.rename(`${root}/${item}`, dest);
235
+ }
236
+ }
237
+ await f.remove(root, { recursive: true });
238
+ }
239
+ }