openlearn-next 0.3.18 → 0.3.20

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.
Files changed (40) hide show
  1. package/dist/assets/{AdminDirectoryView-BtdFA-PR.js → AdminDirectoryView-CGo33RNh.js} +1 -1
  2. package/dist/assets/{BatchPickerModal-BQ_vsb5h.js → BatchPickerModal-CasfiwEQ.js} +1 -1
  3. package/dist/assets/{ClassesView-D_m_99Wk.js → ClassesView-DIyJwydJ.js} +1 -1
  4. package/dist/assets/{CloudDriveModal-BHWCchwE.js → CloudDriveModal-C6fpWLLE.js} +1 -1
  5. package/dist/assets/{ComputerLabView-n5hzKtEx.js → ComputerLabView-DGha_J8B.js} +1 -1
  6. package/dist/assets/CourseManagement-2o8YUTDb.js +1 -0
  7. package/dist/assets/{CourseWizardModal-1F_Ksxgf.js → CourseWizardModal-Clo7hCCp.js} +1 -1
  8. package/dist/assets/{CoursewareHubPanel-B0JwbcQz.js → CoursewareHubPanel-CEK_QEc5.js} +1 -1
  9. package/dist/assets/{Dashboard-CgJNFUha.js → Dashboard-CBIiLzje.js} +1 -1
  10. package/dist/assets/{ExportWeightModal-B2wHglNt.js → ExportWeightModal-DkDuPsiM.js} +1 -1
  11. package/dist/assets/{HelpTour-C3G0EsYT.js → HelpTour-Dlc1vvy3.js} +1 -1
  12. package/dist/assets/{HelpView-Bgxlwpn_.js → HelpView-g1BzpeC9.js} +1 -1
  13. package/dist/assets/{ImportLessonsModal-xjfRYYa0.js → ImportLessonsModal-DMyN1EmC.js} +1 -1
  14. package/dist/assets/{ImportModal-ChLAX_xg.js → ImportModal-B2acbUT8.js} +1 -1
  15. package/dist/assets/{InteractiveCoursewareViewer-pi7sr3lt.js → InteractiveCoursewareViewer-DEicS9GQ.js} +1 -1
  16. package/dist/assets/{InteractiveWhiteboard-CsKeinZt.js → InteractiveWhiteboard-DbpOL5pI.js} +1 -1
  17. package/dist/assets/{LazyCourseware-Bw86DSzq.js → LazyCourseware-Bjy3qJWw.js} +2 -2
  18. package/dist/assets/{LazyWhiteboard-XI8iSIjM.js → LazyWhiteboard-D4Bj3dzG.js} +2 -2
  19. package/dist/assets/{LessonEditorView-3TnocL3v.js → LessonEditorView-DpU2g0Uq.js} +1 -1
  20. package/dist/assets/{LiveClassroomView-CQGaFkzD.js → LiveClassroomView-Bqrth_T2.js} +1 -1
  21. package/dist/assets/{NotificationDetailModal-CI4IfD2v.js → NotificationDetailModal-DC3AYUtD.js} +1 -1
  22. package/dist/assets/PluginView-S6TW3dLz.js +65 -0
  23. package/dist/assets/{ProcessLogsModal-uJqQQFyw.js → ProcessLogsModal-MtK-Pe4R.js} +1 -1
  24. package/dist/assets/{QuizGeneratorModal-Bul00fIH.js → QuizGeneratorModal-Bh6XOC83.js} +1 -1
  25. package/dist/assets/{StudentAssignmentView-CaOXzBkt.js → StudentAssignmentView-BCwbft0k.js} +1 -1
  26. package/dist/assets/{StudentLessonView-6l-A6scB.js → StudentLessonView-CewqFUl5.js} +1 -1
  27. package/dist/assets/{StudentPreviewModal-CnWC2qso.js → StudentPreviewModal-Blww1Gv5.js} +1 -1
  28. package/dist/assets/{StudentView-BtGPu4nl.js → StudentView-CR-84lLY.js} +2 -2
  29. package/dist/assets/{SystemResourceLibraryModal-CiRD12Rw.js → SystemResourceLibraryModal-Dyv2YJ7c.js} +1 -1
  30. package/dist/assets/{TeacherView-_iKPvpXI.js → TeacherView-Cszhv8bO.js} +2 -2
  31. package/dist/assets/{TimetableView-B0yL3omc.js → TimetableView-Do_LWpV2.js} +4 -4
  32. package/dist/assets/index-Ch_mD6B0.css +1 -0
  33. package/dist/assets/{index-AdOtUso0.js → index-CnuEzAfQ.js} +4 -4
  34. package/dist/assets/{vendor-icons-BHXru9a4.js → vendor-icons-B2DRJfZU.js} +1 -1
  35. package/dist/index.html +2 -2
  36. package/dist/server.cjs +422 -232
  37. package/package.json +17 -17
  38. package/dist/assets/CourseManagement-DamuhaYN.js +0 -1
  39. package/dist/assets/PluginView-LokCtvB8.js +0 -65
  40. package/dist/assets/index-CM3Tw-Rg.css +0 -1
package/dist/server.cjs CHANGED
@@ -549,15 +549,77 @@ var init_ai_event_bus = __esm({
549
549
  }
550
550
  });
551
551
 
