@oj-module/cli 0.2.1 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +147 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +46 -7
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +56 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +162 -0
- package/templates/web/src/system/__client/api.ts +382 -0
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
package/src/oj-cert.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Buffer } from "node:buffer";
|
|
|
11
11
|
import crypto from "node:crypto";
|
|
12
12
|
import fs from "node:fs";
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import { readOjServerField } from "./oj-config";
|
|
14
15
|
|
|
15
16
|
/** 默认有效期:3650 天。oj 证书宽限期尽即拒启且无自助续期,短有效期等于时钟炸弹 */
|
|
16
17
|
const DEFAULT_DAYS = 3650;
|
|
@@ -51,3 +52,21 @@ export async function mintDevCert(
|
|
|
51
52
|
cert: write("cert.jws", `${signingInput}.${signature}\n`),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* dev 起后端前确保开发证书就位:config 声明的 `certificate_path` 文件缺失时现场签发。
|
|
58
|
+
*
|
|
59
|
+
* `ojm init` 产出的工程自带证书,但模板仓库本身也是一个可运行工程(不走 init),
|
|
60
|
+
* 直接 `pnpm dev` 会因 oj 读不到 public.pem 而 fail-fast——故 dev 补这一道。
|
|
61
|
+
* 证书路径相对 config 文件所在目录解析(与 oj 一致)。
|
|
62
|
+
*/
|
|
63
|
+
export async function ensureDevCert(configPath: string): Promise<boolean> {
|
|
64
|
+
const rel = readOjServerField(configPath, "certificate_path");
|
|
65
|
+
if (!rel)
|
|
66
|
+
return false;
|
|
67
|
+
const certPath = path.resolve(path.dirname(configPath), rel);
|
|
68
|
+
if (fs.existsSync(certPath))
|
|
69
|
+
return false;
|
|
70
|
+
await mintDevCert(path.dirname(certPath));
|
|
71
|
+
return true;
|
|
72
|
+
}
|
package/src/oj-cmd.ts
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* 2. 用户没给 -c/--config 时补上 <project>/api/config.yaml 的绝对路径。
|
|
7
7
|
*
|
|
8
8
|
* 为什么必须补 -c:这些子命令的 -c 缺省是 `./config.yaml`(相对 CWD),而 ojm
|
|
9
|
-
* 工程的 config 在
|
|
9
|
+
* 工程的 config 在 config/ 下——从工程根跑就会读不到。oj 的 -d/--db 等都有自动
|
|
10
10
|
* 探测(自 config 逐级搜,src 优先),ojm 不去猜,避免替用户做错的决定。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import fs from "node:fs";
|
|
15
|
-
import
|
|
15
|
+
import { resolveConfigPath } from "./layout";
|
|
16
16
|
import { resolveOjBin } from "./vendor";
|
|
17
17
|
|
|
18
18
|
/** 接受 -c 配置的子命令;`secret` 不在内(keygen/seal 不需要 config) */
|
|
@@ -37,7 +37,7 @@ export function runOjSubcommand(
|
|
|
37
37
|
|
|
38
38
|
const args = [sub, ...userArgs];
|
|
39
39
|
const hasConfigFlag = userArgs.includes("-c") || userArgs.includes("--config");
|
|
40
|
-
const configPath =
|
|
40
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
41
41
|
if (CONFIG_SUBCOMMANDS.has(sub) && !hasConfigFlag && fs.existsSync(configPath))
|
|
42
42
|
args.splice(1, 0, "-c", configPath);
|
|
43
43
|
|
package/src/preview.ts
CHANGED
|
@@ -18,7 +18,7 @@ import path, { normalize } from "node:path";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { proxyApi } from "./dev-proxy";
|
|
20
20
|
import { loadHtmlTransform } from "./html-transform";
|
|
21
|
-
import { resolveLayout } from "./layout";
|
|
21
|
+
import { resolveConfigPath, resolveLayout } from "./layout";
|
|
22
22
|
import { startOj } from "./oj";
|
|
23
23
|
import { readOjApiPrefix } from "./oj-config";
|
|
24
24
|
import { createStaticHandler, decodeReqPath, listenOnFreePort } from "./static-handler";
|
|
@@ -42,7 +42,7 @@ export async function previewServer(projectRoot: string, opts: PreviewOptions =
|
|
|
42
42
|
const layout = resolveLayout(projectRoot);
|
|
43
43
|
const siteDir = opts.siteDir ?? layout.distDir;
|
|
44
44
|
const ojBin = resolveOjBin(projectRoot);
|
|
45
|
-
const configPath =
|
|
45
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
46
46
|
const apiDist = path.join(projectRoot, "api", "dist");
|
|
47
47
|
|
|
48
48
|
// fail-fast 四查:缺一即提示对应补救(init 幂等补缺 / ojm build 产物)
|
package/src/publish.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
8
|
+
// ojm publish(P3-5/6/7,作者工具链):对发布目录的 bundles.yaml + manifests.yaml 索引
|
|
9
|
+
// 重算(P3-6:parse→确定性 stringify,消弭格式漂移、使 sha256 稳定可签)→ 签名(P3-7:detached
|
|
10
|
+
// 侧车 .sig,与 archive .sig 同构)→ 写回目录(本地文件目标;CDN 推送走 resolveCdn 同形)。
|
|
11
|
+
// 复用 sign.ts 签名原语 + 发布方 key(~/.ojm/keys,与 ojm sign 同身份体系)。
|
|
12
|
+
import { isValidKeyId } from "./keygen";
|
|
13
|
+
import { readPrivateKeyPem } from "./lifecycle/keygen";
|
|
14
|
+
import { CANON, exportRawPublicKeyHex, fingerprintOfPublicKey, SIG_ALG, signCanonical, verifyCanonical } from "./lifecycle/sign";
|
|
15
|
+
|
|
16
|
+
export interface PublishOptions {
|
|
17
|
+
/** 发布方 key_id(C2-37;.sig 的发布方标识) */
|
|
18
|
+
id?: string
|
|
19
|
+
/** 私钥 PEM 文件(缺省 ~/.ojm/keys/<id>/private.pem) */
|
|
20
|
+
key?: string
|
|
21
|
+
/** 写完离线自检(verifyCanonical 复核两份签名) */
|
|
22
|
+
check?: boolean
|
|
23
|
+
/** 测试注入;缺省 process.cwd() */
|
|
24
|
+
cwd?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PublishResult {
|
|
28
|
+
bundlesPath: string
|
|
29
|
+
manifestsPath: string
|
|
30
|
+
bundlesSigPath: string
|
|
31
|
+
manifestsSigPath: string
|
|
32
|
+
keyId: string
|
|
33
|
+
publicKeyHex: string
|
|
34
|
+
fingerprint: string
|
|
35
|
+
/** --check 的自检结果(未传 --check = undefined) */
|
|
36
|
+
checkPassed?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 索引规范串(P3-7):`index:<kind>:<sha256hex>`,与验签侧(market/api.ts)逐字一致 */
|
|
40
|
+
export function indexCanonical(kind: "bundles" | "manifests", text: string): string {
|
|
41
|
+
const sha256 = createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
42
|
+
return `index:${kind}:${sha256}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 对单个索引文本签名,产出 detached .sig 五字段(与 archive .sig 同构,契约 §2.11 / C2-35) */
|
|
46
|
+
export function signIndex(kind: "bundles" | "manifests", text: string, privateKeyPem: string, keyId: string): { alg: string, key_id: string, public_key: string, value: string, canon: string } {
|
|
47
|
+
const value = signCanonical(privateKeyPem, indexCanonical(kind, text));
|
|
48
|
+
return { alg: SIG_ALG, key_id: keyId, public_key: exportRawPublicKeyHex(privateKeyPem), value, canon: CANON };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 深度按 key 字典序重排(YAML v2 此构建无 sortKeys 原语,故手动对纯对象键序归一化)。
|
|
53
|
+
* YAML map 顺序无关语义,故重排不改变含义,仅使字节确定化(P3-6:sha256 稳定可签可验)。
|
|
54
|
+
*/
|
|
55
|
+
function sortKeysDeep(v: unknown): unknown {
|
|
56
|
+
if (Array.isArray(v))
|
|
57
|
+
return v.map(sortKeysDeep);
|
|
58
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
59
|
+
const out: Record<string, unknown> = {};
|
|
60
|
+
for (const k of Object.keys(v as Record<string, unknown>).sort())
|
|
61
|
+
out[k] = sortKeysDeep((v as Record<string, unknown>)[k]);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 索引重算(P3-6):parse → 键序归一化 → 确定性 stringify,使字节稳定、sha256 可签可验 */
|
|
68
|
+
export function recomputeIndex(text: string): string {
|
|
69
|
+
return yamlStringify(sortKeysDeep(yamlParse(text)), { lineWidth: 0 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function publishCommand(dir: string, opts: PublishOptions = {}): PublishResult {
|
|
73
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
74
|
+
const base = dir ? path.resolve(cwd, dir) : cwd;
|
|
75
|
+
// key 解析(同 ojm sign)
|
|
76
|
+
let keyFile: string;
|
|
77
|
+
let keyId: string;
|
|
78
|
+
if (opts.key !== undefined) {
|
|
79
|
+
keyFile = path.resolve(cwd, opts.key);
|
|
80
|
+
keyId = opts.id ?? "";
|
|
81
|
+
if (keyId === "")
|
|
82
|
+
throw new Error("[ojm] --key 指向自定义位置时须同时给 --id <publisher.id>(.sig 的发布方标识;C2-37 命名空间)");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
keyId = opts.id ?? "";
|
|
86
|
+
if (keyId === "")
|
|
87
|
+
throw new Error("[ojm] 缺少签名身份:--id <publisher.id>(从 ~/.ojm/keys/<id>/ 取私钥)或 --key <pem> --id <publisher.id>。先生成:ojm keygen --id <publisher.id>");
|
|
88
|
+
keyFile = path.join(os.homedir(), ".ojm", "keys", keyId, "private.pem");
|
|
89
|
+
}
|
|
90
|
+
if (!isValidKeyId(keyId))
|
|
91
|
+
throw new Error(`[ojm] key_id 命名空间非法(C2-37):「${keyId}」`);
|
|
92
|
+
if (!fs.existsSync(keyFile))
|
|
93
|
+
throw new Error(`[ojm] 私钥文件不存在:${keyFile}(用 ojm keygen --id ${keyId} 生成,或 --key 指定)`);
|
|
94
|
+
|
|
95
|
+
const bundlesPath = path.join(base, "bundles.yaml");
|
|
96
|
+
const manifestsPath = path.join(base, "manifests.yaml");
|
|
97
|
+
if (!fs.existsSync(bundlesPath) || !fs.existsSync(manifestsPath))
|
|
98
|
+
throw new Error(`[ojm] 发布目录须含 bundles.yaml 与 manifests.yaml:${base}`);
|
|
99
|
+
|
|
100
|
+
// P3-6 重算索引(deterministic stringify 后写回,publish 即生效,无需额外缓存层)
|
|
101
|
+
const bundlesText = recomputeIndex(fs.readFileSync(bundlesPath, "utf8"));
|
|
102
|
+
const manifestsText = recomputeIndex(fs.readFileSync(manifestsPath, "utf8"));
|
|
103
|
+
fs.writeFileSync(bundlesPath, bundlesText);
|
|
104
|
+
fs.writeFileSync(manifestsPath, manifestsText);
|
|
105
|
+
|
|
106
|
+
// P3-7 签名(detached .sig 侧车,与 archive .sig 同构)
|
|
107
|
+
const privateKeyPem = readPrivateKeyPem(keyFile);
|
|
108
|
+
const publicKeyHex = exportRawPublicKeyHex(privateKeyPem);
|
|
109
|
+
const bundlesSig = signIndex("bundles", bundlesText, privateKeyPem, keyId);
|
|
110
|
+
const manifestsSig = signIndex("manifests", manifestsText, privateKeyPem, keyId);
|
|
111
|
+
const bundlesSigPath = `${bundlesPath}.sig`;
|
|
112
|
+
const manifestsSigPath = `${manifestsPath}.sig`;
|
|
113
|
+
fs.writeFileSync(bundlesSigPath, `${JSON.stringify(bundlesSig, null, 2)}\n`);
|
|
114
|
+
fs.writeFileSync(manifestsSigPath, `${JSON.stringify(manifestsSig, null, 2)}\n`);
|
|
115
|
+
console.log(`[ojm] 索引已重算并签名 → ${path.relative(cwd, bundlesSigPath)} / ${path.relative(cwd, manifestsSigPath)}`);
|
|
116
|
+
console.log(`[ojm] 发布方 ${keyId};指纹 ${fingerprintOfPublicKey(publicKeyHex)}`);
|
|
117
|
+
|
|
118
|
+
let checkPassed: boolean | undefined;
|
|
119
|
+
if (opts.check === true) {
|
|
120
|
+
const bundlesOk = verifyCanonical(publicKeyHex, indexCanonical("bundles", bundlesText), bundlesSig.value);
|
|
121
|
+
const manifestsOk = verifyCanonical(publicKeyHex, indexCanonical("manifests", manifestsText), manifestsSig.value);
|
|
122
|
+
checkPassed = bundlesOk && manifestsOk;
|
|
123
|
+
console.log(checkPassed ? "[ojm] 自检通过:索引签名与内容匹配" : "[ojm] 自检失败:索引签名与内容不匹配——请检查私钥与索引后重签");
|
|
124
|
+
if (!checkPassed)
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
return { bundlesPath, manifestsPath, bundlesSigPath, manifestsSigPath, keyId, publicKeyHex, fingerprint: fingerprintOfPublicKey(publicKeyHex), checkPassed };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `ojm publish <dir> [--id <publisher.id>] [--key <pem>] [--check]` 参数解析 */
|
|
131
|
+
export function parsePublishArgs(argv: string[]): { positionals: string[], id?: string, key?: string, check?: boolean } {
|
|
132
|
+
const values: Record<string, string> = {};
|
|
133
|
+
let check = false;
|
|
134
|
+
const positionals: string[] = [];
|
|
135
|
+
for (let i = 0; i < argv.length; i++) {
|
|
136
|
+
const a = argv[i]!;
|
|
137
|
+
if (a === "--check") {
|
|
138
|
+
check = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (a === "--id" || a === "--key") {
|
|
142
|
+
values[a] = argv[i + 1] ?? "";
|
|
143
|
+
i++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!a.startsWith("--"))
|
|
147
|
+
positionals.push(a);
|
|
148
|
+
}
|
|
149
|
+
return { positionals, id: values["--id"], key: values["--key"], check };
|
|
150
|
+
}
|
package/src/sign.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// ojm sign(P2-4,作者工具链):对方案包 tgz 签 detached 侧车 `.sig`(契约 v1.3 §2.11 / C2-35)。
|
|
3
|
+
// 签名原文 = 规范串 `<name>@<version>:<sha256>`(C2-34);.sig 五字段
|
|
4
|
+
// {alg,key_id,public_key,value,canon},与 Form B archive.signature 同源同构。
|
|
5
|
+
// `--check`:写完离线自检(作者不必靠宿主装失败发现签错——先红后绿的第一道防线)。
|
|
6
|
+
// 只出签名不碰包:`ojm build --bundle` 保持只出包不签名(私钥红线见 bundle.ts 入包自检)。
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { isValidKeyId } from "./keygen";
|
|
12
|
+
import { readPrivateKeyPem } from "./lifecycle/keygen";
|
|
13
|
+
import { CANON, canonicalString, exportRawPublicKeyHex, fingerprintOfPublicKey, SIG_ALG, signCanonical, verifyCanonical } from "./lifecycle/sign";
|
|
14
|
+
|
|
15
|
+
export interface SignOptions {
|
|
16
|
+
/** 发布方 key_id(C2-37;.sig 的发布方标识) */
|
|
17
|
+
id?: string
|
|
18
|
+
/** 私钥 PEM 文件(缺省 ~/.ojm/keys/<id>/private.pem) */
|
|
19
|
+
key?: string
|
|
20
|
+
/** 写完离线自检(verifyCanonical 复核) */
|
|
21
|
+
check?: boolean
|
|
22
|
+
/** 测试注入;缺省 process.cwd() */
|
|
23
|
+
cwd?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SignResult {
|
|
27
|
+
sigPath: string
|
|
28
|
+
keyId: string
|
|
29
|
+
publicKeyHex: string
|
|
30
|
+
fingerprint: string
|
|
31
|
+
sha256: string
|
|
32
|
+
canonical: string
|
|
33
|
+
/** --check 的自检结果(未传 --check = undefined) */
|
|
34
|
+
checkPassed?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 从 --key 路径推导 key_id(~/.ojm/keys/<id>/private.pem 形态);推导不出返回 null */
|
|
38
|
+
function keyIdFromKeyPath(keyFile: string): string | null {
|
|
39
|
+
const m = /^\.ojm[\\/]keys[\\/]([^\\/]+)[\\/]private\.pem$/.exec(keyFile) ?? /[/\\]\.ojm[/\\]keys[/\\]([^/\\]+)[/\\]private\.pem$/.exec(keyFile);
|
|
40
|
+
return m?.[1] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function signCommand(tgz: string, opts: SignOptions = {}): SignResult {
|
|
44
|
+
if (!tgz.endsWith(".tgz"))
|
|
45
|
+
throw new Error(`[ojm] 签名目标必须是 .tgz 方案包:${tgz}`);
|
|
46
|
+
if (!fs.existsSync(tgz))
|
|
47
|
+
throw new Error(`[ojm] 方案包不存在:${tgz}`);
|
|
48
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
49
|
+
// key 解析:--key 显式文件优先;否则 ~/.ojm/keys/<id>/private.pem(--id 必填)
|
|
50
|
+
let keyFile: string;
|
|
51
|
+
let keyId: string;
|
|
52
|
+
if (opts.key !== undefined) {
|
|
53
|
+
keyFile = path.resolve(cwd, opts.key);
|
|
54
|
+
keyId = opts.id ?? keyIdFromKeyPath(path.relative(os.homedir(), keyFile)) ?? "";
|
|
55
|
+
if (keyId === "")
|
|
56
|
+
throw new Error("[ojm] --key 指向自定义位置时须同时给 --id <publisher.id>(.sig 的发布方标识;C2-37 命名空间)");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
keyId = opts.id ?? "";
|
|
60
|
+
if (keyId === "")
|
|
61
|
+
throw new Error("[ojm] 缺少签名身份:--id <publisher.id>(从 ~/.ojm/keys/<id>/ 取私钥)或 --key <pem> --id <publisher.id>。先生成:ojm keygen --id <publisher.id>");
|
|
62
|
+
keyFile = path.join(os.homedir(), ".ojm", "keys", keyId, "private.pem");
|
|
63
|
+
}
|
|
64
|
+
if (!isValidKeyId(keyId))
|
|
65
|
+
throw new Error(`[ojm] key_id 命名空间非法(C2-37):「${keyId}」`);
|
|
66
|
+
if (!fs.existsSync(keyFile))
|
|
67
|
+
throw new Error(`[ojm] 私钥文件不存在:${keyFile}(用 ojm keygen --id ${keyId} 生成,或 --key 指定)`);
|
|
68
|
+
|
|
69
|
+
const bytes = new Uint8Array(fs.readFileSync(tgz));
|
|
70
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
71
|
+
const canonical = canonicalString(deriveName(tgz), deriveVersion(tgz), sha256);
|
|
72
|
+
const privateKeyPem = readPrivateKeyPem(keyFile);
|
|
73
|
+
const value = signCanonical(privateKeyPem, canonical);
|
|
74
|
+
const publicKeyHex = exportRawPublicKeyHex(privateKeyPem);
|
|
75
|
+
const sig = { alg: SIG_ALG, key_id: keyId, public_key: publicKeyHex, value, canon: CANON };
|
|
76
|
+
const sigPath = `${tgz}.sig`;
|
|
77
|
+
fs.writeFileSync(sigPath, `${JSON.stringify(sig, null, 2)}\n`);
|
|
78
|
+
console.log(`[ojm] 签名完成 → ${sigPath}`);
|
|
79
|
+
console.log(`[ojm] 发布方 ${keyId};规范串 ${canonical}`);
|
|
80
|
+
let checkPassed: boolean | undefined;
|
|
81
|
+
if (opts.check === true) {
|
|
82
|
+
checkPassed = verifyCanonical(publicKeyHex, canonical, value);
|
|
83
|
+
console.log(checkPassed
|
|
84
|
+
? `[ojm] 自检通过:签名与包体匹配(指纹 ${fingerprintOfPublicKey(publicKeyHex)})`
|
|
85
|
+
: "[ojm] 自检失败:签名与包体不匹配——请检查私钥与包体后重签");
|
|
86
|
+
if (!checkPassed)
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
return { sigPath, keyId, publicKeyHex, fingerprint: fingerprintOfPublicKey(publicKeyHex), sha256, canonical, checkPassed };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `ojm sign <tgz> [--id <publisher.id>] [--key <pem>] [--check]` 参数解析 */
|
|
93
|
+
export function parseSignArgs(argv: string[]): { positionals: string[], id?: string, key?: string, check?: boolean } {
|
|
94
|
+
const values: Record<string, string> = {};
|
|
95
|
+
let check = false;
|
|
96
|
+
const positionals: string[] = [];
|
|
97
|
+
for (let i = 0; i < argv.length; i++) {
|
|
98
|
+
const a = argv[i]!;
|
|
99
|
+
if (a === "--check") {
|
|
100
|
+
check = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (a === "--id" || a === "--key") {
|
|
104
|
+
values[a] = argv[i + 1] ?? "";
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!a.startsWith("--"))
|
|
109
|
+
positionals.push(a);
|
|
110
|
+
}
|
|
111
|
+
return { positionals, id: values["--id"], key: values["--key"], check };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 身份从文件名推导(<name>-<version>.tgz;与 ojm install parseTarget 同规则,Finding C 正则) */
|
|
115
|
+
function deriveIdentity(stem: string): { name: string, version: string } {
|
|
116
|
+
const m = /^(.+)-(\d[\w.]+(?:-[\w.]+)?)$/.exec(stem);
|
|
117
|
+
if (m === null)
|
|
118
|
+
throw new Error(`[ojm] 无法从文件名推导 <name>-<version>:${stem}.tgz——请按 ojm build --bundle 产物命名(或重命名后再签)`);
|
|
119
|
+
return { name: m[1]!, version: m[2]! };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function deriveName(tgz: string): string {
|
|
123
|
+
return deriveIdentity(path.basename(tgz).replace(/\.tgz$/, "")).name;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function deriveVersion(tgz: string): string {
|
|
127
|
+
return deriveIdentity(path.basename(tgz).replace(/\.tgz$/, "")).version;
|
|
128
|
+
}
|
package/src/site.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// 站点瘦客户端(P1-13,D6):ojm install/enable/disable 共用的站点 HTTP 面。
|
|
2
|
+
// 登录(POST /api/auth/login)→ Bearer 注入 → oj 信封解包({code,msg,data},code=0 为成)。
|
|
3
|
+
// 凭据只走内存:flag(--user/--pass)> 环境变量(OJM_URL/OJM_USER/OJM_PASS),不落盘不落配置。
|
|
4
|
+
// 轮询对齐服务端消费面(§10.2):GET task?id= 查询参数形态,800ms 起步指数退避、上限 5s,
|
|
5
|
+
// 总超时对齐 TASK_TIMEOUT_MS(30min)——超时报错退出,不悬挂。
|
|
6
|
+
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
|
|
9
|
+
/** 任务记录(线形状镜像;真源 templates/api/src/_platform/_lifecycle/types.ts:65,客户端不 import 模板内部) */
|
|
10
|
+
export interface TaskRecord {
|
|
11
|
+
/**
|
|
12
|
+
* 记录 schema 版本(P2-10 线形状镜像):缺省(undefined)=旧记录=v1 语义照常渲染;
|
|
13
|
+
* 高于 TASK_SCHEMA_VERSION_KNOWN 的高版本 → renderOutcome 降级提示(人话 + 不误渲染成功)。
|
|
14
|
+
* 升版纪律见真源 types.ts TaskRecord.schemaVersion 注释。
|
|
15
|
+
*/
|
|
16
|
+
schemaVersion?: number
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
version: string
|
|
20
|
+
status: "running" | "done" | "failed" | "partial" | "needs_trust"
|
|
21
|
+
stage: string | null
|
|
22
|
+
progress: number
|
|
23
|
+
message: string
|
|
24
|
+
stages: { stage: string, ok: boolean, message: string }[]
|
|
25
|
+
error?: { reason: string, retryable: boolean, hintKey?: string }
|
|
26
|
+
/** needs_trust 终态:待确认发布方 key(P2-3;CLI trust confirm 展示与确认) */
|
|
27
|
+
trust?: { key_id: string, public_key: string, fingerprint: string }
|
|
28
|
+
appliedSeq?: number
|
|
29
|
+
startedAt: number
|
|
30
|
+
finishedAt?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 模板 serve 缺省端口(templates/api/config.yaml server.port: 9778;host 显式钉 127.0.0.1) */
|
|
34
|
+
export const DEFAULT_SITE_URL = "http://127.0.0.1:9778";
|
|
35
|
+
|
|
36
|
+
/** 本 CLI 已知的任务记录 schema 版本(P2-10;与服务端 tasks.ts TASK_SCHEMA_VERSION 手工同步,更高版本触发降级提示) */
|
|
37
|
+
export const TASK_SCHEMA_VERSION_KNOWN = 1;
|
|
38
|
+
|
|
39
|
+
/** 未来版本判定(P2-10 升版纪律读方侧):undefined=旧记录=已知 v1 语义;数值高于已知版本 → 降级提示 */
|
|
40
|
+
export function isFutureSchema(record: TaskRecord): boolean {
|
|
41
|
+
return typeof record.schemaVersion === "number" && record.schemaVersion > TASK_SCHEMA_VERSION_KNOWN;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SiteAddressing {
|
|
45
|
+
url?: string
|
|
46
|
+
user?: string
|
|
47
|
+
pass?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 站点寻址解析:flag > 环境变量 > 缺省;缺凭据即人话报错(不猜测不交互) */
|
|
51
|
+
export function resolveSite(opts: SiteAddressing): { base: string, user: string, pass: string } {
|
|
52
|
+
const base = (opts.url ?? process.env.OJM_URL ?? DEFAULT_SITE_URL).replace(/\/+$/, "");
|
|
53
|
+
const user = opts.user ?? process.env.OJM_USER ?? "";
|
|
54
|
+
const pass = opts.pass ?? process.env.OJM_PASS ?? "";
|
|
55
|
+
if (user === "" || pass === "")
|
|
56
|
+
throw new Error(`[ojm] 缺少站点凭据:请用 --user/--pass 或环境变量 OJM_USER/OJM_PASS 提供目标站点超管账号(--url / OJM_URL 指定站点地址,缺省 ${DEFAULT_SITE_URL};凭据只走内存,不落盘)`);
|
|
57
|
+
return { base, user, pass };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** oj 统一信封(AC-D16:{code,msg,data},code=0 为成) */
|
|
61
|
+
interface OjEnvelope {
|
|
62
|
+
code?: number
|
|
63
|
+
msg?: string
|
|
64
|
+
data?: unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 文本优先的信封解包(网关 502 HTML / 纯文本常态不炸),code!==0 抛人话 */
|
|
68
|
+
export async function unwrapEnvelope(res: { ok: boolean, status: number, text: () => Promise<string> }): Promise<unknown> {
|
|
69
|
+
const text = await res.text();
|
|
70
|
+
let env: OjEnvelope | null = null;
|
|
71
|
+
try {
|
|
72
|
+
env = JSON.parse(text) as OjEnvelope;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// 非 JSON 错误体(常态),走下方统一报错
|
|
76
|
+
}
|
|
77
|
+
if (env === null || typeof env.code !== "number")
|
|
78
|
+
throw new Error(`[ojm] 站点响应不可解析(HTTP ${res.status}):${text.slice(0, 200)}`);
|
|
79
|
+
if (env.code !== 0)
|
|
80
|
+
throw new Error(`[ojm] 站点返回错误(code=${env.code}):${env.msg ?? "未知错误"}`);
|
|
81
|
+
return env.data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function login(fetchFn: typeof fetch, base: string, user: string, pass: string): Promise<string> {
|
|
85
|
+
const data = await unwrapEnvelope(await fetchFn(`${base}/api/auth/login`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
body: JSON.stringify({ username: user, password: pass }),
|
|
89
|
+
})) as { access_token?: string };
|
|
90
|
+
const token = String(data?.access_token ?? "");
|
|
91
|
+
if (token === "")
|
|
92
|
+
throw new Error("[ojm] 登录响应缺 access_token——核对站点版本与账号");
|
|
93
|
+
return token;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 携 Bearer 的 JSON 请求(GET body 缺省;POST/PUT 序列化 body) */
|
|
97
|
+
export async function callApi<T>(fetchFn: typeof fetch, base: string, token: string, method: "GET" | "POST" | "DELETE", path: string, body?: unknown): Promise<T> {
|
|
98
|
+
const res = await fetchFn(`${base}${path}`, {
|
|
99
|
+
method,
|
|
100
|
+
headers: {
|
|
101
|
+
authorization: `Bearer ${token}`,
|
|
102
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
103
|
+
},
|
|
104
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
105
|
+
});
|
|
106
|
+
return await unwrapEnvelope(res) as T;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface PollOptions {
|
|
110
|
+
/** 起步间隔 ms(缺省 800,对齐 §10.2 服务端轮询消费面) */
|
|
111
|
+
interval0?: number
|
|
112
|
+
/** 间隔上限 ms(缺省 5000) */
|
|
113
|
+
intervalMax?: number
|
|
114
|
+
/** 总等待预算 ms(缺省 30min,对齐 TASK_TIMEOUT_MS) */
|
|
115
|
+
timeoutMs?: number
|
|
116
|
+
/** 测试注入;缺省真睡 */
|
|
117
|
+
sleep?: (ms: number) => Promise<void>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 终态判定:done/failed/partial;running 继续轮 */
|
|
121
|
+
function isTerminal(status: TaskRecord["status"]): boolean {
|
|
122
|
+
return status !== "running";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 任务轮询:800ms 起步、×2 指数退避、上限 5s;等待预算按累计睡眠计(注入 sleep 时确定可测),
|
|
127
|
+
* 预算耗尽即人话报错(对齐服务端 TASK_TIMEOUT_MS 超时判 failed 的口径,R13/B9)。
|
|
128
|
+
*/
|
|
129
|
+
export async function pollTask(fetchFn: typeof fetch, base: string, token: string, taskId: string, opts: PollOptions = {}): Promise<TaskRecord> {
|
|
130
|
+
const interval0 = opts.interval0 ?? 800;
|
|
131
|
+
const intervalMax = opts.intervalMax ?? 5000;
|
|
132
|
+
const timeoutMs = opts.timeoutMs ?? 30 * 60_000;
|
|
133
|
+
const sleep = opts.sleep ?? (ms => new Promise<void>(r => setTimeout(r, ms)));
|
|
134
|
+
let interval = interval0;
|
|
135
|
+
let waited = 0;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const record = await callApi<TaskRecord>(fetchFn, base, token, "GET", `/api/_platform/market/task?id=${encodeURIComponent(taskId)}`);
|
|
138
|
+
if (isTerminal(record.status))
|
|
139
|
+
return record;
|
|
140
|
+
if (waited + interval > timeoutMs)
|
|
141
|
+
throw new Error(`[ojm] 安装任务轮询超时(已等待 ${Math.round(waited / 1000)}s 未达终态,对齐服务端 ${Math.round(timeoutMs / 60000)} 分钟预算)——请到站点任务页查看详情`);
|
|
142
|
+
await sleep(interval);
|
|
143
|
+
waited += interval;
|
|
144
|
+
interval = Math.min(interval * 2, intervalMax);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/trust.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ojm trust(P2-2,C2-38/39,G3 双轨第一轨 / G5):宿主信任库运维瘦客户端。
|
|
2
|
+
// 子命令:
|
|
3
|
+
// ojm trust ls —— 列表(GET /api/_platform/market/trust)
|
|
4
|
+
// ojm trust add <key_id> <pubkey_hex> —— 新增信任(POST /api/_platform/market/trust/key)
|
|
5
|
+
// ojm trust revoke <key_id> —— 吊销(DELETE /api/_platform/market/trust/key?key_id=)
|
|
6
|
+
// ojm trust confirm --task <task_id> —— TOFU 确认(P2-3):先展示指纹再调 POST /market/trust
|
|
7
|
+
// 本地零状态,纯转发(uninstall.ts 同款);operator 由站点侧按登录用户反查,CLI 不传。
|
|
8
|
+
import { Buffer } from "node:buffer";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { callApi, login, resolveSite } from "./site";
|
|
12
|
+
|
|
13
|
+
export interface TrustOptions {
|
|
14
|
+
url?: string
|
|
15
|
+
user?: string
|
|
16
|
+
pass?: string
|
|
17
|
+
/** TOFU 确认目标任务 id(confirm 子命令;--task) */
|
|
18
|
+
task?: string
|
|
19
|
+
/** 测试注入;缺省 globalThis.fetch */
|
|
20
|
+
fetchFn?: typeof fetch
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TrustKeyRow {
|
|
24
|
+
key_id: string
|
|
25
|
+
public_key: string
|
|
26
|
+
added_at: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 指纹分组展示(TOFU 人确认的可读形态:每 8 hex 一组,契约 §2.11) */
|
|
30
|
+
export function formatFingerprint(fp: string): string {
|
|
31
|
+
return (fp.match(/.{1,8}/g) ?? []).join(" ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 指纹 = sha256Hex(publicKey)(契约 §2.11,与 isolate 侧同口径;CLI 在 Node 侧本地计算) */
|
|
35
|
+
function fingerprintOf(publicKeyHex: string): string {
|
|
36
|
+
return createHash("sha256").update(Buffer.from(publicKeyHex, "hex")).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printRows(rows: TrustKeyRow[]): void {
|
|
40
|
+
if (rows.length === 0) {
|
|
41
|
+
console.log("[ojm] 信任库为空(未信任任何发布方 key_id)");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const r of rows)
|
|
45
|
+
console.log(`[ojm] ${r.key_id} added_at=${new Date(r.added_at).toISOString()} fp=${formatFingerprint(fingerprintOf(r.public_key))}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 子命令分派:ls / add <key_id> <pubkey_hex> / revoke <key_id>。
|
|
50
|
+
* 站点侧拒(key_id 命名空间/公钥形态 400、撞绑定 409)打印人话 message 并 exitCode 1。
|
|
51
|
+
*/
|
|
52
|
+
export async function trustCommand(sub: string, args: string[], opts: TrustOptions = {}): Promise<void> {
|
|
53
|
+
// 先校验后登录(uninstall.ts 同纪律:用法错误不发网络)
|
|
54
|
+
switch (sub) {
|
|
55
|
+
case "ls":
|
|
56
|
+
break;
|
|
57
|
+
case "add": {
|
|
58
|
+
const [keyId, publicKey] = args;
|
|
59
|
+
if (!keyId || !publicKey)
|
|
60
|
+
throw new Error("[ojm] 用法:ojm trust add <key_id> <pubkey_hex>——key_id 形如 <publisher>.<id>(全小写),pubkey_hex 为 64 hex Ed25519 公钥");
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "revoke": {
|
|
64
|
+
const [keyId] = args;
|
|
65
|
+
if (!keyId)
|
|
66
|
+
throw new Error("[ojm] 用法:ojm trust revoke <key_id>——吊销该发布方:只阻断新装/升级,不动已装方案");
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "confirm": {
|
|
70
|
+
if (!opts.task || opts.task.trim() === "")
|
|
71
|
+
throw new Error("[ojm] 用法:ojm trust confirm --task <task_id>——对 needs_trust 安装任务确认未知发布方指纹后入信任库");
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`[ojm] 未知 trust 子命令:${sub || "(空)"}——可用:ls / add <key_id> <pubkey_hex> / revoke <key_id> / confirm --task <task_id>`);
|
|
76
|
+
}
|
|
77
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
78
|
+
const { base, user, pass } = resolveSite(opts);
|
|
79
|
+
const token = await login(fetchFn, base, user, pass);
|
|
80
|
+
switch (sub) {
|
|
81
|
+
case "ls": {
|
|
82
|
+
const result = await callApi<{ keys: TrustKeyRow[], note: string | null }>(fetchFn, base, token, "GET", "/api/_platform/market/trust");
|
|
83
|
+
if (result.note)
|
|
84
|
+
console.log(`[ojm] 注意:${result.note}`);
|
|
85
|
+
printRows(result.keys ?? []);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "add": {
|
|
89
|
+
const [keyId, publicKey] = args;
|
|
90
|
+
const result = await callApi<{ result: "added" | "exists_same" }>(fetchFn, base, token, "POST", "/api/_platform/market/trust/key", { key_id: keyId, public_key: publicKey });
|
|
91
|
+
console.log(result.result === "added" ? `[ojm] 已信任发布方:${keyId}` : `[ojm] 该 key_id 已在信任库且公钥一致(无需重复添加):${keyId}`);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "revoke": {
|
|
95
|
+
const [keyId] = args;
|
|
96
|
+
const result = await callApi<{ revoked: boolean }>(fetchFn, base, token, "DELETE", `/api/_platform/market/trust/key?key_id=${encodeURIComponent(keyId)}`);
|
|
97
|
+
console.log(result.revoked ? `[ojm] 已吊销:${keyId}(已装方案继续运行,新装/升级将被拒)` : `[ojm] 信任库中无此 key_id:${keyId}`);
|
|
98
|
+
if (!result.revoked)
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "confirm": {
|
|
103
|
+
const taskId = opts.task!.trim();
|
|
104
|
+
// 先取任务记录展示指纹(防「确认 A、落 B」:人看到的与服务端入信任库的同源)
|
|
105
|
+
const rec = await callApi<{ id: string, status: string, name: string, version: string, trust?: { key_id: string, public_key: string, fingerprint: string } }>(fetchFn, base, token, "GET", `/api/_platform/market/task?id=${encodeURIComponent(taskId)}`);
|
|
106
|
+
if (rec.trust === undefined || rec.status !== "needs_trust")
|
|
107
|
+
throw new Error(`[ojm] 任务 ${taskId} 不在待信任确认状态(status=${rec.status})——无需确认`);
|
|
108
|
+
console.log(`[ojm] 待信任发布方:${rec.trust.key_id}`);
|
|
109
|
+
console.log(`[ojm] 指纹(sha256(publicKey),请与发布方公布值逐字核对):${formatFingerprint(rec.trust.fingerprint)}`);
|
|
110
|
+
console.log("[ojm] 确认即把该公钥加入宿主信任库(首次绑定不可改;吊销只阻断新装/升级,不动已装方案)");
|
|
111
|
+
const result = await callApi<{ result: "added" | "exists_same", key_id: string }>(fetchFn, base, token, "POST", "/api/_platform/market/trust", { task_id: taskId });
|
|
112
|
+
console.log(result.result === "added" ? `[ojm] 已信任发布方:${result.key_id}——重试安装命令即可继续(CDN 源重试自动重下,file 源重传包体与 .sig)` : `[ojm] 该发布方已在信任库:${result.key_id}——直接重试安装即可`);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/uninstall.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ojm uninstall(P1-15a,缺口 B):卸载瘦客户端——POST /api/_platform/market/uninstall
|
|
2
|
+
// 触发站点侧 runUninstall 四处同删(锁/sidecar/sys_solution_route/modules.json + 权限级联)。
|
|
3
|
+
// 本地零状态,纯转发(enable.ts 同款);operator 由站点侧按登录用户反查,CLI 不传。
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { callApi, login, resolveSite } from "./site";
|
|
6
|
+
|
|
7
|
+
export interface UninstallOptions {
|
|
8
|
+
url?: string
|
|
9
|
+
user?: string
|
|
10
|
+
pass?: string
|
|
11
|
+
/** 测试注入;缺省 globalThis.fetch */
|
|
12
|
+
fetchFn?: typeof fetch
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 卸载命令体:登录 → POST uninstall {name} → 打印服务端人话 message。
|
|
17
|
+
* ok:false = 域逻辑拒绝(未装/空锁拒启),打印 message 并置 exitCode 1。
|
|
18
|
+
*/
|
|
19
|
+
export async function uninstallCommand(name: string, opts: UninstallOptions = {}): Promise<void> {
|
|
20
|
+
if (name.trim() === "")
|
|
21
|
+
throw new Error("[ojm] 用法:ojm uninstall <name>——name 为已安装方案名");
|
|
22
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
23
|
+
const { base, user, pass } = resolveSite(opts);
|
|
24
|
+
const token = await login(fetchFn, base, user, pass);
|
|
25
|
+
const result = await callApi<{ ok: boolean, message: string }>(fetchFn, base, token, "POST", "/api/_platform/market/uninstall", { name: name.trim() });
|
|
26
|
+
console.log(`[ojm] ${result.message}`);
|
|
27
|
+
if (!result.ok)
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|