openlearn-next 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/cli-doctor.mjs +18 -5
- package/cli.mjs +49 -0
- package/dist/assets/{ActivityWorkspaceWidget-CMN8elwQ.js → ActivityWorkspaceWidget-0DUqiqFK.js} +1 -1
- package/dist/assets/{AdminDirectoryView-Dlz7q-xf.js → AdminDirectoryView-DL9zZi-_.js} +1 -1
- package/dist/assets/AssessmentCenter-B2JzXBGe.js +1 -0
- package/dist/assets/AssignmentFilePreview-Bk751Blz.js +3 -0
- package/dist/assets/{BatchPickerModal-CzutdtIz.js → BatchPickerModal-9ekXDUUj.js} +1 -1
- package/dist/assets/{ClassesView-RkFPpWRu.js → ClassesView-PCt8I3MU.js} +5 -5
- package/dist/assets/{ClassroomCountdownWidget-gs42lIGW.js → ClassroomCountdownWidget-HxelcHUp.js} +1 -1
- package/dist/assets/{CloudDriveModal-C6JtK08M.js → CloudDriveModal-Mp59sr-C.js} +1 -1
- package/dist/assets/ComputerLabView-FStILvFC.js +1 -0
- package/dist/assets/{CourseManagement-DNeaJcT9.js → CourseManagement-BQw2idgZ.js} +1 -1
- package/dist/assets/{CourseWizardModal-vw876VJr.js → CourseWizardModal-B7-e8otR.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-CyxDIJAF.js → CoursewareHubPanel-DLjH-UzF.js} +1 -1
- package/dist/assets/{Dashboard-C1iYnfCh.js → Dashboard-D3LXLQNC.js} +2 -2
- package/dist/assets/{ExportWeightModal-CTZ-O_wi.js → ExportWeightModal-vd7XgaJ0.js} +1 -1
- package/dist/assets/{HelpTour-D_q1z_MI.js → HelpTour-BLuiS8rr.js} +1 -1
- package/dist/assets/{HelpView-wyKldm_R.js → HelpView-C4BHoNqR.js} +1 -1
- package/dist/assets/{ImportLessonsModal-C09Z-qtx.js → ImportLessonsModal-Y6uIIemO.js} +1 -1
- package/dist/assets/{ImportModal-DqjqNeH4.js → ImportModal-H5s5MyZY.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-V97euiJF.js → InteractiveCoursewareViewer-D9GzG28E.js} +1 -1
- package/dist/assets/InteractiveWhiteboard-DswIV-ES.js +65 -0
- package/dist/assets/{LazyCourseware-0iFYlRGj.js → LazyCourseware-BEoTTg5h.js} +2 -2
- package/dist/assets/LazyWhiteboard-CMBBxnFX.js +2 -0
- package/dist/assets/LessonEditorView-BgJtG8Ks.js +1 -0
- package/dist/assets/{LiveClassroomView-CaS1DzSS.js → LiveClassroomView-Czwt5WW-.js} +7 -7
- package/dist/assets/{NotificationDetailModal-4MdNF4tw.js → NotificationDetailModal-D42vEs3S.js} +1 -1
- package/dist/assets/{PluginView-BqTbAp30.js → PluginView-BcOWIjLu.js} +1 -1
- package/dist/assets/{ProcessLogsModal-Cfr-T9Gd.js → ProcessLogsModal-_BwkmJVw.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-xTq22vDq.js → QuizGeneratorModal-DiItok6z.js} +1 -1
- package/dist/assets/StudentAssignmentEvalPanel-DHoZc3g0.js +1 -0
- package/dist/assets/{StudentAssignmentView-Cy_JdPYB.js → StudentAssignmentView-UK_7_MF3.js} +1 -1
- package/dist/assets/StudentLessonView-C11Qkdi_.js +1 -0
- package/dist/assets/{StudentView-CpJsG7FT.js → StudentView-j0hx3IOi.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-YZ85mv_v.js → SystemResourceLibraryModal-dEdNKbk2.js} +1 -1
- package/dist/assets/TeacherView-COZw2tFl.js +2 -0
- package/dist/assets/{TimetableView-D_4XmrRN.js → TimetableView-AkReskNG.js} +1 -1
- package/dist/assets/{UTIF-FfCsrHhe.js → UTIF-4E-wdh3X.js} +1 -1
- package/dist/assets/index-DN3E-Fmw.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-k90WUDrU.js +1 -0
- package/dist/assets/student-workspace-BDw1i4Vl.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 +602 -200
- 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();
|
|
@@ -3575,6 +3578,19 @@ var init_errors2 = __esm({
|
|
|
3575
3578
|
function scopedId(scopeType, scopeId, key) {
|
|
3576
3579
|
return `${scopeType}\0${scopeId}\0${key}`;
|
|
3577
3580
|
}
|
|
3581
|
+
function getSharedState(db2, manifestId) {
|
|
3582
|
+
let byManifest = sharedStates.get(db2);
|
|
3583
|
+
if (!byManifest) {
|
|
3584
|
+
byManifest = /* @__PURE__ */ new Map();
|
|
3585
|
+
sharedStates.set(db2, byManifest);
|
|
3586
|
+
}
|
|
3587
|
+
let state = byManifest.get(manifestId);
|
|
3588
|
+
if (!state) {
|
|
3589
|
+
state = { values: /* @__PURE__ */ new Map(), instances: /* @__PURE__ */ new Set() };
|
|
3590
|
+
byManifest.set(manifestId, state);
|
|
3591
|
+
}
|
|
3592
|
+
return state;
|
|
3593
|
+
}
|
|
3578
3594
|
function normalizeWriteScope(scope) {
|
|
3579
3595
|
const scopeType = scope?.scopeType ?? "platform";
|
|
3580
3596
|
const scopeId = scope?.scopeId?.trim() ?? "";
|
|
@@ -3583,21 +3599,29 @@ function normalizeWriteScope(scope) {
|
|
|
3583
3599
|
}
|
|
3584
3600
|
return { scopeType, scopeId: scopeType === "platform" ? "" : scopeId };
|
|
3585
3601
|
}
|
|
3586
|
-
var CONFIG_SCOPE_TYPES, ConfigService;
|
|
3602
|
+
var CONFIG_SCOPE_TYPES, sharedStates, instanceCleanup, ConfigService;
|
|
3587
3603
|
var init_config_service = __esm({
|
|
3588
3604
|
"packages/core/plugin-host/config-service.ts"() {
|
|
3589
3605
|
"use strict";
|
|
3590
3606
|
CONFIG_SCOPE_TYPES = ["platform", "school", "class"];
|
|
3607
|
+
sharedStates = /* @__PURE__ */ new WeakMap();
|
|
3608
|
+
instanceCleanup = new FinalizationRegistry(
|
|
3609
|
+
({ state, ref }) => {
|
|
3610
|
+
state.instances.delete(ref);
|
|
3611
|
+
}
|
|
3612
|
+
);
|
|
3591
3613
|
ConfigService = class _ConfigService {
|
|
3592
3614
|
constructor(db2, manifest) {
|
|
3593
|
-
/**
|
|
3594
|
-
this.values = /* @__PURE__ */ new Map();
|
|
3595
|
-
/** Change listeners. */
|
|
3615
|
+
/** Change listeners(每个实例独立,广播时按实例分发)。 */
|
|
3596
3616
|
this.listeners = /* @__PURE__ */ new Set();
|
|
3597
3617
|
this.tableEnsured = false;
|
|
3598
3618
|
this.db = db2;
|
|
3599
3619
|
this.manifestId = manifest.id;
|
|
3600
3620
|
this.schema = manifest.configuration?.properties ?? {};
|
|
3621
|
+
this.state = getSharedState(db2, manifest.id);
|
|
3622
|
+
const ref = new WeakRef(this);
|
|
3623
|
+
this.state.instances.add(ref);
|
|
3624
|
+
instanceCleanup.register(this, { state: this.state, ref });
|
|
3601
3625
|
}
|
|
3602
3626
|
static {
|
|
3603
3627
|
this.KEY_PREFIX = "config:";
|
|
@@ -3660,21 +3684,21 @@ var init_config_service = __esm({
|
|
|
3660
3684
|
}
|
|
3661
3685
|
readScoped(key, scopeType, scopeId) {
|
|
3662
3686
|
const cacheKey2 = scopedId(scopeType, scopeId, key);
|
|
3663
|
-
if (this.values.has(cacheKey2)) {
|
|
3664
|
-
const cached3 = this.values.get(cacheKey2);
|
|
3687
|
+
if (this.state.values.has(cacheKey2)) {
|
|
3688
|
+
const cached3 = this.state.values.get(cacheKey2);
|
|
3665
3689
|
return cached3 === null ? { found: false } : { found: true, value: cached3 };
|
|
3666
3690
|
}
|
|
3667
3691
|
const row = this.queryScoped(key, scopeType, scopeId);
|
|
3668
3692
|
if (!row) {
|
|
3669
|
-
this.values.set(cacheKey2, null);
|
|
3693
|
+
this.state.values.set(cacheKey2, null);
|
|
3670
3694
|
return { found: false };
|
|
3671
3695
|
}
|
|
3672
3696
|
try {
|
|
3673
3697
|
const parsed = JSON.parse(row.value);
|
|
3674
|
-
this.values.set(cacheKey2, parsed);
|
|
3698
|
+
this.state.values.set(cacheKey2, parsed);
|
|
3675
3699
|
return { found: true, value: parsed };
|
|
3676
3700
|
} catch {
|
|
3677
|
-
this.values.set(cacheKey2, row.value);
|
|
3701
|
+
this.state.values.set(cacheKey2, row.value);
|
|
3678
3702
|
return { found: true, value: row.value };
|
|
3679
3703
|
}
|
|
3680
3704
|
}
|
|
@@ -3690,21 +3714,21 @@ var init_config_service = __esm({
|
|
|
3690
3714
|
}
|
|
3691
3715
|
readLegacyPlatformValue(key) {
|
|
3692
3716
|
const cacheKey2 = scopedId("platform", "", `legacy:${key}`);
|
|
3693
|
-
if (this.values.has(cacheKey2)) {
|
|
3694
|
-
const cached3 = this.values.get(cacheKey2);
|
|
3717
|
+
if (this.state.values.has(cacheKey2)) {
|
|
3718
|
+
const cached3 = this.state.values.get(cacheKey2);
|
|
3695
3719
|
return cached3 === null ? { found: false } : { found: true, value: cached3 };
|
|
3696
3720
|
}
|
|
3697
3721
|
try {
|
|
3698
3722
|
const row = this.db.prepare("SELECT value FROM plugin_storage WHERE plugin_id = ? AND key = ?").get(this.manifestId, _ConfigService.KEY_PREFIX + key);
|
|
3699
3723
|
if (!row) {
|
|
3700
|
-
this.values.set(cacheKey2, null);
|
|
3724
|
+
this.state.values.set(cacheKey2, null);
|
|
3701
3725
|
return { found: false };
|
|
3702
3726
|
}
|
|
3703
3727
|
const parsed = JSON.parse(row.value);
|
|
3704
|
-
this.values.set(cacheKey2, parsed);
|
|
3728
|
+
this.state.values.set(cacheKey2, parsed);
|
|
3705
3729
|
return { found: true, value: parsed };
|
|
3706
3730
|
} catch {
|
|
3707
|
-
this.values.set(cacheKey2, null);
|
|
3731
|
+
this.state.values.set(cacheKey2, null);
|
|
3708
3732
|
return { found: false };
|
|
3709
3733
|
}
|
|
3710
3734
|
}
|
|
@@ -3760,8 +3784,8 @@ var init_config_service = __esm({
|
|
|
3760
3784
|
const { scopeType, scopeId } = normalizeWriteScope(scope);
|
|
3761
3785
|
this.assertScopeAllowed(key, scopeType);
|
|
3762
3786
|
const cacheKey2 = scopedId(scopeType, scopeId, key);
|
|
3763
|
-
const previous = this.values.get(cacheKey2);
|
|
3764
|
-
this.values.set(cacheKey2, value);
|
|
3787
|
+
const previous = this.state.values.get(cacheKey2);
|
|
3788
|
+
this.state.values.set(cacheKey2, value);
|
|
3765
3789
|
try {
|
|
3766
3790
|
this.ensureTable();
|
|
3767
3791
|
this.db.prepare(
|
|
@@ -3772,16 +3796,32 @@ var init_config_service = __esm({
|
|
|
3772
3796
|
).run(this.manifestId, scopeType, scopeId, key, JSON.stringify(value), Date.now());
|
|
3773
3797
|
} catch (e) {
|
|
3774
3798
|
if (previous === void 0) {
|
|
3775
|
-
this.values.delete(cacheKey2);
|
|
3799
|
+
this.state.values.delete(cacheKey2);
|
|
3776
3800
|
} else {
|
|
3777
|
-
this.values.set(cacheKey2, previous);
|
|
3801
|
+
this.state.values.set(cacheKey2, previous);
|
|
3778
3802
|
}
|
|
3779
3803
|
throw e;
|
|
3780
3804
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3805
|
+
this.broadcast(key, value, previous ?? void 0);
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* 通知所有共享同一 db + manifest.id 的活跃实例(含自身)。
|
|
3809
|
+
*
|
|
3810
|
+
* 管理端通过临时实例写入时,运行中插件的 `onChange` 也因此能收到。
|
|
3811
|
+
* 已回收的实例在遍历时顺手清理。
|
|
3812
|
+
*/
|
|
3813
|
+
broadcast(key, newValue, oldValue) {
|
|
3814
|
+
for (const ref of this.state.instances) {
|
|
3815
|
+
const instance = ref.deref();
|
|
3816
|
+
if (!instance) {
|
|
3817
|
+
this.state.instances.delete(ref);
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
for (const listener of instance.listeners) {
|
|
3821
|
+
try {
|
|
3822
|
+
listener(key, newValue, oldValue);
|
|
3823
|
+
} catch {
|
|
3824
|
+
}
|
|
3785
3825
|
}
|
|
3786
3826
|
}
|
|
3787
3827
|
}
|
|
@@ -3814,11 +3854,12 @@ var init_config_service = __esm({
|
|
|
3814
3854
|
* 插件在 `activate()` 里通常会马上读配置,提前填充避免每条配置一次查询。
|
|
3815
3855
|
*/
|
|
3816
3856
|
loadFromDB() {
|
|
3857
|
+
this.state.values.clear();
|
|
3817
3858
|
try {
|
|
3818
3859
|
const rows = this.db.prepare(`SELECT scope_type, scope_id, key, value FROM ${_ConfigService.TABLE} WHERE plugin_id = ?`).all(this.manifestId);
|
|
3819
3860
|
for (const row of rows) {
|
|
3820
3861
|
const scopeType = CONFIG_SCOPE_TYPES.includes(row.scope_type) ? row.scope_type : "platform";
|
|
3821
|
-
this.values.set(scopedId(scopeType, row.scope_id ?? "", row.key), this.parseStored(row.value));
|
|
3862
|
+
this.state.values.set(scopedId(scopeType, row.scope_id ?? "", row.key), this.parseStored(row.value));
|
|
3822
3863
|
}
|
|
3823
3864
|
} catch {
|
|
3824
3865
|
}
|
|
@@ -3827,7 +3868,7 @@ var init_config_service = __esm({
|
|
|
3827
3868
|
const rows = this.db.prepare(`SELECT key, value FROM plugin_storage WHERE plugin_id = ? AND key LIKE ?`).all(this.manifestId, prefix + "%");
|
|
3828
3869
|
for (const row of rows) {
|
|
3829
3870
|
const key = row.key.slice(prefix.length);
|
|
3830
|
-
this.values.set(scopedId("platform", "", `legacy:${key}`), this.parseStored(row.value));
|
|
3871
|
+
this.state.values.set(scopedId("platform", "", `legacy:${key}`), this.parseStored(row.value));
|
|
3831
3872
|
}
|
|
3832
3873
|
} catch {
|
|
3833
3874
|
}
|
|
@@ -3839,9 +3880,9 @@ var init_config_service = __esm({
|
|
|
3839
3880
|
return raw;
|
|
3840
3881
|
}
|
|
3841
3882
|
}
|
|
3842
|
-
/**
|
|
3883
|
+
/** 测试/诊断:当前共享缓存里的作用域条目数。 */
|
|
3843
3884
|
cachedEntryCount() {
|
|
3844
|
-
return this.values.size;
|
|
3885
|
+
return this.state.values.size;
|
|
3845
3886
|
}
|
|
3846
3887
|
};
|
|
3847
3888
|
}
|
|
@@ -3875,9 +3916,9 @@ var init_http_router = __esm({
|
|
|
3875
3916
|
/**
|
|
3876
3917
|
* 注册指定动词与路径的 Handler
|
|
3877
3918
|
*/
|
|
3878
|
-
route(method,
|
|
3919
|
+
route(method, path47, handler, options = {}) {
|
|
3879
3920
|
const upperMethod = method.toUpperCase();
|
|
3880
|
-
const normalized =
|
|
3921
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3881
3922
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3882
3923
|
this.warnIfUnscoped(upperMethod, normalized, options);
|
|
3883
3924
|
this.routes.push({
|
|
@@ -3890,20 +3931,20 @@ var init_http_router = __esm({
|
|
|
3890
3931
|
scope: options.scope
|
|
3891
3932
|
});
|
|
3892
3933
|
}
|
|
3893
|
-
get(
|
|
3894
|
-
this.route("GET",
|
|
3934
|
+
get(path47, handler, options) {
|
|
3935
|
+
this.route("GET", path47, handler, options);
|
|
3895
3936
|
}
|
|
3896
|
-
post(
|
|
3897
|
-
this.route("POST",
|
|
3937
|
+
post(path47, handler, options) {
|
|
3938
|
+
this.route("POST", path47, handler, options);
|
|
3898
3939
|
}
|
|
3899
|
-
put(
|
|
3900
|
-
this.route("PUT",
|
|
3940
|
+
put(path47, handler, options) {
|
|
3941
|
+
this.route("PUT", path47, handler, options);
|
|
3901
3942
|
}
|
|
3902
|
-
patch(
|
|
3903
|
-
this.route("PATCH",
|
|
3943
|
+
patch(path47, handler, options) {
|
|
3944
|
+
this.route("PATCH", path47, handler, options);
|
|
3904
3945
|
}
|
|
3905
|
-
delete(
|
|
3906
|
-
this.route("DELETE",
|
|
3946
|
+
delete(path47, handler, options) {
|
|
3947
|
+
this.route("DELETE", path47, handler, options);
|
|
3907
3948
|
}
|
|
3908
3949
|
warnIfUnscoped(method, pattern, options) {
|
|
3909
3950
|
if (options.scope) return;
|
|
@@ -3918,19 +3959,19 @@ var init_http_router = __esm({
|
|
|
3918
3959
|
}
|
|
3919
3960
|
stream(methodOrPath, pathOrHandler, maybeHandler) {
|
|
3920
3961
|
if (typeof pathOrHandler === "function") {
|
|
3921
|
-
const
|
|
3962
|
+
const path47 = methodOrPath;
|
|
3922
3963
|
const handler = pathOrHandler;
|
|
3923
|
-
this.registerStream("GET",
|
|
3924
|
-
this.registerStream("POST",
|
|
3964
|
+
this.registerStream("GET", path47, handler);
|
|
3965
|
+
this.registerStream("POST", path47, handler);
|
|
3925
3966
|
} else {
|
|
3926
3967
|
const method = methodOrPath.toUpperCase();
|
|
3927
|
-
const
|
|
3968
|
+
const path47 = pathOrHandler;
|
|
3928
3969
|
const handler = maybeHandler;
|
|
3929
|
-
this.registerStream(method,
|
|
3970
|
+
this.registerStream(method, path47, handler);
|
|
3930
3971
|
}
|
|
3931
3972
|
}
|
|
3932
|
-
registerStream(method,
|
|
3933
|
-
const normalized =
|
|
3973
|
+
registerStream(method, path47, handler) {
|
|
3974
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3934
3975
|
const { regex, paramNames } = compileRoutePattern(normalized);
|
|
3935
3976
|
this.routes.push({
|
|
3936
3977
|
method: method.toUpperCase(),
|
|
@@ -3944,9 +3985,9 @@ var init_http_router = __esm({
|
|
|
3944
3985
|
/**
|
|
3945
3986
|
* 检查指定动词与路径是否为流式路由
|
|
3946
3987
|
*/
|
|
3947
|
-
isStream(method,
|
|
3988
|
+
isStream(method, path47) {
|
|
3948
3989
|
const upperMethod = method.toUpperCase();
|
|
3949
|
-
const normalized =
|
|
3990
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3950
3991
|
for (const entry of this.routes) {
|
|
3951
3992
|
if (entry.method !== upperMethod) continue;
|
|
3952
3993
|
if (entry.regex.test(normalized)) {
|
|
@@ -3958,9 +3999,9 @@ var init_http_router = __esm({
|
|
|
3958
3999
|
/**
|
|
3959
4000
|
* 匹配路由并提取路由参数
|
|
3960
4001
|
*/
|
|
3961
|
-
match(method,
|
|
4002
|
+
match(method, path47) {
|
|
3962
4003
|
const upperMethod = method.toUpperCase();
|
|
3963
|
-
const normalized =
|
|
4004
|
+
const normalized = path47.startsWith("/") ? path47 : "/" + path47;
|
|
3964
4005
|
for (const entry of this.routes) {
|
|
3965
4006
|
if (entry.method !== upperMethod) continue;
|
|
3966
4007
|
const match = normalized.match(entry.regex);
|
|
@@ -4103,13 +4144,13 @@ function checkMethodPolicy(input) {
|
|
|
4103
4144
|
if (tokens.frontendApi && token === tokens.frontendApi) {
|
|
4104
4145
|
if (isReadOnlyMethod(method)) return null;
|
|
4105
4146
|
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(
|
|
4147
|
+
const path47 = extractPathArg(args);
|
|
4148
|
+
const isHighRisk = HIGH_RISK_PATH_PREFIXES.some((p) => path47 === p || path47.startsWith(`${p}/`));
|
|
4149
|
+
const isIrreversible = IRREVERSIBLE_PATH_SUFFIXES.some((s) => path47.endsWith(s));
|
|
4150
|
+
const isEntityDelete = method === "del" && ENTITY_DELETE_RE.test(path47);
|
|
4110
4151
|
if (isHighRisk || isIrreversible || isEntityDelete) {
|
|
4111
4152
|
if (hasCapability(caps, CAP_API_ADMIN)) return null;
|
|
4112
|
-
const why = isIrreversible ? `\u4E0D\u53EF\u9006\u64CD\u4F5C '${
|
|
4153
|
+
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
4154
|
return `${why} \u9700\u8981 manifest \u58F0\u660E '${CAP_API_ADMIN}' \u80FD\u529B`;
|
|
4114
4155
|
}
|
|
4115
4156
|
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 +4650,53 @@ function wrapPointsDimensionRegistry(registry4, tracker, pluginId) {
|
|
|
4609
4650
|
})
|
|
4610
4651
|
};
|
|
4611
4652
|
}
|
|
4653
|
+
function wrapClassroomLifecycleService(service, tracker, pluginId) {
|
|
4654
|
+
return {
|
|
4655
|
+
getStage: createSafeFunction((lessonId) => service.getStage(lessonId)),
|
|
4656
|
+
transitionStage: createSafeFunction(
|
|
4657
|
+
(lessonId, toStage, actorId, classId) => service.transitionStage(lessonId, toStage, actorId, classId)
|
|
4658
|
+
),
|
|
4659
|
+
registerStageGuard: createSafeFunction((owner, guard) => {
|
|
4660
|
+
service.registerStageGuard(owner, guard);
|
|
4661
|
+
tracker.track(pluginId, {
|
|
4662
|
+
dispose: () => {
|
|
4663
|
+
service.unregisterStageGuard(owner);
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
}),
|
|
4667
|
+
unregisterStageGuard: createSafeFunction((owner) => {
|
|
4668
|
+
service.unregisterStageGuard(owner);
|
|
4669
|
+
})
|
|
4670
|
+
};
|
|
4671
|
+
}
|
|
4672
|
+
function wrapInteractionRuntimeService(service, tracker, pluginId) {
|
|
4673
|
+
return {
|
|
4674
|
+
registerActivityProvider: createSafeFunction((owner, descriptor) => {
|
|
4675
|
+
service.registerActivityProvider(owner, descriptor);
|
|
4676
|
+
const providerId = descriptor?.id;
|
|
4677
|
+
if (providerId) {
|
|
4678
|
+
tracker.track(pluginId, {
|
|
4679
|
+
dispose: () => {
|
|
4680
|
+
service.unregisterActivityProvider(owner, providerId);
|
|
4681
|
+
}
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
}),
|
|
4685
|
+
unregisterActivityProvider: createSafeFunction((owner, id) => {
|
|
4686
|
+
service.unregisterActivityProvider(owner, id);
|
|
4687
|
+
}),
|
|
4688
|
+
listActivityProviders: createSafeFunction(() => service.listActivityProviders())
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId) {
|
|
4692
|
+
if (tokenName === IClassroomLifecycleServiceToken.name) {
|
|
4693
|
+
return wrapClassroomLifecycleService(raw, tracker, pluginId);
|
|
4694
|
+
}
|
|
4695
|
+
if (tokenName === IInteractionRuntimeServiceToken.name) {
|
|
4696
|
+
return wrapInteractionRuntimeService(raw, tracker, pluginId);
|
|
4697
|
+
}
|
|
4698
|
+
return raw;
|
|
4699
|
+
}
|
|
4612
4700
|
function wrapPointsLedgerService(service, pluginId, capabilities) {
|
|
4613
4701
|
const auditAccess = (studentId, method, operation, requiredCap) => {
|
|
4614
4702
|
const hasCap = hasCapability(capabilities ?? [], requiredCap);
|
|
@@ -4903,9 +4991,9 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
|
|
|
4903
4991
|
`[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
4992
|
);
|
|
4905
4993
|
}
|
|
4906
|
-
return serviceRegistry.resolveByName(token);
|
|
4994
|
+
return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId));
|
|
4907
4995
|
}
|
|
4908
|
-
return serviceRegistry.resolve(token);
|
|
4996
|
+
return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId));
|
|
4909
4997
|
},
|
|
4910
4998
|
// V3.2: 插件向 DI 容器提供服务(对应 manifest.provides)
|
|
4911
4999
|
provide: async (token, instance) => {
|
|
@@ -5740,7 +5828,7 @@ function isPidAlive(pid) {
|
|
|
5740
5828
|
}
|
|
5741
5829
|
}
|
|
5742
5830
|
function writeBootstrapTempFile(code) {
|
|
5743
|
-
const file =
|
|
5831
|
+
const file = import_node_path11.default.join(
|
|
5744
5832
|
import_node_os.default.tmpdir(),
|
|
5745
5833
|
`${BOOTSTRAP_PREFIX}${process.pid}-${import_node_crypto7.default.randomBytes(8).toString("hex")}${BOOTSTRAP_SUFFIX}`
|
|
5746
5834
|
);
|
|
@@ -5754,7 +5842,7 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
|
5754
5842
|
let cleaned = 0;
|
|
5755
5843
|
for (const file of files) {
|
|
5756
5844
|
if (!file.startsWith(BOOTSTRAP_PREFIX) || !file.endsWith(BOOTSTRAP_SUFFIX)) continue;
|
|
5757
|
-
const full =
|
|
5845
|
+
const full = import_node_path11.default.join(tmpDir, file);
|
|
5758
5846
|
const ownerPid = bootstrapOwnerPid(file);
|
|
5759
5847
|
if (ownerPid === null) {
|
|
5760
5848
|
try {
|
|
@@ -5779,9 +5867,9 @@ function cleanupStaleBootstrapFiles(now = Date.now()) {
|
|
|
5779
5867
|
function defaultNetPreloadStartDirs() {
|
|
5780
5868
|
const dirs = [];
|
|
5781
5869
|
try {
|
|
5782
|
-
const url =
|
|
5870
|
+
const url = import_meta6.url;
|
|
5783
5871
|
if (typeof url === "string" && url.startsWith("file:")) {
|
|
5784
|
-
dirs.push(
|
|
5872
|
+
dirs.push(import_node_path11.default.dirname((0, import_node_url3.fileURLToPath)(url)));
|
|
5785
5873
|
}
|
|
5786
5874
|
} catch {
|
|
5787
5875
|
}
|
|
@@ -5793,11 +5881,11 @@ function defaultNetPreloadStartDirs() {
|
|
|
5793
5881
|
function resolveNetPreloadPath(options = {}) {
|
|
5794
5882
|
const cwd = options.cwd ?? process.cwd();
|
|
5795
5883
|
const candidates = [
|
|
5796
|
-
|
|
5797
|
-
|
|
5884
|
+
import_node_path11.default.resolve(cwd, "packages/core/worker-runtime/plugin-net-preload.cjs"),
|
|
5885
|
+
import_node_path11.default.resolve(cwd, "dist/plugin-net-preload.cjs")
|
|
5798
5886
|
];
|
|
5799
5887
|
for (const dir of options.startDirs ?? defaultNetPreloadStartDirs()) {
|
|
5800
|
-
candidates.unshift(
|
|
5888
|
+
candidates.unshift(import_node_path11.default.join(dir, "plugin-net-preload.cjs"));
|
|
5801
5889
|
}
|
|
5802
5890
|
const found = candidates.find((candidate) => import_node_fs9.default.existsSync(candidate));
|
|
5803
5891
|
if (!found) {
|
|
@@ -5805,7 +5893,7 @@ function resolveNetPreloadPath(options = {}) {
|
|
|
5805
5893
|
}
|
|
5806
5894
|
return found;
|
|
5807
5895
|
}
|
|
5808
|
-
var import_node_child_process, import_node_crypto7, import_node_fs9, import_node_os,
|
|
5896
|
+
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
5897
|
var init_child_spawn = __esm({
|
|
5810
5898
|
"packages/core/worker-runtime/child-spawn.ts"() {
|
|
5811
5899
|
"use strict";
|
|
@@ -5813,13 +5901,13 @@ var init_child_spawn = __esm({
|
|
|
5813
5901
|
import_node_crypto7 = __toESM(require("node:crypto"), 1);
|
|
5814
5902
|
import_node_fs9 = __toESM(require("node:fs"), 1);
|
|
5815
5903
|
import_node_os = __toESM(require("node:os"), 1);
|
|
5816
|
-
|
|
5904
|
+
import_node_path11 = __toESM(require("node:path"), 1);
|
|
5817
5905
|
import_node_url3 = require("node:url");
|
|
5818
5906
|
init_child_transport();
|
|
5819
5907
|
init_child_isolate();
|
|
5820
5908
|
init_plugin_permission();
|
|
5821
5909
|
init_plugin_limits();
|
|
5822
|
-
|
|
5910
|
+
import_meta6 = {};
|
|
5823
5911
|
ENV_ALLOWLIST = [
|
|
5824
5912
|
"PATH",
|
|
5825
5913
|
"LANG",
|
|
@@ -8923,9 +9011,9 @@ __export(worker_manager_exports, {
|
|
|
8923
9011
|
function collectSharedReadPaths(pluginDir) {
|
|
8924
9012
|
if (!pluginDir) return [];
|
|
8925
9013
|
const paths = /* @__PURE__ */ new Set();
|
|
8926
|
-
const nodeModulesDir =
|
|
9014
|
+
const nodeModulesDir = import_node_path12.default.join(import_node_path12.default.dirname(pluginDir), "node_modules");
|
|
8927
9015
|
paths.add(nodeModulesDir);
|
|
8928
|
-
const sdkLink =
|
|
9016
|
+
const sdkLink = import_node_path12.default.join(nodeModulesDir, "@openlearn", "plugin-sdk");
|
|
8929
9017
|
try {
|
|
8930
9018
|
if (import_fs7.default.existsSync(sdkLink)) paths.add(import_fs7.default.realpathSync(sdkLink));
|
|
8931
9019
|
} catch {
|
|
@@ -8964,7 +9052,7 @@ function generateBootstrapCode() {
|
|
|
8964
9052
|
const rootPath = process.cwd().replace(/\\/g, "/");
|
|
8965
9053
|
return composeBootstrapCode(`${rootPath}/package.json`);
|
|
8966
9054
|
}
|
|
8967
|
-
var import_node_worker_threads, import_fs7,
|
|
9055
|
+
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
9056
|
var init_worker_manager = __esm({
|
|
8969
9057
|
"packages/core/worker-runtime/worker-manager.ts"() {
|
|
8970
9058
|
"use strict";
|
|
@@ -8978,7 +9066,7 @@ var init_worker_manager = __esm({
|
|
|
8978
9066
|
init_plugin_limits();
|
|
8979
9067
|
init_thread_isolate();
|
|
8980
9068
|
import_fs7 = __toESM(require("fs"), 1);
|
|
8981
|
-
|
|
9069
|
+
import_node_path12 = __toESM(require("node:path"), 1);
|
|
8982
9070
|
init_transport();
|
|
8983
9071
|
init_plugin_net_proxy();
|
|
8984
9072
|
init_service_host();
|
|
@@ -9383,7 +9471,7 @@ var init_worker_manager = __esm({
|
|
|
9383
9471
|
const encodedBootstrap = Buffer.from(bootstrapCode, "utf-8").toString("base64");
|
|
9384
9472
|
const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
|
|
9385
9473
|
const allowedTokens = computeAllowedWorkerTokens(manifest, serviceTokens);
|
|
9386
|
-
const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(
|
|
9474
|
+
const resolvedPluginDir = pluginDir && import_fs7.default.existsSync(import_node_path12.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
|
|
9387
9475
|
let approvedNetHosts = [];
|
|
9388
9476
|
try {
|
|
9389
9477
|
approvedNetHosts = listApprovedNetHosts(this.db, [pluginId, manifest.id]);
|
|
@@ -9755,6 +9843,193 @@ __export(server_exports, {
|
|
|
9755
9843
|
startServer: () => startServer
|
|
9756
9844
|
});
|
|
9757
9845
|
module.exports = __toCommonJS(server_exports);
|
|
9846
|
+
|
|
9847
|
+
// server/security/runtime-check.ts
|
|
9848
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
9849
|
+
var import_node_module = require("node:module");
|
|
9850
|
+
|
|
9851
|
+
// scripts/assert-node-runtime.mjs
|
|
9852
|
+
var SUPPORTED_NODE_MAJORS = Object.freeze([20, 22, 24]);
|
|
9853
|
+
var SUPPORTED_NODE_TEXT = "Node.js 20.x / 22.x / 24.x";
|
|
9854
|
+
var PINNED_NODE_MAJOR = 22;
|
|
9855
|
+
function parseNodeMajor(version) {
|
|
9856
|
+
if (typeof version !== "string") return null;
|
|
9857
|
+
const matched = /^v?(\d+)\./.exec(version.trim());
|
|
9858
|
+
if (!matched) return null;
|
|
9859
|
+
const major = Number(matched[1]);
|
|
9860
|
+
return Number.isFinite(major) ? major : null;
|
|
9861
|
+
}
|
|
9862
|
+
function checkNodeVersion(version, supported = SUPPORTED_NODE_MAJORS) {
|
|
9863
|
+
const major = parseNodeMajor(version);
|
|
9864
|
+
if (major === null) {
|
|
9865
|
+
return {
|
|
9866
|
+
ok: false,
|
|
9867
|
+
code: "unparseable",
|
|
9868
|
+
major: null,
|
|
9869
|
+
message: `\u65E0\u6CD5\u89E3\u6790 Node \u7248\u672C\u300C${version}\u300D\u3002`
|
|
9870
|
+
};
|
|
9871
|
+
}
|
|
9872
|
+
if (supported.includes(major)) {
|
|
9873
|
+
return { ok: true, code: "supported", major, message: "" };
|
|
9874
|
+
}
|
|
9875
|
+
return {
|
|
9876
|
+
ok: false,
|
|
9877
|
+
code: "unsupported",
|
|
9878
|
+
major,
|
|
9879
|
+
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`
|
|
9880
|
+
};
|
|
9881
|
+
}
|
|
9882
|
+
function buildFixGuidance(major) {
|
|
9883
|
+
const pinned = PINNED_NODE_MAJOR;
|
|
9884
|
+
const lines = [
|
|
9885
|
+
"\u600E\u4E48\u4FEE\uFF08\u4EFB\u9009\u5176\u4E00\uFF09\uFF1A",
|
|
9886
|
+
` 1) \u5207\u5230\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08\u672C\u9879\u76EE\u5B9E\u6D4B ${SUPPORTED_NODE_MAJORS.join(" / ")}\uFF09\uFF1A`,
|
|
9887
|
+
` nvm install ${pinned} && nvm use ${pinned}`,
|
|
9888
|
+
` fnm install ${pinned} && fnm use ${pinned}`,
|
|
9889
|
+
` asdf install nodejs ${pinned}.16.0 && asdf local nodejs ${pinned}.16.0`,
|
|
9890
|
+
" \u4ED3\u5E93\u6839\u7684 .nvmrc / .node-version \u5DF2\u9489\u4F4F " + pinned + "\uFF0Cnvm / fnm \u4F1A\u81EA\u52A8\u91C7\u7528\u3002",
|
|
9891
|
+
" 2) \u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5185\u542B Node " + pinned + "\uFF0C\u5BBF\u4E3B\u673A\u65E0\u9700\u88C5 Node\uFF09\uFF1A",
|
|
9892
|
+
" docker compose up -d",
|
|
9893
|
+
" 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",
|
|
9894
|
+
" \u4FDD\u7559\u65E7 node_modules \u53EA\u4F1A\u7EE7\u7EED\u590D\u7528 ABI \u4E0D\u5339\u914D\u7684\u4E8C\u8FDB\u5236\uFF1A",
|
|
9895
|
+
" rm -rf node_modules && pnpm install"
|
|
9896
|
+
];
|
|
9897
|
+
if (major !== null && major === 21) {
|
|
9898
|
+
lines.push(
|
|
9899
|
+
" \u6CE8\uFF1ANode 21 \u4E0D\u662F LTS\uFF0C\u4E14 better-sqlite3 \u5B98\u65B9\u4E0D\u63D0\u4F9B 21.x \u9884\u7F16\u8BD1\u4EA7\u7269\uFF0C",
|
|
9900
|
+
" \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"
|
|
9901
|
+
);
|
|
9902
|
+
}
|
|
9903
|
+
return lines.join("\n");
|
|
9904
|
+
}
|
|
9905
|
+
function formatUnsupportedMessage(version, check = checkNodeVersion(version)) {
|
|
9906
|
+
if (check.ok) return "";
|
|
9907
|
+
const head = `[NodeRuntime] FATAL: ${check.message}`;
|
|
9908
|
+
return `${head}
|
|
9909
|
+
|
|
9910
|
+
${buildFixGuidance(check.major)}
|
|
9911
|
+
`;
|
|
9912
|
+
}
|
|
9913
|
+
function runCliGuard() {
|
|
9914
|
+
const result = checkNodeVersion(process.version);
|
|
9915
|
+
if (result.ok) return;
|
|
9916
|
+
console.error(formatUnsupportedMessage(process.version, result));
|
|
9917
|
+
process.exit(1);
|
|
9918
|
+
}
|
|
9919
|
+
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"));
|
|
9920
|
+
if (invokedDirectly) {
|
|
9921
|
+
runCliGuard();
|
|
9922
|
+
}
|
|
9923
|
+
|
|
9924
|
+
// server/security/runtime-check.ts
|
|
9925
|
+
var import_meta = {};
|
|
9926
|
+
var ABI_PATTERN = /NODE_MODULE_VERSION\s+(\d+)/i;
|
|
9927
|
+
var MISSING_BINDINGS_MARKERS = ["Could not locate the bindings file", "bindings file"];
|
|
9928
|
+
function detectNativeModuleFailure(error) {
|
|
9929
|
+
const runtimeAbi = Number(process.versions.modules);
|
|
9930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9931
|
+
const abiMatch = ABI_PATTERN.exec(message);
|
|
9932
|
+
if (abiMatch) {
|
|
9933
|
+
return {
|
|
9934
|
+
kind: "abi-mismatch",
|
|
9935
|
+
// better-sqlite3 的报错里第一个 NODE_MODULE_VERSION 是「模块编译时的 ABI」,
|
|
9936
|
+
// 第二个是「当前运行时要求的 ABI」。
|
|
9937
|
+
moduleAbi: Number(abiMatch[1]),
|
|
9938
|
+
runtimeAbi,
|
|
9939
|
+
detail: message
|
|
9940
|
+
};
|
|
9941
|
+
}
|
|
9942
|
+
if (MISSING_BINDINGS_MARKERS.some((marker) => message.includes(marker))) {
|
|
9943
|
+
return { kind: "missing-bindings", moduleAbi: null, runtimeAbi, detail: message };
|
|
9944
|
+
}
|
|
9945
|
+
return { kind: "unknown", moduleAbi: null, runtimeAbi, detail: message };
|
|
9946
|
+
}
|
|
9947
|
+
function resolveRequire() {
|
|
9948
|
+
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
9949
|
+
return (0, import_node_module.createRequire)(import_meta.url);
|
|
9950
|
+
}
|
|
9951
|
+
if (typeof __filename !== "undefined") {
|
|
9952
|
+
return (0, import_node_module.createRequire)(__filename);
|
|
9953
|
+
}
|
|
9954
|
+
return (0, import_node_module.createRequire)(import_node_path.default.join(process.cwd(), "package.json"));
|
|
9955
|
+
}
|
|
9956
|
+
function loadBetterSqlite3Binding(loadModule) {
|
|
9957
|
+
const load = loadModule ?? resolveRequire();
|
|
9958
|
+
const Database3 = load("better-sqlite3");
|
|
9959
|
+
const probe = new Database3(":memory:");
|
|
9960
|
+
probe.close();
|
|
9961
|
+
}
|
|
9962
|
+
function checkNativeModule(load = loadBetterSqlite3Binding) {
|
|
9963
|
+
try {
|
|
9964
|
+
load();
|
|
9965
|
+
return null;
|
|
9966
|
+
} catch (error) {
|
|
9967
|
+
const failure = detectNativeModuleFailure(error);
|
|
9968
|
+
if (failure.kind === "abi-mismatch") {
|
|
9969
|
+
return {
|
|
9970
|
+
key: "native-module-abi",
|
|
9971
|
+
severity: "error",
|
|
9972
|
+
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
|
|
9973
|
+
\u5E38\u89C1\u539F\u56E0\uFF1Anode_modules \u662F\u5728\u53E6\u4E00\u4E2A Node \u7248\u672C\u4E0B\u5B89\u88C5/\u62F7\u8D1D\u7684\u3002
|
|
9974
|
+
\u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08\u6309\u5F53\u524D Node \u91CD\u65B0\u7F16\u8BD1\u539F\u751F\u6A21\u5757\uFF09\uFF0C
|
|
9975
|
+
\u6216\u5207\u56DE\u53D7\u652F\u6301\u7684 Node \u7248\u672C\uFF08${SUPPORTED_NODE_TEXT}\uFF09\u3002`
|
|
9976
|
+
};
|
|
9977
|
+
}
|
|
9978
|
+
if (failure.kind === "missing-bindings") {
|
|
9979
|
+
return {
|
|
9980
|
+
key: "native-module-missing",
|
|
9981
|
+
severity: "error",
|
|
9982
|
+
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
|
|
9983
|
+
\u4FEE\u590D\uFF1Arm -rf node_modules && pnpm install\uFF08pnpm \u9700\u8981 allowBuilds \u653E\u884C better-sqlite3 \u7684\u6784\u5EFA\u811A\u672C\uFF09\uFF0C
|
|
9984
|
+
\u6216\u6539\u7528\u5B98\u65B9\u5BB9\u5668\u955C\u50CF\uFF08\u5DF2\u5185\u7F6E\u7F16\u8BD1\u597D\u7684\u4E8C\u8FDB\u5236\uFF09\u3002`
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9987
|
+
return {
|
|
9988
|
+
key: "native-module-load",
|
|
9989
|
+
severity: "error",
|
|
9990
|
+
message: `\u539F\u751F\u6A21\u5757 better-sqlite3 \u52A0\u8F7D\u5931\u8D25\uFF1A${failure.detail}`
|
|
9991
|
+
};
|
|
9992
|
+
}
|
|
9993
|
+
}
|
|
9994
|
+
function checkRuntimeEnvironment(input = {}) {
|
|
9995
|
+
const errors = [];
|
|
9996
|
+
const warnings = [];
|
|
9997
|
+
const nodeVersion = input.nodeVersion ?? process.version;
|
|
9998
|
+
const nodeCheck = checkNodeVersion(nodeVersion);
|
|
9999
|
+
if (!nodeCheck.ok) {
|
|
10000
|
+
errors.push({
|
|
10001
|
+
key: "node-version",
|
|
10002
|
+
severity: "error",
|
|
10003
|
+
message: `${nodeCheck.message}
|
|
10004
|
+
${buildFixGuidance(nodeCheck.major)}`
|
|
10005
|
+
});
|
|
10006
|
+
return { errors, warnings };
|
|
10007
|
+
}
|
|
10008
|
+
const nativeIssue = checkNativeModule(input.loadNativeModule);
|
|
10009
|
+
if (nativeIssue) {
|
|
10010
|
+
errors.push(nativeIssue);
|
|
10011
|
+
}
|
|
10012
|
+
return { errors, warnings };
|
|
10013
|
+
}
|
|
10014
|
+
function assertRuntimeEnvironment() {
|
|
10015
|
+
const { errors, warnings } = checkRuntimeEnvironment();
|
|
10016
|
+
for (const warning of warnings) {
|
|
10017
|
+
console.warn(`[NodeRuntime] WARN: ${warning.message}`);
|
|
10018
|
+
}
|
|
10019
|
+
if (errors.length === 0) return;
|
|
10020
|
+
console.error("[NodeRuntime] FATAL: \u8FD0\u884C\u65F6\u73AF\u5883\u4E0D\u6EE1\u8DB3\u8981\u6C42\uFF0C\u62D2\u7EDD\u542F\u52A8\u3002");
|
|
10021
|
+
for (const issue of errors) {
|
|
10022
|
+
console.error(`
|
|
10023
|
+
[NodeRuntime] ${issue.key}
|
|
10024
|
+
${issue.message}`);
|
|
10025
|
+
}
|
|
10026
|
+
process.exit(1);
|
|
10027
|
+
}
|
|
10028
|
+
|
|
10029
|
+
// server/security/runtime-preflight.ts
|
|
10030
|
+
assertRuntimeEnvironment();
|
|
10031
|
+
|
|
10032
|
+
// server.ts
|
|
9758
10033
|
var import_express4 = __toESM(require("express"), 1);
|
|
9759
10034
|
var import_path23 = __toESM(require("path"), 1);
|
|
9760
10035
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
@@ -10786,12 +11061,12 @@ var import_url = require("url");
|
|
|
10786
11061
|
|
|
10787
11062
|
// packages/core/db/backup-manager.ts
|
|
10788
11063
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
10789
|
-
var
|
|
11064
|
+
var import_node_path3 = __toESM(require("node:path"), 1);
|
|
10790
11065
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
10791
11066
|
|
|
10792
11067
|
// packages/core/db/secure-files.ts
|
|
10793
11068
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
10794
|
-
var
|
|
11069
|
+
var import_node_path2 = __toESM(require("node:path"), 1);
|
|
10795
11070
|
var SECURE_FILE_MODE = 384;
|
|
10796
11071
|
var SECURE_DIR_MODE = 448;
|
|
10797
11072
|
var GROUP_OTHER_BITS = 63;
|
|
@@ -10835,7 +11110,7 @@ function ensureSecureFile(filePath) {
|
|
|
10835
11110
|
hardenFile(filePath);
|
|
10836
11111
|
return;
|
|
10837
11112
|
}
|
|
10838
|
-
import_node_fs.default.mkdirSync(
|
|
11113
|
+
import_node_fs.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true });
|
|
10839
11114
|
const fd = import_node_fs.default.openSync(filePath, "a", SECURE_FILE_MODE);
|
|
10840
11115
|
import_node_fs.default.closeSync(fd);
|
|
10841
11116
|
} catch {
|
|
@@ -10853,15 +11128,15 @@ function secureDatabaseFiles(dbPath2) {
|
|
|
10853
11128
|
// packages/core/db/backup-manager.ts
|
|
10854
11129
|
function getDefaultDbPath() {
|
|
10855
11130
|
if (process.env.OPENLEARN_DB_PATH) {
|
|
10856
|
-
return
|
|
11131
|
+
return import_node_path3.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
|
|
10857
11132
|
}
|
|
10858
|
-
return
|
|
11133
|
+
return import_node_path3.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
|
|
10859
11134
|
}
|
|
10860
11135
|
function getDefaultBackupDir() {
|
|
10861
11136
|
if (process.env.BACKUP_DIR) {
|
|
10862
|
-
return
|
|
11137
|
+
return import_node_path3.default.resolve(process.cwd(), process.env.BACKUP_DIR);
|
|
10863
11138
|
}
|
|
10864
|
-
return
|
|
11139
|
+
return import_node_path3.default.resolve(process.cwd(), "backups");
|
|
10865
11140
|
}
|
|
10866
11141
|
function validateBackup(filePath) {
|
|
10867
11142
|
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
@@ -10915,7 +11190,7 @@ function pruneByPrefix(backupDir, prefix, keepCount) {
|
|
|
10915
11190
|
let pruned = 0;
|
|
10916
11191
|
for (const file of files.slice(keepCount)) {
|
|
10917
11192
|
try {
|
|
10918
|
-
import_node_fs2.default.unlinkSync(
|
|
11193
|
+
import_node_fs2.default.unlinkSync(import_node_path3.default.join(backupDir, file));
|
|
10919
11194
|
pruned++;
|
|
10920
11195
|
} catch {
|
|
10921
11196
|
}
|
|
@@ -10934,15 +11209,15 @@ function pruneMigratorSnapshots(backupDir = getDefaultBackupDir(), keepCount) {
|
|
|
10934
11209
|
return pruneByPrefix(backupDir, "pre_migration_", Math.max(1, keepCount ?? resolvePreMigrationKeep()));
|
|
10935
11210
|
}
|
|
10936
11211
|
function performBackup(options = {}) {
|
|
10937
|
-
const dbPath2 = options.dbPath ?
|
|
10938
|
-
const backupDir = options.backupDir ?
|
|
11212
|
+
const dbPath2 = options.dbPath ? import_node_path3.default.resolve(options.dbPath) : getDefaultDbPath();
|
|
11213
|
+
const backupDir = options.backupDir ? import_node_path3.default.resolve(options.backupDir) : getDefaultBackupDir();
|
|
10939
11214
|
const keep = typeof options.keep === "number" ? options.keep : 10;
|
|
10940
11215
|
if (!import_node_fs2.default.existsSync(backupDir)) {
|
|
10941
11216
|
ensureSecureDir(backupDir);
|
|
10942
11217
|
}
|
|
10943
11218
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
10944
11219
|
const filename = options.customName || `backup_${timestamp}.db`;
|
|
10945
|
-
const backupPath =
|
|
11220
|
+
const backupPath = import_node_path3.default.join(backupDir, filename);
|
|
10946
11221
|
if (import_node_fs2.default.existsSync(backupPath)) {
|
|
10947
11222
|
import_node_fs2.default.unlinkSync(backupPath);
|
|
10948
11223
|
}
|
|
@@ -10979,7 +11254,7 @@ function performBackup(options = {}) {
|
|
|
10979
11254
|
}
|
|
10980
11255
|
|
|
10981
11256
|
// packages/core/db/migrator.ts
|
|
10982
|
-
var
|
|
11257
|
+
var import_meta2 = {};
|
|
10983
11258
|
function splitSqlStatements(sql) {
|
|
10984
11259
|
return sql.split(";").map((s) => s.trim()).filter((s) => {
|
|
10985
11260
|
const content = s.replace(/--.*$/gm, "").trim();
|
|
@@ -11135,8 +11410,8 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11135
11410
|
const starts = options.startDirs ? [...options.startDirs] : [];
|
|
11136
11411
|
if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
|
|
11137
11412
|
try {
|
|
11138
|
-
if (!options.startDirs && typeof
|
|
11139
|
-
starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(
|
|
11413
|
+
if (!options.startDirs && typeof import_meta2 !== "undefined" && import_meta2.url) {
|
|
11414
|
+
starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta2.url)));
|
|
11140
11415
|
}
|
|
11141
11416
|
} catch {
|
|
11142
11417
|
}
|
|
@@ -11164,7 +11439,7 @@ function getDefaultMigrationsDir(options = {}) {
|
|
|
11164
11439
|
|
|
11165
11440
|
// packages/core/db/schema-golden.ts
|
|
11166
11441
|
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
11167
|
-
var
|
|
11442
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
11168
11443
|
var SCHEMA_GOLDEN_FILENAME = "schema-golden.json";
|
|
11169
11444
|
function extractSchemaSnapshot(db2) {
|
|
11170
11445
|
const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
|
|
@@ -11200,7 +11475,7 @@ function formatSchemaGoldenDrifts(drifts) {
|
|
|
11200
11475
|
).join("\n");
|
|
11201
11476
|
}
|
|
11202
11477
|
function loadSchemaGolden(migrationsDir) {
|
|
11203
|
-
const filePath =
|
|
11478
|
+
const filePath = import_node_path4.default.join(migrationsDir, SCHEMA_GOLDEN_FILENAME);
|
|
11204
11479
|
if (!import_node_fs3.default.existsSync(filePath)) {
|
|
11205
11480
|
throw new Error(
|
|
11206
11481
|
`[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 +12020,7 @@ function purgeAllExpiredData(db2, options) {
|
|
|
11745
12020
|
|
|
11746
12021
|
// packages/core/db/index.ts
|
|
11747
12022
|
init_api_key_crypto();
|
|
11748
|
-
var
|
|
12023
|
+
var import_meta3 = {};
|
|
11749
12024
|
var BCRYPT_ROUNDS = 10;
|
|
11750
12025
|
function hashPassword(pwd) {
|
|
11751
12026
|
return import_bcryptjs.default.hashSync(pwd, BCRYPT_ROUNDS);
|
|
@@ -11820,8 +12095,8 @@ if (process.env.VITEST) {
|
|
|
11820
12095
|
dbPath = import_path2.default.join(__dirname, "educational_os.db");
|
|
11821
12096
|
} else {
|
|
11822
12097
|
try {
|
|
11823
|
-
if (typeof
|
|
11824
|
-
const __filename2 = (0, import_url2.fileURLToPath)(
|
|
12098
|
+
if (typeof import_meta3 !== "undefined" && import_meta3.url) {
|
|
12099
|
+
const __filename2 = (0, import_url2.fileURLToPath)(import_meta3.url);
|
|
11825
12100
|
const __dir = import_path2.default.dirname(__filename2);
|
|
11826
12101
|
dbPath = import_path2.default.join(__dir, "educational_os.db");
|
|
11827
12102
|
} else {
|
|
@@ -14149,10 +14424,10 @@ function parseScoreFields(fields) {
|
|
|
14149
14424
|
if (typeof fields !== "string" || !fields.trim()) return [];
|
|
14150
14425
|
return fields.split(/[\n\r,,;;|]+/).map((f) => f.trim()).filter(Boolean);
|
|
14151
14426
|
}
|
|
14152
|
-
function getNested(raw,
|
|
14427
|
+
function getNested(raw, path47) {
|
|
14153
14428
|
if (raw === null || raw === void 0) return void 0;
|
|
14154
|
-
if (!
|
|
14155
|
-
const parts = String(
|
|
14429
|
+
if (!path47) return void 0;
|
|
14430
|
+
const parts = String(path47).split(".").map((p) => p.trim()).filter(Boolean);
|
|
14156
14431
|
let cursor = raw;
|
|
14157
14432
|
for (let i = 0; i < parts.length; i += 1) {
|
|
14158
14433
|
if (cursor === null || cursor === void 0) return void 0;
|
|
@@ -18152,7 +18427,7 @@ var import_path11 = __toESM(require("path"), 1);
|
|
|
18152
18427
|
// packages/core/plugin-host/base.ts
|
|
18153
18428
|
var import_path5 = __toESM(require("path"), 1);
|
|
18154
18429
|
var import_express = __toESM(require("express"), 1);
|
|
18155
|
-
var
|
|
18430
|
+
var import_node_module2 = require("node:module");
|
|
18156
18431
|
var import_node_url = require("node:url");
|
|
18157
18432
|
|
|
18158
18433
|
// packages/core/plugin-host/resource-tracker.ts
|
|
@@ -18438,7 +18713,7 @@ var ContributionRegistry = class {
|
|
|
18438
18713
|
// packages/core/plugin-host/base.ts
|
|
18439
18714
|
init_types();
|
|
18440
18715
|
init_errors2();
|
|
18441
|
-
var
|
|
18716
|
+
var import_meta4 = {};
|
|
18442
18717
|
var VALID_TRANSITIONS = {
|
|
18443
18718
|
["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
|
|
18444
18719
|
["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
|
|
@@ -18473,12 +18748,12 @@ function resolveHostSdkDir() {
|
|
|
18473
18748
|
const bases = [];
|
|
18474
18749
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
18475
18750
|
try {
|
|
18476
|
-
bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(
|
|
18751
|
+
bases.push(import_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta4.url)));
|
|
18477
18752
|
} catch {
|
|
18478
18753
|
}
|
|
18479
18754
|
for (const base of bases) {
|
|
18480
18755
|
try {
|
|
18481
|
-
const req = (0,
|
|
18756
|
+
const req = (0, import_node_module2.createRequire)(import_path5.default.join(base, "__openlearn_resolve__.js"));
|
|
18482
18757
|
return import_path5.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
|
|
18483
18758
|
} catch {
|
|
18484
18759
|
}
|
|
@@ -18743,10 +19018,10 @@ var import_semver2 = __toESM(require("semver"), 1);
|
|
|
18743
19018
|
init_types();
|
|
18744
19019
|
|
|
18745
19020
|
// packages/core/plugin-host/execution-policy.ts
|
|
18746
|
-
var
|
|
19021
|
+
var import_node_path9 = __toESM(require("node:path"), 1);
|
|
18747
19022
|
var import_node_url2 = require("node:url");
|
|
18748
19023
|
init_types();
|
|
18749
|
-
var
|
|
19024
|
+
var import_meta5 = {};
|
|
18750
19025
|
function trustedInlineAllowlist(env = process.env) {
|
|
18751
19026
|
return new Set(
|
|
18752
19027
|
(env.OPENLEARN_TRUSTED_INLINE_PLUGINS ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
@@ -18754,23 +19029,23 @@ function trustedInlineAllowlist(env = process.env) {
|
|
|
18754
19029
|
}
|
|
18755
19030
|
function builtinPluginsDir() {
|
|
18756
19031
|
try {
|
|
18757
|
-
const url =
|
|
19032
|
+
const url = import_meta5.url;
|
|
18758
19033
|
if (typeof url === "string" && url.startsWith("file:")) {
|
|
18759
|
-
return
|
|
19034
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname((0, import_node_url2.fileURLToPath)(url)), "../../plugins");
|
|
18760
19035
|
}
|
|
18761
19036
|
} catch {
|
|
18762
19037
|
}
|
|
18763
19038
|
if (typeof __dirname !== "undefined") {
|
|
18764
|
-
return
|
|
19039
|
+
return import_node_path9.default.resolve(__dirname, "../packages/plugins");
|
|
18765
19040
|
}
|
|
18766
|
-
return
|
|
19041
|
+
return import_node_path9.default.resolve(process.cwd(), "packages/plugins");
|
|
18767
19042
|
}
|
|
18768
19043
|
function isInsideBuiltinPluginsDir(sourcePath, builtinDir = builtinPluginsDir()) {
|
|
18769
19044
|
if (!sourcePath) return false;
|
|
18770
|
-
const root =
|
|
18771
|
-
const resolved =
|
|
18772
|
-
const relative2 =
|
|
18773
|
-
return relative2 === "" || !relative2.startsWith("..") && !
|
|
19045
|
+
const root = import_node_path9.default.resolve(builtinDir);
|
|
19046
|
+
const resolved = import_node_path9.default.resolve(sourcePath);
|
|
19047
|
+
const relative2 = import_node_path9.default.relative(root, resolved);
|
|
19048
|
+
return relative2 === "" || !relative2.startsWith("..") && !import_node_path9.default.isAbsolute(relative2);
|
|
18774
19049
|
}
|
|
18775
19050
|
function isTrustedInProcessPlugin(manifestId, env = process.env, sourcePath) {
|
|
18776
19051
|
if (isInsideBuiltinPluginsDir(sourcePath)) return true;
|
|
@@ -19733,6 +20008,37 @@ var PluginHostCore = class _PluginHostCore extends PluginHostBase {
|
|
|
19733
20008
|
console.warn(`[PluginHost] Failed to revoke stage guards for "${pluginId}":`, e);
|
|
19734
20009
|
}
|
|
19735
20010
|
}
|
|
20011
|
+
/**
|
|
20012
|
+
* 回收该插件注册的服务端题型(Issue #320)。
|
|
20013
|
+
*
|
|
20014
|
+
* 题型注册表(`IAssessmentGraderRegistry`)是插件用 `ctx.resolve(token)` 直取的
|
|
20015
|
+
* **裸注册表**,不像 `ctx.services.*` 那样经过 `ResourceTracker` 包装 —— 所以
|
|
20016
|
+
* `disposeAll()` 拿它没办法。不清理的后果已验证为真:
|
|
20017
|
+
*
|
|
20018
|
+
* 1. 停用插件后题型仍留在注册表里 —— 教师继续能出题、继续按旧闭包判分(“幽灵题型”),
|
|
20019
|
+
* `GET /api/assessment-question-types` 也继续把它列给前端;
|
|
20020
|
+
* 2. 重装/升级插件时 `registerQuestionType` 抛 `duplicate question type`,
|
|
20021
|
+
* 表现为「升级后插件激活失败」,必须重启宿主才能恢复。
|
|
20022
|
+
*
|
|
20023
|
+
* 与 `revokePluginStageGuards` 同构:注册方可能传安装行 UUID,也可能传
|
|
20024
|
+
* manifest.id(文档约定的就是后者),两侧都清一遍(幂等)。
|
|
20025
|
+
*/
|
|
20026
|
+
async revokePluginQuestionTypes(pluginId, manifestId) {
|
|
20027
|
+
try {
|
|
20028
|
+
const registry4 = await this.serviceRegistry.tryResolve(IAssessmentGraderRegistryToken);
|
|
20029
|
+
if (!registry4 || typeof registry4.clearOwnedQuestionTypes !== "function") return;
|
|
20030
|
+
let removed = 0;
|
|
20031
|
+
for (const owner of /* @__PURE__ */ new Set([pluginId, manifestId])) {
|
|
20032
|
+
if (!owner) continue;
|
|
20033
|
+
removed += registry4.clearOwnedQuestionTypes(owner) ?? 0;
|
|
20034
|
+
}
|
|
20035
|
+
if (removed > 0) {
|
|
20036
|
+
console.log(`[PluginHost] Revoked ${removed} question type(s) for plugin "${pluginId}"`);
|
|
20037
|
+
}
|
|
20038
|
+
} catch (e) {
|
|
20039
|
+
console.warn(`[PluginHost] Failed to revoke question types for "${pluginId}":`, e);
|
|
20040
|
+
}
|
|
20041
|
+
}
|
|
19736
20042
|
setPluginState(pluginId, state, opts = {}) {
|
|
19737
20043
|
this.pluginStates.set(pluginId, state);
|
|
19738
20044
|
if (!opts.persistDb) return;
|
|
@@ -19830,7 +20136,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19830
20136
|
/**
|
|
19831
20137
|
* 判断指定插件的某个请求路径是否为流式路由 (SSE)
|
|
19832
20138
|
*/
|
|
19833
|
-
isStreamRoute(pluginIdOrManifestId, method,
|
|
20139
|
+
isStreamRoute(pluginIdOrManifestId, method, path47) {
|
|
19834
20140
|
const upperMethod = method.toUpperCase();
|
|
19835
20141
|
const resolvedId = this.resolvePluginUuid(pluginIdOrManifestId);
|
|
19836
20142
|
const instance = this.pluginInstances.get(resolvedId) ?? this.pluginInstances.get(pluginIdOrManifestId);
|
|
@@ -19839,7 +20145,7 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19839
20145
|
for (const r of manifest.api.routes) {
|
|
19840
20146
|
if (r.method.toUpperCase() === upperMethod && r.streaming === true) {
|
|
19841
20147
|
const compiled = compileRoutePattern(r.path);
|
|
19842
|
-
if (compiled.regex.test(
|
|
20148
|
+
if (compiled.regex.test(path47)) {
|
|
19843
20149
|
return true;
|
|
19844
20150
|
}
|
|
19845
20151
|
}
|
|
@@ -19847,14 +20153,14 @@ var PluginHostHttp = class extends PluginHostCore {
|
|
|
19847
20153
|
}
|
|
19848
20154
|
if (!instance) return false;
|
|
19849
20155
|
if (instance.context?.http) {
|
|
19850
|
-
return instance.context.http.isStream(upperMethod,
|
|
20156
|
+
return instance.context.http.isStream(upperMethod, path47);
|
|
19851
20157
|
}
|
|
19852
20158
|
if (instance.workerRef) {
|
|
19853
20159
|
const routes = instance.workerRef.serviceHost.getRegisteredRoutes();
|
|
19854
20160
|
for (const r of routes) {
|
|
19855
20161
|
if (r.method === upperMethod && r.isStream) {
|
|
19856
20162
|
const compiled = compileRoutePattern(r.pattern);
|
|
19857
|
-
if (compiled.regex.test(
|
|
20163
|
+
if (compiled.regex.test(path47)) {
|
|
19858
20164
|
return true;
|
|
19859
20165
|
}
|
|
19860
20166
|
}
|
|
@@ -19928,7 +20234,7 @@ init_context_builder();
|
|
|
19928
20234
|
|
|
19929
20235
|
// packages/core/plugin-host/declarative-migrator.ts
|
|
19930
20236
|
var import_node_fs8 = __toESM(require("node:fs"), 1);
|
|
19931
|
-
var
|
|
20237
|
+
var import_node_path10 = __toESM(require("node:path"), 1);
|
|
19932
20238
|
init_errors2();
|
|
19933
20239
|
init_plugin_tables();
|
|
19934
20240
|
function ensurePluginDeclaredMigrationsTable(db2) {
|
|
@@ -19944,7 +20250,7 @@ function ensurePluginDeclaredMigrationsTable(db2) {
|
|
|
19944
20250
|
`);
|
|
19945
20251
|
}
|
|
19946
20252
|
function parseMigrationFileName(fileName) {
|
|
19947
|
-
const baseName =
|
|
20253
|
+
const baseName = import_node_path10.default.basename(fileName, ".sql");
|
|
19948
20254
|
const match = baseName.match(/^(\d+)[_-]?(.*)$/);
|
|
19949
20255
|
if (match) {
|
|
19950
20256
|
const version = parseInt(match[1], 10);
|
|
@@ -19967,7 +20273,7 @@ function loadPluginMigrations(migrationsDir) {
|
|
|
19967
20273
|
return a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
|
|
19968
20274
|
});
|
|
19969
20275
|
return files.map((file) => {
|
|
19970
|
-
const fullPath =
|
|
20276
|
+
const fullPath = import_node_path10.default.join(migrationsDir, file);
|
|
19971
20277
|
const content = import_node_fs8.default.readFileSync(fullPath, "utf8");
|
|
19972
20278
|
const { version, baseName } = parseMigrationFileName(file);
|
|
19973
20279
|
const parsed = parseMigrationSql(baseName, content);
|
|
@@ -20609,6 +20915,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20609
20915
|
this.resourceTracker.disposeAll(pluginId);
|
|
20610
20916
|
this.pluginInstances.delete(pluginId);
|
|
20611
20917
|
this.revokePluginContributions(pluginId);
|
|
20918
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20612
20919
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20613
20920
|
if (this._hotReloadController) {
|
|
20614
20921
|
this._hotReloadController.unregisterPlugin(pluginId);
|
|
@@ -20627,6 +20934,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20627
20934
|
this.resourceTracker.disposeAll(pluginId);
|
|
20628
20935
|
this.pluginInstances.delete(pluginId);
|
|
20629
20936
|
this.revokePluginContributions(pluginId);
|
|
20937
|
+
await this.revokePluginQuestionTypes(pluginId, deactManifest.id);
|
|
20630
20938
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20631
20939
|
if (actorId) {
|
|
20632
20940
|
await this.revokeCapabilities(actorId);
|
|
@@ -20662,6 +20970,7 @@ var PluginHostLifecycle = class extends PluginHostHttp {
|
|
|
20662
20970
|
this.resourceTracker.disposeAll(pluginId);
|
|
20663
20971
|
this.pluginInstances.delete(pluginId);
|
|
20664
20972
|
this.revokePluginContributions(pluginId);
|
|
20973
|
+
await this.revokePluginQuestionTypes(pluginId, instance?.manifest?.id ?? pluginId);
|
|
20665
20974
|
this.setPluginState(pluginId, "inactive" /* INACTIVE */, { persistDb: true });
|
|
20666
20975
|
if (actorId) {
|
|
20667
20976
|
await this.revokeCapabilities(actorId);
|
|
@@ -20725,26 +21034,26 @@ var import_semver3 = __toESM(require("semver"), 1);
|
|
|
20725
21034
|
|
|
20726
21035
|
// packages/core/version.ts
|
|
20727
21036
|
var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
20728
|
-
var
|
|
21037
|
+
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
20729
21038
|
var import_node_url5 = require("node:url");
|
|
20730
|
-
var
|
|
20731
|
-
var FALLBACK_VERSION = "0.7.
|
|
21039
|
+
var import_meta7 = {};
|
|
21040
|
+
var FALLBACK_VERSION = "0.7.6";
|
|
20732
21041
|
function readPlatformVersion() {
|
|
20733
21042
|
const bases = [];
|
|
20734
21043
|
if (typeof __dirname !== "undefined") bases.push(__dirname);
|
|
20735
21044
|
try {
|
|
20736
|
-
bases.push(
|
|
21045
|
+
bases.push(import_node_path13.default.dirname((0, import_node_url5.fileURLToPath)(import_meta7.url)));
|
|
20737
21046
|
} catch {
|
|
20738
21047
|
}
|
|
20739
21048
|
for (const base of bases) {
|
|
20740
21049
|
let dir = base;
|
|
20741
21050
|
for (; ; ) {
|
|
20742
21051
|
try {
|
|
20743
|
-
const pkg = JSON.parse(import_node_fs11.default.readFileSync(
|
|
21052
|
+
const pkg = JSON.parse(import_node_fs11.default.readFileSync(import_node_path13.default.join(dir, "package.json"), "utf-8"));
|
|
20744
21053
|
if (pkg.name === "openlearn-next" && typeof pkg.version === "string") return pkg.version;
|
|
20745
21054
|
} catch {
|
|
20746
21055
|
}
|
|
20747
|
-
const parent =
|
|
21056
|
+
const parent = import_node_path13.default.dirname(dir);
|
|
20748
21057
|
if (parent === dir) break;
|
|
20749
21058
|
dir = parent;
|
|
20750
21059
|
}
|
|
@@ -21464,7 +21773,7 @@ init_audit_log();
|
|
|
21464
21773
|
// packages/core/plugin-host/dependency-install.ts
|
|
21465
21774
|
var import_node_child_process2 = require("node:child_process");
|
|
21466
21775
|
var import_node_fs12 = __toESM(require("node:fs"), 1);
|
|
21467
|
-
var
|
|
21776
|
+
var import_node_path14 = __toESM(require("node:path"), 1);
|
|
21468
21777
|
var DependencyInstallError = class extends Error {
|
|
21469
21778
|
constructor(message, pluginId, cause) {
|
|
21470
21779
|
super(message);
|
|
@@ -21499,7 +21808,7 @@ function resolveRegistry() {
|
|
|
21499
21808
|
function assertDependenciesResolved(pluginDir, dependencies, pluginId) {
|
|
21500
21809
|
const names = dependencyNames(dependencies);
|
|
21501
21810
|
if (names.length === 0) return;
|
|
21502
|
-
const lockPath =
|
|
21811
|
+
const lockPath = import_node_path14.default.join(pluginDir, "package-lock.json");
|
|
21503
21812
|
if (!import_node_fs12.default.existsSync(lockPath)) {
|
|
21504
21813
|
throw new DependencyInstallError(
|
|
21505
21814
|
`\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 +21890,7 @@ function installPluginDependencies(pluginDir, opts, runNpm = import_node_child_p
|
|
|
21581
21890
|
const { pluginId, dependencies, operation = "install" } = opts;
|
|
21582
21891
|
const depMap = toNpmDependencyMap(dependencies);
|
|
21583
21892
|
if (Object.keys(depMap).length === 0) return;
|
|
21584
|
-
const pkgJsonPath =
|
|
21893
|
+
const pkgJsonPath = import_node_path14.default.join(pluginDir, "package.json");
|
|
21585
21894
|
import_node_fs12.default.writeFileSync(
|
|
21586
21895
|
pkgJsonPath,
|
|
21587
21896
|
JSON.stringify({ name: pluginId, version: "0.0.0", dependencies: depMap }, null, 2),
|
|
@@ -22411,6 +22720,7 @@ var PluginHostUninstall = class extends PluginHostInstall {
|
|
|
22411
22720
|
return pluginId;
|
|
22412
22721
|
}
|
|
22413
22722
|
})();
|
|
22723
|
+
await this.revokePluginQuestionTypes(pluginId, manifestId);
|
|
22414
22724
|
if (state === "uninstalled" /* UNINSTALLED */ && !row) {
|
|
22415
22725
|
this.pluginStates.delete(pluginId);
|
|
22416
22726
|
return {
|
|
@@ -32668,7 +32978,7 @@ ${formatEnvIssues(warnings)}`);
|
|
|
32668
32978
|
|
|
32669
32979
|
// server/security/file-permissions.ts
|
|
32670
32980
|
var import_node_fs14 = __toESM(require("node:fs"), 1);
|
|
32671
|
-
var
|
|
32981
|
+
var import_node_path15 = __toESM(require("node:path"), 1);
|
|
32672
32982
|
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
32983
|
function modeOf(p) {
|
|
32674
32984
|
try {
|
|
@@ -32678,13 +32988,13 @@ function modeOf(p) {
|
|
|
32678
32988
|
}
|
|
32679
32989
|
}
|
|
32680
32990
|
function defaultPaths() {
|
|
32681
|
-
const dbPath2 = process.env.OPENLEARN_DB_PATH ?
|
|
32682
|
-
const backupDir = process.env.BACKUP_DIR ?
|
|
32991
|
+
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");
|
|
32992
|
+
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
32993
|
return {
|
|
32684
|
-
envPath:
|
|
32994
|
+
envPath: import_node_path15.default.resolve(process.cwd(), ".env"),
|
|
32685
32995
|
dbPath: dbPath2,
|
|
32686
32996
|
backupDir,
|
|
32687
|
-
encryptionKeyPath:
|
|
32997
|
+
encryptionKeyPath: import_node_path15.default.resolve(process.cwd(), "storage", "encryption-key")
|
|
32688
32998
|
};
|
|
32689
32999
|
}
|
|
32690
33000
|
function checkSensitiveFilePermissions(options = {}) {
|
|
@@ -32719,7 +33029,7 @@ function checkSensitiveFilePermissions(options = {}) {
|
|
|
32719
33029
|
if (scanned >= maxBackupFiles) break;
|
|
32720
33030
|
if (!entry.endsWith(".db")) continue;
|
|
32721
33031
|
scanned++;
|
|
32722
|
-
checkFile(
|
|
33032
|
+
checkFile(import_node_path15.default.join(backupDir, entry), "600", "backup-file");
|
|
32723
33033
|
}
|
|
32724
33034
|
}
|
|
32725
33035
|
return issues;
|
|
@@ -33620,7 +33930,7 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
33620
33930
|
|
|
33621
33931
|
// server/backup-scheduler.ts
|
|
33622
33932
|
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
33623
|
-
var
|
|
33933
|
+
var import_node_path16 = __toESM(require("node:path"), 1);
|
|
33624
33934
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
33625
33935
|
var DAY_MS2 = 24 * HOUR_MS;
|
|
33626
33936
|
var WAL_FULL_THRESHOLD_BYTES = 50 * 1024 * 1024;
|
|
@@ -33637,8 +33947,8 @@ function shouldFullCheckpoint(walBytes, threshold = WAL_FULL_THRESHOLD_BYTES) {
|
|
|
33637
33947
|
}
|
|
33638
33948
|
function resolveDbPath(explicit) {
|
|
33639
33949
|
if (explicit) return explicit;
|
|
33640
|
-
if (process.env.OPENLEARN_DB_PATH) return
|
|
33641
|
-
return
|
|
33950
|
+
if (process.env.OPENLEARN_DB_PATH) return import_node_path16.default.resolve(process.cwd(), process.env.OPENLEARN_DB_PATH);
|
|
33951
|
+
return import_node_path16.default.resolve(process.cwd(), "packages/core/db/educational_os.db");
|
|
33642
33952
|
}
|
|
33643
33953
|
function startBackupScheduler(deps) {
|
|
33644
33954
|
const noop = { stop: () => {
|
|
@@ -33757,9 +34067,9 @@ function isWhitelisted(route, whitelist) {
|
|
|
33757
34067
|
(entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
|
|
33758
34068
|
);
|
|
33759
34069
|
}
|
|
33760
|
-
function normalizePath2(
|
|
33761
|
-
if (!
|
|
33762
|
-
return
|
|
34070
|
+
function normalizePath2(path47) {
|
|
34071
|
+
if (!path47) return "/";
|
|
34072
|
+
return path47.endsWith("/") && path47.length > 1 ? path47.slice(0, -1) : path47;
|
|
33763
34073
|
}
|
|
33764
34074
|
function auditRouteScopes(app, whitelist = []) {
|
|
33765
34075
|
const declared = [];
|
|
@@ -33937,11 +34247,11 @@ function enforcePasswordChanged(req, res, next) {
|
|
|
33937
34247
|
if (isPasswordChangeGateSkipped()) {
|
|
33938
34248
|
return next();
|
|
33939
34249
|
}
|
|
33940
|
-
const
|
|
33941
|
-
if (!
|
|
34250
|
+
const path47 = req.path || "";
|
|
34251
|
+
if (!path47.startsWith("/api/")) {
|
|
33942
34252
|
return next();
|
|
33943
34253
|
}
|
|
33944
|
-
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(
|
|
34254
|
+
if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path47)) {
|
|
33945
34255
|
return next();
|
|
33946
34256
|
}
|
|
33947
34257
|
const token = getCookieToken(req);
|
|
@@ -34016,8 +34326,8 @@ function csrfGuard(req, res, next) {
|
|
|
34016
34326
|
next();
|
|
34017
34327
|
return;
|
|
34018
34328
|
}
|
|
34019
|
-
const
|
|
34020
|
-
if (EXEMPT_PREFIXES.some((p) =>
|
|
34329
|
+
const path48 = req.path;
|
|
34330
|
+
if (EXEMPT_PREFIXES.some((p) => path48 === p || path48.startsWith(p))) {
|
|
34021
34331
|
next();
|
|
34022
34332
|
return;
|
|
34023
34333
|
}
|
|
@@ -34027,10 +34337,10 @@ function csrfGuard(req, res, next) {
|
|
|
34027
34337
|
const origin = req.headers.origin;
|
|
34028
34338
|
const referer = req.headers.referer;
|
|
34029
34339
|
const host = req.headers.host;
|
|
34030
|
-
const
|
|
34340
|
+
const path47 = req.path;
|
|
34031
34341
|
if (typeof origin === "string" && origin !== "") {
|
|
34032
34342
|
if (origin === "null") {
|
|
34033
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34343
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34034
34344
|
res.status(403).json({ success: false, code: "FORBIDDEN_CROSS_SITE", error: "Opaque origin write request blocked" });
|
|
34035
34345
|
return;
|
|
34036
34346
|
}
|
|
@@ -34038,7 +34348,7 @@ function csrfGuard(req, res, next) {
|
|
|
34038
34348
|
try {
|
|
34039
34349
|
const originHost = new URL(origin).host;
|
|
34040
34350
|
if (host && originHost.toLowerCase() !== host.toLowerCase()) {
|
|
34041
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34351
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34042
34352
|
res.status(403).json({
|
|
34043
34353
|
success: false,
|
|
34044
34354
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34056,7 +34366,7 @@ function csrfGuard(req, res, next) {
|
|
|
34056
34366
|
try {
|
|
34057
34367
|
const refererHost = new URL(referer).host;
|
|
34058
34368
|
if (host && refererHost.toLowerCase() !== host.toLowerCase()) {
|
|
34059
|
-
if (!EXEMPT_PREFIXES.some((p) =>
|
|
34369
|
+
if (!EXEMPT_PREFIXES.some((p) => path47 === p || path47.startsWith(p))) {
|
|
34060
34370
|
res.status(403).json({
|
|
34061
34371
|
success: false,
|
|
34062
34372
|
code: "FORBIDDEN_CROSS_SITE",
|
|
@@ -34109,8 +34419,8 @@ function parseSentryDsn(dsn) {
|
|
|
34109
34419
|
const projectId = url.pathname.split("/").filter(Boolean).pop();
|
|
34110
34420
|
if (!projectId || !/^\d+$/.test(projectId)) return null;
|
|
34111
34421
|
const prefix = url.pathname.split("/").filter(Boolean).slice(0, -1).join("/");
|
|
34112
|
-
const
|
|
34113
|
-
const envelopeUrl = `${url.protocol}//${url.host}${
|
|
34422
|
+
const path47 = prefix ? `/${prefix}` : "";
|
|
34423
|
+
const envelopeUrl = `${url.protocol}//${url.host}${path47}/api/${projectId}/envelope/`;
|
|
34114
34424
|
return { envelopeUrl, publicKey };
|
|
34115
34425
|
} catch {
|
|
34116
34426
|
return null;
|
|
@@ -34551,14 +34861,14 @@ function createSocketIoCors(options) {
|
|
|
34551
34861
|
|
|
34552
34862
|
// server/routes/health.ts
|
|
34553
34863
|
var import_node_fs17 = __toESM(require("node:fs"), 1);
|
|
34554
|
-
var
|
|
34864
|
+
var import_node_path18 = __toESM(require("node:path"), 1);
|
|
34555
34865
|
|
|
34556
34866
|
// packages/core/observability/health.ts
|
|
34557
34867
|
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
34558
|
-
var
|
|
34868
|
+
var import_node_path17 = __toESM(require("node:path"), 1);
|
|
34559
34869
|
var import_node_crypto13 = require("node:crypto");
|
|
34560
34870
|
function probeStorageWritable(dir) {
|
|
34561
|
-
const probe =
|
|
34871
|
+
const probe = import_node_path17.default.join(dir, `.ready-${process.pid}-${(0, import_node_crypto13.randomBytes)(4).toString("hex")}`);
|
|
34562
34872
|
try {
|
|
34563
34873
|
import_node_fs16.default.mkdirSync(dir, { recursive: true });
|
|
34564
34874
|
import_node_fs16.default.writeFileSync(probe, "ok", { flag: "w" });
|
|
@@ -34575,14 +34885,14 @@ function probeStorageWritable(dir) {
|
|
|
34575
34885
|
function listMigrationNames(migrationsDir) {
|
|
34576
34886
|
try {
|
|
34577
34887
|
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) =>
|
|
34888
|
+
const names = import_node_fs16.default.readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).map((file) => import_node_path17.default.basename(file, ".sql"));
|
|
34579
34889
|
return names.length > 0 ? names : null;
|
|
34580
34890
|
} catch {
|
|
34581
34891
|
return null;
|
|
34582
34892
|
}
|
|
34583
34893
|
}
|
|
34584
34894
|
function migrationRecordName(name) {
|
|
34585
|
-
return
|
|
34895
|
+
return import_node_path17.default.basename(name.replace(/\r/g, "").trim(), ".sql");
|
|
34586
34896
|
}
|
|
34587
34897
|
function migrationsComplete(expected, applied) {
|
|
34588
34898
|
if (!expected) return { ok: false, reason: "migrations" };
|
|
@@ -34682,7 +34992,7 @@ function newestBackupAgeSeconds(backupDir, now) {
|
|
|
34682
34992
|
let newestMs = Number.NEGATIVE_INFINITY;
|
|
34683
34993
|
for (const name of names) {
|
|
34684
34994
|
try {
|
|
34685
|
-
const stat = import_node_fs17.default.statSync(
|
|
34995
|
+
const stat = import_node_fs17.default.statSync(import_node_path18.default.join(backupDir, name));
|
|
34686
34996
|
if (!stat.isFile()) continue;
|
|
34687
34997
|
if (stat.mtimeMs > newestMs) newestMs = stat.mtimeMs;
|
|
34688
34998
|
} catch {
|
|
@@ -34714,7 +35024,7 @@ function defaultCountEvents() {
|
|
|
34714
35024
|
function registerHealthRoutes(app, options) {
|
|
34715
35025
|
const startTime = options?.startTime ?? Date.now();
|
|
34716
35026
|
const platformVersion = options?.platformVersion ?? PLATFORM_VERSION;
|
|
34717
|
-
const migrationsDir = options?.migrationsDir ??
|
|
35027
|
+
const migrationsDir = options?.migrationsDir ?? import_node_path18.default.join(process.cwd(), "migrations");
|
|
34718
35028
|
const storageDir = options?.storageDir ?? getStorageDir();
|
|
34719
35029
|
const backupDir = options?.backupDir ?? getBackupsDir();
|
|
34720
35030
|
const countEvents = options?.countEvents ?? defaultCountEvents;
|
|
@@ -34835,7 +35145,7 @@ var import_zod2 = require("zod");
|
|
|
34835
35145
|
|
|
34836
35146
|
// server/services/course-package-assets.ts
|
|
34837
35147
|
var import_node_fs18 = __toESM(require("node:fs"), 1);
|
|
34838
|
-
var
|
|
35148
|
+
var import_node_path19 = __toESM(require("node:path"), 1);
|
|
34839
35149
|
var import_node_crypto14 = __toESM(require("node:crypto"), 1);
|
|
34840
35150
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
34841
35151
|
var import_jspdf = require("jspdf");
|
|
@@ -35102,7 +35412,7 @@ async function materializePackageAssets(assets) {
|
|
|
35102
35412
|
throw new Error(`\u8BFE\u7A0B\u5305\u8D44\u4EA7 "${asset.id}" \u58F0\u660E\u4E3A ${asset.contentType} \u4F46\u5185\u5BB9\u662F\u4E8C\u8FDB\u5236`);
|
|
35103
35413
|
}
|
|
35104
35414
|
const uniqueName = `${Date.now()}-${import_node_crypto14.default.randomBytes(4).toString("hex")}${ext}`;
|
|
35105
|
-
const filePath =
|
|
35415
|
+
const filePath = import_node_path19.default.join(uploadsDir, uniqueName);
|
|
35106
35416
|
import_node_fs18.default.writeFileSync(filePath, buffer);
|
|
35107
35417
|
writtenPaths.push(filePath);
|
|
35108
35418
|
const slideCount = isEmbeddableAssetType(asset.contentType) ? asset.contentType === "pdf" ? countPdfPages(buffer) : await countPptxSlides(buffer) : 0;
|
|
@@ -42066,7 +42376,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
42066
42376
|
|
|
42067
42377
|
// server/services/course-package-import.ts
|
|
42068
42378
|
var import_node_fs20 = __toESM(require("node:fs"), 1);
|
|
42069
|
-
var
|
|
42379
|
+
var import_node_path20 = __toESM(require("node:path"), 1);
|
|
42070
42380
|
var import_uuid18 = require("uuid");
|
|
42071
42381
|
|
|
42072
42382
|
// server/services/course-package-identity.ts
|
|
@@ -42341,8 +42651,8 @@ function findReferencedAsset(rawData, assets) {
|
|
|
42341
42651
|
function cleanupMaterializedAssets(materialized) {
|
|
42342
42652
|
const uploadsDir = getUploadsDir();
|
|
42343
42653
|
for (const asset of materialized.values()) {
|
|
42344
|
-
const fileName =
|
|
42345
|
-
const target =
|
|
42654
|
+
const fileName = import_node_path20.default.basename(asset.fileUrl);
|
|
42655
|
+
const target = import_node_path20.default.join(uploadsDir, fileName);
|
|
42346
42656
|
try {
|
|
42347
42657
|
if (import_node_fs20.default.existsSync(target)) {
|
|
42348
42658
|
import_node_fs20.default.unlinkSync(target);
|
|
@@ -46478,6 +46788,85 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
46478
46788
|
|
|
46479
46789
|
// server/routes/assignment-hub.ts
|
|
46480
46790
|
var import_express3 = __toESM(require("express"), 1);
|
|
46791
|
+
|
|
46792
|
+
// packages/core/assignment/file-preview-policy.ts
|
|
46793
|
+
function extensionOf(fileName) {
|
|
46794
|
+
if (typeof fileName !== "string") return "";
|
|
46795
|
+
const base = fileName.split(/[\\/]/).pop() || "";
|
|
46796
|
+
const dot = base.lastIndexOf(".");
|
|
46797
|
+
if (dot <= 0 || dot === base.length - 1) return "";
|
|
46798
|
+
return base.slice(dot).toLowerCase();
|
|
46799
|
+
}
|
|
46800
|
+
var PREVIEW_CONTENT_TYPES = Object.freeze({
|
|
46801
|
+
// 文档
|
|
46802
|
+
".pdf": "application/pdf",
|
|
46803
|
+
".rtf": "application/rtf",
|
|
46804
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
46805
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
46806
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
46807
|
+
".doc": "application/msword",
|
|
46808
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
46809
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
46810
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
46811
|
+
".xls": "application/vnd.ms-excel",
|
|
46812
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
46813
|
+
// 纯文本 / 代码 / 数据 —— 宿主按文本渲染
|
|
46814
|
+
".txt": "text/plain",
|
|
46815
|
+
".md": "text/plain",
|
|
46816
|
+
".csv": "text/plain",
|
|
46817
|
+
".json": "text/plain",
|
|
46818
|
+
".xml": "text/plain",
|
|
46819
|
+
".html": "text/plain",
|
|
46820
|
+
".css": "text/plain",
|
|
46821
|
+
".py": "text/plain",
|
|
46822
|
+
".js": "text/plain",
|
|
46823
|
+
".mjs": "text/plain",
|
|
46824
|
+
".cjs": "text/plain",
|
|
46825
|
+
".ts": "text/plain",
|
|
46826
|
+
".tsx": "text/plain",
|
|
46827
|
+
".jsx": "text/plain",
|
|
46828
|
+
".java": "text/plain",
|
|
46829
|
+
".c": "text/plain",
|
|
46830
|
+
".h": "text/plain",
|
|
46831
|
+
".cpp": "text/plain",
|
|
46832
|
+
".cs": "text/plain",
|
|
46833
|
+
".go": "text/plain",
|
|
46834
|
+
".rs": "text/plain",
|
|
46835
|
+
".rb": "text/plain",
|
|
46836
|
+
".php": "text/plain",
|
|
46837
|
+
".sql": "text/plain",
|
|
46838
|
+
".ino": "text/plain",
|
|
46839
|
+
// 图片
|
|
46840
|
+
".png": "image/png",
|
|
46841
|
+
".jpg": "image/jpeg",
|
|
46842
|
+
".jpeg": "image/jpeg",
|
|
46843
|
+
".gif": "image/gif",
|
|
46844
|
+
".webp": "image/webp",
|
|
46845
|
+
".bmp": "image/bmp",
|
|
46846
|
+
".svg": "image/svg+xml",
|
|
46847
|
+
".heic": "image/heic",
|
|
46848
|
+
// 压缩包
|
|
46849
|
+
".zip": "application/zip"
|
|
46850
|
+
});
|
|
46851
|
+
var INLINE_SAFE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
46852
|
+
"application/pdf",
|
|
46853
|
+
"text/plain",
|
|
46854
|
+
"image/png",
|
|
46855
|
+
"image/jpeg",
|
|
46856
|
+
"image/gif",
|
|
46857
|
+
"image/webp",
|
|
46858
|
+
"image/bmp"
|
|
46859
|
+
]);
|
|
46860
|
+
var TEXT_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
46861
|
+
function resolveContentType(fileName) {
|
|
46862
|
+
const extension = extensionOf(fileName);
|
|
46863
|
+
return PREVIEW_CONTENT_TYPES[extension] ?? "application/octet-stream";
|
|
46864
|
+
}
|
|
46865
|
+
function isInlineSafeContentType(contentType) {
|
|
46866
|
+
return INLINE_SAFE_CONTENT_TYPES.has((contentType || "").toLowerCase());
|
|
46867
|
+
}
|
|
46868
|
+
|
|
46869
|
+
// server/routes/assignment-hub.ts
|
|
46481
46870
|
function decodeFileName(raw) {
|
|
46482
46871
|
const text = String(raw ?? "").trim();
|
|
46483
46872
|
if (!text) return "";
|
|
@@ -46657,6 +47046,19 @@ function registerAssignmentHubRoutes(ctx) {
|
|
|
46657
47046
|
);
|
|
46658
47047
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
46659
47048
|
res.setHeader("Cache-Control", "private, no-store");
|
|
47049
|
+
const wantsInline = String(req.query.disposition || "").toLowerCase() === "inline";
|
|
47050
|
+
const contentType = resolveContentType(originalName);
|
|
47051
|
+
if (wantsInline && isInlineSafeContentType(contentType)) {
|
|
47052
|
+
res.setHeader("Content-Type", contentType);
|
|
47053
|
+
res.setHeader(
|
|
47054
|
+
"Content-Security-Policy",
|
|
47055
|
+
contentType === "application/pdf" ? "sandbox allow-scripts" : "sandbox"
|
|
47056
|
+
);
|
|
47057
|
+
const safeName = encodeURIComponent(originalName).replace(/['()]/g, escape);
|
|
47058
|
+
res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${safeName}`);
|
|
47059
|
+
res.sendFile(absPath);
|
|
47060
|
+
return;
|
|
47061
|
+
}
|
|
46660
47062
|
res.download(absPath, originalName);
|
|
46661
47063
|
} catch (e) {
|
|
46662
47064
|
sendHubError(res, e);
|
|
@@ -48422,7 +48824,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
48422
48824
|
init_audit_log();
|
|
48423
48825
|
|
|
48424
48826
|
// server/routes/plugin-api-gateway.ts
|
|
48425
|
-
var
|
|
48827
|
+
var import_node_path21 = __toESM(require("node:path"), 1);
|
|
48426
48828
|
init_http_router();
|
|
48427
48829
|
init_interfaces();
|
|
48428
48830
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set([
|
|
@@ -48596,7 +48998,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
48596
48998
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48597
48999
|
return;
|
|
48598
49000
|
}
|
|
48599
|
-
const normalizedSubPath =
|
|
49001
|
+
const normalizedSubPath = import_node_path21.default.posix.normalize("/" + rawSubPath);
|
|
48600
49002
|
if (normalizedSubPath.includes("..")) {
|
|
48601
49003
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
48602
49004
|
return;
|
|
@@ -49034,7 +49436,7 @@ init_fetch_with_retry();
|
|
|
49034
49436
|
// server/services/community-registry.ts
|
|
49035
49437
|
var import_semver7 = __toESM(require("semver"), 1);
|
|
49036
49438
|
var import_node_fs21 = __toESM(require("node:fs"), 1);
|
|
49037
|
-
var
|
|
49439
|
+
var import_node_path22 = __toESM(require("node:path"), 1);
|
|
49038
49440
|
var import_node_os3 = __toESM(require("node:os"), 1);
|
|
49039
49441
|
var import_node_crypto16 = require("node:crypto");
|
|
49040
49442
|
var COMMUNITY_REGISTRY_ENV = "PLUGIN_COMMUNITY_REGISTRY_URL";
|
|
@@ -49256,7 +49658,7 @@ async function downloadPluginPackage(downloadUrl, options = {}) {
|
|
|
49256
49658
|
if (!response.body) {
|
|
49257
49659
|
throw new Error("\u4E0B\u8F7D\u63D2\u4EF6\u5305\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A");
|
|
49258
49660
|
}
|
|
49259
|
-
const zipPath =
|
|
49661
|
+
const zipPath = import_node_path22.default.join(import_node_os3.default.tmpdir(), `plugin-download-${(0, import_node_crypto16.randomUUID)()}.zip`);
|
|
49260
49662
|
const out = import_node_fs21.default.createWriteStream(zipPath);
|
|
49261
49663
|
let received = 0;
|
|
49262
49664
|
try {
|