openlearn-next 0.3.18 → 0.3.19

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 (37) hide show
  1. package/dist/assets/{AdminDirectoryView-BtdFA-PR.js → AdminDirectoryView-CGo33RNh.js} +1 -1
  2. package/dist/assets/{BatchPickerModal-BQ_vsb5h.js → BatchPickerModal-MnNnNwFT.js} +1 -1
  3. package/dist/assets/{ClassesView-D_m_99Wk.js → ClassesView-CpblTKWg.js} +1 -1
  4. package/dist/assets/{CloudDriveModal-BHWCchwE.js → CloudDriveModal-BMjXCoju.js} +1 -1
  5. package/dist/assets/{ComputerLabView-n5hzKtEx.js → ComputerLabView-DGha_J8B.js} +1 -1
  6. package/dist/assets/CourseManagement-BqlboC1S.js +1 -0
  7. package/dist/assets/{CourseWizardModal-1F_Ksxgf.js → CourseWizardModal-DO1n4T2T.js} +1 -1
  8. package/dist/assets/{CoursewareHubPanel-B0JwbcQz.js → CoursewareHubPanel-CEK_QEc5.js} +1 -1
  9. package/dist/assets/{Dashboard-CgJNFUha.js → Dashboard-CfgOPb6z.js} +1 -1
  10. package/dist/assets/{ExportWeightModal-B2wHglNt.js → ExportWeightModal-d9A2R6Ur.js} +1 -1
  11. package/dist/assets/{HelpTour-C3G0EsYT.js → HelpTour-Dlc1vvy3.js} +1 -1
  12. package/dist/assets/{HelpView-Bgxlwpn_.js → HelpView-bHc3tQPD.js} +1 -1
  13. package/dist/assets/{ImportLessonsModal-xjfRYYa0.js → ImportLessonsModal-C25NubUA.js} +1 -1
  14. package/dist/assets/{ImportModal-ChLAX_xg.js → ImportModal-B2acbUT8.js} +1 -1
  15. package/dist/assets/{InteractiveCoursewareViewer-pi7sr3lt.js → InteractiveCoursewareViewer-BcXVoPUb.js} +1 -1
  16. package/dist/assets/{InteractiveWhiteboard-CsKeinZt.js → InteractiveWhiteboard-BxvOZE3l.js} +1 -1
  17. package/dist/assets/{LazyCourseware-Bw86DSzq.js → LazyCourseware-D3_YrYGR.js} +1 -1
  18. package/dist/assets/{LazyWhiteboard-XI8iSIjM.js → LazyWhiteboard-CFPDpdYV.js} +1 -1
  19. package/dist/assets/{LessonEditorView-3TnocL3v.js → LessonEditorView-CYHcC9EA.js} +1 -1
  20. package/dist/assets/{LiveClassroomView-CQGaFkzD.js → LiveClassroomView-BSPW9gVe.js} +1 -1
  21. package/dist/assets/{NotificationDetailModal-CI4IfD2v.js → NotificationDetailModal-D4w-m1D-.js} +1 -1
  22. package/dist/assets/{PluginView-LokCtvB8.js → PluginView-DogON4Dd.js} +1 -1
  23. package/dist/assets/{ProcessLogsModal-uJqQQFyw.js → ProcessLogsModal-MtK-Pe4R.js} +1 -1
  24. package/dist/assets/{QuizGeneratorModal-Bul00fIH.js → QuizGeneratorModal-bCASRfsi.js} +1 -1
  25. package/dist/assets/{StudentAssignmentView-CaOXzBkt.js → StudentAssignmentView-B0b4spYC.js} +1 -1
  26. package/dist/assets/{StudentLessonView-6l-A6scB.js → StudentLessonView-SLcpNqtY.js} +1 -1
  27. package/dist/assets/{StudentPreviewModal-CnWC2qso.js → StudentPreviewModal-nO5C3nq5.js} +1 -1
  28. package/dist/assets/{StudentView-BtGPu4nl.js → StudentView-BtleJCxa.js} +1 -1
  29. package/dist/assets/{SystemResourceLibraryModal-CiRD12Rw.js → SystemResourceLibraryModal-D00BK5Xh.js} +1 -1
  30. package/dist/assets/{TeacherView-_iKPvpXI.js → TeacherView-BP04D9dn.js} +1 -1
  31. package/dist/assets/{TimetableView-B0yL3omc.js → TimetableView-Do_LWpV2.js} +4 -4
  32. package/dist/assets/{index-AdOtUso0.js → index-hTc1h9s2.js} +4 -4
  33. package/dist/assets/{vendor-icons-BHXru9a4.js → vendor-icons-B2DRJfZU.js} +1 -1
  34. package/dist/index.html +1 -1
  35. package/dist/server.cjs +369 -231
  36. package/package.json +17 -17
  37. package/dist/assets/CourseManagement-DamuhaYN.js +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");
@@ -8702,9 +8785,9 @@ var import_node_url = require("node:url");
8702
8785
 
8703
8786
  // packages/core/esm-loader/install-utils.ts
8704
8787
  var import_jszip = __toESM(require("jszip"), 1);
8705
- var fs3 = __toESM(require("fs"), 1);
8788
+ var fs4 = __toESM(require("fs"), 1);
8706
8789
  var os2 = __toESM(require("os"), 1);
8707
- var path4 = __toESM(require("path"), 1);
8790
+ var path5 = __toESM(require("path"), 1);
8708
8791
  var import_uuid10 = require("uuid");
8709
8792
  var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
