@oj-module/cli 0.2.30 → 0.2.43
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/manual.md +5 -5
- package/devkit/module-dev-and-cdn-playground.md +1 -1
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/cli.test.ts +23 -14
- package/src/__tests__/list.test.ts +176 -0
- package/src/__tests__/registry-cli.test.ts +522 -0
- package/src/__tests__/registry-export.test.ts +319 -0
- package/src/__tests__/registry-import.test.ts +336 -0
- package/src/__tests__/registry-sign.test.ts +352 -0
- package/src/cli-output.ts +81 -0
- package/src/cli.ts +298 -39
- package/src/config-cmd.ts +100 -0
- package/src/contract/__tests__/handler-scan.test.ts +74 -2
- package/src/contract/handler-scan.ts +27 -2
- package/src/enable.ts +1 -1
- package/src/exit-codes.ts +33 -0
- package/src/index.ts +5 -9
- package/src/install.ts +120 -38
- package/src/lifecycle/__tests__/authoring.test.ts +6 -5
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +5 -4
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +9 -6
- package/src/lifecycle/__tests__/helpers.ts +10 -6
- package/src/lifecycle/__tests__/install.test.ts +72 -15
- package/src/lifecycle/__tests__/manifest-contract.test.ts +40 -1
- package/src/lifecycle/__tests__/publish.test.ts +174 -33
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +9 -6
- package/src/lifecycle/__tests__/sign.test.ts +9 -8
- package/src/lifecycle/__tests__/smoke.test.ts +1 -1
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/lifecycle/sign.ts +8 -7
- package/src/list.ts +146 -0
- package/src/manifest-contract.ts +11 -5
- package/src/market/__tests__/bootstrap.test.ts +4 -1
- package/src/market/__tests__/build-index.test.ts +529 -42
- package/src/market/__tests__/fixture.test.ts +17 -1
- package/src/market/__tests__/publish.test.ts +8 -7
- package/src/market/__tests__/setup.test.ts +4 -4
- package/src/market/__tests__/sharding.test.ts +90 -0
- package/src/market/__tests__/verify.test.ts +237 -13
- package/src/market/__tests__/yank-ledger.test.ts +60 -0
- package/src/market/bootstrap.ts +8 -3
- package/src/market/build-index.ts +407 -40
- package/src/market/cdn-layout.ts +3 -2
- package/src/market/command.ts +40 -6
- package/src/market/failure-playbook.ts +1 -1
- package/src/market/fixture.ts +20 -3
- package/src/market/index-builder.ts +120 -47
- package/src/market/publish.ts +3 -1
- package/src/market/setup.ts +3 -3
- package/src/market/sharding.ts +94 -0
- package/src/market/trial.ts +1 -1
- package/src/market/verify.ts +220 -53
- package/src/market/walkthrough.ts +7 -6
- package/src/market/yank-ledger.ts +91 -0
- package/src/publish.ts +250 -38
- package/src/registry-export.ts +405 -0
- package/src/registry-import.ts +410 -0
- package/src/registry-sidecar.ts +84 -0
- package/src/registry-sign.ts +322 -0
- package/src/registry-source.ts +192 -0
- package/src/sign.ts +13 -10
- package/src/site.ts +4 -4
- package/src/trust.ts +1 -1
- package/src/uninstall.ts +14 -2
- package/src/usage.ts +130 -25
- package/src/view.ts +94 -0
- package/templates/cdn/README.md +35 -5
- package/templates/config.yaml +46 -96
- package/templates/init.include.yaml +1 -1
- package/templates/src/_platform/api/_lifecycle/atomic.ts +3 -0
- package/templates/src/_platform/api/_lifecycle/installer.ts +44 -7
- package/templates/src/_platform/api/_lifecycle/limits.ts +21 -0
- package/templates/src/_platform/api/_lifecycle/source-seq.ts +88 -0
- package/templates/src/_platform/api/_lifecycle/steps/download.ts +15 -0
- package/templates/src/_platform/api/_lifecycle/steps/extract.ts +61 -2
- package/templates/src/_platform/api/_lifecycle/steps/preflight.ts +55 -6
- package/templates/src/_platform/api/_lifecycle/steps/register.ts +2 -1
- package/templates/src/_platform/api/_lifecycle/steps/verify.ts +22 -8
- package/templates/src/_platform/api/_lifecycle/tgz-guard.ts +96 -0
- package/templates/src/_platform/api/_lifecycle/types.ts +6 -3
- package/templates/src/_platform/api/_lifecycle/verify-checks.ts +8 -0
- package/templates/src/_platform/api/_shared/service/domain/solution-perms.ts +9 -1
- package/templates/src/_platform/api/_shared/site-facade.ts +62 -11
- package/templates/src/_platform/api/manifest.yaml +1 -0
- package/templates/src/_platform/api/market/api.ts +362 -99
- package/templates/src/_platform/api/market/index-source.ts +112 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.mysql.sql +9 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.pg.sql +7 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.sql +8 -0
- package/templates/src/_platform/api/schema.yaml +8 -0
- package/templates/src/_platform/api/solutions/api.ts +8 -3
- package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +13 -0
- package/templates/src/oj-market/web/__tests__/InstallDialog.test.tsx +38 -0
- package/templates/src/oj-market/web/components/CardItem.tsx +10 -0
- package/templates/src/oj-market/web/components/InstallDialog.tsx +18 -0
- package/templates/src/oj-market/web/locales/en-US.json +1 -0
- package/templates/src/oj-market/web/locales/zh-CN.json +1 -0
- package/templates/src/oj-market/web/types.ts +3 -1
- package/templates/tests/lifecycle/installer.spec.ts +24 -0
- package/templates/tests/lifecycle/market-api.spec.ts +641 -85
- package/templates/tests/lifecycle/preflight.spec.ts +52 -0
- package/templates/tests/lifecycle/sig-fixtures.spec.ts +7 -4
- package/templates/tests/lifecycle/source-seq.spec.ts +55 -0
- package/templates/tests/lifecycle/steps-fs.spec.ts +166 -7
- package/templates/tests/lifecycle/tgz-guard.spec.ts +133 -1
- package/templates/tests/lifecycle/verify-checks.spec.ts +27 -13
- package/templates/tests/lifecycle/verify-report.spec.ts +3 -1
- package/templates/tests/rbac-solution-perms.spec.ts +41 -0
- package/vendor/host-versions.json +3 -3
package/src/usage.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface HelpGroup {
|
|
|
58
58
|
|
|
59
59
|
/** 站点寻址三件套(生命周期命令共用) */
|
|
60
60
|
const SITE_OPTIONS: HelpOption[] = [
|
|
61
|
-
{ raw: "--url <url>", zh: "站点地址(缺省 http://127.0.0.1:9778)", en: "Site URL (default http://127.0.0.1:9778)" },
|
|
61
|
+
{ raw: "--site-url <url>", zh: "站点地址(缺省 http://127.0.0.1:9778)", en: "Site URL (default http://127.0.0.1:9778)" },
|
|
62
62
|
{ raw: "--user <user>", zh: "登录账号(或环境变量 OJM_USER)", en: "Login username (or env OJM_USER)" },
|
|
63
63
|
{ raw: "--pass <pass>", zh: "登录密码(或环境变量 OJM_PASS;只走内存不落盘)", en: "Login password (or env OJM_PASS; memory-only, never persisted)" },
|
|
64
64
|
];
|
|
@@ -197,6 +197,27 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
197
197
|
en: "ojm vendor [tag] [--force]\n tag defaults to latest, e.g. v0.1.13",
|
|
198
198
|
},
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
key: "config",
|
|
202
|
+
name: "config [sub]",
|
|
203
|
+
zh: "查看生效配置(仓库源 / 凭据来源 / 站点定位;不回显密钥)",
|
|
204
|
+
en: "Show effective config (registry source / credential origin / site addressing; never echoes secrets)",
|
|
205
|
+
options: [
|
|
206
|
+
{ raw: "--show-origin", zh: "每行附「该值从哪来」(flag / .ojmrc / 用户配置 / 环境变量)", en: "Append where each value came from (flag / .ojmrc / user config / env)" },
|
|
207
|
+
{ raw: "--json", zh: "stdout 只放一份 JSON 信封(人话改走 stderr)", en: "Emit a single JSON envelope on stdout (human lines move to stderr)" },
|
|
208
|
+
],
|
|
209
|
+
subs: {
|
|
210
|
+
ls: {
|
|
211
|
+
short: { zh: "列出生效配置(`ls` 可省略)", en: "List effective config (`ls` may be omitted)" },
|
|
212
|
+
zh: "ojm config ls [--show-origin] [--json]\n 列出仓库源 / 凭据来源 / 站点定位。未配置不是错误(退出码 0):诊断命令的职责就是把「这里空着」显示出来",
|
|
213
|
+
en: "ojm config ls [--show-origin] [--json]\n List the registry source / credential origin / site addressing. Unset is not an error (exit 0): a diagnostics command exists precisely to show that something is empty",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
detail: {
|
|
217
|
+
zh: "ojm config ls [--show-origin] [--json]\n 源解析顺序:--registry > 工程根 .ojmrc > ~/.ojm/config.json;本 CLI 不内置缺省源\n 凭据:OJM_REGISTRY_TOKEN(CI 首选)> ~/.ojm/config.json 的 registries[<origin>].token\n 只报「从哪来」,token 与密码一个字都不打印",
|
|
218
|
+
en: "ojm config ls [--show-origin] [--json]\n Source order: --registry > project .ojmrc > ~/.ojm/config.json; there is no built-in default\n Credential: OJM_REGISTRY_TOKEN (preferred in CI) > registries[<origin>].token in ~/.ojm/config.json\n Only the origin is reported — tokens and passwords are never printed",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
200
221
|
],
|
|
201
222
|
},
|
|
202
223
|
{
|
|
@@ -213,11 +234,27 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
213
234
|
...SITE_OPTIONS,
|
|
214
235
|
{ raw: "--name <name>", zh: "覆盖文件名推导的方案名", en: "Override the solution name derived from the file name" },
|
|
215
236
|
{ raw: "--version <version>", zh: "覆盖文件名推导的版本", en: "Override the version derived from the file name" },
|
|
237
|
+
{ raw: "--registry <url>", zh: "从指定 registry 源安装(白名单在服务端;旧站点 API < 0.2.0 不支持)", en: "Install from the specified registry source (allowlist is server-side; sites with API < 0.2.0 are unsupported)" },
|
|
216
238
|
{ raw: "--locale <locale>", zh: "终态文案语言 zh-CN|en-US", en: "Language of the final status message zh-CN|en-US" },
|
|
217
239
|
],
|
|
218
240
|
detail: {
|
|
219
|
-
zh: "ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]\n 本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导身份,可 --name/--version 覆盖);name[@version] 走 CDN
|
|
220
|
-
en: "ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]\n Local tgz goes through file upload (identity derived from the <name>-<version>.tgz file name, overridable via --name/--version); name[@version] goes through the CDN marketplace\n Login → submit → poll the task to a terminal state and print the result (failures include the reason + guidance)\n Site addressing: --url/--user/--pass, or env vars
|
|
241
|
+
zh: "ojm install <file.tgz | name[@version] | @<scope>/<name>[@version]> [--site-url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--registry <r>] [--locale <zh-CN|en-US>]\n 本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导身份,可 --name/--version 覆盖);name[@version] 走 CDN 市场源;@<scope>/<name>[@version] --registry <url> 走外部 registry 源(目录键由 CLI 归一为物理名 slug,白名单在服务端)\n 登录 → 提交 → 轮询任务至终态并打印结果(失败附原因 + 指引)\n 站点寻址:--site-url/--user/--pass,或环境变量 OJM_SITE_URL / OJM_USER / OJM_PASS",
|
|
242
|
+
en: "ojm install <file.tgz | name[@version] | @<scope>/<name>[@version]> [--site-url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--registry <r>] [--locale <zh-CN|en-US>]\n Local tgz goes through file upload (identity derived from the <name>-<version>.tgz file name, overridable via --name/--version); name[@version] goes through the CDN marketplace; @<scope>/<name>[@version] --registry <url> goes through an external registry source (the directory key is normalized to the physical slug by the CLI; allowlist is server-side)\n Login → submit → poll the task to a terminal state and print the result (failures include the reason + guidance)\n Site addressing: --site-url/--user/--pass, or env vars OJM_SITE_URL / OJM_USER / OJM_PASS",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
key: "list",
|
|
247
|
+
name: "list",
|
|
248
|
+
zh: "列出站点已装方案(版本 / 启用态 / 可升级 / 签名可信)",
|
|
249
|
+
en: "List solutions installed on the site (version / enabled / upgradable / trust)",
|
|
250
|
+
options: [
|
|
251
|
+
...SITE_OPTIONS,
|
|
252
|
+
{ raw: "--all", zh: "连未安装的索引行一起列(缺省只列已装)", en: "Include index rows that are not installed (default: installed only)" },
|
|
253
|
+
{ raw: "--json", zh: "stdout 只放一份 JSON 信封(人话改走 stderr)", en: "Emit a single JSON envelope on stdout (human lines move to stderr)" },
|
|
254
|
+
],
|
|
255
|
+
detail: {
|
|
256
|
+
zh: "ojm list [--site-url <u>] [--user <u>] [--pass <p>] [--all] [--json]\n 读 GET /api/_platform/market/index(superadmin):只挑已装的行,打「名字 / 已装版本 / 启用态 / 最新版 / 可升级 / 签名可信」\n --all 连未安装的索引行一起列(installed 显示 -);升级前看谁落后、禁用后确认状态、卸载前确认名字都靠它\n 退出码:0 成功(清单为空也是 0)/ 1 站点报错 / 2 用法错误(缺凭据)",
|
|
257
|
+
en: "ojm list [--site-url <u>] [--user <u>] [--pass <p>] [--all] [--json]\n Reads GET /api/_platform/market/index (superadmin) and keeps installed rows only: name / installed / enabled / latest / upgradable / trust\n --all also lists index rows that are not installed (installed shows -); use it before upgrading, after disabling, or before uninstalling\n Exit codes: 0 success (an empty list is still 0) / 1 site error / 2 usage error (missing credentials)",
|
|
221
258
|
},
|
|
222
259
|
},
|
|
223
260
|
{
|
|
@@ -227,8 +264,8 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
227
264
|
en: "Enable an installed solution (writes to four places)",
|
|
228
265
|
options: SITE_OPTIONS,
|
|
229
266
|
detail: {
|
|
230
|
-
zh: "ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]\n 启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)\n 站点寻址同上",
|
|
231
|
-
en: "ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]\n Enable/disable an installed solution (four synchronized writes: lock / sidecar / route manifest / module manifest)\n Site addressing: same as above",
|
|
267
|
+
zh: "ojm enable|disable <name> [--site-url <u>] [--user <u>] [--pass <p>]\n 启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)\n 站点寻址同上",
|
|
268
|
+
en: "ojm enable|disable <name> [--site-url <u>] [--user <u>] [--pass <p>]\n Enable/disable an installed solution (four synchronized writes: lock / sidecar / route manifest / module manifest)\n Site addressing: same as above",
|
|
232
269
|
},
|
|
233
270
|
},
|
|
234
271
|
{
|
|
@@ -243,10 +280,28 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
243
280
|
name: "uninstall [name]",
|
|
244
281
|
zh: "卸载已装方案(四处同删 + 权限级联清理)",
|
|
245
282
|
en: "Uninstall a solution (four synchronized removals + permission cascade cleanup)",
|
|
246
|
-
options:
|
|
283
|
+
options: [
|
|
284
|
+
...SITE_OPTIONS,
|
|
285
|
+
// Q7:注册出来只为拒绝——给了即 exit 2,并说明「卸载不涉及仓库」
|
|
286
|
+
{ raw: "--registry <url>", zh: "(不支持)卸载不涉及仓库:本命令只认站点寻址,给了即报错退出 2", en: "(unsupported) Uninstall does not involve a registry: this command only takes site addressing; passing it exits 2" },
|
|
287
|
+
],
|
|
247
288
|
detail: {
|
|
248
|
-
zh: "ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]\n 四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块\n
|
|
249
|
-
en: "ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]\n Four synchronized removals + permission cascade cleanup + version directory removal; a site keeps at least one locked module\n Site addressing: same as above",
|
|
289
|
+
zh: "ojm uninstall <name> [--site-url <u>] [--user <u>] [--pass <p>]\n 四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块\n 站点寻址同上;**卸载不涉及仓库**——包已经在站点上,来源不影响删除范围,故不接 --registry",
|
|
290
|
+
en: "ojm uninstall <name> [--site-url <u>] [--user <u>] [--pass <p>]\n Four synchronized removals + permission cascade cleanup + version directory removal; a site keeps at least one locked module\n Site addressing: same as above; **uninstall does not involve a registry** — the package is already on the site, so its origin has no effect on the removal scope, hence no --registry",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: "view",
|
|
295
|
+
name: "view [target]",
|
|
296
|
+
zh: "查看包映射(目录键 ↔ 物理名)",
|
|
297
|
+
en: "Show a package mapping (directory key ↔ physical name)",
|
|
298
|
+
options: [
|
|
299
|
+
...SITE_OPTIONS,
|
|
300
|
+
{ raw: "--json", zh: "stdout 只放一份 JSON 信封(人话改走 stderr)", en: "Emit a single JSON envelope on stdout (human lines move to stderr)" },
|
|
301
|
+
],
|
|
302
|
+
detail: {
|
|
303
|
+
zh: "ojm view @<scope>/<name> [--site-url <u>] [--user <u>] [--pass <p>] [--json]\n 同时显示目录键(@acme/demo)与物理名(acme-demo):前者是仓库门牌,后者才是磁盘/路由/文件名\n 只收目录键——物理名是单向换算(acme-x-demo 可能来自 @acme-x/demo 或 @acme/x-demo),裸 slug 无法反推\n 站点寻址同上;缺凭据退出 2",
|
|
304
|
+
en: "ojm view @<scope>/<name> [--site-url <u>] [--user <u>] [--pass <p>] [--json]\n Print both the directory key (@acme/demo) and the physical name (acme-demo): the former is the registry door plate, the latter is the on-disk/route/file name\n Directory keys only — the mapping is one-way (acme-x-demo may come from @acme-x/demo or @acme/x-demo), so a bare slug cannot be reversed\n Site addressing: same as above; missing credentials exit 2",
|
|
250
305
|
},
|
|
251
306
|
},
|
|
252
307
|
{
|
|
@@ -259,8 +314,8 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
259
314
|
{ raw: "--task <id>", zh: "TOFU 确认:needs_trust 终态任务 id", en: "TOFU confirm: task id of a needs_trust terminal state" },
|
|
260
315
|
],
|
|
261
316
|
detail: {
|
|
262
|
-
zh: "ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]\n 发布方信任库管理(站点 .solutions/trusted-keys.yaml)",
|
|
263
|
-
en: "ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]\n Publisher trust store management (site-side .solutions/trusted-keys.yaml)",
|
|
317
|
+
zh: "ojm trust ls|add|revoke|confirm [--site-url <u>] [--user <u>] [--pass <p>]\n 发布方信任库管理(站点 .solutions/trusted-keys.yaml)",
|
|
318
|
+
en: "ojm trust ls|add|revoke|confirm [--site-url <u>] [--user <u>] [--pass <p>]\n Publisher trust store management (site-side .solutions/trusted-keys.yaml)",
|
|
264
319
|
},
|
|
265
320
|
subs: {
|
|
266
321
|
ls: {
|
|
@@ -315,26 +370,72 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
315
370
|
options: [
|
|
316
371
|
{ raw: "--id <id>", zh: "发布方 key id", en: "Publisher key id" },
|
|
317
372
|
{ raw: "--key <pem>", zh: "指定私钥 pem(须同时 --id)", en: "Explicit private key pem (requires --id as well)" },
|
|
373
|
+
{ raw: "--registry-id <id>", zh: "签发方 registry 身份(必填;REG-CS3 v2 规范串首段,防跨仓库重放)", en: "Issuing registry id (required; first segment of the REG-CS3 v2 canonical string, blocks cross-registry replay)" },
|
|
318
374
|
{ raw: "--check", zh: "离线自检签名", en: "Offline signature self-check" },
|
|
319
375
|
],
|
|
320
376
|
detail: {
|
|
321
|
-
zh: "ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]\n detached 侧车签名:产出 <tgz>.sig(ojm install 自动探测随包上传);私钥缺省取 ~/.ojm/keys/<id>/private.pem",
|
|
322
|
-
en: "ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]\n Detached sidecar signature: produces <tgz>.sig (ojm install auto-detects and uploads it with the bundle); the private key defaults to ~/.ojm/keys/<id>/private.pem",
|
|
377
|
+
zh: "ojm sign <file.tgz> --registry-id <registry.id> [--id <publisher.id>] [--key <pem>] [--check]\n detached 侧车签名:产出 <tgz>.sig(ojm install 自动探测随包上传);私钥缺省取 ~/.ojm/keys/<id>/private.pem\n 规范串 = <registry_id>:<slug>@<version>:<sha256>——registry_id 防 A 签的包被 B 的 CDN 装;往 registry 发布时必须等于该站的 canonical_origin,否则服务端报 registry_id_mismatch",
|
|
378
|
+
en: "ojm sign <file.tgz> --registry-id <registry.id> [--id <publisher.id>] [--key <pem>] [--check]\n Detached sidecar signature: produces <tgz>.sig (ojm install auto-detects and uploads it with the bundle); the private key defaults to ~/.ojm/keys/<id>/private.pem\n Canonical string = <registry_id>:<slug>@<version>:<sha256> — the registry_id stops a bundle signed by A from being installed from B's CDN; when publishing to a registry it must equal that site's canonical_origin, otherwise the server rejects with registry_id_mismatch",
|
|
323
379
|
},
|
|
324
380
|
},
|
|
325
381
|
{
|
|
326
382
|
key: "publish",
|
|
327
383
|
name: "publish [dir]",
|
|
328
|
-
zh: "索引发布(重算
|
|
329
|
-
en: "Index publishing (recompute
|
|
384
|
+
zh: "索引发布(重算 catalog/shards + 侧车签名)",
|
|
385
|
+
en: "Index publishing (recompute catalog/shards + sidecar signatures)",
|
|
330
386
|
options: [
|
|
331
387
|
{ raw: "--id <id>", zh: "发布方 key id", en: "Publisher key id" },
|
|
332
388
|
{ raw: "--key <pem>", zh: "指定私钥 pem", en: "Explicit private key pem" },
|
|
333
|
-
{ raw: "--check", zh: "
|
|
389
|
+
{ raw: "--check", zh: "离线自检索引签名(catalog + 各分片)", en: "Offline self-check of index signatures (catalog + each shard)" },
|
|
390
|
+
{ raw: "--registry <url>", zh: "仓库源(覆盖 .ojmrc / 用户配置;本 CLI 无内置缺省源)", en: "Registry URL (overrides .ojmrc / user config; no built-in default)" },
|
|
391
|
+
{ raw: "--dry-run", zh: "只算不写:打印本次会写哪些文件,盘上零改动", en: "Compute only: print which files would be written, change nothing on disk" },
|
|
392
|
+
{ raw: "--json", zh: "stdout 只放一份 JSON 信封(人话改走 stderr)", en: "Emit a single JSON envelope on stdout (human lines move to stderr)" },
|
|
334
393
|
],
|
|
335
394
|
detail: {
|
|
336
|
-
zh: "ojm publish <dir> [--id <key>] [--key <pem>] [--check]\n 重算 <dir>/
|
|
337
|
-
en: "ojm publish <dir> [--id <key>] [--key <pem>] [--check]\n Recompute <dir>/
|
|
395
|
+
zh: "ojm publish <dir> [--id <key>] [--key <pem>] [--check] [--registry <url>] [--dry-run] [--json]\n 重算 <dir>/catalog.yaml + shards/<n>.yaml(确定性 stringify,消弭格式漂移)→ 各产出 detached 侧车签名 → 写回 <dir>\n 未配仓库源时只写本地目录(不上传),并明说这一点;--dry-run 全程零写入\n 退出码:0 成功 / 1 业务失败(签名不符、私钥缺失、--check 没过)/ 2 用法错误",
|
|
396
|
+
en: "ojm publish <dir> [--id <key>] [--key <pem>] [--check] [--registry <url>] [--dry-run] [--json]\n Recompute <dir>/catalog.yaml + shards/<n>.yaml (deterministic stringify to eliminate format drift) → produce a detached sidecar signature for each → write back to <dir>\n With no registry source it only writes the local directory (no upload) and says so; --dry-run writes nothing at all\n Exit codes: 0 success / 1 business failure (signature mismatch, missing key, --check failed) / 2 usage error",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
key: "registry",
|
|
403
|
+
zh: "registry 索引外置签名(CI 侧)",
|
|
404
|
+
en: "Registry index signing (CI side)",
|
|
405
|
+
commands: [
|
|
406
|
+
{
|
|
407
|
+
key: "registry",
|
|
408
|
+
name: "registry [sub] [file]",
|
|
409
|
+
zh: "索引外置签名:把 registry 导出的未签索引签成回传请求体",
|
|
410
|
+
en: "Index signing: turn an unsigned export from a registry into an upload body",
|
|
411
|
+
options: [
|
|
412
|
+
{ raw: "--id <id>", zh: "索引签名 key_id(导出里只有一把 active key 时可省)", en: "Index signing key_id (optional when the export has exactly one active key)" },
|
|
413
|
+
{ raw: "--key <pem>", zh: "私钥 pem(缺省 ~/.ojm/keys/<id>/private.pem)", en: "Private key pem (default ~/.ojm/keys/<id>/private.pem)" },
|
|
414
|
+
{ raw: "--site-url <url>", zh: "export:registry 站点地址(该站 canonical_origin,形如 https://site.example.com)", en: "export: registry site URL (that site's canonical_origin, e.g. https://site.example.com)" },
|
|
415
|
+
{ raw: "--out <target>", zh: "sign:回传请求体落盘位置;import:本地索引目录(private 站点无 /cdn 挂载时用);export:产物根目录(落 <out>/registry/ 与 source.json)", en: "sign: upload body path; import: local index directory (use for private sites without a /cdn mount); export: output root (<out>/registry/ and source.json)" },
|
|
416
|
+
{ raw: "--site <dir>", zh: "import:站点数据根,读 config.yaml 的 /cdn 挂载自动落到 <cdn>/registry/(public 载体)", en: "import: site data root; reads the /cdn mount from config.yaml and writes into <cdn>/registry/ (public carrier)" },
|
|
417
|
+
{ raw: "--keys <file>", zh: "import:active 索引 key 集合 JSON(数组 [{key_id,public_key,status}]);缺省读本地信任库", en: "import: active index-key set JSON (array of [{key_id,public_key,status}]); defaults to the local trust store" },
|
|
418
|
+
{ raw: "--registry <origin>", zh: "import:本地 seq 账本源标识(多源各记各的);缺省取 <dir>/source.json 的 origin", en: "import: local seq-ledger origin (one ledger per source); defaults to <dir>/source.json origin" },
|
|
419
|
+
{ raw: "--revocations", zh: "sign:输入改按吊销清单正文原文解释(签 revocations:<seq>:<sha256> 规范串),产出 POST /revocations/signature 的请求体", en: "sign: treat the input as the revocation list body (sign the revocations:<seq>:<sha256> canonical string) and produce the POST /revocations/signature request body" },
|
|
420
|
+
{ raw: "--dry-run", zh: "import:只校验 + 列出本应写入的文件,零落盘;export:只验索引 + 列出待下载文件,不下载包体、零落盘", en: "import: verify only and list files that would be written, write nothing; export: verify the index and list files, download no bundles, write nothing" },
|
|
421
|
+
{ raw: "--json", zh: "stdout 只放一份 JSON 信封(人话改走 stderr)", en: "Emit a single JSON envelope on stdout (human lines move to stderr)" },
|
|
422
|
+
],
|
|
423
|
+
subs: {
|
|
424
|
+
sign: {
|
|
425
|
+
short: { zh: "签名导出的未签索引 / 吊销清单,产出回传请求体", en: "Sign an exported unsigned index or the revocation list, producing the upload body" },
|
|
426
|
+
zh: "ojm registry sign <export.json> --id <key> [--key <pem>] [--out <file>] [--json]\n 输入 = POST /index/unsigned 的响应体(gen + pending + keys);输出 = POST /index/signature 的请求体(gen + signatures)\n 私钥缺省 ~/.ojm/keys/<id>/private.pem;`-` 表示从 stdin 读导出体\n 三条 fail-closed:公钥未登记或指纹不符(index_key_mismatch)/ 正文与声明的 sha256 不符(index_export_tampered)/ 签完自检不过(index_sig_mismatch)\n 这一代没有待签文件时是幂等的:不碰私钥,直接产出空 signatures\n --revocations:输入改按**吊销清单正文原文**解释(GET /api/oj-registry/cdn/revocations 的响应体,或 ojm registry export 产出的 registry/revocations.yaml)\n 签的是 revocations:<seq>:<sha256(正文)>,产出 POST /revocations/signature 的六字段请求体(seq/alg/key_id/public_key/value/canon)\n 必须 --id(这一处没有导出体可推断 key);正文必须逐字节原文——中间任何重渲染都会让 sha 变、服务端按 revocation_sig_format 拒收\n 退出码:0 成功 / 1 业务失败 / 2 用法错误",
|
|
427
|
+
en: "ojm registry sign <export.json> --id <key> [--key <pem>] [--out <file>] [--json]\n Input = the POST /index/unsigned response body (gen + pending + keys); output = the POST /index/signature request body (gen + signatures)\n The private key defaults to ~/.ojm/keys/<id>/private.pem; `-` reads the export from stdin\n Three fail-closed checks: unregistered key or fingerprint mismatch (index_key_mismatch) / body does not match the declared sha256 (index_export_tampered) / self-verification failed (index_sig_mismatch)\n Idempotent when the generation has nothing pending: no key is touched, empty signatures are produced\n --revocations: treat the input as the **revocation list body verbatim** (the GET /api/oj-registry/cdn/revocations response, or registry/revocations.yaml from ojm registry export)\n Signs revocations:<seq>:<sha256(body)> and produces the six-field POST /revocations/signature request body (seq/alg/key_id/public_key/value/canon)\n --id is mandatory here (there is no export body to infer it from); the body must be byte-exact — any re-rendering changes the sha and the server rejects with revocation_sig_format\n Exit codes: 0 success / 1 business failure / 2 usage error",
|
|
428
|
+
},
|
|
429
|
+
import: {
|
|
430
|
+
short: { zh: "把离线包目录恢复到站点本地(全程不触网,源挂了也能救)", en: "Restore an offline package directory to a site locally (fully offline, works even when the source is down)" },
|
|
431
|
+
zh: "ojm registry import <dir> [--site <站点数据根>] [--out <目标目录>] [--keys <active索引key.json>] [--registry <origin>] [--dry-run] [--json]\n 离线包结构(见 REG-R11 草案):<dir>/registry/{catalog.yaml(+.sig), shards/<n>.yaml(+.sig), revocations.yaml(+.sig), <slug>-<version>.tgz[+.sig]}\n 三步(与线上 REG-CS1 / REG-R10 同语义、同判别码):\n ① 验签:catalog / 各分片 / revocations 的正文复用站点已登记的 active 索引 key 验签(Ed25519)\n ② 吊销 seq 护栏:本地账本 ~/.ojm/registry/<origin>/revocation-seq.json 比代次,回退/缩水/同代清空都拒(revocations_not_monotonic)\n ③ 落盘:public 站点用 --site 自动落到 <cdn>/registry/,private 站点用 --out 直接指定本地索引目录;原样拷贝不重渲染\n 信任源:--keys 指定 > ~/.ojm/registry/<origin>/index-keys.json 本地信任库;取不到活跃 key 即 fail-closed(index_key_mismatch);registry 不新增 mounts\n 退出码:0 成功 / 1 业务失败(验签不过、seq 回退、格式错)/ 2 用法错误",
|
|
432
|
+
en: "ojm registry import <dir> [--site <site-root>] [--out <target-dir>] [--keys <active-index-key.json>] [--registry <origin>] [--dry-run] [--json]\n Offline package layout (see REG-R11 draft): <dir>/registry/{catalog.yaml(+.sig), shards/<n>.yaml(+.sig), revocations.yaml(+.sig), <slug>-<version>.tgz[+.sig]}\n Three steps (same semantics and codes as online REG-CS1 / REG-R10):\n ① verify: catalog / each shard / revocations verified against the site's registered active index keys (Ed25519)\n ② revocation seq guard: local ledger ~/.ojm/registry/<origin>/revocation-seq.json compares generations; rollback / shrink / same-gen wipe are all rejected (revocations_not_monotonic)\n ③ write: public sites use --site to land in <cdn>/registry/; private sites use --out for the local index dir; copied verbatim, never re-rendered\n Trust: --keys > ~/.ojm/registry/<origin>/index-keys.json local trust store; no active key → fail-closed (index_key_mismatch); registry adds no mounts\n Exit codes: 0 success / 1 business failure (bad signature, seq rollback, bad format) / 2 usage error",
|
|
433
|
+
},
|
|
434
|
+
export: {
|
|
435
|
+
short: { zh: "从 registry 读面拉一个 import 能直接吃的离线包(private 站点也能产)", en: "Pull an import-ready offline package from the registry read surface (works for private sites too)" },
|
|
436
|
+
zh: "ojm registry export --site-url <registry 站点地址> [--out <目录>] [--dry-run] [--json]\n 产物(与 registry import 逐字对齐):<out>/source.json + <out>/registry/{catalog.yaml(+.sig), shards/<n>.yaml(+.sig), revocations.yaml(+.sig), <slug>-<version>.tgz(+.sig)}\n 走读面 /api/oj-registry/cdn/…(两种载体行为一致),正文与 .sig 侧车分开取\n 三条当面判掉:正文与侧车对不上(index_sig_mismatch)/ 分片代数≠总目录钉值(index_generation_mismatch,源正在重建)/ 包体 sha256 或长度≠索引声明(package_sha_mismatch)\n 只验自洽不验归属:公钥是否属于本站 active 索引 key 由 import 用本地信任库判——导出侧拿源给的钥匙验源给的签名等于没验\n 先全下到隐藏暂存目录、最后整体换位:中途失败不会在 <out>/registry/ 留下半份包;--dry-run 只验索引不下载包体\n 退出码:0 成功 / 1 业务失败(验签不过、摘要不符、站点取不到)/ 2 用法错误",
|
|
437
|
+
en: "ojm registry export --site-url <registry-site-url> [--out <dir>] [--dry-run] [--json]\n Output (byte-for-byte aligned with registry import): <out>/source.json + <out>/registry/{catalog.yaml(+.sig), shards/<n>.yaml(+.sig), revocations.yaml(+.sig), <slug>-<version>.tgz(+.sig)}\n Uses the read surface /api/oj-registry/cdn/… (identical on both carriers), fetching bodies and .sig sidecars separately\n Three checks up front: body does not match its sidecar (index_sig_mismatch) / shard generation ≠ catalog pin (index_generation_mismatch — the source is rebuilding) / bundle sha256 or size ≠ what the index declares (package_sha_mismatch)\n Integrity only, not trust: whether the key is one of the site's active index keys is decided by import against the local trust store — verifying a source-signed body with a source-supplied key proves nothing\n Downloads into a hidden staging dir and swaps at the end: a mid-way failure never leaves half a package in <out>/registry/; --dry-run verifies the index and downloads no bundles\n Exit codes: 0 success / 1 business failure (bad signature, digest mismatch, source unreachable) / 2 usage error",
|
|
438
|
+
},
|
|
338
439
|
},
|
|
339
440
|
},
|
|
340
441
|
],
|
|
@@ -351,7 +452,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
351
452
|
en: "Local mock CDN and marketplace pipeline (setup / publish / build-index / verify / trial)",
|
|
352
453
|
allowUnknownOptions: true,
|
|
353
454
|
options: [
|
|
354
|
-
{ raw: "--url <url>", zh: "站点地址", en: "Site URL" },
|
|
455
|
+
{ raw: "--site-url <url>", zh: "站点地址", en: "Site URL" },
|
|
355
456
|
{ raw: "--user <user>", zh: "登录账号", en: "Login username" },
|
|
356
457
|
{ raw: "--pass <pass>", zh: "登录密码", en: "Login password" },
|
|
357
458
|
{ raw: "--new-pass <pass>", zh: "首次改密期的新密码(setup)", en: "New password during the forced-reset period (setup)" },
|
|
@@ -365,8 +466,8 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
365
466
|
subs: {
|
|
366
467
|
"setup": {
|
|
367
468
|
short: { zh: "framework.market_cdn_url 指向外部真 CDN(脚手架缺省 /cdn 免 setup)", en: "Point framework.market_cdn_url at an external CDN (scaffold defaults to /cdn, no setup needed)" },
|
|
368
|
-
zh: "ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]\n 把系统设置 framework.market_cdn_url 改写为绝对地址(外部真 CDN 场景;脚手架 seed 缺省 /cdn = 本站点自供,免 setup)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入",
|
|
369
|
-
en: "ojm market setup [--url <site>] [--user <u> --pass <p>] [--new-pass <p>]\n Rewrite the framework.market_cdn_url system setting to an absolute URL (external CDN scenario; the scaffold seed defaults to /cdn = site-served, no setup needed). On first run, if the account is in the forced password-reset period (seed admin must_change=1), it resets the password to obtain a full-privilege token before writing",
|
|
469
|
+
zh: "ojm market setup [--site-url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]\n 把系统设置 framework.market_cdn_url 改写为绝对地址(外部真 CDN 场景;脚手架 seed 缺省 /cdn = 本站点自供,免 setup)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入",
|
|
470
|
+
en: "ojm market setup [--site-url <site>] [--user <u> --pass <p>] [--new-pass <p>]\n Rewrite the framework.market_cdn_url system setting to an absolute URL (external CDN scenario; the scaffold seed defaults to /cdn = site-served, no setup needed). On first run, if the account is in the forced password-reset period (seed admin must_change=1), it resets the password to obtain a full-privilege token before writing",
|
|
370
471
|
},
|
|
371
472
|
"publish": {
|
|
372
473
|
short: { zh: "方案包落进 cdn/ + 包级签名(不碰索引)", en: "Drop a bundle into cdn/ + package-level signature (index untouched)" },
|
|
@@ -374,14 +475,14 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
374
475
|
en: "ojm market publish [--fixture <name@version>] [--real <project>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]\n Drop a solution bundle into cdn/: synthesize a fixture (or --real via ojm build --bundle) → deterministic tgz → package-level detached signature <name>-<version>.tgz.sig. Does not touch the index (the index is rebuilt by build-index so concurrent publishers don't overwrite each other)",
|
|
375
476
|
},
|
|
376
477
|
"build-index": {
|
|
377
|
-
short: { zh: "
|
|
378
|
-
zh: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n 扫描 cdn/*.tgz、提取包内 manifest,全量重建
|
|
379
|
-
en: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n Scan cdn/*.tgz, extract each bundle's manifest, fully rebuild
|
|
478
|
+
short: { zh: "扫描重建三级索引(catalog + shards)并签名", en: "Rebuild the three-tier index (catalog + shards) from a scan and sign" },
|
|
479
|
+
zh: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n 扫描 cdn/*.tgz、提取包内 manifest,全量重建 catalog.yaml(包名→分片号)+ shards/<n>.yaml(每片 ≤1000 版本)并签名。原子落盘(先分片后 catalog);结果只取决于目录内容:连续执行字节一致",
|
|
480
|
+
en: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n Scan cdn/*.tgz, extract each bundle's manifest, fully rebuild catalog.yaml (name→shard) + shards/<n>.yaml (≤1000 versions each) and sign them. Atomic landing (shards before catalog); the result depends only on directory content: consecutive runs are byte-identical",
|
|
380
481
|
},
|
|
381
482
|
"verify": {
|
|
382
483
|
short: { zh: "离线验包(sha256 / 验签 / 索引 / manifest 结构)", en: "Offline bundle verification (sha256 / signatures / index / manifest)" },
|
|
383
|
-
zh: "ojm market verify [--file <tgz>] [--rebuild-index]\n 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 /
|
|
384
|
-
en: "ojm market verify [--file <tgz>] [--rebuild-index]\n Offline bundle verification (no site, no install, no credentials): sha256 integrity / package-level Ed25519 signature / index signature / manifest structure in separate sections. --rebuild-index additionally
|
|
484
|
+
zh: "ojm market verify [--file <tgz>] [--rebuild-index]\n 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签(catalog 与各分片分列)/ manifest 结构分列输出。--rebuild-index 额外双向比对「扫描重建的索引」与盘上索引:既报落了包却没重建(索引缺条目),也报索引声明了、盘上包却没了的反向漂移。注意:校验通过只说明来源与完整性,不构成内容安全性背书",
|
|
485
|
+
en: "ojm market verify [--file <tgz>] [--rebuild-index]\n Offline bundle verification (no site, no install, no credentials): sha256 integrity / package-level Ed25519 signature / index signature (catalog and each shard listed separately) / manifest structure in separate sections. --rebuild-index additionally runs a two-way comparison of the index rebuilt from a scan against the one on disk: it reports both a dropped bundle whose index entry is missing, and the reverse drift where the index declares a version whose tgz is gone. Note: passing only proves origin and integrity — it is not an endorsement of content safety",
|
|
385
486
|
},
|
|
386
487
|
"trial": {
|
|
387
488
|
short: { zh: "一次性隔离站点试装,跑完即毁", en: "Full install trial in a throwaway isolated site" },
|
|
@@ -476,6 +577,10 @@ export function topHelpSections(lang: Lang): HelpSection[] {
|
|
|
476
577
|
body: [
|
|
477
578
|
`ojm --version | -V ${lang === "zh-CN" ? "显示 cli 包版本" : "Show the cli package version"}`,
|
|
478
579
|
`ojm <cmd> --help ${lang === "zh-CN" ? "单命令详解(如 ojm market setup --help)" : "Per-command details (e.g. ojm market setup --help)"}`,
|
|
580
|
+
`--json ${lang === "zh-CN" ? "机器通道:stdout 只放一份 JSON 信封,人话改走 stderr" : "Machine channel: one JSON envelope on stdout, human lines move to stderr"}`,
|
|
581
|
+
lang === "zh-CN"
|
|
582
|
+
? "退出码 0 成功 / 1 业务失败 / 2 用法错误(改脚本即可)"
|
|
583
|
+
: "Exit codes 0 success / 1 business failure / 2 usage error (fix the command line)",
|
|
479
584
|
].join("\n"),
|
|
480
585
|
},
|
|
481
586
|
];
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// `ojm view <@scope/name>`(REG-R5 的可查面):把「目录键 ↔ 物理名」**同时**显示出来。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要这个命令:
|
|
4
|
+
// · 目录键是仓库里的门牌(`@acme/demo`),物理名才是磁盘目录/路由前缀/tgz 文件名
|
|
5
|
+
// (`acme-demo`)。用户看得见前者、装得上后者,中间那条换算如果不给查询口,
|
|
6
|
+
// 就只能靠用户自己猜;
|
|
7
|
+
// · 换算**不可逆**(`acme-x-demo` 可能来自 `@acme-x/demo` 或 `@acme/x-demo`),所以
|
|
8
|
+
// 命令只接受目录键,不接受裸 slug——反推出来的答案有歧义时,宁可不答。
|
|
9
|
+
//
|
|
10
|
+
// 命令本身不做业务判断:身份、授权、映射都是站点 registry 模块的职责(REG-R1),
|
|
11
|
+
// 这里只负责「登录 → 取一条 → 两个名字并排打印」。`--json` 走统一信封(REG-R12)。
|
|
12
|
+
import { emit } from "./cli-output";
|
|
13
|
+
import { UsageError } from "./exit-codes";
|
|
14
|
+
import { callApi, login, resolveSite } from "./site";
|
|
15
|
+
|
|
16
|
+
/** registry 模块的目录名(也是路由前缀):与站点侧 manifest.name 同值 */
|
|
17
|
+
export const REGISTRY_MODULE = "oj-registry";
|
|
18
|
+
|
|
19
|
+
/** 目录键每段的字符集:与站点侧 naming.ts 同规则(CLI 先拦一次,省掉一趟网络往返) */
|
|
20
|
+
export const SEGMENT_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
21
|
+
|
|
22
|
+
export interface ViewTarget {
|
|
23
|
+
scope: string
|
|
24
|
+
name: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `@acme/demo` / `acme/demo` → `{ scope, name }`;裸名与非目录键一律用法错误(exit 2) */
|
|
28
|
+
export function parseViewTarget(raw: unknown): ViewTarget {
|
|
29
|
+
const text = String(raw ?? "").trim().replace(/^@/, "");
|
|
30
|
+
const slash = text.indexOf("/");
|
|
31
|
+
if (slash <= 0 || slash === text.length - 1)
|
|
32
|
+
throw new UsageError(`[ojm] view 需要目录键 @<scope>/<name>(如 @acme/demo;收到「${String(raw ?? "")}」)——物理名是单向换算,裸 slug 无法反推唯一的 scope`);
|
|
33
|
+
const scope = text.slice(0, slash);
|
|
34
|
+
const name = text.slice(slash + 1);
|
|
35
|
+
if (!SEGMENT_RE.test(scope) || !SEGMENT_RE.test(name))
|
|
36
|
+
throw new UsageError(`[ojm] 目录键「${String(raw ?? "")}」不合法:scope 与 name 都须匹配 ${SEGMENT_RE.source}`);
|
|
37
|
+
return { scope, name };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PackageView {
|
|
41
|
+
scope: string
|
|
42
|
+
name: string
|
|
43
|
+
slug: string
|
|
44
|
+
directoryKey: string
|
|
45
|
+
createdAt: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ViewOptions {
|
|
49
|
+
siteUrl?: string
|
|
50
|
+
user?: string
|
|
51
|
+
pass?: string
|
|
52
|
+
json?: boolean
|
|
53
|
+
/** 测试注入;缺省全局 fetch */
|
|
54
|
+
fetchFn?: typeof fetch
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 取一条包映射并打印。
|
|
59
|
+
* 站点寻址沿用 install/uninstall 的同一套(`--site-url/--user/--pass` 或 `OJM_SITE_URL/OJM_USER/OJM_PASS`);
|
|
60
|
+
* 缺凭据是**用法错误**(exit 2):换个命令参数就能解决,不该和「站点说没有这个包」混成同一档。
|
|
61
|
+
*/
|
|
62
|
+
export async function viewCommand(raw: string | undefined, opts: ViewOptions = {}): Promise<PackageView> {
|
|
63
|
+
const { scope, name } = parseViewTarget(raw);
|
|
64
|
+
let site: { base: string, user: string, pass: string };
|
|
65
|
+
try {
|
|
66
|
+
site = resolveSite(opts);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new UsageError((error as Error).message);
|
|
70
|
+
}
|
|
71
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
72
|
+
const token = await login(fetchFn, site.base, site.user, site.pass);
|
|
73
|
+
const data = await callApi<PackageView>(
|
|
74
|
+
fetchFn,
|
|
75
|
+
site.base,
|
|
76
|
+
token,
|
|
77
|
+
"GET",
|
|
78
|
+
`/api/${REGISTRY_MODULE}/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
|
|
79
|
+
);
|
|
80
|
+
const createdTs = Number(data.createdAt);
|
|
81
|
+
const when = Number.isFinite(createdTs) && createdTs > 0
|
|
82
|
+
? new Date(createdTs).toISOString().slice(0, 16).replace("T", " ")
|
|
83
|
+
: "(未知)";
|
|
84
|
+
emit({
|
|
85
|
+
command: "view",
|
|
86
|
+
data: { ...data, site: site.base },
|
|
87
|
+
human: [
|
|
88
|
+
`[ojm] 目录键 ${data.directoryKey}`,
|
|
89
|
+
`[ojm] 物理名 ${data.slug}`,
|
|
90
|
+
`[ojm] 建立于 ${when}(UTC)· 站点 ${site.base}`,
|
|
91
|
+
],
|
|
92
|
+
}, { json: !!opts.json });
|
|
93
|
+
return data;
|
|
94
|
+
}
|
package/templates/cdn/README.md
CHANGED
|
@@ -19,7 +19,34 @@
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| 本 README | ✅ 唯一进仓的文件 |
|
|
21
21
|
| `<name>-<version>.tgz` 及其 `.tgz.sig` | ❌ 产物 |
|
|
22
|
-
| `
|
|
22
|
+
| `catalog.yaml` / `shards/<n>.yaml` 及其 `.sig` | ❌ 产物 |
|
|
23
|
+
|
|
24
|
+
## 索引布局(REG-CS5 三级化)
|
|
25
|
+
|
|
26
|
+
发布面(`ojm market build-index` / `ojm publish`)落盘三件套,读面按同一布局消费:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
catalog.yaml 总目录(提交点):{seq, issued_at, expires_at,
|
|
30
|
+
shards: {<片号>: <代次钉值>}, solutions: [{name, shards: [<片号>...]}]}
|
|
31
|
+
——只记「包名 → 分片号」与代次/时效,无版本明细
|
|
32
|
+
shards/<n>.yaml 分片(纯内容无 meta):{solutions: [{name, latest,
|
|
33
|
+
versions: [<Form B 条目>]}]}——版本条目即 manifest,
|
|
34
|
+
新版本进新高片(装箱);byte 相同的分片可跨代复用
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- 每份索引各配一份 detached `.sig`,Ed25519,**签名覆盖 `(kind, seq)`**——换文件种类或换代都验不过。
|
|
38
|
+
- 安装侧读 `catalog.yaml` → 按包名拿分片号 → **只拉命中那片**(latest 安装 ≤ 1 catalog + 1 shard);
|
|
39
|
+
市场页要 `available` 全集,才拉 catalog 指向的全部分片。
|
|
40
|
+
- 同代校验是**钉值**口径:分片签名 seq 必须等于 catalog 对该片的钉值,不等即拒(`index_generation_mismatch`,
|
|
41
|
+
提示源在重建索引)——不是恒等于 catalog 代次。
|
|
42
|
+
|
|
43
|
+
## 缓存口径(读面)
|
|
44
|
+
|
|
45
|
+
- 挂载带 `Cache-Control: max-age=60, stale-while-revalidate=60`(见工程根 `config.yaml`):
|
|
46
|
+
60s 新鲜窗口 + 60s 陈旧可用窗口——yank 最坏 120s 生效(Q8)。
|
|
47
|
+
- oj 静态层原生支持 ETag / If-None-Match → 304:读取器第二轮起发条件请求,未变则不重传正文。
|
|
48
|
+
- **读取器不再发 `cache-control: no-cache`**(旧口径已删):新鲜度由服务端缓存头 + 条件请求承载,
|
|
49
|
+
索引自身的 `expires_at`(过期即拒)与 `seq` 水位(防回放)仍是硬闸。
|
|
23
50
|
|
|
24
51
|
## 生产怎么切出去
|
|
25
52
|
|
|
@@ -28,19 +55,22 @@
|
|
|
28
55
|
```yaml
|
|
29
56
|
- prefix: /cdn
|
|
30
57
|
web: ./cdn
|
|
58
|
+
headers:
|
|
59
|
+
Cache-Control: "max-age=60, stale-while-revalidate=60"
|
|
31
60
|
```
|
|
32
61
|
|
|
33
62
|
2. 把真 CDN(对象存储 / nginx)的**绝对地址**写进系统设置 `framework.market_cdn_url`;
|
|
34
|
-
命令行等价写法是 `ojm market setup --url <地址>`。
|
|
35
|
-
3. 索引(`
|
|
63
|
+
命令行等价写法是 `ojm market setup --site-url <地址>`。
|
|
64
|
+
3. 索引(`catalog.yaml` / `shards/*.yaml`)由 CDN 侧维护:`ojm market build-index` 在本地产出后自行上传。
|
|
65
|
+
真 CDN 侧自行配等价缓存头与 ETag(对象存储默认带 ETag;nginx 可复用上面的 Cache-Control 值)。
|
|
36
66
|
|
|
37
67
|
## 签名与校验口径
|
|
38
68
|
|
|
39
69
|
- **包级(信任锚)**:每个 `<name>-<version>.tgz` 配一份 detached `.tgz.sig`,Ed25519,由发布方私钥签署。
|
|
40
70
|
安装时逐字节校验,验不过即拒绝安装(fail-closed)。
|
|
41
|
-
- **索引级(只做完整性)**:`
|
|
71
|
+
- **索引级(只做完整性)**:`catalog.yaml` / `shards/<n>.yaml` 各自一份 `.sig`,用于发现目录或索引被篡改;
|
|
42
72
|
**它不是信任锚**——陌生发布方的取舍由信任库决定。
|
|
43
|
-
-
|
|
73
|
+
- **fail-closed**:索引缺签名/签名不符/跨文件不同代/结构破损,一律拒载(502),不降级、不静默跳过。
|
|
44
74
|
|
|
45
75
|
## 陌生发布方(TOFU)
|
|
46
76
|
|