@oj-module/cli 0.2.6 → 0.2.14
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/README.md +1 -1
- package/devkit/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +28 -23
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +170 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +9 -3
- package/src/init.ts +24 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +177 -0
- package/src/preview.ts +106 -56
- package/src/sign.ts +21 -6
- package/src/usage.ts +22 -2
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +24 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
- package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +4 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +26 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +26 -22
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +8 -8
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +97 -57
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +4 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +66 -21
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/cdn/README.md +48 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +850 -0
- package/templates/init.include.yaml +14 -2
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +580 -23
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +13 -10
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
- package/templates/api/src/notifications/api.ts +0 -32
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview 派生 config(设计 §6 单服务化,2026-10-19)。
|
|
3
|
+
*
|
|
4
|
+
* 单服务 preview 由 oj 一个进程同时出 API 与静态(web 挂载 + spa: true),
|
|
5
|
+
* 而 spa 只能来自 config 文件(CLI --app-path 的 upsert 保留但不设置
|
|
6
|
+
* spa——实测 oj 0.1.80)。用户在模板里手改 config 不可取(web/dist 缺失时
|
|
7
|
+
* oj serve 整体拒启,会连带挂掉 ojm dev),故 preview 现场生成一份
|
|
8
|
+
* 派生 config:
|
|
9
|
+
*
|
|
10
|
+
* - 与原 config 同目录(api/.ojm-preview.yaml,固定名)——config 内
|
|
11
|
+
* 相对路径(证书 / db / api 挂载目录)按 config 所在目录解析,派生后
|
|
12
|
+
* 与原件逐字一致,无需重写任何路径;
|
|
13
|
+
* - 唯一改动:upsert {prefix: "/", web: <siteDir>, spa: true} 挂载 +
|
|
14
|
+
* 显式传 port 时覆盖 server.port;
|
|
15
|
+
* - 临时产物:preview 进程退出即删(三路清理见 preview.ts),模板
|
|
16
|
+
* .gitignore 已收录该文件名。
|
|
17
|
+
*
|
|
18
|
+
* 纯变换(derivePreviewConfigText)与落盘(writePreviewConfig)分离:
|
|
19
|
+
* 前者可单测到「除注入点外 doc 与原 parse 深相等」,后者只做 IO。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { parse, stringify } from "yaml";
|
|
25
|
+
import { resolveConfigPath } from "./layout";
|
|
26
|
+
|
|
27
|
+
/** 派生 config 文件名(与原 config 同目录;固定名 = 崩溃自愈,不积累残留) */
|
|
28
|
+
export const PREVIEW_CONFIG_NAME = ".ojm-preview.yaml";
|
|
29
|
+
|
|
30
|
+
export interface DerivePreviewConfigOptions {
|
|
31
|
+
/** 全站产物目录(layout.distDir),写入 web 挂载的绝对路径 */
|
|
32
|
+
siteDir: string
|
|
33
|
+
/**
|
|
34
|
+
* release 形态的后端模块根(api/dist)。给了且原 config 声明了 platform.modules 时,
|
|
35
|
+
* 把 `platform.root` 一并改写到该目录——特权模块的受控根必须与 `--api-path` 同源,
|
|
36
|
+
* 否则 release 下 reloadConfig 会按 dev 的 src 判定归属(装好的方案热更不生效)。
|
|
37
|
+
*/
|
|
38
|
+
apiRoot?: string
|
|
39
|
+
/** 显式端口时覆盖 server.port;缺省沿用原 config(oj 代码默认 9778) */
|
|
40
|
+
port?: number
|
|
41
|
+
/** 原 config 文本(测试注入);缺省读 api/config.yaml */
|
|
42
|
+
configText?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PreviewConfigFiles {
|
|
46
|
+
/** 派生 config 路径(startOj 的 configPath) */
|
|
47
|
+
configPath: string
|
|
48
|
+
/** 同步删除派生 config(幂等;文件已不存在不抛) */
|
|
49
|
+
cleanup: () => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** YAML 解析失败的人话报错(口径对齐 oj-config.ts 的 readServerBlock) */
|
|
53
|
+
function parseConfigDoc(configText: string, configPath: string): Record<string, unknown> {
|
|
54
|
+
let doc: unknown;
|
|
55
|
+
try {
|
|
56
|
+
doc = parse(configText);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[ojm] ${configPath} 解析失败(不是合法 YAML):${(error as Error).message}\n`
|
|
61
|
+
+ "该文件由 ojm init 生成,改动后请用 YAML 语法检查确认缩进与引号。",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
// 空 config(parse → null)合法:oj 用内置默认值;派生时从空对象建 mounts/server
|
|
65
|
+
if (doc === null || doc === undefined)
|
|
66
|
+
return {};
|
|
67
|
+
if (typeof doc !== "object" || Array.isArray(doc)) {
|
|
68
|
+
throw new TypeError(
|
|
69
|
+
`[ojm] ${configPath} 顶层不是映射(拿到 ${Array.isArray(doc) ? "序列" : typeof doc})——`
|
|
70
|
+
+ "oj config 必须是键值结构,请检查文件内容。",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return doc as Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 挂载条目 = 非空非数组的普通对象(mounts: 一行一条) */
|
|
77
|
+
function isMountEntry(m: unknown): m is Record<string, unknown> {
|
|
78
|
+
return m !== null && typeof m === "object" && !Array.isArray(m);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 找 prefix === "/" 的 web 挂载条目下标;没有返回 -1 */
|
|
82
|
+
function findRootWebMount(mounts: unknown[]): number {
|
|
83
|
+
return mounts.findIndex(m => isMountEntry(m) && m.prefix === "/" && "web" in m);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 是否存在 prefix === "/" 的 api 挂载(oj 语义:与 web 挂载配对时恒无 SPA
|
|
88
|
+
* 回落)。调用方(preview)必然注入 / web 挂载,故「有 / api 挂载」即「配对」。
|
|
89
|
+
*/
|
|
90
|
+
export function hasRootApiMount(mounts: unknown[]): boolean {
|
|
91
|
+
return mounts.some(m => isMountEntry(m) && m.prefix === "/" && "api" in m);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** mounts 段校验:缺省视为无挂载;存在但非序列 → 人话报错(不静默置空掩盖手误) */
|
|
95
|
+
function readMounts(doc: Record<string, unknown>, configPath: string): unknown[] {
|
|
96
|
+
if (doc.mounts === undefined || doc.mounts === null)
|
|
97
|
+
return [];
|
|
98
|
+
if (!Array.isArray(doc.mounts)) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`[ojm] ${configPath} 的 mounts 不是序列(拿到 ${typeof doc.mounts})——`
|
|
101
|
+
+ "每个挂载占一行「- prefix: ...」列表,请对照 ojm init 生成的模板修正。",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return doc.mounts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 原 config 文本 → 派生 config 文本。
|
|
109
|
+
*
|
|
110
|
+
* upsert 语义(DR-3):已有 / web 条目 → 覆盖 web 指向、强制 spa: true,
|
|
111
|
+
* 保留该条目其余键(如 headers);没有 → 追加新条目。其余挂载与原样
|
|
112
|
+
* 顶层段一律不动。
|
|
113
|
+
*/
|
|
114
|
+
export function derivePreviewConfigText(configText: string, opts: DerivePreviewConfigOptions & { configPath?: string }): string {
|
|
115
|
+
const configPath = opts.configPath ?? "api/config.yaml";
|
|
116
|
+
const doc = parseConfigDoc(configText, configPath);
|
|
117
|
+
|
|
118
|
+
const mounts = [...readMounts(doc, configPath)];
|
|
119
|
+
const idx = findRootWebMount(mounts);
|
|
120
|
+
if (idx >= 0) {
|
|
121
|
+
// 保留用户自定义键(headers 等),只接管指向与回落
|
|
122
|
+
mounts[idx] = { ...(mounts[idx] as Record<string, unknown>), web: opts.siteDir, spa: true };
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
mounts.push({ prefix: "/", web: opts.siteDir, spa: true });
|
|
126
|
+
}
|
|
127
|
+
doc.mounts = mounts;
|
|
128
|
+
|
|
129
|
+
// platform.root 跟随 api 挂载根:dev = src,release(preview)= dist
|
|
130
|
+
const platform = doc.platform;
|
|
131
|
+
if (opts.apiRoot !== undefined && platform !== null && typeof platform === "object" && !Array.isArray(platform)) {
|
|
132
|
+
const mods = (platform as Record<string, unknown>).modules;
|
|
133
|
+
if (Array.isArray(mods) && mods.length > 0)
|
|
134
|
+
(platform as Record<string, unknown>).root = opts.apiRoot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (opts.port !== undefined) {
|
|
138
|
+
const server = doc.server !== null && typeof doc.server === "object" && !Array.isArray(doc.server)
|
|
139
|
+
? { ...(doc.server as Record<string, unknown>) }
|
|
140
|
+
: {};
|
|
141
|
+
server.port = opts.port;
|
|
142
|
+
doc.server = server;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return stringify(doc);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 读 config 文本的 mounts 序列(解析失败 / mounts 非序列即人话报错) */
|
|
149
|
+
export function parseMounts(configText: string, configPath = "api/config.yaml"): unknown[] {
|
|
150
|
+
return readMounts(parseConfigDoc(configText, configPath), configPath);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 落盘派生 config 并返回清理器。config 文本读取失败(不存在/无权限)直接
|
|
155
|
+
* 抛——四查已保证 config 存在,这里只可能是 IO 异常。
|
|
156
|
+
*/
|
|
157
|
+
export function writePreviewConfig(projectRoot: string, opts: DerivePreviewConfigOptions): PreviewConfigFiles {
|
|
158
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
159
|
+
const previewPath = path.join(path.dirname(configPath), PREVIEW_CONFIG_NAME);
|
|
160
|
+
const configText = opts.configText ?? fs.readFileSync(configPath, "utf-8");
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(previewPath, derivePreviewConfigText(configText, { ...opts, configPath }));
|
|
163
|
+
|
|
164
|
+
let cleaned = false;
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (cleaned)
|
|
167
|
+
return;
|
|
168
|
+
cleaned = true;
|
|
169
|
+
try {
|
|
170
|
+
fs.unlinkSync(previewPath);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// 已不存在(并发退出/手工删除)——幂等
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return { configPath: previewPath, cleanup };
|
|
177
|
+
}
|
package/src/preview.ts
CHANGED
|
@@ -1,44 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ojm preview` —— 生产形态预览(设计 §6
|
|
2
|
+
* `ojm preview` —— 生产形态预览(设计 §6,2026-10-19 单服务化)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 职责:fail-fast 四查 → `oj migrate`(失败即退,不起 server)→ 派生
|
|
5
|
+
* preview config(注入 web 挂载 + spa: true)→ `oj serve`(release/js,
|
|
6
|
+
* **单进程同时出 API 与静态**)。ojm 不再自建 http server、不再反代——
|
|
7
|
+
* 前后端同端口同进程,与「oj serve + web 挂载」的生产公式逐字同形。
|
|
8
|
+
*
|
|
9
|
+
* 派生 config 为临时产物(api/.ojm-preview.yaml,与原件同目录保证相对
|
|
10
|
+
* 路径解析一致),三路清理:ready 拒绝路径 / SIGINT / process exit 兜底
|
|
11
|
+
* (语义与落盘见 preview-config.ts)。
|
|
12
|
+
*
|
|
13
|
+
* web.config.ts 的 htmlTransform 是 serve 期钩子,单服务下 oj 直出 HTML
|
|
14
|
+
* 无从介入——启动时检测到配置即 WARN 并指向 oj 的 html_meta /
|
|
15
|
+
* html_meta_handler(devkit 手册「静态站点与 per-route meta」)。
|
|
9
16
|
*
|
|
10
17
|
* 一律传绝对路径:oj 命令行相对 CWD、config 内相对 config 目录,混用即歧义。
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
import type { OjProcess } from "./oj";
|
|
21
|
+
import type { PreviewConfigFiles } from "./preview-config";
|
|
14
22
|
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
16
|
-
import
|
|
17
|
-
import path, { normalize } from "node:path";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
18
25
|
import process from "node:process";
|
|
19
|
-
import {
|
|
20
|
-
import { loadHtmlTransform } from "./html-transform";
|
|
26
|
+
import { loadModulesConfig } from "./config";
|
|
21
27
|
import { resolveConfigPath, resolveLayout } from "./layout";
|
|
22
28
|
import { startOj } from "./oj";
|
|
23
29
|
import { readOjApiPrefix } from "./oj-config";
|
|
24
|
-
import {
|
|
30
|
+
import { hasRootApiMount, parseMounts, writePreviewConfig } from "./preview-config";
|
|
25
31
|
import { resolveOjBin } from "./vendor";
|
|
26
32
|
|
|
27
|
-
const DEFAULT_PORT = 4173;
|
|
28
|
-
|
|
29
33
|
export interface PreviewOptions {
|
|
34
|
+
/** 显式端口(覆盖派生 config 的 server.port);缺省沿用 config(oj 代码默认 9778) */
|
|
30
35
|
port?: number
|
|
31
|
-
/** 静态改由 oj --app-path 直出(history 深链接 404,已知限制) */
|
|
32
|
-
ojStatic?: boolean
|
|
33
36
|
/** 注入桩站点目录(默认 layout.distDir) */
|
|
34
37
|
siteDir?: string
|
|
35
38
|
/** 注入桩 oj 命令执行(migrate);默认 execFileSync(bin/oj, args, inherit) */
|
|
36
39
|
execOj?: (args: string[]) => void
|
|
37
40
|
/** 注入桩 oj 进程;默认 startOj */
|
|
38
41
|
ojStarter?: (configPath: string, base: string, apiPath: string, extraArgs: string[]) => OjProcess
|
|
42
|
+
/** 注入桩派生 config 落盘(测试);默认 writePreviewConfig */
|
|
43
|
+
configWriter?: (projectRoot: string, opts: { siteDir: string, port?: number, apiRoot?: string }) => PreviewConfigFiles
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
export
|
|
46
|
+
export interface PreviewHandle {
|
|
47
|
+
/** 停 oj 子进程并清理派生 config(测试与编程式消费用) */
|
|
48
|
+
stop: () => Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 单服务 preview。返回后进程保活依赖 oj 子进程 stdio 管道(ref 状态)
|
|
53
|
+
* 与已注册的信号 handler;Ctrl+C 走 SIGINT 善后。
|
|
54
|
+
*/
|
|
55
|
+
export async function runPreview(projectRoot: string, opts: PreviewOptions = {}): Promise<PreviewHandle> {
|
|
42
56
|
const layout = resolveLayout(projectRoot);
|
|
43
57
|
const siteDir = opts.siteDir ?? layout.distDir;
|
|
44
58
|
const ojBin = resolveOjBin(projectRoot);
|
|
@@ -55,8 +69,9 @@ export async function previewServer(projectRoot: string, opts: PreviewOptions =
|
|
|
55
69
|
if (!existsSync(path.join(apiDist, "manifests.yaml")))
|
|
56
70
|
throw new Error(`[ojm] ${apiDist} 缺少 manifests.yaml。\n请先 ojm build(后端 oj build 产物)。`);
|
|
57
71
|
|
|
58
|
-
// htmlTransform
|
|
59
|
-
|
|
72
|
+
// DR-5:htmlTransform 存在性检测(只读 web.config.ts 的字段,不 import 变换模块——
|
|
73
|
+
// 单服务下 oj 直出 HTML,钩子不生效;执行用户代码只会白付副作用)
|
|
74
|
+
await warnHtmlTransformUnsupported(projectRoot);
|
|
60
75
|
|
|
61
76
|
const execOj = opts.execOj ?? ((args: string[]) => execFileSync(ojBin, args, { stdio: "inherit" }));
|
|
62
77
|
|
|
@@ -64,57 +79,92 @@ export async function previewServer(projectRoot: string, opts: PreviewOptions =
|
|
|
64
79
|
console.log("[ojm] oj migrate(应用待执行迁移)…");
|
|
65
80
|
execOj(["migrate", "-c", configPath, "-d", apiDist]);
|
|
66
81
|
|
|
82
|
+
// 派生 config 在 migrate 成功后落盘:失败即退路径零残留
|
|
67
83
|
const base = readOjApiPrefix(configPath);
|
|
84
|
+
const writer = opts.configWriter ?? ((root: string, o: { siteDir: string, port?: number, apiRoot?: string }) => writePreviewConfig(root, o));
|
|
85
|
+
// apiRoot 交给派生器:release 下 platform.root 必须指向 api/dist(与 --api-path 同源)
|
|
86
|
+
const preview = writer(projectRoot, { siteDir, port: opts.port, apiRoot: apiDist });
|
|
87
|
+
|
|
88
|
+
// / api 挂载与注入的 / web 挂载配对时 oj 不开 SPA 回落(oj 语义,见 devkit 手册 §13)
|
|
89
|
+
if (hasRootApiMount(parseMounts(readFileSync(configPath, "utf-8"), configPath)))
|
|
90
|
+
console.warn("[ojm] config 的 api 挂载位于 /:与 web 挂载配对时 oj 不做 SPA 深链回落(已知限制),深链接刷新将 404。");
|
|
91
|
+
|
|
68
92
|
const starter = opts.ojStarter ?? ((cfg: string, b: string, apiPath: string, extraArgs: string[]) =>
|
|
69
93
|
startOj(cfg, b, apiPath, undefined, extraArgs));
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
// starter 可能同步抛错(startOj 首行 readOjPort 对缺/坏 server.port 即 throw,
|
|
95
|
+
// 先于返回 OjProcess)——此刻 exit 兜底尚未注册,必须就地清理派生 config
|
|
96
|
+
let oj: OjProcess;
|
|
97
|
+
try {
|
|
98
|
+
oj = starter(preview.configPath, base, apiDist, []);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
preview.cleanup();
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
72
104
|
try {
|
|
73
105
|
await oj.ready;
|
|
74
106
|
}
|
|
75
107
|
catch (error) {
|
|
76
|
-
// F3:ready 拒绝必须先回收子进程,否则 oj
|
|
108
|
+
// F3:ready 拒绝必须先回收子进程,否则 oj 孤儿化继续占端口;派生 config 一并清理
|
|
77
109
|
await oj.stop().catch(() => {});
|
|
110
|
+
preview.cleanup();
|
|
78
111
|
throw error;
|
|
79
112
|
}
|
|
80
|
-
console.log(`[ojm] oj
|
|
113
|
+
console.log(`[ojm] oj 已就绪(release/js,单服务:${base} API + 静态 SPA 同进程):http://127.0.0.1:${oj.port}`);
|
|
114
|
+
console.log(`\n[ojm] preview 已启动:http://localhost:${oj.port}`);
|
|
115
|
+
console.log(`[ojm] 静态由 oj web 挂载直出(spa 深链回落);派生 config ${path.basename(preview.configPath)} 为本进程临时产物,退出自动清理`);
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
res.writeHead(400, { "content-type": "text/plain" });
|
|
90
|
-
res.end("400 Bad Request: malformed URL encoding");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
94
|
-
// 反代前缀跟随 config 的 mounts:(api 挂载 prefix;F2)
|
|
95
|
-
if (rel.startsWith(`${base}/`)) {
|
|
96
|
-
void proxyApi(ojTarget)(req, res);
|
|
117
|
+
// 退出回收:派生 config 必须随 oj 一起消亡(test/integration 断言其不存在)
|
|
118
|
+
// 终端 Ctrl-C 是进程组信号:外层 bin 转发一份 + 组送达一份,内层会收到
|
|
119
|
+
// 两个 SIGINT。用 on + 幂等守卫吞掉第二个——once 在首次触发后即摘除,
|
|
120
|
+
// 第二个 SIGINT 会走默认动作杀死本进程,清理全没跑(2026-10-19 冒烟实测)。
|
|
121
|
+
let stopping = false;
|
|
122
|
+
const onSigint = () => {
|
|
123
|
+
if (stopping)
|
|
97
124
|
return;
|
|
98
|
-
|
|
99
|
-
serveStatic(req, res);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
server.on("close", () => {
|
|
103
|
-
void oj.stop();
|
|
104
|
-
});
|
|
105
|
-
process.once("SIGINT", () => {
|
|
125
|
+
stopping = true;
|
|
106
126
|
console.log("\n[ojm] 正在退出…");
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// 同步先删派生 config:后续任何死亡路径(含信号)都零残留;oj 运行中
|
|
128
|
+
// 已读入 config,运行期删除文件无影响
|
|
129
|
+
preview.cleanup();
|
|
130
|
+
void oj.stop().finally(() => process.exit(0));
|
|
109
131
|
setTimeout(() => process.exit(0), 1500).unref();
|
|
110
|
-
}
|
|
132
|
+
};
|
|
133
|
+
process.on("SIGINT", onSigint);
|
|
134
|
+
// 兜底:非优雅路径退出(未捕获异常等)时同步清掉派生 config,防下次启动读到陈旧文件
|
|
135
|
+
const onExit = () => {
|
|
136
|
+
preview.cleanup();
|
|
137
|
+
};
|
|
138
|
+
process.once("exit", onExit);
|
|
139
|
+
|
|
140
|
+
// stop 摘除信号/退出 handler:编程式同进程多次调用不累积旧实例的闭包
|
|
141
|
+
// (CLI 单次调用行为不变)
|
|
142
|
+
const stop = async () => {
|
|
143
|
+
process.removeListener("SIGINT", onSigint);
|
|
144
|
+
process.removeListener("exit", onExit);
|
|
145
|
+
await oj.stop();
|
|
146
|
+
preview.cleanup();
|
|
147
|
+
};
|
|
111
148
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (opts.ojStatic)
|
|
115
|
-
console.log(`[ojm] --oj-static:oj 已挂静态直出 http://127.0.0.1:${oj.port}(history 深链接 404 为已知限制;ojm 本口仍提供 SPA 兜底 + /api 反代)`);
|
|
116
|
-
else
|
|
117
|
-
console.log(`[ojm] 静态兜底:ojm(SPA 回退)+ ${base} 反代 oj`);
|
|
149
|
+
return { stop };
|
|
150
|
+
}
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
/**
|
|
153
|
+
* web.config.ts 配了 htmlTransform → WARN。加载失败(缺文件/坏配置)静默
|
|
154
|
+
* 跳过:能走到 preview 的工程必然过 ojm build,config 问题会在那里先炸。
|
|
155
|
+
*/
|
|
156
|
+
async function warnHtmlTransformUnsupported(projectRoot: string): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
const config = await loadModulesConfig(projectRoot);
|
|
159
|
+
if (config.htmlTransform) {
|
|
160
|
+
console.warn(
|
|
161
|
+
"[ojm] web.config.ts 配置了 htmlTransform:单服务 preview 由 oj 直出 HTML,该钩子不生效。\n"
|
|
162
|
+
+ "逐路由 meta 请改用 oj 的 server.html_meta(构建期 JSON)或 server.html_meta_handler(运行期 handler),"
|
|
163
|
+
+ "见 bin/devkit/api-manual.md「静态站点与 per-route meta」。",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// config 加载失败:不阻塞 preview(build 已先行校验过)
|
|
169
|
+
}
|
|
120
170
|
}
|
package/src/sign.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface SignOptions {
|
|
|
17
17
|
id?: string
|
|
18
18
|
/** 私钥 PEM 文件(缺省 ~/.ojm/keys/<id>/private.pem) */
|
|
19
19
|
key?: string
|
|
20
|
+
/**
|
|
21
|
+
* 内存私钥 PEM(一次性/隔离场景专用,与 --key 二选一)。
|
|
22
|
+
* 用途:`ojm market trial` 的一次性 keypair 不落盘,经此直接传进来;
|
|
23
|
+
* 给了它就跳过文件存在性检查,也永远不读真 ~/.ojm/keys(计划 §3.3.1)。
|
|
24
|
+
*/
|
|
25
|
+
privateKeyPem?: string
|
|
20
26
|
/** 写完离线自检(verifyCanonical 复核) */
|
|
21
27
|
check?: boolean
|
|
22
28
|
/** 测试注入;缺省 process.cwd() */
|
|
@@ -46,10 +52,17 @@ export function signCommand(tgz: string, opts: SignOptions = {}): SignResult {
|
|
|
46
52
|
if (!fs.existsSync(tgz))
|
|
47
53
|
throw new Error(`[ojm] 方案包不存在:${tgz}`);
|
|
48
54
|
const cwd = opts.cwd ?? process.cwd();
|
|
49
|
-
// key
|
|
50
|
-
let keyFile
|
|
55
|
+
// key 解析:内存 PEM(不落盘)> --key 显式文件 > ~/.ojm/keys/<id>/private.pem(--id 必填)
|
|
56
|
+
let keyFile = "";
|
|
51
57
|
let keyId: string;
|
|
52
|
-
|
|
58
|
+
let memoryPem: string | undefined;
|
|
59
|
+
if (opts.privateKeyPem !== undefined && opts.privateKeyPem !== "") {
|
|
60
|
+
memoryPem = opts.privateKeyPem;
|
|
61
|
+
keyId = opts.id ?? "";
|
|
62
|
+
if (keyId === "")
|
|
63
|
+
throw new Error("[ojm] 使用内存私钥时须同时给 --id <publisher.id>(.sig 的发布方标识;C2-37 命名空间)");
|
|
64
|
+
}
|
|
65
|
+
else if (opts.key !== undefined) {
|
|
53
66
|
keyFile = path.resolve(cwd, opts.key);
|
|
54
67
|
keyId = opts.id ?? keyIdFromKeyPath(path.relative(os.homedir(), keyFile)) ?? "";
|
|
55
68
|
if (keyId === "")
|
|
@@ -63,13 +76,15 @@ export function signCommand(tgz: string, opts: SignOptions = {}): SignResult {
|
|
|
63
76
|
}
|
|
64
77
|
if (!isValidKeyId(keyId))
|
|
65
78
|
throw new Error(`[ojm] key_id 命名空间非法(C2-37):「${keyId}」`);
|
|
66
|
-
if (
|
|
67
|
-
|
|
79
|
+
if (memoryPem === undefined) {
|
|
80
|
+
if (!fs.existsSync(keyFile!))
|
|
81
|
+
throw new Error(`[ojm] 私钥文件不存在:${keyFile}(用 ojm keygen --id ${keyId} 生成,或 --key 指定)`);
|
|
82
|
+
}
|
|
68
83
|
|
|
69
84
|
const bytes = new Uint8Array(fs.readFileSync(tgz));
|
|
70
85
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
71
86
|
const canonical = canonicalString(deriveName(tgz), deriveVersion(tgz), sha256);
|
|
72
|
-
const privateKeyPem = readPrivateKeyPem(keyFile);
|
|
87
|
+
const privateKeyPem = memoryPem ?? readPrivateKeyPem(keyFile!);
|
|
73
88
|
const value = signCanonical(privateKeyPem, canonical);
|
|
74
89
|
const publicKeyHex = exportRawPublicKeyHex(privateKeyPem);
|
|
75
90
|
const sig = { alg: SIG_ALG, key_id: keyId, public_key: publicKeyHex, value, canon: CANON };
|
package/src/usage.ts
CHANGED
|
@@ -20,8 +20,9 @@ export function usageText(): string {
|
|
|
20
20
|
--bundle 方案包构建:manifest.yaml 契约校验(C2,fail-fast)→ 确定性 tgz
|
|
21
21
|
→ dist/<name>-<version>.tgz + sha256(市场安装器的上游产物)
|
|
22
22
|
--out <dir> --bundle 输出目录(缺省 dist/)
|
|
23
|
-
ojm preview [port]
|
|
24
|
-
生产形态预览(migrate → oj serve
|
|
23
|
+
ojm preview [port]
|
|
24
|
+
生产形态预览(migrate → oj serve 单服务:API 与静态 SPA 同进程,深链回落)
|
|
25
|
+
静态由 oj web 挂载直出;ojm 不起第二个服务、不做反代
|
|
25
26
|
ojm api [dir] [--check] [--docs] [--exempt <path>]
|
|
26
27
|
契约代码生成(client / routes / openapi / stub)
|
|
27
28
|
--check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
|
|
@@ -64,6 +65,25 @@ export function usageText(): string {
|
|
|
64
65
|
(与 archive .sig 同构,公钥取 ~/.ojm/keys/<id>/private.pem,缺省 --id 推导)→ 写回
|
|
65
66
|
<dir>。CDN 侧 resolveCdn/index 拉取索引时校验签名(篡改/不符即拒,G4 fail-closed)。
|
|
66
67
|
--check 离线自检两份索引签名
|
|
68
|
+
ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
|
|
69
|
+
把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由
|
|
70
|
+
api/config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin
|
|
71
|
+
must_change=1),会自动改密换全权 token 后再写入。
|
|
72
|
+
ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
|
|
73
|
+
把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz
|
|
74
|
+
→ 包级 detached 签名 <name>-<version>.tgz.sig。**不碰索引**(D-1:索引由
|
|
75
|
+
build-index 扫描重建,多人发布才不会互相覆盖)。
|
|
76
|
+
ojm market build-index [--id <key>] [--key <pem>] [--check]
|
|
77
|
+
扫描 cdn/*.tgz、提取包内 manifest,全量重建 bundles.yaml + manifests.yaml 并签名。
|
|
78
|
+
结果只取决于目录内容:连续执行字节一致。
|
|
79
|
+
ojm market verify [--file <tgz>] [--rebuild-index]
|
|
80
|
+
离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 /
|
|
81
|
+
索引验签 / manifest 结构(含 C2-31 desc===summary、各版本 0001 迁移一致)分列输出。
|
|
82
|
+
--rebuild-index 额外比对「扫描重建的索引」与盘上索引,检出落了包却没重建的漂移。
|
|
83
|
+
注意:结论恒带边界声明——校验通过只说明来源与完整性,不构成内容安全性背书。
|
|
84
|
+
ojm market trial [--fixture <name>@<version>] [--keep]
|
|
85
|
+
在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,
|
|
86
|
+
跑完即毁;--keep 保留现场供排查。用于确认「这个包到底能不能装」,不污染你的工程。
|
|
67
87
|
ojm test|exec|openapi|migrate|schema [args...]
|
|
68
88
|
透传给 oj 同名子命令(自动补 -c <project>/api/config.yaml;其余参数原样转发)
|
|
69
89
|
例:ojm test --anonymous / ojm migrate --db report / ojm openapi --check
|
package/src/vendor.ts
CHANGED
|
@@ -32,9 +32,11 @@ export const NPM_PKG = "@oj-bin/oj";
|
|
|
32
32
|
* docs/superpowers/specs/2026-10-14-ojm-0.1.18-design.md);**0.1.20 抬到 0.1.73**:
|
|
33
33
|
* 平级 `.response` 出参声明位自 oj v0.1.72 起、`oj openapi` 的 `del`→`delete`
|
|
34
34
|
* 操作键归一自 v0.1.73 起,`ojm api` 的 openapi 流水线以这两者为前提
|
|
35
|
-
* (见 CHANGELOG 0.1.20 / docs/guides/api-codegen-openapi.md
|
|
35
|
+
* (见 CHANGELOG 0.1.20 / docs/guides/api-codegen-openapi.md)。**0.2.13 抬到 0.1.81**:
|
|
36
|
+
* 模板侧 `solution-perms.ts` 权限级联清理用 `startswith` 操作键(v0.1.81 补齐,
|
|
37
|
+
* 构造器迁移收尾),oj < 0.1.81 时卸载方案报 `unknown variant startswith` fail-closed。
|
|
36
38
|
*/
|
|
37
|
-
export const MIN_OJ_VERSION = "0.1.
|
|
39
|
+
export const MIN_OJ_VERSION = "0.1.81";
|
|
38
40
|
|
|
39
41
|
/** 按段数值比较(字符串比较会把 0.1.9 判成大于 0.1.69);非数字段按 0 处理 */
|
|
40
42
|
export function isSupportedOjVersion(version: string): boolean {
|
package/src/versions.ts
CHANGED
|
@@ -100,9 +100,37 @@ export function validateSharedVersions(
|
|
|
100
100
|
return errors;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* 源码形态守卫(R5 prepack 断言的本地版):cli 源码树内 shell-dist 与
|
|
105
|
+
* packages/runtime 同源于一次宿主构建,升 runtime 后忘跑 build:shell 会让
|
|
106
|
+
* versions.json 停在旧版本,门禁误报「请对齐版本后重新安装」。此处先对比
|
|
107
|
+
* 两者版本,过期直接报真因。发布形态(pack 后)无同级 runtime 目录,跳过。
|
|
108
|
+
*/
|
|
109
|
+
export function hostArtifactFreshnessError(
|
|
110
|
+
hostRuntime: string | undefined,
|
|
111
|
+
workspaceRuntime: string | undefined,
|
|
112
|
+
): string | undefined {
|
|
113
|
+
if (!hostRuntime || !workspaceRuntime || hostRuntime === workspaceRuntime)
|
|
114
|
+
return undefined;
|
|
115
|
+
return `内置宿主产物(shell-dist/versions.json)过期:其中 runtime 版本(${hostRuntime})≠ 本仓 packages/runtime 版本(${workspaceRuntime})。
|
|
116
|
+
这是升 runtime 后未重建宿主导致的漂移,请执行:pnpm --filter @oj-module/cli build:shell`;
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
/** 校验模块工程的共享依赖版本矩阵,违规直接抛错并打印期望值 */
|
|
104
120
|
export function checkSharedVersions(projectRoot: string, shellDist: string): void {
|
|
105
121
|
const versions = readHostVersions(shellDist);
|
|
122
|
+
|
|
123
|
+
let workspaceRuntime: string | undefined;
|
|
124
|
+
try {
|
|
125
|
+
workspaceRuntime = JSON.parse(fs.readFileSync(path.join(CLI_ROOT, "..", "runtime", "package.json"), "utf-8")).version;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* 发布形态:cli 包旁无 runtime 源码目录,跳过守卫 */
|
|
129
|
+
}
|
|
130
|
+
const stale = hostArtifactFreshnessError(versions["@oj-module/runtime"], workspaceRuntime);
|
|
131
|
+
if (stale)
|
|
132
|
+
throw new Error(stale);
|
|
133
|
+
|
|
106
134
|
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
|
|
107
135
|
const errors = validateSharedVersions(pkg, versions, spec => readInstalledVersion(projectRoot, spec));
|
|
108
136
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "ojm api --check 豁免清单(D12/F19):把契约与 oj 路由对账中「确认无害」的差异降级为 skip。modules=整模块跳过双向对账(值是 oj 后端模块名,即 api/src/<模块> 的目录名);paths=按路径跳过,'/xxx/*' 为一层通配(只盖 /xxx/<单段>),其余为精确前缀匹配。仅降级 error→skip,绝不引入新错误;文件缺失或 JSON 损坏时静默按空豁免处理。注意:本文件是严格 JSON,写 // 注释会导致解析失败、豁免静默全失效——说明请写在本 _comment 字段。",
|
|
3
|
-
"modules": [
|
|
4
|
-
|
|
3
|
+
"modules": [
|
|
4
|
+
"web"
|
|
5
|
+
],
|
|
6
|
+
"paths": [
|
|
7
|
+
"/auth/*"
|
|
8
|
+
]
|
|
5
9
|
}
|
|
@@ -24,11 +24,18 @@
|
|
|
24
24
|
|
|
25
25
|
# 站点挂载(oj v0.1.58 起顶层扁平表:一行=一个 URL 前缀+一个目录,api/web 恰好其一)。
|
|
26
26
|
# 被删除的 server.api_prefix / app_spa_fallback 已由 mounts 取代;
|
|
27
|
-
# ojm dev
|
|
27
|
+
# ojm dev 的 SPA 深链回落由 ojm 自建静态层负责;ojm preview 为单服务形态,
|
|
28
|
+
# 现场生成派生 config(api/.ojm-preview.yaml,注入下面的 web 挂载 + spa: true),无需手改。
|
|
28
29
|
mounts:
|
|
29
30
|
- prefix: /api # api 挂载:/api/<module>/...(目录相对 config 所在目录 → api/src)
|
|
30
31
|
api: src
|
|
31
|
-
#
|
|
32
|
+
# 本地模拟 CDN:直出工程根 cdn/(内容见 cdn/README.md)。
|
|
33
|
+
# 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN。
|
|
34
|
+
# 不得给此挂载设 spa——缺文件必须返回 404,回落 index.html 会让市场解析崩。
|
|
35
|
+
- prefix: /cdn
|
|
36
|
+
web: ../cdn
|
|
37
|
+
# 生产若由 oj 直出前端(前后端同进程,即 ojm preview 的生产形态),
|
|
38
|
+
# 取消下一行注释并先 build 出 web/dist(preview 会自动注入等价挂载,无需手改):
|
|
32
39
|
# - prefix: "/"
|
|
33
40
|
# web: "../web/dist" # 相对 api/config.yaml → 项目根/web/dist
|
|
34
41
|
# spa: true
|
|
@@ -74,6 +81,12 @@ vars:
|
|
|
74
81
|
# ---- 平台 exec 白名单(ojm install 的解压/迁移两处消费;模板逐字对齐 lifecycle steps 命令构造,
|
|
75
82
|
# 不得增删——多一个 token 都会 NotCapable)----
|
|
76
83
|
platform:
|
|
84
|
+
# 平台特权模块名单:方案安装器的 reloading 段调 server.reloadConfig 触发热更。
|
|
85
|
+
# 缺这条 → 装在最后一步报 NotCapable: module '_platform' is not platform-privileged。
|
|
86
|
+
modules: [_platform]
|
|
87
|
+
# 受控挂载根 = api 挂载目录(dev 形态 src)。ojm preview 派生 config 时会改写为 dist,
|
|
88
|
+
# 保持与 `--api-path` 同源;手写部署请按实际模块根填写。
|
|
89
|
+
root: src
|
|
77
90
|
# ---- U6 信任库加固(P3-1;P2-2 红线 spike 结论)----
|
|
78
91
|
# 威胁:devkit 0.1.75 语义下 fs: 段对全部模块生效(jail = fs.root),且 trusted_paths 只增不减——
|
|
79
92
|
# 信任库落在第三方方案模块可写的 fs.root 内即可被其伪造信任锚。加固须两端配合:
|
|
@@ -90,6 +103,15 @@ platform:
|
|
|
90
103
|
- 'tar -xzf ${path:archive} -C ${path:dest}'
|
|
91
104
|
- 'oj migrate --format json -d ${path:dir} ${raw:module}'
|
|
92
105
|
|
|
106
|
+
# ---- 方案安装器的落盘面(fs.* 全局)----
|
|
107
|
+
# 安装器要解包到 modules/.staging、写 dist/modules、备份 db.sqlite——全在 config 目录内。
|
|
108
|
+
# 缺本段 = fs.* 抛 NotCapable,且失败发生在任务文件落盘之前:表现为「POST 安装返回 200、
|
|
109
|
+
# 但 task 查询恒 404」,排查成本极高。生产收紧口径见上面 platform 段的 U6 说明
|
|
110
|
+
# (fs.root 收窄到安装子树 + third-party 模块 jail 隔离属部署清单项)。
|
|
111
|
+
fs:
|
|
112
|
+
root: .
|
|
113
|
+
readonly: false
|
|
114
|
+
|
|
93
115
|
# ---- LDAP 连接段(D1/PRD §4.5 两段式之连接段;oj 插件严格解析本段,未知键启动 fail-fast——只写下列键)----
|
|
94
116
|
# 取消注释并填值即接通目录服务;应用映射段在 DB/设置页(ldap.enabled、ldap_base_dn、
|
|
95
117
|
# ldap_uid_attr、ldap_email_attr,group=ldap,即时生效)。本段改动属 restart_pending,需重启。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# `_platform` —— 平台内核层
|
|
2
|
+
|
|
3
|
+
本目录是 oj 框架的**平台内核**,不是业务模块(manifest `name: _platform` 仅声明共有表,
|
|
4
|
+
不拥有业务路由前缀)。它服务于 `api/src` 下所有第一方内置模块(`system`/`auth`/`home`/…)
|
|
5
|
+
以及运行时经方案市场安装的方案包。
|
|
6
|
+
|
|
7
|
+
## 三层职责
|
|
8
|
+
|
|
9
|
+
1. **`_shared/service/`** —— 跨模块服务(被业务模块向上 import)。
|
|
10
|
+
- `infra/`:跨模块基础设施(与具体业务无关的通用能力)
|
|
11
|
+
- `guard.ts` 判权决策矩阵(AC1-1~6)
|
|
12
|
+
- `access.ts` 判权 IO 出口(`assertSuperAdmin` / `assertSystemAdmin` / `assertPermission`)
|
|
13
|
+
- `query.ts` 分页/模糊查询工具
|
|
14
|
+
- `rate.ts` 速率限制(登录失败/联系人查询)
|
|
15
|
+
- `audit.ts` 审计原语(`auditOp` / `auditDeny`)
|
|
16
|
+
- `oplog.ts` 操作日志查询(`searchOplog`)
|
|
17
|
+
- `domain/`:管理域共享服务(多模块共用的「系统管理」域逻辑,所有权归 `_platform` 的 `sys_*` 表)
|
|
18
|
+
- `users.ts` / `roles.ts` / `settings.ts` / `solution-perms.ts` / `contacts.ts`
|
|
19
|
+
2. **`_lifecycle/`** —— 方案生命周期引擎(安装/卸载/注册/验签/信任的声明式状态机,
|
|
20
|
+
与编排端点 `market/api.ts` 职责分离,见 `market/api.ts` 顶部 ISP 注释)。
|
|
21
|
+
3. **编排 API 面** —— 经 oj 扫描 `src/**/api.ts` 暴露,路径前缀即目录名:
|
|
22
|
+
- `contacts/api.ts` → `/api/_platform/contacts`
|
|
23
|
+
- `market/api.ts` → `/api/_platform/market`(方案市场,全 superadmin 门禁,R9)
|
|
24
|
+
- `solutions/api.ts` → `/api/_platform/solutions`(方案路由/验签数据源)
|
|
25
|
+
|
|
26
|
+
## 边界约定
|
|
27
|
+
|
|
28
|
+
- 平台内核**不写**业务功能;业务功能归属第一方模块(`/api/<模块>/*`)或运行时方案包。
|
|
29
|
+
- `migrations/` / `schema.yaml` / `seed*.sql` 落地平台共有表(`users` + RBAC 六表 + 设置/日志/路由注册 + 信任库)。
|
|
30
|
+
- `system` 模块管理这些 `sys_*` 表,但其领域服务在本目录 `domain/`(共享内核),
|
|
31
|
+
非 `system/` 内——避免跨模块 import 私有目录。
|
|
@@ -9,7 +9,7 @@ export interface FsFace {
|
|
|
9
9
|
writeFile: (path: string, data: Uint8Array) => Promise<void>
|
|
10
10
|
readFile: (path: string) => Promise<Uint8Array>
|
|
11
11
|
mkdir: (path: string, opts?: { recursive?: boolean }) => Promise<void>
|
|
12
|
-
remove: (path: string, opts?: { recursive?: boolean }) => Promise<void>
|
|
12
|
+
remove: (path: string, opts?: { recursive?: boolean, force?: boolean }) => Promise<void>
|
|
13
13
|
rename: (oldPath: string, newPath: string) => Promise<void>
|
|
14
14
|
/** 复制文件(R14 迁移前置备份:sqlite 库 copy 为 <db>.bak-<ts>);两路径须同文件系统无需递归 */
|
|
15
15
|
copyFile: (src: string, dest: string) => Promise<void>
|