8710
8793
  async function bundlePlugin(entryCode, resolveDir) {
@@ -8727,12 +8810,12 @@ async function bundlePlugin(entryCode, resolveDir) {
8727
8810
  setup(build) {
8728
8811
  build.onResolve({ filter: /.*/ }, (args) => {
8729
8812
  if (args.path.includes("core/di/interfaces")) {
8730
- const tsPath = path4.resolve(process.cwd(), "packages/core/di/interfaces.ts");
8731
- if (fs3.existsSync(tsPath)) {
8813
+ const tsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.ts");
8814
+ if (fs4.existsSync(tsPath)) {
8732
8815
  return { path: tsPath };
8733
8816
  }
8734
- const jsPath = path4.resolve(process.cwd(), "packages/core/di/interfaces.js");
8735
- if (fs3.existsSync(jsPath)) {
8817
+ const jsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.js");
8818
+ if (fs4.existsSync(jsPath)) {
8736
8819
  return { path: jsPath };
8737
8820
  }
8738
8821
  }
@@ -8788,22 +8871,22 @@ async function validateAndBundleZip(zipBuffer) {
8788
8871
  throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
8789
8872
  }
8790
8873
  const entryCode = await entryFile.async("string");
8791
- const tmpDir = path4.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
8874
+ const tmpDir = path5.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
8792
8875
  try {
8793
- fs3.mkdirSync(tmpDir, { recursive: true });
8876
+ fs4.mkdirSync(tmpDir, { recursive: true });
8794
8877
  for (const [name, file] of Object.entries(zip.files)) {
8795
8878
  if (file.dir) continue;
8796
- const filePath = path4.join(tmpDir, name);
8797
- const fileDir = path4.dirname(filePath);
8798
- fs3.mkdirSync(fileDir, { recursive: true });
8879
+ const filePath = path5.join(tmpDir, name);
8880
+ const fileDir = path5.dirname(filePath);
8881
+ fs4.mkdirSync(fileDir, { recursive: true });
8799
8882
  const content = await file.async("nodebuffer");
8800
- fs3.writeFileSync(filePath, content);
8883
+ fs4.writeFileSync(filePath, content);
8801
8884
  }
8802
8885
  const bundledCode = await bundlePlugin(entryCode, tmpDir);
8803
8886
  return { manifest, bundledCode, entryFileName: manifest.main };
8804
8887
  } finally {
8805
8888
  try {
8806
- fs3.rmSync(tmpDir, { recursive: true, force: true });
8889
+ fs4.rmSync(tmpDir, { recursive: true, force: true });
8807
8890
  } catch {
8808
8891
  }
8809
8892
  }
@@ -19743,93 +19826,14 @@ function registerOfficialActivities(registry, actionRegistry) {
19743
19826
  var IActivityRegistryToken = new Token("@openlearn/activity-ecosystem:IActivityRegistry");
19744
19827
 
19745
19828
  // server/routes/os.ts
19746
- var import_path9 = __toESM(require("path"), 1);
19747
- var import_fs9 = __toESM(require("fs"), 1);
19829
+ var import_path8 = __toESM(require("path"), 1);
19830
+ var import_fs8 = __toESM(require("fs"), 1);
19748
19831
  var import_child_process = require("child_process");
19749
- var import_crypto7 = __toESM(require("crypto"), 1);
19832
+ var import_crypto6 = __toESM(require("crypto"), 1);
19750
19833
  var import_xss = require("xss");
19751
19834
 
19752
19835
  // 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
- }
19836
+ init_api_key_crypto();
19833
19837
  function maskApiKey(key) {
19834
19838
  if (!key || key.length <= 8) return key ? "****" : "";
19835
19839
  return key.substring(0, 4) + "****" + key.substring(key.length - 4);
@@ -19847,8 +19851,8 @@ function detectPromptInjection(input) {
19847
19851
  }
19848
19852
 
19849
19853
  // server/routes/shared.ts
19850
- var import_path8 = __toESM(require("path"), 1);
19851
- var import_crypto6 = __toESM(require("crypto"), 1);
19854
+ var import_path7 = __toESM(require("path"), 1);
19855
+ var import_crypto5 = __toESM(require("crypto"), 1);
19852
19856
 
19853
19857
  // server/utils/bridge-sdk.ts
19854
19858
  var BRIDGE_SDK_CODE = `(function() {
@@ -20315,7 +20319,7 @@ var BRIDGE_SDK_CODE = `(function() {
20315
20319
  const text = (el.textContent || el.innerText || '').trim();
20316
20320
  if (text) {
20317
20321
  logData.push("Selector '" + selector + "' matched text: '" + text + "'");
20318
- const fractionMatch = text.match(/(\\\\d+(\\\\.\\\\d+)?)\\\\s*[\\\\/|\u4E4B]\\\\s*(\\\\d+)/);
20322
+ const fractionMatch = text.match(/(\\d+(\\.\\d+)?)\\s*[\\/|\u4E4B]\\s*(\\d+)/);
20319
20323
  if (fractionMatch) {
20320
20324
  const num = parseFloat(fractionMatch[1]);
20321
20325
  const den = parseFloat(fractionMatch[3]);
@@ -20325,7 +20329,7 @@ var BRIDGE_SDK_CODE = `(function() {
20325
20329
  return { score: pct, log: logData };
20326
20330
  }
20327
20331
  }
20328
- const match = text.match(/\\\\d+(\\\\.\\\\d+)?/);
20332
+ const match = text.match(/\\d+(\\.\\d+)?/);
20329
20333
  if (match) {
20330
20334
  const num = parseFloat(match[0]);
20331
20335
  if (!isNaN(num)) {
@@ -20366,7 +20370,7 @@ var BRIDGE_SDK_CODE = `(function() {
20366
20370
  if (txt) {
20367
20371
  const hasKey = txt.includes('\u5F97\u5206') || txt.includes('\u5206\u6570') || txt.includes('\u6210\u7EE9') || txt.toLowerCase().includes('score') || txt.toLowerCase().includes('points');
20368
20372
  if (hasKey) {
20369
- const m = txt.match(/\\\\d+(\\\\.\\\\d+)?/);
20373
+ const m = txt.match(/\\d+(\\.\\d+)?/);
20370
20374
  if (m) {
20371
20375
  const val = parseFloat(m[0]);
20372
20376
  logData.push("Fallback leaf <" + el.tagName + "> '" + txt + "' parsed: " + val);
@@ -20384,6 +20388,139 @@ var BRIDGE_SDK_CODE = `(function() {
20384
20388
  return { score: null, log: logData };
20385
20389
  }
20386
20390
 
20391
+ // --- RESULT-SCREEN AUTO SUBMIT ---------------------------------------
20392
+ // \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
20393
+ // \u8FD9\u91CC\u76D1\u542C\u7ED3\u7B97\u9875\u51FA\u73B0\uFF0C\u81EA\u52A8\u6293\u53D6\u5206\u6570\u5E76\u4E0A\u62A5\u4E00\u6B21\u3002
20394
+ // \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
20395
+ var __lmsResultSubmitted = false;
20396
+ var __lmsResultScheduled = false;
20397
+ var __lmsResultPolling = false;
20398
+
20399
+ // \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
20400
+ 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/;
20401
+ // \u7528 [0-9] / [^0-9] \u4EE3\u66FF \\d / \\s\uFF0C\u89C4\u907F\u6A21\u677F\u5B57\u7B26\u4E32\u7684\u8F6C\u4E49\u9677\u9631
20402
+ var __LMS_RATIO_RE = /([0-9]+(?:[.][0-9]+)?)[^0-9]*[/\uFF0F|\u4E4B][^0-9]*([0-9]+)/;
20403
+ var __LMS_NUM_RE = /[0-9]+(?:[.][0-9]+)?/;
20404
+
20405
+ function __lmsVisibleText() {
20406
+ try {
20407
+ if (!document.body) return '';
20408
+ if (typeof document.body.innerText === 'string') return document.body.innerText;
20409
+ return document.body.textContent || '';
20410
+ } catch (e) { return ''; }
20411
+ }
20412
+
20413
+ function __lmsIsShown(el) {
20414
+ try {
20415
+ if (!el || !el.getBoundingClientRect) return false;
20416
+ var rect = el.getBoundingClientRect();
20417
+ if (rect.width <= 0 && rect.height <= 0) return false;
20418
+ var node = el;
20419
+ while (node && node.nodeType === 1) {
20420
+ var st = window.getComputedStyle ? window.getComputedStyle(node) : null;
20421
+ if (st && (st.display === 'none' || st.visibility === 'hidden')) return false;
20422
+ node = node.parentElement;
20423
+ }
20424
+ return true;
20425
+ } catch (e) { return false; }
20426
+ }
20427
+
20428
+ // \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
20429
+ function __lmsFindVisibleScore() {
20430
+ var selectors = [
20431
+ '#finalScore', '#score', '#scoreDisplay', '.score', '.final-score',
20432
+ '[id*="score" i]', '[id*="point" i]', '[id*="grade" i]'
20433
+ ];
20434
+ for (var i = 0; i < selectors.length; i++) {
20435
+ var nodes = [];
20436
+ try { nodes = document.querySelectorAll(selectors[i]); } catch (e) { nodes = []; }
20437
+ for (var j = 0; j < nodes.length; j++) {
20438
+ if (!__lmsIsShown(nodes[j])) continue;
20439
+ var txt = (nodes[j].textContent || '').trim();
20440
+ if (!txt) continue;
20441
+ var frac = txt.match(__LMS_RATIO_RE);
20442
+ if (frac) {
20443
+ var fn = parseFloat(frac[1]);
20444
+ var fd = parseFloat(frac[2]);
20445
+ if (fd > 0 && fn >= 0 && fn <= fd) return Math.round((fn / fd) * 10000) / 100;
20446
+ }
20447
+ var num = txt.match(__LMS_NUM_RE);
20448
+ if (num) return parseFloat(num[0]);
20449
+ }
20450
+ }
20451
+ return null;
20452
+ }
20453
+
20454
+ function __lmsExtractResultScore() {
20455
+ // 1) \u6B63\u786E\u9898\u6570 X/Y \u2192 \u767E\u5206\u5236\uFF08\u53E3\u5F84\u6700\u7A33\u5B9A\uFF09
20456
+ var ratioSelectors = [
20457
+ '#correctCount', '[id*="correct" i]', '[id*="accuracy" i]',
20458
+ '[class*="correct" i]', '[class*="accuracy" i]', '[id*="ratio" i]'
20459
+ ];
20460
+ for (var i = 0; i < ratioSelectors.length; i++) {
20461
+ var nodes = [];
20462
+ try { nodes = document.querySelectorAll(ratioSelectors[i]); } catch (e) { nodes = []; }
20463
+ for (var j = 0; j < nodes.length; j++) {
20464
+ if (!__lmsIsShown(nodes[j])) continue;
20465
+ var m = (nodes[j].textContent || '').trim().match(__LMS_RATIO_RE);
20466
+ if (m) {
20467
+ var num = parseFloat(m[1]);
20468
+ var den = parseFloat(m[2]);
20469
+ if (den > 0 && num >= 0 && num <= den) return Math.round((num / den) * 10000) / 100;
20470
+ }
20471
+ }
20472
+ }
20473
+ // 2) \u56DE\u843D\uFF1A\u53EF\u89C1\u5206\u6570\u5143\u7D20
20474
+ return __lmsFindVisibleScore();
20475
+ }
20476
+
20477
+ function __lmsMaybeSubmitResult() {
20478
+ if (__lmsResultSubmitted || __lmsResultPolling) return;
20479
+ if (!__LMS_DONE_RE.test(__lmsVisibleText())) return;
20480
+
20481
+ __lmsResultPolling = true;
20482
+ // \u7ED3\u7B97\u9875 DOM \u53EF\u80FD\u5206\u6B65\u6E32\u67D3\uFF0C\u77ED\u8F6E\u8BE2\u53D6\u6700\u540E\u4E00\u6B21\u6709\u6548\u5206\u6570
20483
+ var tries = 0;
20484
+ var delays = [0, 120, 250, 400, 700, 1100];
20485
+ var latest = null;
20486
+
20487
+ function poll() {
20488
+ var s = __lmsExtractResultScore();
20489
+ if (s !== null && !isNaN(s)) latest = s;
20490
+ tries++;
20491
+ if (tries < delays.length) {
20492
+ setTimeout(poll, delays[tries]);
20493
+ return;
20494
+ }
20495
+ __lmsResultPolling = false;
20496
+ if (latest !== null && !__lmsResultSubmitted) {
20497
+ __lmsResultSubmitted = true;
20498
+ logToServer('Result screen detected, auto-submitting score: ' + latest);
20499
+ window.LMS.submit({ score: latest, completion: 1.0, comment: '\u7ED3\u7B97\u9875\u81EA\u52A8\u63D0\u53D6\u5F97\u5206' });
20500
+ }
20501
+ }
20502
+ poll();
20503
+ }
20504
+
20505
+ function initResultWatcher() {
20506
+ try {
20507
+ if (!document.body) return;
20508
+ var observer = new MutationObserver(function() {
20509
+ if (__lmsResultSubmitted || __lmsResultScheduled) return;
20510
+ __lmsResultScheduled = true;
20511
+ setTimeout(function() {
20512
+ __lmsResultScheduled = false;
20513
+ __lmsMaybeSubmitResult();
20514
+ }, 600);
20515
+ });
20516
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
20517
+ // \u5C11\u6570\u8BFE\u4EF6\u9996\u5C4F\u5373\u7ED3\u7B97
20518
+ setTimeout(__lmsMaybeSubmitResult, 800);
20519
+ } catch (e) {
20520
+ logToServer('Error in initResultWatcher: ' + e.message);
20521
+ }
20522
+ }
20523
+
20387
20524
  function attachListeners() {
20388
20525
  try {
20389
20526
  const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], .btn, .button');
@@ -20477,6 +20614,7 @@ var BRIDGE_SDK_CODE = `(function() {
20477
20614
  });
20478
20615
  observer.observe(document.body, { childList: true, subtree: true });
20479
20616
  attachListeners();
20617
+ initResultWatcher();
20480
20618
  } catch (e) {
20481
20619
  logToServer("Error in initAutoSubmit: " + e.message);
20482
20620
  }
@@ -20509,7 +20647,7 @@ function validateMagicBytes(buffer, fileName) {
20509
20647
  ".webp": [[82, 73, 70, 70]]
20510
20648
  // RIFF
20511
20649
  };
20512
- const ext = import_path8.default.extname(fileName || "").toLowerCase();
20650
+ const ext = import_path7.default.extname(fileName || "").toLowerCase();
20513
20651
  const signatures = MAGIC_BYTES[ext];
20514
20652
  if (!signatures) return true;
20515
20653
  return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
@@ -20531,7 +20669,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20531
20669
  const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
20532
20670
  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
20671
  if (!attempt) {
20534
- const attemptId = "att_" + import_crypto6.default.randomBytes(8).toString("hex");
20672
+ const attemptId = "att_" + import_crypto5.default.randomBytes(8).toString("hex");
20535
20673
  kernelContainer.db.prepare(
20536
20674
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20537
20675
  ).run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
@@ -20546,7 +20684,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20546
20684
  } else if (session.role === "teacher" || session.role === "administrator") {
20547
20685
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
20548
20686
  if (!attempt) {
20549
- const attemptId = "att_teacher_" + import_crypto6.default.randomBytes(8).toString("hex");
20687
+ const attemptId = "att_teacher_" + import_crypto5.default.randomBytes(8).toString("hex");
20550
20688
  kernelContainer.db.prepare(
20551
20689
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20552
20690
  ).run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
@@ -20564,7 +20702,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
20564
20702
  if (studentInfo.attempt_id === "guest-attempt") {
20565
20703
  let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
20566
20704
  if (!attempt) {
20567
- const attemptId = "att_guest_" + import_crypto6.default.randomBytes(8).toString("hex");
20705
+ const attemptId = "att_guest_" + import_crypto5.default.randomBytes(8).toString("hex");
20568
20706
  kernelContainer.db.prepare(
20569
20707
  "INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
20570
20708
  ).run(attemptId, cwInfo.id, "guest", Date.now(), "active");
@@ -20648,7 +20786,7 @@ function registerOsRoutes(ctx) {
20648
20786
  if (!filename || !base64Data) {
20649
20787
  return res.status(400).json({ error: "Filename and base64Data are required" });
20650
20788
  }
20651
- const ext = import_path9.default.extname(filename).toLowerCase();
20789
+ const ext = import_path8.default.extname(filename).toLowerCase();
20652
20790
  if (BLOCKED_EXTENSIONS.includes(ext)) {
20653
20791
  return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
20654
20792
  }
@@ -20660,13 +20798,13 @@ function registerOsRoutes(ctx) {
20660
20798
  if (!validateMagicBytes(fileBuffer, filename)) {
20661
20799
  return res.status(400).json({ error: "File content does not match the declared file type." });
20662
20800
  }
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 });
20801
+ const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
20802
+ if (!import_fs8.default.existsSync(uploadsDir)) {
20803
+ import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
20666
20804
  }
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);
20805
+ const uniqueName = `${Date.now()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
20806
+ const filePath = import_path8.default.join(uploadsDir, uniqueName);
20807
+ import_fs8.default.writeFileSync(filePath, fileBuffer);
20670
20808
  let slideCount = 1;
20671
20809
  if (ext === ".pdf") {
20672
20810
  try {
@@ -20879,8 +21017,8 @@ function registerOsRoutes(ctx) {
20879
21017
  });
20880
21018
  if (result && typeof result.agentText === "string") {
20881
21019
  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);
21020
+ 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);
21021
+ 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
21022
  }
20885
21023
  res.json({
20886
21024
  success: true,
@@ -20929,8 +21067,8 @@ function registerOsRoutes(ctx) {
20929
21067
  }
20930
21068
 
20931
21069
  // server/routes/resources.ts
20932
- var import_path10 = __toESM(require("path"), 1);
20933
- var import_fs10 = __toESM(require("fs"), 1);
21070
+ var import_path9 = __toESM(require("path"), 1);
21071
+ var import_fs9 = __toESM(require("fs"), 1);
20934
21072
  function registerResourcesRoutes(ctx) {
20935
21073
  const { app } = ctx;
20936
21074
  app.get("/api/resources", (req, res) => {
@@ -20946,23 +21084,23 @@ function registerResourcesRoutes(ctx) {
20946
21084
  const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
20947
21085
  if (!resource) return res.status(404).send("Resource not found");
20948
21086
  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";
21087
+ const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ? import_path9.default.basename(resource.name.replace(/\\/g, "/")) : "index.html";
20950
21088
  const existingCw2 = kernelContainer.db.prepare("SELECT id FROM courseware WHERE id = ?").get(resource.id);
20951
21089
  if (!existingCw2) {
20952
21090
  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
21091
  }
20954
21092
  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 });
21093
+ const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
21094
+ if (!import_fs9.default.existsSync(storageDir)) {
21095
+ import_fs9.default.mkdirSync(storageDir, { recursive: true });
20958
21096
  }
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");
21097
+ const namedPath = import_path9.default.resolve(storageDir, entryName);
21098
+ if (!import_fs9.default.existsSync(namedPath)) {
21099
+ import_fs9.default.writeFileSync(namedPath, resource.content || "", "utf8");
20962
21100
  }
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");
21101
+ const indexHtmlPath = import_path9.default.resolve(storageDir, "index.html");
21102
+ if (!import_fs9.default.existsSync(indexHtmlPath)) {
21103
+ import_fs9.default.writeFileSync(indexHtmlPath, resource.content || "", "utf8");
20966
21104
  }
20967
21105
  } catch (storageErr) {
20968
21106
  console.warn("[resources] Failed to write courseware storage cache:", storageErr);
@@ -21002,21 +21140,21 @@ function registerResourcesRoutes(ctx) {
21002
21140
  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
21141
  }
21004
21142
  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 });
21143
+ const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
21144
+ if (!import_fs9.default.existsSync(storageDir)) {
21145
+ import_fs9.default.mkdirSync(storageDir, { recursive: true });
21008
21146
  for (const f of files) {
21009
21147
  if (f.path && f.content !== void 0) {
21010
21148
  const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
21011
- const target = import_path10.default.resolve(storageDir, cleanRel);
21149
+ const target = import_path9.default.resolve(storageDir, cleanRel);
21012
21150
  if (target.startsWith(storageDir)) {
21013
- import_fs10.default.mkdirSync(import_path10.default.dirname(target), { recursive: true });
21151
+ import_fs9.default.mkdirSync(import_path9.default.dirname(target), { recursive: true });
21014
21152
  const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
21015
21153
  if (isBin) {
21016
21154
  const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
21017
- import_fs10.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21155
+ import_fs9.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21018
21156
  } else {
21019
- import_fs10.default.writeFileSync(target, f.content, "utf8");
21157
+ import_fs9.default.writeFileSync(target, f.content, "utf8");
21020
21158
  }
21021
21159
  }
21022
21160
  }
@@ -21239,9 +21377,9 @@ function registerResourcesRoutes(ctx) {
21239
21377
  }
21240
21378
 
21241
21379
  // 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);
21380
+ var import_path10 = __toESM(require("path"), 1);
21381
+ var import_fs10 = __toESM(require("fs"), 1);
21382
+ var import_crypto8 = __toESM(require("crypto"), 1);
21245
21383
  function registerCoursewareRoutes(ctx) {
21246
21384
  const { app, io } = ctx;
21247
21385
  app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
@@ -21391,7 +21529,7 @@ function registerCoursewareRoutes(ctx) {
21391
21529
  return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
21392
21530
  }
21393
21531
  }
21394
- const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
21532
+ const rawId = "raw_" + import_crypto8.default.randomBytes(8).toString("hex");
21395
21533
  kernelContainer.db.prepare(
21396
21534
  "INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
21397
21535
  ).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
@@ -21419,7 +21557,7 @@ function registerCoursewareRoutes(ctx) {
21419
21557
  kernelContainer.db.prepare(
21420
21558
  "INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
21421
21559
  ).run(
21422
- "res_" + import_crypto9.default.randomBytes(8).toString("hex"),
21560
+ "res_" + import_crypto8.default.randomBytes(8).toString("hex"),
21423
21561
  attemptId,
21424
21562
  parsedScore,
21425
21563
  comment || null,
@@ -21445,7 +21583,7 @@ function registerCoursewareRoutes(ctx) {
21445
21583
  }
21446
21584
  io.emit("courseware-attempt-updated", { attemptId, type: "log" });
21447
21585
  void kernelContainer.eventBus.publish({
21448
- id: "evt_" + import_crypto9.default.randomBytes(8).toString("hex"),
21586
+ id: "evt_" + import_crypto8.default.randomBytes(8).toString("hex"),
21449
21587
  type: "courseware.event_logged",
21450
21588
  source: "builtin.courseware",
21451
21589
  payload: { attemptId, eventType, payload },
@@ -21542,8 +21680,8 @@ function registerCoursewareRoutes(ctx) {
21542
21680
  const { msg, url, student, courseware } = req.body;
21543
21681
  const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
21544
21682
  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}
21683
+ const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
21684
+ import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
21547
21685
  `);
21548
21686
  res.json({ success: true });
21549
21687
  } catch (e) {
@@ -21619,7 +21757,7 @@ function registerCoursewareRoutes(ctx) {
21619
21757
  let assignment = kernelContainer.db.prepare("SELECT id FROM assignments WHERE class_id = ? AND lesson_id = ? AND title = ?").get(classId, lessonId, assignmentTitle);
21620
21758
  let assignmentId = assignment?.id;
21621
21759
  if (!assignmentId) {
21622
- assignmentId = "ast-cw-" + import_crypto9.default.randomBytes(8).toString("hex");
21760
+ assignmentId = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
21623
21761
  kernelContainer.db.prepare(
21624
21762
  "INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
21625
21763
  ).run(
@@ -21692,8 +21830,8 @@ function registerCoursewareRoutes(ctx) {
21692
21830
  }
21693
21831
 
21694
21832
  // server/routes/bridge.ts
21695
- var import_path12 = __toESM(require("path"), 1);
21696
- var import_fs12 = __toESM(require("fs"), 1);
21833
+ var import_path11 = __toESM(require("path"), 1);
21834
+ var import_fs11 = __toESM(require("fs"), 1);
21697
21835
  function registerBridgeRoutes(ctx) {
21698
21836
  const { app } = ctx;
21699
21837
  app.get("/bridge.js", (_req, res) => {
@@ -21713,7 +21851,7 @@ function registerBridgeRoutes(ctx) {
21713
21851
  if (!courseware) {
21714
21852
  const resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(uuid);
21715
21853
  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";
21854
+ const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : "index.html";
21717
21855
  const createdAt = resRow.created_at || Date.now();
21718
21856
  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
21857
  courseware = {
@@ -21729,21 +21867,21 @@ function registerBridgeRoutes(ctx) {
21729
21867
  if (!courseware) {
21730
21868
  return res.status(404).send("Courseware not found");
21731
21869
  }
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) {
21870
+ const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", courseware.uuid);
21871
+ if (!import_fs11.default.existsSync(storageDir) || import_fs11.default.readdirSync(storageDir).length === 0) {
21734
21872
  let resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ? OR id = ?").get(courseware.uuid, courseware.id);
21735
21873
  if (!resRow && courseware.name) {
21736
21874
  const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
21737
21875
  resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE name = ? OR name = ?").get(courseware.name, nameCandidate);
21738
21876
  }
21739
21877
  if (resRow && resRow.content) {
21740
- import_fs12.default.mkdirSync(storageDir, { recursive: true });
21878
+ import_fs11.default.mkdirSync(storageDir, { recursive: true });
21741
21879
  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");
21880
+ const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : courseware.entry || "index.html";
21881
+ const safeEntry = import_path11.default.basename(originalName.replace(/\\/g, "/"));
21882
+ import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, safeEntry), resRow.content, "utf8");
21745
21883
  if (safeEntry !== "index.html") {
21746
- import_fs12.default.writeFileSync(import_path12.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
21884
+ import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
21747
21885
  }
21748
21886
  } else if (resRow.type === "folder") {
21749
21887
  try {
@@ -21751,15 +21889,15 @@ function registerBridgeRoutes(ctx) {
21751
21889
  for (const f of files) {
21752
21890
  if (f.path && f.content !== void 0) {
21753
21891
  const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
21754
- const target = import_path12.default.resolve(storageDir, cleanRel);
21892
+ const target = import_path11.default.resolve(storageDir, cleanRel);
21755
21893
  if (target.startsWith(storageDir)) {
21756
- import_fs12.default.mkdirSync(import_path12.default.dirname(target), { recursive: true });
21894
+ import_fs11.default.mkdirSync(import_path11.default.dirname(target), { recursive: true });
21757
21895
  const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
21758
21896
  if (isBin) {
21759
21897
  const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
21760
- import_fs12.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21898
+ import_fs11.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
21761
21899
  } else {
21762
- import_fs12.default.writeFileSync(target, f.content, "utf8");
21900
+ import_fs11.default.writeFileSync(target, f.content, "utf8");
21763
21901
  }
21764
21902
  }
21765
21903
  }
@@ -21773,35 +21911,35 @@ function registerBridgeRoutes(ctx) {
21773
21911
  if (!subpath || subpath === "") {
21774
21912
  subpath = courseware.entry || "index.html";
21775
21913
  }
21776
- let filePath = import_path12.default.resolve(storageDir, subpath);
21914
+ let filePath = import_path11.default.resolve(storageDir, subpath);
21777
21915
  if (!filePath.startsWith(storageDir)) {
21778
21916
  return res.status(403).send("Access denied");
21779
21917
  }
21780
- if (!import_fs12.default.existsSync(filePath) && import_fs12.default.existsSync(storageDir)) {
21781
- const dirEntries = import_fs12.default.readdirSync(storageDir);
21918
+ if (!import_fs11.default.existsSync(filePath) && import_fs11.default.existsSync(storageDir)) {
21919
+ const dirEntries = import_fs11.default.readdirSync(storageDir);
21782
21920
  const isLookingForEntry = !subpath || subpath === "" || subpath === "index.html" || subpath === "index.htm" || subpath === courseware.entry;
21783
21921
  if (isLookingForEntry) {
21784
21922
  let candidate = dirEntries.find(
21785
- (f) => f === courseware.entry && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile()
21923
+ (f) => f === courseware.entry && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
21786
21924
  );
21787
21925
  if (!candidate && courseware.name) {
21788
21926
  const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
21789
21927
  candidate = dirEntries.find(
21790
- (f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile()
21928
+ (f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
21791
21929
  );
21792
21930
  }
21793
21931
  if (!candidate) {
21794
21932
  candidate = dirEntries.find((f) => {
21795
21933
  const lower = f.toLowerCase();
21796
- return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs12.default.statSync(import_path12.default.join(storageDir, f)).isFile();
21934
+ return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile();
21797
21935
  });
21798
21936
  }
21799
21937
  if (candidate) {
21800
- filePath = import_path12.default.resolve(storageDir, candidate);
21938
+ filePath = import_path11.default.resolve(storageDir, candidate);
21801
21939
  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);
21940
+ const indexHtmlPath = import_path11.default.resolve(storageDir, "index.html");
21941
+ if (!import_fs11.default.existsSync(indexHtmlPath)) {
21942
+ import_fs11.default.copyFileSync(filePath, indexHtmlPath);
21805
21943
  }
21806
21944
  if (courseware.entry !== candidate) {
21807
21945
  kernelContainer.db.prepare("UPDATE courseware SET entry = ? WHERE id = ?").run(candidate, courseware.id);
@@ -21812,9 +21950,9 @@ function registerBridgeRoutes(ctx) {
21812
21950
  }
21813
21951
  } else {
21814
21952
  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);
21953
+ const subDirPath = import_path11.default.join(storageDir, entry);
21954
+ if (import_fs11.default.statSync(subDirPath).isDirectory()) {
21955
+ const subEntries = import_fs11.default.readdirSync(subDirPath);
21818
21956
  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
21957
  if (subCandidate) {
21820
21958
  const redirectPath = `${entry}/${subCandidate}`;
@@ -21825,12 +21963,12 @@ function registerBridgeRoutes(ctx) {
21825
21963
  }
21826
21964
  }
21827
21965
  }
21828
- if (!import_fs12.default.existsSync(filePath)) {
21966
+ if (!import_fs11.default.existsSync(filePath)) {
21829
21967
  return res.status(404).send(`File not found: ${subpath}`);
21830
21968
  }
21831
- const stat = import_fs12.default.statSync(filePath);
21969
+ const stat = import_fs11.default.statSync(filePath);
21832
21970
  if (stat.isDirectory()) {
21833
- const dirFiles = import_fs12.default.readdirSync(filePath);
21971
+ const dirFiles = import_fs11.default.readdirSync(filePath);
21834
21972
  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
21973
  if (htmlFile) {
21836
21974
  const base = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
@@ -21838,7 +21976,7 @@ function registerBridgeRoutes(ctx) {
21838
21976
  }
21839
21977
  return res.status(404).send("Directory index not found");
21840
21978
  }
21841
- const ext = import_path12.default.extname(filePath).toLowerCase();
21979
+ const ext = import_path11.default.extname(filePath).toLowerCase();
21842
21980
  let contentType = "text/plain; charset=utf-8";
21843
21981
  if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
21844
21982
  else if (ext === ".css") contentType = "text/css; charset=utf-8";
@@ -21858,7 +21996,7 @@ function registerBridgeRoutes(ctx) {
21858
21996
  "Content-Security-Policy",
21859
21997
  "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
21998
  );
21861
- let html = import_fs12.default.readFileSync(filePath, "utf8");
21999
+ let html = import_fs11.default.readFileSync(filePath, "utf8");
21862
22000
  html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
21863
22001
  return res.send(html);
21864
22002
  } else {
@@ -22448,7 +22586,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
22448
22586
  }
22449
22587
 
22450
22588
  // server/routes/workspace.ts
22451
- var import_path13 = __toESM(require("path"), 1);
22589
+ var import_path12 = __toESM(require("path"), 1);
22452
22590
  function registerWorkspaceRoutes(ctx) {
22453
22591
  const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
22454
22592
  app.get("/api/events", requireAuth("administrator"), (req, res) => {
@@ -22597,7 +22735,7 @@ ${sub.content}`
22597
22735
  }
22598
22736
  const filename = parts[parts.length - 1];
22599
22737
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
22600
- const ext = import_path13.default.extname(filename).toLowerCase();
22738
+ const ext = import_path12.default.extname(filename).toLowerCase();
22601
22739
  const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
22602
22740
  const content = foundNode.content || "";
22603
22741
  if (binaryExtensions.includes(ext)) {
@@ -22839,9 +22977,9 @@ function registerAdminRoutes(ctx) {
22839
22977
  }
22840
22978
 
22841
22979
  // 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);
22980
+ var import_path13 = __toESM(require("path"), 1);
22981
+ var import_fs12 = __toESM(require("fs"), 1);
22982
+ var import_crypto9 = __toESM(require("crypto"), 1);
22845
22983
  var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
22846
22984
  function registerRosterRoutes(ctx) {
22847
22985
  const { app, io, loginLimiter } = ctx;
@@ -23095,7 +23233,7 @@ function registerRosterRoutes(ctx) {
23095
23233
  if (storedPwd.startsWith("$2")) {
23096
23234
  matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
23097
23235
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
23098
- matches = import_crypto10.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
23236
+ matches = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
23099
23237
  } else {
23100
23238
  matches = storedPwd === oldPassword;
23101
23239
  }
@@ -23152,7 +23290,7 @@ function registerRosterRoutes(ctx) {
23152
23290
  if (!filename || !base64Data) {
23153
23291
  return res.status(400).json({ error: "Filename and base64Data are required" });
23154
23292
  }
23155
- const ext = import_path14.default.extname(filename).toLowerCase();
23293
+ const ext = import_path13.default.extname(filename).toLowerCase();
23156
23294
  const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
23157
23295
  if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
23158
23296
  return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
@@ -23165,11 +23303,11 @@ function registerRosterRoutes(ctx) {
23165
23303
  if (!validateMagicBytes(fileBuffer, filename)) {
23166
23304
  return res.status(400).json({ error: "File content does not match the declared image type" });
23167
23305
  }
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);
23306
+ const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
23307
+ import_fs12.default.mkdirSync(avatarDir, { recursive: true });
23308
+ const uniqueName = `${Date.now()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
23309
+ const filePath = import_path13.default.join(avatarDir, uniqueName);
23310
+ import_fs12.default.writeFileSync(filePath, fileBuffer);
23173
23311
  const avatarUrl = `/uploads/avatars/${uniqueName}`;
23174
23312
  let oldAvatar = null;
23175
23313
  if (session.role === "student") {
@@ -23183,7 +23321,7 @@ function registerRosterRoutes(ctx) {
23183
23321
  }
23184
23322
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
23185
23323
  try {
23186
- import_fs13.default.unlinkSync(import_path14.default.join(process.cwd(), oldAvatar));
23324
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
23187
23325
  } catch {
23188
23326
  }
23189
23327
  }
@@ -23219,7 +23357,7 @@ function registerRosterRoutes(ctx) {
23219
23357
  }
23220
23358
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
23221
23359
  try {
23222
- import_fs13.default.unlinkSync(import_path14.default.join(process.cwd(), oldAvatar));
23360
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
23223
23361
  } catch {
23224
23362
  }
23225
23363
  }
@@ -23286,7 +23424,7 @@ function registerRosterRoutes(ctx) {
23286
23424
  if (storedPwd.startsWith("$2")) {
23287
23425
  matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
23288
23426
  } else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
23289
- const sha256Hash = import_crypto10.default.createHash("sha256").update(providedPassword).digest("hex");
23427
+ const sha256Hash = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
23290
23428
  if (sha256Hash === storedPwd) {
23291
23429
  matchesOwnPassword = true;
23292
23430
  kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
@@ -23330,7 +23468,7 @@ function registerRosterRoutes(ctx) {
23330
23468
  };
23331
23469
  }
23332
23470
  if (sessionData) {
23333
- const sessionToken = "token_" + import_crypto10.default.randomBytes(16).toString("hex");
23471
+ const sessionToken = "token_" + import_crypto9.default.randomBytes(16).toString("hex");
23334
23472
  const now = Date.now();
23335
23473
  const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
23336
23474
  kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
@@ -25168,8 +25306,8 @@ ${examsText}
25168
25306
 
25169
25307
  // server/routes/plugins.ts
25170
25308
  var import_express2 = __toESM(require("express"), 1);
25171
- var import_path15 = __toESM(require("path"), 1);
25172
- var import_fs14 = __toESM(require("fs"), 1);
25309
+ var import_path14 = __toESM(require("path"), 1);
25310
+ var import_fs13 = __toESM(require("fs"), 1);
25173
25311
  var import_semver3 = __toESM(require("semver"), 1);
25174
25312
 
25175
25313
  // server/services/version-fetcher.ts
@@ -25327,7 +25465,7 @@ async function checkVersion(source, installedVersion) {
25327
25465
  }
25328
25466
 
25329
25467
  // server/routes/plugin-api-gateway.ts
25330
- var import_node_path3 = __toESM(require("node:path"), 1);
25468
+ var import_node_path4 = __toESM(require("node:path"), 1);
25331
25469
  init_http_router();
25332
25470
  var RESERVED_ACTIONS = /* @__PURE__ */ new Set(["config", "toggle", "contributions", "check-update", "one-click-update"]);
25333
25471
  var FORBIDDEN_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
@@ -25461,7 +25599,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
25461
25599
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
25462
25600
  return;
25463
25601
  }
25464
- const normalizedSubPath = import_node_path3.default.posix.normalize("/" + rawSubPath);
25602
+ const normalizedSubPath = import_node_path4.default.posix.normalize("/" + rawSubPath);
25465
25603
  if (normalizedSubPath.includes("..")) {
25466
25604
  res.status(400).json({ success: false, error: "Path traversal attempt detected" });
25467
25605
  return;
@@ -25681,16 +25819,16 @@ data: ${JSON.stringify({ error: msg })}
25681
25819
 
25682
25820
  // server/routes/plugins.ts
25683
25821
  function findLocalPluginSource(manifestId) {
25684
- const root = import_path15.default.resolve(process.cwd(), "v2_plugins");
25685
- if (!import_fs14.default.existsSync(root)) return null;
25822
+ const root = import_path14.default.resolve(process.cwd(), "v2_plugins");
25823
+ if (!import_fs13.default.existsSync(root)) return null;
25686
25824
  let best = null;
25687
25825
  try {
25688
- for (const entry of import_fs14.default.readdirSync(root, { withFileTypes: true })) {
25826
+ for (const entry of import_fs13.default.readdirSync(root, { withFileTypes: true })) {
25689
25827
  if (!entry.isDirectory()) continue;
25690
- const manifestPath = import_path15.default.join(root, entry.name, "manifest.json");
25691
- if (!import_fs14.default.existsSync(manifestPath)) continue;
25828
+ const manifestPath = import_path14.default.join(root, entry.name, "manifest.json");
25829
+ if (!import_fs13.default.existsSync(manifestPath)) continue;
25692
25830
  try {
25693
- const m = JSON.parse(import_fs14.default.readFileSync(manifestPath, "utf-8"));
25831
+ const m = JSON.parse(import_fs13.default.readFileSync(manifestPath, "utf-8"));
25694
25832
  if (m?.id !== manifestId || !m?.version) continue;
25695
25833
  if (!best || import_semver3.default.gt(m.version, best.version)) best = { dir: entry.name, version: m.version };
25696
25834
  } catch {
@@ -25726,11 +25864,11 @@ function registerPluginsRoutes(ctx) {
25726
25864
  const { app, buildOpenAITools: buildOpenAITools2, executeAgentToolCall: executeAgentToolCall2, buildOpenAIChatUrl: buildOpenAIChatUrl2, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2 } = ctx;
25727
25865
  app.get("/api/docs/plugin-guide", (req, res) => {
25728
25866
  try {
25729
- const docPath = import_path15.default.join(process.cwd(), "docs_plugin_guide.md");
25730
- if (!import_fs14.default.existsSync(docPath)) {
25867
+ const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
25868
+ if (!import_fs13.default.existsSync(docPath)) {
25731
25869
  return res.status(404).json({ success: false, error: "Document not found" });
25732
25870
  }
25733
- const content = import_fs14.default.readFileSync(docPath, "utf-8");
25871
+ const content = import_fs13.default.readFileSync(docPath, "utf-8");
25734
25872
  res.json({ success: true, content });
25735
25873
  } catch (e) {
25736
25874
  sendSafeError(res, e);
@@ -25741,11 +25879,11 @@ function registerPluginsRoutes(ctx) {
25741
25879
  const limit = parseInt(req.query.limit) || 200;
25742
25880
  const component = req.query.component;
25743
25881
  const levelFilter = req.query.level;
25744
- const logFile2 = import_path15.default.resolve(process.cwd(), "logs", "openlearn.log");
25745
- if (!import_fs14.default.existsSync(logFile2)) {
25882
+ const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
25883
+ if (!import_fs13.default.existsSync(logFile2)) {
25746
25884
  return res.json({ success: true, logs: [] });
25747
25885
  }
25748
- const content = import_fs14.default.readFileSync(logFile2, "utf-8");
25886
+ const content = import_fs13.default.readFileSync(logFile2, "utf-8");
25749
25887
  const lines = content.split("\n").filter(Boolean);
25750
25888
  let parsedLogs = lines.map((line) => {
25751
25889
  try {
@@ -26307,7 +26445,7 @@ async function startServer() {
26307
26445
  config: { port: Number(process.env.PORT) || 9e3 }
26308
26446
  });
26309
26447
  try {
26310
- const migrationsDir = import_path16.default.join(process.cwd(), "migrations");
26448
+ const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
26311
26449
  const migrations = loadMigrationsFromDirectory(migrationsDir);
26312
26450
  if (migrations.length > 0) {
26313
26451
  runMigrations(kernelContainer.db, migrations);
@@ -26371,8 +26509,8 @@ async function startServer() {
26371
26509
  });
26372
26510
  app.use(import_express3.default.json({ limit: "10mb" }));
26373
26511
  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")));
26512
+ app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
26513
+ app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
26376
26514
  const configuredOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean) : [];
26377
26515
  const isOriginAllowed = (origin, hostHeader) => {
26378
26516
  if (!origin) return true;
@@ -26545,10 +26683,10 @@ async function startServer() {
26545
26683
  });
26546
26684
  app.use(vite.middlewares);
26547
26685
  } else {
26548
- const distPath = __dirname || import_path16.default.join(process.cwd(), "dist");
26686
+ const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
26549
26687
  app.use(import_express3.default.static(distPath));
26550
26688
  app.get("*", (req, res) => {
26551
- res.sendFile(import_path16.default.join(distPath, "index.html"));
26689
+ res.sendFile(import_path15.default.join(distPath, "index.html"));
26552
26690
  });
26553
26691
  }
26554
26692
  const HOST = process.env.HOST || "0.0.0.0";