@oj-module/cli 0.2.69 → 0.2.71

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 (147) hide show
  1. package/devkit/README.md +10 -8
  2. package/devkit/framework-dev.md +1 -1
  3. package/devkit/manual.md +45 -19
  4. package/devkit/module-dev-and-cdn-playground.md +20 -14
  5. package/devkit/publish-walkthrough.md +132 -43
  6. package/devkit/wecom-customer-reauth-guide.md +110 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/{runtime.zrv2zhj9j0.js → runtime.7x99hmm020.js} +2 -2
  9. package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
  10. package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
  11. package/shell-dist/index.html +4 -4
  12. package/shell-dist/versions.json +3 -3
  13. package/src/__tests__/list.test.ts +7 -7
  14. package/src/__tests__/oj-health-host.test.ts +19 -0
  15. package/src/__tests__/site-tenant-header.test.ts +2 -2
  16. package/src/__tests__/templates-builtin-list.test.ts +47 -0
  17. package/src/__tests__/templates-schema-columns.test.ts +11 -2
  18. package/src/__tests__/templates-tenant-scope.test.ts +2 -3
  19. package/src/__tests__/users-roles-display.test.ts +84 -0
  20. package/src/bundle.ts +1 -1
  21. package/src/contract/__tests__/market-client.integration.test.ts +23 -22
  22. package/src/enable.ts +2 -2
  23. package/src/init.ts +45 -0
  24. package/src/install.ts +5 -5
  25. package/src/lifecycle/__tests__/install.test.ts +29 -29
  26. package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
  27. package/src/lifecycle/__tests__/publish.test.ts +8 -10
  28. package/src/lifecycle/__tests__/trust.test.ts +14 -14
  29. package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
  30. package/src/list.ts +3 -3
  31. package/src/market/__tests__/bootstrap.test.ts +24 -7
  32. package/src/market/__tests__/failure-playbook.test.ts +0 -1
  33. package/src/market/__tests__/remote-push.test.ts +228 -0
  34. package/src/market/__tests__/verify.test.ts +45 -207
  35. package/src/market/bootstrap.ts +12 -7
  36. package/src/market/command.ts +33 -35
  37. package/src/market/db-seed.ts +129 -0
  38. package/src/market/failure-playbook.ts +3 -9
  39. package/src/market/fixture.ts +2 -6
  40. package/src/market/publish.ts +145 -89
  41. package/src/market/remote-push.ts +236 -0
  42. package/src/market/trial.ts +2 -2
  43. package/src/market/verify.ts +17 -270
  44. package/src/market/walkthrough.ts +38 -63
  45. package/src/oj.ts +13 -2
  46. package/src/preview.ts +5 -3
  47. package/src/publish.ts +3 -2
  48. package/src/scaffold-guard.ts +4 -0
  49. package/src/site.ts +1 -1
  50. package/src/trust.ts +8 -8
  51. package/src/uninstall.ts +2 -2
  52. package/src/usage.ts +23 -21
  53. package/templates/config.yaml +22 -3
  54. package/templates/init.include.yaml +16 -11
  55. package/templates/openapi.json +3948 -3655
  56. package/templates/src/.ojm-builtin.json +13 -0
  57. package/templates/src/README.md +5 -2
  58. package/templates/src/app/api/_platform/README.md +3 -2
  59. package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
  60. package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
  61. package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
  62. package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
  63. package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
  64. package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
  65. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
  66. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
  67. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
  68. package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
  69. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
  70. package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
  71. package/templates/src/app/api/_platform/manifest.yaml +1 -2
  72. package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
  73. package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
  74. package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
  75. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
  76. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
  77. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
  78. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
  79. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
  80. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
  81. package/templates/src/app/api/_platform/schema.yaml +8 -11
  82. package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
  83. package/templates/src/app/api/_platform/seed.pg.sql +1 -1
  84. package/templates/src/app/api/_platform/seed.sql +1 -1
  85. package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
  86. package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -114
  87. package/templates/src/app/web/_platform/__client/index.ts +1 -333
  88. package/templates/src/app/web/system/locales/en-US.json +15 -1
  89. package/templates/src/app/web/system/locales/zh-CN.json +15 -1
  90. package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
  91. package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
  92. package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
  93. package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
  94. package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
  95. package/templates/src/app/web/system/pages/users/index.tsx +12 -2
  96. package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
  97. package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
  98. package/templates/src/app/web/system/routes.ts +18 -1
  99. package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
  100. package/templates/src/oj-market/api/backfill.ts +221 -0
  101. package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
  102. package/templates/src/oj-market/api/cdn/api.ts +312 -0
  103. package/templates/src/oj-market/api/cdn/shared.ts +104 -0
  104. package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
  105. package/templates/src/oj-market/api/manifest.yaml +8 -0
  106. package/templates/src/oj-market/solution.yaml +21 -0
  107. package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
  108. package/templates/src/oj-market/web/__client/index.ts +509 -0
  109. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
  110. package/templates/src/oj-market/web/api/client.ts +6 -6
  111. package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
  112. package/templates/src/oj-market/web/entry.ts +5 -3
  113. package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
  114. package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
  115. package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
  116. package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
  117. package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
  118. package/templates/src/oj-market/web/routes.ts +12 -0
  119. package/templates/src/oj-market/web/types.ts +14 -8
  120. package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
  121. package/templates/tests/lifecycle/backfill.spec.ts +392 -0
  122. package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
  123. package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
  124. package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
  125. package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
  126. package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
  127. package/templates/tests/lifecycle/installer.spec.ts +36 -0
  128. package/templates/tests/lifecycle/market-api.spec.ts +448 -803
  129. package/templates/tests/lifecycle/preflight.spec.ts +18 -0
  130. package/templates/tests/lifecycle/trust.spec.ts +2 -2
  131. package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
  132. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  133. package/templates/tests/solutions-entry.spec.ts +55 -0
  134. package/templates/tsconfig.paths.json +1 -0
  135. package/templates/web.config.ts +6 -6
  136. package/vendor/host-versions.json +3 -3
  137. package/bin/oj +0 -4
  138. package/src/market/__tests__/build-index.test.ts +0 -641
  139. package/src/market/__tests__/sharding.test.ts +0 -90
  140. package/src/market/__tests__/yank-ledger.test.ts +0 -60
  141. package/src/market/build-index.ts +0 -526
  142. package/src/market/index-builder.ts +0 -149
  143. package/src/market/sharding.ts +0 -94
  144. package/src/market/yank-ledger.ts +0 -91
  145. package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
  146. package/templates/src/app/api/_platform/market/index-source.ts +0 -112
  147. package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
