@oj-module/cli 0.2.68 → 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.1xzahckt10.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/global.d.ts +6 -4
- package/templates/init.include.yaml +16 -11
- package/templates/openapi.json +3971 -3663
- 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/service/domain/users.ts +7 -1
- 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/api/system/users/api.ts +3 -0
- package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -113
- package/templates/src/app/web/_platform/__client/index.ts +1 -333
- package/templates/src/app/web/system/__client/api.schemas.ts +2 -0
- 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/rbac-users.spec.ts +8 -0
- package/templates/tests/solutions-entry.spec.ts +55 -0
- package/templates/tsconfig.paths.json +1 -0
- package/templates/types/stream-probes/stream-handlers.ts +34 -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/publish.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 市场发布包构建(A6 重构):buildSignedPackage = 合成/构建 + 确定性 tgz + 包级 detached 签名,
|
|
3
|
+
* 产出**内存字节**(不落 CDN)——本地落盘(publishPackage)与远端推送(remote-push.ts)
|
|
4
|
+
* 共用同一构建面(DRY:签名/打包一行不重复)。
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 没有共享可变索引文件,也就没有 TOCTOU。
|
|
6
|
+
* 本地落盘模式(publishPackage)仍保留:walkthrough/trial 的隔离站点需要 cdn/ 字节 +
|
|
7
|
+
* DB 目录行双写(内部通道);CLI 命令层 `ojm market publish` 缺省走远端推送。
|
|
7
8
|
*
|
|
8
|
-
* 签名原语一行都不重写:11 复用 `ojm sign` 的 `signCommand`(规范串 `<name>@<version>:<sha256>`,
|
|
9
|
-
* detached `.tgz.sig`
|
|
9
|
+
* 签名原语一行都不重写:11 复用 `ojm sign` 的 `signCommand`(规范串 `<registry_id>:<name>@<version>:<sha256>`,
|
|
10
|
+
* detached `.tgz.sig` 六字段,与 Form B archive.signature 同源同构)。
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { createHash } from "node:crypto";
|
|
@@ -17,6 +18,8 @@ import process from "node:process";
|
|
|
17
18
|
|
|
18
19
|
import { buildBundle } from "../bundle";
|
|
19
20
|
import { createDeterministicTgz } from "../deterministic-tar";
|
|
21
|
+
import { isValidKeyId } from "../keygen";
|
|
22
|
+
import { readPrivateKeyPem } from "../lifecycle/keygen";
|
|
20
23
|
import { assertDefaultModulesUntouched } from "../scaffold-guard";
|
|
21
24
|
import { signCommand } from "../sign";
|
|
22
25
|
import { resolveCdnLayout } from "./cdn-layout";
|
|
@@ -25,7 +28,7 @@ import { buildFixturePackage, parseFixtureSpec } from "./fixture";
|
|
|
25
28
|
export interface MarketPublishOptions {
|
|
26
29
|
/** 站点工程根(含 api/config.yaml 与 cdn/);缺省 process.cwd() */
|
|
27
30
|
projectRoot?: string
|
|
28
|
-
/** `--fixture <name
|
|
31
|
+
/** `--fixture <name@version>`;缺省 demo-solution@0.1.0 */
|
|
29
32
|
fixture?: string
|
|
30
33
|
/** `--real <方案工程根>`:走 ojm build --bundle 真构建(替代 fixture) */
|
|
31
34
|
real?: string
|
|
@@ -35,77 +38,97 @@ export interface MarketPublishOptions {
|
|
|
35
38
|
key?: string
|
|
36
39
|
/** 内存私钥 PEM(一次性/隔离场景,不落盘) */
|
|
37
40
|
privateKeyPem?: string
|
|
38
|
-
/** 落包后离线自检(verifyCanonical 复核包级签名) */
|
|
39
|
-
check?: boolean
|
|
40
41
|
/** 签发方 registry 身份(REG-CS3 v2 防跨仓库重放;透传 signCommand --registry-id) */
|
|
41
42
|
registryId?: string
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/** 落包后离线自检(verifyCanonical 复核包级签名) */
|
|
44
|
+
check?: boolean
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
/** 已签名发布包(内存字节态;落盘/推送由调用方决定) */
|
|
48
|
+
export interface SignedPackage {
|
|
47
49
|
name: string
|
|
48
50
|
version: string
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
tgzBytes: Uint8Array
|
|
52
|
+
/** tgz 字节 sha256(内容寻址;上传 canonical 与 Form B archive.sha256 同源) */
|
|
51
53
|
sha256: string
|
|
52
54
|
size: number
|
|
53
|
-
|
|
55
|
+
/** 包级 detached 签名(六字段,与 .tgz.sig 旁车文件内容一致) */
|
|
56
|
+
sig: Record<string, unknown>
|
|
57
|
+
sigBytes: Uint8Array
|
|
54
58
|
keyId: string
|
|
55
59
|
publicKeyHex: string
|
|
56
60
|
fingerprint: string
|
|
61
|
+
/** 包级 canonical(`<registry_id>:<name>@<version>:<sha256>`) */
|
|
62
|
+
canonical: string
|
|
63
|
+
/** 本次签名用私钥(远端推送构造上传 canonical 要用;内存态不落盘纪律不变) */
|
|
64
|
+
privateKeyPem: string
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
/**
|
|
68
|
+
* 解析签名身份(内存 PEM > --key 显式文件 > ~/.ojm/keys/<id>/private.pem)。
|
|
69
|
+
* 与 sign.ts signCommand 同源规则;独立抽出是因为推送路径不经 signCommand 也要签
|
|
70
|
+
* 上传 canonical(`cdn-upload:<站点 registry>:<file>:<sha256>`)——同一身份两把签名。
|
|
71
|
+
*/
|
|
72
|
+
export function resolveSigningIdentity(opts: { id?: string, key?: string, privateKeyPem?: string, cwd?: string }): { keyId: string, privateKeyPem: string } {
|
|
73
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
74
|
+
if (opts.privateKeyPem !== undefined && opts.privateKeyPem !== "") {
|
|
75
|
+
const keyId = opts.id ?? "";
|
|
76
|
+
if (keyId === "")
|
|
77
|
+
throw new Error("[ojm] 使用内存私钥时须同时给 --id <publisher.id>(.sig 的发布方标识;C2-37 命名空间)");
|
|
78
|
+
if (!isValidKeyId(keyId))
|
|
79
|
+
throw new Error(`[ojm] key_id 命名空间非法(C2-37):「${keyId}」`);
|
|
80
|
+
return { keyId, privateKeyPem: opts.privateKeyPem };
|
|
81
|
+
}
|
|
82
|
+
let keyFile: string;
|
|
83
|
+
let keyId: string;
|
|
84
|
+
if (opts.key !== undefined) {
|
|
85
|
+
keyFile = path.resolve(cwd, opts.key);
|
|
86
|
+
const m = /[/\\]\.ojm[/\\]keys[/\\]([^/\\]+)[/\\]private\.pem$/.exec(keyFile);
|
|
87
|
+
keyId = opts.id ?? m?.[1] ?? "";
|
|
88
|
+
if (keyId === "")
|
|
89
|
+
throw new Error("[ojm] --key 指向自定义位置时须同时给 --id <publisher.id>(.sig 的发布方标识;C2-37 命名空间)");
|
|
65
90
|
}
|
|
91
|
+
else {
|
|
92
|
+
keyId = opts.id ?? "";
|
|
93
|
+
if (keyId === "")
|
|
94
|
+
throw new Error("[ojm] 缺少签名身份:--id <publisher.id>(从 ~/.ojm/keys/<id>/ 取私钥)或 --key <pem> --id <publisher.id>。先生成:ojm keygen --id <publisher.id>");
|
|
95
|
+
keyFile = path.join(os.homedir(), ".ojm", "keys", keyId, "private.pem");
|
|
96
|
+
}
|
|
97
|
+
if (!isValidKeyId(keyId))
|
|
98
|
+
throw new Error(`[ojm] key_id 命名空间非法(C2-37):「${keyId}」`);
|
|
99
|
+
if (!fs.existsSync(keyFile))
|
|
100
|
+
throw new Error(`[ojm] 私钥文件不存在:${keyFile}(用 ojm keygen --id ${keyId} 生成,或 --key 指定)`);
|
|
101
|
+
return { keyId, privateKeyPem: readPrivateKeyPem(keyFile) };
|
|
102
|
+
}
|
|
66
103
|
|
|
67
|
-
|
|
104
|
+
/** 构建 + 包级签名(staging 内完成,staging 自清理;产出内存字节) */
|
|
105
|
+
export async function buildSignedPackage(opts: MarketPublishOptions = {}): Promise<SignedPackage> {
|
|
106
|
+
const staging = mkdtempSync(path.join(os.tmpdir(), "ojm-market-build-"));
|
|
68
107
|
try {
|
|
69
|
-
const {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// 原子性:tgz 已落盘而后续环节失败时,新包必须捡回来,不留半成品
|
|
74
|
-
const rollbackPackage = () => {
|
|
75
|
-
if (!preExisted)
|
|
76
|
-
rmSync(tgzPath, { force: true });
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
let signed: ReturnType<typeof signCommand>;
|
|
80
|
-
try {
|
|
81
|
-
signed = signCommand(tgzPath, { id: opts.id, key: opts.key, privateKeyPem: opts.privateKeyPem, check: opts.check, registryId: opts.registryId });
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
rollbackPackage();
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
if (signed.checkPassed === false) {
|
|
88
|
-
rollbackPackage();
|
|
89
|
-
throw new Error(`[ojm] 包级签名自检失败,已移除未签通的包:${tgzPath}`);
|
|
90
|
-
}
|
|
91
|
-
const size = fs.statSync(tgzPath).size;
|
|
92
|
-
console.log(`[ojm] 包已落 CDN → ${path.relative(projectRoot, tgzPath)}(${size} 字节)`);
|
|
93
|
-
console.log(`[ojm] 发布方 ${signed.keyId};指纹 ${signed.fingerprint}`);
|
|
94
|
-
// 带了 --rebuild-index 时由命令层紧接着重建索引,这里不能再报「未重建」(否则前后两句自相矛盾)
|
|
95
|
-
if (opts.rebuildIndex !== true)
|
|
96
|
-
console.log("[ojm] 索引未重建:多人场景请由维护者跑 ojm market build-index");
|
|
108
|
+
const identity = resolveSigningIdentity({ id: opts.id, key: opts.key, privateKeyPem: opts.privateKeyPem, cwd: opts.projectRoot });
|
|
109
|
+
const { name, version, tgzPath, sha256 } = opts.real !== undefined
|
|
110
|
+
? await buildRealPackage(opts.real, staging)
|
|
111
|
+
: await buildFixture({ fixture: opts.fixture, staging });
|
|
97
112
|
|
|
113
|
+
const signed = signCommand(tgzPath, { id: identity.keyId, privateKeyPem: identity.privateKeyPem, check: opts.check, registryId: opts.registryId });
|
|
114
|
+
if (signed.checkPassed === false)
|
|
115
|
+
throw new Error("[ojm] 包级签名自检失败,已丢弃未签通的包");
|
|
116
|
+
const tgzBytes = new Uint8Array(fs.readFileSync(tgzPath));
|
|
117
|
+
const sigBytes = new Uint8Array(fs.readFileSync(signed.sigPath));
|
|
118
|
+
const sig = JSON.parse(new TextDecoder().decode(sigBytes)) as Record<string, unknown>;
|
|
98
119
|
return {
|
|
99
120
|
name,
|
|
100
121
|
version,
|
|
101
|
-
|
|
102
|
-
sigPath: signed.sigPath,
|
|
122
|
+
tgzBytes,
|
|
103
123
|
sha256,
|
|
104
|
-
size,
|
|
105
|
-
|
|
124
|
+
size: tgzBytes.byteLength,
|
|
125
|
+
sig,
|
|
126
|
+
sigBytes,
|
|
106
127
|
keyId: signed.keyId,
|
|
107
128
|
publicKeyHex: signed.publicKeyHex,
|
|
108
129
|
fingerprint: signed.fingerprint,
|
|
130
|
+
canonical: signed.canonical,
|
|
131
|
+
privateKeyPem: identity.privateKeyPem,
|
|
109
132
|
};
|
|
110
133
|
}
|
|
111
134
|
finally {
|
|
@@ -113,61 +136,94 @@ export async function publishPackage(opts: MarketPublishOptions = {}): Promise<M
|
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
export interface MarketPublishResult {
|
|
140
|
+
name: string
|
|
141
|
+
version: string
|
|
142
|
+
tgzPath: string
|
|
143
|
+
sigPath: string
|
|
144
|
+
sha256: string
|
|
145
|
+
size: number
|
|
146
|
+
canonical: string
|
|
147
|
+
keyId: string
|
|
148
|
+
publicKeyHex: string
|
|
149
|
+
fingerprint: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 本地落盘模式:把已签名包写进 cdn/(内部通道——walkthrough/trial 隔离站点用;
|
|
154
|
+
* CLI 命令层 publish 缺省走远端推送,本地-only 发布模式已退役)。
|
|
155
|
+
* 多人可并发:每人只写 `<name>-<version>.tgz`(名字含名+版本,天然不撞)。
|
|
156
|
+
*/
|
|
157
|
+
export async function publishPackage(opts: MarketPublishOptions = {}): Promise<MarketPublishResult> {
|
|
158
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
159
|
+
const layout = resolveCdnLayout(projectRoot);
|
|
160
|
+
if (!fs.existsSync(layout.dir)) {
|
|
161
|
+
fs.mkdirSync(layout.dir, { recursive: true });
|
|
162
|
+
console.log(`[ojm] 已建立 CDN 目录 ${path.relative(projectRoot, layout.dir)}`);
|
|
163
|
+
}
|
|
164
|
+
const built = await buildSignedPackage(opts);
|
|
165
|
+
|
|
166
|
+
const tgzPath = path.join(layout.dir, `${built.name}-${built.version}.tgz`);
|
|
167
|
+
const sigPath = `${tgzPath}.sig`;
|
|
121
168
|
const before = captureExisting(tgzPath);
|
|
169
|
+
// 同 name@version 重发:字节一致即幂等;字节不一致是真正的冲突——同一版本号出现两份
|
|
170
|
+
// 不同的包会毁掉内容寻址前提,必须升 version 重发
|
|
171
|
+
if (before.exists && before.sha256 !== built.sha256) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[ojm] 包冲突:${built.name}@${built.version} 已存在且字节不一致(${before.sha256.slice(0, 12)} vs ${built.sha256.slice(0, 12)})。\n`
|
|
174
|
+
+ "下一步:内容有变更请升 version 后重发(同一版本号只允许一份字节);"
|
|
175
|
+
+ "确需覆盖请先删除 CDN 目录里的旧 tgz。",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
fs.writeFileSync(tgzPath, built.tgzBytes);
|
|
179
|
+
fs.writeFileSync(sigPath, built.sigBytes);
|
|
180
|
+
console.log(`[ojm] 包已落 CDN → ${path.relative(projectRoot, tgzPath)}(${built.size} 字节)`);
|
|
181
|
+
console.log(`[ojm] 发布方 ${built.keyId};指纹 ${built.fingerprint}`);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: built.name,
|
|
185
|
+
version: built.version,
|
|
186
|
+
tgzPath,
|
|
187
|
+
sigPath,
|
|
188
|
+
sha256: built.sha256,
|
|
189
|
+
size: built.size,
|
|
190
|
+
canonical: built.canonical,
|
|
191
|
+
keyId: built.keyId,
|
|
192
|
+
publicKeyHex: built.publicKeyHex,
|
|
193
|
+
fingerprint: built.fingerprint,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
122
196
|
|
|
197
|
+
/** fixture 路径:合成 → 确定性打包(staging 内) */
|
|
198
|
+
async function buildFixture(args: { fixture?: string, staging: string }): Promise<{ name: string, version: string, tgzPath: string, sha256: string }> {
|
|
199
|
+
const spec = parseFixtureSpec(args.fixture);
|
|
200
|
+
const pkg = buildFixturePackage({ stagingDir: args.staging, name: spec.name, version: spec.version });
|
|
201
|
+
const tgzPath = path.join(args.staging, `${spec.name}-${spec.version}.tgz`);
|
|
123
202
|
const sha256 = await createDeterministicTgz(pkg.dir, tgzPath);
|
|
124
|
-
|
|
125
|
-
return { name: spec.name, version: spec.version, tgzPath, sha256, preExisted: before.exists };
|
|
203
|
+
return { name: spec.name, version: spec.version, tgzPath, sha256 };
|
|
126
204
|
}
|
|
127
205
|
|
|
128
|
-
/** `--real`:走既有 ojm build --bundle
|
|
129
|
-
async function buildRealPackage(solutionRoot: string, staging: string
|
|
206
|
+
/** `--real`:走既有 ojm build --bundle,产物归拢 staging(一次只发一份:多方案工程请显式指定) */
|
|
207
|
+
async function buildRealPackage(solutionRoot: string, staging: string): Promise<{ name: string, version: string, tgzPath: string, sha256: string }> {
|
|
130
208
|
// 与 ojm build 同一道完整性门禁:发布链路同样不许携带被改过的默认模块
|
|
131
209
|
assertDefaultModulesUntouched(solutionRoot);
|
|
132
210
|
const builtList = await buildBundle(solutionRoot, { out: staging });
|
|
133
211
|
if (builtList.length !== 1) {
|
|
134
212
|
throw new Error(
|
|
135
213
|
`[ojm] market publish --real 一次只发布一份方案,但该工程打出了 ${builtList.length} 份用户方案。`
|
|
136
|
-
+ "请改用显式链路:在方案工程里 `ojm build --bundle <name>`,再 `ojm sign dist/<name>-<version>.tgz --id <publisher>.<key
|
|
137
|
-
+ "把 tgz + .sig 放进 cdn/ 后 `ojm market build-index`。",
|
|
214
|
+
+ "请改用显式链路:在方案工程里 `ojm build --bundle <name>`,再 `ojm sign dist/<name>-<version>.tgz --id <publisher>.<key>`。",
|
|
138
215
|
);
|
|
139
216
|
}
|
|
140
217
|
const built = builtList[0]!;
|
|
141
|
-
const tgzPath = path.join(
|
|
142
|
-
const before = captureExisting(tgzPath);
|
|
143
|
-
|
|
218
|
+
const tgzPath = path.join(staging, `${built.name}-${built.version}.tgz`);
|
|
144
219
|
fs.copyFileSync(built.tgz, tgzPath);
|
|
145
|
-
|
|
146
|
-
return { name: built.name, version: built.version, tgzPath, sha256: built.sha256, preExisted: before.exists };
|
|
220
|
+
return { name: built.name, version: built.version, tgzPath, sha256: built.sha256 };
|
|
147
221
|
}
|
|
148
222
|
|
|
149
|
-
/**
|
|
223
|
+
/** 落包前记下同名文件的指纹(可能不存在) */
|
|
150
224
|
function captureExisting(tgzPath: string): { exists: boolean, sha256: string } {
|
|
151
225
|
if (!fs.existsSync(tgzPath))
|
|
152
226
|
return { exists: false, sha256: "" };
|
|
153
227
|
const bytes = new Uint8Array(fs.readFileSync(tgzPath));
|
|
154
228
|
return { exists: true, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
155
229
|
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* 同 name@version 重发:字节一致即幂等;字节不一致是真正的冲突——
|
|
159
|
-
* 同一个版本号出现两份不同的包会毁掉内容寻址前提,必须升 version 重发。
|
|
160
|
-
*/
|
|
161
|
-
function assertNoByteConflict(tgzPath: string, before: { exists: boolean, sha256: string }, spec: { name: string, version: string }): void {
|
|
162
|
-
if (!before.exists)
|
|
163
|
-
return;
|
|
164
|
-
const bytes = new Uint8Array(fs.readFileSync(tgzPath));
|
|
165
|
-
const after = createHash("sha256").update(bytes).digest("hex");
|
|
166
|
-
if (after === before.sha256)
|
|
167
|
-
return;
|
|
168
|
-
throw new Error(
|
|
169
|
-
`[ojm] 包冲突:${spec.name}@${spec.version} 已存在且字节不一致(${before.sha256.slice(0, 12)} vs ${after.slice(0, 12)})。\n`
|
|
170
|
-
+ "下一步:内容有变更请升 version 后重发(同一版本号只允许一份字节);"
|
|
171
|
-
+ "确需覆盖请先删除 CDN 目录里的旧 tgz。",
|
|
172
|
-
);
|
|
173
|
-
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 市场远端推送(A6,计划 v4 §五):`ojm market publish` 缺省链路。
|
|
3
|
+
*
|
|
4
|
+
* 远端 = oj 模板站点(纯站点无 ojm CLI):签名制推送(不经账号口令;企微用户无障碍)——
|
|
5
|
+
* X-OJM-Key-Id / X-OJM-Signature 头 + 站点信任库 + Ed25519,canonical 由**站点**从实收字节
|
|
6
|
+
* 重算 sha256 重构(`cdn-upload:<站点 registry_id>:<file>:<sha256>`)——换字节即 401。
|
|
7
|
+
*
|
|
8
|
+
* 顺序(pair 约束,钉死):先推 `.sig` 旁车再推 `.tgz`——服务端 tgz 未配对不入库(409)。
|
|
9
|
+
* 半成功(sig 已推、tgz 被拒):报错文案带「重跑本命令幂等补齐」——同字节重传幂等(服务端
|
|
10
|
+
* 查库仲裁),无人工清场负担。退出码 1 由命令层置(本库以抛错表达失败)。
|
|
11
|
+
*
|
|
12
|
+
* 站点身份发现:推送前先 GET catalog(匿名)——响应顶层 registry_id(A6 新增发现面);
|
|
13
|
+
* 同请求兼作 2×2 兼容矩阵探测:旧站点无 catalog API → 404 → 「站点版本过旧」人话。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { MarketPublishOptions } from "./publish";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
|
|
19
|
+
import process from "node:process";
|
|
20
|
+
import { signCanonical } from "../lifecycle/sign";
|
|
21
|
+
import { unwrapEnvelope } from "../site";
|
|
22
|
+
import { buildSignedPackage, resolveSigningIdentity } from "./publish";
|
|
23
|
+
|
|
24
|
+
/** 站点目录探测结果(catalog 响应顶层字段) */
|
|
25
|
+
export interface MarketDiscovery {
|
|
26
|
+
apiVersion: string | null
|
|
27
|
+
/** 站点身份(= 上传/yank canonical 首段);缺失在发现期即人话拒,故此后恒非空 */
|
|
28
|
+
registryId: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 去掉尾斜杠的站点基址(env 回落与 resolveSite 同序:flag > OJM_SITE_URL) */
|
|
32
|
+
export function resolvePushTarget(siteUrl?: string): string {
|
|
33
|
+
const base = (siteUrl ?? process.env.OJM_SITE_URL ?? "").replace(/\/+$/, "");
|
|
34
|
+
if (base === "")
|
|
35
|
+
throw new Error("[ojm] 远端发布须指定目标站点:--site-url <https://your-site>(或环境变量 OJM_SITE_URL)");
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* GET /api/oj-market/catalog 探测(page=1&size=1 最小请求):发现 registry_id +
|
|
41
|
+
* 旧站点人话分流(404 → 「站点版本过旧」+ 迁移指向;网络层失败 → 人话不抛原生错)。
|
|
42
|
+
*/
|
|
43
|
+
export async function discoverMarket(fetchFn: typeof fetch, base: string): Promise<MarketDiscovery> {
|
|
44
|
+
let res: Response;
|
|
45
|
+
try {
|
|
46
|
+
res = await fetchFn(`${base}/api/oj-market/catalog?page=1&size=1`, { method: "GET" });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`[ojm] 连不上站点 ${base}(${(error as Error).message})——核对 --site-url / 网络后重试`);
|
|
50
|
+
}
|
|
51
|
+
if (res.status === 404)
|
|
52
|
+
throw new Error(`[ojm] 站点 ${base} 版本过旧(无 /api/oj-market/catalog 目录 API)——目标站点须先升级到 DB 目录版本;迁移指南见 docs/migration/ 市场远程发布篇`);
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new Error(`[ojm] 站点目录探测被拒(HTTP ${res.status})——匿名可读端点不应要求凭据,核对站点配置`);
|
|
55
|
+
const raw = await res.json() as { code?: number, data?: unknown };
|
|
56
|
+
// 站点统一信封 {code,msg,data}(AC-D16);匿名读端点 code!==0 时按无目录处理
|
|
57
|
+
if (typeof raw.code === "number" && raw.code !== 0)
|
|
58
|
+
throw new Error(`[ojm] 站点目录探测失败(code=${raw.code})——目标站点须为 DB 目录版本;迁移指南见 docs/migration/ 市场远程发布篇`);
|
|
59
|
+
const body = (typeof raw.code === "number" ? raw.data : raw) as { api_version?: unknown, registry_id?: unknown };
|
|
60
|
+
const registryId = typeof body.registry_id === "string" && body.registry_id !== "" ? body.registry_id : null;
|
|
61
|
+
if (registryId === null)
|
|
62
|
+
throw new Error(`[ojm] 站点 ${base} 未配置 registry_id(签名推送 canonical 首段 = 站点身份)——请站点管理员在 config.yaml vars 增加 registry_id 后重试`);
|
|
63
|
+
return { apiVersion: typeof body.api_version === "string" ? body.api_version : null, registryId };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 推送失败:带阶段(sig/tgz)与 HTTP 语义的人话错(命令层据此置 exitCode 1) */
|
|
67
|
+
export class RemotePushError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
public readonly stage: "sig" | "tgz",
|
|
70
|
+
public readonly status: number,
|
|
71
|
+
message: string,
|
|
72
|
+
) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "RemotePushError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 单文件签名推送:canonical `cdn-upload:<站点 registry_id>:<文件名>:<实收字节 sha256>`
|
|
80
|
+
* 由发布方私钥签名(与服务端重算口径逐字一致——本函数按同一公式构造)。
|
|
81
|
+
*/
|
|
82
|
+
async function pushSignedFile(
|
|
83
|
+
fetchFn: typeof fetch,
|
|
84
|
+
base: string,
|
|
85
|
+
registryId: string,
|
|
86
|
+
fileName: string,
|
|
87
|
+
bytes: Uint8Array,
|
|
88
|
+
keyId: string,
|
|
89
|
+
privateKeyPem: string,
|
|
90
|
+
): Promise<unknown> {
|
|
91
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
92
|
+
const signature = signCanonical(privateKeyPem, `cdn-upload:${registryId}:${fileName}:${sha256}`);
|
|
93
|
+
const form = new FormData();
|
|
94
|
+
form.append("file", new Blob([bytes.slice()]), fileName);
|
|
95
|
+
const res = await fetchFn(`${base}/api/oj-market/cdn/upload?name=${encodeURIComponent(fileName)}`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "x-ojm-key-id": keyId, "x-ojm-signature": signature },
|
|
98
|
+
body: form,
|
|
99
|
+
});
|
|
100
|
+
// unwrapEnvelope 的人话已含服务端指引(403 附 trust add、409 附升 version 指引);
|
|
101
|
+
// 这里包阶段上下文 + 下一步(错误文案数据表面)
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
let env: { code?: number, msg?: string } | null = null;
|
|
104
|
+
try {
|
|
105
|
+
env = JSON.parse(text) as { code?: number, msg?: string };
|
|
106
|
+
}
|
|
107
|
+
catch {}
|
|
108
|
+
const status = res.status;
|
|
109
|
+
if (!res.ok || (typeof env?.code === "number" && env.code !== 0)) {
|
|
110
|
+
const msg = env?.msg ?? text.slice(0, 200);
|
|
111
|
+
throw new RemotePushError(fileName.endsWith(".sig") ? "sig" : "tgz", status, `[ojm] 推送 ${fileName} 被拒(HTTP ${res.status}):${msg}`);
|
|
112
|
+
}
|
|
113
|
+
return env?.code === 0 ? (env as { data?: unknown }).data : env;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface RemotePublishOptions extends MarketPublishOptions {
|
|
117
|
+
/** 目标站点基址(缺省 OJM_SITE_URL;无则人话报错——不猜默认站点) */
|
|
118
|
+
siteUrl?: string
|
|
119
|
+
/** 测试注入 fetch;缺省全局 fetch */
|
|
120
|
+
fetchFn?: typeof fetch
|
|
121
|
+
/** 日志旁路(测试收集用) */
|
|
122
|
+
onLog?: (line: string) => void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface RemotePublishResult {
|
|
126
|
+
name: string
|
|
127
|
+
version: string
|
|
128
|
+
sha256: string
|
|
129
|
+
size: number
|
|
130
|
+
keyId: string
|
|
131
|
+
fingerprint: string
|
|
132
|
+
/** 站点身份(= 上传 canonical 首段) */
|
|
133
|
+
registryId: string
|
|
134
|
+
siteUrl: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 远端发布主链:构建 + 包级签名(buildSignedPackage)→ catalog 探测(registry_id)→
|
|
139
|
+
* sig→tgz 顺序推送。半成功(sig 成 tgz 败)时错误文案追加「重跑本命令幂等补齐」。
|
|
140
|
+
*/
|
|
141
|
+
export async function remotePublish(opts: RemotePublishOptions = {}): Promise<RemotePublishResult> {
|
|
142
|
+
const log = opts.onLog ?? ((line: string) => console.log(line));
|
|
143
|
+
const base = resolvePushTarget(opts.siteUrl);
|
|
144
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
145
|
+
|
|
146
|
+
log(`[ojm] 远端发布目标 ${base}:构建发布包…`);
|
|
147
|
+
const built = await buildSignedPackage(opts);
|
|
148
|
+
log(`[ojm] 包已构建并签名:${built.name}@${built.version}(${built.size} 字节,发布方 ${built.keyId})`);
|
|
149
|
+
|
|
150
|
+
const discovery = await discoverMarket(fetchFn, base);
|
|
151
|
+
log(`[ojm] 站点目录就绪(api_version ${discovery.apiVersion ?? "?"},registry_id ${discovery.registryId})——先推 .sig 旁车`);
|
|
152
|
+
|
|
153
|
+
let sigPushed = false;
|
|
154
|
+
try {
|
|
155
|
+
await pushSignedFile(fetchFn, base, discovery.registryId, `${built.name}-${built.version}.tgz.sig`, built.sigBytes, built.keyId, built.privateKeyPem);
|
|
156
|
+
sigPushed = true;
|
|
157
|
+
log(`[ojm] .sig 旁车已推送(key_id ${built.keyId})`);
|
|
158
|
+
await pushSignedFile(fetchFn, base, discovery.registryId, `${built.name}-${built.version}.tgz`, built.tgzBytes, built.keyId, built.privateKeyPem);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// 半成功:sig 已落、tgz 未至——服务端 pair 约束下重跑本命令同字节幂等补齐,无清场负担
|
|
162
|
+
if (sigPushed && error instanceof RemotePushError && error.stage === "tgz")
|
|
163
|
+
throw new RemotePushError(error.stage, error.status, `${error.message}\n[ojm] .sig 旁车已推送成功——重跑本命令幂等补齐(同字节重传幂等,pair 自动完成)`);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
log(`[ojm] 发布完成:${built.name}@${built.version} 已进 ${base} 市场目录(DB 目录行 + cdn/ 字节)`);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
name: built.name,
|
|
170
|
+
version: built.version,
|
|
171
|
+
sha256: built.sha256,
|
|
172
|
+
size: built.size,
|
|
173
|
+
keyId: built.keyId,
|
|
174
|
+
fingerprint: built.fingerprint,
|
|
175
|
+
registryId: discovery.registryId,
|
|
176
|
+
siteUrl: base,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface RemoteYankOptions {
|
|
181
|
+
/** 目标站点基址(缺省 OJM_SITE_URL) */
|
|
182
|
+
siteUrl?: string
|
|
183
|
+
/** 方案名(product) */
|
|
184
|
+
name: string
|
|
185
|
+
/** 版本 */
|
|
186
|
+
version: string
|
|
187
|
+
/** 恢复(unyank);缺省 = 下线 */
|
|
188
|
+
restore?: boolean
|
|
189
|
+
/** 下线原因(审计留痕;可选) */
|
|
190
|
+
reason?: string
|
|
191
|
+
/** 操作人(审计留痕;缺省由站点记 key_id) */
|
|
192
|
+
by?: string
|
|
193
|
+
id?: string
|
|
194
|
+
key?: string
|
|
195
|
+
privateKeyPem?: string
|
|
196
|
+
/** 测试注入 fetch;缺省全局 fetch */
|
|
197
|
+
fetchFn?: typeof fetch
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface RemoteYankResult {
|
|
201
|
+
product: string
|
|
202
|
+
version: string
|
|
203
|
+
yanked: boolean
|
|
204
|
+
idempotent: boolean
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 远端 yank(计划 v4 §五):POST /api/oj-market/yank,canonical
|
|
209
|
+
* `market-yank:<站点 registry_id>:<product>:<version>`(无内容哈希——重放退化为幂等,
|
|
210
|
+
* 与服务端口径一致)。仅发布者本人可下线/恢复(服务端按行 key_id 授权,不符 403)。
|
|
211
|
+
*/
|
|
212
|
+
export async function remoteYank(opts: RemoteYankOptions): Promise<RemoteYankResult> {
|
|
213
|
+
const base = resolvePushTarget(opts.siteUrl);
|
|
214
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
215
|
+
const discovery = await discoverMarket(fetchFn, base);
|
|
216
|
+
const { keyId, privateKeyPem } = resolveSigningIdentity({ id: opts.id, key: opts.key, privateKeyPem: opts.privateKeyPem });
|
|
217
|
+
const signature = signCanonical(privateKeyPem, `market-yank:${discovery.registryId}:${opts.name}:${opts.version}`);
|
|
218
|
+
const res = await fetchFn(`${base}/api/oj-market/yank`, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: { "content-type": "application/json", "x-ojm-key-id": keyId, "x-ojm-signature": signature },
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
product: opts.name,
|
|
223
|
+
version: opts.version,
|
|
224
|
+
...(opts.restore === true ? { restore: true } : {}),
|
|
225
|
+
...(opts.reason !== undefined ? { reason: opts.reason } : {}),
|
|
226
|
+
...(opts.by !== undefined ? { by: opts.by } : {}),
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
const data = await unwrapEnvelope(res) as { product?: string, version?: string, yanked?: boolean, idempotent?: boolean };
|
|
230
|
+
return {
|
|
231
|
+
product: String(data.product ?? opts.name),
|
|
232
|
+
version: String(data.version ?? opts.version),
|
|
233
|
+
yanked: data.yanked === true,
|
|
234
|
+
idempotent: data.idempotent === true,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/market/trial.ts
CHANGED
|
@@ -68,7 +68,7 @@ export async function trialInstall(opts: TrialOptions = {}): Promise<TrialResult
|
|
|
68
68
|
if (task.status === "needs_trust") {
|
|
69
69
|
const fingerprint = task.trust?.fingerprint ?? "";
|
|
70
70
|
log(`[ojm] 待信任发布方 ${task.trust?.key_id ?? "?"},指纹 ${fingerprint.slice(0, 16)}…(隔离站点,自动确认)`);
|
|
71
|
-
await callApi(fetch, site.baseUrl, token, "POST", "/api/
|
|
71
|
+
await callApi(fetch, site.baseUrl, token, "POST", "/api/oj-market/trust", { task_id: task.id });
|
|
72
72
|
task = await installOnce(site.baseUrl, token, spec.name, spec.version);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -86,7 +86,7 @@ export async function trialInstall(opts: TrialOptions = {}): Promise<TrialResult
|
|
|
86
86
|
/** 一次安装 + 轮询到终态 */
|
|
87
87
|
async function installOnce(base: string, token: string, name: string, version: string): Promise<TaskRecord> {
|
|
88
88
|
// 站点返回 {task_id}(安装同步跑完后回填);mock 兼容 id 形态
|
|
89
|
-
const started = await callApi<{ task_id?: string, id?: string }>(fetch, base, token, "POST", "/api/
|
|
89
|
+
const started = await callApi<{ task_id?: string, id?: string }>(fetch, base, token, "POST", "/api/oj-market/install", {
|
|
90
90
|
name,
|
|
91
91
|
version,
|
|
92
92
|
source: "cdn",
|