@oj-module/cli 0.2.6 → 0.2.14

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 (169) hide show
  1. package/README.md +1 -1
  2. package/devkit/README.md +70 -24
  3. package/devkit/framework-dev.md +14 -5
  4. package/devkit/manual.md +28 -23
  5. package/devkit/module-dev-and-cdn-playground.md +197 -0
  6. package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/runtime.js +130 -45
  9. package/shell-dist/index.html +2 -2
  10. package/shell-dist/versions.json +3 -3
  11. package/src/__tests__/build-backend.test.ts +64 -0
  12. package/src/__tests__/versions.test.ts +26 -0
  13. package/src/build.ts +9 -0
  14. package/src/bundle.ts +8 -5
  15. package/src/contract/__tests__/handler-scan.test.ts +29 -0
  16. package/src/contract/__tests__/market-client.integration.test.ts +170 -0
  17. package/src/contract/handler-scan.ts +35 -4
  18. package/src/dev.ts +10 -2
  19. package/src/index.ts +9 -3
  20. package/src/init.ts +24 -0
  21. package/src/lifecycle/__tests__/bundle.test.ts +3 -3
  22. package/src/market/__tests__/bootstrap.test.ts +204 -0
  23. package/src/market/__tests__/build-index.test.ts +154 -0
  24. package/src/market/__tests__/cdn-layout.test.ts +96 -0
  25. package/src/market/__tests__/failure-playbook.test.ts +33 -0
  26. package/src/market/__tests__/fixture.test.ts +48 -0
  27. package/src/market/__tests__/publish.test.ts +109 -0
  28. package/src/market/__tests__/report.test.ts +51 -0
  29. package/src/market/__tests__/setup.test.ts +114 -0
  30. package/src/market/__tests__/verify.test.ts +111 -0
  31. package/src/market/bootstrap.ts +255 -0
  32. package/src/market/build-index.ts +159 -0
  33. package/src/market/cdn-layout.ts +122 -0
  34. package/src/market/command.ts +139 -0
  35. package/src/market/failure-playbook.ts +57 -0
  36. package/src/market/fixture.ts +138 -0
  37. package/src/market/index-builder.ts +76 -0
  38. package/src/market/publish.ts +156 -0
  39. package/src/market/report.ts +67 -0
  40. package/src/market/scan.ts +99 -0
  41. package/src/market/setup.ts +144 -0
  42. package/src/market/trial.ts +121 -0
  43. package/src/market/verify.ts +298 -0
  44. package/src/market/walkthrough.ts +506 -0
  45. package/src/oj.ts +3 -1
  46. package/src/preview-config.ts +177 -0
  47. package/src/preview.ts +106 -56
  48. package/src/sign.ts +21 -6
  49. package/src/usage.ts +22 -2
  50. package/src/vendor.ts +4 -2
  51. package/src/versions.ts +28 -0
  52. package/templates/api/.ojm-api-exempt.json +6 -2
  53. package/templates/api/config.yaml +24 -2
  54. package/templates/api/src/_platform/README.md +31 -0
  55. package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
  56. package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
  57. package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
  58. package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
  59. package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
  60. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
  61. package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
  62. package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
  63. package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
  64. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
  65. package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
  66. package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
  67. package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
  68. package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
  69. package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
  70. package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
  71. package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
  72. package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
  73. package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
  74. package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
  75. package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
  76. package/templates/api/src/_platform/contacts/api.ts +2 -2
  77. package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
  78. package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
  79. package/templates/api/src/_platform/manifest.yaml +4 -1
  80. package/templates/api/src/_platform/market/api.ts +54 -16
  81. package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
  82. package/templates/api/src/_platform/schema.yaml +26 -0
  83. package/templates/api/src/_platform/solutions/api.ts +13 -12
  84. package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
  85. package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
  86. package/templates/api/src/auth/_shared/session.ts +26 -22
  87. package/templates/api/src/auth/api.ts +1 -1
  88. package/templates/api/src/demo/todos/api.ts +8 -8
  89. package/templates/api/src/home/line/api.ts +4 -4
  90. package/templates/api/src/home/pie/api.ts +4 -4
  91. package/templates/api/src/notification/_shared/notify.ts +97 -57
  92. package/templates/api/src/notification/api.ts +1 -1
  93. package/templates/api/src/notification/manifest.yaml +1 -0
  94. package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
  95. package/templates/api/src/notification/schema.yaml +7 -0
  96. package/templates/api/src/notification/seed.sql +5 -4
  97. package/templates/api/src/personal-center/profile/api.ts +63 -0
  98. package/templates/api/src/personal-center/upload/api.ts +1 -1
  99. package/templates/api/src/system/login-methods/api.ts +1 -1
  100. package/templates/api/src/system/oplog/api.ts +3 -3
  101. package/templates/api/src/system/permissions/api.ts +2 -2
  102. package/templates/api/src/system/roles/api.ts +2 -2
  103. package/templates/api/src/system/roles/bindings/api.ts +2 -2
  104. package/templates/api/src/system/settings/api.ts +2 -2
  105. package/templates/api/src/system/users/api.ts +2 -2
  106. package/templates/api/src/system/users/forbidden/api.ts +2 -2
  107. package/templates/api/src/system/users/password/api.ts +2 -2
  108. package/templates/api/src/system/users/roles/api.ts +2 -2
  109. package/templates/api/src/web/user-info/api.ts +4 -5
  110. package/templates/api/tests/auth-session.spec.ts +1 -1
  111. package/templates/api/tests/auth-throttle.spec.ts +1 -1
  112. package/templates/api/tests/fixture.ts +8 -16
  113. package/templates/api/tests/guard.spec.ts +1 -1
  114. package/templates/api/tests/l1-builder.test.ts +91 -0
  115. package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
  116. package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
  117. package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
  118. package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
  119. package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
  120. package/templates/api/tests/query-utils.spec.ts +85 -1
  121. package/templates/api/tests/rbac-audit.spec.ts +1 -1
  122. package/templates/api/tests/rbac-contacts.spec.ts +2 -2
  123. package/templates/api/tests/rbac-guard.spec.ts +1 -1
  124. package/templates/api/tests/rbac-roles.spec.ts +2 -2
  125. package/templates/api/tests/rbac-settings.spec.ts +66 -21
  126. package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
  127. package/templates/api/tests/rbac-users.spec.ts +1 -1
  128. package/templates/api/tsconfig.json +8 -0
  129. package/templates/cdn/README.md +48 -0
  130. package/templates/env.d.ts +2 -0
  131. package/templates/global.d.ts +850 -0
  132. package/templates/init.include.yaml +14 -2
  133. package/templates/logs/task-events.jsonl +0 -0
  134. package/templates/openapi.json +580 -23
  135. package/templates/package.json +2 -1
  136. package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
  137. package/templates/web/src/_platform/__client/api.ts +152 -39
  138. package/templates/web/src/notification/entry.ts +46 -9
  139. package/templates/web/src/notification/locales/en-US.json +19 -0
  140. package/templates/web/src/notification/locales/zh-CN.json +19 -0
  141. package/templates/web/src/notification/pages/center/index.tsx +131 -0
  142. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
  143. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
  144. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
  145. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
  146. package/templates/web/src/oj-market/api/client.ts +5 -105
  147. package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
  148. package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
  149. package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
  150. package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
  151. package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
  152. package/templates/web/src/oj-market/entry.ts +8 -2
  153. package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
  154. package/templates/web/src/oj-market/locales/en-US.json +2 -1
  155. package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
  156. package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
  157. package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
  158. package/templates/web/src/personal-center/__client/api.ts +25 -1
  159. package/templates/web/src/personal-center/entry.ts +5 -0
  160. package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
  161. package/templates/web/src/system/entry.ts +2 -1
  162. package/templates/web.config.ts +13 -10
  163. package/vendor/host-versions.json +3 -3
  164. package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
  165. package/templates/api/src/notifications/api.ts +0 -32
  166. package/templates/api/src/notifications/manifest.yaml +0 -5
  167. package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
  168. package/templates/web/src/notifications/__client/api.ts +0 -80
  169. /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * `ojm market verify`(T7):离线验包——不起站点、不装、不要凭据、不要 DB。