@@ -9,7 +9,8 @@
9
9
  * 私钥只在内存,临时根内无 PEM(站点自己的开发证书走 mintDevCert,属站点身份);
10
10
  * - **凭据**:隔离库是全新 seed,admin 口令即种子默认值,与真站点凭据零交集;
11
11
  * - **信任库**:`.solutions/trusted-keys.yaml` 只在安装器写入时生成,全新根内不存在;
12
- * - **CDN 目录**:临时根内 cdn/,由本模块自建(publish + build-index 直落)。
12
+ * - **CDN 目录**:临时根内 cdn/,由本模块自建(publish 落字节 + db-seed 直写 market_release
13
+ * 目录行——DB 目录化后安装解析直读目录表,索引机制已退役,v4 §五内部通道)。
13
14
  *
14
15
  * 提库来源:`scripts/verify-solution-smoke/bootstrap-host.mjs`(同手法,脚本形态);
15
16
  * 本文件是它的 cli 内嵌版(计划 §9.1:phase1 从这里提库而非重写)。
@@ -31,7 +32,7 @@ import { exportRawPublicKeyHex, fingerprintOfPublicKey } from "../lifecycle/sign
31
32
  import { startOj } from "../oj";
32
33
  import { mintDevCert } from "../oj-cert";
33
34
  import { resolveOjBin } from "../vendor";
34
- import { buildIndex } from "./build-index";
35
+ import { seedMarketRelease } from "./db-seed";
35
36
  import { parseFixtureSpec } from "./fixture";
36
37
  import { publishPackage } from "./publish";
37
38
 
@@ -205,10 +206,9 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
205
206
  const keyId = "trial.publisher";
