@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.
- 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 +189 -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 +62 -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,883 @@
|
|
|
1
|
+
// 编排端点 + 市场索引(P1-12,R9/ISP):/api/_platform/market/* 端点族同文件嵌套 handler 组
|
|
2
|
+
// (oj v0.1.64 组形态,notification/api.ts 同款;组名 = 路径段,路由不变;嵌套组最深 4 层,oj bridge/mod.rs walker)。
|
|
3
|
+
// 门(R9):install/task/index 全 assertSuperAdmin 独占(市场页是「市场即样例」的第一方特权
|
|
4
|
+
// 例外,R29;权限码不构成门);meta 登录即可(peerPlatform 预检先于判权)。
|
|
5
|
+
// 职责边界(ISP):端点只做 门禁 + Form B 索引解析(安装器不拼 URL,installer.ts 同纪律)+
|
|
6
|
+
// InstallRequest 组装;任务生命周期/失败三分/终态审计全归 runInstall 状态机(P1-10),端点不重复编排。
|
|
7
|
+
// CDN 基址:sys_setting `framework.market_cdn_url`(PRD §5.3,缺省空 = 市场不可用,可读错误不崩);
|
|
8
|
+
// 索引/清单拉取恒带 Cache-Control: no-cache(索引时效性)。
|
|
9
|
+
// 站点路径缺省(marketEnv):oj serve release 布局生产口径(P1-16 接线 B3/B4)——共享根 = serve
|
|
10
|
+
// 挂载根 dist(锁 dist/manifests.yaml、版本目录 dist/<name>-<ver>/、versions.json 同根),
|
|
11
|
+
// 暂存/任务/sidecar 留 dist 外(R13 不与装配树冲突);全字段可注入覆盖(测试 stub 同款)。
|
|
12
|
+
// file 源上传形态:照 personal-center/upload/api.ts 先例(http.files 双路径,大文件走 blob);
|
|
13
|
+
// 上传包先过 R1 三步范式(list→validate→extract)取出 manifest.yaml 供 preflight——
|
|
14
|
+
// 未校验不解包,zip-slip 在信任边界外被拒。
|
|
15
|
+
import type { FetchFace } from "../_lifecycle/steps/download";
|
|
16
|
+
import type { ExecFace } from "../_lifecycle/steps/extract";
|
|
17
|
+
import type { ReloadFace } from "../_lifecycle/steps/reload";
|
|
18
|
+
import type { FetchInitFace } from "../_shared/site-facade";
|
|
19
|
+
import { atomicWriteText, fsFace } from "../_lifecycle/atomic";
|
|
20
|
+
import { latestAppliedSeqOf, reconcile, runInstall, setEnabled } from "../_lifecycle/installer";
|
|
21
|
+
import { compareVersions } from "../_lifecycle/semver";
|
|
22
|
+
import { sha384B64 } from "../_lifecycle/sha384";
|
|
23
|
+
import { createStateStore } from "../_lifecycle/state";
|
|
24
|
+
import { flattenBundleRoot } from "../_lifecycle/steps/extract";
|
|
25
|
+
import { getTask, readTasks } from "../_lifecycle/tasks";
|
|
26
|
+
import { validateTarListing } from "../_lifecycle/tgz-guard";
|
|
27
|
+
import { createTrustStore } from "../_lifecycle/trust";
|
|
28
|
+
import { computeUninstallImpact, runUninstall } from "../_lifecycle/uninstall";
|
|
29
|
+
import { respond } from "../_shared/respond";
|
|
30
|
+
import { assertSuperAdmin } from "../_shared/service/access";
|
|
31
|
+
import { journalHeadOf, siteOrigin, withSiteOrigin } from "../_shared/site-facade";
|
|
32
|
+
import { BOOTSTRAP_KEY, seedBootstrapKey } from "./bootstrap";
|
|
33
|
+
|
|
34
|
+
// 站点编排面共享真源落 _shared/site-facade(P2-16 真机构建钉出:api.ts 是路由入口不可被模块内
|
|
35
|
+
// import);此处 re-export 保持既有引用面(market-api.spec / 历史 import 路径)不变。
|
|
36
|
+
export { journalHeadOf, siteOrigin, withSiteOrigin };
|
|
37
|
+
export type { FetchInitFace };
|
|
38
|
+
|
|
39
|
+
/** 平台编排面版本(镜像 _platform/manifest.yaml version;peerPlatform 预检口径,manifest 升版须同步) */
|
|
40
|
+
const API_VERSION = "0.1.0";
|
|
41
|
+
|
|
42
|
+
export interface MarketEnv {
|
|
43
|
+
/** 共享根(manifests.yaml 所在目录 = 版本目录父级,D3 单 mount 形态) */
|
|
44
|
+
sharedRoot: string
|
|
45
|
+
/** 暂存目录(.staging) */
|
|
46
|
+
staging: string
|
|
47
|
+
/** 任务状态目录(.install-tasks) */
|
|
48
|
+
taskDir: string
|
|
49
|
+
/** sidecar 状态文件(.solutions/state.yaml) */
|
|
50
|
+
statePath: string
|
|
51
|
+
/** 宿主信任库(.solutions/trusted-keys.yaml;P2-2;U6/P3-1 起移出 modules/ 安装子树,落 deploy 根,仅 _platform 特权经 platform.trusted_paths 可写) */
|
|
52
|
+
trustPath: string
|
|
53
|
+
/** sqlite 库文件路径(R14 备份源;模板 config.yaml `sqlite://db.sqlite` → 站点根 db.sqlite) */
|
|
54
|
+
dbPath: string
|
|
55
|
+
/** 站点 web 模块目录(<dist>/modules) */
|
|
56
|
+
siteModulesDir: string
|
|
57
|
+
/** 站点 modules.json 路径(与 siteModulesDir 同属 <dist>) */
|
|
58
|
+
modulesJsonPath: string
|
|
59
|
+
/** API 前缀(smoke 探针 URL 前缀) */
|
|
60
|
+
apiPrefix: string
|
|
61
|
+
/** 站点 config.yaml 路径(reloading 段 vars.ojm_install_seq 自增落盘处;缺省 config.yaml) */
|
|
62
|
+
configPath?: string
|
|
63
|
+
/** CDN 基址(缺省读 sys_setting `framework.market_cdn_url`;测试注入) */
|
|
64
|
+
cdnBase?: () => Promise<string | null>
|
|
65
|
+
/** 注入面(DIP):缺省回落 isolate 全局 fetch / exec.run / server.reloadConfig / 本地 sha384(过渡实现) */
|
|
66
|
+
fetch?: FetchFace
|
|
67
|
+
exec?: ExecFace
|
|
68
|
+
reload?: ReloadFace
|
|
69
|
+
sha384B64?: (data: Uint8Array) => string
|
|
70
|
+
/** sha256 摘要面(verifying 段;缺省回落运行时全局,同 verify.ts) */
|
|
71
|
+
sha256Hex?: (data: Uint8Array) => string
|
|
72
|
+
/** Ed25519 验签面(P2-3;缺省回落运行时全局 crypto.verifyEd25519,同 verify.ts;测试注入) */
|
|
73
|
+
verifyEd25519?: (publicKey: string, msg: string, sigBase64: string) => boolean
|
|
74
|
+
/** 第一方 bootstrap key 覆盖(P2-12 测试注入;缺省回落 bootstrap.ts 内置常量,发布时回填) */
|
|
75
|
+
bootstrapKey?: { key_id: string, public_key: string } | null
|
|
76
|
+
/** 迁移账本 head(缺省 null = 跳过禁降版判定,同 installer) */
|
|
77
|
+
ledgerHead?: number | bigint | null
|
|
78
|
+
/** 迁移账本 head 真源(P1-16 接线①:缺省读 oj journal 表 `_oj_migrations` max(version),查询失败 → null 不猜) */
|
|
79
|
+
ledgerHeadOf?: (name: string) => Promise<number | bigint | null>
|
|
80
|
+
/**
|
|
81
|
+
* 宿主 versions.json(C2-09 peerRuntime 比对真源,键 = 包名)。缺省读 <dist>/versions.json
|
|
82
|
+
* (ojm 宿主构建产物,与 modules.json 同目录,B-11 接线);不可得 → undefined,manifest 声明
|
|
83
|
+
* peerRuntime 即预检 fail-closed 拒装(C2-09 语义保持)。
|
|
84
|
+
*/
|
|
85
|
+
hostVersions?: Record<string, string>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 宿主 versions.json 布局推导(B-11):与 modules.json 同目录(<dist>/versions.json) */
|
|
89
|
+
export function versionsJsonPathOf(modulesJsonPath: string): string {
|
|
90
|
+
const slash = modulesJsonPath.lastIndexOf("/");
|
|
91
|
+
return slash === -1 ? "versions.json" : `${modulesJsonPath.slice(0, slash)}/versions.json`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 宿主 versions.json 解析(纯函数):{packageName: version} 全字符串表;任何违约 → undefined(fail-closed 不猜) */
|
|
95
|
+
export function parseVersionsJson(text: string): Record<string, string> | undefined {
|
|
96
|
+
let raw: unknown;
|
|
97
|
+
try {
|
|
98
|
+
raw = JSON.parse(text);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (!isObj(raw)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const out: Record<string, string> = {};
|
|
107
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
108
|
+
if (typeof v !== "string") {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
out[k] = v;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** hostVersions 注入缺省:显式注入优先,否则读 <dist>/versions.json;不可得 → undefined(C2-09 fail-closed) */
|
|
117
|
+
async function hostVersionsOf(env: MarketEnv): Promise<Record<string, string> | undefined> {
|
|
118
|
+
if (env.hostVersions !== undefined) {
|
|
119
|
+
return env.hostVersions;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return parseVersionsJson(new TextDecoder().decode(await fsFace().readFile(versionsJsonPathOf(env.modulesJsonPath))));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖) */
|
|
130
|
+
export function marketEnv(): MarketEnv {
|
|
131
|
+
return {
|
|
132
|
+
sharedRoot: "dist",
|
|
133
|
+
staging: "modules/.staging",
|
|
134
|
+
taskDir: "modules/.install-tasks",
|
|
135
|
+
statePath: "modules/.solutions/state.yaml",
|
|
136
|
+
trustPath: ".solutions/trusted-keys.yaml",
|
|
137
|
+
dbPath: "db.sqlite",
|
|
138
|
+
siteModulesDir: "dist/modules",
|
|
139
|
+
modulesJsonPath: "dist/modules.json",
|
|
140
|
+
apiPrefix: "/api",
|
|
141
|
+
ledgerHeadOf: journalHeadOf,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
146
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 带 status 的可读失败(respond 收口成 json.fail,与 D14/B5 同一通道) */
|
|
150
|
+
function fail(status: number, msg: string): never {
|
|
151
|
+
throw Object.assign(new Error(msg), { status });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fetchOf(env: MarketEnv): FetchInitFace {
|
|
155
|
+
return env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function execOf(env: MarketEnv): ExecFace {
|
|
159
|
+
const g = globalThis as { exec?: { run: ExecFace["run"] } };
|
|
160
|
+
return env.exec ?? { run: (argv, opts) => g.exec!.run(argv, opts) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reloadOf(env: MarketEnv): ReloadFace {
|
|
164
|
+
const g = globalThis as { server?: { reloadConfig: ReloadFace["reloadConfig"] } };
|
|
165
|
+
return env.reload ?? { reloadConfig: () => g.server!.reloadConfig() };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sha384Of(env: MarketEnv): (data: Uint8Array) => string {
|
|
169
|
+
// oj isolate 无 sha384 原语(0.1.78 实测)→ 纯 JS 过渡实现兜底;isolate 原语落地后可换回
|
|
170
|
+
return env.sha384B64 ?? sha384B64;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sha256Of(env: MarketEnv): (data: Uint8Array) => string {
|
|
174
|
+
const g = globalThis as { crypto?: { sha256Hex?: (data: Uint8Array) => string } };
|
|
175
|
+
return env.sha256Hex ?? ((data: Uint8Array) => g.crypto!.sha256Hex!(data));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** CDN 基址:注入面优先,缺省读设置表(值/缺省值俱空 = 未配置;表异常视同未配置,消费端给可读错误) */
|
|
179
|
+
async function cdnBaseOf(env: MarketEnv): Promise<string | null> {
|
|
180
|
+
if (env.cdnBase) {
|
|
181
|
+
const injected = String(await env.cdnBase() ?? "").trim();
|
|
182
|
+
return injected === "" ? null : injected.replace(/\/+$/, "");
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const rows = await db.query("select value, default_value from sys_setting where key = ?", ["framework.market_cdn_url"]) as Array<Record<string, unknown>>;
|
|
186
|
+
const v = String(rows[0]?.value ?? rows[0]?.default_value ?? "").trim().replace(/\/+$/, "");
|
|
187
|
+
return v === "" ? null : v;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** CDN 文本拉取(bundles.yaml/manifests.yaml):恒带 Cache-Control: no-cache(索引时效性) */
|
|
195
|
+
async function fetchText(fetch: FetchInitFace, url: string): Promise<string> {
|
|
196
|
+
const res = await fetch(url, { headers: { "cache-control": "no-cache" } });
|
|
197
|
+
if (!res.ok)
|
|
198
|
+
fail(502, `索引拉取失败:${url} 返回 HTTP ${res.status}(请检查市场地址与网络)`);
|
|
199
|
+
return new TextDecoder().decode(await res.arrayBuffer());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 索引签名算法标识(与 ojm publish 同源:Ed25519;C2-35) */
|
|
203
|
+
const INDEX_SIG_ALG = "Ed25519";
|
|
204
|
+
|
|
205
|
+
/** 索引规范串(P3-7,与 ojm publish indexCanonical 逐字一致):`index:<kind>:<sha256hex>` */
|
|
206
|
+
function indexCanonical(env: MarketEnv, kind: "bundles" | "manifests", text: string): string {
|
|
207
|
+
const sha256 = sha256Of(env)(new TextEncoder().encode(text));
|
|
208
|
+
return `index:${kind}:${sha256}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Ed25519 验签面(P2-3;env 注入优先,缺省回落 isolate 全局 crypto.verifyEd25519) */
|
|
212
|
+
function verifyEd25519Of(env: MarketEnv): (publicKey: string, msg: string, sigBase64: string) => boolean {
|
|
213
|
+
const g = globalThis as { crypto?: { verifyEd25519?: (pk: string, msg: string, sig: string) => boolean } };
|
|
214
|
+
return env.verifyEd25519 ?? ((pk, msg, sig) => g.crypto!.verifyEd25519!(pk, msg, sig));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 索引签名校验(P3-7,G4 fail-closed 口径):解析 detached .sig 五字段 → 结构校验 →
|
|
219
|
+
* 重算规范串 → verifyEd25519。返回 true 仅当签名完整且匹配索引字节;任一结构/验签失败 → false。
|
|
220
|
+
* 注:发布方信任(key 是否在信任库/bootstrap)由安装期 TOFU/信任库承载,此处仅做完整性(防 CDN 索引篡改)。
|
|
221
|
+
*/
|
|
222
|
+
function verifyIndexSig(env: MarketEnv, kind: "bundles" | "manifests", text: string, sigText: string): boolean {
|
|
223
|
+
let sig: unknown;
|
|
224
|
+
try {
|
|
225
|
+
sig = JSON.parse(sigText);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (!isObj(sig) || sig.alg !== INDEX_SIG_ALG || typeof sig.value !== "string" || typeof sig.public_key !== "string")
|
|
231
|
+
return false;
|
|
232
|
+
const canonical = indexCanonical(env, kind, text);
|
|
233
|
+
return verifyEd25519Of(env)(sig.public_key, canonical, sig.value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 拉取索引并校验签名(P3-7):索引字节先取;若同目录存在非空 `<kind>.yaml.sig` 则校验,
|
|
238
|
+
* 篡改/不符/格式破损 → fail-closed 拒载(502,G4);`.sig` 缺失或空(滚动发布期)暂容忍
|
|
239
|
+
* (备案:缺失即拒的硬开关留后续 rollout)。
|
|
240
|
+
* 关键:验签失败须抛出 try 之外——fail-closed 不能被「缺失容忍」的 catch 吞掉。
|
|
241
|
+
*/
|
|
242
|
+
async function fetchIndex(env: MarketEnv, fetch: FetchInitFace, base: string, kind: "bundles" | "manifests"): Promise<string> {
|
|
243
|
+
const text = await fetchText(fetch, `${base}/${kind}.yaml`);
|
|
244
|
+
let sigText: string | null = null;
|
|
245
|
+
try {
|
|
246
|
+
// 先取 .sig 字节:404 / 空体 → 视为缺失(滚动发布期容忍),不进验签分支
|
|
247
|
+
const body = await fetchText(fetch, `${base}/${kind}.yaml.sig`);
|
|
248
|
+
if (body.trim() !== "")
|
|
249
|
+
sigText = body;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// .sig 缺失/不可读:滚动发布期容忍(不阻断索引加载),硬开关留后续 rollout
|
|
253
|
+
}
|
|
254
|
+
if (sigText !== null && !verifyIndexSig(env, kind, text, sigText))
|
|
255
|
+
fail(502, `市场索引 ${kind}.yaml 签名校验失败(索引被篡改或签名不符,G4 fail-closed)——请重新发布并签名`);
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** http.user.id → username(operator 历史留痕,uninstall 端点同款反查;查询失败不阻断编排) */
|
|
260
|
+
async function operatorOf(): Promise<string | null> {
|
|
261
|
+
const uid = (http as { user?: { id?: unknown } }).user?.id;
|
|
262
|
+
if (uid == null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const u = await db.query("select username from users where id = ?", [uid]) as Array<{ username: string }>;
|
|
267
|
+
return u[0]?.username ?? null;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** sys_solution_route enabled(reconcile route 一致性面;无行/查询失败 → null 不猜) */
|
|
275
|
+
async function routeEnabledOf(name: string): Promise<boolean | null> {
|
|
276
|
+
try {
|
|
277
|
+
const rows = await db.query("select enabled from sys_solution_route where module = ? and deprecated = 0 limit 1", [name]) as Array<{ enabled: unknown }>;
|
|
278
|
+
if (rows.length === 0) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
return Number(rows[0]!.enabled) === 1;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 对账入口(P1-10 接线③,市场页刷新即对账时点):锁 ↔ 账本 head 自愈 + enabled 不一致自动修复。
|
|
290
|
+
* 账本 head 缺省 = oj journal 表查询(journalHeadOf),floor = 安装任务记录 appliedSeq(latestAppliedSeqOf)。
|
|
291
|
+
* 自愈失败静默降级不阻断市场页(禁 console——release isolate console 抛 BadResource 会把
|
|
292
|
+
* 市场页打 500,Finding A;index 可用性优先,下次刷新重试)。
|
|
293
|
+
*/
|
|
294
|
+
async function reconcileOf(env: MarketEnv): Promise<void> {
|
|
295
|
+
try {
|
|
296
|
+
await reconcile({
|
|
297
|
+
lockPath: `${env.sharedRoot}/manifests.yaml`,
|
|
298
|
+
sharedRoot: env.sharedRoot,
|
|
299
|
+
taskDir: env.taskDir,
|
|
300
|
+
modulesJsonPath: env.modulesJsonPath,
|
|
301
|
+
state: await createStateStore(env.statePath),
|
|
302
|
+
ledgerHeadOf: env.ledgerHeadOf ?? journalHeadOf,
|
|
303
|
+
headFloorOf: (name, version) => latestAppliedSeqOf(env.taskDir, name, version),
|
|
304
|
+
routeEnabledOf,
|
|
305
|
+
trustPath: env.trustPath,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// 降级静默:对账自愈可重入(下次刷新重试),失败不外溢
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface ResolvedBundle {
|
|
314
|
+
version: string
|
|
315
|
+
archiveUrl: string
|
|
316
|
+
manifest: unknown
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Form B 索引解析(安装器不拼 URL——市场侧职责,installer.ts 同纪律):
|
|
321
|
+
* bundles.yaml 定版本(缺省 latest,同名只展示最高版本 PRD §5.3)→ manifests.yaml 按
|
|
322
|
+
* name+version 精确取 manifest → 分发包 URL = base + 索引 file 字段。
|
|
323
|
+
*/
|
|
324
|
+
async function resolveCdn(env: MarketEnv, base: string, name: string, reqVersion: string | null, fetch: FetchInitFace): Promise<ResolvedBundle> {
|
|
325
|
+
const g = globalThis as { yaml?: { parse: (text: string) => Promise<unknown> } };
|
|
326
|
+
const bundles = await g.yaml!.parse(await fetchIndex(env, fetch, base, "bundles")) as { solutions?: unknown };
|
|
327
|
+
const entry = (Array.isArray(bundles.solutions) ? bundles.solutions : []).find(s => isObj(s) && s.name === name);
|
|
328
|
+
if (!isObj(entry))
|
|
329
|
+
fail(404, `市场索引中无方案「${name}」(${base}/bundles.yaml)`);
|
|
330
|
+
const versions = Array.isArray(entry.versions) ? entry.versions.filter(isObj) : [];
|
|
331
|
+
const version = reqVersion ?? (typeof entry.latest === "string" ? entry.latest : null);
|
|
332
|
+
const hit = versions.find(v => String(v.version) === version);
|
|
333
|
+
if (version === null || !isObj(hit))
|
|
334
|
+
fail(404, `市场索引中无方案「${name}」版本「${reqVersion ?? "latest"}」(${base}/bundles.yaml)`);
|
|
335
|
+
const list = await g.yaml!.parse(await fetchIndex(env, fetch, base, "manifests")) as unknown;
|
|
336
|
+
const manifest = (Array.isArray(list) ? list : []).find(m => isObj(m) && m.name === name && String(m.version) === version);
|
|
337
|
+
if (!isObj(manifest))
|
|
338
|
+
fail(404, `聚合清单缺 ${name}@${version} 的 manifest(manifests.yaml 与 bundles.yaml 不一致,Form B 真源破损)`);
|
|
339
|
+
// P2-11 索引投毒加固:bundles.yaml 的 file 逐字 <name>-<version>.tgz(索引未签名属不可信输入,
|
|
340
|
+
// 过弱形态可拼宿主任意路径;逐字等值排除 ../、绝对路径、异名、异版本)
|
|
341
|
+
const file = typeof hit.file === "string" ? hit.file : "";
|
|
342
|
+
if (file !== `${name}-${version}.tgz`) {
|
|
343
|
+
fail(502, `索引分发包 file「${file}」须逐字为 <name>-<version>.tgz(「${name}-${version}.tgz」)——索引投毒加固(P2-11),疑似被篡改的 bundles.yaml`);
|
|
344
|
+
}
|
|
345
|
+
return { version, archiveUrl: `${base}/${file}`, manifest };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 上传载荷字节(upload/api.ts 先例双路径:小文件字节在手,大文件已流式落 blob 须走 blob.get) */
|
|
349
|
+
async function uploadBytes(): Promise<Uint8Array | null> {
|
|
350
|
+
const h = http as { files?: Array<{ key?: string }>, file?: (i: number) => Promise<Uint8Array> };
|
|
351
|
+
if (!Array.isArray(h.files) || h.files.length === 0)
|
|
352
|
+
return null;
|
|
353
|
+
const meta = h.files[0]!;
|
|
354
|
+
return meta.key ? await blob.get(meta.key) : await h.file!(0);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* detached 侧车 `.sig` 上传字节(P2-3,契约 §2.11;file 源签名载体——Form A 禁带 signature 的
|
|
359
|
+
* 自引用悖论 B-2 由侧车消解,与 Form B 同源同语义)。files[1] 缺省 = 未随包上传签名 →
|
|
360
|
+
* verify 段 sig_missing(G4 硬 fail-closed,无逃生门)。JSON 形态校验留 verify 段六码分流,
|
|
361
|
+
* 此处只保「是 JSON 对象」——非 JSON 即上传错误(人话 400,不进安装器)。
|
|
362
|
+
*/
|
|
363
|
+
async function uploadSig(): Promise<Record<string, unknown> | null> {
|
|
364
|
+
const h = http as { files?: Array<{ key?: string }>, file?: (i: number) => Promise<Uint8Array> };
|
|
365
|
+
if (!Array.isArray(h.files) || h.files.length < 2)
|
|
366
|
+
return null;
|
|
367
|
+
const meta = h.files[1]!;
|
|
368
|
+
const bytes = meta.key ? await blob.get(meta.key) : await h.file!(1);
|
|
369
|
+
let parsed: unknown;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
return fail(400, `签名侧车文件不是合法 JSON(${String(e)})——.sig 应为 {alg,key_id,public_key,value,canon} 对象`);
|
|
375
|
+
}
|
|
376
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
377
|
+
fail(400, "签名侧车文件必须是 JSON 对象({alg,key_id,public_key,value,canon})");
|
|
378
|
+
return parsed as Record<string, unknown>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* file 源 manifest 提取:上传包先过 R1 三步范式(tar -tvzf 列表 → validateTarListing →
|
|
383
|
+
* tar -xzf -C staging)再读 manifest.yaml——未校验不解包,zip-slip 拒在信任边界外。
|
|
384
|
+
* 预解包产物与安装器 extracting 段同目录幂等(staging 就位即止,rename 归 lock_commit)。
|
|
385
|
+
*/
|
|
386
|
+
async function manifestOfUpload(env: MarketEnv, bytes: Uint8Array, exec: ExecFace): Promise<unknown> {
|
|
387
|
+
const f = fsFace();
|
|
388
|
+
const staged = `${env.staging}/upload.tgz`; // 常量名不含用户输入,无穿越面;download 段按 manifest.archive.file 重写正名
|
|
389
|
+
await f.mkdir(env.staging, { recursive: true });
|
|
390
|
+
await f.writeFile(staged, bytes);
|
|
391
|
+
const listed = await exec.run(["tar", "-tvzf", staged]);
|
|
392
|
+
if (listed.code !== 0)
|
|
393
|
+
fail(400, `上传包列表失败(tar -tvzf):${(listed.stderr || listed.stdout).trim()}——包体损坏?`);
|
|
394
|
+
// 铁律②:tar 输出恒带尾随换行,先 trim 尾部空行再 split(extract.ts 同款)
|
|
395
|
+
const lines = listed.stdout.replace(/\n+$/, "").split("\n");
|
|
396
|
+
const v = validateTarListing(lines);
|
|
397
|
+
if (!v.ok)
|
|
398
|
+
fail(400, `上传包含非法成员「${v.offender}」——R1 zip-slip 防护拒收(list→validate→extract 三步范式)`);
|
|
399
|
+
const extracted = await exec.run(["tar", "-xzf", staged, "-C", env.staging]);
|
|
400
|
+
if (extracted.code !== 0)
|
|
401
|
+
fail(400, `上传包解包失败(tar -xzf):${(extracted.stderr || extracted.stdout).trim()}`);
|
|
402
|
+
await flattenBundleRoot(env.staging);
|
|
403
|
+
const g = globalThis as { yaml?: { parse: (text: string) => Promise<unknown> } };
|
|
404
|
+
try {
|
|
405
|
+
return await g.yaml!.parse(new TextDecoder().decode(await f.readFile(`${env.staging}/manifest.yaml`)));
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
return fail(400, `上传包 manifest.yaml 不可解析:${String(e)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 取消安装文件握手(P3-12 端点接线,U5/P3-8 备案①落地):oj isolate 按请求隔离,内存信号
|
|
414
|
+
* 不共享——改走任务目录下标记文件 `${taskDir}/.cancel-<name>`(跨 isolate 唯一共享面 = FS)。
|
|
415
|
+
* install/cancel 端点写标记(superadmin,R17 同名互斥保证按名寻址无歧义);runInstall 的
|
|
416
|
+
* cancel 闭包每次探测时若旗标未置位即发起一次异步读标记(fire-and-forget,读到位翻转旗标,
|
|
417
|
+
* 下一次探测生效)——无定时器、无后台循环、无每块同步 IO。installBody 终态后清标记。
|
|
418
|
+
*/
|
|
419
|
+
function cancelMarkerPath(taskDir: string, name: string): string {
|
|
420
|
+
return `${taskDir}/.cancel-${name}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** POST install 主体:门禁后组装 InstallRequest 交 runInstall(端点不重复编排),返回任务 id 供轮询 */
|
|
424
|
+
async function installBody(env: MarketEnv): Promise<{ task_id: string }> {
|
|
425
|
+
const body = isObj(http.body) ? http.body : {};
|
|
426
|
+
const source = body.source;
|
|
427
|
+
if (source !== "cdn" && source !== "file")
|
|
428
|
+
fail(400, `source 必须是 "cdn" | "file",实为「${String(source)}」`);
|
|
429
|
+
const name = String(body.name ?? "").trim();
|
|
430
|
+
if (name === "")
|
|
431
|
+
fail(400, "name is required");
|
|
432
|
+
const reqVersion = body.version == null || body.version === "" ? null : String(body.version);
|
|
433
|
+
|
|
434
|
+
let manifest: unknown;
|
|
435
|
+
let version: string;
|
|
436
|
+
let archiveUrl: string;
|
|
437
|
+
let fetch: FetchInitFace;
|
|
438
|
+
if (source === "file") {
|
|
439
|
+
const bytes = await uploadBytes();
|
|
440
|
+
if (bytes === null)
|
|
441
|
+
fail(400, "source=file 须随 multipart 上传分发包(http.files)");
|
|
442
|
+
const exec = execOf(env);
|
|
443
|
+
manifest = await manifestOfUpload(env, bytes, exec);
|
|
444
|
+
const m = isObj(manifest) ? manifest : {};
|
|
445
|
+
// 身份:body 权威、manifest 兜底;两者俱在且冲突即拒(防锁名/注册名分叉的坏装)
|
|
446
|
+
const mVersion = typeof m.version === "string" ? m.version : "";
|
|
447
|
+
if (reqVersion !== null && mVersion !== "" && mVersion !== reqVersion)
|
|
448
|
+
fail(400, `body.version「${reqVersion}」与包内 manifest.version「${mVersion}」不一致——请核对分发包`);
|
|
449
|
+
const mName = typeof m.name === "string" ? m.name : "";
|
|
450
|
+
if (mName !== "" && mName !== name)
|
|
451
|
+
fail(400, `body.name「${name}」与包内 manifest.name「${mName}」不一致——请核对分发包`);
|
|
452
|
+
if (reqVersion === null && mVersion === "")
|
|
453
|
+
fail(400, "version 不可得(body.version 与包内 manifest.version 均缺失)");
|
|
454
|
+
// Form A 补摘要(C2-29 布局消费点,P1-13 真机实测——附录 B-10):ojm build --bundle 产物的
|
|
455
|
+
// 包内 manifest 是 Form A(archive 仅 file,sha256/size 由 P3 publish 填充于 Form B),而安装器
|
|
456
|
+
// verify 段对 archive.sha256 fail-closed。file 源字节在手——上传时点即算摘要补成可校验清单
|
|
457
|
+
// (verify 复核同批字节,R10 语义保持);CDN 源不受影响(Form B 真源自带摘要)。
|
|
458
|
+
// 正名(P2-11 索引投毒加固):archive.file 逐字 <name>-<version>.tgz——name(C2-01)/version
|
|
459
|
+
// (无路径分隔符)拼出的单文件名安全时即正名(download 段按其落盘);否则退回常量名
|
|
460
|
+
// upload.tgz(此刻尚未过预检,恶意命名不提前 400 也不给穿越面——预检稍后照拒)。
|
|
461
|
+
const effectiveVersion = reqVersion ?? mVersion;
|
|
462
|
+
const namedFile = /^[a-z][a-z0-9-]{0,63}$/.test(name) && /^[0-9A-Z.-]+$/i.test(effectiveVersion) && !effectiveVersion.includes("..")
|
|
463
|
+
? `${name}-${effectiveVersion}.tgz`
|
|
464
|
+
: "upload.tgz";
|
|
465
|
+
m.archive = { file: namedFile, sha256: sha256Of(env)(bytes), size: bytes.byteLength };
|
|
466
|
+
// detached 侧车签名注入(P2-3,G4 补裁):同目录 .sig 随 multipart 第二文件上传——
|
|
467
|
+
// 与 Form B archive.signature 同结构注入,verify 段统一验签(六码分流)
|
|
468
|
+
const sig = await uploadSig();
|
|
469
|
+
if (sig !== null) {
|
|
470
|
+
m.archive = { ...(m.archive as Record<string, unknown>), signature: sig };
|
|
471
|
+
}
|
|
472
|
+
version = reqVersion ?? mVersion;
|
|
473
|
+
archiveUrl = `${env.staging}/upload.tgz`;
|
|
474
|
+
fetch = async () => ({ ok: true, status: 200, arrayBuffer: async () => bytes.slice().buffer as ArrayBuffer });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const base = await cdnBaseOf(env);
|
|
478
|
+
if (base === null)
|
|
479
|
+
fail(503, "未配置市场地址(framework.market_cdn_url)——市场安装不可用,请先在系统设置配置 CDN 基址");
|
|
480
|
+
const r = await resolveCdn(env, base, name, reqVersion, fetchOf(env));
|
|
481
|
+
manifest = r.manifest;
|
|
482
|
+
version = r.version;
|
|
483
|
+
archiveUrl = r.archiveUrl;
|
|
484
|
+
fetch = fetchOf(env);
|
|
485
|
+
}
|
|
486
|
+
// 第一方 bootstrap key 播种(P2-12,G3 双轨第一轨):runInstall 之前——market 首装免 TOFU。
|
|
487
|
+
// key 未回填(null)或信任库已初始化 → no-op(不与运维吊销对抗)
|
|
488
|
+
await seedBootstrapKey(env.trustPath, env.bootstrapKey !== undefined ? env.bootstrapKey : BOOTSTRAP_KEY);
|
|
489
|
+
// U5 取消安装文件握手(P3-12):清残留标记 → cancel 闭包「未置位则发一次异步读标记,
|
|
490
|
+
// 读到位翻转旗标」(isolate 无定时器/无共享内存,FS 是唯一跨请求通道)
|
|
491
|
+
const markerPath = cancelMarkerPath(env.taskDir, name);
|
|
492
|
+
try {
|
|
493
|
+
await fsFace().remove(markerPath, { force: true });
|
|
494
|
+
}
|
|
495
|
+
catch {}
|
|
496
|
+
let cancelled = false;
|
|
497
|
+
try {
|
|
498
|
+
const task = await runInstall({
|
|
499
|
+
manifest,
|
|
500
|
+
name,
|
|
501
|
+
version,
|
|
502
|
+
operator: await operatorOf(),
|
|
503
|
+
state: await createStateStore(env.statePath),
|
|
504
|
+
archiveUrl,
|
|
505
|
+
sharedRoot: env.sharedRoot,
|
|
506
|
+
staging: env.staging,
|
|
507
|
+
taskDir: env.taskDir,
|
|
508
|
+
dbPath: env.dbPath,
|
|
509
|
+
siteModulesDir: env.siteModulesDir,
|
|
510
|
+
modulesJsonPath: env.modulesJsonPath,
|
|
511
|
+
apiPrefix: env.apiPrefix,
|
|
512
|
+
configPath: env.configPath,
|
|
513
|
+
fetch: withSiteOrigin(fetch),
|
|
514
|
+
exec: execOf(env),
|
|
515
|
+
reload: reloadOf(env),
|
|
516
|
+
sha384B64: sha384Of(env),
|
|
517
|
+
sha256Hex: env.sha256Hex,
|
|
518
|
+
verifyEd25519: env.verifyEd25519,
|
|
519
|
+
ledgerHead: env.ledgerHeadOf ? await env.ledgerHeadOf(name) : (env.ledgerHead ?? null),
|
|
520
|
+
hostVersions: await hostVersionsOf(env),
|
|
521
|
+
// 宿主平台编排面 API 版本(C2-11 peerPlatform 比对真源;与 meta 端点 apiVersion、_platform/manifest.yaml 版本同源,手工同步)
|
|
522
|
+
hostApiVersion: API_VERSION,
|
|
523
|
+
trustPath: env.trustPath,
|
|
524
|
+
cancel: () => {
|
|
525
|
+
if (!cancelled) {
|
|
526
|
+
void fsFace().readFile(markerPath).then(() => {
|
|
527
|
+
cancelled = true;
|
|
528
|
+
}).catch(() => {});
|
|
529
|
+
}
|
|
530
|
+
return cancelled;
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
return { task_id: task.id };
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
cancelled = true;
|
|
537
|
+
try {
|
|
538
|
+
await fsFace().remove(markerPath, { force: true });
|
|
539
|
+
}
|
|
540
|
+
catch {}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface IndexRow {
|
|
545
|
+
name: string
|
|
546
|
+
latest: string | null
|
|
547
|
+
installed: string | null
|
|
548
|
+
enabled: boolean | null
|
|
549
|
+
upgradable: boolean
|
|
550
|
+
/** CDN 版本全集(P2-11):回滚候选不止本站已装旧版;索引缺段/畸形 → [] 降级不炸 */
|
|
551
|
+
available: string[]
|
|
552
|
+
/** 最新版 changelog(C2-08,R27 卡片版本对比;manifests.yaml 不可得 → null) */
|
|
553
|
+
changelog: unknown
|
|
554
|
+
/** 最新版发布方(manifest author,C2-07;manifests.yaml 不可得 → null) */
|
|
555
|
+
publisher: string | null
|
|
556
|
+
/** 本地安装的发布方 key 状态(P2-6 sidecar;未装 → null) */
|
|
557
|
+
trust_state: "trusted" | "untrusted" | "absent" | null
|
|
558
|
+
/** 本地安装记录的签名 key id(sidecar archive.key_id;未装/P1 时代条目 → null) */
|
|
559
|
+
key_id: string | null
|
|
560
|
+
/** 最近一次安装操作人(P2-11 裁决:operator 是历史动作语义,回落 market_history 行,不在卡片现算) */
|
|
561
|
+
operator: string | null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** 最近一次安装操作人(回落 market_history 行;表不存在/查询失败/无行 → null,观测面不阻断索引) */
|
|
565
|
+
async function lastInstallOperatorOf(name: string): Promise<string | null> {
|
|
566
|
+
try {
|
|
567
|
+
const rows = await db.query(
|
|
568
|
+
"select operator from market_history where solution = ? and action = 'install' order by created_at desc, rowid desc limit 1",
|
|
569
|
+
[name],
|
|
570
|
+
) as Array<{ operator: unknown }>;
|
|
571
|
+
const op = rows[0]?.operator;
|
|
572
|
+
return typeof op === "string" ? op : null;
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** GET index 主体:CDN bundles.yaml(no-cache)⊕ 本地 sidecar state——CDN 序优先,本地独有方案殿后 */
|
|
580
|
+
async function indexBody(env: MarketEnv): Promise<IndexRow[]> {
|
|
581
|
+
// 对账即服务(P1-10 接线③):市场页刷新即对账时点——锁 ↔ 账本 head 自愈 + enabled 不一致
|
|
582
|
+
// 自动修复先行(失败只记日志,不阻断索引),失败站点的市场页每次打开都在尝试自愈
|
|
583
|
+
await reconcileOf(env);
|
|
584
|
+
const base = await cdnBaseOf(env);
|
|
585
|
+
if (base === null)
|
|
586
|
+
fail(503, "未配置市场地址(framework.market_cdn_url)——卡片区显示「未配置市场地址」(PRD §5.3)");
|
|
587
|
+
const g = globalThis as { yaml?: { parse: (text: string) => Promise<unknown> } };
|
|
588
|
+
const text = await fetchIndex(env, fetchOf(env), base, "bundles");
|
|
589
|
+
let bundles: { solutions?: unknown };
|
|
590
|
+
try {
|
|
591
|
+
bundles = await g.yaml!.parse(text) as { solutions?: unknown };
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
fail(502, `市场索引解析失败:${base}/bundles.yaml 不是合法索引`);
|
|
595
|
+
}
|
|
596
|
+
const cdn = (Array.isArray(bundles.solutions) ? bundles.solutions : []).filter(isObj);
|
|
597
|
+
const local = (await createStateStore(env.statePath)).read().solutions;
|
|
598
|
+
// manifests.yaml 单次拉取供 changelog/publisher(P2-11):失败降级 null 不阻断索引(可用性优先)
|
|
599
|
+
let manifests: Record<string, unknown>[] = [];
|
|
600
|
+
try {
|
|
601
|
+
const list = await g.yaml!.parse(await fetchIndex(env, fetchOf(env), base, "manifests"));
|
|
602
|
+
if (Array.isArray(list))
|
|
603
|
+
manifests = list.filter(isObj);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// 降级:changelog/publisher 全 null,索引主体照常返回
|
|
607
|
+
}
|
|
608
|
+
const manifestOf = (name: string, version: string | null): Record<string, unknown> | undefined =>
|
|
609
|
+
version === null ? undefined : manifests.find(m => m.name === name && String(m.version) === version);
|
|
610
|
+
const rows: IndexRow[] = [];
|
|
611
|
+
const seen = new Set<string>();
|
|
612
|
+
for (const s of cdn) {
|
|
613
|
+
const name = typeof s.name === "string" ? s.name : "";
|
|
614
|
+
if (name === "" || seen.has(name))
|
|
615
|
+
continue;
|
|
616
|
+
seen.add(name);
|
|
617
|
+
const latest = typeof s.latest === "string" ? s.latest : null;
|
|
618
|
+
const side = local[name];
|
|
619
|
+
// available:versions[].version 字符串全集(缺段/畸形 → [] 降级)
|
|
620
|
+
const available = (Array.isArray(s.versions) ? s.versions : [])
|
|
621
|
+
.map(v => isObj(v) && typeof v.version === "string" ? v.version : "")
|
|
622
|
+
.filter(x => x !== "");
|
|
623
|
+
const m = manifestOf(name, latest) ?? manifestOf(name, side?.version ?? null);
|
|
624
|
+
rows.push({
|
|
625
|
+
name,
|
|
626
|
+
latest,
|
|
627
|
+
installed: side?.version ?? null,
|
|
628
|
+
enabled: side?.enabled ?? null,
|
|
629
|
+
upgradable: latest !== null && side !== undefined && compareVersions(side.version, latest) < 0,
|
|
630
|
+
available,
|
|
631
|
+
changelog: m?.changelog ?? null,
|
|
632
|
+
publisher: typeof m?.author === "string" ? m.author : null,
|
|
633
|
+
trust_state: side?.trust_state ?? null,
|
|
634
|
+
key_id: side?.archive?.key_id ?? null,
|
|
635
|
+
operator: await lastInstallOperatorOf(name),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
for (const [name, side] of Object.entries(local)) {
|
|
639
|
+
if (!seen.has(name)) {
|
|
640
|
+
rows.push({
|
|
641
|
+
name,
|
|
642
|
+
latest: null,
|
|
643
|
+
installed: side.version,
|
|
644
|
+
enabled: side.enabled,
|
|
645
|
+
upgradable: false,
|
|
646
|
+
available: [],
|
|
647
|
+
changelog: null,
|
|
648
|
+
publisher: null,
|
|
649
|
+
trust_state: side.trust_state ?? null,
|
|
650
|
+
key_id: side.archive?.key_id ?? null,
|
|
651
|
+
operator: await lastInstallOperatorOf(name),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return rows;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** 四端点组(oj v0.1.64 嵌套组:组名 = 路径段);全业务体经 respond 收口(status 语义不落 500) */
|
|
659
|
+
export function createMarketApi(env: MarketEnv) {
|
|
660
|
+
return {
|
|
661
|
+
// POST /api/_platform/market/install —— 安装编排(superadmin,R9)
|
|
662
|
+
"install": {
|
|
663
|
+
async post() {
|
|
664
|
+
await respond(async () => {
|
|
665
|
+
await assertSuperAdmin();
|
|
666
|
+
return await installBody(env);
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
// POST /api/_platform/market/install/cancel —— 取消进行中的安装(superadmin,U5/P3-12)。
|
|
670
|
+
// 按方案名寻址(R17 任务互斥:同名同时至多一个 running install);写取消标记文件
|
|
671
|
+
// (isolate 按请求隔离,FS 是唯一跨请求通道),runInstall 的 cancel 闭包在段间/migrate
|
|
672
|
+
// spawn 逐块探测命中 → h.kill + 回退装前态(P3-8 语义);终态 failed/partial + cancelled
|
|
673
|
+
// 标记随任务文件可查。无 running 任务 → 404。
|
|
674
|
+
cancel: {
|
|
675
|
+
async post() {
|
|
676
|
+
await respond(async () => {
|
|
677
|
+
await assertSuperAdmin();
|
|
678
|
+
const body = isObj(http.body) ? http.body : {};
|
|
679
|
+
const name = String(body.name ?? "").trim();
|
|
680
|
+
if (name === "")
|
|
681
|
+
fail(400, "name is required");
|
|
682
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name))
|
|
683
|
+
fail(400, `name 非法(须 ^[a-z][a-z0-9-]{0,63}$):${name}`);
|
|
684
|
+
const running = (await readTasks(env.taskDir)).some(t => t.name === name && t.status === "running");
|
|
685
|
+
if (!running)
|
|
686
|
+
fail(404, `「${name}」当前没有进行中的安装任务(取消只对 running 态有效)`);
|
|
687
|
+
await atomicWriteText(cancelMarkerPath(env.taskDir, name), new Date().toISOString());
|
|
688
|
+
return { ok: true, name, requested: true };
|
|
689
|
+
});
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
// GET /api/_platform/market/task?id= —— 任务轮询消费面(superadmin,§10.2 800ms 轮询)。
|
|
694
|
+
// 简报 `task/:id` 段参数形态在模板 http 面无先例,照 contacts/detail?id= 惯例走 query(报告备案)。
|
|
695
|
+
"task": {
|
|
696
|
+
async get() {
|
|
697
|
+
await respond(async () => {
|
|
698
|
+
await assertSuperAdmin();
|
|
699
|
+
const id = String((http as { query?: Record<string, unknown> }).query?.id ?? "").trim();
|
|
700
|
+
if (id === "")
|
|
701
|
+
fail(400, "id is required");
|
|
702
|
+
const rec = await getTask(env.taskDir, id);
|
|
703
|
+
if (rec === null)
|
|
704
|
+
fail(404, `任务不存在或已过期(任务文件保留最近 10 条):${id}`);
|
|
705
|
+
return rec;
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
// POST /api/_platform/market/enable —— 生命周期开关(superadmin;P1-13 瘦客户端触发面)。
|
|
710
|
+
// 端点只做门禁 + 取参 + SetEnabledInput 组装,四处同写全归 setEnabled(D12,installer.ts 同纪律);
|
|
711
|
+
// 领域拒绝(未装/空锁/版本目录缺失)返回 {ok:false, message} 200——操作已执行、被域逻辑拒绝,
|
|
712
|
+
// 人话在 message,CLI/市场页照 ok 位渲染。
|
|
713
|
+
"enable": {
|
|
714
|
+
async post() {
|
|
715
|
+
await respond(async () => {
|
|
716
|
+
await assertSuperAdmin();
|
|
717
|
+
const body = isObj(http.body) ? http.body : {};
|
|
718
|
+
const name = String(body.name ?? "").trim();
|
|
719
|
+
if (name === "")
|
|
720
|
+
fail(400, "name is required");
|
|
721
|
+
if (typeof body.enabled !== "boolean")
|
|
722
|
+
fail(400, `enabled 必须是布尔,实为「${String(body.enabled)}」`);
|
|
723
|
+
return await setEnabled({
|
|
724
|
+
name,
|
|
725
|
+
enabled: body.enabled,
|
|
726
|
+
sharedRoot: env.sharedRoot,
|
|
727
|
+
lockPath: `${env.sharedRoot}/manifests.yaml`,
|
|
728
|
+
state: await createStateStore(env.statePath),
|
|
729
|
+
modulesJsonPath: env.modulesJsonPath,
|
|
730
|
+
trustPath: env.trustPath,
|
|
731
|
+
operator: await operatorOf(),
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
// GET /api/_platform/market/meta —— 登录即可({apiVersion},peerPlatform 预检;非 superadmin)
|
|
737
|
+
"meta": {
|
|
738
|
+
async get() {
|
|
739
|
+
await respond(async () => {
|
|
740
|
+
const uid = (http as { user?: { id?: unknown } }).user?.id;
|
|
741
|
+
if (uid == null)
|
|
742
|
+
fail(401, "unauthorized");
|
|
743
|
+
return { apiVersion: API_VERSION };
|
|
744
|
+
});
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
// 信任库运维面(P2-2,C2-38/39,G3 双轨第一轨 / G5;全 superadmin,R9):
|
|
748
|
+
// GET /market/trust 列表(含 .bak 回落 note——损坏时运维可见)
|
|
749
|
+
// POST /market/trust TOFU 确认 {task_id}(P2-3):自任务记录重取公钥复算指纹
|
|
750
|
+
// 逐字比对后入信任库——防「人确认 A、库里落 B」混淆代理
|
|
751
|
+
// POST /market/trust/key 新增信任(首次绑定不可改:撞绑定 409,C2-38)
|
|
752
|
+
// DELETE /market/trust/key?key_id= 吊销(G5:只阻断新装/升级,不动已装方案)
|
|
753
|
+
// 端点只做门禁 + 取参 + operator 反查;读写/原子写/首次绑定/吊销语义全归 trust.ts(ISP,同 enable/uninstall 纪律)。
|
|
754
|
+
// del 动词(oj 运行期命名,devkit §http:DELETE 的方法名是 del 不是 delete)+ query 取参(system/roles del 同款先例)。
|
|
755
|
+
"trust": {
|
|
756
|
+
async get() {
|
|
757
|
+
await respond(async () => {
|
|
758
|
+
await assertSuperAdmin();
|
|
759
|
+
const store = createTrustStore(env.trustPath);
|
|
760
|
+
return { keys: await store.list(), note: store.corruptNote?.() ?? null };
|
|
761
|
+
});
|
|
762
|
+
},
|
|
763
|
+
async post() {
|
|
764
|
+
await respond(async () => {
|
|
765
|
+
await assertSuperAdmin();
|
|
766
|
+
const body = isObj(http.body) ? http.body : {};
|
|
767
|
+
const taskId = String(body.task_id ?? "").trim();
|
|
768
|
+
if (taskId === "")
|
|
769
|
+
fail(400, "task_id is required");
|
|
770
|
+
const rec = await getTask(env.taskDir, taskId);
|
|
771
|
+
if (rec === null)
|
|
772
|
+
fail(404, `任务不存在或已过期(任务文件保留最近 10 条):${taskId}`);
|
|
773
|
+
if (rec.status !== "needs_trust" || rec.trust === undefined)
|
|
774
|
+
fail(409, `任务 ${taskId} 不在待信任确认状态(status=${rec.status})——无需确认`);
|
|
775
|
+
// 混淆代理防线:公钥自任务记录重取,指纹复算逐字比对(记录疑被篡改即拒,信任库零变化)
|
|
776
|
+
const raw = new Uint8Array(32);
|
|
777
|
+
for (let i = 0; i < 32; i++)
|
|
778
|
+
raw[i] = Number.parseInt(rec.trust.public_key.slice(i * 2, i * 2 + 2), 16);
|
|
779
|
+
const fp = sha256Of(env)(raw);
|
|
780
|
+
if (fp !== rec.trust.fingerprint)
|
|
781
|
+
fail(409, "任务记录内指纹与公钥不一致——记录疑被篡改,拒绝确认(信任库零变化)");
|
|
782
|
+
const result = await createTrustStore(env.trustPath).add(rec.trust.key_id, rec.trust.public_key, { operator: await operatorOf() });
|
|
783
|
+
return { result, key_id: rec.trust.key_id, fingerprint: rec.trust.fingerprint };
|
|
784
|
+
});
|
|
785
|
+
},
|
|
786
|
+
key: {
|
|
787
|
+
async post() {
|
|
788
|
+
await respond(async () => {
|
|
789
|
+
await assertSuperAdmin();
|
|
790
|
+
const body = isObj(http.body) ? http.body : {};
|
|
791
|
+
const keyId = String(body.key_id ?? "").trim();
|
|
792
|
+
const publicKey = String(body.public_key ?? "").trim();
|
|
793
|
+
if (keyId === "" || publicKey === "")
|
|
794
|
+
fail(400, "key_id and public_key are required");
|
|
795
|
+
return { result: await createTrustStore(env.trustPath).add(keyId, publicKey, { operator: await operatorOf() }) };
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
async del() {
|
|
799
|
+
await respond(async () => {
|
|
800
|
+
await assertSuperAdmin();
|
|
801
|
+
const keyId = String((http as { query?: Record<string, unknown> }).query?.key_id ?? "").trim();
|
|
802
|
+
if (keyId === "")
|
|
803
|
+
fail(400, "key_id is required");
|
|
804
|
+
return { revoked: await createTrustStore(env.trustPath).remove(keyId, { operator: await operatorOf() }) };
|
|
805
|
+
});
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
// GET /api/_platform/market/index —— 市场索引聚合(superadmin,R9)
|
|
810
|
+
"index": {
|
|
811
|
+
async get() {
|
|
812
|
+
await respond(async () => {
|
|
813
|
+
await assertSuperAdmin();
|
|
814
|
+
return await indexBody(env);
|
|
815
|
+
});
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
// GET /api/_platform/market/uninstall-impact?name= —— 卸载影响面预检(superadmin,R9)。
|
|
819
|
+
// 返回前端二次确认所需的影响计数(角色数 + 受影响用户数),不改动任何状态。
|
|
820
|
+
"uninstall-impact": {
|
|
821
|
+
async get() {
|
|
822
|
+
await respond(async () => {
|
|
823
|
+
await assertSuperAdmin();
|
|
824
|
+
const name = String((http as { query?: Record<string, unknown> }).query?.name ?? "").trim();
|
|
825
|
+
if (name === "")
|
|
826
|
+
fail(400, "name is required");
|
|
827
|
+
return await computeUninstallImpact({
|
|
828
|
+
name,
|
|
829
|
+
sharedRoot: env.sharedRoot,
|
|
830
|
+
lockPath: `${env.sharedRoot}/manifests.yaml`,
|
|
831
|
+
state: await createStateStore(env.statePath),
|
|
832
|
+
modulesJsonPath: env.modulesJsonPath,
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
// POST /api/_platform/market/uninstall —— 卸载编排(superadmin,R9)。
|
|
838
|
+
// 端点只做门禁 + 取参 + operator 反查(历史留痕),四处同删全归 runUninstall(uninstall.ts 同纪律);
|
|
839
|
+
// 领域拒绝(未装/空锁拒启)返回 {ok:false, message} 200——人话在 message,CLI/市场页照 ok 位渲染。
|
|
840
|
+
"uninstall": {
|
|
841
|
+
async post() {
|
|
842
|
+
await respond(async () => {
|
|
843
|
+
await assertSuperAdmin();
|
|
844
|
+
const body = isObj(http.body) ? http.body : {};
|
|
845
|
+
const name = String(body.name ?? "").trim();
|
|
846
|
+
if (name === "")
|
|
847
|
+
fail(400, "name is required");
|
|
848
|
+
const r = await runUninstall({
|
|
849
|
+
name,
|
|
850
|
+
sharedRoot: env.sharedRoot,
|
|
851
|
+
lockPath: `${env.sharedRoot}/manifests.yaml`,
|
|
852
|
+
state: await createStateStore(env.statePath),
|
|
853
|
+
modulesJsonPath: env.modulesJsonPath,
|
|
854
|
+
operator: await operatorOf(),
|
|
855
|
+
});
|
|
856
|
+
// P3-11 宿主打磨:方案已不存在 = 资源缺失语义 → 404(非 200 {ok:false});
|
|
857
|
+
// 其余领域拒绝(空锁拒启/级联失败等)维持 200 {ok:false} 人话在 message。
|
|
858
|
+
if (!r.ok && r.reason === "not-installed")
|
|
859
|
+
fail(404, r.message);
|
|
860
|
+
return r;
|
|
861
|
+
});
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
// GET /api/_platform/market/history —— 市场操作历史(只读消费面,供前端 HistoryDrawer)。
|
|
866
|
+
// 平台级审计面(P4 上收 _platform):install/enable/disable/uninstall/upgrade/rollback 留痕
|
|
867
|
+
// 由 _lifecycle/history.ts recordHistory 写入;本端点仅登录用户可读(非匿名名单,Bearer 守卫保护)。
|
|
868
|
+
// 表不存在/查询失败一律返回空数组(历史是观测面,不阻断主流程,与终态审计附录 B-5 同语义)。
|
|
869
|
+
"history": {
|
|
870
|
+
async get() {
|
|
871
|
+
await respond(async () => {
|
|
872
|
+
const rows = (await db.query(
|
|
873
|
+
"select solution, version, action, status, operator, message, created_at from market_history order by created_at desc",
|
|
874
|
+
[],
|
|
875
|
+
)) as Array<Record<string, unknown>>;
|
|
876
|
+
return rows ?? [];
|
|
877
|
+
});
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export default createMarketApi(marketEnv());
|