@oj-module/cli 0.2.2 → 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 +1 -1
- 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 +12 -0
- package/templates/web/src/system/__client/api.ts +25 -1
- 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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
// BDD:ojm trust 瘦客户端(P2-2,C2-38/39)——纯转发(uninstall.test.ts 同范式):
|
|
3
|
+
// 登录 → GET/POST/DELETE /api/_platform/market/trust* → 打印人话;fetch 全 stub 不真发网络。
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { formatFingerprint, trustCommand } from "../../trust";
|
|
6
|
+
|
|
7
|
+
function fetchStub(handlers: Record<string, (url: string, init?: RequestInit) => unknown>): {
|
|
8
|
+
fetchFn: typeof fetch
|
|
9
|
+
calls: { url: string, init?: RequestInit }[]
|
|
10
|
+
} {
|
|
11
|
+
const calls: { url: string, init?: RequestInit }[] = [];
|
|
12
|
+
const fetchFn = (async (url: string | URL, init?: RequestInit) => {
|
|
13
|
+
const u = String(url);
|
|
14
|
+
calls.push({ url: u, init });
|
|
15
|
+
for (const [prefix, handler] of Object.entries(handlers)) {
|
|
16
|
+
if (u.includes(prefix))
|
|
17
|
+
return handler(u, init);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`fetchStub 未覆盖:${u}`);
|
|
20
|
+
}) as typeof fetch;
|
|
21
|
+
return { fetchFn, calls };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ok = (data: unknown) => ({ ok: true, status: 200, text: async () => JSON.stringify({ code: 0, data }) });
|
|
25
|
+
|
|
26
|
+
const KEY_A = { key_id: "ojm.test", public_key: "a".repeat(64), added_at: 1730000000000 };
|
|
27
|
+
|
|
28
|
+
let logs: string[];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
logs = [];
|
|
32
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
33
|
+
logs.push(args.map(String).join(" "));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const SITE = { url: "http://x", user: "u", pass: "p" };
|
|
42
|
+
|
|
43
|
+
describe("trustCommand · ls(P2-2)", () => {
|
|
44
|
+
it("登录后 GET /market/trust → 逐行打印 key_id + 分组指纹", async () => {
|
|
45
|
+
const { fetchFn, calls } = fetchStub({
|
|
46
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
47
|
+
"/api/_platform/market/trust": () => ok({ keys: [KEY_A], note: null }),
|
|
48
|
+
});
|
|
49
|
+
await trustCommand("ls", [], { ...SITE, fetchFn });
|
|
50
|
+
expect(calls[1]?.url).toContain("/api/_platform/market/trust");
|
|
51
|
+
expect(calls[1]?.init?.method).toBe("GET");
|
|
52
|
+
expect((calls[1]?.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
|
|
53
|
+
const out = logs.join("\n");
|
|
54
|
+
expect(out).toContain("ojm.test");
|
|
55
|
+
expect(out).toMatch(/fp=([0-9a-f]{8} ){7}[0-9a-f]{8}/); // 64hex 分 8 组
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("空库 → 可读空态;note 在场(.bak 回落)→ 先打注意行", async () => {
|
|
59
|
+
const { fetchFn } = fetchStub({
|
|
60
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
61
|
+
"/api/_platform/market/trust": () => ok({ keys: [], note: "x 损坏,已自动回落 x.bak" }),
|
|
62
|
+
});
|
|
63
|
+
await trustCommand("ls", [], { ...SITE, fetchFn });
|
|
64
|
+
expect(logs.join("\n")).toContain("已自动回落");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("trustCommand · add / revoke(P2-2)", () => {
|
|
69
|
+
it("add:POST {key_id, public_key};added / exists_same 两种回显", async () => {
|
|
70
|
+
const added = fetchStub({
|
|
71
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
72
|
+
"/api/_platform/market/trust/key": () => ok({ result: "added" }),
|
|
73
|
+
});
|
|
74
|
+
await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: added.fetchFn });
|
|
75
|
+
const call = added.calls[1]!;
|
|
76
|
+
expect(call.init?.method).toBe("POST");
|
|
77
|
+
expect(JSON.parse(String(call.init?.body))).toEqual({ key_id: "ojm.test", public_key: "a".repeat(64) });
|
|
78
|
+
expect(logs.join("\n")).toContain("已信任发布方:ojm.test");
|
|
79
|
+
|
|
80
|
+
const idem = fetchStub({
|
|
81
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
82
|
+
"/api/_platform/market/trust/key": () => ok({ result: "exists_same" }),
|
|
83
|
+
});
|
|
84
|
+
await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: idem.fetchFn });
|
|
85
|
+
expect(logs.join("\n")).toContain("无需重复添加");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("add 缺参 → 人话用法报错(不发网络)", async () => {
|
|
89
|
+
const { fetchFn, calls } = fetchStub({});
|
|
90
|
+
await expect(trustCommand("add", ["ojm.test"], { ...SITE, fetchFn })).rejects.toThrow(/用法/);
|
|
91
|
+
expect(calls).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("revoke:DELETE ?key_id=(URI 编码);revoked=false 置 exitCode 1", async () => {
|
|
95
|
+
const exitBackup = process.exitCode;
|
|
96
|
+
process.exitCode = 0;
|
|
97
|
+
try {
|
|
98
|
+
const yes = fetchStub({
|
|
99
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
100
|
+
"/api/_platform/market/trust/key": () => ok({ revoked: true }),
|
|
101
|
+
});
|
|
102
|
+
await trustCommand("revoke", ["ojm.test"], { ...SITE, fetchFn: yes.fetchFn });
|
|
103
|
+
const call = yes.calls[1]!;
|
|
104
|
+
expect(call.init?.method).toBe("DELETE");
|
|
105
|
+
expect(call.url).toContain("/api/_platform/market/trust/key?key_id=ojm.test");
|
|
106
|
+
expect(process.exitCode).toBe(0);
|
|
107
|
+
|
|
108
|
+
process.exitCode = 0;
|
|
109
|
+
const no = fetchStub({
|
|
110
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
111
|
+
"/api/_platform/market/trust/key": () => ok({ revoked: false }),
|
|
112
|
+
});
|
|
113
|
+
await trustCommand("revoke", ["nope.key"], { ...SITE, fetchFn: no.fetchFn });
|
|
114
|
+
expect(process.exitCode).toBe(1);
|
|
115
|
+
expect(logs.join("\n")).toContain("无此 key_id");
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
process.exitCode = exitBackup;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("未知子命令 → 人话报错(列可用子命令)", async () => {
|
|
123
|
+
const { fetchFn } = fetchStub({});
|
|
124
|
+
await expect(trustCommand("frobnicate", [], { ...SITE, fetchFn })).rejects.toThrow(/可用:ls/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("trustCommand · confirm(P2-3 TOFU 确认链)", () => {
|
|
129
|
+
const needsTrustTask = {
|
|
130
|
+
id: "install-1-abc",
|
|
131
|
+
name: "demo-solution",
|
|
132
|
+
version: "0.1.0",
|
|
133
|
+
status: "needs_trust",
|
|
134
|
+
stage: "verifying",
|
|
135
|
+
trust: { key_id: "stranger.pub", public_key: "b".repeat(64), fingerprint: "f".repeat(64) },
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
it("先展示指纹再调端点:GET task → 打印 key_id + 分组指纹 → POST /market/trust {task_id}", async () => {
|
|
139
|
+
const { fetchFn, calls } = fetchStub({
|
|
140
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
141
|
+
"/api/_platform/market/task": () => ok(needsTrustTask),
|
|
142
|
+
"/api/_platform/market/trust": () => ok({ result: "added", key_id: "stranger.pub" }),
|
|
143
|
+
});
|
|
144
|
+
await trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn });
|
|
145
|
+
// 调用序:登录 → GET task → POST trust
|
|
146
|
+
expect(calls[1]?.url).toContain("/api/_platform/market/task?id=install-1-abc");
|
|
147
|
+
expect(calls[2]?.init?.method).toBe("POST");
|
|
148
|
+
expect(calls[2]?.url).toContain("/api/_platform/market/trust");
|
|
149
|
+
expect(JSON.parse(String(calls[2]?.init?.body))).toEqual({ task_id: "install-1-abc" });
|
|
150
|
+
const out = logs.join("\n");
|
|
151
|
+
expect(out).toContain("stranger.pub");
|
|
152
|
+
expect(out).toMatch(/ffffffff ffffffff ffffffff ffffffff/); // 指纹分组先于人看到
|
|
153
|
+
expect(out).toContain("已信任发布方:stranger.pub");
|
|
154
|
+
expect(out).toContain("重试安装命令即可继续");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("任务非 needs_trust → 本地可读报错(不发确认请求)", async () => {
|
|
158
|
+
const { fetchFn, calls } = fetchStub({
|
|
159
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
160
|
+
"/api/_platform/market/task": () => ok({ ...needsTrustTask, status: "done", trust: undefined }),
|
|
161
|
+
});
|
|
162
|
+
await expect(trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn })).rejects.toThrow(/不在待信任确认状态/);
|
|
163
|
+
expect(calls).toHaveLength(2); // 登录 + GET task,无 POST
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("缺 --task → 用法报错(不发网络)", async () => {
|
|
167
|
+
const { fetchFn, calls } = fetchStub({});
|
|
168
|
+
await expect(trustCommand("confirm", [], { ...SITE, fetchFn })).rejects.toThrow(/--task/);
|
|
169
|
+
expect(calls).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("formatFingerprint(契约 §2.11 展示口径)", () => {
|
|
174
|
+
it("64hex → 8 组 × 8 字符空格分隔", () => {
|
|
175
|
+
expect(formatFingerprint("0".repeat(64))).toBe("00000000 ".repeat(8).trim());
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
// BDD:ojm uninstall 瘦客户端(P1-15a 缺口 B,D6/D8 同款)——纯转发:
|
|
3
|
+
// 登录 → POST /api/_platform/market/uninstall {name} → 打印服务端人话 message;
|
|
4
|
+
// ok:false(未装/空锁拒启)置 exitCode 1。fetch 全 stub 不真发网络(install.test.ts 同范式)。
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { uninstallCommand } from "../../uninstall";
|
|
7
|
+
|
|
8
|
+
/** fetch stub:按 URL 前缀分发,记录全部调用(url + init) */
|
|
9
|
+
function fetchStub(handlers: Record<string, (url: string, init?: RequestInit) => unknown>): {
|
|
10
|
+
fetchFn: typeof fetch
|
|
11
|
+
calls: { url: string, init?: RequestInit }[]
|
|
12
|
+
} {
|
|
13
|
+
const calls: { url: string, init?: RequestInit }[] = [];
|
|
14
|
+
const fetchFn = (async (url: string | URL, init?: RequestInit) => {
|
|
15
|
+
const u = String(url);
|
|
16
|
+
calls.push({ url: u, init });
|
|
17
|
+
for (const [prefix, handler] of Object.entries(handlers)) {
|
|
18
|
+
if (u.includes(prefix))
|
|
19
|
+
return handler(u, init);
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`fetchStub 未覆盖:${u}`);
|
|
22
|
+
}) as typeof fetch;
|
|
23
|
+
return { fetchFn, calls };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ok = (data: unknown) => ({ ok: true, status: 200, text: async () => JSON.stringify({ code: 0, data }) });
|
|
27
|
+
|
|
28
|
+
let logs: string[];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
logs = [];
|
|
32
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
33
|
+
logs.push(args.map(String).join(" "));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("uninstallCommand · 卸载瘦客户端(P1-15a 缺口 B)", () => {
|
|
42
|
+
it("登录后 POST uninstall {name}(Bearer 注入)→ 打印服务端 message", async () => {
|
|
43
|
+
const { fetchFn, calls } = fetchStub({
|
|
44
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
45
|
+
"/api/_platform/market/uninstall": () => ok({ ok: true, message: "卸载完成:四处同删(demo-solution@0.1.0)" }),
|
|
46
|
+
});
|
|
47
|
+
await uninstallCommand("demo-solution", { url: "http://x", user: "u", pass: "p", fetchFn });
|
|
48
|
+
// 登录体 + uninstall JSON 体 + Bearer
|
|
49
|
+
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ username: "u", password: "p" });
|
|
50
|
+
const call = calls[1]!;
|
|
51
|
+
expect(call.url).toContain("/api/_platform/market/uninstall");
|
|
52
|
+
expect(call.init?.method).toBe("POST");
|
|
53
|
+
expect((call.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
|
|
54
|
+
expect(JSON.parse(String(call.init?.body))).toEqual({ name: "demo-solution" });
|
|
55
|
+
expect(logs.join("\n")).toContain("卸载完成:四处同删(demo-solution@0.1.0)");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ok:false(域逻辑拒绝)→ message 照打 + exitCode 1", async () => {
|
|
59
|
+
const { fetchFn } = fetchStub({
|
|
60
|
+
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
61
|
+
"/api/_platform/market/uninstall": () => ok({ ok: false, message: "卸载失败:「x」未安装(锁与 sidecar 均无记录)" }),
|
|
62
|
+
});
|
|
63
|
+
const exitBackup = process.exitCode;
|
|
64
|
+
process.exitCode = 0;
|
|
65
|
+
try {
|
|
66
|
+
await uninstallCommand("x", { url: "http://x", user: "u", pass: "p", fetchFn });
|
|
67
|
+
expect(process.exitCode).toBe(1);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
process.exitCode = exitBackup;
|
|
71
|
+
}
|
|
72
|
+
expect(logs.join("\n")).toContain("未安装");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("空名 → 人话用法报错(不发网络)", async () => {
|
|
76
|
+
const { fetchFn, calls } = fetchStub({});
|
|
77
|
+
await expect(uninstallCommand("", { url: "http://x", user: "u", pass: "p", fetchFn })).rejects.toThrow(/用法/);
|
|
78
|
+
expect(calls).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("信封非 0:可读报错(code + msg)", async () => {
|
|
82
|
+
const { fetchFn } = fetchStub({
|
|
83
|
+
"/api/auth/login": () => ({ ok: true, status: 200, text: async () => JSON.stringify({ code: 403, msg: "forbidden" }) }),
|
|
84
|
+
});
|
|
85
|
+
await expect(uninstallCommand("x", { url: "http://x", user: "u", pass: "p", fetchFn })).rejects.toThrow(/forbidden/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 密钥工具(P2-1):Ed25519 密钥对生成与 PEM 读写。
|
|
2
|
+
// 与 sign.ts 的分界(SRP):sign.ts 管「签/验/规范串」语义,本文件管「key 生命周期」。
|
|
3
|
+
// CLI 命令接线(ojm keygen 的 --out 警告/0600 落盘)属 P2-4;此处只交付纯函数面。
|
|
4
|
+
// 站点既有 RSA cert 对 / X25519 secrets 对不可复用(算法不符、per-site、已暴露)——
|
|
5
|
+
// bootstrap key 与本工具一律新生成 Ed25519(执行计划 §0 bootstrap key 定性)。
|
|
6
|
+
import type { Buffer } from "node:buffer";
|
|
7
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
export interface KeyPair {
|
|
11
|
+
/** 32B raw 公钥 hex(契约 C2-35 public_key 字段口径) */
|
|
12
|
+
publicKeyHex: string
|
|
13
|
+
/** PKCS#8 PEM 私钥(写盘 0600;永不入日志/任务记录——P2-5 redact 钉面) */
|
|
14
|
+
privateKeyPem: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 生成 Ed25519 密钥对(Node 侧唯一入口;确定性由 Ed25519 算法本身保证同 key 同签) */
|
|
18
|
+
export function generateKeyPair(): KeyPair {
|
|
19
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519", {
|
|
20
|
+
publicKeyEncoding: { format: "der", type: "spki" },
|
|
21
|
+
privateKeyEncoding: { format: "pem", type: "pkcs8" },
|
|
22
|
+
});
|
|
23
|
+
const spki = publicKey as Buffer;
|
|
24
|
+
const raw = spki.subarray(spki.length - 32);
|
|
25
|
+
return { publicKeyHex: raw.toString("hex"), privateKeyPem: privateKey as string };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 从 PEM 文件加载私钥(不存在/形状错抛可读错误——fail-closed 不做静默回落) */
|
|
29
|
+
export function readPrivateKeyPem(file: string): string {
|
|
30
|
+
let pem: string;
|
|
31
|
+
try {
|
|
32
|
+
pem = readFileSync(file, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
throw new Error(`[ojm] 私钥文件读取失败:${file}(${String(e)})`);
|
|
36
|
+
}
|
|
37
|
+
if (!pem.includes("PRIVATE KEY"))
|
|
38
|
+
throw new Error(`[ojm] 不是合法 PEM 私钥文件:${file}`);
|
|
39
|
+
return pem;
|
|
40
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 签名纯函数(P2-1,契约 v1.3 §2.11 / C2-34~36)。
|
|
2
|
+
// 与 oj isolate 侧(P2-3 steps/verify.ts + CryptoFace.verifyEd25519)三方同语义:
|
|
3
|
+
// 规范串逐字、编码严格拒(不做归一)、确定性签名。isolate 面无 node:crypto,
|
|
4
|
+
// 其实现由上游原语承担;本文件是 Node 侧(ojm sign/深检/夹具)唯一实现,
|
|
5
|
+
// 与 isolate 面的一致性由 P2-0 夹具 + 本 spec 的夹具对拍钉住。
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { createHash, createPrivateKey, createPublicKey, sign as nodeSign, verify as nodeVerify } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
/** 规范串版本标记(契约 §2.11:canon 字段恒 "v1") */
|
|
10
|
+
export const CANON = "v1";
|
|
11
|
+
/** 签名算法标识(大小写敏感,契约 §2.11 / C2-35) */
|
|
12
|
+
export const SIG_ALG = "Ed25519";
|
|
13
|
+
|
|
14
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
15
|
+
/** 标准 base64(契约 C2-36:含 -/_ 即 base64url,一律拒,禁归一) */
|
|
16
|
+
const STD_B64 = /^[A-Z0-9+/]+={0,2}$/i;
|
|
17
|
+
/** Ed25519 SPKI DER 前缀(12B)+ 32B raw 公钥;从 PEM 导出 raw hex 用 */
|
|
18
|
+
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 规范串(C2-34,拼接式逐字):`<name>@<version>:<sha256>`。
|
|
22
|
+
* name/version 取 bundle manifest 值,sha256 = tgz 字节 sha256(hex 小写);
|
|
23
|
+
* 禁止自造分隔符/归一化/大小写变换——两端与夹具三方逐字一致的前提。
|
|
24
|
+
*/
|
|
25
|
+
export function canonicalString(name: string, version: string, sha256: string): string {
|
|
26
|
+
return `${name}@${version}:${sha256}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 指纹 = sha256Hex(publicKey) 全 64 hex(契约 §2.11,TOFU 展示用) */
|
|
30
|
+
export function fingerprintOfPublicKey(publicKeyHex: string): string {
|
|
31
|
+
if (!HEX64.test(publicKeyHex))
|
|
32
|
+
throw new Error(`[ojm] 公钥不是 64 hex:${publicKeyHex.slice(0, 8)}…`);
|
|
33
|
+
return createHashSha256(Buffer.from(publicKeyHex, "hex"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 用私钥 PEM 签规范串,返回标准 base64(C2-35 value 字段口径) */
|
|
37
|
+
export function signCanonical(privateKeyPem: string, canonical: string): string {
|
|
38
|
+
const key = createPrivateKey(privateKeyPem);
|
|
39
|
+
return nodeSign(null, Buffer.from(canonical, "utf8"), key).toString("base64");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 从私钥 PEM 导出 32B raw 公钥 hex(C2-35 public_key 字段口径) */
|
|
43
|
+
export function exportRawPublicKeyHex(privateKeyPem: string): string {
|
|
44
|
+
const spki = createPublicKey(createPrivateKey(privateKeyPem)).export({ format: "der", type: "spki" }) as Buffer;
|
|
45
|
+
if (spki.length !== SPKI_PREFIX.length + 32 || !spki.subarray(0, SPKI_PREFIX.length).equals(SPKI_PREFIX))
|
|
46
|
+
throw new Error("[ojm] 私钥不是 Ed25519(SPKI 形状不符)");
|
|
47
|
+
return spki.subarray(SPKI_PREFIX.length).toString("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 验签(与上游 crypto.verifyEd25519 / Node crypto.verify 同语义)。
|
|
52
|
+
* **严格拒不做归一**:public_key 非 64hex、value 非标准 base64(含 -/_)→ false;
|
|
53
|
+
* 形状合法但验不过 → false。分码(sig_format vs tampered)由调用方结构校验序负责(P2-3)。
|
|
54
|
+
*/
|
|
55
|
+
export function verifyCanonical(publicKeyHex: string, canonical: string, sigB64: string): boolean {
|
|
56
|
+
if (!HEX64.test(publicKeyHex) || !STD_B64.test(sigB64))
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
const key = createPublicKey({
|
|
60
|
+
key: Buffer.concat([SPKI_PREFIX, Buffer.from(publicKeyHex, "hex")]),
|
|
61
|
+
format: "der",
|
|
62
|
+
type: "spki",
|
|
63
|
+
});
|
|
64
|
+
return nodeVerify(null, Buffer.from(canonical, "utf8"), key, Buffer.from(sigB64, "base64"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false; // 形状逃逸检测的极端输入(如非 base64 解码残留)同样拒
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// node:crypto 的 sha256 仅指纹一处使用——独立小封装,避免把 hash 面扩散成第二契约
|
|
72
|
+
function createHashSha256(data: Buffer): string {
|
|
73
|
+
return createHash("sha256").update(data).digest("hex");
|
|
74
|
+
}
|