206
207
  const publisher = derivePublisherKey(randomBytes(16).toString("hex"));
207
208
 
208
- // 6) 隔离 CDN:落 fixture 包 + 重建索引(publish/buildIndex 直落临时根)
209
+ // 6) 隔离 CDN:落 fixture 包字节(publish 内部通道)——目录行在 oj 起站迁移后 seed(步骤 7b)
209
210
  mkdirSync(cdnDir, { recursive: true });
210
- await publishPackage({ projectRoot: root, fixture: opts.fixture, id: keyId, privateKeyPem: publisher.privateKeyPem, registryId: "trial.registry" });
211
- await buildIndex({ projectRoot: root, id: keyId, privateKeyPem: publisher.privateKeyPem });
211
+ const published = await publishPackage({ projectRoot: root, fixture: opts.fixture, id: keyId, privateKeyPem: publisher.privateKeyPem, registryId: "trial.registry" });
212
212
 
213
213
  // 7) 后端构建(dist/api + versions.json;安装与 reloading 都依赖它)
214
214
  await buildBackend(root);
@@ -224,7 +224,11 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
224
224
  rmSync(root, { recursive: true, force: true });
225
225
  throw error;
226
226
  }
227
- log(`[ojm] 隔离站点就绪:http://127.0.0.1:${port}(根 ${root})`);
227
+ // 7c) 目录行 seed(站点内部通道,v4 §五):oj serve 起站已应用迁移——直写 market_release,
228
+ // 安装解析/index 卡片/releases 的目录真源;不走上传签名面(试车夹具不该依赖被测面之外
229
+ // 的签名/信任库/限速)。CLI 是本库唯一写者(隔离场景无并发)。
230
+ seedMarketRelease(dbPath, { tgzPath: published.tgzPath });
231
+ log(`[ojm] 隔离站点就绪:http://127.0.0.1:${port}(根 ${root},目录行已 seed)`);
228
232
 
229
233
  // 8) cleanup:停进程 → 按 keep 决定删不删根 → 摘信号;全程幂等
230
234
  let done = false;
@@ -309,7 +313,8 @@ function teardownReport(root: string, port: number): string[] {
309
313
  " src/ 后端模块源码树(src/<name>/api,拷自 cli 内置模板)",
310
314
  " dist/api/ 构建产物(安装目标)",
311
315
  " bin/ oj 二进制与插件(拷贝自源工程)",
312
- " cdn/ 隔离 CDN(fixture 包 + 两份索引 + 签名)",
316
+ " cdn/ 隔离 CDN(fixture 包 + .sig 旁车)",
317
+ " db.sqlite 含 market_release 目录行(seed 直写)",
313
318
  `监听端口:127.0.0.1:${port}(进程已停)`,
314
319
  `手动清理:rm -rf ${root}`,
315
320
  ];
@@ -1,19 +1,20 @@
1
1
  /**
2
- * `ojm market <sub>` 参数面(T3–T5 的命令入口)。
2
+ * `ojm market <sub>` 参数面(A6:远端发布形态)的命令入口。
3
3
  *
4
- * 只做参数解析 + 调库函数:所有真逻辑都在同目录的 publish / build-index / setup 里,
4
+ * 只做参数解析 + 调库函数:所有真逻辑都在同目录的 remote-push / publish / setup 里,
5
5
  * 这里不掺业务,便于测试直接打函数而不走 argv。
6
+ *
7
+ * 发布缺省 = 远端签名推送(--site-url / OJM_SITE_URL);yank = 远端签名下线(同名发现面)。
8
+ * 索引面(build-index)随 DB 目录化退役——目录真源 = 站点 market_release 表。
6
9
  */
7
10
 
8
11
  import process from "node:process";
9
12
 
10
- import { buildIndex } from "./build-index";
11
- import { publishPackage } from "./publish";
13
+ import { remotePublish, remoteYank } from "./remote-push";
12
14
  import { setupMarketUrl } from "./setup";
13
15
  import { trialCommand } from "./trial";
14
16
  import { verifyMarket } from "./verify";
15
17
  import { walkthroughCommand } from "./walkthrough";
