@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.
- package/devkit/README.md +10 -8
- package/devkit/framework-dev.md +1 -1
- package/devkit/manual.md +45 -19
- package/devkit/module-dev-and-cdn-playground.md +20 -14
- package/devkit/publish-walkthrough.md +132 -43
- package/devkit/wecom-customer-reauth-guide.md +110 -0
- package/package.json +2 -2
- package/shell-dist/assets/{runtime.zrv2zhj9j0.js → runtime.7x99hmm020.js} +2 -2
- package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
- package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
- package/shell-dist/index.html +4 -4
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/list.test.ts +7 -7
- package/src/__tests__/oj-health-host.test.ts +19 -0
- package/src/__tests__/site-tenant-header.test.ts +2 -2
- package/src/__tests__/templates-builtin-list.test.ts +47 -0
- package/src/__tests__/templates-schema-columns.test.ts +11 -2
- package/src/__tests__/templates-tenant-scope.test.ts +2 -3
- package/src/__tests__/users-roles-display.test.ts +84 -0
- package/src/bundle.ts +1 -1
- package/src/contract/__tests__/market-client.integration.test.ts +23 -22
- package/src/enable.ts +2 -2
- package/src/init.ts +45 -0
- package/src/install.ts +5 -5
- package/src/lifecycle/__tests__/install.test.ts +29 -29
- package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
- package/src/lifecycle/__tests__/publish.test.ts +8 -10
- package/src/lifecycle/__tests__/trust.test.ts +14 -14
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/list.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -7
- package/src/market/__tests__/failure-playbook.test.ts +0 -1
- package/src/market/__tests__/remote-push.test.ts +228 -0
- package/src/market/__tests__/verify.test.ts +45 -207
- package/src/market/bootstrap.ts +12 -7
- package/src/market/command.ts +33 -35
- package/src/market/db-seed.ts +129 -0
- package/src/market/failure-playbook.ts +3 -9
- package/src/market/fixture.ts +2 -6
- package/src/market/publish.ts +145 -89
- package/src/market/remote-push.ts +236 -0
- package/src/market/trial.ts +2 -2
- package/src/market/verify.ts +17 -270
- package/src/market/walkthrough.ts +38 -63
- package/src/oj.ts +13 -2
- package/src/preview.ts +5 -3
- package/src/publish.ts +3 -2
- package/src/scaffold-guard.ts +4 -0
- package/src/site.ts +1 -1
- package/src/trust.ts +8 -8
- package/src/uninstall.ts +2 -2
- package/src/usage.ts +23 -21
- package/templates/config.yaml +22 -3
- package/templates/init.include.yaml +16 -11
- package/templates/openapi.json +3948 -3655
- package/templates/src/.ojm-builtin.json +13 -0
- package/templates/src/README.md +5 -2
- package/templates/src/app/api/_platform/README.md +3 -2
- package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
- package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
- package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
- package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
- package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
- package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
- package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
- package/templates/src/app/api/_platform/manifest.yaml +1 -2
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +8 -11
- package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
- package/templates/src/app/api/_platform/seed.pg.sql +1 -1
- package/templates/src/app/api/_platform/seed.sql +1 -1
- package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
- package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -114
- package/templates/src/app/web/_platform/__client/index.ts +1 -333
- package/templates/src/app/web/system/locales/en-US.json +15 -1
- package/templates/src/app/web/system/locales/zh-CN.json +15 -1
- package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
- package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
- package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
- package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
- package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
- package/templates/src/app/web/system/pages/users/index.tsx +12 -2
- package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
- package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
- package/templates/src/app/web/system/routes.ts +18 -1
- package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
- package/templates/src/oj-market/api/backfill.ts +221 -0
- package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
- package/templates/src/oj-market/api/cdn/api.ts +312 -0
- package/templates/src/oj-market/api/cdn/shared.ts +104 -0
- package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
- package/templates/src/oj-market/api/manifest.yaml +8 -0
- package/templates/src/oj-market/solution.yaml +21 -0
- package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
- package/templates/src/oj-market/web/__client/index.ts +509 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
- package/templates/src/oj-market/web/api/client.ts +6 -6
- package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
- package/templates/src/oj-market/web/entry.ts +5 -3
- package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
- package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
- package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
- package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
- package/templates/src/oj-market/web/routes.ts +12 -0
- package/templates/src/oj-market/web/types.ts +14 -8
- package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
- package/templates/tests/lifecycle/backfill.spec.ts +392 -0
- package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
- package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
- package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
- package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
- package/templates/tests/lifecycle/installer.spec.ts +36 -0
- package/templates/tests/lifecycle/market-api.spec.ts +448 -803
- package/templates/tests/lifecycle/preflight.spec.ts +18 -0
- package/templates/tests/lifecycle/trust.spec.ts +2 -2
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/solutions-entry.spec.ts +55 -0
- package/templates/tsconfig.paths.json +1 -0
- package/templates/web.config.ts +6 -6
- package/vendor/host-versions.json +3 -3
- package/bin/oj +0 -4
- package/src/market/__tests__/build-index.test.ts +0 -641
- package/src/market/__tests__/sharding.test.ts +0 -90
- package/src/market/__tests__/yank-ledger.test.ts +0 -60
- package/src/market/build-index.ts +0 -526
- package/src/market/index-builder.ts +0 -149
- package/src/market/sharding.ts +0 -94
- package/src/market/yank-ledger.ts +0 -91
- package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
- package/templates/src/app/api/_platform/market/index-source.ts +0 -112
- package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
package/src/market/bootstrap.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 私钥只在内存,临时根内无 PEM(站点自己的开发证书走 mintDevCert,属站点身份);
|
|
10
10
|
* - **凭据**:隔离库是全新 seed,admin 口令即种子默认值,与真站点凭据零交集;
|
|
11
11
|
* - **信任库**:`.solutions/trusted-keys.yaml` 只在安装器写入时生成,全新根内不存在;
|
|
12
|
-
* - **CDN 目录**:临时根内 cdn/,由本模块自建(publish +
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
];
|
package/src/market/command.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ojm market <sub>` 参数面(
|
|
2
|
+
* `ojm market <sub>` 参数面(A6:远端发布形态)的命令入口。
|
|
3
3
|
*
|
|
4
|
-
* 只做参数解析 + 调库函数:所有真逻辑都在同目录的
|
|
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 {
|
|
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
|
|
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 === "--
|
|
43
|
-
flags.
|
|
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> [--
|
|
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
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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 /
|
|
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
|
|
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: "安装前(上传校验)",
|
package/src/market/fixture.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|