openlearn-next 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli-doctor.mjs +18 -5
- package/cli.mjs +49 -0
- package/dist/assets/{ActivityWorkspaceWidget-5PzyxF6a.js → ActivityWorkspaceWidget-Dq4kjBS2.js} +1 -1
- package/dist/assets/{AdminDirectoryView-BCtmQTEK.js → AdminDirectoryView-B6OAs5Rj.js} +1 -1
- package/dist/assets/AssessmentCenter-B3f1DxSG.js +1 -0
- package/dist/assets/AssignmentFilePreview-DQbvbgnd.js +3 -0
- package/dist/assets/{BatchPickerModal-CbmjInp7.js → BatchPickerModal-CoOWloSS.js} +1 -1
- package/dist/assets/{ClassesView-DEnMaDOt.js → ClassesView-Q56Kf4eL.js} +5 -5
- package/dist/assets/{ClassroomCountdownWidget-CHq4983w.js → ClassroomCountdownWidget-C6luhy8P.js} +1 -1
- package/dist/assets/{CloudDriveModal-D2t3Uyzr.js → CloudDriveModal-DLf4fbKR.js} +1 -1
- package/dist/assets/ComputerLabView-D9-Tk7yA.js +1 -0
- package/dist/assets/{CourseManagement-DOXyJ_j-.js → CourseManagement-DNSfyre-.js} +1 -1
- package/dist/assets/{CourseWizardModal-Dbi6Caw2.js → CourseWizardModal-DAueHXcl.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-CyxDIJAF.js → CoursewareHubPanel-DLjH-UzF.js} +1 -1
- package/dist/assets/{Dashboard-2rLu_ZBy.js → Dashboard-qMVadkd2.js} +2 -2
- package/dist/assets/{ExportWeightModal-BrmAh6VX.js → ExportWeightModal-Dkh7OC8U.js} +1 -1
- package/dist/assets/{HelpTour-D_q1z_MI.js → HelpTour-BLuiS8rr.js} +1 -1
- package/dist/assets/{HelpView-CVBrKDrd.js → HelpView-CTF6B9ml.js} +1 -1
- package/dist/assets/{ImportLessonsModal-PhHtX31R.js → ImportLessonsModal-Ra-OF_Vo.js} +1 -1
- package/dist/assets/{ImportModal-DqjqNeH4.js → ImportModal-H5s5MyZY.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-Vi6lLz7a.js → InteractiveCoursewareViewer-BfHEDrCf.js} +1 -1
- package/dist/assets/InteractiveWhiteboard-KBzmiNBG.js +65 -0
- package/dist/assets/{LazyCourseware-KIt0rGtW.js → LazyCourseware-BaZS8-i1.js} +2 -2
- package/dist/assets/LazyWhiteboard-DGyln3JC.js +2 -0
- package/dist/assets/LessonEditorView-B5JXYyup.js +1 -0
- package/dist/assets/{LiveClassroomView-BRsZ6QEq.js → LiveClassroomView-DezAlve2.js} +7 -7
- package/dist/assets/{NotificationDetailModal-tY5rxYCN.js → NotificationDetailModal-DbMMrIHQ.js} +1 -1
- package/dist/assets/{PluginView-CedpAW6d.js → PluginView-CvIr-ANj.js} +1 -1
- package/dist/assets/{ProcessLogsModal-Cfr-T9Gd.js → ProcessLogsModal-_BwkmJVw.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-bUA002kP.js → QuizGeneratorModal-CTj2moWM.js} +1 -1
- package/dist/assets/StudentAssignmentEvalPanel-DrfZT8Lr.js +1 -0
- package/dist/assets/{StudentAssignmentView-BhAz6Hfk.js → StudentAssignmentView-D_EhblVQ.js} +1 -1
- package/dist/assets/StudentLessonView-BHG50-4M.js +1 -0
- package/dist/assets/{StudentView-PHKxIV9T.js → StudentView-DKDhnJQx.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-DgXYe6_x.js → SystemResourceLibraryModal-C5y8H5Pf.js} +1 -1
- package/dist/assets/TeacherView-CWDK3DOe.js +2 -0
- package/dist/assets/{TimetableView-D2ozoj_D.js → TimetableView-C8kdJlf7.js} +1 -1
- package/dist/assets/{UTIF-FfCsrHhe.js → UTIF-4E-wdh3X.js} +1 -1
- package/dist/assets/index-BBznBaN5.js +131 -0
- package/dist/assets/index-DPky2leG.css +1 -0
- package/dist/assets/{index.es-DyEF0CZ9.js → index.es-WXdI0L-F.js} +1 -1
- package/dist/assets/pluginParsers-DkiO-BBu.js +1 -0
- package/dist/assets/student-workspace-1yVTbWHC.js +1 -0
- package/dist/assets/{vendor-icons-CXrZ3R2V.js → vendor-icons-hc6MjYFv.js} +1 -1
- package/dist/assets/{vendor-pdf-C1Vh2Aqq.js → vendor-pdf-By0fofA0.js} +3 -3
- package/dist/assets/{vendor-pptx-Bzn6whfK.js → vendor-pptx-EjNUtdws.js} +178 -178
- package/dist/cli-migrator.cjs +18 -1
- package/dist/index.html +3 -3
- package/dist/server.cjs +731 -277
- package/package.json +7 -4
- package/scripts/assert-node-runtime.mjs +165 -0
- package/dist/assets/AssessmentCenter-B43ojHG-.js +0 -1
- package/dist/assets/ComputerLabView-YtnoT5Rb.js +0 -1
- package/dist/assets/InteractiveWhiteboard-lTzeayX5.js +0 -65
- package/dist/assets/LazyWhiteboard-CCN3pYcW.js +0 -2
- package/dist/assets/LessonEditorView-D_GVhHuM.js +0 -1
- package/dist/assets/StudentLessonView-DC1yX4kG.js +0 -1
- package/dist/assets/TeacherView-DscJtOJ9.js +0 -2
- package/dist/assets/index-2676X8YB.js +0 -131
- package/dist/assets/index-C6hlh8pw.css +0 -1
- package/dist/assets/pluginParsers-e4PSm2Iy.js +0 -1
package/dist/server.cjs
CHANGED
|
@@ -944,6 +944,9 @@ var init_manifest_schema = __esm({
|
|
|
944
944
|
"stage.display.overlay": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
945
945
|
"stage.display.action": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
946
946
|
"assignment.submission.preview": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
947
|
+
// 作业**附件**预览:插件声明自己能处理的文件类型(extensions / mimeTypes),
|
|
948
|
+
// 宿主在预览对话框里按类型匹配后给出入口。数据型贡献(远程插件可用)。
|
|
949
|
+
"assignment.file.preview": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
947
950
|
"student.classroom.overlay": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
948
951
|
"student.profile.tab": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
949
952
|
"whiteboard.renderer": import_zod.z.array(frontendSlotEntrySchema).optional(),
|
|
@@ -1188,12 +1191,12 @@ var init_manifest_schema = __esm({
|
|
|
1188
1191
|
// packages/core/di/api-key-crypto.ts
|
|
1189
1192
|
function resolveEncryptionKeyStorageDir(env = process.env, cwd = process.cwd()) {
|
|
1190
1193
|
if (env.OPENLEARN_DATA_DIR?.trim()) {
|
|
1191
|
-
return
|
|
1194
|
+
return import_node_path5.default.join(import_node_path5.default.resolve(env.OPENLEARN_DATA_DIR.trim()), "storage");
|
|
1192
1195
|
}
|
|
1193
1196
|
if (env.OPENLEARN_DB_PATH?.trim()) {
|
|
1194
|
-
return
|
|
1197
|
+
return import_node_path5.default.join(import_node_path5.default.dirname(import_node_path5.default.resolve(env.OPENLEARN_DB_PATH.trim())), "storage");
|
|
1195
1198
|
}
|
|
1196
|
-
return
|
|
1199
|
+
return import_node_path5.default.join(cwd, "storage");
|
|
1197
1200
|
}
|
|
1198
1201
|
function normalizeKeyHex(raw) {
|
|
1199
1202
|
let value = raw.trim();
|
|
@@ -1214,11 +1217,11 @@ function readProcessEnvKey(env) {
|
|
|
1214
1217
|
return normalizeKeyHex(raw);
|
|
1215
1218
|
}
|
|
1216
1219
|
function readDotEnvKey(cwd) {
|
|
1217
|
-
const envPath =
|
|
1218
|
-
if (!
|
|
1220
|
+
const envPath = import_node_path5.default.resolve(cwd, ".env");
|
|
1221
|
+
if (!import_node_fs4.default.existsSync(envPath)) return null;
|
|
1219
1222
|
let content = "";
|
|
1220
1223
|
try {
|
|
1221
|
-
content =
|
|
1224
|
+
content = import_node_fs4.default.readFileSync(envPath, "utf8");
|
|
1222
1225
|
} catch {
|
|
1223
1226
|
return null;
|
|
1224
1227
|
}
|
|
@@ -1227,11 +1230,11 @@ function readDotEnvKey(cwd) {
|
|
|
1227
1230
|
return normalizeKeyHex(match[1] ?? "");
|
|
1228
1231
|
}
|
|
1229
1232
|
function keyFilePath(storageDir) {
|
|
1230
|
-
return
|
|
1233
|
+
return import_node_path5.default.join(storageDir, ENCRYPTION_KEY_FILE_NAME);
|
|
1231
1234
|
}
|
|
1232
1235
|
function chmodKeyFile(filePath) {
|
|
1233
1236
|
try {
|
|
1234
|
-
|
|
1237
|
+
import_node_fs4.default.chmodSync(filePath, 384);
|
|
1235
1238
|
} catch (err) {
|
|
1236
1239
|
throw new Error(
|
|
1237
1240
|
`\u65E0\u6CD5\u5C06 ${filePath} \u7684\u6743\u9650\u8BBE\u4E3A 0600\uFF08${err instanceof Error ? err.message : err}\uFF09\u3002\u5DF2\u62D2\u7EDD\u7EE7\u7EED\u4F7F\u7528\u8BE5\u5BC6\u94A5\u6587\u4EF6\u3002`
|
|
@@ -1239,8 +1242,8 @@ function chmodKeyFile(filePath) {
|
|
|
1239
1242
|
}
|
|
1240
1243
|
}
|
|
1241
1244
|
function readKeyFile(filePath) {
|
|
1242
|
-
if (!
|
|
1243
|
-
const normalized = normalizeKeyHex(
|
|
1245
|
+
if (!import_node_fs4.default.existsSync(filePath)) return null;
|
|
1246
|
+
const normalized = normalizeKeyHex(import_node_fs4.default.readFileSync(filePath, "utf8"));
|
|
1244
1247
|
if (!normalized) {
|
|
1245
1248
|
throw new EncryptionKeyInvalidError(
|
|
1246
1249
|
`\u5BC6\u94A5\u6587\u4EF6 ${filePath} \u662F\u7A7A\u7684\u3002\u8BF7\u5220\u6389\u5B83\uFF0C\u6216\u5199\u5165\u539F\u6765\u7684 64 \u4F4D hex\u3002\u4E0D\u8981\u8BA9\u7CFB\u7EDF\u53E6\u9020\u4E00\u628A\u94A5\u5319\u3002`
|
|
@@ -1253,9 +1256,9 @@ function keysEqual(left, right) {
|
|
|
1253
1256
|
return import_node_crypto.default.timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
1254
1257
|
}
|
|
1255
1258
|
function persistGeneratedKey(filePath, generated) {
|
|
1256
|
-
|
|
1259
|
+
import_node_fs4.default.mkdirSync(import_node_path5.default.dirname(filePath), { recursive: true });
|
|
1257
1260
|
try {
|
|
1258
|
-
|
|
1261
|
+
import_node_fs4.default.writeFileSync(filePath, `${generated}
|
|
1259
1262
|
`, { mode: 384, flag: "wx" });
|
|
1260
1263
|
} catch (err) {
|
|
1261
1264
|
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
@@ -1291,7 +1294,7 @@ function resolveEncryptionKey(options = {}) {
|
|
|
1291
1294
|
const fromDotEnv = fromProcess ? null : readDotEnvKey(cwd);
|
|
1292
1295
|
const configured = fromProcess ?? fromDotEnv;
|
|
1293
1296
|
const configuredSource = fromProcess ? "env" : fromDotEnv ? "env-file" : null;
|
|
1294
|
-
const fileKey =
|
|
1297
|
+
const fileKey = import_node_fs4.default.existsSync(filePath) ? readKeyFile(filePath) : null;
|
|
1295
1298
|
if (configured && fileKey && !keysEqual(configured, fileKey)) {
|
|
1296
1299
|
throw new EncryptionKeyMismatchError(filePath);
|
|
1297
1300
|
}
|
|
@@ -1400,13 +1403,13 @@ function encryptStoredPlaintextApiKeys(db2) {
|
|
|
1400
1403
|
}
|
|
1401
1404
|
return { updated, skipped };
|
|
1402
1405
|
}
|
|
1403
|
-
var import_node_crypto,
|
|
1406
|
+
var import_node_crypto, import_node_fs4, import_node_path5, ENCRYPTION_KEY_FILE_NAME, AUTO_GENERATED_KEY_WARNING, HEX_64, EncryptionKeyInvalidError, EncryptionKeyMismatchError, cached;
|
|
1404
1407
|
var init_api_key_crypto = __esm({
|
|
1405
1408
|
"packages/core/di/api-key-crypto.ts"() {
|
|
1406
1409
|
"use strict";
|
|
1407
1410
|
import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
1408
|
-
|
|
1409
|
-
|
|
1411
|
+
import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
1412
|
+
import_node_path5 = __toESM(require("node:path"), 1);
|
|
1410
1413
|
ENCRYPTION_KEY_FILE_NAME = "encryption-key";
|
|
1411
1414
|
AUTO_GENERATED_KEY_WARNING = "\u6B63\u5728\u4F7F\u7528\u81EA\u52A8\u751F\u6210\u7684\u52A0\u5BC6\u5BC6\u94A5\uFF08storage/encryption-key\uFF0C\u4E0E bootstrap-credentials.txt \u540C\u76EE\u5F55\uFF0C\u6743\u9650 600\uFF09\u3002\u5EFA\u8BAE\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ENCRYPTION_KEY \u4E3A\u8BE5\u6587\u4EF6\u4E2D\u7684 64 \u4F4D hex\uFF0C\u5E76\u79BB\u7EBF\u5907\u4EFD\u8BE5\u6587\u4EF6\u3002site-backup \u4E0D\u5305\u542B\u6B64\u6587\u4EF6\u3002\u4E22\u5931\u540E\u65E0\u6CD5\u89E3\u5BC6 AI Provider API Key \u4E0E\u73ED\u7EA7\u4E34\u65F6\u53E3\u4EE4\u3002";
|
|
1412
1415
|
HEX_64 = /^[0-9a-fA-F]{64}$/;
|
|
@@ -1921,49 +1924,49 @@ function getMaxUncompressedSize() {
|
|
|
1921
1924
|
}
|
|
1922
1925
|
function isInsideRoot(root, target) {
|
|
1923
1926
|
if (!root || !target) return false;
|
|
1924
|
-
const rel =
|
|
1927
|
+
const rel = path9.relative(path9.resolve(root), path9.resolve(target));
|
|
1925
1928
|
if (rel === "") return false;
|
|
1926
|
-
if (
|
|
1927
|
-
return rel !== ".." && !rel.startsWith(".." +
|
|
1929
|
+
if (path9.isAbsolute(rel)) return false;
|
|
1930
|
+
return rel !== ".." && !rel.startsWith(".." + path9.sep);
|
|
1928
1931
|
}
|
|
1929
1932
|
function realPathOrNull(target) {
|
|
1930
1933
|
try {
|
|
1931
|
-
return
|
|
1934
|
+
return fs7.realpathSync(target);
|
|
1932
1935
|
} catch {
|
|
1933
1936
|
return null;
|
|
1934
1937
|
}
|
|
1935
1938
|
}
|
|
1936
1939
|
function realPathOrSelf(target) {
|
|
1937
|
-
return realPathOrNull(target) ??
|
|
1940
|
+
return realPathOrNull(target) ?? path9.resolve(target);
|
|
1938
1941
|
}
|
|
1939
1942
|
function getMaxUploadBodyBytes() {
|
|
1940
1943
|
return getMaxUncompressedSize();
|
|
1941
1944
|
}
|
|
1942
1945
|
function readZipBufferFromPath(zipPath) {
|
|
1943
|
-
if (!
|
|
1946
|
+
if (!fs7.existsSync(zipPath)) {
|
|
1944
1947
|
throw new Error(`[install-utils] ZIP \u6587\u4EF6\u4E0D\u5B58\u5728: ${zipPath}`);
|
|
1945
1948
|
}
|
|
1946
|
-
return
|
|
1949
|
+
return fs7.readFileSync(zipPath);
|
|
1947
1950
|
}
|
|
1948
1951
|
function writeBufferToTempZip(buffer, prefix = "plugin-upload-") {
|
|
1949
1952
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
1950
1953
|
throw new Error("[install-utils] \u7A7A\u7684 ZIP Buffer");
|
|
1951
1954
|
}
|
|
1952
|
-
const zipPath =
|
|
1953
|
-
|
|
1955
|
+
const zipPath = path9.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
|
|
1956
|
+
fs7.writeFileSync(zipPath, buffer);
|
|
1954
1957
|
return zipPath;
|
|
1955
1958
|
}
|
|
1956
1959
|
function removeTempZip(zipPath) {
|
|
1957
1960
|
if (!zipPath) return;
|
|
1958
1961
|
try {
|
|
1959
|
-
|
|
1962
|
+
fs7.rmSync(zipPath, { force: true });
|
|
1960
1963
|
} catch {
|
|
1961
1964
|
}
|
|
1962
1965
|
}
|
|
1963
1966
|
function removeTempFile(filePath) {
|
|
1964
1967
|
if (!filePath) return;
|
|
1965
1968
|
try {
|
|
1966
|
-
|
|
1969
|
+
fs7.rmSync(filePath, { force: true });
|
|
1967
1970
|
} catch {
|
|
1968
1971
|
}
|
|
1969
1972
|
}
|
|
@@ -2209,13 +2212,13 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
|
|
|
2209
2212
|
const esbuild = await import("esbuild");
|
|
2210
2213
|
const allowedRoots = [];
|
|
2211
2214
|
const allowRoot = (dir) => {
|
|
2212
|
-
const lexical =
|
|
2215
|
+
const lexical = path9.resolve(dir);
|
|
2213
2216
|
if (allowedRoots.some((r) => r.lexical === lexical)) return;
|
|
2214
2217
|
allowedRoots.push({ lexical, real: realPathOrSelf(lexical) });
|
|
2215
2218
|
};
|
|
2216
2219
|
allowRoot(resolveDir);
|
|
2217
2220
|
const isPermittedPath = (target) => {
|
|
2218
|
-
const resolved =
|
|
2221
|
+
const resolved = path9.resolve(target);
|
|
2219
2222
|
if (allowedRoots.some((r) => isInsideRoot(r.lexical, resolved))) return true;
|
|
2220
2223
|
const real = realPathOrNull(resolved);
|
|
2221
2224
|
if (!real) return false;
|
|
@@ -2242,14 +2245,14 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
|
|
|
2242
2245
|
setup(build) {
|
|
2243
2246
|
build.onResolve({ filter: /.*/ }, (args) => {
|
|
2244
2247
|
if (args.path.includes("core/di/interfaces")) {
|
|
2245
|
-
const tsPath =
|
|
2246
|
-
if (
|
|
2247
|
-
allowRoot(
|
|
2248
|
+
const tsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.ts");
|
|
2249
|
+
if (fs7.existsSync(tsPath)) {
|
|
2250
|
+
allowRoot(path9.dirname(tsPath));
|
|
2248
2251
|
return { path: tsPath };
|
|
2249
2252
|
}
|
|
2250
|
-
const jsPath =
|
|
2251
|
-
if (
|
|
2252
|
-
allowRoot(
|
|
2253
|
+
const jsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.js");
|
|
2254
|
+
if (fs7.existsSync(jsPath)) {
|
|
2255
|
+
allowRoot(path9.dirname(jsPath));
|
|
2253
2256
|
return { path: jsPath };
|
|
2254
2257
|
}
|
|
2255
2258
|
}
|
|
@@ -2263,8 +2266,8 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
|
|
|
2263
2266
|
};
|
|
2264
2267
|
}
|
|
2265
2268
|
if (args.path.startsWith(".")) {
|
|
2266
|
-
const importerDir = args.resolveDir ||
|
|
2267
|
-
const candidate =
|
|
2269
|
+
const importerDir = args.resolveDir || path9.resolve(resolveDir);
|
|
2270
|
+
const candidate = path9.resolve(importerDir, args.path);
|
|
2268
2271
|
if (!isPermittedPath(candidate)) {
|
|
2269
2272
|
return {
|
|
2270
2273
|
errors: [
|
|
@@ -2302,7 +2305,7 @@ async function bundlePlugin(entryCode, resolveDir, outfile) {
|
|
|
2302
2305
|
]
|
|
2303
2306
|
});
|
|
2304
2307
|
if (outfile !== void 0) {
|
|
2305
|
-
assertPluginCodeSafe(
|
|
2308
|
+
assertPluginCodeSafe(fs7.readFileSync(outfile, "utf-8"));
|
|
2306
2309
|
return void 0;
|
|
2307
2310
|
}
|
|
2308
2311
|
const bundled = result.outputFiles?.[0]?.text;
|
|
@@ -2316,8 +2319,8 @@ async function extractZipEntries(zip, destDir, maxTotalBytes) {
|
|
|
2316
2319
|
let totalBytes = 0;
|
|
2317
2320
|
for (const [name, file] of Object.entries(zip.files)) {
|
|
2318
2321
|
if (file.dir) continue;
|
|
2319
|
-
const filePath =
|
|
2320
|
-
|
|
2322
|
+
const filePath = path9.join(destDir, name);
|
|
2323
|
+
fs7.mkdirSync(path9.dirname(filePath), { recursive: true });
|
|
2321
2324
|
const content = await file.async("nodebuffer");
|
|
2322
2325
|
totalBytes += content.length;
|
|
2323
2326
|
if (totalBytes > maxTotalBytes) {
|
|
@@ -2325,7 +2328,7 @@ async function extractZipEntries(zip, destDir, maxTotalBytes) {
|
|
|
2325
2328
|
`ZIP bomb prevention: actual uncompressed size ${totalBytes} bytes exceeds limit of ${maxTotalBytes} bytes`
|
|
2326
2329
|
);
|
|
2327
2330
|
}
|
|
2328
|
-
|
|
2331
|
+
fs7.writeFileSync(filePath, content);
|
|
2329
2332
|
}
|
|
2330
2333
|
}
|
|
2331
2334
|
async function validateAndBundleZip(zipPath) {
|
|
@@ -2373,16 +2376,16 @@ async function validateAndBundleZip(zipPath) {
|
|
|
2373
2376
|
throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
|
|
2374
2377
|
}
|
|
2375
2378
|
const entryCode = await entryFile.async("string");
|
|
2376
|
-
const tmpDir =
|
|
2379
|
+
const tmpDir = path9.join(os2.tmpdir(), `plugin-build-${(0, import_uuid6.v7)()}`);
|
|
2377
2380
|
try {
|
|
2378
|
-
|
|
2381
|
+
fs7.mkdirSync(tmpDir, { recursive: true });
|
|
2379
2382
|
await extractZipEntries(zip, tmpDir, getMaxUncompressedSize());
|
|
2380
|
-
const bundledPath =
|
|
2383
|
+
const bundledPath = path9.join(os2.tmpdir(), `plugin-bundle-${(0, import_uuid6.v7)()}.mjs`);
|
|
2381
2384
|
await bundlePlugin(entryCode, tmpDir, bundledPath);
|
|
2382
2385
|
return { manifest, bundledPath, entryFileName: resolvedMain, package: zip };
|
|
2383
2386
|
} finally {
|
|
2384
2387
|
try {
|
|
2385
|
-
|
|
2388
|
+
fs7.rmSync(tmpDir, { recursive: true, force: true });
|
|
2386
2389
|
} catch {
|
|
2387
2390
|
}
|
|
2388
2391
|
}
|
|
@@ -2390,14 +2393,14 @@ async function validateAndBundleZip(zipPath) {
|
|
|
2390
2393
|
async function extractManifestFromBundle(_bundledCode) {
|
|
2391
2394
|
throw new Error("extractManifestFromBundle is not yet implemented");
|
|
2392
2395
|
}
|
|
2393
|
-
var import_jszip,
|
|
2396
|
+
var import_jszip, fs7, os2, path9, import_uuid6, PluginCodeSecurityError, LEX_SCAN_LIMIT;
|
|
2394
2397
|
var init_install_utils = __esm({
|
|
2395
2398
|
"packages/core/esm-loader/install-utils.ts"() {
|
|
2396
2399
|
"use strict";
|
|
2397
2400
|
import_jszip = __toESM(require("jszip"), 1);
|
|
2398
|
-
|
|
2401
|
+
fs7 = __toESM(require("fs"), 1);
|
|
2399
2402
|
os2 = __toESM(require("os"), 1);
|
|
2400
|
-
|
|
2403
|
+
path9 = __toESM(require("path"), 1);
|
|
2401
2404
|
import_uuid6 = require("uuid");
|
|
2402
2405
|
init_manifest_schema();
|
|
2403
2406
|
PluginCodeSecurityError = class extends Error {
|
|
@@ -2472,7 +2475,7 @@ var init_redaction = __esm({
|
|
|
2472
2475
|
// packages/core/observability/rotating-file-stream.ts
|
|
2473
2476
|
function removeIfExists(filePath) {
|
|
2474
2477
|
try {
|
|
2475
|
-
|
|
2478
|
+
import_node_fs5.default.unlinkSync(filePath);
|
|
2476
2479
|
} catch {
|
|
2477
2480
|
}
|
|
2478
2481
|
}
|
|
@@ -2491,7 +2494,7 @@ function unregisterStream(stream) {
|
|
|
2491
2494
|
function safeRename(from, to) {
|
|
2492
2495
|
try {
|
|
2493
2496
|
removeIfExists(to);
|
|
2494
|
-
|
|
2497
|
+
import_node_fs5.default.renameSync(from, to);
|
|
2495
2498
|
return true;
|
|
2496
2499
|
} catch {
|
|
2497
2500
|
return false;
|
|
@@ -2516,12 +2519,12 @@ function resolveAuditRotationConfig(env = process.env) {
|
|
|
2516
2519
|
const maxFiles = Math.floor(readPositiveInt(env.AUDIT_LOG_MAX_FILES, 20, "AUDIT_LOG_MAX_FILES"));
|
|
2517
2520
|
return { maxBytes: Math.floor(maxSizeMb * 1024 * 1024), maxFiles };
|
|
2518
2521
|
}
|
|
2519
|
-
var
|
|
2522
|
+
var import_node_fs5, import_node_path6, import_node_stream, activeStreams, exitHookInstalled, RotatingFileStream;
|
|
2520
2523
|
var init_rotating_file_stream = __esm({
|
|
2521
2524
|
"packages/core/observability/rotating-file-stream.ts"() {
|
|
2522
2525
|
"use strict";
|
|
2523
|
-
|
|
2524
|
-
|
|
2526
|
+
import_node_fs5 = __toESM(require("node:fs"), 1);
|
|
2527
|
+
import_node_path6 = __toESM(require("node:path"), 1);
|
|
2525
2528
|
import_node_stream = require("node:stream");
|
|
2526
2529
|
activeStreams = /* @__PURE__ */ new Set();
|
|
2527
2530
|
exitHookInstalled = false;
|
|
@@ -2535,7 +2538,7 @@ var init_rotating_file_stream = __esm({
|
|
|
2535
2538
|
this.filePath = options.filePath;
|
|
2536
2539
|
this.maxBytes = options.maxBytes;
|
|
2537
2540
|
this.maxFiles = options.maxFiles;
|
|
2538
|
-
this.dir =
|
|
2541
|
+
this.dir = import_node_path6.default.dirname(this.filePath);
|
|
2539
2542
|
}
|
|
2540
2543
|
/** 当前活动文件路径(测试与排障用) */
|
|
2541
2544
|
get activeFilePath() {
|
|
@@ -2556,7 +2559,7 @@ var init_rotating_file_stream = __esm({
|
|
|
2556
2559
|
if (this.currentSize > 0 && this.currentSize + buf.length > this.maxBytes) {
|
|
2557
2560
|
this.rotate();
|
|
2558
2561
|
}
|
|
2559
|
-
const written =
|
|
2562
|
+
const written = import_node_fs5.default.writeSync(this.fd, buf, 0, buf.length, null);
|
|
2560
2563
|
this.currentSize += written;
|
|
2561
2564
|
callback();
|
|
2562
2565
|
} catch (err) {
|
|
@@ -2576,7 +2579,7 @@ var init_rotating_file_stream = __esm({
|
|
|
2576
2579
|
closeFd() {
|
|
2577
2580
|
if (this.fd !== null) {
|
|
2578
2581
|
try {
|
|
2579
|
-
|
|
2582
|
+
import_node_fs5.default.closeSync(this.fd);
|
|
2580
2583
|
} catch {
|
|
2581
2584
|
}
|
|
2582
2585
|
this.fd = null;
|
|
@@ -2585,11 +2588,11 @@ var init_rotating_file_stream = __esm({
|
|
|
2585
2588
|
}
|
|
2586
2589
|
ensureOpen() {
|
|
2587
2590
|
if (this.fd !== null) return;
|
|
2588
|
-
|
|
2589
|
-
this.fd =
|
|
2591
|
+
import_node_fs5.default.mkdirSync(this.dir, { recursive: true });
|
|
2592
|
+
this.fd = import_node_fs5.default.openSync(this.filePath, "a");
|
|
2590
2593
|
registerStream(this);
|
|
2591
2594
|
try {
|
|
2592
|
-
this.currentSize =
|
|
2595
|
+
this.currentSize = import_node_fs5.default.statSync(this.filePath).size;
|
|
2593
2596
|
} catch {
|
|
2594
2597
|
this.currentSize = 0;
|
|
2595
2598
|
}
|
|
@@ -2603,13 +2606,13 @@ var init_rotating_file_stream = __esm({
|
|
|
2603
2606
|
this.closeFd();
|
|
2604
2607
|
for (let i = this.maxFiles - 1; i >= 1; i -= 1) {
|
|
2605
2608
|
const from = this.historyFilePath(i);
|
|
2606
|
-
if (
|
|
2609
|
+
if (import_node_fs5.default.existsSync(from)) {
|
|
2607
2610
|
safeRename(from, this.historyFilePath(i + 1));
|
|
2608
2611
|
}
|
|
2609
2612
|
}
|
|
2610
2613
|
safeRename(this.filePath, this.historyFilePath(1));
|
|
2611
2614
|
this.currentSize = 0;
|
|
2612
|
-
this.fd =
|
|
2615
|
+
this.fd = import_node_fs5.default.openSync(this.filePath, "a");
|
|
2613
2616
|
}
|
|
2614
2617
|
};
|
|
2615
2618
|
}
|
|
@@ -2640,28 +2643,28 @@ function consoleStream() {
|
|
|
2640
2643
|
function createLogger(component) {
|
|
2641
2644
|
return logger.child({ component });
|
|
2642
2645
|
}
|
|
2643
|
-
var import_pino,
|
|
2646
|
+
var import_pino, import_node_fs6, import_node_path7, defaultLevel, consoleLevel, fileLevel, rootLevel, logDir, logFile, streams, fileLogging, logger;
|
|
2644
2647
|
var init_logger = __esm({
|
|
2645
2648
|
"packages/core/observability/logger.ts"() {
|
|
2646
2649
|
"use strict";
|
|
2647
2650
|
import_pino = __toESM(require("pino"), 1);
|
|
2648
|
-
|
|
2649
|
-
|
|
2651
|
+
import_node_fs6 = __toESM(require("node:fs"), 1);
|
|
2652
|
+
import_node_path7 = __toESM(require("node:path"), 1);
|
|
2650
2653
|
init_redaction();
|
|
2651
2654
|
init_rotating_file_stream();
|
|
2652
2655
|
defaultLevel = process.env.LOG_LEVEL || (process.env.HTTP_ACCESS_LOG === "all" ? "debug" : process.env.NODE_ENV === "production" ? "info" : "debug");
|
|
2653
2656
|
consoleLevel = process.env.LOG_CONSOLE_LEVEL || defaultLevel;
|
|
2654
2657
|
fileLevel = defaultLevel;
|
|
2655
2658
|
rootLevel = minPinoLevel(consoleLevel, fileLevel);
|
|
2656
|
-
logDir =
|
|
2657
|
-
logFile =
|
|
2659
|
+
logDir = import_node_path7.default.resolve(process.env.LOG_DIR || import_node_path7.default.join(process.cwd(), "logs"));
|
|
2660
|
+
logFile = import_node_path7.default.join(logDir, "openlearn.log");
|
|
2658
2661
|
streams = [
|
|
2659
2662
|
{ level: consoleLevel, stream: consoleStream() }
|
|
2660
2663
|
];
|
|
2661
2664
|
fileLogging = false;
|
|
2662
2665
|
try {
|
|
2663
|
-
if (!
|
|
2664
|
-
|
|
2666
|
+
if (!import_node_fs6.default.existsSync(logDir)) import_node_fs6.default.mkdirSync(logDir, { recursive: true });
|
|
2667
|
+
import_node_fs6.default.accessSync(logDir, import_node_fs6.default.constants.W_OK);
|
|
2665
2668
|
fileLogging = true;
|
|
2666
2669
|
} catch (err) {
|
|
2667
2670
|
console.warn(`[logger] \u65E0\u6CD5\u5199\u5165\u65E5\u5FD7\u76EE\u5F55 ${logDir}\uFF0C\u4EC5\u8F93\u51FA\u5230 stdout\u3002`, err instanceof Error ? err.message : err);
|
|
@@ -2787,15 +2790,15 @@ function buildAuditLogger() {
|
|
|
2787
2790
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
2788
2791
|
return (0, import_pino2.default)({ level: "silent" }, import_pino2.default.destination(1));
|
|
2789
2792
|
}
|
|
2790
|
-
const logDir2 =
|
|
2793
|
+
const logDir2 = import_node_path8.default.resolve(process.env.LOG_DIR || import_node_path8.default.join(process.cwd(), "logs"));
|
|
2791
2794
|
try {
|
|
2792
|
-
if (!
|
|
2793
|
-
|
|
2795
|
+
if (!import_node_fs7.default.existsSync(logDir2)) import_node_fs7.default.mkdirSync(logDir2, { recursive: true });
|
|
2796
|
+
import_node_fs7.default.accessSync(logDir2, import_node_fs7.default.constants.W_OK);
|
|
2794
2797
|
const rotation = resolveAuditRotationConfig();
|
|
2795
2798
|
streams2.push({
|
|
2796
2799
|
level: "info",
|
|
2797
2800
|
stream: new RotatingFileStream({
|
|
2798
|
-
filePath:
|
|
2801
|
+
filePath: import_node_path8.default.join(logDir2, "audit.log"),
|
|
2799
2802
|
maxBytes: rotation.maxBytes,
|
|
2800
2803
|
maxFiles: rotation.maxFiles
|
|
2801
2804
|
})
|
|
@@ -2820,12 +2823,12 @@ function recordSecurityAudit(event, fields = {}) {
|
|
|
2820
2823
|
}
|
|
2821
2824
|
return record;
|
|
2822
2825
|
}
|
|
2823
|
-
var
|
|
2826
|
+
var import_node_fs7, import_node_path8, import_pino2, testSink, auditLogger;
|
|
2824
2827
|
var init_audit_log = __esm({
|
|
2825
2828
|
"packages/core/observability/audit-log.ts"() {
|
|
2826
2829
|
"use strict";
|
|
2827
|
-
|
|
2828
|
-
|
|
2830
|
+
import_node_fs7 = __toESM(require("node:fs"), 1);
|
|
2831
|
+
import_node_path8 = __toESM(require("node:path"), 1);
|
|
2829
2832
|
import_pino2 = __toESM(require("pino"), 1);
|
|
2830
2833
|
init_logger();
|
|
2831
2834
|
init_request_context();
|
|
@@ -3875,9 +3878,9 @@ var init_http_router = __esm({
|
|
|
3875
3878
|
/**
|
|
3876
3879
|
* 注册指定动词与路径的 Handler
|
|
3877
3880
|
*/
|
|
3878
|
-
route(method,
|
|
3881
|
+
route(method, path47, handler, options = {}) {
|
|
3879
3882
|
const upperMethod = method.toUpperCase();
|
|
3880
|
-
const normalized =
|
|
3883
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3881
3884
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3882
3885
|
this.warnIfUnscoped(upperMethod, normalized, options);
|
|
3883
3886
|
this.routes.push({
|
|
@@ -3890,20 +3893,20 @@ var init_http_router = __esm({
|
|
|
3890
3893
|
scope: options.scope
|
|
3891
3894
|
});
|
|
3892
3895
|
}
|
|
3893
|
-
get(
|
|
3894
|
-
this.route("GET",
|
|
3896
|
+
get(path47, handler, options) {
|
|
3897
|
+
this.route("GET", path47, handler, options);
|
|
3895
3898
|
}
|
|
3896
|
-
post(
|
|
3897
|
-
this.route("POST",
|
|
3899
|
+
post(path47, handler, options) {
|
|
3900
|
+
this.route("POST", path47, handler, options);
|
|
3898
3901
|
}
|
|
3899
|
-
put(
|
|
3900
|
-
this.route("PUT",
|
|
3902
|
+
put(path47, handler, options) {
|
|
3903
|
+
this.route("PUT", path47, handler, options);
|
|
3901
3904
|
}
|
|
3902
|
-
patch(
|
|
3903
|
-
this.route("PATCH",
|
|
3905
|
+
patch(path47, handler, options) {
|
|
3906
|
+
this.route("PATCH", path47, handler, options);
|
|
3904
3907
|
}
|
|
3905
|
-
delete(
|
|
3906
|
-
this.route("DELETE",
|
|
3908
|
+
delete(path47, handler, options) {
|
|
3909
|
+
this.route("DELETE", path47, handler, options);
|
|
3907
3910
|
}
|
|
3908
3911
|
warnIfUnscoped(method, pattern, options) {
|
|
3909
3912
|
if (options.scope) return;
|
|
@@ -3918,19 +3921,19 @@ var init_http_router = __esm({
|
|
|
3918
3921
|
}
|
|
3919
3922
|
stream(methodOrPath, pathOrHandler, maybeHandler) {
|
|
3920
3923
|
if (typeof pathOrHandler === "function") {
|
|
3921
|
-
const
|
|
3924
|
+
const path47 = methodOrPath;
|
|
3922
3925
|
const handler = pathOrHandler;
|
|
3923
|
-
this.registerStream("GET",
|
|
3924
|
-
this.registerStream("POST",
|
|
3926
|
+
this.registerStream("GET", path47, handler);
|
|
3927
|
+
this.registerStream("POST", path47, handler);
|
|
3925
3928
|
} else {
|
|
3926
3929
|
const method = methodOrPath.toUpperCase();
|
|
3927
|
-
const
|
|
3930
|
+
const path47 = pathOrHandler;
|
|
3928
3931
|
const handler = maybeHandler;
|
|
3929
|
-
this.registerStream(method,
|
|
3932
|
+
this.registerStream(method, path47, handler);
|
|
3930
3933
|
}
|
|
3931
3934
|
}
|
|
3932
|
-
registerStream(method,
|
|
3933
|
-
const normalized =
|
|
3935
|
+
registerStream(method, path47, handler) {
|
|
3936
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3934
3937
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3935
3938
|
this.routes.push({
|
|
3936
3939
|
method: method.toUpperCase(),
|
|
@@ -3944,9 +3947,9 @@ var init_http_router = __esm({
|
|
|
3944
3947
|
/**
|
|
3945
3948
|
* 检查指定动词与路径是否为流式路由
|
|
3946
3949
|
*/
|
|
3947
|
-
isStream(method,
|
|
3950
|
+
isStream(method, path47) {
|
|
3948
3951
|
const upperMethod = method.toUpperCase();
|
|
3949
|
-
const normalized =
|
|
3952
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3950
3953
|
for (const entry of this.routes) {
|
|
3951
3954
|
if (entry.method !== upperMethod) continue;
|
|
3952
3955
|
if (entry.regex.test(normalized)) {
|
|
@@ -3958,9 +3961,9 @@ var init_http_router = __esm({
|
|
|
3958
3961
|
/**
|
|
3959
3962
|
* 匹配路由并提取路由参数
|
|
3960
3963
|
*/
|
|
3961
|
-
match(method,
|
|
3964
|
+
match(method, path47) {
|
|
3962
3965
|
const upperMethod = method.toUpperCase();
|
|
3963
|
-
const normalized =
|
|
3966
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3964
3967
|
for (const entry of this.routes) {
|
|
3965
3968
|
if (entry.method !== upperMethod) continue;
|
|
3966
3969
|
const match = normalized.match(entry.regex);
|
|
@@ -4103,13 +4106,13 @@ function checkMethodPolicy(input) {
|
|
|
4103
4106
|
if (tokens.frontendApi && token === tokens.frontendApi) {
|
|
4104
4107
|
if (isReadOnlyMethod(method)) return null;
|
|
4105
4108
|
if (method === "request" && !Array.isArray(args)) return null;
|
|
4106
|
-
const
|
|
4107
|
-
const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) =>
|
|
4108
|
-
const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) =>
|
|
4109
|
-
const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(
|
|
4109
|
+
const path47 = extractPathArg(args);
|
|
4110
|
+
const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path47 === p || path47.startsWith(`${p}/`));
|
|
4111
|
+
const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path47.endsWith(s));
|
|
4112
|
+
const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path47);
|
|
4110
4113
|
if (isHighRisk || isIrreversible || isEntityDelete) {
|
|
4111
4114
|
if (hasCapability(caps, CAP_API_ADMIN)) return null;
|
|
4112
|
-
const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${
|
|
4115
|
+
const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${path47}'` : isEntityDelete ? `\u5B9E\u4F53\u7EA7\u5220\u9664 '${path47}'` : `\u5199\u5165\u9AD8\u5371\u8DEF\u5F84 '${path47 || "<\u672A\u63D0\u4F9B\u8DEF\u5F84>"}'`;
|
|
4113
4116
|
return `${why} \u9700\u8981 manifest \u58F0\u660E '${CAP_API_ADMIN}' \u80FD\u529B`;
|
|
4114
4117
|
}
|
|
4115
4118
|
return hasCapability(caps, CAP_API_WRITE) ? null : `\u5199\u64CD\u4F5C\uFF08${method}\uFF09\u9700\u8981 manifest \u58F0\u660E '${CAP_API_WRITE}' \u80FD\u529B\uFF08\u666E\u901A\u53EA\u8BFB\u8C03\u7528\u8BF7\u7528 get\uFF09`;
|
|
@@ -4609,6 +4612,53 @@ function wrapPointsDimensionRegistry(registry4, tracker, pluginId) {
|
|
|
4609
4612
|
})
|
|
4610
4613
|
};
|
|
4611
4614
|
}
|
|
4615
|
+
function wrapClassroomLifecycleService(service, tracker, pluginId) {
|
|
4616
|
+
return {
|
|
4617
|
+
getStage: createSafeFunction((lessonId) => service.getStage(lessonId)),
|
|
4618
|
+
transitionStage: createSafeFunction(
|
|
4619
|
+
(lessonId, toStage, actorId, classId) => service.transitionStage(lessonId, toStage, actorId, classId)
|
|
4620
|
+
),
|
|
4621
|
+
registerStageGuard: createSafeFunction((owner, guard) => {
|
|
4622
|
+
service.registerStageGuard(owner, guard);
|
|
4623
|
+
tracker.track(pluginId, {
|
|
4624
|
+
dispose: () => {
|
|
4625
|
+
service.unregisterStageGuard(owner);
|
|
4626
|
+
}
|
|
4627
|
+
});
|
|
4628
|
+
}),
|
|
4629
|
+
unregisterStageGuard: createSafeFunction((owner) => {
|
|
4630
|
+
service.unregisterStageGuard(owner);
|
|
4631
|
+
})
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
function wrapInteractionRuntimeService(service, tracker, pluginId) {
|
|
4635
|
+
return {
|
|
4636
|
+
registerActivityProvider: createSafeFunction((owner, descriptor) => {
|
|
4637
|
+
service.registerActivityProvider(owner, descriptor);
|
|
4638
|
+
const providerId = descriptor?.id;
|
|
4639
|
+
if (providerId) {
|
|
4640
|
+
tracker.track(pluginId, {
|
|
4641
|
+
dispose: () => {
|
|
4642
|
+
service.unregisterActivityProvider(owner, providerId);
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4646
|
+
}),
|
|
4647
|
+
unregisterActivityProvider: createSafeFunction((owner, id) => {
|
|
4648
|
+
service.unregisterActivityProvider(owner, id);
|
|
4649
|
+
}),
|
|
4650
|
+
listActivityProviders: createSafeFunction(() => service.listActivityProviders())
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId) {
|
|
4654
|
+
if (tokenName === IClassroomLifecycleServiceToken.name) {
|
|
4655
|
+
return wrapClassroomLifecycleService(raw, tracker, pluginId);
|
|
4656
|
+
}
|
|
4657
|
+
if (tokenName === IInteractionRuntimeServiceToken.name) {
|
|
4658
|
+
return wrapInteractionRuntimeService(raw, tracker, pluginId);
|
|
4659
|
+
}
|
|
4660
|
+
return raw;
|
|
4661
|
+
}
|
|
4612
4662
|
function wrapPointsLedgerService(service, pluginId, capabilities) {
|
|
4613
4663
|
const auditAccess = (studentId, method, operation, requiredCap) => {
|
|
4614
4664
|
const hasCap = hasCapability(capabilities ?? [], requiredCap);
|
|
@@ -4903,9 +4953,9 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4903
4953
|
`[PluginHost] Plugin "${manifest.id}" cannot resolve "${token}" by name. Use the Token object imported from @openlearn/plugin-sdk, or one of the SDK-exported Token names.`
|
|
4904
4954
|
);
|
|
4905
4955
|
}
|
|
4906
|
-
return serviceRegistry.resolveByName(token);
|
|
4956
|
+
return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId));
|
|
4907
4957
|
}
|
|
4908
|
-
return serviceRegistry.resolve(token);
|
|
4958
|
+
return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId));
|
|
4909
4959
|
},
|
|
4910
4960
|
// V3.2: 插件向 DI 容器提供服务(对应 manifest.provides)
|
|
4911
4961
|
provide: async (token, instance) => {
|
|
@@ -5714,7 +5764,7 @@ function spawnPluginChild(opts) {
|
|
|
5714
5764
|
});
|
|
5715
5765
|
const removeBootstrapFile = () => {
|
|
5716
5766
|
try {
|
|
5717
|
-
|
|
5767
|
+
import_node_fs9.default.unlinkSync(bootstrapFile);
|
|
5718
5768
|
} catch {
|
|
5719
5769
|
}
|
|
5720
5770
|
};
|
|
@@ -5740,25 +5790,25 @@ function isPidAlive(pid) {
|
|
|
5740
5790
|
}
|
|
5741
5791
|
}
|
|
5742
5792
|
function writeBootstrapTempFile(code) {
|
|
5743
|
-
const file =
|
|
5793
|
+
const file = import_node_path11.default.join(
|
|
5744
5794
|
import_node_os.default.tmpdir(),
|
|
5745
5795
|
`${BOOTSTRAP_PREFIX}${process.pid}-${import_node_crypto7.default.randomBytes(8).toString("hex")}${BOOTSTRAP_SUFFIX}`
|
|
5746
5796
|
);
|
|
5747
|
-
|
|
5797
|
+
import_node_fs9.default.writeFileSync(file, code, { encoding: "utf8", mode: 384 });
|
|
5748
5798
|
return file;
|
|
5749
5799
|
}
|
|
5750
5800
|
function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
5751
5801
|
try {
|
|
5752
5802
|
const tmpDir = import_node_os.default.tmpdir();
|
|
5753
|
-
const files =
|
|
5803
|
+
const files = import_node_fs9.default.readdirSync(tmpDir);
|
|
5754
5804
|
let cleaned = 0;
|
|
5755
5805
|
for (const file of files) {
|
|
5756
5806
|
if (!file.startsWith(BOOTSTRAP_PREFIX) || !file.endsWith(BOOTSTRAP_SUFFIX)) continue;
|
|
5757
|
-
const full =
|
|
5807
|
+
const full = import_node_path11.default.join(tmpDir, file);
|
|
5758
5808
|
const ownerPid = bootstrapOwnerPid(file);
|
|
5759
5809
|
if (ownerPid === null) {
|
|
5760
5810
|
try {
|
|
5761
|
-
if (now -
|
|
5811
|
+
if (now - import_node_fs9.default.statSync(full).mtimeMs < LEGACY_BOOTSTRAP_STALE_MS) continue;
|
|
5762
5812
|
} catch {
|
|
5763
5813
|
continue;
|
|
5764
5814
|
}
|
|
@@ -5766,7 +5816,7 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
|
5766
5816
|
continue;
|
|
5767
5817
|
}
|
|
5768
5818
|
try {
|
|
5769
|
-
|
|
5819
|
+
import_node_fs9.default.unlinkSync(full);
|
|
5770
5820
|
cleaned++;
|
|
5771
5821
|
} catch {
|
|
5772
5822
|
}
|
|
@@ -5779,9 +5829,9 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
|
5779
5829
|
function defaultNetPreloadStartDirs() {
|
|
5780
5830
|
const dirs = [];
|
|
5781
5831
|
try {
|
|
5782
|
-
const url =
|
|
5832
|
+
const url = import_meta6.url;
|
|
5783
5833
|
if (typeof url === "string" && url.startsWith("file:")) {
|
|
5784
|
-
dirs.push(
|
|
5834
|
+
dirs.push(import_node_path11.default.dirname((0, import_node_url3.fileURLToPath)(url)));
|
|
5785
5835
|
}
|
|
5786
5836
|
} catch {
|
|
5787
5837
|
}
|
|
@@ -5793,33 +5843,33 @@ function defaultNetPreloadStartDirs() {
|
|
|
5793
5843
|
function resolveNetPreloadPath(options = {}) {
|
|
5794
5844
|
const cwd = options.cwd ?? process.cwd();
|
|
5795
5845
|
const candidates = [
|
|
5796
|
-
|
|
5797
|
-
|
|
5846
|
+
import_node_path11.default.resolve(cwd, "packages/core/worker-runtime/plugin-net-preload.cjs"),
|
|
5847
|
+
import_node_path11.default.resolve(cwd, "dist/plugin-net-preload.cjs")
|
|
5798
5848
|
];
|
|
5799
5849
|
for (const dir of options.startDirs ?? defaultNetPreloadStartDirs()) {
|
|
5800
|
-
candidates.unshift(
|
|
5850
|
+
candidates.unshift(import_node_path11.default.join(dir, "plugin-net-preload.cjs"));
|
|
5801
5851
|
}
|
|
5802
|
-
const found = candidates.find((candidate) =>
|
|
5852
|
+
const found = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
|
|
5803
5853
|
if (!found) {
|
|
5804
5854
|
throw new Error(`[PluginNet] preload script not found. Looked in: ${candidates.join(", ")}`);
|
|
5805
5855
|
}
|
|
5806
5856
|
return found;
|
|
5807
5857
|
}
|
|
5808
|
-
var import_node_child_process, import_node_crypto7,
|
|
5858
|
+
var import_node_child_process, import_node_crypto7, import_node_fs9, import_node_os, import_node_path11, import_node_url3, import_meta6, ENV_ALLOWLIST, ENV_DENYLIST, WORKER_DATA_ENV, BOOTSTRAP_PREFIX, BOOTSTRAP_SUFFIX, LEGACY_BOOTSTRAP_STALE_MS;
|
|
5809
5859
|
var init_child_spawn = __esm({
|
|
5810
5860
|
"packages/core/worker-runtime/child-spawn.ts"() {
|
|
5811
5861
|
"use strict";
|
|
5812
5862
|
import_node_child_process = require("node:child_process");
|
|
5813
5863
|
import_node_crypto7 = __toESM(require("node:crypto"), 1);
|
|
5814
|
-
|
|
5864
|
+
import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
5815
5865
|
import_node_os = __toESM(require("node:os"), 1);
|
|
5816
|
-
|
|
5866
|
+
import_node_path11 = __toESM(require("node:path"), 1);
|
|
5817
5867
|
import_node_url3 = require("node:url");
|
|
5818
5868
|
init_child_transport();
|
|
5819
5869
|
init_child_isolate();
|
|
5820
5870
|
init_plugin_permission();
|
|
5821
5871
|
init_plugin_limits();
|
|
5822
|
-
|
|
5872
|
+
import_meta6 = {};
|
|
5823
5873
|
ENV_ALLOWLIST = [
|
|
5824
5874
|
"PATH",
|
|
5825
5875
|
"LANG",
|
|
@@ -5869,12 +5919,12 @@ function quotaExceeded(input) {
|
|
|
5869
5919
|
}
|
|
5870
5920
|
function readProcSample(pid) {
|
|
5871
5921
|
try {
|
|
5872
|
-
const stat =
|
|
5922
|
+
const stat = import_node_fs10.default.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
5873
5923
|
const rest = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
5874
5924
|
const utime = Number(rest[11]);
|
|
5875
5925
|
const stime = Number(rest[12]);
|
|
5876
5926
|
if (!Number.isFinite(utime) || !Number.isFinite(stime)) return null;
|
|
5877
|
-
const status =
|
|
5927
|
+
const status = import_node_fs10.default.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
5878
5928
|
const rss = /^VmRSS:\s+(\d+)\s+kB/m.exec(status);
|
|
5879
5929
|
return { cpuMs: (utime + stime) / 100 * 1e3, rssKb: rss ? Number(rss[1]) : 0 };
|
|
5880
5930
|
} catch {
|
|
@@ -5917,11 +5967,11 @@ function startProcessQuota(pid, onBreach, env = process.env) {
|
|
|
5917
5967
|
}
|
|
5918
5968
|
};
|
|
5919
5969
|
}
|
|
5920
|
-
var
|
|
5970
|
+
var import_node_fs10, DEFAULT_MAX_RSS_MB, DEFAULT_CPU_QUOTA_MS, DEFAULT_CPU_WINDOW_MS;
|
|
5921
5971
|
var init_plugin_quota = __esm({
|
|
5922
5972
|
"packages/core/worker-runtime/plugin-quota.ts"() {
|
|
5923
5973
|
"use strict";
|
|
5924
|
-
|
|
5974
|
+
import_node_fs10 = __toESM(require("node:fs"), 1);
|
|
5925
5975
|
DEFAULT_MAX_RSS_MB = 512;
|
|
5926
5976
|
DEFAULT_CPU_QUOTA_MS = 8e3;
|
|
5927
5977
|
DEFAULT_CPU_WINDOW_MS = 1e4;
|
|
@@ -8923,9 +8973,9 @@ __export(worker_manager_exports, {
|
|
|
8923
8973
|
function collectSharedReadPaths(pluginDir) {
|
|
8924
8974
|
if (!pluginDir) return [];
|
|
8925
8975
|
const paths = /* @__PURE__ */ new Set();
|
|
8926
|
-
const nodeModulesDir =
|
|
8976
|
+
const nodeModulesDir = import_node_path12.default.join(import_node_path12.default.dirname(pluginDir), "node_modules");
|
|
8927
8977
|
paths.add(nodeModulesDir);
|
|
8928
|
-
const sdkLink =
|
|
8978
|
+
const sdkLink = import_node_path12.default.join(nodeModulesDir, "@openlearn", "plugin-sdk");
|
|
8929
8979
|
try {
|
|
8930
8980
|
if (import_fs7.default.existsSync(sdkLink)) paths.add(import_fs7.default.realpathSync(sdkLink));
|
|
8931
8981
|
} catch {
|
|
@@ -8964,7 +9014,7 @@ function generateBootstrapCode() {
|
|
|
8964
9014
|
const rootPath = process.cwd().replace(/\\/g, "/");
|
|
8965
9015
|
return composeBootstrapCode(`${rootPath}/package.json`);
|
|
8966
9016
|
}
|
|
8967
|
-
var import_node_worker_threads, import_fs7,
|
|
9017
|
+
var import_node_worker_threads, import_fs7, import_node_path12, import_uuid12, BASE_WORKER_SERVICE_TOKENS, ALL_SERVICE_TOKENS, POINTS_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
|
|
8968
9018
|
var init_worker_manager = __esm({
|
|
8969
9019
|
"packages/core/worker-runtime/worker-manager.ts"() {
|
|
8970
9020
|
"use strict";
|
|
@@ -8978,7 +9028,7 @@ var init_worker_manager = __esm({
|
|
|
8978
9028
|
init_plugin_limits();
|
|
8979
9029
|
init_thread_isolate();
|
|
8980
9030
|
import_fs7 = __toESM(require("fs"), 1);
|
|
8981
|
-
|
|
9031
|
+
import_node_path12 = __toESM(require("node:path"), 1);
|
|
8982
9032
|
init_transport();
|
|
8983
9033
|
init_plugin_net_proxy();
|
|
8984
9034
|
init_service_host();
|
|
@@ -9383,7 +9433,7 @@ var init_worker_manager = __esm({
|
|
|
9383
9433
|
const encodedBootstrap = Buffer.from(bootstrapCode, "utf-8").toString("base64");
|
|
9384
9434
|
const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
|
|
9385
9435
|
const allowedTokens = computeAllowedWorkerTokens(manifest, serviceTokens);
|
|
9386
|
-
const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(
|
|
9436
|
+
const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(import_node_path12.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
|
|
9387
9437
|
let approvedNetHosts = [];
|
|
9388
9438
|
try {
|
|
9389
9439
|
approvedNetHosts = listApprovedNetHosts(this.db, [pluginId, manifest.id]);
|
|
@@ -9755,6 +9805,193 @@ __export(server_exports, {
|
|
|
9755
9805
|
startServer: () => startServer
|
|
9756
9806
|
});
|
|
9757
9807
|
module.exports = __toCommonJS(server_exports);
|
|
9808
|
+
|
|
9809
|
+
// server/security/runtime-check.ts
|
|
9810
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
9811
|
+
var import_node_module = require("node:module");
|
|
9812
|
+
|
|
9813
|
+
// scripts/assert-node-runtime.mjs
|
|
9814
|
+
var SUPPORTED_NODE_MAJORS = Object.freeze([20, 22, 24]);
|
|
9815
|
+
var SUPPORTED_NODE_TEXT = "Node.js 20.x / 22.x / 24.x";
|
|
9816
|
+
var PINNED_NODE_MAJOR = 22;
|
|
9817
|
+
function parseNodeMajor(version) {
|
|
9818
|
+
if (typeof version !== "string") return null;
|
|
9819
|
+
const matched = /^v?(\d+)\./.exec(version.trim());
|
|
9820
|
+
if (!matched) return null;
|
|
9821
|
+
const major = Number(matched[1]);
|
|
9822
|
+
return Number.isFinite(major) ? major : null;
|
|
9823
|
+
}
|
|
9824
|
+
function checkNodeVersion(version, supported = SUPPORTED_NODE_MAJORS) {
|
|
9825
|
+
const major = parseNodeMajor(version);
|
|
9826
|
+
if (major === null) {
|
|
9827
|
+
return {
|
|
9828
|
+
ok: false,
|
|
9829
|
+
code: "unparseable",
|
|
9830
|
+
major: null,
|
|
9831
|
+
message: `\u65E0\u6CD5\u89E3\u6790 Node \u7248\u672C\u300C${version}\u300D\u3002`
|
|
9832
|
+
};
|
|
9833
|
+
}
|
|
9834
|
+
if (supported.includes(major)) {
|
|
9835
|
+
return { ok: true, code: "supported", major, message: "" };
|
|
9836
|
+
}
|
|
9837
|
+
return {
|
|
9838
|
+
ok: false,
|
|
9839
|
+
code: "unsupported",
|
|
9840
|
+
major,
|
|
9841
|
+
message: `\u5F53\u524D Node \u7248\u672C v${major}.x \u4E0D\u53D7\u652F\u6301\uFF08\u652F\u6301\u8303\u56F4\uFF1A${SUPPORTED_NODE_TEXT}\uFF09\u3002\u672C\u9879\u76EE\u7684\u539F\u751F\u4F9D\u8D56\uFF08better-sqlite3\uFF09\u6309 Node ABI \u7F16\u8BD1\uFF0C\u590D\u7528\u65E7 node_modules \u4F1A\u5728\u8FD0\u884C\u671F\u62A5 ERR_DLOPEN_FAILED / NODE_MODULE_VERSION \u4E0D\u4E00\u81F4\u3002`
|
|
9842
|
+
};
|
|
9843
|
+
}
|
|
9844
|
+
function buildFixGuidance(major) {
|
|
9845
|
+
const pinned = PINNED_NODE_MAJOR;
|
|
9846
|
+
const lines = [
|
|
9847
|
+
"\u600E\u4E48\u4FEE\uFF08\u4EFB\u9009\u5176\u4E00\uFF09\uFF1A",
|
|
9848
|
+
` 1) \u5207\u5230\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08\u672C\u9879\u76EE\u5B9E\u6D4B ${SUPPORTED_NODE_MAJORS.join(" / ")}\uFF09\uFF1A`,
|
|
9849
|
+
` nvm install ${pinned} && nvm use ${pinned}`,
|
|
9850
|
+
` fnm install ${pinned} && fnm use ${pinned}`,
|
|
9851
|
+
` asdf install nodejs ${pinned}.16.0 && asdf local nodejs ${pinned}.16.0`,
|
|
9852
|
+
" \u4ED3\u5E93\u6839\u7684 .nvmrc / .node-version \u5DF2\u9489\u4F4F " + pinned + "\uFF0Cnvm / fnm \u4F1A\u81EA\u52A8\u91C7\u7528\u3002",
|
|
9853
|
+
" 2) \u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5185\u542B Node " + pinned + "\uFF0C\u5BBF\u4E3B\u673A\u65E0\u9700\u88C5 Node\uFF09\uFF1A",
|
|
9854
|
+
" docker compose up -d",
|
|
9855
|
+
" 3) \u786E\u5B9E\u8981\u7528\u522B\u7684 Node \u7248\u672C\uFF1A\u5FC5\u987B\u8BA9\u539F\u751F\u6A21\u5757\u6309\u65B0 ABI \u91CD\u65B0\u7F16\u8BD1 \u2014\u2014",
|
|
9856
|
+
" \u4FDD\u7559\u65E7 node_modules \u53EA\u4F1A\u7EE7\u7EED\u590D\u7528 ABI \u4E0D\u5339\u914D\u7684\u4E8C\u8FDB\u5236\uFF1A",
|
|
9857
|
+
" rm -rf node_modules && pnpm install"
|
|
9858
|
+
];
|
|
9859
|
+
if (major !== null && major === 21) {
|
|
9860
|
+
lines.push(
|
|
9861
|
+
" \u6CE8\uFF1ANode 21 \u4E0D\u662F LTS\uFF0C\u4E14 better-sqlite3 \u5B98\u65B9\u4E0D\u63D0\u4F9B 21.x \u9884\u7F16\u8BD1\u4EA7\u7269\uFF0C",
|
|
9862
|
+
" \u91CD\u88C5\u540E\u4ECD\u9700\u8981\u672C\u5730\u7F16\u8BD1\u5DE5\u5177\u94FE\uFF08python3 + make + g++\uFF09\uFF0C\u4E0D\u5EFA\u8BAE\u7528\u4E8E\u751F\u4EA7\u3002"
|
|
9863
|
+
);
|
|
9864
|
+
}
|
|
9865
|
+
return lines.join("\n");
|
|
9866
|
+
}
|
|
9867
|
+
function formatUnsupportedMessage(version, check = checkNodeVersion(version)) {
|
|
9868
|
+
if (check.ok) return "";
|
|
9869
|
+
const head = `[NodeRuntime] FATAL: ${check.message}`;
|
|
9870
|
+
return `${head}
|
|
9871
|
+
|
|
9872
|
+
${buildFixGuidance(check.major)}
|
|
9873
|
+
`;
|
|
9874
|
+
}
|
|
9875
|
+
function runCliGuard() {
|
|
9876
|
+
const result = checkNodeVersion(process.version);
|
|
9877
|
+
if (result.ok) return;
|
|
9878
|
+
console.error(formatUnsupportedMessage(process.version, result));
|
|
9879
|
+
process.exit(1);
|
|
9880
|
+
}
|
|
9881
|
+
var invokedDirectly = process.argv[1] && (process.argv[1].endsWith("assert-node-runtime.mjs") || process.argv[1].endsWith("assert-node-runtime") || process.argv[1].includes("assert-node-runtime"));
|
|
9882
|
+
if (invokedDirectly) {
|
|
9883
|
+
runCliGuard();
|
|
9884
|
+
}
|
|
9885
|
+
|
|
9886
|
+
// server/security/runtime-check.ts
|
|
9887
|
+
var import_meta = {};
|
|
9888
|
+
var ABI_PATTERN = /NODE_MODULE_VERSION\s+(\d+)/i;
|
|
9889
|
+
var MISSING_BINDINGS_MARKERS = ["Could not locate the bindings file", "bindings file"];
|
|
9890
|
+
function detectNativeModuleFailure(error) {
|
|
9891
|
+
const runtimeAbi = Number(process.versions.modules);
|
|
9892
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9893
|
+
const abiMatch = ABI_PATTERN.exec(message);
|
|
9894
|
+
if (abiMatch) {
|
|
9895
|
+
return {
|
|
9896
|
+
kind: "abi-mismatch",
|
|
9897
|
+
// better-sqlite3 的报错里第一个 NODE_MODULE_VERSION 是「模块编译时的 ABI」,
|
|
9898
|
+
// 第二个是「当前运行时要求的 ABI」。
|
|
9899
|
+
moduleAbi: Number(abiMatch[1]),
|
|
9900
|
+
runtimeAbi,
|
|
9901
|
+
detail: message
|
|
9902
|
+
};
|
|
9903
|
+
}
|
|
9904
|
+
if (MISSING_BINDINGS_MARKERS.some((marker) => message.includes(marker))) {
|
|
9905
|
+
return { kind: "missing-bindings", moduleAbi: null, runtimeAbi, detail: message };
|
|
9906
|
+
}
|
|
9907
|
+
return { kind: "unknown", moduleAbi: null, runtimeAbi, detail: message };
|
|
9908
|
+
}
|
|
9909
|
+
function resolveRequire() {
|
|
9910
|
+
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
9911
|
+
return (0, import_node_module.createRequire)(import_meta.url);
|
|
9912
|
+
}
|
|
9913
|
+
if (typeof __filename !== "undefined") {
|
|
9914
|
+
return (0, import_node_module.createRequire)(__filename);
|
|
9915
|
+
}
|
|
9916
|
+
return (0, import_node_module.createRequire)(import_node_path.default.join(process.cwd(), "package.json"));
|
|
9917
|
+
}
|
|
9918
|
+
function loadBetterSqlite3Binding(loadModule) {
|
|
9919
|
+
const load = loadModule ?? resolveRequire();
|
|
9920
|
+
const Database3 = load("better-sqlite3");
|
|
9921
|
+
const probe = new Database3(":memory:");
|
|
9922
|
+
probe.close();
|
|
9923
|
+
}
|
|
9924
|
+
function checkNativeModule(load = loadBetterSqlite3Binding) {
|
|
9925
|
+
try {
|
|
9926
|
+
load();
|
|
9927
|
+
return null;
|
|
9928
|
+
} catch (error) {
|
|
9929
|
+
const failure = detectNativeModuleFailure(error);
|
|
9930
|
+
if (failure.kind === "abi-mismatch") {
|
|
9931
|
+
return {
|
|
9932
|
+
key: "native-module-abi",
|
|
9933
|
+
severity: "error",
|
|
9934
|
+
message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u4E0E\u5F53\u524D Node \u8FD0\u884C\u65F6\u4E0D\u517C\u5BB9\uFF1A\u4E8C\u8FDB\u5236\u662F\u6309 ABI ${failure.moduleAbi} \u7F16\u8BD1\u7684\uFF0C\u5F53\u524D\u8FD0\u884C\u65F6\uFF08Node ${process.versions.node}\uFF09\u8981\u6C42 ABI ${failure.runtimeAbi}\u3002
|
|
9935
|
+
\u5E38\u89C1\u539F\u56E0\uFF1Anode_modules \u662F\u5728\u53E6\u4E00\u4E2A Node \u7248\u672C\u4E0B\u5B89\u88C5/\u62F7\u8D1D\u7684\u3002
|
|
9936
|
+
\u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08\u6309\u5F53\u524D Node \u91CD\u65B0\u7F16\u8BD1\u539F\u751F\u6A21\u5757\uFF09\uFF0C
|
|
9937
|
+
\u6216\u5207\u56DE\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08${SUPPORTED_NODE_TEXT}\uFF09\u3002`
|
|
9938
|
+
};
|
|
9939
|
+
}
|
|
9940
|
+
if (failure.kind === "missing-bindings") {
|
|
9941
|
+
return {
|
|
9942
|
+
key: "native-module-missing",
|
|
9943
|
+
severity: "error",
|
|
9944
|
+
message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u7684\u4E8C\u8FDB\u5236\u4E0D\u5B58\u5728\uFF08\u672A\u7F16\u8BD1\u6216\u4F9D\u8D56\u88AB\u88C1\u526A\uFF09\u3002
|
|
9945
|
+
\u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08pnpm \u9700\u8981 allowBuilds \u653E\u884C better-sqlite3 \u7684\u6784\u5EFA\u811A\u672C\uFF09\uFF0C
|
|
9946
|
+
\u6216\u6539\u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5DF2\u5185\u7F6E\u7F16\u8BD1\u597D\u7684\u4E8C\u8FDB\u5236\uFF09\u3002`
|
|
9947
|
+
};
|
|
9948
|
+
}
|
|
9949
|
+
return {
|
|
9950
|
+
key: "native-module-load",
|
|
9951
|
+
severity: "error",
|
|
9952
|
+
message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u52A0\u8F7D\u5931\u8D25\uFF1A${failure.detail}`
|
|
9953
|
+
};
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
function checkRuntimeEnvironment(input = {}) {
|
|
9957
|
+
const errors = [];
|
|
9958
|
+
const warnings = [];
|
|
9959
|
+
const nodeVersion = input.nodeVersion ?? process.version;
|
|
9960
|
+
const nodeCheck = checkNodeVersion(nodeVersion);
|
|
9961
|
+
if (!nodeCheck.ok) {
|
|
9962
|
+
errors.push({
|
|
9963
|
+
key: "node-version",
|
|
9964
|
+
severity: "error",
|
|
9965
|
+
message: `${nodeCheck.message}
|
|
9966
|
+
${buildFixGuidance(nodeCheck.major)}`
|
|
9967
|
+
});
|
|
9968
|
+
return { errors, warnings };
|
|
9969
|
+
}
|
|
9970
|
+
const nativeIssue = checkNativeModule(input.loadNativeModule);
|
|
9971
|
+
if (nativeIssue) {
|
|
9972
|
+
errors.push(nativeIssue);
|
|
9973
|
+
}
|
|
9974
|
+
return { errors, warnings };
|
|
9975
|
+
}
|
|
9976
|
+
function assertRuntimeEnvironment() {
|
|
9977
|
+
const { errors, warnings } = checkRuntimeEnvironment();
|
|
9978
|
+
for (const warning of warnings) {
|
|
9979
|
+
console.warn(`[NodeRuntime] WARN: ${warning.message}`);
|
|
9980
|
+
}
|
|
9981
|
+
if (errors.length === 0) return;
|
|
9982
|
+
console.error("[NodeRuntime] FATAL: \u8FD0\u884C\u65F6\u73AF\u5883\u4E0D\u6EE1\u8DB3\u8981\u6C42\uFF0C\u62D2\u7EDD\u542F\u52A8\u3002");
|
|
9983
|
+
for (const issue of errors) {
|
|
9984
|
+
console.error(`
|
|
9985
|
+
[NodeRuntime] ${issue.key}
|
|
9986
|
+
${issue.message}`);
|
|
9987
|
+
}
|
|
9988
|
+
process.exit(1);
|
|
9989
|
+
}
|
|
9990
|
+
|
|
9991
|
+
// server/security/runtime-preflight.ts
|
|
9992
|
+
assertRuntimeEnvironment();
|
|
9993
|
+
|
|
9994
|
+
// server.ts
|
|
9758
9995
|
var import_express4 = __toESM(require("express"), 1);
|
|
9759
9996
|
var import_path23 = __toESM(require("path"), 1);
|
|
9760
9997
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
@@ -10786,12 +11023,12 @@ var import_url = require("url");
|
|
|
10786
11023
|
|
|
10787
11024
|
// packages/core/db/backup-manager.ts
|
|
10788
11025
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
10789
|
-
var
|
|
11026
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
10790
11027
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
10791
11028
|
|
|
10792
11029
|
// packages/core/db/secure-files.ts
|
|
10793
11030
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
10794
|
-
var
|
|
11031
|
+
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
10795
11032
|
var SECURE_FILE_MODE = 384;
|
|
10796
11033
|
var SECURE_DIR_MODE = 448;
|
|
10797
11034
|
var GROUP_OTHER_BITS = 63;
|
|
@@ -10835,7 +11072,7 @@ function ensureSecureFile(filePath) {
|
|
|
10835
11072
|
hardenFile(filePath);
|
|
10836
11073
|
return;
|
|
10837
11074
|
}
|
|
10838
|
-
import_node_fs.default.mkdirSync(
|
|
11075
|
+
import_node_fs.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true });
|
|
10839
11076
|
const fd = import_node_fs.default.openSync(filePath, "a", SECURE_FILE_MODE);
|
|
10840
11077
|
import_node_fs.default.closeSync(fd);
|
|
10841
11078
|
} catch {
|
|
@@ -10853,15 +11090,15 @@ function secureDatabaseFiles(dbPath2) {
|
|
|
10853
11090
|
// packages/core/db/backup-manager.ts
|
|
10854
11091
|
function getDefaultDbPath() {
|
|
10855
11092
|
if (process.env.OPENLEARN_DB_PATH) {
|
|
10856
|
-
return
|
|
11093
|
+
return import_node_path3.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
|
|
10857
11094
|
}
|
|
10858
|
-
return
|
|
11095
|
+
return import_node_path3.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
|
|
10859
11096
|
}
|
|
10860
11097
|
function getDefaultBackupDir() {
|
|
10861
11098
|
if (process.env.BACKUP_DIR) {
|
|
10862
|
-
return
|
|
11099
|
+
return import_node_path3.default.resolve(process.cwd(), process.env.BACKUP_DIR);
|
|
10863
11100
|
}
|
|
10864
|
-
return
|
|
11101
|
+
return import_node_path3.default.resolve(process.cwd(), "backups");
|
|
10865
11102
|
}
|
|
10866
11103
|
function validateBackup(filePath) {
|
|
10867
11104
|
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
@@ -10915,7 +11152,7 @@ function pruneByPrefix(backupDir, prefix, keepCount) {
|
|
|
10915
11152
|
let pruned = 0;
|
|
10916
11153
|
for (const file of files.slice(keepCount)) {
|
|
10917
11154
|
try {
|
|
10918
|
-
import_node_fs2.default.unlinkSync(
|
|
11155
|
+
import_node_fs2.default.unlinkSync(import_node_path3.default.join(backupDir, file));
|
|
10919
11156
|
pruned++;
|
|
10920
11157
|
} catch {
|
|
10921
11158
|
}
|
|
@@ -10934,15 +11171,15 @@ function pruneMigratorSnapshots(backupDir = getDefaultBackupDir(), keepCount) {
|
|
|
10934
11171
|
return pruneByPrefix(backupDir, "pre_migration_", Math.max(1, keepCount ?? resolvePreMigrationKeep()));
|
|
10935
11172
|
}
|
|
10936
11173
|
function performBackup(options = {}) {
|
|
10937
|
-
const dbPath2 = options.dbPath ?
|
|
10938
|
-
const backupDir = options.backupDir ?
|
|
11174
|
+
const dbPath2 = options.dbPath ? import_node_path3.default.resolve(options.dbPath) : getDefaultDbPath();
|
|
11175
|
+
const backupDir = options.backupDir ? import_node_path3.default.resolve(options.backupDir) : getDefaultBackupDir();
|
|
10939
11176
|
const keep = typeof options.keep === "number" ? options.keep : 10;
|
|
10940
11177
|
if (!import_node_fs2.default.existsSync(backupDir)) {
|
|
10941
11178
|
ensureSecureDir(backupDir);
|
|
10942
11179
|
}
|
|
10943
11180
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
10944
11181
|
const filename = options.customName || `backup_${timestamp}.db`;
|
|
10945
|
-
const backupPath =
|
|
11182
|
+
const backupPath = import_node_path3.default.join(backupDir, filename);
|
|
10946
11183
|
if (import_node_fs2.default.existsSync(backupPath)) {
|
|
10947
11184
|
import_node_fs2.default.unlinkSync(backupPath);
|
|
10948
11185
|
}
|
|
@@ -10979,7 +11216,7 @@ function performBackup(options = {}) {
|
|
|
10979
11216
|
}
|
|
10980
11217
|
|
|
10981
11218
|
// packages/core/db/migrator.ts
|
|
10982
|
-
var
|
|
11219
|
+
var import_meta2 = {};
|
|
10983
11220
|
function splitSqlStatements(sql) {
|
|
10984
11221
|
return sql.split(";").map((s) => s.trim()).filter((s) => {
|
|
10985
11222
|
const content = s.replace(/--.*$/gm, "").trim();
|
|
@@ -11027,7 +11264,7 @@ function simpleChecksum(text) {
|
|
|
11027
11264
|
}
|
|
11028
11265
|
return hash.toString(16);
|
|
11029
11266
|
}
|
|
11030
|
-
function runMigrations(db2, migrations) {
|
|
11267
|
+
function runMigrations(db2, migrations, options = {}) {
|
|
11031
11268
|
db2.exec(`
|
|
11032
11269
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
11033
11270
|
name TEXT PRIMARY KEY,
|
|
@@ -11038,6 +11275,23 @@ function runMigrations(db2, migrations) {
|
|
|
11038
11275
|
const appliedRows = db2.prepare("SELECT name, checksum FROM _migrations").all();
|
|
11039
11276
|
const applied = new Map(appliedRows.map((r) => [r.name, r.checksum]));
|
|
11040
11277
|
const byName = new Map(migrations.map((m) => [m.name, m]));
|
|
11278
|
+
const unknown = [...applied.keys()].filter((name) => !byName.has(name));
|
|
11279
|
+
if (unknown.length > 0) {
|
|
11280
|
+
const detail = unknown.map((n) => ` - ${n}`).join("\n");
|
|
11281
|
+
const msg = `[Migration] \u53F0\u8D26\u91CC\u6709 ${unknown.length} \u4E2A\u672C\u6B21\u8FC1\u79FB\u5217\u8868\u672A\u5305\u542B\u7684\u6761\u76EE\uFF1A
|
|
11282
|
+
${detail}
|
|
11283
|
+
` + (options.strictLedger ? ` \u542F\u52A8\u94FE\u8DEF\u5224\u5B9A\u4E3A FATAL\uFF1A
|
|
11284
|
+
\u6210\u56E0\u4E00\uFF1A\u7528\u66F4\u65B0\u7248\u672C\u7684\u5907\u4EFD\u6062\u590D\u4E86\u65E7\u4EE3\u7801\uFF08\u4EE3\u7801\u964D\u7EA7\uFF09\u2014\u2014\u8BF7\u5148\u5347\u7EA7\u4EE3\u7801\u518D\u542F\u52A8\uFF1B
|
|
11285
|
+
\u6210\u56E0\u4E8C\uFF1A\u5DF2\u5E94\u7528\u7684\u8FC1\u79FB\u6587\u4EF6\u88AB\u6539\u540D\u6216\u5220\u9664\u2014\u2014\u8FC1\u79FB\u4E00\u65E6\u5E94\u7528\u4E0D\u53EF\u6539\u540D\uFF0C\u8BF7\u6062\u590D\u6587\u4EF6\u6216\u8865\u4E00\u6761\u65B0\u8FC1\u79FB\u3002
|
|
11286
|
+
\u786E\u5C5E\u6709\u610F\u7684\u964D\u7EA7\u8FD0\u7EF4\uFF1A\u8BBE\u7F6E MIGRATION_ALLOW_UNKNOWN_LEDGER=true \u653E\u884C\uFF08\u4E0D\u63A8\u8350\uFF09\u3002` : ` \uFF08\u975E\u4E25\u683C\u53E3\u5F84\uFF1A\u8FC1\u79FB\u5B50\u96C6\u8C03\u7528\u5C5E\u5408\u6CD5\u7528\u6CD5\uFF0C\u4EC5\u63D0\u793A\uFF09`);
|
|
11287
|
+
if (process.env.MIGRATION_ALLOW_UNKNOWN_LEDGER === "true") {
|
|
11288
|
+
console.warn("[Migration] WARNING: \u672A\u77E5\u53F0\u8D26\u6761\u76EE\u5DF2\u6309 MIGRATION_ALLOW_UNKNOWN_LEDGER \u653E\u884C\uFF1A\n" + msg);
|
|
11289
|
+
} else if (options.strictLedger) {
|
|
11290
|
+
throw new Error(msg);
|
|
11291
|
+
} else {
|
|
11292
|
+
console.warn("[Migration] " + msg);
|
|
11293
|
+
}
|
|
11294
|
+
}
|
|
11041
11295
|
const drift = [];
|
|
11042
11296
|
for (const [name, stored] of applied) {
|
|
11043
11297
|
const migration = byName.get(name);
|
|
@@ -11118,8 +11372,8 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11118
11372
|
const starts = options.startDirs ? [...options.startDirs] : [];
|
|
11119
11373
|
if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
|
|
11120
11374
|
try {
|
|
11121
|
-
if (!options.startDirs && typeof
|
|
11122
|
-
starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(
|
|
11375
|
+
if (!options.startDirs && typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
11376
|
+
starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta2.url)));
|
|
11123
11377
|
}
|
|
11124
11378
|
} catch {
|
|
11125
11379
|
}
|
|
@@ -11145,6 +11399,68 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11145
11399
|
return import_path.default.resolve(cwd, "migrations");
|
|
11146
11400
|
}
|
|
11147
11401
|
|
|
11402
|
+
// packages/core/db/schema-golden.ts
|
|
11403
|
+
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
11404
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
11405
|
+
var SCHEMA_GOLDEN_FILENAME = "schema-golden.json";
|
|
11406
|
+
function extractSchemaSnapshot(db2) {
|
|
11407
|
+
const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
|
|
11408
|
+
const snapshot = {};
|
|
11409
|
+
for (const { name } of tables) {
|
|
11410
|
+
const columns = db2.prepare(`PRAGMA table_info(${JSON.stringify(name)})`).all().map(
|
|
11411
|
+
(c) => c.name
|
|
11412
|
+
);
|
|
11413
|
+
snapshot[name] = columns.sort();
|
|
11414
|
+
}
|
|
11415
|
+
return snapshot;
|
|
11416
|
+
}
|
|
11417
|
+
function verifySchemaAgainstGolden(db2, golden) {
|
|
11418
|
+
const actual = extractSchemaSnapshot(db2);
|
|
11419
|
+
const drifts = [];
|
|
11420
|
+
for (const [table, expectedColumns] of Object.entries(golden.tables)) {
|
|
11421
|
+
const actualColumns = actual[table];
|
|
11422
|
+
if (!actualColumns) {
|
|
11423
|
+
drifts.push({ table, missingColumns: expectedColumns, tableMissing: true });
|
|
11424
|
+
continue;
|
|
11425
|
+
}
|
|
11426
|
+
const actualSet = new Set(actualColumns);
|
|
11427
|
+
const missing = expectedColumns.filter((c) => !actualSet.has(c));
|
|
11428
|
+
if (missing.length > 0) {
|
|
11429
|
+
drifts.push({ table, missingColumns: missing, tableMissing: false });
|
|
11430
|
+
}
|
|
11431
|
+
}
|
|
11432
|
+
return drifts;
|
|
11433
|
+
}
|
|
11434
|
+
function formatSchemaGoldenDrifts(drifts) {
|
|
11435
|
+
return drifts.map(
|
|
11436
|
+
(d) => d.tableMissing ? ` - \u8868 ${d.table} \u6574\u8868\u7F3A\u5931\uFF08\u671F\u671B\u5217\uFF1A${d.missingColumns.join(", ")}\uFF09` : ` - \u8868 ${d.table} \u7F3A\u5217\uFF1A${d.missingColumns.join(", ")}`
|
|
11437
|
+
).join("\n");
|
|
11438
|
+
}
|
|
11439
|
+
function loadSchemaGolden(migrationsDir) {
|
|
11440
|
+
const filePath = import_node_path4.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
|
|
11441
|
+
if (!import_node_fs3.default.existsSync(filePath)) {
|
|
11442
|
+
throw new Error(
|
|
11443
|
+
`[SchemaGolden] FATAL: ${filePath} \u4E0D\u5B58\u5728\u3002\u91D1\u6837\u4E0E\u8FC1\u79FB\u6587\u4EF6\u540C\u76EE\u5F55\u53D1\u5E03\uFF0C\u7F3A\u5931\u8BF4\u660E\u5B89\u88C5\u5305\u4E0D\u5B8C\u6574\uFF1B\u6E90\u7801\u4ED3\u5E93\u91CC\u8BF7\u8FD0\u884C pnpm db:gen-golden \u751F\u6210\u3002`
|
|
11444
|
+
);
|
|
11445
|
+
}
|
|
11446
|
+
try {
|
|
11447
|
+
return JSON.parse(import_node_fs3.default.readFileSync(filePath, "utf8"));
|
|
11448
|
+
} catch (err) {
|
|
11449
|
+
throw new Error(`[SchemaGolden] FATAL: \u91D1\u6837\u6587\u4EF6\u89E3\u6790\u5931\u8D25\uFF08${filePath}\uFF09\uFF1A${String(err)}`, { cause: err });
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11452
|
+
function assertSchemaMatchesGolden(db2, migrationsDir) {
|
|
11453
|
+
const golden = loadSchemaGolden(migrationsDir);
|
|
11454
|
+
const drifts = verifySchemaAgainstGolden(db2, golden);
|
|
11455
|
+
if (drifts.length > 0) {
|
|
11456
|
+
throw new Error(
|
|
11457
|
+
`[SchemaGolden] FATAL: \u6570\u636E\u5E93\u7ED3\u6784\u4F4E\u4E8E\u4EE3\u7801\u671F\u671B\uFF08${drifts.length} \u5904\u7F3A\u5931\uFF09\u2014\u2014\u8FC1\u79FB\u53F0\u8D26\u867D\u5DF2\u8FFD\u5E73\uFF0C\u4F46\u5E93\u88AB\u5916\u90E8\u4FEE\u6539\u8FC7\u6216\u4ECE\u65E7\u5907\u4EFD\u505A\u4E86\u90E8\u5206\u6062\u590D\u3002\u7F3A\u5931\u660E\u7EC6\uFF1A
|
|
11458
|
+
${formatSchemaGoldenDrifts(drifts)}
|
|
11459
|
+
\u6062\u590D\u624B\u6BB5\uFF1A\u7528 backups/ \u91CC\u6700\u65B0\u7684 pre_migration_*.db \u5FEB\u7167\u6062\u590D\uFF0C\u6216\u6838\u5BF9\u6700\u8FD1\u4E00\u6B21\u6062\u590D\u64CD\u4F5C\u7684\u6765\u6E90\u3002`
|
|
11460
|
+
);
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
11463
|
+
|
|
11148
11464
|
// packages/core/db/class-passcode-crypto.ts
|
|
11149
11465
|
var import_node_crypto2 = __toESM(require("node:crypto"), 1);
|
|
11150
11466
|
init_api_key_crypto();
|
|
@@ -11666,7 +11982,7 @@ function purgeAllExpiredData(db2, options) {
|
|
|
11666
11982
|
|
|
11667
11983
|
// packages/core/db/index.ts
|
|
11668
11984
|
init_api_key_crypto();
|
|
11669
|
-
var
|
|
11985
|
+
var import_meta3 = {};
|
|
11670
11986
|
var BCRYPT_ROUNDS = 10;
|
|
11671
11987
|
function hashPassword(pwd) {
|
|
11672
11988
|
return import_bcryptjs.default.hashSync(pwd, BCRYPT_ROUNDS);
|
|
@@ -11741,8 +12057,8 @@ if (process.env.VITEST) {
|
|
|
11741
12057
|
dbPath = import_path2.default.join(__dirname, "educational_os.db");
|
|
11742
12058
|
} else {
|
|
11743
12059
|
try {
|
|
11744
|
-
if (typeof
|
|
11745
|
-
const __filename2 = (0, import_url2.fileURLToPath)(
|
|
12060
|
+
if (typeof import_meta3 !== "undefined" && import_meta3.url) {
|
|
12061
|
+
const __filename2 = (0, import_url2.fileURLToPath)(import_meta3.url);
|
|
11746
12062
|
const __dir = import_path2.default.dirname(__filename2);
|
|
11747
12063
|
dbPath = import_path2.default.join(__dir, "educational_os.db");
|
|
11748
12064
|
} else {
|
|
@@ -11802,7 +12118,16 @@ function initializeDatabase(instance) {
|
|
|
11802
12118
|
if (migrations.length === 0) {
|
|
11803
12119
|
throw new Error(`[DB SSOT] FATAL: \u8FC1\u79FB\u76EE\u5F55 ${migrationsDir} \u91CC\u6CA1\u6709 .sql \u6587\u4EF6\u3002\u62D2\u7EDD\u5728\u6CA1\u6709 schema \u7684\u5E93\u4E0A\u542F\u52A8\u3002`);
|
|
11804
12120
|
}
|
|
11805
|
-
runMigrations(instance, migrations);
|
|
12121
|
+
runMigrations(instance, migrations, { strictLedger: true });
|
|
12122
|
+
if (process.env.VITEST) {
|
|
12123
|
+
try {
|
|
12124
|
+
assertSchemaMatchesGolden(instance, migrationsDir);
|
|
12125
|
+
} catch (err) {
|
|
12126
|
+
console.warn("[SchemaGolden] \u6D4B\u8BD5\u73AF\u5883\u7ED3\u6784\u7EC8\u68C0\u672A\u901A\u8FC7\uFF08\u5FFD\u7565\uFF09\uFF1A", err instanceof Error ? err.message : err);
|
|
12127
|
+
}
|
|
12128
|
+
} else {
|
|
12129
|
+
assertSchemaMatchesGolden(instance, migrationsDir);
|
|
12130
|
+
}
|
|
11806
12131
|
applyStartupDataMigrations(instance);
|
|
11807
12132
|
const apiKeyMigration = encryptStoredPlaintextApiKeys(instance);
|
|
11808
12133
|
if (apiKeyMigration.updated > 0) {
|
|
@@ -14061,10 +14386,10 @@ function parseScoreFields(fields) {
|
|
|
14061
14386
|
if (typeof fields !== "string" || !fields.trim()) return [];
|
|
14062
14387
|
return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
|
|
14063
14388
|
}
|
|
14064
|
-
function getNested(raw,
|
|
14389
|
+
function getNested(raw, path47) {
|
|
14065
14390
|
if (raw === null || raw === void 0) return void 0;
|
|
14066
|
-
if (!
|
|
14067
|
-
const parts = String(
|
|
14391
|
+
if (!path47) return void 0;
|
|
14392
|
+
const parts = String(path47).split(".").map((p) => p.trim()).filter(Boolean);
|
|
14068
14393
|
let cursor = raw;
|
|
14069
14394
|
for (let i = 0; i < parts.length; i += 1) {
|
|
14070
14395
|
if (cursor === null || cursor === void 0) return void 0;
|
|
@@ -18064,7 +18389,7 @@ var import_path11 = __toESM(require("path"), 1);
|
|
|
18064
18389
|
// packages/core/plugin-host/base.ts
|
|
18065
18390
|
var import_path5 = __toESM(require("path"), 1);
|
|
18066
18391
|
var import_express = __toESM(require("express"), 1);
|
|
18067
|
-
var
|
|
18392
|
+
var import_node_module2 = require("node:module");
|
|
18068
18393
|
var import_node_url = require("node:url");
|
|
18069
18394
|
|
|
18070
18395
|
// packages/core/plugin-host/resource-tracker.ts
|
|
@@ -18350,7 +18675,7 @@ var ContributionRegistry = class {
|
|
|
18350
18675
|
// packages/core/plugin-host/base.ts
|
|
18351
18676
|
init_types();
|
|
18352
18677
|
init_errors2();
|
|
18353
|
-
var
|
|
18678
|
+
var import_meta4 = {};
|
|
18354
18679
|
var VALID_TRANSITIONS = {
|
|
18355
18680
|
["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
|
|
18356
18681
|
["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
|
|
@@ -18385,12 +18710,12 @@ function resolveHostSdkDir() {
|
|
|
18385
18710
|
const bases = [];
|
|
18386
18711
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
18387
18712
|
try {
|
|
18388
|
-
bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(
|
|
18713
|
+
bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta4.url)));
|
|
18389
18714
|
} catch {
|
|
18390
18715
|
}
|
|
18391
18716
|
for (const base of bases) {
|
|
18392
18717
|
try {
|
|
18393
|
-
const req = (0,
|
|
18718
|
+
const req = (0, import_node_module2.createRequire)(import_path5.default.join(base, "__openlearn_resolve__.js"));
|
|
18394
18719
|
return import_path5.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
|
|
18395
18720
|
} catch {
|
|
18396
18721
|
}
|
|
@@ -18655,10 +18980,10 @@ var import_semver2 = __toESM(require("semver"), 1);
|
|
|
18655
18980
|
init_types();
|
|
18656
18981
|
|
|
18657
18982
|
// packages/core/plugin-host/execution-policy.ts
|
|
18658
|
-
var
|
|
18983
|
+
var import_node_path9 = __toESM(require("node:path"), 1);
|
|
18659
18984
|
var import_node_url2 = require("node:url");
|
|
18660
18985
|
init_types();
|
|
18661
|
-
var
|
|
18986
|
+
var import_meta5 = {};
|
|
18662
18987
|
function trustedInlineAllowlist(env = process.env) {
|
|
18663
18988
|
return new Set(
|
|
18664
18989
|
(env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
@@ -18666,23 +18991,23 @@ function trustedInlineAllowlist(env = process.env) {
|
|
|
18666
18991
|
}
|
|
18667
18992
|
function builtinPluginsDir() {
|
|
18668
18993
|
try {
|
|
18669
|
-
const url =
|
|
18994
|
+
const url = import_meta5.url;
|
|
18670
18995
|
if (typeof url === "string" && url.startsWith("file:")) {
|
|
18671
|
-
return
|
|
18996
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname((0, import_node_url2.fileURLToPath)(url)), "../../plugins");
|
|
18672
18997
|
}
|
|
18673
18998
|
} catch {
|
|
18674
18999
|
}
|
|
18675
19000
|
if (typeof __dirname !== "undefined") {
|
|
18676
|
-
return
|
|
19001
|
+
return import_node_path9.default.resolve(__dirname, "../packages/plugins");
|
|
18677
19002
|
}
|
|
18678
|
-
return
|
|
19003
|
+
return import_node_path9.default.resolve(process.cwd(), "packages/plugins");
|
|
18679
19004
|
}
|
|
18680
19005
|
function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir()) {
|
|
18681
19006
|
if (!sourcePath) return false;
|
|
18682
|
-
const root =
|
|
18683
|
-
const resolved =
|
|
18684
|
-
const relative2 =
|
|
18685
|
-
return relative2 === "" || !relative2.startsWith("..") && !
|
|
19007
|
+
const root = import_node_path9.default.resolve(builtinDir);
|
|
19008
|
+
const resolved = import_node_path9.default.resolve(sourcePath);
|
|
19009
|
+
const relative2 = import_node_path9.default.relative(root, resolved);
|
|
19010
|
+
return relative2 === "" || !relative2.startsWith("..") && !import_node_path9.default.isAbsolute(relative2);
|
|
18686
19011
|
}
|
|
18687
19012
|
function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
|
|
18688
19013
|
if (isInsideBuiltinPluginsDir(sourcePath)) return true;
|
|
@@ -19645,6 +19970,37 @@ var PluginHostCore = class _PluginHostCore extends PluginHostBase {
|
|
|
19645
19970
|
console.warn(`[PluginHost] Failed to revoke stage guards for "${pluginId}":`, e);
|
|
19646
19971
|
}
|
|
19647
19972
|
}
|
|
19973
|
+
/**
|
|
19974
|
+
* 回收该插件注册的服务端题型(Issue #320)。
|
|
19975
|
+
*
|
|
19976
|
+
* 题型注册表(`IAssessmentGraderRegistry`)是插件用 `ctx.resolve(token)` 直取的
|
|
19977
|
+
* **裸注册表**,不像 `ctx.services.*` 那样经过 `ResourceTracker` 包装 —— 所以
|
|
19978
|
+
* `disposeAll()` 拿它没办法。不清理的后果已验证为真:
|
|
19979
|
+
*
|
|
19980
|
+
* 1. 停用插件后题型仍留在注册表里 —— 教师继续能出题、继续按旧闭包判分(“幽灵题型”),
|
|
19981
|
+
* `GET /api/assessment-question-types` 也继续把它列给前端;
|
|
19982
|
+
* 2. 重装/升级插件时 `registerQuestionType` 抛 `duplicate question type`,
|
|
19983
|
+
* 表现为「升级后插件激活失败」,必须重启宿主才能恢复。
|
|
19984
|
+
*
|
|
19985
|
+
* 与 `revokePluginStageGuards` 同构:注册方可能传安装行 UUID,也可能传
|
|
19986
|
+
* manifest.id(文档约定的就是后者),两侧都清一遍(幂等)。
|
|
19987
|
+
*/
|
|
19988
|
+
async revokePluginQuestionTypes(pluginId, manifestId) {
|
|
19989
|
+
try {
|
|
19990
|
+
const registry4 = await this.serviceRegistry.tryResolve(IAssessmentGraderRegistryToken);
|
|
19991
|
+
if (!registry4 || typeof registry4.clearOwnedQuestionTypes !== "function") return;
|
|
19992
|
+
let removed = 0;
|
|
19993
|
+
for (const owner of /* @__PURE__ */ new Set([pluginId, manifestId])) {
|
|
19994
|
+
if (!owner) continue;
|
|
19995
|
+
removed += registry4.clearOwnedQuestionTypes(owner) ?? 0;
|
|
19996
|
+
}
|
|
19997
|
+
if (removed > 0) {
|
|
19998
|
+
console.log(`[PluginHost] Revoked ${removed} question type(s) for plugin "${pluginId}"`);
|
|
19999
|
+
}
|
|
20000
|
+
} catch (e) {
|
|
20001
|
+
console.warn(`[PluginHost] Failed to revoke question types for "${pluginId}":`, e);
|
|
20002
|
+
}
|
|
20003
|
+
}
|
|
19648
20004
|
setPluginState(pluginId, state, opts = {}) {
|
|
19649
20005
|
this.pluginStates.set(pluginId, state);
|
|
19650
20006
|
if (!opts.persistDb) return;
|
|
@@ -19742,7 +20098,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19742
20098
|
/**
|
|
19743
20099
|
* 判断指定插件的某个请求路径是否为流式路由 (SSE)
|
|
19744
20100
|
*/
|
|
19745
|
-
isStreamRoute(pluginIdOrManifestId, method,
|
|
20101
|
+
isStreamRoute(pluginIdOrManifestId, method, path47) {
|
|
19746
20102
|
const upperMethod = method.toUpperCase();
|
|
19747
20103
|
const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
|
|
19748
20104
|
const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
|
|
@@ -19751,7 +20107,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19751
20107
|
for (const r of manifest.api.routes) {
|
|
19752
20108
|
if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
|
|
19753
20109
|
const compiled = compileRoutePattern(r.path);
|
|
19754
|
-
if (compiled.regex.test(
|
|
20110
|
+
if (compiled.regex.test(path47)) {
|
|
19755
20111
|
return true;
|
|
19756
20112
|
}
|
|
19757
20113
|
}
|
|
@@ -19759,14 +20115,14 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19759
20115
|
}
|
|
19760
20116
|
if (!instance) return false;
|
|
19761
20117
|
if (instance.context?.http) {
|
|
19762
|
-
return instance.context.http.isStream(upperMethod,
|
|
20118
|
+
return instance.context.http.isStream(upperMethod, path47);
|
|
19763
20119
|
}
|
|
19764
20120
|
if (instance.workerRef) {
|
|
19765
20121
|
const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
|
|
19766
20122
|
for (const r of routes) {
|
|
19767
20123
|
if (r.method === upperMethod && r.isStream) {
|
|
19768
20124
|
const compiled = compileRoutePattern(r.pattern);
|
|
19769
|
-
if (compiled.regex.test(
|
|
20125
|
+
if (compiled.regex.test(path47)) {
|
|
19770
20126
|
return true;
|
|
19771
20127
|
}
|
|
19772
20128
|
}
|
|
@@ -19839,8 +20195,8 @@ init_manifest_schema();
|
|
|
19839
20195
|
init_context_builder();
|
|
19840
20196
|
|
|
19841
20197
|
// packages/core/plugin-host/declarative-migrator.ts
|
|
19842
|
-
var
|
|
19843
|
-
var
|
|
20198
|
+
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
20199
|
+
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
19844
20200
|
init_errors2();
|
|
19845
20201
|
init_plugin_tables();
|
|
19846
20202
|
function ensurePluginDeclaredMigrationsTable(db2) {
|
|
@@ -19856,7 +20212,7 @@ function ensurePluginDeclaredMigrationsTable(db2) {
|
|
|
19856
20212
|
`);
|
|
19857
20213
|
}
|
|
19858
20214
|
function parseMigrationFileName(fileName) {
|
|
19859
|
-
const baseName =
|
|
20215
|
+
const baseName = import_node_path10.default.basename(fileName, ".sql");
|
|
19860
20216
|
const match = baseName.match(/^(\d+)[_-]?(.*)$/);
|
|
19861
20217
|
if (match) {
|
|
19862
20218
|
const version = parseInt(match[1], 10);
|
|
@@ -19865,22 +20221,22 @@ function parseMigrationFileName(fileName) {
|
|
|
19865
20221
|
return { version: 0, baseName };
|
|
19866
20222
|
}
|
|
19867
20223
|
function loadPluginMigrations(migrationsDir) {
|
|
19868
|
-
if (!
|
|
20224
|
+
if (!import_node_fs8.default.existsSync(migrationsDir)) {
|
|
19869
20225
|
return [];
|
|
19870
20226
|
}
|
|
19871
|
-
const stat =
|
|
20227
|
+
const stat = import_node_fs8.default.statSync(migrationsDir);
|
|
19872
20228
|
if (!stat.isDirectory()) {
|
|
19873
20229
|
return [];
|
|
19874
20230
|
}
|
|
19875
|
-
const files =
|
|
20231
|
+
const files = import_node_fs8.default.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort((a, b) => {
|
|
19876
20232
|
const vA = parseMigrationFileName(a).version;
|
|
19877
20233
|
const vB = parseMigrationFileName(b).version;
|
|
19878
20234
|
if (vA !== vB) return vA - vB;
|
|
19879
20235
|
return a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
|
|
19880
20236
|
});
|
|
19881
20237
|
return files.map((file) => {
|
|
19882
|
-
const fullPath =
|
|
19883
|
-
const content =
|
|
20238
|
+
const fullPath = import_node_path10.default.join(migrationsDir, file);
|
|
20239
|
+
const content = import_node_fs8.default.readFileSync(fullPath, "utf8");
|
|
19884
20240
|
const { version, baseName } = parseMigrationFileName(file);
|
|
19885
20241
|
const parsed = parseMigrationSql(baseName, content);
|
|
19886
20242
|
return {
|
|
@@ -20521,6 +20877,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20521
20877
|
this.resourceTracker.disposeAll(pluginId);
|
|
20522
20878
|
this.pluginInstances.delete(pluginId);
|
|
20523
20879
|
this.revokePluginContributions(pluginId);
|
|
20880
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20524
20881
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20525
20882
|
if (this._hotReloadController) {
|
|
20526
20883
|
this._hotReloadController.unregisterPlugin(pluginId);
|
|
@@ -20539,6 +20896,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20539
20896
|
this.resourceTracker.disposeAll(pluginId);
|
|
20540
20897
|
this.pluginInstances.delete(pluginId);
|
|
20541
20898
|
this.revokePluginContributions(pluginId);
|
|
20899
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20542
20900
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20543
20901
|
if (actorId) {
|
|
20544
20902
|
await this.revokeCapabilities(actorId);
|
|
@@ -20574,6 +20932,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20574
20932
|
this.resourceTracker.disposeAll(pluginId);
|
|
20575
20933
|
this.pluginInstances.delete(pluginId);
|
|
20576
20934
|
this.revokePluginContributions(pluginId);
|
|
20935
|
+
await this.revokePluginQuestionTypes(pluginId, instance?.manifest?.id ?? pluginId);
|
|
20577
20936
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20578
20937
|
if (actorId) {
|
|
20579
20938
|
await this.revokeCapabilities(actorId);
|
|
@@ -20636,27 +20995,27 @@ var import_path8 = __toESM(require("path"), 1);
|
|
|
20636
20995
|
var import_semver3 = __toESM(require("semver"), 1);
|
|
20637
20996
|
|
|
20638
20997
|
// packages/core/version.ts
|
|
20639
|
-
var
|
|
20640
|
-
var
|
|
20998
|
+
var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
20999
|
+
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
20641
21000
|
var import_node_url5 = require("node:url");
|
|
20642
|
-
var
|
|
20643
|
-
var FALLBACK_VERSION = "0.7.
|
|
21001
|
+
var import_meta7 = {};
|
|
21002
|
+
var FALLBACK_VERSION = "0.7.5";
|
|
20644
21003
|
function readPlatformVersion() {
|
|
20645
21004
|
const bases = [];
|
|
20646
21005
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
20647
21006
|
try {
|
|
20648
|
-
bases.push(
|
|
21007
|
+
bases.push(import_node_path13.default.dirname((0, import_node_url5.fileURLToPath)(import_meta7.url)));
|
|
20649
21008
|
} catch {
|
|
20650
21009
|
}
|
|
20651
21010
|
for (const base of bases) {
|
|
20652
21011
|
let dir = base;
|
|
20653
21012
|
for (; ; ) {
|
|
20654
21013
|
try {
|
|
20655
|
-
const pkg = JSON.parse(
|
|
21014
|
+
const pkg = JSON.parse(import_node_fs11.default.readFileSync(import_node_path13.default.join(dir, "package.json"), "utf-8"));
|
|
20656
21015
|
if (pkg.name === "openlearn-next" && typeof pkg.version === "string") return pkg.version;
|
|
20657
21016
|
} catch {
|
|
20658
21017
|
}
|
|
20659
|
-
const parent =
|
|
21018
|
+
const parent = import_node_path13.default.dirname(dir);
|
|
20660
21019
|
if (parent === dir) break;
|
|
20661
21020
|
dir = parent;
|
|
20662
21021
|
}
|
|
@@ -21375,8 +21734,8 @@ init_audit_log();
|
|
|
21375
21734
|
|
|
21376
21735
|
// packages/core/plugin-host/dependency-install.ts
|
|
21377
21736
|
var import_node_child_process2 = require("node:child_process");
|
|
21378
|
-
var
|
|
21379
|
-
var
|
|
21737
|
+
var import_node_fs12 = __toESM(require("node:fs"), 1);
|
|
21738
|
+
var import_node_path14 = __toESM(require("node:path"), 1);
|
|
21380
21739
|
var DependencyInstallError = class extends Error {
|
|
21381
21740
|
constructor(message, pluginId, cause) {
|
|
21382
21741
|
super(message);
|
|
@@ -21411,8 +21770,8 @@ function resolveRegistry() {
|
|
|
21411
21770
|
function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
|
|
21412
21771
|
const names = dependencyNames(dependencies);
|
|
21413
21772
|
if (names.length === 0) return;
|
|
21414
|
-
const lockPath =
|
|
21415
|
-
if (!
|
|
21773
|
+
const lockPath = import_node_path14.default.join(pluginDir, "package-lock.json");
|
|
21774
|
+
if (!import_node_fs12.default.existsSync(lockPath)) {
|
|
21416
21775
|
throw new DependencyInstallError(
|
|
21417
21776
|
`\u4F9D\u8D56\u5B89\u88C5\u672A\u4EA7\u51FA package-lock.json \u2014\u2014 \u65E0\u6CD5\u786E\u8BA4\u4F9D\u8D56\u5DF2\u843D\u76D8\u3002pluginId=${pluginId}\uFF0C\u58F0\u660E\u4F9D\u8D56\uFF1A${names.join(", ")}`,
|
|
21418
21777
|
pluginId
|
|
@@ -21420,7 +21779,7 @@ function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
|
|
|
21420
21779
|
}
|
|
21421
21780
|
let lock;
|
|
21422
21781
|
try {
|
|
21423
|
-
lock = JSON.parse(
|
|
21782
|
+
lock = JSON.parse(import_node_fs12.default.readFileSync(lockPath, "utf-8"));
|
|
21424
21783
|
} catch (err) {
|
|
21425
21784
|
throw new DependencyInstallError(
|
|
21426
21785
|
`package-lock.json \u4E0D\u662F\u5408\u6CD5 JSON \u2014\u2014 \u4F9D\u8D56\u5B89\u88C5\u7ED3\u679C\u4E0D\u53EF\u4FE1\u3002pluginId=${pluginId}`,
|
|
@@ -21493,8 +21852,8 @@ function installPluginDependencies(pluginDir, opts, runNpm = import_node_child_p
|
|
|
21493
21852
|
const { pluginId, dependencies, operation = "install" } = opts;
|
|
21494
21853
|
const depMap = toNpmDependencyMap(dependencies);
|
|
21495
21854
|
if (Object.keys(depMap).length === 0) return;
|
|
21496
|
-
const pkgJsonPath =
|
|
21497
|
-
|
|
21855
|
+
const pkgJsonPath = import_node_path14.default.join(pluginDir, "package.json");
|
|
21856
|
+
import_node_fs12.default.writeFileSync(
|
|
21498
21857
|
pkgJsonPath,
|
|
21499
21858
|
JSON.stringify({ name: pluginId, version: "0.0.0", dependencies: depMap }, null, 2),
|
|
21500
21859
|
"utf-8"
|
|
@@ -22323,6 +22682,7 @@ var PluginHostUninstall = class extends PluginHostInstall {
|
|
|
22323
22682
|
return pluginId;
|
|
22324
22683
|
}
|
|
22325
22684
|
})();
|
|
22685
|
+
await this.revokePluginQuestionTypes(pluginId, manifestId);
|
|
22326
22686
|
if (state === "uninstalled" /* UNINSTALLED */ && !row) {
|
|
22327
22687
|
this.pluginStates.delete(pluginId);
|
|
22328
22688
|
return {
|
|
@@ -22763,20 +23123,20 @@ init_install_utils();
|
|
|
22763
23123
|
|
|
22764
23124
|
// packages/core/security/plugin-signature.ts
|
|
22765
23125
|
var import_node_crypto9 = __toESM(require("node:crypto"), 1);
|
|
22766
|
-
var
|
|
23126
|
+
var import_node_fs13 = __toESM(require("node:fs"), 1);
|
|
22767
23127
|
function computeSha256(input) {
|
|
22768
23128
|
const hash = import_node_crypto9.default.createHash("sha256");
|
|
22769
23129
|
if (typeof input === "string") {
|
|
22770
|
-
if (
|
|
22771
|
-
const fd =
|
|
23130
|
+
if (import_node_fs13.default.existsSync(input)) {
|
|
23131
|
+
const fd = import_node_fs13.default.openSync(input, "r");
|
|
22772
23132
|
const buf = Buffer.alloc(64 * 1024);
|
|
22773
23133
|
let bytesRead = 0;
|
|
22774
23134
|
try {
|
|
22775
|
-
while ((bytesRead =
|
|
23135
|
+
while ((bytesRead = import_node_fs13.default.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
22776
23136
|
hash.update(buf.subarray(0, bytesRead));
|
|
22777
23137
|
}
|
|
22778
23138
|
} finally {
|
|
22779
|
-
|
|
23139
|
+
import_node_fs13.default.closeSync(fd);
|
|
22780
23140
|
}
|
|
22781
23141
|
const digest4 = hash.digest();
|
|
22782
23142
|
return { hex: digest4.toString("hex"), base64: digest4.toString("base64") };
|
|
@@ -22845,8 +23205,8 @@ async function extractEmbeddedSignature(zipPathOrBuffer) {
|
|
|
22845
23205
|
const JSZip5 = (await import("jszip")).default;
|
|
22846
23206
|
let buffer;
|
|
22847
23207
|
if (typeof zipPathOrBuffer === "string") {
|
|
22848
|
-
if (!
|
|
22849
|
-
buffer =
|
|
23208
|
+
if (!import_node_fs13.default.existsSync(zipPathOrBuffer)) return null;
|
|
23209
|
+
buffer = import_node_fs13.default.readFileSync(zipPathOrBuffer);
|
|
22850
23210
|
} else {
|
|
22851
23211
|
buffer = zipPathOrBuffer;
|
|
22852
23212
|
}
|
|
@@ -22891,9 +23251,9 @@ function resolveTrustedPublicKeys(explicitKeys, env = process.env) {
|
|
|
22891
23251
|
for (const part of remaining.split(/[,;\n]/)) {
|
|
22892
23252
|
const p = part.trim();
|
|
22893
23253
|
if (!p) continue;
|
|
22894
|
-
if (
|
|
23254
|
+
if (import_node_fs13.default.existsSync(p)) {
|
|
22895
23255
|
try {
|
|
22896
|
-
const fileContent =
|
|
23256
|
+
const fileContent = import_node_fs13.default.readFileSync(p, "utf8").trim();
|
|
22897
23257
|
const filePems = fileContent.match(/-----BEGIN [A-Z0-9 ]+-----[\s\S]+?-----END [A-Z0-9 ]+-----/g);
|
|
22898
23258
|
if (filePems) {
|
|
22899
23259
|
for (const fpem of filePems) {
|
|
@@ -22946,7 +23306,7 @@ async function verifyPluginPackage(options) {
|
|
|
22946
23306
|
const JSZip5 = (await import("jszip")).default;
|
|
22947
23307
|
let buffer;
|
|
22948
23308
|
if (typeof packageSource === "string") {
|
|
22949
|
-
buffer =
|
|
23309
|
+
buffer = import_node_fs13.default.readFileSync(packageSource);
|
|
22950
23310
|
} else {
|
|
22951
23311
|
buffer = packageSource;
|
|
22952
23312
|
}
|
|
@@ -23152,11 +23512,11 @@ var PluginDistributionManager = class {
|
|
|
23152
23512
|
return;
|
|
23153
23513
|
}
|
|
23154
23514
|
const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
|
|
23155
|
-
const
|
|
23156
|
-
if (!
|
|
23515
|
+
const fs40 = await import("node:fs");
|
|
23516
|
+
if (!fs40.existsSync(filePath)) {
|
|
23157
23517
|
throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
|
|
23158
23518
|
}
|
|
23159
|
-
const code =
|
|
23519
|
+
const code = fs40.readFileSync(filePath, "utf-8");
|
|
23160
23520
|
await this.pluginHost.reloadPlugin(pluginId, code);
|
|
23161
23521
|
}
|
|
23162
23522
|
async updateFromZip(zipPath, options = {}) {
|
|
@@ -23345,8 +23705,8 @@ var HotReloadController = class {
|
|
|
23345
23705
|
const timer = setTimeout(async () => {
|
|
23346
23706
|
this.debounceTimers.delete(pluginId);
|
|
23347
23707
|
try {
|
|
23348
|
-
const
|
|
23349
|
-
const newSourceCode = await
|
|
23708
|
+
const fs40 = await import("fs/promises");
|
|
23709
|
+
const newSourceCode = await fs40.readFile(filePath, "utf-8");
|
|
23350
23710
|
console.log(`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`);
|
|
23351
23711
|
await this.pluginHost.reloadPlugin(pluginId, newSourceCode);
|
|
23352
23712
|
} catch (err) {
|
|
@@ -32579,24 +32939,24 @@ ${formatEnvIssues(warnings)}`);
|
|
|
32579
32939
|
}
|
|
32580
32940
|
|
|
32581
32941
|
// server/security/file-permissions.ts
|
|
32582
|
-
var
|
|
32583
|
-
var
|
|
32942
|
+
var import_node_fs14 = __toESM(require("node:fs"), 1);
|
|
32943
|
+
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
32584
32944
|
var FILE_PERMISSION_BASELINE_DOC = "\u4FEE\u590D\uFF1Achmod 600 <file>\uFF08\u76EE\u5F55 chmod 700\uFF09\uFF1B\u8BE6\u89C1 docs/administrator-guide/admin-manual.md \u6587\u4EF6\u6743\u9650\u57FA\u7EBF\u4E00\u8282\u3002";
|
|
32585
32945
|
function modeOf(p) {
|
|
32586
32946
|
try {
|
|
32587
|
-
return
|
|
32947
|
+
return import_node_fs14.default.statSync(p).mode & 511;
|
|
32588
32948
|
} catch {
|
|
32589
32949
|
return null;
|
|
32590
32950
|
}
|
|
32591
32951
|
}
|
|
32592
32952
|
function defaultPaths() {
|
|
32593
|
-
const dbPath2 = process.env.OPENLEARN_DB_PATH ?
|
|
32594
|
-
const backupDir = process.env.BACKUP_DIR ?
|
|
32953
|
+
const dbPath2 = process.env.OPENLEARN_DB_PATH ? import_node_path15.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH) : import_node_path15.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
|
|
32954
|
+
const backupDir = process.env.BACKUP_DIR ? import_node_path15.default.resolve(process.cwd(), process.env.BACKUP_DIR) : import_node_path15.default.join(import_node_path15.default.dirname(dbPath2), "backups");
|
|
32595
32955
|
return {
|
|
32596
|
-
envPath:
|
|
32956
|
+
envPath: import_node_path15.default.resolve(process.cwd(), ".env"),
|
|
32597
32957
|
dbPath: dbPath2,
|
|
32598
32958
|
backupDir,
|
|
32599
|
-
encryptionKeyPath:
|
|
32959
|
+
encryptionKeyPath: import_node_path15.default.resolve(process.cwd(), "storage", "encryption-key")
|
|
32600
32960
|
};
|
|
32601
32961
|
}
|
|
32602
32962
|
function checkSensitiveFilePermissions(options = {}) {
|
|
@@ -32627,11 +32987,11 @@ function checkSensitiveFilePermissions(options = {}) {
|
|
|
32627
32987
|
}
|
|
32628
32988
|
if (dirMode !== null) {
|
|
32629
32989
|
let scanned = 0;
|
|
32630
|
-
for (const entry of
|
|
32990
|
+
for (const entry of import_node_fs14.default.readdirSync(backupDir)) {
|
|
32631
32991
|
if (scanned >= maxBackupFiles) break;
|
|
32632
32992
|
if (!entry.endsWith(".db")) continue;
|
|
32633
32993
|
scanned++;
|
|
32634
|
-
checkFile(
|
|
32994
|
+
checkFile(import_node_path15.default.join(backupDir, entry), "600", "backup-file");
|
|
32635
32995
|
}
|
|
32636
32996
|
}
|
|
32637
32997
|
return issues;
|
|
@@ -33531,8 +33891,8 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
33531
33891
|
};
|
|
33532
33892
|
|
|
33533
33893
|
// server/backup-scheduler.ts
|
|
33534
|
-
var
|
|
33535
|
-
var
|
|
33894
|
+
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
33895
|
+
var import_node_path16 = __toESM(require("node:path"), 1);
|
|
33536
33896
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
33537
33897
|
var DAY_MS2 = 24 * HOUR_MS;
|
|
33538
33898
|
var WAL_FULL_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
@@ -33549,8 +33909,8 @@ function shouldFullCheckpoint(walBytes, threshold = WAL_FULL_THRESHOLD_BYTES) {
|
|
|
33549
33909
|
}
|
|
33550
33910
|
function resolveDbPath(explicit) {
|
|
33551
33911
|
if (explicit) return explicit;
|
|
33552
|
-
if (process.env.OPENLEARN_DB_PATH) return
|
|
33553
|
-
return
|
|
33912
|
+
if (process.env.OPENLEARN_DB_PATH) return import_node_path16.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
|
|
33913
|
+
return import_node_path16.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
|
|
33554
33914
|
}
|
|
33555
33915
|
function startBackupScheduler(deps) {
|
|
33556
33916
|
const noop = { stop: () => {
|
|
@@ -33578,7 +33938,7 @@ function startBackupScheduler(deps) {
|
|
|
33578
33938
|
try {
|
|
33579
33939
|
let walBytes = 0;
|
|
33580
33940
|
try {
|
|
33581
|
-
walBytes =
|
|
33941
|
+
walBytes = import_node_fs15.default.statSync(dbPath2 + "-wal").size;
|
|
33582
33942
|
} catch {
|
|
33583
33943
|
walBytes = 0;
|
|
33584
33944
|
}
|
|
@@ -33669,9 +34029,9 @@ function isWhitelisted(route, whitelist) {
|
|
|
33669
34029
|
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
33670
34030
|
);
|
|
33671
34031
|
}
|
|
33672
|
-
function normalizePath2(
|
|
33673
|
-
if (!
|
|
33674
|
-
return
|
|
34032
|
+
function normalizePath2(path47) {
|
|
34033
|
+
if (!path47) return "/";
|
|
34034
|
+
return path47.endsWith("/") && path47.length > 1 ? path47.slice(0, -1) : path47;
|
|
33675
34035
|
}
|
|
33676
34036
|
function auditRouteScopes(app, whitelist = []) {
|
|
33677
34037
|
const declared = [];
|
|
@@ -33849,11 +34209,11 @@ function enforcePasswordChanged(req, res, next) {
|
|
|
33849
34209
|
if (isPasswordChangeGateSkipped()) {
|
|
33850
34210
|
return next();
|
|
33851
34211
|
}
|
|
33852
|
-
const
|
|
33853
|
-
if (!
|
|
34212
|
+
const path47 = req.path || "";
|
|
34213
|
+
if (!path47.startsWith("/api/")) {
|
|
33854
34214
|
return next();
|
|
33855
34215
|
}
|
|
33856
|
-
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(
|
|
34216
|
+
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path47)) {
|
|
33857
34217
|
return next();
|
|
33858
34218
|
}
|
|
33859
34219
|
const token = getCookieToken(req);
|
|
@@ -33928,8 +34288,8 @@ function csrfGuard(req, res, next) {
|
|
|
33928
34288
|
next();
|
|
33929
34289
|
return;
|
|
33930
34290
|
}
|
|
33931
|
-
const
|
|
33932
|
-
if (EXEMPT_PREFIXES.some((p) =>
|
|
34291
|
+
const path48 = req.path;
|
|
34292
|
+
if (EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
33933
34293
|
next();
|
|
33934
34294
|
return;
|
|
33935
34295
|
}
|
|
@@ -33939,10 +34299,10 @@ function csrfGuard(req, res, next) {
|
|
|
33939
34299
|
const origin = req.headers.origin;
|
|
33940
34300
|
const referer = req.headers.referer;
|
|
33941
34301
|
const host = req.headers.host;
|
|
33942
|
-
const
|
|
34302
|
+
const path47 = req.path;
|
|
33943
34303
|
if (typeof origin === "string" && origin !== "") {
|
|
33944
34304
|
if (origin === "null") {
|
|
33945
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34305
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
33946
34306
|
res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
|
|
33947
34307
|
return;
|
|
33948
34308
|
}
|
|
@@ -33950,7 +34310,7 @@ function csrfGuard(req, res, next) {
|
|
|
33950
34310
|
try {
|
|
33951
34311
|
const originHost = new URL(origin).host;
|
|
33952
34312
|
if (host && originHost.toLowerCase() !== host.toLowerCase()) {
|
|
33953
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34313
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
33954
34314
|
res.status(403).json({
|
|
33955
34315
|
success: false,
|
|
33956
34316
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -33968,7 +34328,7 @@ function csrfGuard(req, res, next) {
|
|
|
33968
34328
|
try {
|
|
33969
34329
|
const refererHost = new URL(referer).host;
|
|
33970
34330
|
if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
|
|
33971
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34331
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
33972
34332
|
res.status(403).json({
|
|
33973
34333
|
success: false,
|
|
33974
34334
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34021,8 +34381,8 @@ function parseSentryDsn(dsn) {
|
|
|
34021
34381
|
const projectId = url.pathname.split("/").filter(Boolean).pop();
|
|
34022
34382
|
if (!projectId || !/^\d+$/.test(projectId)) return null;
|
|
34023
34383
|
const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
|
|
34024
|
-
const
|
|
34025
|
-
const envelopeUrl = `${url.protocol}//${url.host}${
|
|
34384
|
+
const path47 = prefix ? `/${prefix}` : "";
|
|
34385
|
+
const envelopeUrl = `${url.protocol}//${url.host}${path47}/api/${projectId}/envelope/`;
|
|
34026
34386
|
return { envelopeUrl, publicKey };
|
|
34027
34387
|
} catch {
|
|
34028
34388
|
return null;
|
|
@@ -34462,23 +34822,23 @@ function createSocketIoCors(options) {
|
|
|
34462
34822
|
}
|
|
34463
34823
|
|
|
34464
34824
|
// server/routes/health.ts
|
|
34465
|
-
var
|
|
34466
|
-
var
|
|
34825
|
+
var import_node_fs17 = __toESM(require("node:fs"), 1);
|
|
34826
|
+
var import_node_path18 = __toESM(require("node:path"), 1);
|
|
34467
34827
|
|
|
34468
34828
|
// packages/core/observability/health.ts
|
|
34469
|
-
var
|
|
34470
|
-
var
|
|
34829
|
+
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
34830
|
+
var import_node_path17 = __toESM(require("node:path"), 1);
|
|
34471
34831
|
var import_node_crypto13 = require("node:crypto");
|
|
34472
34832
|
function probeStorageWritable(dir) {
|
|
34473
|
-
const probe =
|
|
34833
|
+
const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
|
|
34474
34834
|
try {
|
|
34475
|
-
|
|
34476
|
-
|
|
34477
|
-
|
|
34835
|
+
import_node_fs16.default.mkdirSync(dir, { recursive: true });
|
|
34836
|
+
import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
|
|
34837
|
+
import_node_fs16.default.unlinkSync(probe);
|
|
34478
34838
|
return true;
|
|
34479
34839
|
} catch {
|
|
34480
34840
|
try {
|
|
34481
|
-
|
|
34841
|
+
import_node_fs16.default.unlinkSync(probe);
|
|
34482
34842
|
} catch {
|
|
34483
34843
|
}
|
|
34484
34844
|
return false;
|
|
@@ -34486,15 +34846,15 @@ function probeStorageWritable(dir) {
|
|
|
34486
34846
|
}
|
|
34487
34847
|
function listMigrationNames(migrationsDir) {
|
|
34488
34848
|
try {
|
|
34489
|
-
if (!
|
|
34490
|
-
const names =
|
|
34849
|
+
if (!import_node_fs16.default.existsSync(migrationsDir)) return null;
|
|
34850
|
+
const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path17.default.basename(file, ".sql"));
|
|
34491
34851
|
return names.length > 0 ? names : null;
|
|
34492
34852
|
} catch {
|
|
34493
34853
|
return null;
|
|
34494
34854
|
}
|
|
34495
34855
|
}
|
|
34496
34856
|
function migrationRecordName(name) {
|
|
34497
|
-
return
|
|
34857
|
+
return import_node_path17.default.basename(name.replace(/\r/g, "").trim(), ".sql");
|
|
34498
34858
|
}
|
|
34499
34859
|
function migrationsComplete(expected, applied) {
|
|
34500
34860
|
if (!expected) return { ok: false, reason: "migrations" };
|
|
@@ -34586,15 +34946,15 @@ function countPendingMigrations(migrationsDir, appliedMigrations) {
|
|
|
34586
34946
|
function newestBackupAgeSeconds(backupDir, now) {
|
|
34587
34947
|
let names;
|
|
34588
34948
|
try {
|
|
34589
|
-
if (!
|
|
34590
|
-
names =
|
|
34949
|
+
if (!import_node_fs17.default.existsSync(backupDir)) return OPERATIONAL_GAUGE_UNKNOWN;
|
|
34950
|
+
names = import_node_fs17.default.readdirSync(backupDir);
|
|
34591
34951
|
} catch {
|
|
34592
34952
|
return OPERATIONAL_GAUGE_UNKNOWN;
|
|
34593
34953
|
}
|
|
34594
34954
|
let newestMs = Number.NEGATIVE_INFINITY;
|
|
34595
34955
|
for (const name of names) {
|
|
34596
34956
|
try {
|
|
34597
|
-
const stat =
|
|
34957
|
+
const stat = import_node_fs17.default.statSync(import_node_path18.default.join(backupDir, name));
|
|
34598
34958
|
if (!stat.isFile()) continue;
|
|
34599
34959
|
if (stat.mtimeMs > newestMs) newestMs = stat.mtimeMs;
|
|
34600
34960
|
} catch {
|
|
@@ -34626,7 +34986,7 @@ function defaultCountEvents() {
|
|
|
34626
34986
|
function registerHealthRoutes(app, options) {
|
|
34627
34987
|
const startTime = options?.startTime ?? Date.now();
|
|
34628
34988
|
const platformVersion = options?.platformVersion ?? PLATFORM_VERSION;
|
|
34629
|
-
const migrationsDir = options?.migrationsDir ??
|
|
34989
|
+
const migrationsDir = options?.migrationsDir ?? import_node_path18.default.join(process.cwd(), "migrations");
|
|
34630
34990
|
const storageDir = options?.storageDir ?? getStorageDir();
|
|
34631
34991
|
const backupDir = options?.backupDir ?? getBackupsDir();
|
|
34632
34992
|
const countEvents = options?.countEvents ?? defaultCountEvents;
|
|
@@ -34746,8 +35106,8 @@ var import_express2 = __toESM(require("express"), 1);
|
|
|
34746
35106
|
var import_zod2 = require("zod");
|
|
34747
35107
|
|
|
34748
35108
|
// server/services/course-package-assets.ts
|
|
34749
|
-
var
|
|
34750
|
-
var
|
|
35109
|
+
var import_node_fs18 = __toESM(require("node:fs"), 1);
|
|
35110
|
+
var import_node_path19 = __toESM(require("node:path"), 1);
|
|
34751
35111
|
var import_node_crypto14 = __toESM(require("node:crypto"), 1);
|
|
34752
35112
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
34753
35113
|
var import_jspdf = require("jspdf");
|
|
@@ -34991,8 +35351,8 @@ async function materializePackageAssets(assets) {
|
|
|
34991
35351
|
const result = /* @__PURE__ */ new Map();
|
|
34992
35352
|
if (assets.length === 0) return result;
|
|
34993
35353
|
const uploadsDir = getUploadsDir();
|
|
34994
|
-
if (!
|
|
34995
|
-
|
|
35354
|
+
if (!import_node_fs18.default.existsSync(uploadsDir)) {
|
|
35355
|
+
import_node_fs18.default.mkdirSync(uploadsDir, { recursive: true });
|
|
34996
35356
|
}
|
|
34997
35357
|
const writtenPaths = [];
|
|
34998
35358
|
try {
|
|
@@ -35014,8 +35374,8 @@ async function materializePackageAssets(assets) {
|
|
|
35014
35374
|
throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
|
|
35015
35375
|
}
|
|
35016
35376
|
const uniqueName = `${Date.now()}-${import_node_crypto14.default.randomBytes(4).toString("hex")}${ext}`;
|
|
35017
|
-
const filePath =
|
|
35018
|
-
|
|
35377
|
+
const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
|
|
35378
|
+
import_node_fs18.default.writeFileSync(filePath, buffer);
|
|
35019
35379
|
writtenPaths.push(filePath);
|
|
35020
35380
|
const slideCount = isEmbeddableAssetType(asset.contentType) ? asset.contentType === "pdf" ? countPdfPages(buffer) : await countPptxSlides(buffer) : 0;
|
|
35021
35381
|
result.set(asset.id, {
|
|
@@ -35029,7 +35389,7 @@ async function materializePackageAssets(assets) {
|
|
|
35029
35389
|
} catch (err) {
|
|
35030
35390
|
for (const filePath of writtenPaths) {
|
|
35031
35391
|
try {
|
|
35032
|
-
|
|
35392
|
+
import_node_fs18.default.unlinkSync(filePath);
|
|
35033
35393
|
} catch (cleanupErr) {
|
|
35034
35394
|
console.warn("[course-package] \u9644\u4EF6\u8865\u507F\u6E05\u7406\u5931\u8D25:", cleanupErr.message);
|
|
35035
35395
|
}
|
|
@@ -40031,7 +40391,7 @@ ${runtimeScripts}` : BRIDGE_SDK_CODE;
|
|
|
40031
40391
|
}
|
|
40032
40392
|
|
|
40033
40393
|
// server/routes/lessons.ts
|
|
40034
|
-
var
|
|
40394
|
+
var import_node_fs19 = __toESM(require("node:fs"), 1);
|
|
40035
40395
|
|
|
40036
40396
|
// server/services/diagnostic-service.ts
|
|
40037
40397
|
var import_crypto12 = require("crypto");
|
|
@@ -41943,8 +42303,8 @@ function registerLessonsRoutes(ctx) {
|
|
|
41943
42303
|
passed += 1;
|
|
41944
42304
|
try {
|
|
41945
42305
|
const dir = getUploadsDir();
|
|
41946
|
-
|
|
41947
|
-
|
|
42306
|
+
import_node_fs19.default.mkdirSync(dir, { recursive: true });
|
|
42307
|
+
import_node_fs19.default.accessSync(dir, import_node_fs19.default.constants.W_OK);
|
|
41948
42308
|
checks.staticResources = { status: "passed", message: "\u8BFE\u4EF6\u5B58\u50A8\u76EE\u5F55\u53EF\u5199" };
|
|
41949
42309
|
passed += 1;
|
|
41950
42310
|
} catch (err) {
|
|
@@ -41977,8 +42337,8 @@ function registerLessonsRoutes(ctx) {
|
|
|
41977
42337
|
}
|
|
41978
42338
|
|
|
41979
42339
|
// server/services/course-package-import.ts
|
|
41980
|
-
var
|
|
41981
|
-
var
|
|
42340
|
+
var import_node_fs20 = __toESM(require("node:fs"), 1);
|
|
42341
|
+
var import_node_path20 = __toESM(require("node:path"), 1);
|
|
41982
42342
|
var import_uuid18 = require("uuid");
|
|
41983
42343
|
|
|
41984
42344
|
// server/services/course-package-identity.ts
|
|
@@ -42253,11 +42613,11 @@ function findReferencedAsset(rawData, assets) {
|
|
|
42253
42613
|
function cleanupMaterializedAssets(materialized) {
|
|
42254
42614
|
const uploadsDir = getUploadsDir();
|
|
42255
42615
|
for (const asset of materialized.values()) {
|
|
42256
|
-
const fileName =
|
|
42257
|
-
const target =
|
|
42616
|
+
const fileName = import_node_path20.default.basename(asset.fileUrl);
|
|
42617
|
+
const target = import_node_path20.default.join(uploadsDir, fileName);
|
|
42258
42618
|
try {
|
|
42259
|
-
if (
|
|
42260
|
-
|
|
42619
|
+
if (import_node_fs20.default.existsSync(target)) {
|
|
42620
|
+
import_node_fs20.default.unlinkSync(target);
|
|
42261
42621
|
}
|
|
42262
42622
|
} catch (err) {
|
|
42263
42623
|
console.warn(`[course-package] \u6E05\u7406\u5B64\u513F\u9644\u4EF6\u5931\u8D25\uFF1A${target}`, err.message);
|
|
@@ -46390,6 +46750,85 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
46390
46750
|
|
|
46391
46751
|
// server/routes/assignment-hub.ts
|
|
46392
46752
|
var import_express3 = __toESM(require("express"), 1);
|
|
46753
|
+
|
|
46754
|
+
// packages/core/assignment/file-preview-policy.ts
|
|
46755
|
+
function extensionOf(fileName) {
|
|
46756
|
+
if (typeof fileName !== "string") return "";
|
|
46757
|
+
const base = fileName.split(/[\\/]/).pop() || "";
|
|
46758
|
+
const dot = base.lastIndexOf(".");
|
|
46759
|
+
if (dot <= 0 || dot === base.length - 1) return "";
|
|
46760
|
+
return base.slice(dot).toLowerCase();
|
|
46761
|
+
}
|
|
46762
|
+
var PREVIEW_CONTENT_TYPES = Object.freeze({
|
|
46763
|
+
// 文档
|
|
46764
|
+
".pdf": "application/pdf",
|
|
46765
|
+
".rtf": "application/rtf",
|
|
46766
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
46767
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
46768
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
46769
|
+
".doc": "application/msword",
|
|
46770
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
46771
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
46772
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
46773
|
+
".xls": "application/vnd.ms-excel",
|
|
46774
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
46775
|
+
// 纯文本 / 代码 / 数据 —— 宿主按文本渲染
|
|
46776
|
+
".txt": "text/plain",
|
|
46777
|
+
".md": "text/plain",
|
|
46778
|
+
".csv": "text/plain",
|
|
46779
|
+
".json": "text/plain",
|
|
46780
|
+
".xml": "text/plain",
|
|
46781
|
+
".html": "text/plain",
|
|
46782
|
+
".css": "text/plain",
|
|
46783
|
+
".py": "text/plain",
|
|
46784
|
+
".js": "text/plain",
|
|
46785
|
+
".mjs": "text/plain",
|
|
46786
|
+
".cjs": "text/plain",
|
|
46787
|
+
".ts": "text/plain",
|
|
46788
|
+
".tsx": "text/plain",
|
|
46789
|
+
".jsx": "text/plain",
|
|
46790
|
+
".java": "text/plain",
|
|
46791
|
+
".c": "text/plain",
|
|
46792
|
+
".h": "text/plain",
|
|
46793
|
+
".cpp": "text/plain",
|
|
46794
|
+
".cs": "text/plain",
|
|
46795
|
+
".go": "text/plain",
|
|
46796
|
+
".rs": "text/plain",
|
|
46797
|
+
".rb": "text/plain",
|
|
46798
|
+
".php": "text/plain",
|
|
46799
|
+
".sql": "text/plain",
|
|
46800
|
+
".ino": "text/plain",
|
|
46801
|
+
// 图片
|
|
46802
|
+
".png": "image/png",
|
|
46803
|
+
".jpg": "image/jpeg",
|
|
46804
|
+
".jpeg": "image/jpeg",
|
|
46805
|
+
".gif": "image/gif",
|
|
46806
|
+
".webp": "image/webp",
|
|
46807
|
+
".bmp": "image/bmp",
|
|
46808
|
+
".svg": "image/svg+xml",
|
|
46809
|
+
".heic": "image/heic",
|
|
46810
|
+
// 压缩包
|
|
46811
|
+
".zip": "application/zip"
|
|
46812
|
+
});
|
|
46813
|
+
var INLINE_SAFE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
46814
|
+
"application/pdf",
|
|
46815
|
+
"text/plain",
|
|
46816
|
+
"image/png",
|
|
46817
|
+
"image/jpeg",
|
|
46818
|
+
"image/gif",
|
|
46819
|
+
"image/webp",
|
|
46820
|
+
"image/bmp"
|
|
46821
|
+
]);
|
|
46822
|
+
var TEXT_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
46823
|
+
function resolveContentType(fileName) {
|
|
46824
|
+
const extension = extensionOf(fileName);
|
|
46825
|
+
return PREVIEW_CONTENT_TYPES[extension] ?? "application/octet-stream";
|
|
46826
|
+
}
|
|
46827
|
+
function isInlineSafeContentType(contentType) {
|
|
46828
|
+
return INLINE_SAFE_CONTENT_TYPES.has((contentType || "").toLowerCase());
|
|
46829
|
+
}
|
|
46830
|
+
|
|
46831
|
+
// server/routes/assignment-hub.ts
|
|
46393
46832
|
function decodeFileName(raw) {
|
|
46394
46833
|
const text = String(raw ?? "").trim();
|
|
46395
46834
|
if (!text) return "";
|
|
@@ -46569,6 +47008,19 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
46569
47008
|
);
|
|
46570
47009
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
46571
47010
|
res.setHeader("Cache-Control", "private, no-store");
|
|
47011
|
+
const wantsInline = String(req.query.disposition || "").toLowerCase() === "inline";
|
|
47012
|
+
const contentType = resolveContentType(originalName);
|
|
47013
|
+
if (wantsInline && isInlineSafeContentType(contentType)) {
|
|
47014
|
+
res.setHeader("Content-Type", contentType);
|
|
47015
|
+
res.setHeader(
|
|
47016
|
+
"Content-Security-Policy",
|
|
47017
|
+
contentType === "application/pdf" ? "sandbox allow-scripts" : "sandbox"
|
|
47018
|
+
);
|
|
47019
|
+
const safeName = encodeURIComponent(originalName).replace(/['()]/g, escape);
|
|
47020
|
+
res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${safeName}`);
|
|
47021
|
+
res.sendFile(absPath);
|
|
47022
|
+
return;
|
|
47023
|
+
}
|
|
46572
47024
|
res.download(absPath, originalName);
|
|
46573
47025
|
} catch (e) {
|
|
46574
47026
|
sendHubError(res, e);
|
|
@@ -48334,7 +48786,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
48334
48786
|
init_audit_log();
|
|
48335
48787
|
|
|
48336
48788
|
// server/routes/plugin-api-gateway.ts
|
|
48337
|
-
var
|
|
48789
|
+
var import_node_path21 = __toESM(require("node:path"), 1);
|
|
48338
48790
|
init_http_router();
|
|
48339
48791
|
init_interfaces();
|
|
48340
48792
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
|
|
@@ -48508,7 +48960,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
48508
48960
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48509
48961
|
return;
|
|
48510
48962
|
}
|
|
48511
|
-
const normalizedSubPath =
|
|
48963
|
+
const normalizedSubPath = import_node_path21.default.posix.normalize("/" + rawSubPath);
|
|
48512
48964
|
if (normalizedSubPath.includes("..")) {
|
|
48513
48965
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48514
48966
|
return;
|
|
@@ -48945,8 +49397,8 @@ init_fetch_with_retry();
|
|
|
48945
49397
|
|
|
48946
49398
|
// server/services/community-registry.ts
|
|
48947
49399
|
var import_semver7 = __toESM(require("semver"), 1);
|
|
48948
|
-
var
|
|
48949
|
-
var
|
|
49400
|
+
var import_node_fs21 = __toESM(require("node:fs"), 1);
|
|
49401
|
+
var import_node_path22 = __toESM(require("node:path"), 1);
|
|
48950
49402
|
var import_node_os3 = __toESM(require("node:os"), 1);
|
|
48951
49403
|
var import_node_crypto16 = require("node:crypto");
|
|
48952
49404
|
var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
|
|
@@ -49168,8 +49620,8 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49168
49620
|
if (!response.body) {
|
|
49169
49621
|
throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
49170
49622
|
}
|
|
49171
|
-
const zipPath =
|
|
49172
|
-
const out =
|
|
49623
|
+
const zipPath = import_node_path22.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
|
|
49624
|
+
const out = import_node_fs21.default.createWriteStream(zipPath);
|
|
49173
49625
|
let received = 0;
|
|
49174
49626
|
try {
|
|
49175
49627
|
for await (const chunk of response.body) {
|
|
@@ -49191,13 +49643,13 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49191
49643
|
} catch (err) {
|
|
49192
49644
|
out.destroy();
|
|
49193
49645
|
try {
|
|
49194
|
-
|
|
49646
|
+
import_node_fs21.default.rmSync(zipPath, { force: true });
|
|
49195
49647
|
} catch {
|
|
49196
49648
|
}
|
|
49197
49649
|
throw err;
|
|
49198
49650
|
}
|
|
49199
49651
|
if (received === 0) {
|
|
49200
|
-
|
|
49652
|
+
import_node_fs21.default.rmSync(zipPath, { force: true });
|
|
49201
49653
|
throw new Error("\u63D2\u4EF6\u5305\u4E3A\u7A7A");
|
|
49202
49654
|
}
|
|
49203
49655
|
return { zipPath, filename: resolveFilename(response, downloadUrl), bytes: received };
|
|
@@ -57121,8 +57573,10 @@ async function startServer() {
|
|
|
57121
57573
|
if (migrations.length === 0) {
|
|
57122
57574
|
throw new Error(`[Migration] FATAL: ${migrationsDir} \u4E2D\u6CA1\u6709\u8FC1\u79FB\u6587\u4EF6\uFF0C\u62D2\u7EDD\u542F\u52A8\u3002`);
|
|
57123
57575
|
}
|
|
57124
|
-
runMigrations(kernelContainer.db, migrations);
|
|
57576
|
+
runMigrations(kernelContainer.db, migrations, { strictLedger: true });
|
|
57125
57577
|
console.log(`[Migration] \u542F\u52A8\u590D\u67E5\u5B8C\u6210\uFF1A${migrationsDir}\uFF08${migrations.length} \u4E2A\u6587\u4EF6\uFF09`);
|
|
57578
|
+
assertSchemaMatchesGolden(kernelContainer.db, migrationsDir);
|
|
57579
|
+
console.log("[SchemaGolden] \u542F\u52A8\u7EC8\u68C0\u901A\u8FC7\uFF1A\u6570\u636E\u5E93\u7ED3\u6784\u8986\u76D6\u4EE3\u7801\u671F\u671B\u7684\u5168\u90E8\u8868\u4E0E\u5217\u3002");
|
|
57126
57580
|
} catch (err) {
|
|
57127
57581
|
console.error("[Migration] FATAL: \u6570\u636E\u5E93\u8FC1\u79FB\u5931\u8D25\uFF0C\u62D2\u7EDD\u5728\u672A\u5B8C\u6210\u8FC1\u79FB\u7684\u5E93\u4E0A\u63D0\u4F9B\u670D\u52A1\u3002");
|
|
57128
57582
|
console.error(err);
|