552
- // packages/core/di/ai-service.ts
553
- function decryptKeyIfNeeded(encrypted) {
552
+ // packages/core/di/api-key-crypto.ts
553
+ function getEncryptionKey() {
554
+ if (_encryptionKey) return _encryptionKey;
555
+ const envPath = import_node_path.default.resolve(process.cwd(), ".env");
556
+ const keyHex = process.env.ENCRYPTION_KEY;
557
+ if (keyHex && keyHex.trim() !== "") {
558
+ _encryptionKey = Buffer.from(keyHex.trim(), "hex");
559
+ return _encryptionKey;
560
+ }
561
+ if (import_node_fs.default.existsSync(envPath)) {
562
+ try {
563
+ const content = import_node_fs.default.readFileSync(envPath, "utf-8");
564
+ const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
565
+ if (match && match[1].trim() !== "") {
566
+ const fileKeyHex = match[1].trim();
567
+ _encryptionKey = Buffer.from(fileKeyHex, "hex");
568
+ process.env.ENCRYPTION_KEY = fileKeyHex;
569
+ console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
570
+ return _encryptionKey;
571
+ }
572
+ } catch {
573
+ }
574
+ }
575
+ const newKey = import_node_crypto.default.randomBytes(32).toString("hex");
576
+ try {
577
+ if (import_node_fs.default.existsSync(envPath)) {
578
+ const existingContent = import_node_fs.default.readFileSync(envPath, "utf-8");
579
+ if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
580
+ console.warn(
581
+ "[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
582
+ );
583
+ process.env.ENCRYPTION_KEY = newKey;
584
+ _encryptionKey = Buffer.from(newKey, "hex");
585
+ return _encryptionKey;
586
+ }
587
+ import_node_fs.default.appendFileSync(envPath, `
588
+ ENCRYPTION_KEY=${newKey}
589
+ `);
590
+ console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
591
+ } else {
592
+ import_node_fs.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
593
+ `);
594
+ console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
595
+ }
596
+ } catch {
597
+ console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
598
+ }
599
+ process.env.ENCRYPTION_KEY = newKey;
600
+ _encryptionKey = Buffer.from(newKey, "hex");
601
+ return _encryptionKey;
602
+ }
603
+ function encryptApiKey(plaintext) {
604
+ if (!plaintext) return "";
605
+ const key = getEncryptionKey();
606
+ const iv = import_node_crypto.default.randomBytes(16);
607
+ const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", key, iv);
608
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
609
+ const authTag = cipher.getAuthTag();
610
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
611
+ }
612
+ function looksLikeCiphertext(value) {
613
+ const parts = (value || "").split(":");
614
+ if (parts.length !== 3) return false;
615
+ return parts.every((p) => p.length > 0 && /^[0-9a-fA-F]+$/.test(p));
616
+ }
617
+ function decryptApiKey(encrypted) {
554
618
  if (!encrypted) return "";
555
619
  const parts = encrypted.split(":");
556
620
  if (parts.length !== 3) return encrypted;
557
621
  try {
558
- const rawKey = process.env.ENCRYPTION_KEY;
559
- if (!rawKey) return encrypted;
560
- const key = rawKey.length === 64 ? Buffer.from(rawKey, "hex") : import_node_crypto.default.createHash("sha256").update(rawKey).digest();
622
+ const key = getEncryptionKey();
561
623
  const iv = Buffer.from(parts[0], "hex");
562
624
  const authTag = Buffer.from(parts[1], "hex");
563
625
  const ciphertext = Buffer.from(parts[2], "hex");
@@ -565,15 +627,27 @@ function decryptKeyIfNeeded(encrypted) {
565
627
  decipher.setAuthTag(authTag);
566
628
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
567
629
  } catch {
630
+ console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
568
631
  return encrypted;
569
632
  }
570
633
  }
571
- var import_node_crypto, AIService;
634
+ var import_node_crypto, import_node_fs, import_node_path, _encryptionKey;
635
+ var init_api_key_crypto = __esm({
636
+ "packages/core/di/api-key-crypto.ts"() {
637
+ import_node_crypto = __toESM(require("node:crypto"), 1);
638
+ import_node_fs = __toESM(require("node:fs"), 1);
639
+ import_node_path = __toESM(require("node:path"), 1);
640
+ _encryptionKey = null;
641
+ }
642
+ });
643
+
644
+ // packages/core/di/ai-service.ts
645
+ var AIService;
572
646
  var init_ai_service = __esm({
573
647
  "packages/core/di/ai-service.ts"() {
574
- import_node_crypto = __toESM(require("node:crypto"), 1);
575
648
  init_provider_gateway();
576
649
  init_ai_event_bus();
650
+ init_api_key_crypto();
577
651
  AIService = class {
578
652
  constructor(db2) {
579
653
  this.db = db2;
@@ -586,11 +660,20 @@ var init_ai_service = __esm({
586
660
  if (!provider || !provider.api_key) {
587
661
  throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002");
588
662
  }
663
+ const apiKey = decryptApiKey(provider.api_key);
664
+ if (!apiKey || !apiKey.trim()) {
665
+ throw new Error("AI Provider API Key \u89E3\u5BC6\u4E3A\u7A7A\uFF0C\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58\u5BC6\u94A5\u3002");
666
+ }
667
+ if (looksLikeCiphertext(apiKey)) {
668
+ throw new Error(
669
+ "AI Provider API Key \u89E3\u5BC6\u5931\u8D25\uFF1AENCRYPTION_KEY \u4E0E\u52A0\u5BC6\u65F6\u4E0D\u4E00\u81F4\uFF08\u5E38\u89C1\u4E8E PM2 \u5C06 ENCRYPTION_KEY \u7F6E\u7A7A\u6216\u5BC6\u94A5\u88AB\u8F6E\u6362\uFF09\u3002\u8BF7\u6062\u590D\u539F ENCRYPTION_KEY\uFF0C\u6216\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58 API Key\u3002"
670
+ );
671
+ }
589
672
  const config = {
590
673
  id: provider.id,
591
674
  name: provider.name,
592
675
  apiUrl: provider.api_url,
593
- apiKey: decryptKeyIfNeeded(provider.api_key),
676
+ apiKey,
594
677
  modelName: provider.model_name
595
678
  };
596
679
  return this.gateway.generateText(prompt, options, config);
@@ -969,18 +1052,18 @@ var init_http_router = __esm({
969
1052
  function createLogger(component) {
970
1053
  return logger.child({ component });
971
1054
  }
972
- var import_pino, import_node_fs, import_node_path, level, logDir, logFile, streams, logger;
1055
+ var import_pino, import_node_fs2, import_node_path2, level, logDir, logFile, streams, logger;
973
1056
  var init_logger = __esm({
974
1057
  "server/utils/logger.ts"() {
975
1058
  import_pino = __toESM(require("pino"), 1);
976
- import_node_fs = __toESM(require("node:fs"), 1);
977
- import_node_path = __toESM(require("node:path"), 1);
1059
+ import_node_fs2 = __toESM(require("node:fs"), 1);
1060
+ import_node_path2 = __toESM(require("node:path"), 1);
978
1061
  level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug");
979
- logDir = import_node_path.default.resolve(process.cwd(), "logs");
980
- if (!import_node_fs.default.existsSync(logDir)) {
981
- import_node_fs.default.mkdirSync(logDir, { recursive: true });
1062
+ logDir = import_node_path2.default.resolve(process.cwd(), "logs");
1063
+ if (!import_node_fs2.default.existsSync(logDir)) {
1064
+ import_node_fs2.default.mkdirSync(logDir, { recursive: true });
982
1065
  }
983
- logFile = import_node_path.default.join(logDir, "openlearn.log");
1066
+ logFile = import_node_path2.default.join(logDir, "openlearn.log");
984
1067
  streams = [];
985
1068
  if (process.env.NODE_ENV !== "production") {
986
1069
  streams.push({
@@ -1002,7 +1085,7 @@ var init_logger = __esm({
1002
1085
  }
1003
1086
  streams.push({
1004
1087
  level,
1005
- stream: import_node_fs.default.createWriteStream(logFile, { flags: "a" })
1088
+ stream: import_node_fs2.default.createWriteStream(logFile, { flags: "a" })
1006
1089
  });
1007
1090
  logger = (0, import_pino.default)({ level }, import_pino.default.multistream(streams));
1008
1091
  }
@@ -3509,12 +3592,12 @@ parentPort.on('message', async function(msg) {
3509
3592
  });
3510
3593
  `;
3511
3594
  }
3512
- var import_node_worker_threads, import_fs4, import_node_path2, import_uuid12, ALL_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
3595
+ var import_node_worker_threads, import_fs4, import_node_path3, import_uuid12, ALL_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
3513
3596
  var init_worker_manager = __esm({
3514
3597
  "packages/core/worker-runtime/worker-manager.ts"() {
3515
3598
  import_node_worker_threads = require("node:worker_threads");
3516
3599
  import_fs4 = __toESM(require("fs"), 1);
3517
- import_node_path2 = __toESM(require("node:path"), 1);
3600
+ import_node_path3 = __toESM(require("node:path"), 1);
3518
3601
  init_transport();
3519
3602
  init_service_host();
3520
3603
  init_logger();
@@ -3802,7 +3885,7 @@ var init_worker_manager = __esm({
3802
3885
  const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
3803
3886
  let worker;
3804
3887
  try {
3805
- const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(import_node_path2.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
3888
+ const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(import_node_path3.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
3806
3889
  worker = new import_node_worker_threads.Worker(new URL(bootstrapDataUrl), {
3807
3890
  // Pass both the DB id (`pluginId` — used as the actor/registry key)
3808
3891
  // and `manifestId` (used as the namespace prefix for command types).
@@ -4047,7 +4130,7 @@ __export(server_exports, {
4047
4130
  });
4048
4131
  module.exports = __toCommonJS(server_exports);
4049
4132
  var import_express3 = __toESM(require("express"), 1);
4050
- var import_path16 = __toESM(require("path"), 1);
4133
+ var import_path15 = __toESM(require("path"), 1);
4051
4134
  var import_dotenv = __toESM(require("dotenv"), 1);
4052
4135
  var import_os2 = __toESM(require("os"), 1);
4053
4136
  var import_child_process3 = require("child_process");
@@ -8698,13 +8781,14 @@ var import_fs5 = __toESM(require("fs"), 1);
8698
8781
  var import_jszip2 = __toESM(require("jszip"), 1);
8699
8782
  var import_path4 = __toESM(require("path"), 1);
8700
8783
  var import_express = __toESM(require("express"), 1);
8784
+ var import_node_module = require("node:module");
8701
8785
  var import_node_url = require("node:url");
8702
8786
 
8703
8787
  // packages/core/esm-loader/install-utils.ts
8704
8788
  var import_jszip = __toESM(require("jszip"), 1);
8705
- var fs3 = __toESM(require("fs"), 1);
8789
+ var fs4 = __toESM(require("fs"), 1);
8706
8790
  var os2 = __toESM(require("os"), 1);
8707
- var path4 = __toESM(require("path"), 1);
8791
+ var path5 = __toESM(require("path"), 1);
8708
8792
  var import_uuid10 = require("uuid");
8709
8793
  var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
8710
8794
  async function bundlePlugin(entryCode, resolveDir) {
@@ -8727,12 +8811,12 @@ async function bundlePlugin(entryCode, resolveDir) {
8727
8811
  setup(build) {
8728
8812
  build.onResolve({ filter: /.*/ }, (args) => {
8729
8813
  if (args.path.includes("core/di/interfaces")) {
8730
- const tsPath = path4.resolve(process.cwd(), "packages/core/di/interfaces.ts");
8731
- if (fs3.existsSync(tsPath)) {
8814
+ const tsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.ts");
8815
+ if (fs4.existsSync(tsPath)) {
8732
8816
  return { path: tsPath };
8733
8817
  }
8734
- const jsPath = path4.resolve(process.cwd(), "packages/core/di/interfaces.js");
8735
- if (fs3.existsSync(jsPath)) {
8818
+ const jsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.js");
8819
+ if (fs4.existsSync(jsPath)) {
8736
8820
  return { path: jsPath };
8737
8821
  }
8738
8822
  }
@@ -8788,22 +8872,22 @@ async function validateAndBundleZip(zipBuffer) {
8788
8872
  throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
8789
8873
  }
8790
8874
  const entryCode = await entryFile.async("string");
8791
- const tmpDir = path4.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
8875
+ const tmpDir = path5.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
8792
8876
  try {
8793
- fs3.mkdirSync(tmpDir, { recursive: true });
8877
+ fs4.mkdirSync(tmpDir, { recursive: true });
8794
8878
  for (const [name, file] of Object.entries(zip.files)) {
8795
8879
  if (file.dir) continue;
8796
- const filePath = path4.join(tmpDir, name);
8797
- const fileDir = path4.dirname(filePath);
8798
- fs3.mkdirSync(fileDir, { recursive: true });
8880
+ const filePath = path5.join(tmpDir, name);
8881
+ const fileDir = path5.dirname(filePath);
8882
+ fs4.mkdirSync(fileDir, { recursive: true });
8799
8883
  const content = await file.async("nodebuffer");
8800
- fs3.writeFileSync(filePath, content);
8884
+ fs4.writeFileSync(filePath, content);
8801
8885
  }
8802
8886
  const bundledCode = await bundlePlugin(entryCode, tmpDir);
8803
8887
  return { manifest, bundledCode, entryFileName: manifest.main };
8804
8888
  } finally {
8805
8889
  try {
8806
- fs3.rmSync(tmpDir, { recursive: true, force: true });
8890
+ fs4.rmSync(tmpDir, { recursive: true, force: true });
8807
8891
  } catch {
8808
8892
  }
8809
8893
  }
@@ -9605,6 +9689,7 @@ var PluginDistributionManager = class {
9605
9689
 
9606
9690
  // packages/core/plugin-host/index.ts
9607
9691
  init_http_router();
9692
+ var import_meta2 = {};
9608
9693
  var VALID_TRANSITIONS = {
9609
9694
  ["installed" /* INSTALLED */]: ["activating" /* ACTIVATING */, "uninstalled" /* UNINSTALLED */],
9610
9695
  ["activating" /* ACTIVATING */]: ["active" /* ACTIVE */, "error" /* ERROR */],
@@ -9635,6 +9720,22 @@ function createPluginStaticMiddleware(absDir) {
9635
9720
  import_express.default.static(absDir)
9636
9721
  ];
9637
9722
  }
9723
+ function resolveHostSdkDir() {
9724
+ const bases = [];
9725
+ if (typeof __dirname !== "undefined") bases.push(__dirname);
9726
+ try {
9727
+ bases.push(import_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta2.url)));
9728
+ } catch {
9729
+ }
9730
+ for (const base of bases) {
9731
+ try {
9732
+ const req = (0, import_node_module.createRequire)(import_path4.default.join(base, "__openlearn_resolve__.js"));
9733
+ return import_path4.default.dirname(req.resolve("@openlearn/plugin-sdk/package.json"));
9734
+ } catch {
9735
+ }
9736
+ }
9737
+ return null;
9738
+ }
9638
9739
  var PluginHost = class {
9639
9740
  constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
9640
9741
  this.serviceRegistry = serviceRegistry;
@@ -9721,6 +9822,33 @@ var PluginHost = class {
9721
9822
  getPluginManifestPath(pluginId) {
9722
9823
  return import_path4.default.join(this.getPluginDir(pluginId), "manifest.json");
9723
9824
  }
9825
+ /**
9826
+ * 确保宿主的 @openlearn/plugin-sdk 能被插件解析到。
9827
+ *
9828
+ * 插件 index.js 位于 `<pluginsDir>/<id>/`,Node 只从该文件所在目录向上查找
9829
+ * node_modules。宿主的 node_modules(npx 缓存、仓库目录等)通常不在其祖先链上,
9830
+ * 于是 external 的 `@openlearn/plugin-sdk` 会抛 ERR_MODULE_NOT_FOUND。
9831
+ * 这里在 `<pluginsDir>/node_modules/@openlearn/plugin-sdk` 建立指向宿主 SDK 的
9832
+ * 链接,使该目录下所有插件共享宿主的同一份 SDK。
9833
+ */
9834
+ ensureHostSdkResolution() {
9835
+ const linkPath = import_path4.default.join(this.pluginsDir, "node_modules", "@openlearn", "plugin-sdk");
9836
+ if (import_fs5.default.existsSync(linkPath)) return;
9837
+ const sdkDir = resolveHostSdkDir();
9838
+ if (!sdkDir) {
9839
+ console.warn(
9840
+ "[PluginHost] \u672A\u80FD\u5B9A\u4F4D\u5BBF\u4E3B\u7684 @openlearn/plugin-sdk\uFF0C\u63D2\u4EF6\u52A0\u8F7D\u53EF\u80FD\u56E0\u7F3A\u5C11\u8FD0\u884C\u65F6\u4F9D\u8D56\u800C\u5931\u8D25"
9841
+ );
9842
+ return;
9843
+ }
9844
+ try {
9845
+ import_fs5.default.mkdirSync(import_path4.default.dirname(linkPath), { recursive: true });
9846
+ import_fs5.default.symlinkSync(sdkDir, linkPath, "junction");
9847
+ console.log(`[PluginHost] Linked host @openlearn/plugin-sdk: ${linkPath} -> ${sdkDir}`);
9848
+ } catch (err) {
9849
+ console.warn("[PluginHost] \u5EFA\u7ACB @openlearn/plugin-sdk \u94FE\u63A5\u5931\u8D25:", err);
9850
+ }
9851
+ }
9724
9852
  // ── V5.2: RESTful API Gateway Dispatcher ───────────────────────────────
9725
9853
  /**
9726
9854
  * 向插件派发 HTTP 请求(自动路由到 Worker 隔离线程或 Inline 实例)
@@ -10421,6 +10549,7 @@ var PluginHost = class {
10421
10549
  try {
10422
10550
  let mod;
10423
10551
  if (process.env.NODE_ENV !== "test" && row.file_path && import_fs5.default.existsSync(row.file_path)) {
10552
+ this.ensureHostSdkResolution();
10424
10553
  const fileUrl = (0, import_node_url.pathToFileURL)(row.file_path);
10425
10554
  mod = await import(`${fileUrl.href}?t=${Date.now()}`);
10426
10555
  } else {
@@ -10843,8 +10972,14 @@ var PluginHost = class {
10843
10972
  }
10844
10973
  }
10845
10974
  const state = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
10846
- this.validateTransition(pluginId, state, "uninstalled" /* UNINSTALLED */);
10847
10975
  const row = this.db.prepare("SELECT manifest, file_path FROM plugins WHERE id = ?").get(pluginId);
10976
+ if (state === "uninstalled" /* UNINSTALLED */ && !row) {
10977
+ this.pluginStates.delete(pluginId);
10978
+ return;
10979
+ }
10980
+ if (state !== "uninstalled" /* UNINSTALLED */) {
10981
+ this.validateTransition(pluginId, state, "uninstalled" /* UNINSTALLED */);
10982
+ }
10848
10983
  const manifestId = (() => {
10849
10984
  if (!row) return pluginId;
10850
10985
  try {
@@ -19743,93 +19878,14 @@ function registerOfficialActivities(registry, actionRegistry) {
19743
19878
  var IActivityRegistryToken = new Token("@openlearn/activity-ecosystem:IActivityRegistry");
19744
19879
 
19745
19880
  // server/routes/os.ts
19746
- var import_path9 = __toESM(require("path"), 1);
19747
- var import_fs9 = __toESM(require("fs"), 1);
19881
+ var import_path8 = __toESM(require("path"), 1);
19882
+ var import_fs8 = __toESM(require("fs"), 1);
19748
19883
  var import_child_process = require("child_process");
19749
- var import_crypto7 = __toESM(require("crypto"), 1);
19884
+ var import_crypto6 = __toESM(require("crypto"), 1);
19750
19885
  var import_xss = require("xss");
19751
19886
 
19752
19887
  // server/utils/crypto.ts
19753
- var import_crypto5 = __toESM(require("crypto"), 1);
19754
- var import_fs8 = __toESM(require("fs"), 1);
19755
- var import_path7 = __toESM(require("path"), 1);
19756
- var _encryptionKey = null;
19757
- function getEncryptionKey() {
19758
- if (_encryptionKey) return _encryptionKey;
19759
- const envPath = import_path7.default.resolve(process.cwd(), ".env");
19760
- const keyHex = process.env.ENCRYPTION_KEY;
19761
- if (keyHex && keyHex.trim() !== "") {
19762
- _encryptionKey = Buffer.from(keyHex.trim(), "hex");
19763
- return _encryptionKey;
19764
- }
19765
- if (import_fs8.default.existsSync(envPath)) {
19766
- try {
19767
- const content = import_fs8.default.readFileSync(envPath, "utf-8");
19768
- const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
19769
- if (match && match[1].trim() !== "") {
19770
- const fileKeyHex = match[1].trim();
19771
- _encryptionKey = Buffer.from(fileKeyHex, "hex");
19772
- process.env.ENCRYPTION_KEY = fileKeyHex;
19773
- console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
19774
- return _encryptionKey;
19775
- }
19776
- } catch {
19777
- }
19778
- }
19779
- const newKey = import_crypto5.default.randomBytes(32).toString("hex");
19780
- try {
19781
- if (import_fs8.default.existsSync(envPath)) {
19782
- const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
19783
- if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
19784
- console.warn(
19785
- "[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
19786
- );
19787
- process.env.ENCRYPTION_KEY = newKey;
19788
- _encryptionKey = Buffer.from(newKey, "hex");
19789
- return _encryptionKey;
19790
- }
19791
- import_fs8.default.appendFileSync(envPath, `
19792
- ENCRYPTION_KEY=${newKey}
19793
- `);
19794
- console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
19795
- } else {
19796
- import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
19797
- `);
19798
- console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
19799
- }
19800
- } catch (e) {
19801
- console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
19802
- }
19803
- process.env.ENCRYPTION_KEY = newKey;
19804
- _encryptionKey = Buffer.from(newKey, "hex");
19805
- return _encryptionKey;
19806
- }
19807
- function encryptApiKey(plaintext) {
19808
- if (!plaintext) return "";
19809
- const key = getEncryptionKey();
19810
- const iv = import_crypto5.default.randomBytes(16);
19811
- const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
19812
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
19813
- const authTag = cipher.getAuthTag();
19814
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
19815
- }
19816
- function decryptApiKey(encrypted) {
19817
- if (!encrypted) return "";
19818
- const parts = encrypted.split(":");
19819
- if (parts.length !== 3) return encrypted;
19820
- try {
19821
- const key = getEncryptionKey();
19822
- const iv = Buffer.from(parts[0], "hex");
19823
- const authTag = Buffer.from(parts[1], "hex");
19824
- const ciphertext = Buffer.from(parts[2], "hex");
19825
- const decipher = import_crypto5.default.createDecipheriv("aes-256-gcm", key, iv);
19826
- decipher.setAuthTag(authTag);
19827
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
19828
- } catch {
19829
- console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
19830
- return encrypted;
19831
- }
19832
- }
19888
+ init_api_key_crypto();
19833
19889
  function maskApiKey(key) {
19834
19890
  if (!key || key.length <= 8) return key ? "****" : "";
19835
19891
  return key.substring(0, 4) + "****" + key.substring(key.length - 4);
@@ -19847,8 +19903,8 @@ function detectPromptInjection(input) {
19847
19903
  }
19848
19904
 
19849
19905
  // server/routes/shared.ts
19850
- var import_path8 = __toESM(require("path"), 1);
19851
- var import_crypto6 = __toESM(require("crypto"), 1);
19906
+ var import_path7 = __toESM(require("path"), 1);
19907
+ var import_crypto5 = __toESM(require("crypto"), 1);
19852
19908
 
19853
19909
  // server/utils/bridge-sdk.ts
19854
19910
  var BRIDGE_SDK_CODE = `(function() {
@@ -20315,7 +20371,7 @@ var BRIDGE_SDK_CODE = `(function() {
20315
20371
  const text = (el.textContent || el.innerText || '').trim();
20316
20372
  if (text) {
20317
20373
  logData.push("Selector '" + selector + "' matched text: '" + text + "'");
20318
- const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
20374
+ const fractionMatch = text.match(/(\\d+(\\.\\d+)?)\\s*[\\/|\u4E4B]\\s*(\\d+)/);
20319
20375
  if (fractionMatch) {
20320
20376
  const num = parseFloat(fractionMatch[1]);
20321
20377
  const den = parseFloat(fractionMatch[3]);
@@ -20325,7 +20381,7 @@ var BRIDGE_SDK_CODE = `(function() {
20325
20381
  return { score: pct, log: logData };
20326
20382
  }
20327
20383
  }
20328
- const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
20384
+ const match = text.match(/\\d+(\\.\\d+)?/);
20329
20385
  if (match) {
20330
20386
  const num = parseFloat(match[0]);
20331
20387
  if (!isNaN(num)) {
@@ -20366,7 +20422,7 @@ var BRIDGE_SDK_CODE = `(function() {
20366
20422
  if (txt) {
20367
20423
  const hasKey = txt.includes('\u5F97\u5206') || txt.includes('\u5206\u6570') || txt.includes('\u6210\u7EE9') || txt.toLowerCase().includes('score') || txt.toLowerCase().includes('points');
20368
20424
  if (hasKey) {
20369
- const m = txt.match(/\\\\d+(\\\\.\\\\d+)?/);
20425
+ const m = txt.match(/\\d+(\\.\\d+)?/);
20370
20426
  if (m) {
20371
20427
  const val = parseFloat(m[0]);
20372
20428
  logData.push("Fallback leaf <" + el.tagName + "> '" + txt + "' parsed: " + val);
@@ -20384,6 +20440,139 @@ var BRIDGE_SDK_CODE = `(function() {
20384
20440
  return { score: null, log: logData };
20385
20441
  }
20386
20442
 
20443
+ // --- RESULT-SCREEN AUTO SUBMIT ---------------------------------------
20444
+ // \u90E8\u5206\u8BFE\u4EF6\u7B54\u5B8C\u9898\u540E\u76F4\u63A5\u5207\u5230\u300C\u7ED3\u7B97 / \u7ED3\u679C\u9875\u300D\uFF0C\u5E76\u4E0D\u70B9\u51FB\u4EFB\u4F55\u63D0\u4EA4\u6309\u94AE\u3002
20445
+ // \u8FD9\u91CC\u76D1\u542C\u7ED3\u7B97\u9875\u51FA\u73B0\uFF0C\u81EA\u52A8\u6293\u53D6\u5206\u6570\u5E76\u4E0A\u62A5\u4E00\u6B21\u3002
20446
+ // \u53E3\u5F84\u4F18\u5148\u300C\u6B63\u786E\u9898\u6570 X/Y\u300D\u2192 \u767E\u5206\u5236\uFF0C\u5426\u5219\u56DE\u843D\u53EF\u89C1\u7684\u5206\u6570\u5143\u7D20\u3002
20447
+ var __lmsResultSubmitted = false;
20448
+ var __lmsResultScheduled = false;
20449
+ var __lmsResultPolling = false;
20450
+
20451
+ // \u5F3A\u7ED3\u675F\u4FE1\u53F7\uFF1A\u53EA\u8BA4\u7ED3\u7B97/\u7ED3\u679C\u9875\u6587\u6848\uFF0C\u907F\u514D\u7B54\u9898\u8FC7\u7A0B\u4E2D\u8BEF\u89E6\u53D1
20452
+ var __LMS_DONE_RE = /(\u95EF\u5173|\u6311\u6218|\u7B54\u9898|\u6D4B\u8BD5|\u6E38\u620F|\u672C\u8F6E|\u672C\u5173)(\u7ED3\u675F|\u5B8C\u6210|\u6210\u529F)|\u901A\u5173|\u7ED3\u7B97|\u67E5\u770B\u89E3\u6790|\u6B63\u786E\u7387|\u6700\u7EC8\u5F97\u5206|\u603B\u5F97\u5206|\u5168\u90E8\u7B54\u5B8C|\u7B54\u9898\u5B8C\u6BD5/;
20453
+ // \u7528 [0-9] / [^0-9] \u4EE3\u66FF \\d / \\s\uFF0C\u89C4\u907F\u6A21\u677F\u5B57\u7B26\u4E32\u7684\u8F6C\u4E49\u9677\u9631
20454
+ var __LMS_RATIO_RE = /([0-9]+(?:[.][0-9]+)?)[^0-9]*[/\uFF0F|\u4E4B][^0-9]*([0-9]+)/;
20455
+ var __LMS_NUM_RE = /[0-9]+(?:[.][0-9]+)?/;
20456
+
20457
+ function __lmsVisibleText() {
20458
+ try {
20459
+ if (!document.body) return '';
20460
+ if (typeof document.body.innerText === 'string') return document.body.innerText;
20461
+ return document.body.textContent || '';
20462
+ } catch (e) { return ''; }
20463
+ }
20464
+
20465
+ function __lmsIsShown(el) {
20466
+ try {
20467
+ if (!el || !el.getBoundingClientRect) return false;
20468
+ var rect = el.getBoundingClientRect();
20469
+ if (rect.width <= 0 && rect.height <= 0) return false;
20470
+ var node = el;
20471
+ while (node && node.nodeType === 1) {
20472
+ var st = window.getComputedStyle ? window.getComputedStyle(node) : null;
20473
+ if (st && (st.display === 'none' || st.visibility === 'hidden')) return false;
20474
+ node = node.parentElement;
20475
+ }
20476
+ return true;
20477
+ } catch (e) { return false; }
20478
+ }
20479
+
20480
+ // \u4EC5\u4ECE\u300C\u53EF\u89C1\u300D\u7684\u5206\u6570\u5143\u7D20\u53D6\u503C\uFF0C\u907F\u514D\u7ED3\u7B97\u9875\u5C1A\u672A\u5C55\u5F00\u65F6\u8BFB\u5230\u9690\u85CF\u7684\u521D\u59CB\u503C 0
20481
+ function __lmsFindVisibleScore() {
20482
+ var selectors = [
20483
+ '#finalScore', '#score', '#scoreDisplay', '.score', '.final-score',
20484
+ '[id*="score" i]', '[id*="point" i]', '[id*="grade" i]'
20485
+ ];
20486
+ for (var i = 0; i < selectors.length; i++) {
20487
+ var nodes = [];
20488
+ try { nodes = document.querySelectorAll(selectors[i]); } catch (e) { nodes = []; }
20489
+ for (var j = 0; j < nodes.length; j++) {
20490
+ if (!__lmsIsShown(nodes[j])) continue;
20491
+ var txt = (nodes[j].textContent || '').trim();
20492
+ if (!txt) continue;
20493
+ var frac = txt.match(__LMS_RATIO_RE);
20494
+ if (frac) {
20495
+ var fn = parseFloat(frac[1]);
20496
+ var fd = parseFloat(frac[2]);
20497
+ if (fd > 0 && fn >= 0 && fn <= fd) return Math.round((fn / fd) * 10000) / 100;
20498
+ }
20499
+ var num = txt.match(__LMS_NUM_RE);
20500
+ if (num) return parseFloat(num[0]);
20501
+ }
20502
+ }
20503
+ return null;
20504
+ }
20505
+
20506
+ function __lmsExtractResultScore() {
20507
+ // 1) \u6B63\u786E\u9898\u6570 X/Y \u2192 \u767E\u5206\u5236\uFF08\u53E3\u5F84\u6700\u7A33\u5B9A\uFF09
20508
+ var ratioSelectors = [
20509
+ '#correctCount', '[id*="correct" i]', '[id*="accuracy" i]',
20510
+ '[class*="correct" i]', '[class*="accuracy" i]', '[id*="ratio" i]'
20511
+ ];
20512
+ for (var i = 0; i < ratioSelectors.length; i++) {
20513
+ var nodes = [];
20514
+ try { nodes = document.querySelectorAll(ratioSelectors[i]); } catch (e) { nodes = []; }
20515
+ for (var j = 0; j < nodes.length; j++) {
20516
+ if (!__lmsIsShown(nodes[j])) continue;
20517
+ var m = (nodes[j].textContent || '').trim().match(__LMS_RATIO_RE);
20518
+ if (m) {
20519
+ var num = parseFloat(m[1]);
20520
+ var den = parseFloat(m[2]);
20521
+ if (den > 0 && num >= 0 && num <= den) return Math.round((num / den) * 10000) / 100;
20522
+ }
20523
+ }
20524
+ }
20525
+ // 2) \u56DE\u843D\uFF1A\u53EF\u89C1\u5206\u6570\u5143\u7D20
20526
+ return __lmsFindVisibleScore();
20527
+ }
20528
+
20529
+ function __lmsMaybeSubmitResult() {
20530
+ if (__lmsResultSubmitted || __lmsResultPolling) return;
20531
+ if (!__LMS_DONE_RE.test(__lmsVisibleText())) return;
20532
+
20533
+ __lmsResultPolling = true;
20534
+ // \u7ED3\u7B97\u9875 DOM \u53EF\u80FD\u5206\u6B65\u6E32\u67D3\uFF0C\u77ED\u8F6E\u8BE2\u53D6\u6700\u540E\u4E00\u6B21\u6709\u6548\u5206\u6570
20535
+ var tries = 0;
20536
+ var delays = [0, 120, 250, 400, 700, 1100];
20537
+ var latest = null;
20538
+
20539
+ function poll() {
20540
+ var s = __lmsExtractResultScore();
20541
+ if (s !== null && !isNaN(s)) latest = s;
20542
+ tries++;
20543
+ if (tries < delays.length) {
20544
+ setTimeout(poll, delays[tries]);
20545
+ return;
20546
+ }
20547
+ __lmsResultPolling = false;
20548
+ if (latest !== null && !__lmsResultSubmitted) {
20549
+ __lmsResultSubmitted = true;
20550
+ logToServer('Result screen detected, auto-submitting score: ' + latest);
20551
+ window.LMS.submit({ score: latest, completion: 1.0, comment: '\u7ED3\u7B97\u9875\u81EA\u52A8\u63D0\u53D6\u5F97\u5206' });
20552
+ }
20553
+ }
20554
+ poll();
20555
+ }
20556
+
20557
+ function initResultWatcher() {
20558
+ try {
20559
+ if (!document.body) return;
20560
+ var observer = new MutationObserver(function() {
20561
+ if (__lmsResultSubmitted || __lmsResultScheduled) return;
20562
+ __lmsResultScheduled = true;
20563
+ setTimeout(function() {
20564
+ __lmsResultScheduled = false;
20565
+ __lmsMaybeSubmitResult();
20566
+ }, 600);
20567
+ });
20568
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
20569
+ // \u5C11\u6570\u8BFE\u4EF6\u9996\u5C4F\u5373\u7ED3\u7B97
20570
+ setTimeout(__lmsMaybeSubmitResult, 800);
20571
+ } catch (e) {
20572
+ logToServer('Error in initResultWatcher: ' + e.message);
20573
+ }
20574
+ }
20575
+
20387
20576
  function attachListeners() {
20388
20577
  try {
20389
20578
  const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], .btn, .button');
@@ -20477,6 +20666,7 @@ var BRIDGE_SDK_CODE = `(function() {
20477
20666
  });
20478
20667
  observer.observe(document.body, { childList: true, subtree: true });
20479
20668
  attachListeners();
20669
+ initResultWatcher();
20480
20670
  } catch (e) {
20481
20671
  logToServer("Error in initAutoSubmit: " + e.message);
20482
20672
  }
@@ -20509,7 +20699,7 @@ function validateMagicBytes(buffer, fileName) {
20509
20699
  ".webp": [[82, 73, 70, 70]]
20510
20700
  // RIFF
20511
20701
  };
20512
- const ext = import_path8.default.extname(fileName || "").toLowerCase();
20702
+ const ext = import_path7.default.extname(fileName || "").toLowerCase();
20513
20703
  const signatures = MAGIC_BYTES[ext];
20514
20704
  if (!signatures) return true;
20515
20705
  return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
@@ -20531,7 +20721,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20531
20721
  const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
20532
20722
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, session.studentId, "active");
20533
20723
  if (!attempt) {
20534
- const attemptId = "att_" + import_crypto6.default.randomBytes(8).toString("hex");
20724
+ const attemptId = "att_" + import_crypto5.default.randomBytes(8).toString("hex");
20535
20725
  kernelContainer.db.prepare(
20536
20726
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20537
20727
  ).run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
@@ -20546,7 +20736,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20546
20736
  } else if (session.role === "teacher" || session.role === "administrator") {
20547
20737
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
20548
20738
  if (!attempt) {
20549
- const attemptId = "att_teacher_" + import_crypto6.default.randomBytes(8).toString("hex");
20739
+ const attemptId = "att_teacher_" + import_crypto5.default.randomBytes(8).toString("hex");
20550
20740
  kernelContainer.db.prepare(
20551
20741
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20552
20742
  ).run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
@@ -20564,7 +20754,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20564
20754
  if (studentInfo.attempt_id === "guest-attempt") {
20565
20755
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
20566
20756
  if (!attempt) {
20567
- const attemptId = "att_guest_" + import_crypto6.default.randomBytes(8).toString("hex");
20757
+ const attemptId = "att_guest_" + import_crypto5.default.randomBytes(8).toString("hex");
20568
20758
  kernelContainer.db.prepare(
20569
20759
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20570
20760
  ).run(attemptId, cwInfo.id, "guest", Date.now(), "active");
@@ -20648,7 +20838,7 @@ function registerOsRoutes(ctx) {
20648
20838
  if (!filename || !base64Data) {
20649
20839
  return res.status(400).json({ error: "Filename and base64Data are required" });
20650
20840
  }
20651
- const ext = import_path9.default.extname(filename).toLowerCase();
20841
+ const ext = import_path8.default.extname(filename).toLowerCase();
20652
20842
  if (BLOCKED_EXTENSIONS.includes(ext)) {
20653
20843
  return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
20654
20844
  }
@@ -20660,13 +20850,13 @@ function registerOsRoutes(ctx) {
20660
20850
  if (!validateMagicBytes(fileBuffer, filename)) {
20661
20851
  return res.status(400).json({ error: "File content does not match the declared file type." });
20662
20852
  }
20663
- const uploadsDir = import_path9.default.join(process.cwd(), "uploads");
20664
- if (!import_fs9.default.existsSync(uploadsDir)) {
20665
- import_fs9.default.mkdirSync(uploadsDir, { recursive: true });
20853
+ const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
20854
+ if (!import_fs8.default.existsSync(uploadsDir)) {
20855
+ import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
20666
20856
  }
20667
- const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
20668
- const filePath = import_path9.default.join(uploadsDir, uniqueName);
20669
- import_fs9.default.writeFileSync(filePath, fileBuffer);
20857
+ const uniqueName = `${Date.now()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
20858
+ const filePath = import_path8.default.join(uploadsDir, uniqueName);
20859
+ import_fs8.default.writeFileSync(filePath, fileBuffer);
20670
20860
  let slideCount = 1;
20671
20861
  if (ext === ".pdf") {
20672
20862
  try {
@@ -20879,8 +21069,8 @@ function registerOsRoutes(ctx) {
20879
21069
  });
20880
21070
  if (result && typeof result.agentText === "string") {
20881
21071
  const now = Date.now();
20882
- kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "user", message, now);
20883
- kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto7.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
21072
+ kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "user", message, now);
21073
+ kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
20884
21074
  }
20885
21075
  res.json({
20886
21076
  success: true,
@@ -20929,8 +21119,8 @@ function registerOsRoutes(ctx) {
20929
21119
  }
20930
21120
 
20931
21121
  // server/routes/resources.ts
20932
- var import_path10 = __toESM(require("path"), 1);
20933
- var import_fs10 = __toESM(require("fs"), 1);
21122
+ var import_path9 = __toESM(require("path"), 1);
21123
+ var import_fs9 = __toESM(require("fs"), 1);
20934
21124
  function registerResourcesRoutes(ctx) {
20935
21125
  const { app } = ctx;
20936
21126
  app.get("/api/resources", (req, res) => {
@@ -20946,23 +21136,23 @@ function registerResourcesRoutes(ctx) {
20946
21136
  const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
20947
21137
  if (!resource) return res.status(404).send("Resource not found");
20948
21138
  if (resource.type === "html") {
20949
- const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ? import_path10.default.basename(resource.name.replace(/\\/g, "/")) : "index.html";
21139
+ const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ? import_path9.default.basename(resource.name.replace(/\\/g, "/")) : "index.html";
20950
21140
  const existingCw2 = kernelContainer.db.prepare("SELECT id FROM courseware WHERE id = ?").get(resource.id);
20951
21141
  if (!existingCw2) {
20952
21142
  kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "html", entryName, resource.created_at || Date.now());
20953
21143
  }
20954
21144
  try {
20955
- const storageDir = import_path10.default.resolve(process.cwd(), "storage", "courseware", resource.id);
20956
- if (!import_fs10.default.existsSync(storageDir)) {
20957
- import_fs10.default.mkdirSync(storageDir, { recursive: true });
21145
+ const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
21146
+ if (!import_fs9.default.existsSync(storageDir)) {
21147
+ import_fs9.default.mkdirSync(storageDir, { recursive: true });
20958
21148
  }
20959
- const namedPath = import_path10.default.resolve(storageDir, entryName);
20960
- if (!import_fs10.default.existsSync(namedPath)) {
20961
- import_fs10.default.writeFileSync(namedPath, resource.content || "", "utf8");
21149
+ const namedPath = import_path9.default.resolve(storageDir, entryName);
21150
+ if (!import_fs9.default.existsSync(namedPath)) {
21151
+ import_fs9.default.writeFileSync(namedPath, resource.content || "", "utf8");
20962
21152
  }
20963
- const indexHtmlPath = import_path10.default.resolve(storageDir, "index.html");
20964
- if (!import_fs10.default.existsSync(indexHtmlPath)) {
20965
- import_fs10.default.writeFileSync(indexHtmlPath, resource.content || "", "utf8");
21153
+ const indexHtmlPath = import_path9.default.resolve(storageDir, "index.html");
21154
+ if (!import_fs9.default.existsSync(indexHtmlPath)) {
21155
+ import_fs9.default.writeFileSync(indexHtmlPath, resource.content || "", "utf8");
20966
21156
  }
20967
21157
  } catch (storageErr) {
20968
21158
  console.warn("[resources] Failed to write courseware storage cache:", storageErr);
@@ -21002,21 +21192,21 @@ function registerResourcesRoutes(ctx) {
21002
21192
  kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "folder", indexFile.path, resource.created_at || Date.now());
21003
21193
  }
21004
21194
  try {
21005
- const storageDir = import_path10.default.resolve(process.cwd(), "storage", "courseware", resource.id);
21006
- if (!import_fs10.default.existsSync(storageDir)) {
21007
- import_fs10.default.mkdirSync(storageDir, { recursive: true });
21195
+ const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
21196
+ if (!import_fs9.default.existsSync(storageDir)) {
21197
+ import_fs9.default.mkdirSync(storageDir, { recursive: true });
21008
21198
  for (const f of files) {
21009
21199
  if (f.path && f.content !== void 0) {
21010
21200
  const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
21011
- const target = import_path10.default.resolve(storageDir, cleanRel);
21201
+ const target = import_path9.default.resolve(storageDir, cleanRel);
21012
21202
  if (target.startsWith(storageDir)) {
21013
- import_fs10.default.mkdirSync(import_path10.default.dirname(target), { recursive: true });
21203
+ import_fs9.default.mkdirSync(import_path9.default.dirname(target), { recursive: true });
21014
21204
  const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
21015
21205
  if (isBin) {
21016
21206
  const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
21017
- import_fs10.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21207
+ import_fs9.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21018
21208
  } else {
21019
- import_fs10.default.writeFileSync(target, f.content, "utf8");
21209
+ import_fs9.default.writeFileSync(target, f.content, "utf8");
21020
21210
  }
21021
21211
  }
21022
21212
  }
@@ -21239,9 +21429,9 @@ function registerResourcesRoutes(ctx) {
21239
21429
  }
21240
21430
 
21241
21431
  // server/routes/courseware.ts
21242
- var import_path11 = __toESM(require("path"), 1);
21243
- var import_fs11 = __toESM(require("fs"), 1);
21244
- var import_crypto9 = __toESM(require("crypto"), 1);
21432
+ var import_path10 = __toESM(require("path"), 1);
21433
+ var import_fs10 = __toESM(require("fs"), 1);
21434
+ var import_crypto8 = __toESM(require("crypto"), 1);
21245
21435
  function registerCoursewareRoutes(ctx) {
21246
21436
  const { app, io } = ctx;
21247
21437
  app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
@@ -21391,7 +21581,7 @@ function registerCoursewareRoutes(ctx) {
21391
21581
  return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
21392
21582
  }
21393
21583
  }
21394
- const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
21584
+ const rawId = "raw_" + import_crypto8.default.randomBytes(8).toString("hex");
21395
21585
  kernelContainer.db.prepare(
21396
21586
  "INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
21397
21587
  ).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
@@ -21419,7 +21609,7 @@ function registerCoursewareRoutes(ctx) {
21419
21609
  kernelContainer.db.prepare(
21420
21610
  "INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
21421
21611
  ).run(
21422
- "res_" + import_crypto9.default.randomBytes(8).toString("hex"),
21612
+ "res_" + import_crypto8.default.randomBytes(8).toString("hex"),
21423
21613
  attemptId,
21424
21614
  parsedScore,
21425
21615
  comment || null,
@@ -21445,7 +21635,7 @@ function registerCoursewareRoutes(ctx) {
21445
21635
  }
21446
21636
  io.emit("courseware-attempt-updated", { attemptId, type: "log" });
21447
21637
  void kernelContainer.eventBus.publish({
21448
- id: "evt_" + import_crypto9.default.randomBytes(8).toString("hex"),
21638
+ id: "evt_" + import_crypto8.default.randomBytes(8).toString("hex"),
21449
21639
  type: "courseware.event_logged",
21450
21640
  source: "builtin.courseware",
21451
21641
  payload: { attemptId, eventType, payload },
@@ -21542,8 +21732,8 @@ function registerCoursewareRoutes(ctx) {
21542
21732
  const { msg, url, student, courseware } = req.body;
21543
21733
  const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
21544
21734
  console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
21545
- const logFile2 = import_path11.default.join(process.cwd(), "client_debug.log");
21546
- import_fs11.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
21735
+ const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
21736
+ import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
21547
21737
  `);
21548
21738
  res.json({ success: true });
21549
21739
  } catch (e) {
@@ -21619,7 +21809,7 @@ function registerCoursewareRoutes(ctx) {
21619
21809
  let assignment = kernelContainer.db.prepare("SELECT id FROM assignments WHERE class_id = ? AND lesson_id = ? AND title = ?").get(classId, lessonId, assignmentTitle);
21620
21810
  let assignmentId = assignment?.id;
21621
21811
  if (!assignmentId) {
21622
- assignmentId = "ast-cw-" + import_crypto9.default.randomBytes(8).toString("hex");
21812
+ assignmentId = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
21623
21813
  kernelContainer.db.prepare(
21624
21814
  "INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
21625
21815
  ).run(
@@ -21692,8 +21882,8 @@ function registerCoursewareRoutes(ctx) {
21692
21882
  }
21693
21883
 
21694
21884
  // server/routes/bridge.ts
21695
- var import_path12 = __toESM(require("path"), 1);
21696
- var import_fs12 = __toESM(require("fs"), 1);
21885
+ var import_path11 = __toESM(require("path"), 1);
21886
+ var import_fs11 = __toESM(require("fs"), 1);
21697
21887
  function registerBridgeRoutes(ctx) {
21698
21888
  const { app } = ctx;
21699
21889
  app.get("/bridge.js", (_req, res) => {
@@ -21713,7 +21903,7 @@ function registerBridgeRoutes(ctx) {
21713
21903
  if (!courseware) {
21714
21904
  const resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(uuid);
21715
21905
  if (resRow) {
21716
- const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path12.default.basename(resRow.name.replace(/\\/g, "/")) : "index.html";
21906
+ const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : "index.html";
21717
21907
  const createdAt = resRow.created_at || Date.now();
21718
21908
  kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resRow.id, resRow.id, resRow.name, resRow.type || "html", entryName, createdAt);
21719
21909
  courseware = {
@@ -21729,21 +21919,21 @@ function registerBridgeRoutes(ctx) {
21729
21919
  if (!courseware) {
21730
21920
  return res.status(404).send("Courseware not found");
21731
21921
  }
21732
- const storageDir = import_path12.default.resolve(process.cwd(), "storage", "courseware", courseware.uuid);
21733
- if (!import_fs12.default.existsSync(storageDir) || import_fs12.default.readdirSync(storageDir).length === 0) {
21922
+ const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", courseware.uuid);
21923
+ if (!import_fs11.default.existsSync(storageDir) || import_fs11.default.readdirSync(storageDir).length === 0) {
21734
21924
  let resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ? OR id = ?").get(courseware.uuid, courseware.id);
21735
21925
  if (!resRow && courseware.name) {
21736
21926
  const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
21737
21927
  resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE name = ? OR name = ?").get(courseware.name, nameCandidate);
21738
21928
  }
21739
21929
  if (resRow && resRow.content) {
21740
- import_fs12.default.mkdirSync(storageDir, { recursive: true });
21930
+ import_fs11.default.mkdirSync(storageDir, { recursive: true });
21741
21931
  if (resRow.type === "html") {
21742
- const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path12.default.basename(resRow.name.replace(/\\/g, "/")) : courseware.entry || "index.html";
21743
- const safeEntry = import_path12.default.basename(originalName.replace(/\\/g, "/"));
21744
- import_fs12.default.writeFileSync(import_path12.default.resolve(storageDir, safeEntry), resRow.content, "utf8");
21932
+ const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : courseware.entry || "index.html";
21933
+ const safeEntry = import_path11.default.basename(originalName.replace(/\\/g, "/"));
21934
+ import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, safeEntry), resRow.content, "utf8");
21745
21935
  if (safeEntry !== "index.html") {
21746
- import_fs12.default.writeFileSync(import_path12.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
21936
+ import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
21747
21937
  }
21748
21938
  } else if (resRow.type === "folder") {
21749
21939
  try {
@@ -21751,15 +21941,15 @@ function registerBridgeRoutes(ctx) {
21751
21941
  for (const f of files) {
21752
21942
  if (f.path && f.content !== void 0) {
21753
21943
  const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
21754
- const target = import_path12.default.resolve(storageDir, cleanRel);
21944
+ const target = import_path11.default.resolve(storageDir, cleanRel);
21755
21945
  if (target.startsWith(storageDir)) {
21756
- import_fs12.default.mkdirSync(import_path12.default.dirname(target), { recursive: true });
21946
+ import_fs11.default.mkdirSync(import_path11.default.dirname(target), { recursive: true });
21757
21947
  const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
21758
21948
  if (isBin) {
21759
21949
  const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
21760
- import_fs12.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21950
+ import_fs11.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21761
21951
  } else {
21762
- import_fs12.default.writeFileSync(target, f.content, "utf8");
21952
+ import_fs11.default.writeFileSync(target, f.content, "utf8");
21763
21953
  }
21764
21954
  }
21765
21955
  }
@@ -21773,35 +21963,35 @@ function registerBridgeRoutes(ctx) {
21773
21963
  if (!subpath || subpath === "") {
21774
21964
  subpath = courseware.entry || "index.html";
21775
21965
  }
21776
- let filePath = import_path12.default.resolve(storageDir, subpath);
21966
+ let filePath = import_path11.default.resolve(storageDir, subpath);
21777
21967
  if (!filePath.startsWith(storageDir)) {
21778
21968
  return res.status(403).send("Access denied");
21779
21969
  }
21780
- if (!import_fs12.default.existsSync(filePath) && import_fs12.default.existsSync(storageDir)) {
21781
- const dirEntries = import_fs12.default.readdirSync(storageDir);
21970
+ if (!import_fs11.default.existsSync(filePath) && import_fs11.default.existsSync(storageDir)) {
21971
+ const dirEntries = import_fs11.default.readdirSync(storageDir);
21782
21972
  const isLookingForEntry = !subpath || subpath === "" || subpath === "index.html" || subpath === "index.htm" || subpath === courseware.entry;
21783
21973
  if (isLookingForEntry) {
21784
21974
  let candidate = dirEntries.find(
21785
- (f) => f === courseware.entry && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile()
21975
+ (f) => f === courseware.entry && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
21786
21976
  );
21787
21977
  if (!candidate && courseware.name) {
21788
21978
  const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
21789
21979
  candidate = dirEntries.find(
21790
- (f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile()
21980
+ (f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
21791
21981
  );
21792
21982
  }
21793
21983
  if (!candidate) {
21794
21984
  candidate = dirEntries.find((f) => {
21795
21985
  const lower = f.toLowerCase();
21796
- return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile();
21986
+ return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile();
21797
21987
  });
21798
21988
  }
21799
21989
  if (candidate) {
21800
- filePath = import_path12.default.resolve(storageDir, candidate);
21990
+ filePath = import_path11.default.resolve(storageDir, candidate);
21801
21991
  try {
21802
- const indexHtmlPath = import_path12.default.resolve(storageDir, "index.html");
21803
- if (!import_fs12.default.existsSync(indexHtmlPath)) {
21804
- import_fs12.default.copyFileSync(filePath, indexHtmlPath);
21992
+ const indexHtmlPath = import_path11.default.resolve(storageDir, "index.html");
21993
+ if (!import_fs11.default.existsSync(indexHtmlPath)) {
21994
+ import_fs11.default.copyFileSync(filePath, indexHtmlPath);
21805
21995
  }
21806
21996
  if (courseware.entry !== candidate) {
21807
21997
  kernelContainer.db.prepare("UPDATE courseware SET entry = ? WHERE id = ?").run(candidate, courseware.id);
@@ -21812,9 +22002,9 @@ function registerBridgeRoutes(ctx) {
21812
22002
  }
21813
22003
  } else {
21814
22004
  for (const entry of dirEntries) {
21815
- const subDirPath = import_path12.default.join(storageDir, entry);
21816
- if (import_fs12.default.statSync(subDirPath).isDirectory()) {
21817
- const subEntries = import_fs12.default.readdirSync(subDirPath);
22005
+ const subDirPath = import_path11.default.join(storageDir, entry);
22006
+ if (import_fs11.default.statSync(subDirPath).isDirectory()) {
22007
+ const subEntries = import_fs11.default.readdirSync(subDirPath);
21818
22008
  const subCandidate = subEntries.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || subEntries.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
21819
22009
  if (subCandidate) {
21820
22010
  const redirectPath = `${entry}/${subCandidate}`;
@@ -21825,12 +22015,12 @@ function registerBridgeRoutes(ctx) {
21825
22015
  }
21826
22016
  }
21827
22017
  }
21828
- if (!import_fs12.default.existsSync(filePath)) {
22018
+ if (!import_fs11.default.existsSync(filePath)) {
21829
22019
  return res.status(404).send(`File not found: ${subpath}`);
21830
22020
  }
21831
- const stat = import_fs12.default.statSync(filePath);
22021
+ const stat = import_fs11.default.statSync(filePath);
21832
22022
  if (stat.isDirectory()) {
21833
- const dirFiles = import_fs12.default.readdirSync(filePath);
22023
+ const dirFiles = import_fs11.default.readdirSync(filePath);
21834
22024
  const htmlFile = dirFiles.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || dirFiles.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
21835
22025
  if (htmlFile) {
21836
22026
  const base = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
@@ -21838,7 +22028,7 @@ function registerBridgeRoutes(ctx) {
21838
22028
  }
21839
22029
  return res.status(404).send("Directory index not found");
21840
22030
  }
21841
- const ext = import_path12.default.extname(filePath).toLowerCase();
22031
+ const ext = import_path11.default.extname(filePath).toLowerCase();
21842
22032
  let contentType = "text/plain; charset=utf-8";
21843
22033
  if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
21844
22034
  else if (ext === ".css") contentType = "text/css; charset=utf-8";
@@ -21858,7 +22048,7 @@ function registerBridgeRoutes(ctx) {
21858
22048
  "Content-Security-Policy",
21859
22049
  "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src-attr 'unsafe-inline';"
21860
22050
  );
21861
- let html = import_fs12.default.readFileSync(filePath, "utf8");
22051
+ let html = import_fs11.default.readFileSync(filePath, "utf8");
21862
22052
  html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
21863
22053
  return res.send(html);
21864
22054
  } else {
@@ -22448,7 +22638,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
22448
22638
  }
22449
22639
 
22450
22640
  // server/routes/workspace.ts
22451
- var import_path13 = __toESM(require("path"), 1);
22641
+ var import_path12 = __toESM(require("path"), 1);
22452
22642
  function registerWorkspaceRoutes(ctx) {
22453
22643
  const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
22454
22644
  app.get("/api/events", requireAuth("administrator"), (req, res) => {
@@ -22597,7 +22787,7 @@ ${sub.content}`
22597
22787
  }
22598
22788
  const filename = parts[parts.length - 1];
22599
22789
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
22600
- const ext = import_path13.default.extname(filename).toLowerCase();
22790
+ const ext = import_path12.default.extname(filename).toLowerCase();
22601
22791
  const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
22602
22792
  const content = foundNode.content || "";
22603
22793
  if (binaryExtensions.includes(ext)) {
@@ -22839,9 +23029,9 @@ function registerAdminRoutes(ctx) {
22839
23029
  }
22840
23030
 
22841
23031
  // server/routes/roster.ts
22842
- var import_path14 = __toESM(require("path"), 1);
22843
- var import_fs13 = __toESM(require("fs"), 1);
22844
- var import_crypto10 = __toESM(require("crypto"), 1);
23032
+ var import_path13 = __toESM(require("path"), 1);
23033
+ var import_fs12 = __toESM(require("fs"), 1);
23034
+ var import_crypto9 = __toESM(require("crypto"), 1);
22845
23035
  var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
22846
23036
  function registerRosterRoutes(ctx) {
22847
23037
  const { app, io, loginLimiter } = ctx;
@@ -23095,7 +23285,7 @@ function registerRosterRoutes(ctx) {
23095
23285
  if (storedPwd.startsWith("$2")) {
23096
23286
  matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
23097
23287
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
23098
- matches = import_crypto10.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
23288
+ matches = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
23099
23289
  } else {
23100
23290
  matches = storedPwd === oldPassword;
23101
23291
  }
@@ -23152,7 +23342,7 @@ function registerRosterRoutes(ctx) {
23152
23342
  if (!filename || !base64Data) {
23153
23343
  return res.status(400).json({ error: "Filename and base64Data are required" });
23154
23344
  }
23155
- const ext = import_path14.default.extname(filename).toLowerCase();
23345
+ const ext = import_path13.default.extname(filename).toLowerCase();
23156
23346
  const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
23157
23347
  if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
23158
23348
  return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
@@ -23165,11 +23355,11 @@ function registerRosterRoutes(ctx) {
23165
23355
  if (!validateMagicBytes(fileBuffer, filename)) {
23166
23356
  return res.status(400).json({ error: "File content does not match the declared image type" });
23167
23357
  }
23168
- const avatarDir = import_path14.default.join(process.cwd(), "uploads", "avatars");
23169
- import_fs13.default.mkdirSync(avatarDir, { recursive: true });
23170
- const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
23171
- const filePath = import_path14.default.join(avatarDir, uniqueName);
23172
- import_fs13.default.writeFileSync(filePath, fileBuffer);
23358
+ const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
23359
+ import_fs12.default.mkdirSync(avatarDir, { recursive: true });
23360
+ const uniqueName = `${Date.now()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
23361
+ const filePath = import_path13.default.join(avatarDir, uniqueName);
23362
+ import_fs12.default.writeFileSync(filePath, fileBuffer);
23173
23363
  const avatarUrl = `/uploads/avatars/${uniqueName}`;
23174
23364
  let oldAvatar = null;
23175
23365
  if (session.role === "student") {
@@ -23183,7 +23373,7 @@ function registerRosterRoutes(ctx) {
23183
23373
  }
23184
23374
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
23185
23375
  try {
23186
- import_fs13.default.unlinkSync(import_path14.default.join(process.cwd(), oldAvatar));
23376
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
23187
23377
  } catch {
23188
23378
  }
23189
23379
  }
@@ -23219,7 +23409,7 @@ function registerRosterRoutes(ctx) {
23219
23409
  }
23220
23410
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
23221
23411
  try {
23222
- import_fs13.default.unlinkSync(import_path14.default.join(process.cwd(), oldAvatar));
23412
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
23223
23413
  } catch {
23224
23414
  }
23225
23415
  }
@@ -23286,7 +23476,7 @@ function registerRosterRoutes(ctx) {
23286
23476
  if (storedPwd.startsWith("$2")) {
23287
23477
  matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
23288
23478
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
23289
- const sha256Hash = import_crypto10.default.createHash("sha256").update(providedPassword).digest("hex");
23479
+ const sha256Hash = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
23290
23480
  if (sha256Hash === storedPwd) {
23291
23481
  matchesOwnPassword = true;
23292
23482
  kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
@@ -23330,7 +23520,7 @@ function registerRosterRoutes(ctx) {
23330
23520
  };
23331
23521
  }
23332
23522
  if (sessionData) {
23333
- const sessionToken = "token_" + import_crypto10.default.randomBytes(16).toString("hex");
23523
+ const sessionToken = "token_" + import_crypto9.default.randomBytes(16).toString("hex");
23334
23524
  const now = Date.now();
23335
23525
  const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
23336
23526
  kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
@@ -25168,8 +25358,8 @@ ${examsText}
25168
25358
 
25169
25359
  // server/routes/plugins.ts
25170
25360
  var import_express2 = __toESM(require("express"), 1);
25171
- var import_path15 = __toESM(require("path"), 1);
25172
- var import_fs14 = __toESM(require("fs"), 1);
25361
+ var import_path14 = __toESM(require("path"), 1);
25362
+ var import_fs13 = __toESM(require("fs"), 1);
25173
25363
  var import_semver3 = __toESM(require("semver"), 1);
25174
25364
 
25175
25365
  // server/services/version-fetcher.ts
@@ -25327,7 +25517,7 @@ async function checkVersion(source, installedVersion) {
25327
25517
  }
25328
25518
 
25329
25519
  // server/routes/plugin-api-gateway.ts
25330
- var import_node_path3 = __toESM(require("node:path"), 1);
25520
+ var import_node_path4 = __toESM(require("node:path"), 1);
25331
25521
  init_http_router();
25332
25522
  var RESERVED_ACTIONS = /* @__PURE__ */ new Set(["config", "toggle", "contributions", "check-update", "one-click-update"]);
25333
25523
  var FORBIDDEN_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
@@ -25461,7 +25651,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
25461
25651
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
25462
25652
  return;
25463
25653
  }
25464
- const normalizedSubPath = import_node_path3.default.posix.normalize("/" + rawSubPath);
25654
+ const normalizedSubPath = import_node_path4.default.posix.normalize("/" + rawSubPath);
25465
25655
  if (normalizedSubPath.includes("..")) {
25466
25656
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
25467
25657
  return;
@@ -25681,16 +25871,16 @@ data: ${JSON.stringify({ error: msg })}
25681
25871
 
25682
25872
  // server/routes/plugins.ts
25683
25873
  function findLocalPluginSource(manifestId) {
25684
- const root = import_path15.default.resolve(process.cwd(), "v2_plugins");
25685
- if (!import_fs14.default.existsSync(root)) return null;
25874
+ const root = import_path14.default.resolve(process.cwd(), "v2_plugins");
25875
+ if (!import_fs13.default.existsSync(root)) return null;
25686
25876
  let best = null;
25687
25877
  try {
25688
- for (const entry of import_fs14.default.readdirSync(root, { withFileTypes: true })) {
25878
+ for (const entry of import_fs13.default.readdirSync(root, { withFileTypes: true })) {
25689
25879
  if (!entry.isDirectory()) continue;
25690
- const manifestPath = import_path15.default.join(root, entry.name, "manifest.json");
25691
- if (!import_fs14.default.existsSync(manifestPath)) continue;
25880
+ const manifestPath = import_path14.default.join(root, entry.name, "manifest.json");
25881
+ if (!import_fs13.default.existsSync(manifestPath)) continue;
25692
25882
  try {
25693
- const m = JSON.parse(import_fs14.default.readFileSync(manifestPath, "utf-8"));
25883
+ const m = JSON.parse(import_fs13.default.readFileSync(manifestPath, "utf-8"));
25694
25884
  if (m?.id !== manifestId || !m?.version) continue;
25695
25885
  if (!best || import_semver3.default.gt(m.version, best.version)) best = { dir: entry.name, version: m.version };
25696
25886
  } catch {
@@ -25726,11 +25916,11 @@ function registerPluginsRoutes(ctx) {
25726
25916
  const { app, buildOpenAITools: buildOpenAITools2, executeAgentToolCall: executeAgentToolCall2, buildOpenAIChatUrl: buildOpenAIChatUrl2, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2 } = ctx;
25727
25917
  app.get("/api/docs/plugin-guide", (req, res) => {
25728
25918
  try {
25729
- const docPath = import_path15.default.join(process.cwd(), "docs_plugin_guide.md");
25730
- if (!import_fs14.default.existsSync(docPath)) {
25919
+ const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
25920
+ if (!import_fs13.default.existsSync(docPath)) {
25731
25921
  return res.status(404).json({ success: false, error: "Document not found" });
25732
25922
  }
25733
- const content = import_fs14.default.readFileSync(docPath, "utf-8");
25923
+ const content = import_fs13.default.readFileSync(docPath, "utf-8");
25734
25924
  res.json({ success: true, content });
25735
25925
  } catch (e) {
25736
25926
  sendSafeError(res, e);
@@ -25741,11 +25931,11 @@ function registerPluginsRoutes(ctx) {
25741
25931
  const limit = parseInt(req.query.limit) || 200;
25742
25932
  const component = req.query.component;
25743
25933
  const levelFilter = req.query.level;
25744
- const logFile2 = import_path15.default.resolve(process.cwd(), "logs", "openlearn.log");
25745
- if (!import_fs14.default.existsSync(logFile2)) {
25934
+ const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
25935
+ if (!import_fs13.default.existsSync(logFile2)) {
25746
25936
  return res.json({ success: true, logs: [] });
25747
25937
  }
25748
- const content = import_fs14.default.readFileSync(logFile2, "utf-8");
25938
+ const content = import_fs13.default.readFileSync(logFile2, "utf-8");
25749
25939
  const lines = content.split("\n").filter(Boolean);
25750
25940
  let parsedLogs = lines.map((line) => {
25751
25941
  try {
@@ -26307,7 +26497,7 @@ async function startServer() {
26307
26497
  config: { port: Number(process.env.PORT) || 9e3 }
26308
26498
  });
26309
26499
  try {
26310
- const migrationsDir = import_path16.default.join(process.cwd(), "migrations");
26500
+ const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
26311
26501
  const migrations = loadMigrationsFromDirectory(migrationsDir);
26312
26502
  if (migrations.length > 0) {
26313
26503
  runMigrations(kernelContainer.db, migrations);
@@ -26371,8 +26561,8 @@ async function startServer() {
26371
26561
  });
26372
26562
  app.use(import_express3.default.json({ limit: "10mb" }));
26373
26563
  app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
26374
- app.use("/uploads", import_express3.default.static(import_path16.default.join(process.cwd(), "uploads")));
26375
- app.use("/plugins", import_express3.default.static(import_path16.default.join(process.cwd(), "plugins")));
26564
+ app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
26565
+ app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
26376
26566
  const configuredOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean) : [];
26377
26567
  const isOriginAllowed = (origin, hostHeader) => {
26378
26568
  if (!origin) return true;
@@ -26545,10 +26735,10 @@ async function startServer() {
26545
26735
  });
26546
26736
  app.use(vite.middlewares);
26547
26737
  } else {
26548
- const distPath = __dirname || import_path16.default.join(process.cwd(), "dist");
26738
+ const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
26549
26739
  app.use(import_express3.default.static(distPath));
26550
26740
  app.get("*", (req, res) => {
26551
- res.sendFile(import_path16.default.join(distPath, "index.html"));
26741
+ res.sendFile(import_path15.default.join(distPath, "index.html"));
26552
26742
  });
26553
26743
  }
26554
26744
  const HOST = process.env.HOST || "0.0.0.0";