openlearn-next 0.7.4 → 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-CMN8elwQ.js → ActivityWorkspaceWidget-Dq4kjBS2.js} +1 -1
- package/dist/assets/{AdminDirectoryView-Dlz7q-xf.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-CzutdtIz.js → BatchPickerModal-CoOWloSS.js} +1 -1
- package/dist/assets/{ClassesView-RkFPpWRu.js → ClassesView-Q56Kf4eL.js} +5 -5
- package/dist/assets/{ClassroomCountdownWidget-gs42lIGW.js → ClassroomCountdownWidget-C6luhy8P.js} +1 -1
- package/dist/assets/{CloudDriveModal-C6JtK08M.js → CloudDriveModal-DLf4fbKR.js} +1 -1
- package/dist/assets/ComputerLabView-D9-Tk7yA.js +1 -0
- package/dist/assets/{CourseManagement-DNeaJcT9.js → CourseManagement-DNSfyre-.js} +1 -1
- package/dist/assets/{CourseWizardModal-vw876VJr.js → CourseWizardModal-DAueHXcl.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-CyxDIJAF.js → CoursewareHubPanel-DLjH-UzF.js} +1 -1
- package/dist/assets/{Dashboard-C1iYnfCh.js → Dashboard-qMVadkd2.js} +2 -2
- package/dist/assets/{ExportWeightModal-CTZ-O_wi.js → ExportWeightModal-Dkh7OC8U.js} +1 -1
- package/dist/assets/{HelpTour-D_q1z_MI.js → HelpTour-BLuiS8rr.js} +1 -1
- package/dist/assets/{HelpView-wyKldm_R.js → HelpView-CTF6B9ml.js} +1 -1
- package/dist/assets/{ImportLessonsModal-C09Z-qtx.js → ImportLessonsModal-Ra-OF_Vo.js} +1 -1
- package/dist/assets/{ImportModal-DqjqNeH4.js → ImportModal-H5s5MyZY.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-V97euiJF.js → InteractiveCoursewareViewer-BfHEDrCf.js} +1 -1
- package/dist/assets/InteractiveWhiteboard-KBzmiNBG.js +65 -0
- package/dist/assets/{LazyCourseware-0iFYlRGj.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-CaS1DzSS.js → LiveClassroomView-DezAlve2.js} +7 -7
- package/dist/assets/{NotificationDetailModal-4MdNF4tw.js → NotificationDetailModal-DbMMrIHQ.js} +1 -1
- package/dist/assets/{PluginView-BqTbAp30.js → PluginView-CvIr-ANj.js} +1 -1
- package/dist/assets/{ProcessLogsModal-Cfr-T9Gd.js → ProcessLogsModal-_BwkmJVw.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-xTq22vDq.js → QuizGeneratorModal-CTj2moWM.js} +1 -1
- package/dist/assets/StudentAssignmentEvalPanel-DrfZT8Lr.js +1 -0
- package/dist/assets/{StudentAssignmentView-Cy_JdPYB.js → StudentAssignmentView-D_EhblVQ.js} +1 -1
- package/dist/assets/StudentLessonView-BHG50-4M.js +1 -0
- package/dist/assets/{StudentView-CpJsG7FT.js → StudentView-DKDhnJQx.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-YZ85mv_v.js → SystemResourceLibraryModal-C5y8H5Pf.js} +1 -1
- package/dist/assets/TeacherView-CWDK3DOe.js +2 -0
- package/dist/assets/{TimetableView-D_4XmrRN.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/index.html +3 -3
- package/dist/server.cjs +538 -174
- package/package.json +6 -4
- package/scripts/assert-node-runtime.mjs +165 -0
- package/dist/assets/AssessmentCenter-CaoAI9Ke.js +0 -1
- package/dist/assets/ComputerLabView-Dr-g8FrU.js +0 -1
- package/dist/assets/InteractiveWhiteboard-COLMh_-w.js +0 -65
- package/dist/assets/LazyWhiteboard-CcoBIx6V.js +0 -2
- package/dist/assets/LessonEditorView-BWmtKwmP.js +0 -1
- package/dist/assets/StudentLessonView-B3fKejCU.js +0 -1
- package/dist/assets/TeacherView-B3zhzd5g.js +0 -2
- package/dist/assets/index-BzXADCXZ.js +0 -131
- package/dist/assets/index-C6hlh8pw.css +0 -1
- package/dist/assets/pluginParsers-PzgFzxS9.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,7 +1217,7 @@ function readProcessEnvKey(env) {
|
|
|
1214
1217
|
return normalizeKeyHex(raw);
|
|
1215
1218
|
}
|
|
1216
1219
|
function readDotEnvKey(cwd) {
|
|
1217
|
-
const envPath =
|
|
1220
|
+
const envPath = import_node_path5.default.resolve(cwd, ".env");
|
|
1218
1221
|
if (!import_node_fs4.default.existsSync(envPath)) return null;
|
|
1219
1222
|
let content = "";
|
|
1220
1223
|
try {
|
|
@@ -1227,7 +1230,7 @@ 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 {
|
|
@@ -1253,7 +1256,7 @@ 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
|
-
import_node_fs4.default.mkdirSync(
|
|
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" });
|
|
@@ -1400,13 +1403,13 @@ function encryptStoredPlaintextApiKeys(db2) {
|
|
|
1400
1403
|
}
|
|
1401
1404
|
return { updated, skipped };
|
|
1402
1405
|
}
|
|
1403
|
-
var import_node_crypto, import_node_fs4,
|
|
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
1411
|
import_node_fs4 = __toESM(require("node:fs"), 1);
|
|
1409
|
-
|
|
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,10 +1924,10 @@ 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 {
|
|
@@ -1934,7 +1937,7 @@ function realPathOrNull(target) {
|
|
|
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();
|
|
@@ -1949,7 +1952,7 @@ 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 =
|
|
1955
|
+
const zipPath = path9.join(os2.tmpdir(), `${prefix}${(0, import_uuid6.v7)()}.zip`);
|
|
1953
1956
|
fs7.writeFileSync(zipPath, buffer);
|
|
1954
1957
|
return zipPath;
|
|
1955
1958
|
}
|
|
@@ -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 =
|
|
2248
|
+
const tsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.ts");
|
|
2246
2249
|
if (fs7.existsSync(tsPath)) {
|
|
2247
|
-
allowRoot(
|
|
2250
|
+
allowRoot(path9.dirname(tsPath));
|
|
2248
2251
|
return { path: tsPath };
|
|
2249
2252
|
}
|
|
2250
|
-
const jsPath =
|
|
2253
|
+
const jsPath = path9.resolve(process.cwd(), "packages/core/di/interfaces.js");
|
|
2251
2254
|
if (fs7.existsSync(jsPath)) {
|
|
2252
|
-
allowRoot(
|
|
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: [
|
|
@@ -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
|
-
fs7.mkdirSync(
|
|
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) {
|
|
@@ -2373,11 +2376,11 @@ 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 {
|
|
@@ -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, fs7, os2,
|
|
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 {
|
|
@@ -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 import_node_fs5,
|
|
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
2526
|
import_node_fs5 = __toESM(require("node:fs"), 1);
|
|
2524
|
-
|
|
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() {
|
|
@@ -2640,21 +2643,21 @@ function consoleStream() {
|
|
|
2640
2643
|
function createLogger(component) {
|
|
2641
2644
|
return logger.child({ component });
|
|
2642
2645
|
}
|
|
2643
|
-
var import_pino, import_node_fs6,
|
|
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
2651
|
import_node_fs6 = __toESM(require("node:fs"), 1);
|
|
2649
|
-
|
|
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
|
];
|
|
@@ -2787,7 +2790,7 @@ 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
2795
|
if (!import_node_fs7.default.existsSync(logDir2)) import_node_fs7.default.mkdirSync(logDir2, { recursive: true });
|
|
2793
2796
|
import_node_fs7.default.accessSync(logDir2, import_node_fs7.default.constants.W_OK);
|
|
@@ -2795,7 +2798,7 @@ function buildAuditLogger() {
|
|
|
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 import_node_fs7,
|
|
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
2830
|
import_node_fs7 = __toESM(require("node:fs"), 1);
|
|
2828
|
-
|
|
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) => {
|
|
@@ -5740,7 +5790,7 @@ 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
|
);
|
|
@@ -5754,7 +5804,7 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
|
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 {
|
|
@@ -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,11 +5843,11 @@ 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
5852
|
const found = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
|
|
5803
5853
|
if (!found) {
|
|
@@ -5805,7 +5855,7 @@ function resolveNetPreloadPath(options = {}) {
|
|
|
5805
5855
|
}
|
|
5806
5856
|
return found;
|
|
5807
5857
|
}
|
|
5808
|
-
var import_node_child_process, import_node_crypto7, import_node_fs9, import_node_os,
|
|
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";
|
|
@@ -5813,13 +5863,13 @@ var init_child_spawn = __esm({
|
|
|
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",
|
|
@@ -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();
|
|
@@ -11135,8 +11372,8 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11135
11372
|
const starts = options.startDirs ? [...options.startDirs] : [];
|
|
11136
11373
|
if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
|
|
11137
11374
|
try {
|
|
11138
|
-
if (!options.startDirs && typeof
|
|
11139
|
-
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)));
|
|
11140
11377
|
}
|
|
11141
11378
|
} catch {
|
|
11142
11379
|
}
|
|
@@ -11164,7 +11401,7 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11164
11401
|
|
|
11165
11402
|
// packages/core/db/schema-golden.ts
|
|
11166
11403
|
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
11167
|
-
var
|
|
11404
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
11168
11405
|
var SCHEMA_GOLDEN_FILENAME = "schema-golden.json";
|
|
11169
11406
|
function extractSchemaSnapshot(db2) {
|
|
11170
11407
|
const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
|
|
@@ -11200,7 +11437,7 @@ function formatSchemaGoldenDrifts(drifts) {
|
|
|
11200
11437
|
).join("\n");
|
|
11201
11438
|
}
|
|
11202
11439
|
function loadSchemaGolden(migrationsDir) {
|
|
11203
|
-
const filePath =
|
|
11440
|
+
const filePath = import_node_path4.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
|
|
11204
11441
|
if (!import_node_fs3.default.existsSync(filePath)) {
|
|
11205
11442
|
throw new Error(
|
|
11206
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`
|
|
@@ -11745,7 +11982,7 @@ function purgeAllExpiredData(db2, options) {
|
|
|
11745
11982
|
|
|
11746
11983
|
// packages/core/db/index.ts
|
|
11747
11984
|
init_api_key_crypto();
|
|
11748
|
-
var
|
|
11985
|
+
var import_meta3 = {};
|
|
11749
11986
|
var BCRYPT_ROUNDS = 10;
|
|
11750
11987
|
function hashPassword(pwd) {
|
|
11751
11988
|
return import_bcryptjs.default.hashSync(pwd, BCRYPT_ROUNDS);
|
|
@@ -11820,8 +12057,8 @@ if (process.env.VITEST) {
|
|
|
11820
12057
|
dbPath = import_path2.default.join(__dirname, "educational_os.db");
|
|
11821
12058
|
} else {
|
|
11822
12059
|
try {
|
|
11823
|
-
if (typeof
|
|
11824
|
-
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);
|
|
11825
12062
|
const __dir = import_path2.default.dirname(__filename2);
|
|
11826
12063
|
dbPath = import_path2.default.join(__dir, "educational_os.db");
|
|
11827
12064
|
} else {
|
|
@@ -14149,10 +14386,10 @@ function parseScoreFields(fields) {
|
|
|
14149
14386
|
if (typeof fields !== "string" || !fields.trim()) return [];
|
|
14150
14387
|
return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
|
|
14151
14388
|
}
|
|
14152
|
-
function getNested(raw,
|
|
14389
|
+
function getNested(raw, path47) {
|
|
14153
14390
|
if (raw === null || raw === void 0) return void 0;
|
|
14154
|
-
if (!
|
|
14155
|
-
const parts = String(
|
|
14391
|
+
if (!path47) return void 0;
|
|
14392
|
+
const parts = String(path47).split(".").map((p) => p.trim()).filter(Boolean);
|
|
14156
14393
|
let cursor = raw;
|
|
14157
14394
|
for (let i = 0; i < parts.length; i += 1) {
|
|
14158
14395
|
if (cursor === null || cursor === void 0) return void 0;
|
|
@@ -18152,7 +18389,7 @@ var import_path11 = __toESM(require("path"), 1);
|
|
|
18152
18389
|
// packages/core/plugin-host/base.ts
|
|
18153
18390
|
var import_path5 = __toESM(require("path"), 1);
|
|
18154
18391
|
var import_express = __toESM(require("express"), 1);
|
|
18155
|
-
var
|
|
18392
|
+
var import_node_module2 = require("node:module");
|
|
18156
18393
|
var import_node_url = require("node:url");
|
|
18157
18394
|
|
|
18158
18395
|
// packages/core/plugin-host/resource-tracker.ts
|
|
@@ -18438,7 +18675,7 @@ var ContributionRegistry = class {
|
|
|
18438
18675
|
// packages/core/plugin-host/base.ts
|
|
18439
18676
|
init_types();
|
|
18440
18677
|
init_errors2();
|
|
18441
|
-
var
|
|
18678
|
+
var import_meta4 = {};
|
|
18442
18679
|
var VALID_TRANSITIONS = {
|
|
18443
18680
|
["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
|
|
18444
18681
|
["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
|
|
@@ -18473,12 +18710,12 @@ function resolveHostSdkDir() {
|
|
|
18473
18710
|
const bases = [];
|
|
18474
18711
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
18475
18712
|
try {
|
|
18476
|
-
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)));
|
|
18477
18714
|
} catch {
|
|
18478
18715
|
}
|
|
18479
18716
|
for (const base of bases) {
|
|
18480
18717
|
try {
|
|
18481
|
-
const req = (0,
|
|
18718
|
+
const req = (0, import_node_module2.createRequire)(import_path5.default.join(base, "__openlearn_resolve__.js"));
|
|
18482
18719
|
return import_path5.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
|
|
18483
18720
|
} catch {
|
|
18484
18721
|
}
|
|
@@ -18743,10 +18980,10 @@ var import_semver2 = __toESM(require("semver"), 1);
|
|
|
18743
18980
|
init_types();
|
|
18744
18981
|
|
|
18745
18982
|
// packages/core/plugin-host/execution-policy.ts
|
|
18746
|
-
var
|
|
18983
|
+
var import_node_path9 = __toESM(require("node:path"), 1);
|
|
18747
18984
|
var import_node_url2 = require("node:url");
|
|
18748
18985
|
init_types();
|
|
18749
|
-
var
|
|
18986
|
+
var import_meta5 = {};
|
|
18750
18987
|
function trustedInlineAllowlist(env = process.env) {
|
|
18751
18988
|
return new Set(
|
|
18752
18989
|
(env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
@@ -18754,23 +18991,23 @@ function trustedInlineAllowlist(env = process.env) {
|
|
|
18754
18991
|
}
|
|
18755
18992
|
function builtinPluginsDir() {
|
|
18756
18993
|
try {
|
|
18757
|
-
const url =
|
|
18994
|
+
const url = import_meta5.url;
|
|
18758
18995
|
if (typeof url === "string" && url.startsWith("file:")) {
|
|
18759
|
-
return
|
|
18996
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname((0, import_node_url2.fileURLToPath)(url)), "../../plugins");
|
|
18760
18997
|
}
|
|
18761
18998
|
} catch {
|
|
18762
18999
|
}
|
|
18763
19000
|
if (typeof __dirname !== "undefined") {
|
|
18764
|
-
return
|
|
19001
|
+
return import_node_path9.default.resolve(__dirname, "../packages/plugins");
|
|
18765
19002
|
}
|
|
18766
|
-
return
|
|
19003
|
+
return import_node_path9.default.resolve(process.cwd(), "packages/plugins");
|
|
18767
19004
|
}
|
|
18768
19005
|
function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir()) {
|
|
18769
19006
|
if (!sourcePath) return false;
|
|
18770
|
-
const root =
|
|
18771
|
-
const resolved =
|
|
18772
|
-
const relative2 =
|
|
18773
|
-
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);
|
|
18774
19011
|
}
|
|
18775
19012
|
function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
|
|
18776
19013
|
if (isInsideBuiltinPluginsDir(sourcePath)) return true;
|
|
@@ -19733,6 +19970,37 @@ var PluginHostCore = class _PluginHostCore extends PluginHostBase {
|
|
|
19733
19970
|
console.warn(`[PluginHost] Failed to revoke stage guards for "${pluginId}":`, e);
|
|
19734
19971
|
}
|
|
19735
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
|
+
}
|
|
19736
20004
|
setPluginState(pluginId, state, opts = {}) {
|
|
19737
20005
|
this.pluginStates.set(pluginId, state);
|
|
19738
20006
|
if (!opts.persistDb) return;
|
|
@@ -19830,7 +20098,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19830
20098
|
/**
|
|
19831
20099
|
* 判断指定插件的某个请求路径是否为流式路由 (SSE)
|
|
19832
20100
|
*/
|
|
19833
|
-
isStreamRoute(pluginIdOrManifestId, method,
|
|
20101
|
+
isStreamRoute(pluginIdOrManifestId, method, path47) {
|
|
19834
20102
|
const upperMethod = method.toUpperCase();
|
|
19835
20103
|
const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
|
|
19836
20104
|
const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
|
|
@@ -19839,7 +20107,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19839
20107
|
for (const r of manifest.api.routes) {
|
|
19840
20108
|
if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
|
|
19841
20109
|
const compiled = compileRoutePattern(r.path);
|
|
19842
|
-
if (compiled.regex.test(
|
|
20110
|
+
if (compiled.regex.test(path47)) {
|
|
19843
20111
|
return true;
|
|
19844
20112
|
}
|
|
19845
20113
|
}
|
|
@@ -19847,14 +20115,14 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19847
20115
|
}
|
|
19848
20116
|
if (!instance) return false;
|
|
19849
20117
|
if (instance.context?.http) {
|
|
19850
|
-
return instance.context.http.isStream(upperMethod,
|
|
20118
|
+
return instance.context.http.isStream(upperMethod, path47);
|
|
19851
20119
|
}
|
|
19852
20120
|
if (instance.workerRef) {
|
|
19853
20121
|
const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
|
|
19854
20122
|
for (const r of routes) {
|
|
19855
20123
|
if (r.method === upperMethod && r.isStream) {
|
|
19856
20124
|
const compiled = compileRoutePattern(r.pattern);
|
|
19857
|
-
if (compiled.regex.test(
|
|
20125
|
+
if (compiled.regex.test(path47)) {
|
|
19858
20126
|
return true;
|
|
19859
20127
|
}
|
|
19860
20128
|
}
|
|
@@ -19928,7 +20196,7 @@ init_context_builder();
|
|
|
19928
20196
|
|
|
19929
20197
|
// packages/core/plugin-host/declarative-migrator.ts
|
|
19930
20198
|
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
19931
|
-
var
|
|
20199
|
+
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
19932
20200
|
init_errors2();
|
|
19933
20201
|
init_plugin_tables();
|
|
19934
20202
|
function ensurePluginDeclaredMigrationsTable(db2) {
|
|
@@ -19944,7 +20212,7 @@ function ensurePluginDeclaredMigrationsTable(db2) {
|
|
|
19944
20212
|
`);
|
|
19945
20213
|
}
|
|
19946
20214
|
function parseMigrationFileName(fileName) {
|
|
19947
|
-
const baseName =
|
|
20215
|
+
const baseName = import_node_path10.default.basename(fileName, ".sql");
|
|
19948
20216
|
const match = baseName.match(/^(\d+)[_-]?(.*)$/);
|
|
19949
20217
|
if (match) {
|
|
19950
20218
|
const version = parseInt(match[1], 10);
|
|
@@ -19967,7 +20235,7 @@ function loadPluginMigrations(migrationsDir) {
|
|
|
19967
20235
|
return a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
|
|
19968
20236
|
});
|
|
19969
20237
|
return files.map((file) => {
|
|
19970
|
-
const fullPath =
|
|
20238
|
+
const fullPath = import_node_path10.default.join(migrationsDir, file);
|
|
19971
20239
|
const content = import_node_fs8.default.readFileSync(fullPath, "utf8");
|
|
19972
20240
|
const { version, baseName } = parseMigrationFileName(file);
|
|
19973
20241
|
const parsed = parseMigrationSql(baseName, content);
|
|
@@ -20609,6 +20877,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20609
20877
|
this.resourceTracker.disposeAll(pluginId);
|
|
20610
20878
|
this.pluginInstances.delete(pluginId);
|
|
20611
20879
|
this.revokePluginContributions(pluginId);
|
|
20880
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20612
20881
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20613
20882
|
if (this._hotReloadController) {
|
|
20614
20883
|
this._hotReloadController.unregisterPlugin(pluginId);
|
|
@@ -20627,6 +20896,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20627
20896
|
this.resourceTracker.disposeAll(pluginId);
|
|
20628
20897
|
this.pluginInstances.delete(pluginId);
|
|
20629
20898
|
this.revokePluginContributions(pluginId);
|
|
20899
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20630
20900
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20631
20901
|
if (actorId) {
|
|
20632
20902
|
await this.revokeCapabilities(actorId);
|
|
@@ -20662,6 +20932,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20662
20932
|
this.resourceTracker.disposeAll(pluginId);
|
|
20663
20933
|
this.pluginInstances.delete(pluginId);
|
|
20664
20934
|
this.revokePluginContributions(pluginId);
|
|
20935
|
+
await this.revokePluginQuestionTypes(pluginId, instance?.manifest?.id ?? pluginId);
|
|
20665
20936
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20666
20937
|
if (actorId) {
|
|
20667
20938
|
await this.revokeCapabilities(actorId);
|
|
@@ -20725,26 +20996,26 @@ var import_semver3 = __toESM(require("semver"), 1);
|
|
|
20725
20996
|
|
|
20726
20997
|
// packages/core/version.ts
|
|
20727
20998
|
var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
20728
|
-
var
|
|
20999
|
+
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
20729
21000
|
var import_node_url5 = require("node:url");
|
|
20730
|
-
var
|
|
20731
|
-
var FALLBACK_VERSION = "0.7.
|
|
21001
|
+
var import_meta7 = {};
|
|
21002
|
+
var FALLBACK_VERSION = "0.7.5";
|
|
20732
21003
|
function readPlatformVersion() {
|
|
20733
21004
|
const bases = [];
|
|
20734
21005
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
20735
21006
|
try {
|
|
20736
|
-
bases.push(
|
|
21007
|
+
bases.push(import_node_path13.default.dirname((0, import_node_url5.fileURLToPath)(import_meta7.url)));
|
|
20737
21008
|
} catch {
|
|
20738
21009
|
}
|
|
20739
21010
|
for (const base of bases) {
|
|
20740
21011
|
let dir = base;
|
|
20741
21012
|
for (; ; ) {
|
|
20742
21013
|
try {
|
|
20743
|
-
const pkg = JSON.parse(import_node_fs11.default.readFileSync(
|
|
21014
|
+
const pkg = JSON.parse(import_node_fs11.default.readFileSync(import_node_path13.default.join(dir, "package.json"), "utf-8"));
|
|
20744
21015
|
if (pkg.name === "openlearn-next" && typeof pkg.version === "string") return pkg.version;
|
|
20745
21016
|
} catch {
|
|
20746
21017
|
}
|
|
20747
|
-
const parent =
|
|
21018
|
+
const parent = import_node_path13.default.dirname(dir);
|
|
20748
21019
|
if (parent === dir) break;
|
|
20749
21020
|
dir = parent;
|
|
20750
21021
|
}
|
|
@@ -21464,7 +21735,7 @@ init_audit_log();
|
|
|
21464
21735
|
// packages/core/plugin-host/dependency-install.ts
|
|
21465
21736
|
var import_node_child_process2 = require("node:child_process");
|
|
21466
21737
|
var import_node_fs12 = __toESM(require("node:fs"), 1);
|
|
21467
|
-
var
|
|
21738
|
+
var import_node_path14 = __toESM(require("node:path"), 1);
|
|
21468
21739
|
var DependencyInstallError = class extends Error {
|
|
21469
21740
|
constructor(message, pluginId, cause) {
|
|
21470
21741
|
super(message);
|
|
@@ -21499,7 +21770,7 @@ function resolveRegistry() {
|
|
|
21499
21770
|
function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
|
|
21500
21771
|
const names = dependencyNames(dependencies);
|
|
21501
21772
|
if (names.length === 0) return;
|
|
21502
|
-
const lockPath =
|
|
21773
|
+
const lockPath = import_node_path14.default.join(pluginDir, "package-lock.json");
|
|
21503
21774
|
if (!import_node_fs12.default.existsSync(lockPath)) {
|
|
21504
21775
|
throw new DependencyInstallError(
|
|
21505
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(", ")}`,
|
|
@@ -21581,7 +21852,7 @@ function installPluginDependencies(pluginDir, opts, runNpm = import_node_child_p
|
|
|
21581
21852
|
const { pluginId, dependencies, operation = "install" } = opts;
|
|
21582
21853
|
const depMap = toNpmDependencyMap(dependencies);
|
|
21583
21854
|
if (Object.keys(depMap).length === 0) return;
|
|
21584
|
-
const pkgJsonPath =
|
|
21855
|
+
const pkgJsonPath = import_node_path14.default.join(pluginDir, "package.json");
|
|
21585
21856
|
import_node_fs12.default.writeFileSync(
|
|
21586
21857
|
pkgJsonPath,
|
|
21587
21858
|
JSON.stringify({ name: pluginId, version: "0.0.0", dependencies: depMap }, null, 2),
|
|
@@ -22411,6 +22682,7 @@ var PluginHostUninstall = class extends PluginHostInstall {
|
|
|
22411
22682
|
return pluginId;
|
|
22412
22683
|
}
|
|
22413
22684
|
})();
|
|
22685
|
+
await this.revokePluginQuestionTypes(pluginId, manifestId);
|
|
22414
22686
|
if (state === "uninstalled" /* UNINSTALLED */ && !row) {
|
|
22415
22687
|
this.pluginStates.delete(pluginId);
|
|
22416
22688
|
return {
|
|
@@ -32668,7 +32940,7 @@ ${formatEnvIssues(warnings)}`);
|
|
|
32668
32940
|
|
|
32669
32941
|
// server/security/file-permissions.ts
|
|
32670
32942
|
var import_node_fs14 = __toESM(require("node:fs"), 1);
|
|
32671
|
-
var
|
|
32943
|
+
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
32672
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";
|
|
32673
32945
|
function modeOf(p) {
|
|
32674
32946
|
try {
|
|
@@ -32678,13 +32950,13 @@ function modeOf(p) {
|
|
|
32678
32950
|
}
|
|
32679
32951
|
}
|
|
32680
32952
|
function defaultPaths() {
|
|
32681
|
-
const dbPath2 = process.env.OPENLEARN_DB_PATH ?
|
|
32682
|
-
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");
|
|
32683
32955
|
return {
|
|
32684
|
-
envPath:
|
|
32956
|
+
envPath: import_node_path15.default.resolve(process.cwd(), ".env"),
|
|
32685
32957
|
dbPath: dbPath2,
|
|
32686
32958
|
backupDir,
|
|
32687
|
-
encryptionKeyPath:
|
|
32959
|
+
encryptionKeyPath: import_node_path15.default.resolve(process.cwd(), "storage", "encryption-key")
|
|
32688
32960
|
};
|
|
32689
32961
|
}
|
|
32690
32962
|
function checkSensitiveFilePermissions(options = {}) {
|
|
@@ -32719,7 +32991,7 @@ function checkSensitiveFilePermissions(options = {}) {
|
|
|
32719
32991
|
if (scanned >= maxBackupFiles) break;
|
|
32720
32992
|
if (!entry.endsWith(".db")) continue;
|
|
32721
32993
|
scanned++;
|
|
32722
|
-
checkFile(
|
|
32994
|
+
checkFile(import_node_path15.default.join(backupDir, entry), "600", "backup-file");
|
|
32723
32995
|
}
|
|
32724
32996
|
}
|
|
32725
32997
|
return issues;
|
|
@@ -33620,7 +33892,7 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
33620
33892
|
|
|
33621
33893
|
// server/backup-scheduler.ts
|
|
33622
33894
|
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
33623
|
-
var
|
|
33895
|
+
var import_node_path16 = __toESM(require("node:path"), 1);
|
|
33624
33896
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
33625
33897
|
var DAY_MS2 = 24 * HOUR_MS;
|
|
33626
33898
|
var WAL_FULL_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
@@ -33637,8 +33909,8 @@ function shouldFullCheckpoint(walBytes, threshold = WAL_FULL_THRESHOLD_BYTES) {
|
|
|
33637
33909
|
}
|
|
33638
33910
|
function resolveDbPath(explicit) {
|
|
33639
33911
|
if (explicit) return explicit;
|
|
33640
|
-
if (process.env.OPENLEARN_DB_PATH) return
|
|
33641
|
-
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");
|
|
33642
33914
|
}
|
|
33643
33915
|
function startBackupScheduler(deps) {
|
|
33644
33916
|
const noop = { stop: () => {
|
|
@@ -33757,9 +34029,9 @@ function isWhitelisted(route, whitelist) {
|
|
|
33757
34029
|
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
33758
34030
|
);
|
|
33759
34031
|
}
|
|
33760
|
-
function normalizePath2(
|
|
33761
|
-
if (!
|
|
33762
|
-
return
|
|
34032
|
+
function normalizePath2(path47) {
|
|
34033
|
+
if (!path47) return "/";
|
|
34034
|
+
return path47.endsWith("/") && path47.length > 1 ? path47.slice(0, -1) : path47;
|
|
33763
34035
|
}
|
|
33764
34036
|
function auditRouteScopes(app, whitelist = []) {
|
|
33765
34037
|
const declared = [];
|
|
@@ -33937,11 +34209,11 @@ function enforcePasswordChanged(req, res, next) {
|
|
|
33937
34209
|
if (isPasswordChangeGateSkipped()) {
|
|
33938
34210
|
return next();
|
|
33939
34211
|
}
|
|
33940
|
-
const
|
|
33941
|
-
if (!
|
|
34212
|
+
const path47 = req.path || "";
|
|
34213
|
+
if (!path47.startsWith("/api/")) {
|
|
33942
34214
|
return next();
|
|
33943
34215
|
}
|
|
33944
|
-
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(
|
|
34216
|
+
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path47)) {
|
|
33945
34217
|
return next();
|
|
33946
34218
|
}
|
|
33947
34219
|
const token = getCookieToken(req);
|
|
@@ -34016,8 +34288,8 @@ function csrfGuard(req, res, next) {
|
|
|
34016
34288
|
next();
|
|
34017
34289
|
return;
|
|
34018
34290
|
}
|
|
34019
|
-
const
|
|
34020
|
-
if (EXEMPT_PREFIXES.some((p) =>
|
|
34291
|
+
const path48 = req.path;
|
|
34292
|
+
if (EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
34021
34293
|
next();
|
|
34022
34294
|
return;
|
|
34023
34295
|
}
|
|
@@ -34027,10 +34299,10 @@ function csrfGuard(req, res, next) {
|
|
|
34027
34299
|
const origin = req.headers.origin;
|
|
34028
34300
|
const referer = req.headers.referer;
|
|
34029
34301
|
const host = req.headers.host;
|
|
34030
|
-
const
|
|
34302
|
+
const path47 = req.path;
|
|
34031
34303
|
if (typeof origin === "string" && origin !== "") {
|
|
34032
34304
|
if (origin === "null") {
|
|
34033
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34305
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34034
34306
|
res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
|
|
34035
34307
|
return;
|
|
34036
34308
|
}
|
|
@@ -34038,7 +34310,7 @@ function csrfGuard(req, res, next) {
|
|
|
34038
34310
|
try {
|
|
34039
34311
|
const originHost = new URL(origin).host;
|
|
34040
34312
|
if (host && originHost.toLowerCase() !== host.toLowerCase()) {
|
|
34041
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34313
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34042
34314
|
res.status(403).json({
|
|
34043
34315
|
success: false,
|
|
34044
34316
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34056,7 +34328,7 @@ function csrfGuard(req, res, next) {
|
|
|
34056
34328
|
try {
|
|
34057
34329
|
const refererHost = new URL(referer).host;
|
|
34058
34330
|
if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
|
|
34059
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34331
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34060
34332
|
res.status(403).json({
|
|
34061
34333
|
success: false,
|
|
34062
34334
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34109,8 +34381,8 @@ function parseSentryDsn(dsn) {
|
|
|
34109
34381
|
const projectId = url.pathname.split("/").filter(Boolean).pop();
|
|
34110
34382
|
if (!projectId || !/^\d+$/.test(projectId)) return null;
|
|
34111
34383
|
const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
|
|
34112
|
-
const
|
|
34113
|
-
const envelopeUrl = `${url.protocol}//${url.host}${
|
|
34384
|
+
const path47 = prefix ? `/${prefix}` : "";
|
|
34385
|
+
const envelopeUrl = `${url.protocol}//${url.host}${path47}/api/${projectId}/envelope/`;
|
|
34114
34386
|
return { envelopeUrl, publicKey };
|
|
34115
34387
|
} catch {
|
|
34116
34388
|
return null;
|
|
@@ -34551,14 +34823,14 @@ function createSocketIoCors(options) {
|
|
|
34551
34823
|
|
|
34552
34824
|
// server/routes/health.ts
|
|
34553
34825
|
var import_node_fs17 = __toESM(require("node:fs"), 1);
|
|
34554
|
-
var
|
|
34826
|
+
var import_node_path18 = __toESM(require("node:path"), 1);
|
|
34555
34827
|
|
|
34556
34828
|
// packages/core/observability/health.ts
|
|
34557
34829
|
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
34558
|
-
var
|
|
34830
|
+
var import_node_path17 = __toESM(require("node:path"), 1);
|
|
34559
34831
|
var import_node_crypto13 = require("node:crypto");
|
|
34560
34832
|
function probeStorageWritable(dir) {
|
|
34561
|
-
const probe =
|
|
34833
|
+
const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
|
|
34562
34834
|
try {
|
|
34563
34835
|
import_node_fs16.default.mkdirSync(dir, { recursive: true });
|
|
34564
34836
|
import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
|
|
@@ -34575,14 +34847,14 @@ function probeStorageWritable(dir) {
|
|
|
34575
34847
|
function listMigrationNames(migrationsDir) {
|
|
34576
34848
|
try {
|
|
34577
34849
|
if (!import_node_fs16.default.existsSync(migrationsDir)) return null;
|
|
34578
|
-
const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) =>
|
|
34850
|
+
const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path17.default.basename(file, ".sql"));
|
|
34579
34851
|
return names.length > 0 ? names : null;
|
|
34580
34852
|
} catch {
|
|
34581
34853
|
return null;
|
|
34582
34854
|
}
|
|
34583
34855
|
}
|
|
34584
34856
|
function migrationRecordName(name) {
|
|
34585
|
-
return
|
|
34857
|
+
return import_node_path17.default.basename(name.replace(/\r/g, "").trim(), ".sql");
|
|
34586
34858
|
}
|
|
34587
34859
|
function migrationsComplete(expected, applied) {
|
|
34588
34860
|
if (!expected) return { ok: false, reason: "migrations" };
|
|
@@ -34682,7 +34954,7 @@ function newestBackupAgeSeconds(backupDir, now) {
|
|
|
34682
34954
|
let newestMs = Number.NEGATIVE_INFINITY;
|
|
34683
34955
|
for (const name of names) {
|
|
34684
34956
|
try {
|
|
34685
|
-
const stat = import_node_fs17.default.statSync(
|
|
34957
|
+
const stat = import_node_fs17.default.statSync(import_node_path18.default.join(backupDir, name));
|
|
34686
34958
|
if (!stat.isFile()) continue;
|
|
34687
34959
|
if (stat.mtimeMs > newestMs) newestMs = stat.mtimeMs;
|
|
34688
34960
|
} catch {
|
|
@@ -34714,7 +34986,7 @@ function defaultCountEvents() {
|
|
|
34714
34986
|
function registerHealthRoutes(app, options) {
|
|
34715
34987
|
const startTime = options?.startTime ?? Date.now();
|
|
34716
34988
|
const platformVersion = options?.platformVersion ?? PLATFORM_VERSION;
|
|
34717
|
-
const migrationsDir = options?.migrationsDir ??
|
|
34989
|
+
const migrationsDir = options?.migrationsDir ?? import_node_path18.default.join(process.cwd(), "migrations");
|
|
34718
34990
|
const storageDir = options?.storageDir ?? getStorageDir();
|
|
34719
34991
|
const backupDir = options?.backupDir ?? getBackupsDir();
|
|
34720
34992
|
const countEvents = options?.countEvents ?? defaultCountEvents;
|
|
@@ -34835,7 +35107,7 @@ var import_zod2 = require("zod");
|
|
|
34835
35107
|
|
|
34836
35108
|
// server/services/course-package-assets.ts
|
|
34837
35109
|
var import_node_fs18 = __toESM(require("node:fs"), 1);
|
|
34838
|
-
var
|
|
35110
|
+
var import_node_path19 = __toESM(require("node:path"), 1);
|
|
34839
35111
|
var import_node_crypto14 = __toESM(require("node:crypto"), 1);
|
|
34840
35112
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
34841
35113
|
var import_jspdf = require("jspdf");
|
|
@@ -35102,7 +35374,7 @@ async function materializePackageAssets(assets) {
|
|
|
35102
35374
|
throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
|
|
35103
35375
|
}
|
|
35104
35376
|
const uniqueName = `${Date.now()}-${import_node_crypto14.default.randomBytes(4).toString("hex")}${ext}`;
|
|
35105
|
-
const filePath =
|
|
35377
|
+
const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
|
|
35106
35378
|
import_node_fs18.default.writeFileSync(filePath, buffer);
|
|
35107
35379
|
writtenPaths.push(filePath);
|
|
35108
35380
|
const slideCount = isEmbeddableAssetType(asset.contentType) ? asset.contentType === "pdf" ? countPdfPages(buffer) : await countPptxSlides(buffer) : 0;
|
|
@@ -42066,7 +42338,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
42066
42338
|
|
|
42067
42339
|
// server/services/course-package-import.ts
|
|
42068
42340
|
var import_node_fs20 = __toESM(require("node:fs"), 1);
|
|
42069
|
-
var
|
|
42341
|
+
var import_node_path20 = __toESM(require("node:path"), 1);
|
|
42070
42342
|
var import_uuid18 = require("uuid");
|
|
42071
42343
|
|
|
42072
42344
|
// server/services/course-package-identity.ts
|
|
@@ -42341,8 +42613,8 @@ function findReferencedAsset(rawData, assets) {
|
|
|
42341
42613
|
function cleanupMaterializedAssets(materialized) {
|
|
42342
42614
|
const uploadsDir = getUploadsDir();
|
|
42343
42615
|
for (const asset of materialized.values()) {
|
|
42344
|
-
const fileName =
|
|
42345
|
-
const target =
|
|
42616
|
+
const fileName = import_node_path20.default.basename(asset.fileUrl);
|
|
42617
|
+
const target = import_node_path20.default.join(uploadsDir, fileName);
|
|
42346
42618
|
try {
|
|
42347
42619
|
if (import_node_fs20.default.existsSync(target)) {
|
|
42348
42620
|
import_node_fs20.default.unlinkSync(target);
|
|
@@ -46478,6 +46750,85 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
46478
46750
|
|
|
46479
46751
|
// server/routes/assignment-hub.ts
|
|
46480
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
|
|
46481
46832
|
function decodeFileName(raw) {
|
|
46482
46833
|
const text = String(raw ?? "").trim();
|
|
46483
46834
|
if (!text) return "";
|
|
@@ -46657,6 +47008,19 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
46657
47008
|
);
|
|
46658
47009
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
46659
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
|
+
}
|
|
46660
47024
|
res.download(absPath, originalName);
|
|
46661
47025
|
} catch (e) {
|
|
46662
47026
|
sendHubError(res, e);
|
|
@@ -48422,7 +48786,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
48422
48786
|
init_audit_log();
|
|
48423
48787
|
|
|
48424
48788
|
// server/routes/plugin-api-gateway.ts
|
|
48425
|
-
var
|
|
48789
|
+
var import_node_path21 = __toESM(require("node:path"), 1);
|
|
48426
48790
|
init_http_router();
|
|
48427
48791
|
init_interfaces();
|
|
48428
48792
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
|
|
@@ -48596,7 +48960,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
48596
48960
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48597
48961
|
return;
|
|
48598
48962
|
}
|
|
48599
|
-
const normalizedSubPath =
|
|
48963
|
+
const normalizedSubPath = import_node_path21.default.posix.normalize("/" + rawSubPath);
|
|
48600
48964
|
if (normalizedSubPath.includes("..")) {
|
|
48601
48965
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48602
48966
|
return;
|
|
@@ -49034,7 +49398,7 @@ init_fetch_with_retry();
|
|
|
49034
49398
|
// server/services/community-registry.ts
|
|
49035
49399
|
var import_semver7 = __toESM(require("semver"), 1);
|
|
49036
49400
|
var import_node_fs21 = __toESM(require("node:fs"), 1);
|
|
49037
|
-
var
|
|
49401
|
+
var import_node_path22 = __toESM(require("node:path"), 1);
|
|
49038
49402
|
var import_node_os3 = __toESM(require("node:os"), 1);
|
|
49039
49403
|
var import_node_crypto16 = require("node:crypto");
|
|
49040
49404
|
var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
|
|
@@ -49256,7 +49620,7 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49256
49620
|
if (!response.body) {
|
|
49257
49621
|
throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
49258
49622
|
}
|
|
49259
|
-
const zipPath =
|
|
49623
|
+
const zipPath = import_node_path22.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
|
|
49260
49624
|
const out = import_node_fs21.default.createWriteStream(zipPath);
|
|
49261
49625
|
let received = 0;
|
|
49262
49626
|
try {
|