3
+ *
4
+ * 分四类列结果(包级与索引级**分开**,因为语义不同):
5
+ * - 完整性:tgz 实际 sha256 与索引声明的 archive.sha256 是否一致;
6
+ * - 包级签名:`<tgz>.sig` 能否用包内公钥复核规范串 `<name>@<version>:<sha256>`——**这是信任锚**;
7
+ * - 索引签名:索引字节与其 `.sig`——只是完整性层(防目录/索引被改),不是信任锚;
8
+ * - manifest 结构:必填字段、内嵌原生清单 desc 是否非空(oj 装配面必填)、同方案各版本 0001 迁移是否逐字节一致。
9
+ *
10
+ * 复用既有原语,一行密码学都不重写:`lifecycle/sign.ts` 的 canonicalString/verifyCanonical、
11
+ * `publish.ts` 的 indexCanonical、`node:crypto` 的 sha256。
12
+ */
13
+
14
+ import type { IndexedPackage } from "./index-builder";
15
+ import { createHash } from "node:crypto";
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import process from "node:process";
19
+
20
+ import { parse as yamlParse } from "yaml";
21
+ import { canonicalString, verifyCanonical } from "../lifecycle/sign";
22
+ import { indexCanonical } from "../publish";
23
+ import { resolveCdnLayout } from "./cdn-layout";
24
+ import { readTgz } from "./scan";
25
+
26
+ export interface VerifyCheck {
27
+ item: string
28
+ ok: boolean
29
+ detail: string
30
+ }
31
+
32
+ export interface VerifyOptions {
33
+ projectRoot?: string
34
+ /** 验指定的 tgz(缺省扫 cdn/ 下全部包) */
35
+ file?: string
36
+ /** 比对扫描重建出的索引与盘上索引,检出漂移 */
37
+ rebuildCheck?: boolean
38
+ }
39
+
40
+ export interface VerifyResult {
41
+ checks: VerifyCheck[]
42
+ passed: boolean
43
+ conclusion: string
44
+ /** `--rebuild-check` 的漂移描述;未传该开关时为 undefined */
45
+ drift?: string[]
46
+ }
47
+
48
+ const BOUNDARY = "完整性 / 来源校验通过——不构成内容安全性背书";
49
+
50
+ export function verifyMarket(opts: VerifyOptions = {}): VerifyResult {
51
+ const projectRoot = opts.projectRoot ?? process.cwd();
52
+ const layout = resolveCdnLayout(projectRoot);
53
+ const checks: VerifyCheck[] = [];
54
+ const drift: string[] = [];
55
+
56
+ const bundlesText = readOptional(path.join(layout.dir, "bundles.yaml"));
57
+ const manifestsText = readOptional(path.join(layout.dir, "manifests.yaml"));
58
+ const declared = declaredHashes(manifestsText);
59
+
60
+ const tgzFiles = opts.file !== undefined
61
+ ? [opts.file]
62
+ : fs.existsSync(layout.dir) ? fs.readdirSync(layout.dir).filter(f => f.endsWith(".tgz")).sort() : [];
63
+
64
+ for (const file of tgzFiles) {
65
+ const tgzPath = path.isAbsolute(file) ? file : path.join(layout.dir, file);
66
+ if (!fs.existsSync(tgzPath)) {
67
+ checks.push({ item: "完整性", ok: false, detail: `${file}:文件不存在` });
68
+ continue;
69
+ }
70
+ const bytes = new Uint8Array(fs.readFileSync(tgzPath));
71
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
72
+ const name = nameOf(file);
73
+ const version = versionOf(file);
74
+
75
+ const want = declared.get(`${name}@${version}`);
76
+ checks.push({
77
+ item: "完整性",
78
+ ok: want === undefined || want === sha256,
79
+ detail: want === undefined
80
+ ? `${file}:索引未声明该版本,只做了摘要计算(${sha256.slice(0, 12)}…)`
81
+ : `${file}:${want === sha256 ? `sha256 与索引一致(${sha256.slice(0, 12)}…)` : `sha256 不一致(索引 ${want.slice(0, 12)}… vs 实际 ${sha256.slice(0, 12)}…)`}`,
82
+ });
83
+
84
+ checks.push(signatureCheck(tgzPath, name, version, sha256));
85
+ checks.push(...structureCheck(bytes, file));
86
+ }
87
+
88
+ checks.push(indexSigCheck(layout.dir, "bundles", bundlesText));
89
+ checks.push(indexSigCheck(layout.dir, "manifests", manifestsText));
90
+ checks.push(...migrationConsistencyCheck(layout.dir, tgzFiles));
91
+
92
+ if (opts.rebuildCheck === true)
93
+ drift.push(...driftOf(layout.dir));
94
+
95
+ const passed = checks.every(c => c.ok) && drift.length === 0;
96
+ return {
97
+ checks,
98
+ passed,
99
+ conclusion: passed
100
+ ? `全部校验通过(${checks.length} 项)。${BOUNDARY}——验签过只说明来源与完整性,不说明代码无害。`
101
+ : `存在未通过项。${BOUNDARY}:未通过项须逐条排查后再发布。`,
102
+ drift: opts.rebuildCheck === true ? drift : undefined,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * `.sig` 里验签只用到 public_key / value 两字段;缺任一即不可复核。
108
+ * 抽成类型守卫:包级与索引级两处验签共用(DRY),也顺带让调用点的布尔式保持单行
109
+ * (本仓 indent-binary-ops 与 no-mixed-spaces-and-tabs 对续行缩进的要求互相打架)。
110
+ */
111
+ function hasSigFields(sig: { public_key?: string, value?: string }): sig is { public_key: string, value: string } {
112
+ return typeof sig.public_key === "string" && typeof sig.value === "string";
113
+ }
114
+
115
+ /** 包级 detached 验签:`<tgz>.sig` 的公钥复核规范串 */
116
+ function signatureCheck(tgzPath: string, name: string, version: string, sha256: string): VerifyCheck {
117
+ const sigPath = `${tgzPath}.sig`;
118
+ if (!fs.existsSync(sigPath))
119
+ return { item: "包级签名", ok: false, detail: `${path.basename(tgzPath)}:缺 .tgz.sig(sig_missing)——用 ojm market publish 重发,或向发布方索取签名包` };
120
+ try {
121
+ const sig = JSON.parse(fs.readFileSync(sigPath, "utf-8")) as { public_key?: string, value?: string };
122
+ const canonical = canonicalString(name, version, sha256);
123
+ const ok = hasSigFields(sig) && verifyCanonical(sig.public_key, canonical, sig.value);
124
+ return {
125
+ item: "包级签名",
126
+ ok,
127
+ detail: ok
128
+ ? `${path.basename(tgzPath)}:Ed25519 验签通过(规范串 ${name}@${version}:${sha256.slice(0, 12)}…)`
129
+ : `${path.basename(tgzPath)}:验签失败——包体被改或签名不匹配`,
130
+ };
131
+ }
132
+ catch (error) {
133
+ return { item: "包级签名", ok: false, detail: `${path.basename(sigPath)} 不可解析:${(error as Error).message}` };
134
+ }
135
+ }
136
+
137
+ /** 索引级验签(完整性层) */
138
+ function indexSigCheck(cdnDir: string, kind: "bundles" | "manifests", text: string | null): VerifyCheck {
139
+ if (text === null)
140
+ return { item: "索引签名", ok: true, detail: `${kind}.yaml 不存在,跳过` };
141
+ const sigPath = path.join(cdnDir, `${kind}.yaml.sig`);
142
+ if (!fs.existsSync(sigPath))
143
+ return { item: "索引签名", ok: true, detail: `${kind}.yaml.sig 缺失——运行期容忍(滚动发布期),完整性无保障` };
144
+ try {
145
+ const sig = JSON.parse(fs.readFileSync(sigPath, "utf-8")) as { public_key?: string, value?: string };
146
+ const ok = hasSigFields(sig) && verifyCanonical(sig.public_key, indexCanonical(kind, text), sig.value);
147
+ return { item: "索引签名", ok, detail: ok ? `${kind}.yaml 验签通过` : `${kind}.yaml 验签失败——索引被改或签名不符` };
148
+ }
149
+ catch (error) {
150
+ return { item: "索引签名", ok: false, detail: `${kind}.yaml.sig 不可解析:${(error as Error).message}` };
151
+ }
152
+ }
153
+
154
+ /** manifest 结构:必填字段 + 内嵌原生清单 desc 非空(等值要求已移除,见 §9.3) */
155
+ function structureCheck(bytes: Uint8Array, file: string): VerifyCheck[] {
156
+ let entries: ReturnType<typeof readTgz>;
157
+ try {
158
+ entries = readTgz(bytes);
159
+ }
160
+ catch (error) {
161
+ return [{ item: "manifest 结构", ok: false, detail: `${file}:包无法解开(${(error as Error).message})` }];
162
+ }
163
+ const root = entries.find(e => /^(?:[^/]+\/)?manifest\.yaml$/.test(e.name));
164
+ if (root === undefined)
165
+ return [{ item: "manifest 结构", ok: false, detail: `${file}:包内缺 manifest.yaml` }];
166
+ const manifest = (yamlParse(root.content.toString("utf-8")) ?? {}) as Record<string, unknown>;
167
+ const missing = ["name", "version", "summary", "peerRuntime", "archive"].filter(k => manifest[k] === undefined);
168
+ const out: VerifyCheck[] = [{
169
+ item: "manifest 结构",
170
+ ok: missing.length === 0,
171
+ detail: missing.length === 0
172
+ ? `${file}:必填字段齐备(name/version/summary/peerRuntime/archive)`
173
+ : `${file}:缺必填字段 ${missing.join(", ")}`,
174
+ }];
175
+
176
+ const inner = entries.find(e => /^api\/[^/]+\/manifest\.yaml$/.test(e.name));
177
+ if (inner !== undefined) {
178
+ const innerManifest = (yamlParse(inner.content.toString("utf-8")) ?? {}) as Record<string, unknown>;
179
+ const desc = innerManifest.desc;
180
+ const ok = typeof desc === "string" && desc.trim() !== "";
181
+ out.push({
182
+ item: "manifest 结构",
183
+ ok,
184
+ detail: ok
185
+ ? `${file}:原生清单 desc 非空(oj 装配面必填)`
186
+ : `${file}:原生清单 desc 缺失或空白——oj 启动装配会失败`,
187
+ });
188
+ }
189
+ return out;
190
+ }
191
+
192
+ /** 同方案各版本的 0001 迁移必须逐字节一致(否则升级链的迁移校验会假失败) */
193
+ function migrationConsistencyCheck(cdnDir: string, files: string[]): VerifyCheck[] {
194
+ const byName = new Map<string, Array<{ version: string, bytes: string }>>();
195
+ for (const file of files) {
196
+ const abs = path.isAbsolute(file) ? file : path.join(cdnDir, file);
197
+ if (!fs.existsSync(abs))
198
+ continue;
199
+ let entries: ReturnType<typeof readTgz>;
200
+ try {
201
+ entries = readTgz(new Uint8Array(fs.readFileSync(abs)));
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ const mig = entries.find(e => /\/migrations\/0001[^/]*\.sql$/.test(e.name));
207
+ if (mig === undefined)
208
+ continue;
209
+ const name = nameOf(file);
210
+ const list = byName.get(name) ?? [];
211
+ list.push({ version: versionOf(file), bytes: createHash("sha256").update(mig.content).digest("hex") });
212
+ byName.set(name, list);
213
+ }
214
+ const out: VerifyCheck[] = [];
215
+ for (const [name, list] of byName) {
216
+ if (list.length < 2)
217
+ continue;
218
+ const first = list[0]!.bytes;
219
+ const bad = list.filter(v => v.bytes !== first).map(v => v.version);
220
+ out.push({
221
+ item: "manifest 结构",
222
+ ok: bad.length === 0,
223
+ detail: bad.length === 0
224
+ ? `${name}:各版本 0001 迁移逐字节一致`
225
+ : `${name}:0001 迁移在版本 ${bad.join(", ")} 与首版不一致——升级链会报迁移缺失,须只增不改`,
226
+ });
227
+ }
228
+ return out;
229
+ }
230
+
231
+ /** 扫描重建出的索引与盘上索引比对,检出「落了包没重建 / 索引被手改」 */
232
+ function driftOf(cdnDir: string): string[] {
233
+ const out: string[] = [];
234
+ const files = fs.readdirSync(cdnDir).filter(f => f.endsWith(".tgz")).sort();
235
+ const scanned: IndexedPackage[] = [];
236
+ for (const file of files) {
237
+ const bytes = new Uint8Array(fs.readFileSync(path.join(cdnDir, file)));
238
+ const entries = readTgz(bytes);
239
+ const root = entries.find(e => /^(?:[^/]+\/)?manifest\.yaml$/.test(e.name));
240
+ if (root === undefined)
241
+ continue;
242
+ const manifest = (yamlParse(root.content.toString("utf-8")) ?? {}) as Record<string, unknown>;
243
+ const name = String(manifest.name ?? "");
244
+ const version = String(manifest.version ?? "");
245
+ if (name === "" || version === "")
246
+ continue;
247
+ scanned.push({ name, version, file, sha256: createHash("sha256").update(bytes).digest("hex"), size: bytes.byteLength, manifest });
248
+ }
249
+ const onDisk = readOptional(path.join(cdnDir, "bundles.yaml"));
250
+ if (onDisk === null) {
251
+ if (scanned.length > 0)
252
+ out.push(`盘上没有 bundles.yaml,但目录里有 ${scanned.length} 个包——索引从未重建`);
253
+ return out;
254
+ }
255
+ const parsed = (yamlParse(onDisk) ?? { solutions: [] }) as { solutions?: Array<Record<string, unknown>> };
256
+ const inIndex = new Set<string>();
257
+ for (const solution of parsed.solutions ?? []) {
258
+ const name = String(solution.name ?? "");
259
+ for (const v of (solution.versions ?? []) as Array<Record<string, unknown>>)
260
+ inIndex.add(`${name}@${String(v.version ?? "")}`);
261
+ }
262
+ for (const pkg of scanned) {
263
+ if (!inIndex.has(`${pkg.name}@${pkg.version}`))
264
+ out.push(`索引缺条目:${pkg.name}@${pkg.version}(${pkg.file})——落了包但没跑 ojm market build-index`);
265
+ }
266
+ return out;
267
+ }
268
+
269
+ function declaredHashes(manifestsText: string | null): Map<string, string> {
270
+ const out = new Map<string, string>();
271
+ if (manifestsText === null)
272
+ return out;
273
+ const list = (yamlParse(manifestsText) ?? []) as Array<Record<string, unknown>>;
274
+ for (const item of Array.isArray(list) ? list : []) {
275
+ const archive = item.archive as Record<string, unknown> | undefined;
276
+ const sha = archive?.sha256;
277
+ if (typeof sha === "string")
278
+ out.set(`${String(item.name ?? "")}@${String(item.version ?? "")}`, sha);
279
+ }
280
+ return out;
281
+ }
282
+
283
+ function readOptional(file: string): string | null {
284
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : null;
285
+ }
286
+
287
+ /** `<name>-<version>.tgz` → name(取最后一个 - 之前,版本段不含 -) */
288
+ function nameOf(file: string): string {
289
+ const base = path.basename(file).replace(/\.tgz$/, "");
290
+ const at = base.lastIndexOf("-");
291
+ return at === -1 ? base : base.slice(0, at);
292
+ }
293
+
294
+ function versionOf(file: string): string {
295
+ const base = path.basename(file).replace(/\.tgz$/, "");
296
+ const at = base.lastIndexOf("-");
297
+ return at === -1 ? "" : base.slice(at + 1);
298
+ }