@oj-module/cli 0.2.2 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +189 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +1 -1
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +62 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. 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
+ }