16
- import { markYanked } from "./yank-ledger";
17
18
 
18
19
  interface Flags {
19
20
  id?: string
@@ -25,9 +26,10 @@ interface Flags {
25
26
  pass?: string
26
27
  newPass?: string
27
28
  registryId?: string
28
- rebuildIndex?: boolean
29
29
  check?: boolean
30
- /** yank:操作人(审计留痕;缺省 "local-cli") */
30
+ /** yank:恢复(unyank);缺省 = 下线 */
31
+ restore?: boolean
32
+ /** yank:操作人(审计留痕;缺省由站点记 key_id) */
31
33
  by?: string
32
34
  /** yank:下线原因(审计留痕;可选) */
33
35
  reason?: string
@@ -39,8 +41,8 @@ function parseFlags(argv: string[]): Flags {
39
41
  const flags: Flags = {};
40
42
  for (let i = 0; i < argv.length; i++) {
41
43
  const arg = argv[i]!;
42
- if (arg === "--rebuild-index") {
43
- flags.rebuildIndex = true;
44
+ if (arg === "--restore") {
45
+ flags.restore = true;
44
46
  continue;
45
47
  }
46
48
  if (arg === "--check") {
@@ -84,45 +86,45 @@ export async function marketCommand(sub: string, argv: string[]): Promise<void>
84
86
  // 位置参数:<name>@<version>(非 -- 开头的首个 token)
85
87
  const target = argv.find(a => !a.startsWith("--"));
86
88
  if (target === undefined || !target.includes("@"))
87
- throw new Error("[ojm] 用法:ojm market yank <name>@<version> [--by <操作人>] [--reason <原因>]");
89
+ throw new Error("[ojm] 用法:ojm market yank <name>@<version> --site-url <url> --id <签名身份> [--key <pem>] [--reason <原因>] [--by <操作人>] [--restore]");
88
90
  const at = target.indexOf("@");
89
91
  const name = target.slice(0, at);
90
92
  const version = target.slice(at + 1);
91
- const by = flags.by ?? "local-cli";
92
- // 重建索引须给签名身份——先校验再落账,避免「账本已记 yank 但索引未重建」的半成品态
93
- if (flags.id === undefined)
94
- throw new Error("[ojm] yank 须给定 --id <签名身份> 才能重建索引生效——请补 --id 后重试,已下线条目将随即写进分片正文(REG-CS4)");
95
- const r = markYanked(process.cwd(), name, version, { by, reason: flags.reason });
96
- if (r.created)
97
- console.log(`[ojm] 已下线 ${name}@${version}(操作人 ${by}${flags.reason !== undefined ? `,原因:${flags.reason}` : ""})——重建索引以生效`);
98
- else
99
- console.log(`[ojm] ${name}@${version} 此前已下线,幂等跳过`);
100
- // 重建索引让 yanked 标记进分片正文(REG-CS4:latest 跳过 + 安装侧精确版本拒装)
101
- await buildIndex({ id: flags.id, key: flags.key, check: flags.check });
93
+ // 签名下线:DB 目录行级(清单即摘除、详情保留 yanked 标记);不动已装方案。
94
+ // --id 语义 = 签名身份(须为该行发布者,服务端 403 仲裁)——索引重建约束随索引退役消失
95
+ const r = await remoteYank({
96
+ siteUrl: flags.siteUrl,
97
+ name,
98
+ version,
99
+ restore: flags.restore === true,
100
+ reason: flags.reason,
101
+ by: flags.by,
102
+ id: flags.id,
103
+ key: flags.key,
104
+ });
105
+ console.log(flags.restore === true
106
+ ? (r.idempotent ? `[ojm] ${name}@${version} 此前已恢复,幂等跳过` : `[ojm] 已恢复 ${name}@${version}(重新在架)`)
107
+ : (r.idempotent ? `[ojm] ${name}@${version} 此前已下线,幂等跳过` : `[ojm] 已下线 ${name}@${version}——清单已摘除,新装/升级被阻断(已装方案不受影响)`));
102
108
  return;
103
109
  }
104
110
  case "publish": {
105
- await publishPackage({
111
+ // 缺省远端签名推送(v4 §五:本地-only 落盘模式退役——DB 模式下本地落盘 = 市场不可见);
112
+ // 本地落盘仅内部通道保留(walkthrough/trial 隔离站点双写用 publishPackage)
113
+ await remotePublish({
114
+ siteUrl: flags.siteUrl,
106
115
  fixture: flags.fixture,
107
116
  real: flags.real,
108
117
  id: flags.id,
109
118
  key: flags.key,
110
119
  check: flags.check,
111
120
  registryId: flags.registryId,
112
- rebuildIndex: flags.rebuildIndex === true,
113
121
  });
114
- if (flags.rebuildIndex === true)
115
- await buildIndex({ id: flags.id, key: flags.key, check: flags.check });
116
122
  return;
117
123
  }
118
124
  case "verify": {
119
- const result = verifyMarket({ file: flags.fixture === undefined ? undefined : flags.fixture, rebuildCheck: flags.rebuildIndex });
125
+ const result = verifyMarket({ file: flags.fixture === undefined ? undefined : flags.fixture });
120
126
  for (const check of result.checks)
121
127
  console.log(`${check.ok ? "✓" : "✗"} ${check.item}:${check.detail}`);
122
- if (result.drift !== undefined && result.drift.length > 0) {
123
- for (const line of result.drift)
124
- console.log(`✗ 索引漂移:${line}`);
125
- }
126
128
  console.log(result.conclusion);
127
129
  if (!result.passed)
128
130
  process.exitCode = 1;
@@ -136,16 +138,12 @@ export async function marketCommand(sub: string, argv: string[]): Promise<void>
136
138
  await walkthroughCommand(argv);
137
139
  return;
138
140
  }
139
- case "build-index": {
140
- await buildIndex({ id: flags.id, key: flags.key, check: flags.check });
141
- return;
142
- }
143
141
  case "setup": {
144
142
  const result = await setupMarketUrl({ siteUrl: flags.siteUrl, user: flags.user, pass: flags.pass, newPass: flags.newPass });
145
143
  console.log(`[ojm] 站点 ${result.siteUrl} 的市场地址已指向 ${result.cdnBaseUrl}`);
146
144
  return;
147
145
  }
148
146
  default:
149
- throw new Error(`[ojm] 未知子命令:ojm market ${sub}\n可用:setup / publish / yank / build-index / verify / trial / walkthrough`);
147
+ throw new Error(`[ojm] 未知子命令:ojm market ${sub}\n可用:setup / publish / yank / verify / trial / walkthrough`);
150
148
  }
151
149
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * market_release 直写 seed(A6,计划 v4 §五「站点内部通道,不走上传签名面」):
3
+ * `ojm market trial` / `walkthrough` 的隔离站点目录来源。
4
+ *
5
+ * 为什么直写 DB:DB 目录化后安装解析/index 卡片/releases 全部直读 market_release 表——
6
+ * 隔离站点要把 fixture 变成「可装」,必须让目录行存在;上传签名面是给发布者的远端通道,
7
+ * 试车夹具不该依赖它(签名/信任库/限速全是被测面之外的噪音)。oj serve 起站时迁移已
8
+ * 应用(migrate_on_start),本模块在其后单写——隔离场景 CLI 是唯一写者,无并发。
9
+ *
10
+ * 行内容与上传端点落库口径同源:sha256/size 实重算、签名四列取 .sig 旁车、manifest 取
11
+ * 包内嵌抽取物(clamp 口径照抄服务端钳制——catalog 展示字段 ≤200/≤2000,全量 manifest
12
+ * 仍在包内)。
13
+ */
14
+
15
+ import { createHash } from "node:crypto";
16
+ import { readFileSync } from "node:fs";
17
+ import { DatabaseSync } from "node:sqlite";
18
+
19
+ import { parse as yamlParse } from "yaml";
20
+ import { readTgz } from "./scan";
21
+
22
+ /** .sig 旁车六字段(与契约 C2-35 同构) */
23
+ interface SidecarSig {
24
+ alg: string
25
+ key_id: string
26
+ public_key: string
27
+ value: string
28
+ canon: string
29
+ registry_id: string
30
+ }
31
+
32
+ function parseSidecar(tgzPath: string): SidecarSig {
33
+ const sig = JSON.parse(readFileSync(`${tgzPath}.sig`, "utf-8")) as Partial<SidecarSig>;
34
+ const missing = (["alg", "key_id", "public_key", "value", "registry_id"] as const).filter(k => typeof sig[k] !== "string");
35
+ if (missing.length > 0)
36
+ throw new Error(`[ojm] seed 目录行失败:${tgzPath}.sig 旁车字段不全(须 alg/key_id/public_key/value/registry_id;缺 ${missing.join("/")})`);
37
+ return sig as SidecarSig;
38
+ }
39
+
40
+ /** 包内嵌根 manifest 抽取物(坏包 fail-closed:试车夹具不该有坏包) */
41
+ function manifestOf(tgzPath: string): Record<string, unknown> {
42
+ const entries = readTgz(new Uint8Array(readFileSync(tgzPath)));
43
+ const root = entries.find(e => /^(?:[^/]+\/)?manifest\.yaml$/.test(e.name));
44
+ if (root === undefined)
45
+ throw new Error(`[ojm] seed 目录行失败:${tgzPath} 包内缺 manifest.yaml`);
46
+ return (yamlParse(root.content.toString("utf-8")) ?? {}) as Record<string, unknown>;
47
+ }
48
+
49
+ /** 入库钳制(与上传端点 clampManifest 同源口径:catalog 展示字段截断,全量 manifest 仍在包内) */
50
+ function clampManifest(m: Record<string, unknown>): Record<string, unknown> {
51
+ const out = { ...m };
52
+ if (typeof out.summary === "string" && out.summary.length > 200)
53
+ out.summary = out.summary.slice(0, 200);
54
+ if (typeof out.description === "string" && out.description.length > 2000)
55
+ out.description = out.description.slice(0, 2000);
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * upsert 一行目录(trial/walkthrough 发布 fixture 的内部通道)。
61
+ * 同 product@version 重 seed = 全列覆盖(试车语义:最后一次 seed 为准)。
62
+ */
63
+ export function seedMarketRelease(dbPath: string, opts: { tgzPath: string, now?: number }): void {
64
+ const tgzPath = opts.tgzPath;
65
+ const at = opts.now ?? Math.floor(Date.now() / 1000);
66
+ const bytes = new Uint8Array(readFileSync(tgzPath));
67
+ const sig = parseSidecar(tgzPath);
68
+ const manifest = clampManifest(manifestOf(tgzPath));
69
+ if (typeof manifest.name !== "string" || manifest.name === "")
70
+ throw new Error(`[ojm] seed 目录行失败:${tgzPath} 包内 manifest 缺 name`);
71
+ if (typeof manifest.version !== "string" || manifest.version === "")
72
+ throw new Error(`[ojm] seed 目录行失败:${tgzPath} 包内 manifest 缺 version`);
73
+ const db = new DatabaseSync(dbPath);
74
+ try {
75
+ db.prepare(
76
+ `INSERT INTO market_release (product,version,key_id,sha256,size,published_at,published_at_source,yanked,first_seen_at,sig_alg,sig_value,sig_public_key,sig_registry_id,manifest)
77
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
78
+ ON CONFLICT(product,version) DO UPDATE SET
79
+ key_id=excluded.key_id, sha256=excluded.sha256, size=excluded.size,
80
+ published_at=excluded.published_at, published_at_source=excluded.published_at_source,
81
+ yanked=excluded.yanked, sig_alg=excluded.sig_alg, sig_value=excluded.sig_value,
82
+ sig_public_key=excluded.sig_public_key, sig_registry_id=excluded.sig_registry_id,
83
+ manifest=excluded.manifest`,
84
+ ).run(
85
+ manifest.name,
86
+ manifest.version,
87
+ sig.key_id,
88
+ createHash("sha256").update(bytes).digest("hex"),
89
+ bytes.byteLength,
90
+ at,
91
+ "upload",
92
+ 0,
93
+ at,
94
+ sig.alg,
95
+ sig.value,
96
+ sig.public_key,
97
+ sig.registry_id,
98
+ JSON.stringify(manifest),
99
+ );
100
+ }
101
+ finally {
102
+ db.close();
103
+ }
104
+ }
105
+
106
+ /** 摘除一行目录 + 其字节(neg op 收尾;walkthrough 恢复主链路原状用) */
107
+ export function removeMarketRelease(dbPath: string, name: string, version: string): void {
108
+ const db = new DatabaseSync(dbPath);
109
+ try {
110
+ db.prepare("DELETE FROM market_release WHERE product = ? AND version = ?").run(name, version);
111
+ }
112
+ finally {
113
+ db.close();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 行签名列置 NULL(遗留目录行形态——neg-nosig:锚定「无签名行拒装」的 fail-closed 语义;
119
+ * 上传通道恒写四列,唯有 seed/遗留路径能造出 NULL 行)。
120
+ */
121
+ export function nullReleaseSig(dbPath: string, name: string, version: string): void {
122
+ const db = new DatabaseSync(dbPath);
123
+ try {
124
+ db.prepare("UPDATE market_release SET sig_alg = NULL, sig_value = NULL, sig_public_key = NULL, sig_registry_id = NULL WHERE product = ? AND version = ?").run(name, version);
125
+ }
126
+ finally {
127
+ db.close();
128
+ }
129
+ }
@@ -21,13 +21,13 @@ export const FAILURE_PLAYBOOK: FailurePlaybookEntry[] = [
21
21
  {
22
22
  code: "sig_missing",
23
23
  stage: "verifying",
24
- cause: "分发包未签名:发布方未做 detached 签名,或 CDN 目录里 .sig 被裁剪",
25
- next: "请发布方重发 `ojm market publish --check` 复核签名后重建索引",
24
+ cause: "分发包未签名:发布方未做 detached 签名,或目录行无签名列(遗留行未经回填)",
25
+ next: "请发布方重发 `ojm market publish --check` 复核签名;遗留行跑站点扫描回填补齐",
26
26
  },
27
27
  {
28
28
  code: "tampered",
29
29
  stage: "verifying",
30
- cause: "包体或签名与索引不符:分发包被篡改,或同版本号被换包重发",
30
+ cause: "包体与目录行 sha256 不符:分发包被篡改,或同版本号被换包重发",
31
31
  next: "从可信源重新获取分发包;发布方应升版本号重发(同版本号只允许一份字节)",
32
32
  },
33
33
  {
@@ -42,12 +42,6 @@ export const FAILURE_PLAYBOOK: FailurePlaybookEntry[] = [
42
42
  cause: "包内迁移最大 seq 低于账本 head:禁降版部署(OJ-9 红线),库保持原状",
43
43
  next: "正道是升级到新版本;确需回退请联系运维走对账自愈,勿强行降版",
44
44
  },
45
- {
46
- code: "index_sig",
47
- stage: "安装前(索引解析)",
48
- cause: "市场索引签名校验失败:catalog.yaml / shards/*.yaml 被篡改或签名与字节不符",
49
- next: "重建索引并签名 `ojm market build-index`;同时核对 CDN 目录写入来源",
50
- },
51
45
  {
52
46
  code: "zip_slip",
53
47
  stage: "安装前(上传校验)",
@@ -126,12 +126,8 @@ function cssClassOf(name: string, version: string): string {
126
126
  return `${name}-v${version.replace(/[^0-9A-Z]/gi, "-")}`;
127
127
  }
128
128
 
129
- /**
130
- * 方案清单(Form A:archive 只写 file,sha256/size 由索引侧填 Form B)。
131
- * 导出给 build-index 的体积实测用:需求 §7 估「单条 Form B ≈535–684B」偏乐观——本形态
132
- * 实测 945B YAML、进索引 ≈1.2KB/条,SHARD_MAX_VERSIONS 的 1000 校准即以它为据。
133
- */
134
- export function solutionManifest(name: string, version: string): Record<string, unknown> {
129
+ /** fixture 包的根 manifest(Form A:archive 只写 file,sha256/size 由服务端组表填 Form B) */
130
+ function solutionManifest(name: string, version: string): Record<string, unknown> {
135
131
  return {
136
132
  name,
137
133
  version,