openlearn-next 0.2.9 → 0.3.1

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 (67) hide show
  1. package/dist/assets/AdminDirectoryView-0XV86Ues.js +1 -0
  2. package/dist/assets/{BatchPickerModal-9BhmNM2R.js → BatchPickerModal-of9a5EJz.js} +1 -1
  3. package/dist/assets/{ClassesView-DKScQDos.js → ClassesView-CefnsjZ8.js} +2 -2
  4. package/dist/assets/CloudDriveModal-DgVQLbO5.js +1 -0
  5. package/dist/assets/ComputerLabView-BId0ZarM.js +1 -0
  6. package/dist/assets/CourseManagement-Cxhc784Q.js +1 -0
  7. package/dist/assets/CourseWizardModal-Da6PKhnU.js +3 -0
  8. package/dist/assets/CoursewareHubPanel-LpUgjqbH.js +1 -0
  9. package/dist/assets/{Dashboard-BPxkmnvI.js → Dashboard-LWbFbup4.js} +2 -2
  10. package/dist/assets/{ExportWeightModal-DSzySeqh.js → ExportWeightModal-Cyg1zMEd.js} +1 -1
  11. package/dist/assets/HelpTour-CHPIK2ae.js +1 -0
  12. package/dist/assets/{HelpView-CuNDka5n.js → HelpView-CvpgBpdr.js} +3 -3
  13. package/dist/assets/ImportLessonsModal-CfWIrW1K.js +1 -0
  14. package/dist/assets/ImportModal-BTfia0Ah.js +1 -0
  15. package/dist/assets/InteractiveCoursewareViewer-CS5BqfJZ.js +1 -0
  16. package/dist/assets/InteractiveWhiteboard-U23CsqGo.js +78 -0
  17. package/dist/assets/{LazyCourseware-g7b8EXR_.js → LazyCourseware-DXL9IxZO.js} +1 -1
  18. package/dist/assets/{LazyWhiteboard-BIepzrv9.js → LazyWhiteboard-CtkzSFtN.js} +2 -2
  19. package/dist/assets/LessonEditorView-Bfxf4VVB.js +1 -0
  20. package/dist/assets/LiveClassroomView-DHzCM4qQ.js +1 -0
  21. package/dist/assets/{NotificationDetailModal-Bk1g_prI.js → NotificationDetailModal-BxrpnlWV.js} +1 -1
  22. package/dist/assets/{PluginView-C6s7Ltcy.js → PluginView-12qYzmnq.js} +3 -3
  23. package/dist/assets/{ProcessLogsModal-B-PY780w.js → ProcessLogsModal-01S7zsw0.js} +1 -1
  24. package/dist/assets/{QuizGeneratorModal-DWH1zP0X.js → QuizGeneratorModal-DfZI40Ii.js} +1 -1
  25. package/dist/assets/StudentAssignmentView-BP6zbHuM.js +1 -0
  26. package/dist/assets/StudentLessonView-DEpMzrH2.js +1 -0
  27. package/dist/assets/StudentPreviewModal-DCmai53s.js +1 -0
  28. package/dist/assets/StudentView-CcGlfW25.js +2 -0
  29. package/dist/assets/SystemResourceLibraryModal-BzHw1edT.js +1 -0
  30. package/dist/assets/TeacherView-Dw6mKwcv.js +2 -0
  31. package/dist/assets/{TimetableView-BMsjwi-s.js → TimetableView-DJgOlGSf.js} +3 -3
  32. package/dist/assets/index-B-YxO39A.css +1 -0
  33. package/dist/assets/index-C8sCKzmN.js +2 -0
  34. package/dist/assets/index-CYzNsYZB.js +193 -0
  35. package/dist/assets/{index.es-DZ6UeYE-.js → index.es-CfxBkm_z.js} +1 -1
  36. package/dist/assets/{vendor-charts-B7ViBL9w.js → vendor-charts-CRXa4AlU.js} +1 -1
  37. package/dist/assets/vendor-content-CYR4BTuT.js +3 -0
  38. package/dist/assets/{vendor-icons-_c3xpwjZ.js → vendor-icons-Dq6MhgH9.js} +1 -1
  39. package/dist/assets/{vendor-pdf-CVyNPGVr.js → vendor-pdf-Cp-cYteE.js} +2 -2
  40. package/dist/assets/vendor-utils-BEhuP0Zq.js +1 -0
  41. package/dist/index.html +2 -2
  42. package/dist/server.cjs +989 -700
  43. package/package.json +9 -6
  44. package/dist/assets/AdminDirectoryView-CD7tW4mc.js +0 -1
  45. package/dist/assets/CloudDriveModal-cPtTMriS.js +0 -1
  46. package/dist/assets/ComputerLabView-B6-twB0u.js +0 -1
  47. package/dist/assets/CourseManagement-AoauIULq.js +0 -1
  48. package/dist/assets/CourseWizardModal-BOgLpLz7.js +0 -3
  49. package/dist/assets/CoursewareHubPanel-Cb1mAqHS.js +0 -1
  50. package/dist/assets/HelpTour-DwM9UBiA.js +0 -1
  51. package/dist/assets/ImportLessonsModal-Bwy2r-96.js +0 -1
  52. package/dist/assets/ImportModal-CmUt1YeX.js +0 -1
  53. package/dist/assets/InteractiveCoursewareViewer-DWnzq20c.js +0 -1
  54. package/dist/assets/InteractiveWhiteboard-C7JMplbp.js +0 -62
  55. package/dist/assets/LessonEditorView-C-mvYRUd.js +0 -1
  56. package/dist/assets/LiveClassroomView-CqRbWNis.js +0 -1
  57. package/dist/assets/StudentAssignmentView-CfTjeoAp.js +0 -1
  58. package/dist/assets/StudentLessonView-D2XvNGnW.js +0 -1
  59. package/dist/assets/StudentPreviewModal-BwXG6Cis.js +0 -1
  60. package/dist/assets/StudentView-DT-373QP.js +0 -2
  61. package/dist/assets/SystemResourceLibraryModal-BTL8rB0g.js +0 -1
  62. package/dist/assets/TeacherView-XdD-y67J.js +0 -2
  63. package/dist/assets/index-C5FKWIUq.js +0 -190
  64. package/dist/assets/index-CATki2cp.css +0 -1
  65. package/dist/assets/index-wdWIWwbg.js +0 -1
  66. package/dist/assets/vendor-content-V6uLfjnH.js +0 -3
  67. package/dist/assets/vendor-utils-4FcTwKPj.js +0 -1
package/dist/server.cjs CHANGED
@@ -1557,7 +1557,7 @@ var init_service_host = __esm({
1557
1557
  init_plugin_namespace();
1558
1558
  STACK_CAP = 4096;
1559
1559
  ServiceHost = class {
1560
- constructor(serviceRegistry, capabilityGuard, pluginActorId, manifestCapabilities, eventBus, eventForwarder, pluginId) {
1560
+ constructor(serviceRegistry, capabilityGuard, pluginActorId, manifestCapabilities, eventBus, eventForwarder, pluginId, dbPluginId) {
1561
1561
  this.serviceRegistry = serviceRegistry;
1562
1562
  this.capabilityGuard = capabilityGuard;
1563
1563
  this.pluginActorId = pluginActorId;
@@ -1565,6 +1565,7 @@ var init_service_host = __esm({
1565
1565
  this.eventBus = eventBus;
1566
1566
  this.eventForwarder = eventForwarder;
1567
1567
  this.pluginId = pluginId;
1568
+ this.dbPluginId = dbPluginId;
1568
1569
  /**
1569
1570
  * @param serviceRegistry - DI container for resolving service instances
1570
1571
  * @param capabilityGuard - CapabilityGuard instance (sync access, NOT async ICapabilityService wrapper)
@@ -1974,13 +1975,30 @@ var init_service_host = __esm({
1974
1975
  /**
1975
1976
  * Security barrier for Worker plugin IDatabase RPC access.
1976
1977
  *
1977
- * 1. Blocks access to core security tables (auth/session/plugin/admin).
1978
- * 2. Restricts DDL (CREATE/DROP/ALTER TABLE) to the plugin's own namespace
1979
- * (`plugin_<id>_` prefix).
1978
+ * 1. Blocks dangerous SQLite low-level operations (ATTACH, DETACH, PRAGMA, TRIGGERS, VIEWS, VACUUM).
1979
+ * 2. Blocks access to all platform core tables (auth, sessions, roster, grading, lessons, etc.).
1980
+ * 3. Restricts DDL (CREATE/DROP/ALTER TABLE) strictly to the plugin's own namespace (`plugin_<id>_` prefix).
1980
1981
  */
1981
1982
  assertDatabaseAccessAllowed(sql) {
1982
1983
  if (typeof sql !== "string" || sql.trim() === "") return;
1983
1984
  const pluginId = this.pluginId || this.pluginActorId.replace(/^plugin:/, "");
1985
+ const FORBIDDEN_OPERATIONS = [
1986
+ /\bATTACH(?:\s+DATABASE)?\b/i,
1987
+ /\bDETACH(?:\s+DATABASE)?\b/i,
1988
+ /\bPRAGMA\b/i,
1989
+ /\bVACUUM\b/i,
1990
+ /\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
1991
+ /\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
1992
+ ];
1993
+ for (const opRegex of FORBIDDEN_OPERATIONS) {
1994
+ if (opRegex.test(sql)) {
1995
+ throw new WorkerCapabilityError(
1996
+ this.pluginActorId,
1997
+ "@openlearn/core:IDatabase",
1998
+ `Worker plugin "${pluginId}" is forbidden from executing dangerous statement "${sql.trim()}"`
1999
+ );
2000
+ }
2001
+ }
1984
2002
  const FORBIDDEN_CORE_TABLES = [
1985
2003
  "users",
1986
2004
  "client_sessions",
@@ -1988,26 +2006,54 @@ var init_service_host = __esm({
1988
2006
  "plugin_storage",
1989
2007
  "ai_providers",
1990
2008
  "processes",
1991
- "pending_commands"
2009
+ "pending_commands",
2010
+ "classes",
2011
+ "students",
2012
+ "class_students",
2013
+ "lessons",
2014
+ "assignments",
2015
+ "assignment_submissions",
2016
+ "attendance",
2017
+ "schedules",
2018
+ "class_grade_weights",
2019
+ "exams",
2020
+ "exam_scores",
2021
+ "student_semester_reports",
2022
+ "student_rollcalls",
2023
+ "student_seats",
2024
+ "student_read_notifications",
2025
+ "student_lesson_progress",
2026
+ "courseware",
2027
+ "courseware_attempt",
2028
+ "system_resources",
2029
+ "events",
2030
+ "_migrations"
1992
2031
  ];
1993
- for (const table of FORBIDDEN_CORE_TABLES) {
1994
- if (new RegExp(`\\b${table}\\b`, "i").test(sql)) {
2032
+ const ddlMatch = /^\s*(CREATE|DROP|ALTER)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?["'`]?([a-zA-Z_][\w]*)["'`]?/i.exec(sql);
2033
+ if (ddlMatch) {
2034
+ const table = ddlMatch[2];
2035
+ const validNamespaces = [];
2036
+ if (this.dbPluginId) {
2037
+ validNamespaces.push(`plugin_${this.dbPluginId.replace(/[^a-zA-Z0-9_]/g, "_")}_`);
2038
+ }
2039
+ if (pluginId) {
2040
+ validNamespaces.push(`plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, "_")}_`);
2041
+ }
2042
+ const isAllowed = validNamespaces.some((ns) => table.startsWith(ns));
2043
+ if (!isAllowed) {
1995
2044
  throw new WorkerCapabilityError(
1996
2045
  this.pluginActorId,
1997
2046
  "@openlearn/core:IDatabase",
1998
- `Worker plugin "${pluginId}" is forbidden from accessing core security table "${table}"`
2047
+ `Worker plugin "${pluginId}" is not permitted to perform DDL on table "${table}"`
1999
2048
  );
2000
2049
  }
2001
2050
  }
2002
- const ddlMatch = /^\s*(CREATE|DROP|ALTER)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?["'`]?([a-zA-Z_][\w]*)["'`]?/i.exec(sql);
2003
- if (ddlMatch) {
2004
- const table = ddlMatch[2];
2005
- const namespace = `plugin_${pluginId.replace(/-/g, "_")}_`;
2006
- if (!table.startsWith(namespace)) {
2051
+ for (const table of FORBIDDEN_CORE_TABLES) {
2052
+ if (new RegExp(`\\b${table}\\b`, "i").test(sql)) {
2007
2053
  throw new WorkerCapabilityError(
2008
2054
  this.pluginActorId,
2009
2055
  "@openlearn/core:IDatabase",
2010
- `Worker plugin "${pluginId}" is not permitted to perform DDL on table "${table}"`
2056
+ `Worker plugin "${pluginId}" is forbidden from accessing core security table "${table}"`
2011
2057
  );
2012
2058
  }
2013
2059
  }
@@ -3078,7 +3124,11 @@ var init_worker_manager = __esm({
3078
3124
  workerData: { pluginId, manifestId: manifest.id, serviceTokens, pluginDir },
3079
3125
  eval: false,
3080
3126
  stdout: true,
3081
- stderr: true
3127
+ stderr: true,
3128
+ resourceLimits: {
3129
+ maxOldGenerationSizeMb: 128,
3130
+ maxYoungGenerationSizeMb: 32
3131
+ }
3082
3132
  });
3083
3133
  const pluginLogger = createLogger(`Plugin:${manifest.id || pluginId}`);
3084
3134
  worker.stdout.on("data", (chunk) => {
@@ -3110,7 +3160,8 @@ var init_worker_manager = __esm({
3110
3160
  // `manifest.id` (NOT the DB UUID `pluginId`) so the prefix
3111
3161
  // matches what the frontend `ctx.invokeCommand` produces —
3112
3162
  // see packages/core/plugin-host/plugin-namespace.ts.
3113
- manifest.id
3163
+ manifest.id,
3164
+ pluginId
3114
3165
  );
3115
3166
  const createdAt = Date.now();
3116
3167
  this.registry.register(pluginId, {
@@ -3283,8 +3334,8 @@ __export(server_exports, {
3283
3334
  });
3284
3335
  module.exports = __toCommonJS(server_exports);
3285
3336
  var import_express3 = __toESM(require("express"), 1);
3286
- var import_path14 = __toESM(require("path"), 1);
3287
- var import_fs13 = __toESM(require("fs"), 1);
3337
+ var import_path15 = __toESM(require("path"), 1);
3338
+ var import_fs14 = __toESM(require("fs"), 1);
3288
3339
  var import_dotenv = __toESM(require("dotenv"), 1);
3289
3340
  var import_vite = require("vite");
3290
3341
  var import_http = require("http");
@@ -3831,6 +3882,7 @@ function parseRequiresEntry(entry) {
3831
3882
  // packages/core/db/index.ts
3832
3883
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
3833
3884
  var import_path = __toESM(require("path"), 1);
3885
+ var import_os = __toESM(require("os"), 1);
3834
3886
  var import_fs = require("fs");
3835
3887
  var import_url = require("url");
3836
3888
  var import_crypto = __toESM(require("crypto"), 1);
@@ -3854,6 +3906,11 @@ var dbPath;
3854
3906
  if (process.env.OPENLEARN_DB_PATH) {
3855
3907
  dbPath = process.env.OPENLEARN_DB_PATH;
3856
3908
  (0, import_fs.mkdirSync)(import_path.default.dirname(dbPath), { recursive: true });
3909
+ } else if (process.env.VITEST) {
3910
+ const poolId = process.env.VITEST_POOL_ID || process.pid;
3911
+ const testDbDir = import_path.default.join(import_os.default.tmpdir(), "openlearn_test_dbs");
3912
+ (0, import_fs.mkdirSync)(testDbDir, { recursive: true });
3913
+ dbPath = import_path.default.join(testDbDir, `openlearn_test_${poolId}.db`);
3857
3914
  } else {
3858
3915
  if (typeof import_meta !== "undefined" && import_meta.url) {
3859
3916
  const __filename2 = (0, import_url.fileURLToPath)(import_meta.url);
@@ -3884,6 +3941,7 @@ db.exec(`
3884
3941
  timeline TEXT,
3885
3942
  progress_mode TEXT DEFAULT 'manual',
3886
3943
  progress_conditions TEXT,
3944
+ creator_id TEXT,
3887
3945
  created_at INTEGER NOT NULL,
3888
3946
  updated_at INTEGER NOT NULL
3889
3947
  );
@@ -4221,6 +4279,10 @@ db.exec(`
4221
4279
  CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
4222
4280
  CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
4223
4281
  `);
4282
+ try {
4283
+ db.exec("ALTER TABLE lessons ADD COLUMN creator_id TEXT");
4284
+ } catch {
4285
+ }
4224
4286
  try {
4225
4287
  db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
4226
4288
  } catch {
@@ -4317,6 +4379,9 @@ try {
4317
4379
  const insertStmt = db.prepare("INSERT INTO users (id, username, password_hash, role, name, created_at) VALUES (?, ?, ?, ?, ?, ?)");
4318
4380
  insertStmt.run("usr_admin", "admin", hashPassword("admin"), "administrator", "System Admin", Date.now());
4319
4381
  insertStmt.run("usr_teacher", "teacher", hashPassword("teacher"), "teacher", "Regular Teacher", Date.now());
4382
+ console.warn(
4383
+ "[SECURITY WARNING] Default users initialized (admin/admin, teacher/teacher). In production environments, immediately change these passwords via POST /api/auth/change-password!"
4384
+ );
4320
4385
  }
4321
4386
  } catch (e) {
4322
4387
  console.error("Failed to seed default users:", e);
@@ -5671,13 +5736,15 @@ var BuiltinPlugin = {
5671
5736
  async execute(command) {
5672
5737
  const payload = command.payload;
5673
5738
  const lessonId = (0, import_uuid6.v7)();
5674
- const stmt = db2.prepare("INSERT INTO lessons (id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
5675
- stmt.run(lessonId, payload.title, payload.content || "", Date.now(), Date.now());
5739
+ const rawCreatorId = payload.creatorId || command.actorId || "admin";
5740
+ const creatorId = typeof rawCreatorId === "string" && rawCreatorId.startsWith("user:") ? rawCreatorId.split(":")[1] : rawCreatorId;
5741
+ const stmt = db2.prepare("INSERT INTO lessons (id, title, content, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)");
5742
+ stmt.run(lessonId, payload.title, payload.content || "", creatorId, Date.now(), Date.now());
5676
5743
  await eventBus.publish({
5677
5744
  id: (0, import_uuid6.v7)(),
5678
5745
  type: "lesson.created",
5679
5746
  source: "builtin.lesson",
5680
- payload: { id: lessonId, title: payload.title },
5747
+ payload: { id: lessonId, title: payload.title, creatorId },
5681
5748
  timestamp: Date.now(),
5682
5749
  correlationId: command.id
5683
5750
  });
@@ -6650,12 +6717,27 @@ var BuiltinPlugin = {
6650
6717
  const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
6651
6718
  const fileBuffer = Buffer.from(base64Content, "base64");
6652
6719
  if (ext === ".html" || ext === ".htm") {
6653
- const entryName = filename;
6654
- import_fs3.default.writeFileSync(import_path3.default.join(storageDir, entryName), fileBuffer);
6720
+ const entryName = import_path3.default.basename(filename.replace(/\\/g, "/"));
6721
+ if (!entryName || entryName === "." || entryName === "..") {
6722
+ throw new Error("Invalid courseware filename");
6723
+ }
6724
+ const destPath = import_path3.default.resolve(storageDir, entryName);
6725
+ if (!destPath.startsWith(storageDir)) {
6726
+ throw new Error(`Path traversal attempt in HTML courseware upload: ${filename}`);
6727
+ }
6728
+ import_fs3.default.writeFileSync(destPath, fileBuffer);
6655
6729
  const coursewareId = "cw_" + import_crypto3.default.randomBytes(8).toString("hex");
6656
6730
  db2.prepare(
6657
6731
  "INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
6658
6732
  ).run(coursewareId, uuid, name, "html", entryName, Date.now());
6733
+ try {
6734
+ const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
6735
+ db2.prepare(
6736
+ "INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
6737
+ ).run(resId, name, "html", fileBuffer.toString("utf-8"), Date.now());
6738
+ } catch (resErr) {
6739
+ console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
6740
+ }
6659
6741
  await eventBus.publish({
6660
6742
  id: (0, import_uuid6.v7)(),
6661
6743
  type: "courseware.uploaded",
@@ -6744,6 +6826,23 @@ var BuiltinPlugin = {
6744
6826
  db2.prepare(
6745
6827
  "INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
6746
6828
  ).run(coursewareId, uuid, name, "folder", entry, Date.now());
6829
+ try {
6830
+ const resFiles = [];
6831
+ for (const [relativePath, file] of Object.entries(loadedZip.files)) {
6832
+ const fileObj = file;
6833
+ if (!fileObj.dir) {
6834
+ const buf = await fileObj.async("nodebuffer");
6835
+ const isBinary = /\.(png|jpe?g|gif|webp|ico|mp3|wav|mp4|woff2?|ttf|otf|eot)$/i.test(relativePath);
6836
+ resFiles.push({ path: relativePath, content: isBinary ? buf.toString("base64") : buf.toString("utf-8") });
6837
+ }
6838
+ }
6839
+ const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
6840
+ db2.prepare(
6841
+ "INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
6842
+ ).run(resId, name, "folder", JSON.stringify(resFiles), Date.now());
6843
+ } catch (resErr) {
6844
+ console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
6845
+ }
6747
6846
  await eventBus.publish({
6748
6847
  id: (0, import_uuid6.v7)(),
6749
6848
  type: "courseware.uploaded",
@@ -7582,7 +7681,7 @@ var import_node_url = require("node:url");
7582
7681
  var esbuild = __toESM(require("esbuild"), 1);
7583
7682
  var import_jszip = __toESM(require("jszip"), 1);
7584
7683
  var fs3 = __toESM(require("fs"), 1);
7585
- var os = __toESM(require("os"), 1);
7684
+ var os2 = __toESM(require("os"), 1);
7586
7685
  var path4 = __toESM(require("path"), 1);
7587
7686
  var import_uuid10 = require("uuid");
7588
7687
  var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
@@ -7670,7 +7769,7 @@ async function validateAndBundleZip(zipBuffer) {
7670
7769
  );
7671
7770
  }
7672
7771
  const entryCode = await entryFile.async("string");
7673
- const tmpDir = path4.join(os.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
7772
+ const tmpDir = path4.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
7674
7773
  try {
7675
7774
  fs3.mkdirSync(tmpDir, { recursive: true });
7676
7775
  for (const [name, file] of Object.entries(zip.files)) {
@@ -8460,11 +8559,11 @@ var PluginDistributionManager = class {
8460
8559
  return;
8461
8560
  }
8462
8561
  const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
8463
- const fs15 = await import("node:fs");
8464
- if (!fs15.existsSync(filePath)) {
8562
+ const fs16 = await import("node:fs");
8563
+ if (!fs16.existsSync(filePath)) {
8465
8564
  throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
8466
8565
  }
8467
- const code = fs15.readFileSync(filePath, "utf-8");
8566
+ const code = fs16.readFileSync(filePath, "utf-8");
8468
8567
  await this.pluginHost.reloadPlugin(pluginId, code);
8469
8568
  }
8470
8569
  async updateFromZip(zipBuffer, options = {}) {
@@ -8511,6 +8610,16 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
8511
8610
  var ACTIVATION_TIMEOUT_MS = 5e3;
8512
8611
  var DEACTIVATION_TIMEOUT_MS = 5e3;
8513
8612
  var OPENLEARN_VERSION = "0.2.5";
8613
+ function createPluginStaticMiddleware(absDir) {
8614
+ return [
8615
+ (_req, res, next) => {
8616
+ res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' blob: data:");
8617
+ res.setHeader("X-Content-Type-Options", "nosniff");
8618
+ next();
8619
+ },
8620
+ import_express.default.static(absDir)
8621
+ ];
8622
+ }
8514
8623
  var PluginHost = class {
8515
8624
  constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
8516
8625
  this.serviceRegistry = serviceRegistry;
@@ -8556,7 +8665,7 @@ var PluginHost = class {
8556
8665
  const pluginDir = this.getPluginDir(p.id);
8557
8666
  const absDir = import_path4.default.join(pluginDir, m.deploy.staticDir);
8558
8667
  if (import_fs5.default.existsSync(absDir)) {
8559
- this.expressApp.use(m.deploy.staticRoute, import_express.default.static(absDir));
8668
+ this.expressApp.use(m.deploy.staticRoute, ...createPluginStaticMiddleware(absDir));
8560
8669
  this._registeredRoutes.set(m.id, m.deploy.staticRoute);
8561
8670
  console.log(`[PluginHost] Restored static route "${m.deploy.staticRoute}" for plugin "${m.id}"`);
8562
8671
  }
@@ -9766,10 +9875,15 @@ var PluginHost = class {
9766
9875
  import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, "frontend.js"), frontendCode, "utf-8");
9767
9876
  }
9768
9877
  if (manifest.deploy?.script) {
9878
+ const rawScript = manifest.deploy.script.replace(/\\/g, "/");
9879
+ const resolvedScript = import_path4.default.resolve(pluginDir, rawScript);
9880
+ if (rawScript.includes("..") || !resolvedScript.startsWith(pluginDir + import_path4.default.sep)) {
9881
+ throw new Error(`Security Violation: Zip Slip detected in deploy script path "${manifest.deploy.script}"`);
9882
+ }
9769
9883
  const deployFile = zip.file(manifest.deploy.script);
9770
9884
  if (deployFile) {
9771
9885
  const deployCode = await deployFile.async("string");
9772
- import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, manifest.deploy.script), deployCode, "utf-8");
9886
+ import_fs5.default.writeFileSync(resolvedScript, deployCode, "utf-8");
9773
9887
  }
9774
9888
  }
9775
9889
  this.emitProgress(manifest.id, "extracting", "Extracting assets...");
@@ -9779,20 +9893,29 @@ var PluginHost = class {
9779
9893
  if (storageEntries.length > 0) {
9780
9894
  console.log(`[PluginHost] Extracting ${storageEntries.length} static asset files for plugin "${manifest.id}"...`);
9781
9895
  const dirs = /* @__PURE__ */ new Set();
9782
- for (const name of storageEntries) {
9783
- dirs.add(import_path4.default.dirname(name));
9896
+ for (const rawName of storageEntries) {
9897
+ const normalized = rawName.replace(/\\/g, "/");
9898
+ const destPath = import_path4.default.resolve(pluginDir, normalized);
9899
+ if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
9900
+ throw new Error(`Security Violation: Zip Slip detected in asset path "${rawName}"`);
9901
+ }
9902
+ dirs.add(import_path4.default.dirname(destPath));
9784
9903
  }
9785
9904
  for (const dir of dirs) {
9786
- import_fs5.default.mkdirSync(import_path4.default.join(pluginDir, dir), { recursive: true });
9905
+ import_fs5.default.mkdirSync(dir, { recursive: true });
9787
9906
  }
9788
9907
  const BATCH_SIZE = 10;
9789
9908
  for (let i = 0; i < storageEntries.length; i += BATCH_SIZE) {
9790
9909
  const batch = storageEntries.slice(i, i + BATCH_SIZE);
9791
9910
  await Promise.all(batch.map(async (name) => {
9911
+ const normalized = name.replace(/\\/g, "/");
9912
+ const destPath = import_path4.default.resolve(pluginDir, normalized);
9913
+ if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
9914
+ throw new Error(`Security Violation: Zip Slip detected in asset path "${name}"`);
9915
+ }
9792
9916
  const file = zip.file(name);
9793
9917
  if (file) {
9794
9918
  const content = await file.async("nodebuffer");
9795
- const destPath = import_path4.default.join(pluginDir, name);
9796
9919
  import_fs5.default.writeFileSync(destPath, content);
9797
9920
  }
9798
9921
  }));
@@ -9810,7 +9933,7 @@ var PluginHost = class {
9810
9933
  };
9811
9934
  import_fs5.default.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
9812
9935
  const { execSync: execSync2 } = await import("node:child_process");
9813
- execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --registry=https://registry.npmmirror.com", {
9936
+ execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --ignore-scripts --registry=https://registry.npmmirror.com", {
9814
9937
  cwd: pluginDir,
9815
9938
  stdio: "ignore"
9816
9939
  });
@@ -9820,30 +9943,36 @@ var PluginHost = class {
9820
9943
  }
9821
9944
  }
9822
9945
  if (manifest.deploy?.script) {
9823
- let deployScriptPath = import_path4.default.join(pluginDir, manifest.deploy.script);
9824
- if (!import_fs5.default.existsSync(deployScriptPath)) {
9825
- const altPath = import_path4.default.join(process.cwd(), "v2_plugins", "scratch-editor-deploy", manifest.deploy.script);
9826
- if (import_fs5.default.existsSync(altPath)) deployScriptPath = altPath;
9827
- }
9828
- if (import_fs5.default.existsSync(deployScriptPath)) {
9829
- console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
9830
- try {
9831
- const { execSync: execSync2 } = await import("node:child_process");
9832
- execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
9833
- console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
9834
- } catch (deployErr) {
9835
- console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
9836
- throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
9837
- }
9946
+ if (process.env.ALLOW_UNSAFE_PLUGIN_SCRIPTS !== "true") {
9947
+ console.warn(
9948
+ `[SECURITY WARNING] Deploy script "${manifest.deploy.script}" for plugin "${manifest.id}" blocked by default security policy. Set ALLOW_UNSAFE_PLUGIN_SCRIPTS=true to enable.`
9949
+ );
9838
9950
  } else {
9839
- console.warn(`[PluginHost] Deploy script "${manifest.deploy.script}" not found for plugin "${manifest.id}"`);
9951
+ let deployScriptPath = import_path4.default.join(pluginDir, manifest.deploy.script);
9952
+ if (!import_fs5.default.existsSync(deployScriptPath)) {
9953
+ const altPath = import_path4.default.join(process.cwd(), "v2_plugins", "scratch-editor-deploy", manifest.deploy.script);
9954
+ if (import_fs5.default.existsSync(altPath)) deployScriptPath = altPath;
9955
+ }
9956
+ if (import_fs5.default.existsSync(deployScriptPath)) {
9957
+ console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
9958
+ try {
9959
+ const { execSync: execSync2 } = await import("node:child_process");
9960
+ execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
9961
+ console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
9962
+ } catch (deployErr) {
9963
+ console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
9964
+ throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
9965
+ }
9966
+ } else {
9967
+ console.warn(`[PluginHost] Deploy script "${manifest.deploy.script}" not found for plugin "${manifest.id}"`);
9968
+ }
9840
9969
  }
9841
9970
  }
9842
9971
  if (manifest.deploy?.staticRoute && manifest.deploy?.staticDir && this.expressApp) {
9843
9972
  const route = manifest.deploy.staticRoute;
9844
9973
  const absDir = import_path4.default.join(pluginDir, manifest.deploy.staticDir);
9845
9974
  if (import_fs5.default.existsSync(absDir)) {
9846
- this.expressApp.use(route, import_express.default.static(absDir));
9975
+ this.expressApp.use(route, ...createPluginStaticMiddleware(absDir));
9847
9976
  this._registeredRoutes.set(manifest.id, route);
9848
9977
  console.log(`[PluginHost] Registered static route "${route}" for plugin "${manifest.id}"`);
9849
9978
  } else {
@@ -10580,8 +10709,8 @@ var HotReloadController = class {
10580
10709
  const timer = setTimeout(async () => {
10581
10710
  this.debounceTimers.delete(pluginId);
10582
10711
  try {
10583
- const fs15 = await import("fs/promises");
10584
- const newSourceCode = await fs15.readFile(filePath, "utf-8");
10712
+ const fs16 = await import("fs/promises");
10713
+ const newSourceCode = await fs16.readFile(filePath, "utf-8");
10585
10714
  console.log(
10586
10715
  `[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`
10587
10716
  );
@@ -17372,13 +17501,22 @@ function setupPresence({ io, eventBus }) {
17372
17501
  };
17373
17502
  io.on("connection", (socket) => {
17374
17503
  let registeredStudentId = null;
17504
+ const session = socket.data?.session;
17505
+ const isTeacherOrAdmin = session?.role === "teacher" || session?.role === "administrator";
17375
17506
  socket.on("register-student", (data) => {
17507
+ if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
17508
+ console.warn(`[Presence Security] Student ${session.userId} attempted to impersonate ${data.studentId}`);
17509
+ return socket.emit("error", { message: "Forbidden: Cannot register presence for another student" });
17510
+ }
17376
17511
  registeredStudentId = data.studentId;
17377
17512
  onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
17378
17513
  console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
17379
17514
  broadcastPresence();
17380
17515
  });
17381
17516
  socket.on("enter-lesson", (data) => {
17517
+ if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
17518
+ return socket.emit("error", { message: "Forbidden: Cannot enter lesson for another student" });
17519
+ }
17382
17520
  activeStudentLessons.set(data.studentId, data.lessonId);
17383
17521
  socket.join(data.lessonId);
17384
17522
  console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
@@ -17392,6 +17530,9 @@ function setupPresence({ io, eventBus }) {
17392
17530
  }
17393
17531
  });
17394
17532
  socket.on("leave-lesson", (data) => {
17533
+ if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
17534
+ return socket.emit("error", { message: "Forbidden: Cannot leave lesson for another student" });
17535
+ }
17395
17536
  const oldRoom = activeStudentLessons.get(data.studentId);
17396
17537
  if (oldRoom) {
17397
17538
  socket.leave(oldRoom);
@@ -17428,10 +17569,18 @@ function setupPresence({ io, eventBus }) {
17428
17569
  }
17429
17570
  );
17430
17571
  socket.on("teacher-broadcast-segment", (data) => {
17572
+ if (session && !isTeacherOrAdmin) {
17573
+ console.warn(`[Presence Security] Unauthorized teacher-broadcast-segment by ${session?.userId}`);
17574
+ return socket.emit("error", { message: "Forbidden: Only teachers or administrators can broadcast segments" });
17575
+ }
17431
17576
  lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
17432
17577
  io.to(data.lessonId).emit("student-active-segment-changed", data);
17433
17578
  });
17434
17579
  socket.on("teacher-ping-student", (data) => {
17580
+ if (session && !isTeacherOrAdmin) {
17581
+ console.warn(`[Presence Security] Unauthorized teacher-ping-student by ${session?.userId}`);
17582
+ return socket.emit("error", { message: "Forbidden: Only teachers or administrators can ping students" });
17583
+ }
17435
17584
  console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
17436
17585
  const studentOnlineInfo = onlineStudents.get(data.studentId);
17437
17586
  if (studentOnlineInfo) {
@@ -17539,6 +17688,83 @@ async function runStartupMigrations(db2) {
17539
17688
  }
17540
17689
  }
17541
17690
 
17691
+ // server/utils/migrate.ts
17692
+ var import_fs7 = __toESM(require("fs"), 1);
17693
+ var import_path6 = __toESM(require("path"), 1);
17694
+ function splitSqlStatements(sql) {
17695
+ return sql.split(";").map((s) => s.trim()).filter((s) => {
17696
+ const content = s.replace(/--.*$/gm, "").trim();
17697
+ return content.length > 0;
17698
+ });
17699
+ }
17700
+ function parseMigrationSql(name, content) {
17701
+ const upMatch = content.match(/--\s*UP([\s\S]*?)(?=--\s*DOWN|$)/i);
17702
+ const downMatch = content.match(/--\s*DOWN([\s\S]*)$/i);
17703
+ const up = upMatch ? upMatch[1].trim() : content.trim();
17704
+ const down = downMatch ? downMatch[1].trim() : "";
17705
+ return { name, up, down };
17706
+ }
17707
+ function loadMigrationsFromDirectory(dirPath) {
17708
+ if (!import_fs7.default.existsSync(dirPath)) {
17709
+ return [];
17710
+ }
17711
+ const files = import_fs7.default.readdirSync(dirPath).filter((f) => f.endsWith(".sql")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" }));
17712
+ return files.map((file) => {
17713
+ const fullPath = import_path6.default.join(dirPath, file);
17714
+ const content = import_fs7.default.readFileSync(fullPath, "utf8");
17715
+ const name = import_path6.default.basename(file, ".sql");
17716
+ return parseMigrationSql(name, content);
17717
+ });
17718
+ }
17719
+ function executeSqlStatements(db2, sql) {
17720
+ const statements = splitSqlStatements(sql);
17721
+ for (const stmt of statements) {
17722
+ try {
17723
+ db2.exec(stmt);
17724
+ } catch (err) {
17725
+ if (err?.message && err.message.includes("duplicate column name")) {
17726
+ console.warn(`[Migration] Column already exists, safely continuing: ${stmt.slice(0, 60)}...`);
17727
+ continue;
17728
+ }
17729
+ throw err;
17730
+ }
17731
+ }
17732
+ }
17733
+ function runMigrations(db2, migrations) {
17734
+ db2.exec(`
17735
+ CREATE TABLE IF NOT EXISTS _migrations (
17736
+ name TEXT PRIMARY KEY,
17737
+ applied_at INTEGER NOT NULL,
17738
+ checksum TEXT NOT NULL
17739
+ );
17740
+ `);
17741
+ const applied = new Set(
17742
+ db2.prepare("SELECT name FROM _migrations").all().map((r) => r.name)
17743
+ );
17744
+ for (const migration of migrations) {
17745
+ if (applied.has(migration.name)) continue;
17746
+ console.log(`[Migration] Applying: ${migration.name}`);
17747
+ try {
17748
+ executeSqlStatements(db2, migration.up);
17749
+ const checksum = simpleChecksum(migration.up);
17750
+ db2.prepare("INSERT INTO _migrations (name, applied_at, checksum) VALUES (?, ?, ?)").run(migration.name, Date.now(), checksum);
17751
+ console.log(`[Migration] Applied: ${migration.name}`);
17752
+ } catch (err) {
17753
+ console.error(`[Migration] FAILED: ${migration.name}`, err);
17754
+ throw err;
17755
+ }
17756
+ }
17757
+ }
17758
+ function simpleChecksum(text) {
17759
+ let hash = 0;
17760
+ for (let i = 0; i < text.length; i++) {
17761
+ const char = text.charCodeAt(i);
17762
+ hash = (hash << 5) - hash + char;
17763
+ hash |= 0;
17764
+ }
17765
+ return hash.toString(16);
17766
+ }
17767
+
17542
17768
  // server/ai-agent.ts
17543
17769
  var import_genai2 = require("@google/genai");
17544
17770
  var import_crypto4 = __toESM(require("crypto"), 1);
@@ -17850,6 +18076,90 @@ var runOpenAIAgentChat = async (provider, request) => {
17850
18076
  };
17851
18077
  };
17852
18078
 
18079
+ // server/middleware/auth.ts
18080
+ function getValidSession(token) {
18081
+ let sessionRow;
18082
+ try {
18083
+ sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
18084
+ } catch {
18085
+ return null;
18086
+ }
18087
+ if (!sessionRow) return null;
18088
+ const now = Date.now();
18089
+ if (sessionRow.expires_at && sessionRow.expires_at < now) {
18090
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18091
+ return null;
18092
+ }
18093
+ const idleTimeout = 24 * 60 * 60 * 1e3;
18094
+ if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
18095
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18096
+ return null;
18097
+ }
18098
+ kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
18099
+ return JSON.parse(sessionRow.session_data);
18100
+ }
18101
+ function getCookieToken(req) {
18102
+ const rc = req.headers.cookie;
18103
+ if (!rc) return null;
18104
+ const parts = rc.split(";");
18105
+ for (const part of parts) {
18106
+ const trimmed = part.trim();
18107
+ if (trimmed.startsWith("edu_os_token=")) {
18108
+ return trimmed.substring("edu_os_token=".length);
18109
+ }
18110
+ }
18111
+ return null;
18112
+ }
18113
+ function getActorId(req) {
18114
+ const token = getCookieToken(req);
18115
+ if (!token) return "anonymous";
18116
+ try {
18117
+ const session = getValidSession(token);
18118
+ if (!session) return "anonymous";
18119
+ let role = session.subRole || session.role;
18120
+ if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18121
+ role = "administrator";
18122
+ }
18123
+ if (role) {
18124
+ return `user:${session.userId || "demo"}:${role}`;
18125
+ }
18126
+ return "anonymous";
18127
+ } catch {
18128
+ return "anonymous";
18129
+ }
18130
+ }
18131
+ function requireAuth(...roles) {
18132
+ return (req, res, next) => {
18133
+ const token = getCookieToken(req);
18134
+ if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
18135
+ const session = getValidSession(token);
18136
+ if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
18137
+ if (roles.length > 0) {
18138
+ let userRole = session.role;
18139
+ if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
18140
+ userRole = "administrator";
18141
+ }
18142
+ const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
18143
+ if (!effectiveRoles.includes(userRole)) {
18144
+ return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
18145
+ }
18146
+ }
18147
+ req.session = session;
18148
+ next();
18149
+ };
18150
+ }
18151
+ function checkIsTeacherOrAdmin(req) {
18152
+ const token = getCookieToken(req);
18153
+ if (!token) return false;
18154
+ try {
18155
+ const session = getValidSession(token);
18156
+ if (!session) return false;
18157
+ return session.role === "teacher" || session.role === "administrator";
18158
+ } catch {
18159
+ return false;
18160
+ }
18161
+ }
18162
+
17853
18163
  // packages/core/bootstrap/adapter/bootstrap-registration.ts
17854
18164
  var BootstrapRegistration = class {
17855
18165
  static registerConfiguration(builder, context) {
@@ -18334,28 +18644,28 @@ var IActivityRegistryToken = new Token(
18334
18644
  );
18335
18645
 
18336
18646
  // server/routes/os.ts
18337
- var import_path8 = __toESM(require("path"), 1);
18338
- var import_fs8 = __toESM(require("fs"), 1);
18647
+ var import_path9 = __toESM(require("path"), 1);
18648
+ var import_fs9 = __toESM(require("fs"), 1);
18339
18649
  var import_child_process = require("child_process");
18340
18650
  var import_crypto7 = __toESM(require("crypto"), 1);
18341
18651
  var import_xss = require("xss");
18342
18652
 
18343
18653
  // server/utils/crypto.ts
18344
18654
  var import_crypto5 = __toESM(require("crypto"), 1);
18345
- var import_fs7 = __toESM(require("fs"), 1);
18346
- var import_path6 = __toESM(require("path"), 1);
18655
+ var import_fs8 = __toESM(require("fs"), 1);
18656
+ var import_path7 = __toESM(require("path"), 1);
18347
18657
  var _encryptionKey = null;
18348
18658
  function getEncryptionKey() {
18349
18659
  if (_encryptionKey) return _encryptionKey;
18350
- const envPath = import_path6.default.resolve(process.cwd(), ".env");
18660
+ const envPath = import_path7.default.resolve(process.cwd(), ".env");
18351
18661
  const keyHex = process.env.ENCRYPTION_KEY;
18352
18662
  if (keyHex && keyHex.trim() !== "") {
18353
18663
  _encryptionKey = Buffer.from(keyHex.trim(), "hex");
18354
18664
  return _encryptionKey;
18355
18665
  }
18356
- if (import_fs7.default.existsSync(envPath)) {
18666
+ if (import_fs8.default.existsSync(envPath)) {
18357
18667
  try {
18358
- const content = import_fs7.default.readFileSync(envPath, "utf-8");
18668
+ const content = import_fs8.default.readFileSync(envPath, "utf-8");
18359
18669
  const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
18360
18670
  if (match && match[1].trim() !== "") {
18361
18671
  const fileKeyHex = match[1].trim();
@@ -18369,8 +18679,8 @@ function getEncryptionKey() {
18369
18679
  }
18370
18680
  const newKey = import_crypto5.default.randomBytes(32).toString("hex");
18371
18681
  try {
18372
- if (import_fs7.default.existsSync(envPath)) {
18373
- const existingContent = import_fs7.default.readFileSync(envPath, "utf-8");
18682
+ if (import_fs8.default.existsSync(envPath)) {
18683
+ const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
18374
18684
  if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
18375
18685
  console.warn(
18376
18686
  "[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."
@@ -18379,12 +18689,12 @@ function getEncryptionKey() {
18379
18689
  _encryptionKey = Buffer.from(newKey, "hex");
18380
18690
  return _encryptionKey;
18381
18691
  }
18382
- import_fs7.default.appendFileSync(envPath, `
18692
+ import_fs8.default.appendFileSync(envPath, `
18383
18693
  ENCRYPTION_KEY=${newKey}
18384
18694
  `);
18385
18695
  console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
18386
18696
  } else {
18387
- import_fs7.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
18697
+ import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
18388
18698
  `);
18389
18699
  console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
18390
18700
  }
@@ -18437,142 +18747,58 @@ function detectPromptInjection(input) {
18437
18747
  return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
18438
18748
  }
18439
18749
 
18440
- // server/middleware/auth.ts
18441
- function getValidSession(token) {
18442
- let sessionRow;
18750
+ // server/routes/shared.ts
18751
+ var import_path8 = __toESM(require("path"), 1);
18752
+ var import_crypto6 = __toESM(require("crypto"), 1);
18753
+
18754
+ // server/utils/bridge-sdk.ts
18755
+ var BRIDGE_SDK_CODE = `(function() {
18756
+ // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
18443
18757
  try {
18444
- sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
18445
- } catch {
18446
- return null;
18447
- }
18448
- if (!sessionRow) return null;
18449
- const now = Date.now();
18450
- if (sessionRow.expires_at && sessionRow.expires_at < now) {
18451
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18452
- return null;
18758
+ Object.defineProperty(document, 'cookie', {
18759
+ get: function() { return ""; },
18760
+ set: function(val) {},
18761
+ configurable: true
18762
+ });
18763
+ } catch (e) {
18764
+ try {
18765
+ Object.defineProperty(Document.prototype, 'cookie', {
18766
+ get: function() { return ""; },
18767
+ set: function(val) {},
18768
+ configurable: true
18769
+ });
18770
+ } catch (err) {}
18453
18771
  }
18454
- const idleTimeout = 24 * 60 * 60 * 1e3;
18455
- if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
18456
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18457
- return null;
18458
- }
18459
- kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
18460
- return JSON.parse(sessionRow.session_data);
18461
- }
18462
- function getCookieToken(req) {
18463
- const rc = req.headers.cookie;
18464
- if (!rc) return null;
18465
- const parts = rc.split(";");
18466
- for (const part of parts) {
18467
- const trimmed = part.trim();
18468
- if (trimmed.startsWith("edu_os_token=")) {
18469
- return trimmed.substring("edu_os_token=".length);
18470
- }
18471
- }
18472
- return null;
18473
- }
18474
- function getActorId(req) {
18475
- const token = getCookieToken(req);
18476
- if (!token) return "anonymous";
18477
- try {
18478
- const session = getValidSession(token);
18479
- if (!session) return "anonymous";
18480
- let role = session.subRole || session.role;
18481
- if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18482
- role = "administrator";
18483
- }
18484
- if (role) {
18485
- return `user:${session.userId || "demo"}:${role}`;
18486
- }
18487
- return "anonymous";
18488
- } catch {
18489
- return "anonymous";
18490
- }
18491
- }
18492
- function requireAuth(...roles) {
18493
- return (req, res, next) => {
18494
- const token = getCookieToken(req);
18495
- if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
18496
- const session = getValidSession(token);
18497
- if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
18498
- if (roles.length > 0) {
18499
- let userRole = session.role;
18500
- if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
18501
- userRole = "administrator";
18502
- }
18503
- const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
18504
- if (!effectiveRoles.includes(userRole)) {
18505
- return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
18506
- }
18507
- }
18508
- req.session = session;
18509
- next();
18510
- };
18511
- }
18512
- function checkIsTeacherOrAdmin(req) {
18513
- const token = getCookieToken(req);
18514
- if (!token) return false;
18515
- try {
18516
- const session = getValidSession(token);
18517
- if (!session) return false;
18518
- return session.role === "teacher" || session.role === "administrator";
18519
- } catch {
18520
- return false;
18521
- }
18522
- }
18523
-
18524
- // server/routes/shared.ts
18525
- var import_path7 = __toESM(require("path"), 1);
18526
- var import_crypto6 = __toESM(require("crypto"), 1);
18527
-
18528
- // server/utils/bridge-sdk.ts
18529
- var BRIDGE_SDK_CODE = `(function() {
18530
- // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
18531
- try {
18532
- Object.defineProperty(document, 'cookie', {
18533
- get: function() { return ""; },
18534
- set: function(val) {},
18535
- configurable: true
18536
- });
18537
- } catch (e) {
18538
- try {
18539
- Object.defineProperty(Document.prototype, 'cookie', {
18540
- get: function() { return ""; },
18541
- set: function(val) {},
18542
- configurable: true
18543
- });
18544
- } catch (err) {}
18545
- }
18546
-
18547
- // \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
18548
- function __makeSafePostMessage(realTarget, label) {
18549
- return function(message, targetOrigin, transfer) {
18550
- try {
18551
- if (message && typeof message === 'object') {
18552
- if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
18553
- message.attempt_id = window.__LMS_STUDENT__.attempt_id;
18554
- }
18555
- if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
18556
- message.uuid = window.__LMS_COURSEWARE__.uuid;
18557
- }
18558
- }
18559
- } catch (e) {}
18560
-
18561
- var origin = targetOrigin;
18562
- if (origin === 'null' || origin === null || origin === undefined) {
18563
- console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
18564
- origin = '*';
18565
- }
18566
- try {
18567
- return realTarget.postMessage.call(realTarget, message, origin, transfer);
18568
- } catch (err) {
18569
- if (err.name === 'SyntaxError' && origin !== '*') {
18570
- console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
18571
- return realTarget.postMessage.call(realTarget, message, '*', transfer);
18572
- }
18573
- throw err;
18574
- }
18575
- };
18772
+
18773
+ // \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
18774
+ function __makeSafePostMessage(realTarget, label) {
18775
+ return function(message, targetOrigin, transfer) {
18776
+ try {
18777
+ if (message && typeof message === 'object') {
18778
+ if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
18779
+ message.attempt_id = window.__LMS_STUDENT__.attempt_id;
18780
+ }
18781
+ if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
18782
+ message.uuid = window.__LMS_COURSEWARE__.uuid;
18783
+ }
18784
+ }
18785
+ } catch (e) {}
18786
+
18787
+ var origin = targetOrigin;
18788
+ if (origin === 'null' || origin === null || origin === undefined) {
18789
+ console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
18790
+ origin = '*';
18791
+ }
18792
+ try {
18793
+ return realTarget.postMessage.call(realTarget, message, origin, transfer);
18794
+ } catch (err) {
18795
+ if (err.name === 'SyntaxError' && origin !== '*') {
18796
+ console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
18797
+ return realTarget.postMessage.call(realTarget, message, '*', transfer);
18798
+ }
18799
+ throw err;
18800
+ }
18801
+ };
18576
18802
  }
18577
18803
 
18578
18804
  // \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
@@ -18683,17 +18909,51 @@ var BRIDGE_SDK_CODE = `(function() {
18683
18909
  }
18684
18910
  } catch (e) {}
18685
18911
 
18686
- // \u2500\u2500 Bidirectional host\u2192courseware command bus \u2500\u2500
18912
+ // \u2500\u2500 Bidirectional host\u2192courseware command bus & Theme Bridge \u2500\u2500
18687
18913
  var __lmsHandlers = {};
18688
18914
  var __lmsPendingRequests = {};
18915
+
18916
+ function __applyThemeTokens(themeData) {
18917
+ if (!themeData || typeof themeData !== 'object') return;
18918
+ try {
18919
+ window.__LMS_THEME__ = themeData;
18920
+ var docEl = document.documentElement;
18921
+ if (docEl) {
18922
+ if (themeData.theme) {
18923
+ docEl.setAttribute('data-theme', themeData.theme);
18924
+ }
18925
+ if (themeData.tokens && typeof themeData.tokens === 'object') {
18926
+ for (var k in themeData.tokens) {
18927
+ if (Object.prototype.hasOwnProperty.call(themeData.tokens, k)) {
18928
+ docEl.style.setProperty(k, themeData.tokens[k]);
18929
+ }
18930
+ }
18931
+ }
18932
+ }
18933
+ } catch (e) {}
18934
+ }
18935
+
18936
+ if (window.__LMS_THEME__) {
18937
+ __applyThemeTokens(window.__LMS_THEME__);
18938
+ }
18939
+
18689
18940
  window.addEventListener('message', function(event) {
18690
18941
  var d = event.data;
18691
18942
  if (!d || typeof d !== 'object') return;
18692
18943
  if (d.type === 'LMS_HOST_COMMAND' && d.event) {
18944
+ if (d.event === 'theme:changed' && d.payload) {
18945
+ __applyThemeTokens(d.payload);
18946
+ }
18693
18947
  var cbs = __lmsHandlers[d.event] || [];
18694
18948
  for (var i = 0; i < cbs.length; i++) {
18695
18949
  try { cbs[i](d.payload); } catch (e) {}
18696
18950
  }
18951
+ } else if (d.type === 'LMS_THEME_CHANGED' && (d.theme || d.tokens)) {
18952
+ __applyThemeTokens(d);
18953
+ var themeCbs = __lmsHandlers['theme:changed'] || [];
18954
+ for (var j = 0; j < themeCbs.length; j++) {
18955
+ try { themeCbs[j](d); } catch (e) {}
18956
+ }
18697
18957
  } else if (d.type === 'LMS_PROGRESS_RESPONSE' && d.requestId) {
18698
18958
  var pending = __lmsPendingRequests[d.requestId];
18699
18959
  if (pending) {
@@ -18735,6 +18995,9 @@ var BRIDGE_SDK_CODE = `(function() {
18735
18995
  getCourseware() {
18736
18996
  return window.__LMS_COURSEWARE__;
18737
18997
  },
18998
+ getTheme() {
18999
+ return window.__LMS_THEME__ || null;
19000
+ },
18738
19001
  log(event, data) {
18739
19002
  window.parent.postMessage({
18740
19003
  type: "LMS_LOG",
@@ -19147,7 +19410,7 @@ function validateMagicBytes(buffer, fileName) {
19147
19410
  ".webp": [[82, 73, 70, 70]]
19148
19411
  // RIFF
19149
19412
  };
19150
- const ext = import_path7.default.extname(fileName || "").toLowerCase();
19413
+ const ext = import_path8.default.extname(fileName || "").toLowerCase();
19151
19414
  const signatures = MAGIC_BYTES[ext];
19152
19415
  if (!signatures) return true;
19153
19416
  return signatures.some(
@@ -19250,20 +19513,21 @@ var generateStudentNumber = (db2) => {
19250
19513
  return `S${nextSeq.toString().padStart(3, "0")}`;
19251
19514
  };
19252
19515
 
19516
+ // server/utils/error-handler.ts
19517
+ function sendSafeError(res, err, status = 500, fallbackMessage = "Internal server error") {
19518
+ const message = err instanceof Error ? err.message : String(err);
19519
+ if (process.env.NODE_ENV !== "test") {
19520
+ console.error(`[API Error ${status}]:`, err);
19521
+ }
19522
+ const isProd = process.env.NODE_ENV === "production";
19523
+ const exposedMessage = isProd ? fallbackMessage : message;
19524
+ return res.status(status).json({ success: false, error: exposedMessage });
19525
+ }
19526
+
19253
19527
  // server/routes/os.ts
19254
19528
  function registerOsRoutes(ctx) {
19255
19529
  const {
19256
19530
  app,
19257
- io,
19258
- loginLimiter,
19259
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19260
- lessonActiveSegments: lessonActiveSegments2,
19261
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19262
- buildAgentFinalMessage: buildAgentFinalMessage2,
19263
- normalizeToolSchema: normalizeToolSchema2,
19264
- buildOpenAITools: buildOpenAITools2,
19265
- executeAgentToolCall: executeAgentToolCall2,
19266
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19267
19531
  runGeminiAgentChat: runGeminiAgentChat2,
19268
19532
  runOpenAIAgentChat: runOpenAIAgentChat2,
19269
19533
  activityRegistry
@@ -19274,7 +19538,7 @@ function registerOsRoutes(ctx) {
19274
19538
  if (!filename || !base64Data) {
19275
19539
  return res.status(400).json({ error: "Filename and base64Data are required" });
19276
19540
  }
19277
- const ext = import_path8.default.extname(filename).toLowerCase();
19541
+ const ext = import_path9.default.extname(filename).toLowerCase();
19278
19542
  if (BLOCKED_EXTENSIONS.includes(ext)) {
19279
19543
  return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
19280
19544
  }
@@ -19286,13 +19550,13 @@ function registerOsRoutes(ctx) {
19286
19550
  if (!validateMagicBytes(fileBuffer, filename)) {
19287
19551
  return res.status(400).json({ error: "File content does not match the declared file type." });
19288
19552
  }
19289
- const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
19290
- if (!import_fs8.default.existsSync(uploadsDir)) {
19291
- import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
19553
+ const uploadsDir = import_path9.default.join(process.cwd(), "uploads");
19554
+ if (!import_fs9.default.existsSync(uploadsDir)) {
19555
+ import_fs9.default.mkdirSync(uploadsDir, { recursive: true });
19292
19556
  }
19293
19557
  const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
19294
- const filePath = import_path8.default.join(uploadsDir, uniqueName);
19295
- import_fs8.default.writeFileSync(filePath, fileBuffer);
19558
+ const filePath = import_path9.default.join(uploadsDir, uniqueName);
19559
+ import_fs9.default.writeFileSync(filePath, fileBuffer);
19296
19560
  let slideCount = 1;
19297
19561
  if (ext === ".pdf") {
19298
19562
  try {
@@ -19326,7 +19590,7 @@ function registerOsRoutes(ctx) {
19326
19590
  });
19327
19591
  } catch (e) {
19328
19592
  console.error("Upload error:", e);
19329
- res.status(500).json({ error: e.message });
19593
+ sendSafeError(res, e);
19330
19594
  }
19331
19595
  });
19332
19596
  app.post("/api/commands", requireAuth(), async (req, res) => {
@@ -19340,7 +19604,7 @@ function registerOsRoutes(ctx) {
19340
19604
  const result = await kernelContainer.commandBus.execute(cmd);
19341
19605
  res.json({ success: true, result });
19342
19606
  } catch (err) {
19343
- res.status(500).json({ success: false, error: err.message });
19607
+ sendSafeError(res, err);
19344
19608
  }
19345
19609
  });
19346
19610
  app.get("/api/commands/registered", (req, res) => {
@@ -19348,7 +19612,7 @@ function registerOsRoutes(ctx) {
19348
19612
  const actions = kernelContainer.actionRegistry.getAllActions();
19349
19613
  res.json(actions);
19350
19614
  } catch (err) {
19351
- res.status(500).json({ error: err.message });
19615
+ sendSafeError(res, err);
19352
19616
  }
19353
19617
  });
19354
19618
  app.get("/api/activities", async (req, res) => {
@@ -19357,7 +19621,7 @@ function registerOsRoutes(ctx) {
19357
19621
  const list = role === "all" ? activityRegistry.listDescriptors() : activityRegistry.listByRole(role).map((p) => p.descriptor);
19358
19622
  res.json({ activities: list });
19359
19623
  } catch (err) {
19360
- res.status(500).json({ error: err.message });
19624
+ sendSafeError(res, err);
19361
19625
  }
19362
19626
  });
19363
19627
  app.post("/api/activities/:id/start", async (req, res) => {
@@ -19379,7 +19643,7 @@ function registerOsRoutes(ctx) {
19379
19643
  res.json({ ok: true, ...result });
19380
19644
  } catch (err) {
19381
19645
  const status = err?.code === "PERMISSION_DENIED" ? 403 : 500;
19382
- res.status(status).json({ ok: false, error: err.message });
19646
+ sendSafeError(res, err, status);
19383
19647
  }
19384
19648
  });
19385
19649
  app.get("/api/activities/running", (_req, res) => {
@@ -19395,7 +19659,7 @@ function registerOsRoutes(ctx) {
19395
19659
  }));
19396
19660
  res.json({ activities });
19397
19661
  } catch (err) {
19398
- res.status(500).json({ error: err.message });
19662
+ sendSafeError(res, err);
19399
19663
  }
19400
19664
  });
19401
19665
  app.post("/api/activities/:id/finish", async (req, res) => {
@@ -19414,7 +19678,7 @@ function registerOsRoutes(ctx) {
19414
19678
  await provider.finish(context);
19415
19679
  res.json({ ok: true });
19416
19680
  } catch (err) {
19417
- res.status(500).json({ ok: false, error: err.message });
19681
+ sendSafeError(res, err);
19418
19682
  }
19419
19683
  });
19420
19684
  app.post("/api/activities/:id/pause", async (req, res) => {
@@ -19433,7 +19697,7 @@ function registerOsRoutes(ctx) {
19433
19697
  await provider.pause(context);
19434
19698
  res.json({ ok: true });
19435
19699
  } catch (err) {
19436
- res.status(500).json({ ok: false, error: err.message });
19700
+ sendSafeError(res, err);
19437
19701
  }
19438
19702
  });
19439
19703
  app.post("/api/activities/:id/resume", async (req, res) => {
@@ -19452,7 +19716,7 @@ function registerOsRoutes(ctx) {
19452
19716
  await provider.resume(context);
19453
19717
  res.json({ ok: true });
19454
19718
  } catch (err) {
19455
- res.status(500).json({ ok: false, error: err.message });
19719
+ sendSafeError(res, err);
19456
19720
  }
19457
19721
  });
19458
19722
  app.post("/api/agent/chat", async (req, res) => {
@@ -19510,7 +19774,7 @@ function registerOsRoutes(ctx) {
19510
19774
  });
19511
19775
  } catch (err) {
19512
19776
  console.error(err);
19513
- res.status(500).json({ success: false, error: err.message });
19777
+ sendSafeError(res, err);
19514
19778
  }
19515
19779
  });
19516
19780
  app.get("/api/agent/conversations", (req, res) => {
@@ -19530,7 +19794,7 @@ function registerOsRoutes(ctx) {
19530
19794
  messages: rows.map((r) => ({ role: r.role, content: r.content, createdAt: r.created_at }))
19531
19795
  });
19532
19796
  } catch (e) {
19533
- res.status(500).json({ error: e.message });
19797
+ sendSafeError(res, e);
19534
19798
  }
19535
19799
  });
19536
19800
  app.delete("/api/agent/conversations", (req, res) => {
@@ -19546,34 +19810,20 @@ function registerOsRoutes(ctx) {
19546
19810
  kernelContainer.db.prepare("DELETE FROM agent_conversations WHERE conv_key = ?").run(convKey);
19547
19811
  res.json({ success: true });
19548
19812
  } catch (e) {
19549
- res.status(500).json({ error: e.message });
19813
+ sendSafeError(res, e);
19550
19814
  }
19551
19815
  });
19552
19816
  }
19553
19817
 
19554
19818
  // server/routes/resources.ts
19555
19819
  function registerResourcesRoutes(ctx) {
19556
- const {
19557
- app,
19558
- io,
19559
- loginLimiter,
19560
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19561
- lessonActiveSegments: lessonActiveSegments2,
19562
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19563
- buildAgentFinalMessage: buildAgentFinalMessage2,
19564
- normalizeToolSchema: normalizeToolSchema2,
19565
- buildOpenAITools: buildOpenAITools2,
19566
- executeAgentToolCall: executeAgentToolCall2,
19567
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19568
- runGeminiAgentChat: runGeminiAgentChat2,
19569
- runOpenAIAgentChat: runOpenAIAgentChat2
19570
- } = ctx;
19820
+ const { app } = ctx;
19571
19821
  app.get("/api/resources", (req, res) => {
19572
19822
  try {
19573
19823
  const resources = kernelContainer.db.prepare("SELECT id, name, type, created_at FROM system_resources ORDER BY created_at DESC").all();
19574
19824
  res.json(resources);
19575
19825
  } catch (e) {
19576
- res.status(500).json({ error: e.message });
19826
+ sendSafeError(res, e);
19577
19827
  }
19578
19828
  });
19579
19829
  app.get("/api/resources/:id", (req, res) => {
@@ -19588,6 +19838,7 @@ function registerResourcesRoutes(ctx) {
19588
19838
  ).run(resource.id, resource.id, resource.name, "html", "index.html", resource.created_at || Date.now());
19589
19839
  }
19590
19840
  res.setHeader("Content-Type", "text/html; charset=utf-8");
19841
+ res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
19591
19842
  let html2 = resource.content || "";
19592
19843
  const baseTag2 = `<base href="/api/resources/${req.params.id}/">`;
19593
19844
  if (html2.toLowerCase().includes("<head>")) {
@@ -19632,7 +19883,7 @@ function registerResourcesRoutes(ctx) {
19632
19883
  html = injectLmsSdk(html, req, { id: resource.id, name: resource.name, uuid: resource.id });
19633
19884
  return res.send(html);
19634
19885
  } catch (e) {
19635
- res.status(500).send(e.message);
19886
+ sendSafeError(res, e);
19636
19887
  }
19637
19888
  });
19638
19889
  app.get("/api/resources/:id/*", (req, res) => {
@@ -19718,15 +19969,16 @@ function registerResourcesRoutes(ctx) {
19718
19969
  } else {
19719
19970
  let content = fileObj.content;
19720
19971
  if (contentType.startsWith("text/html")) {
19972
+ res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
19721
19973
  content = injectLmsSdk(content, req, { id: resource.id, name: resource.name, uuid: resource.id });
19722
19974
  }
19723
19975
  return res.send(content);
19724
19976
  }
19725
19977
  } catch (e) {
19726
- res.status(500).send(e.message);
19978
+ sendSafeError(res, e);
19727
19979
  }
19728
19980
  });
19729
- app.post("/api/resources", async (req, res) => {
19981
+ app.post("/api/resources", requireAuth("teacher", "administrator"), async (req, res) => {
19730
19982
  try {
19731
19983
  const { name, type, content } = req.body;
19732
19984
  if (!name || !type) {
@@ -19783,15 +20035,15 @@ function registerResourcesRoutes(ctx) {
19783
20035
  }
19784
20036
  res.json({ success: true, id, name, type });
19785
20037
  } catch (e) {
19786
- res.status(500).json({ error: e.message });
20038
+ sendSafeError(res, e);
19787
20039
  }
19788
20040
  });
19789
- app.delete("/api/resources/:id", (req, res) => {
20041
+ app.delete("/api/resources/:id", requireAuth("teacher", "administrator"), (req, res) => {
19790
20042
  try {
19791
20043
  kernelContainer.db.prepare("DELETE FROM system_resources WHERE id = ?").run(req.params.id);
19792
20044
  res.json({ success: true });
19793
20045
  } catch (e) {
19794
- res.status(500).json({ error: e.message });
20046
+ sendSafeError(res, e);
19795
20047
  }
19796
20048
  });
19797
20049
  const RESOURCE_HANDLERS = {
@@ -19841,61 +20093,51 @@ function registerResourcesRoutes(ctx) {
19841
20093
  }
19842
20094
 
19843
20095
  // server/routes/courseware.ts
19844
- var import_path9 = __toESM(require("path"), 1);
19845
- var import_fs9 = __toESM(require("fs"), 1);
20096
+ var import_path10 = __toESM(require("path"), 1);
20097
+ var import_fs10 = __toESM(require("fs"), 1);
19846
20098
  var import_crypto9 = __toESM(require("crypto"), 1);
19847
20099
  function registerCoursewareRoutes(ctx) {
19848
- const {
19849
- app,
19850
- io,
19851
- loginLimiter,
19852
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19853
- lessonActiveSegments: lessonActiveSegments2,
19854
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19855
- buildAgentFinalMessage: buildAgentFinalMessage2,
19856
- normalizeToolSchema: normalizeToolSchema2,
19857
- buildOpenAITools: buildOpenAITools2,
19858
- executeAgentToolCall: executeAgentToolCall2,
19859
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19860
- runGeminiAgentChat: runGeminiAgentChat2,
19861
- runOpenAIAgentChat: runOpenAIAgentChat2
19862
- } = ctx;
19863
- app.post("/api/courseware/upload", async (req, res) => {
20100
+ const { app, io } = ctx;
20101
+ app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
19864
20102
  try {
19865
20103
  const { name, filename, base64Data } = req.body;
19866
- const cmd = kernelContainer.commandBus.createCommand("courseware.upload", { name, filename, base64Data }, "teacher-demo");
20104
+ const actorId = getActorId(req) || "teacher";
20105
+ const cmd = kernelContainer.commandBus.createCommand("courseware.upload", { name, filename, base64Data }, actorId);
19867
20106
  const result = await kernelContainer.commandBus.execute(cmd);
19868
20107
  res.json(result);
19869
20108
  } catch (e) {
19870
- res.status(500).json({ error: e.message });
20109
+ sendSafeError(res, e);
19871
20110
  }
19872
20111
  });
19873
- app.post("/api/courseware/confirm", async (req, res) => {
20112
+ app.post("/api/courseware/confirm", requireAuth("teacher", "administrator"), async (req, res) => {
19874
20113
  try {
19875
20114
  const { uuid, name, entry } = req.body;
19876
- const cmd = kernelContainer.commandBus.createCommand("courseware.confirm", { uuid, name, entry }, "teacher-demo");
20115
+ const actorId = getActorId(req) || "teacher";
20116
+ const cmd = kernelContainer.commandBus.createCommand("courseware.confirm", { uuid, name, entry }, actorId);
19877
20117
  const result = await kernelContainer.commandBus.execute(cmd);
19878
20118
  res.json(result);
19879
20119
  } catch (e) {
19880
- res.status(500).json({ error: e.message });
20120
+ sendSafeError(res, e);
19881
20121
  }
19882
20122
  });
19883
- app.get("/api/courseware", async (req, res) => {
20123
+ app.get("/api/courseware", requireAuth(), async (req, res) => {
19884
20124
  try {
19885
- const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, "teacher-demo");
20125
+ const actorId = getActorId(req) || "system";
20126
+ const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId);
19886
20127
  const result = await kernelContainer.commandBus.execute(cmd);
19887
20128
  res.json(result);
19888
20129
  } catch (e) {
19889
- res.status(500).json({ error: e.message });
20130
+ sendSafeError(res, e);
19890
20131
  }
19891
20132
  });
19892
- app.delete("/api/courseware/:id", async (req, res) => {
20133
+ app.delete("/api/courseware/:id", requireAuth("teacher", "administrator"), async (req, res) => {
19893
20134
  try {
19894
- const cmd = kernelContainer.commandBus.createCommand("courseware.delete", { id: req.params.id }, "teacher-demo");
20135
+ const actorId = getActorId(req) || "teacher";
20136
+ const cmd = kernelContainer.commandBus.createCommand("courseware.delete", { id: req.params.id }, actorId);
19895
20137
  const result = await kernelContainer.commandBus.execute(cmd);
19896
20138
  res.json(result);
19897
20139
  } catch (e) {
19898
- res.status(500).json({ error: e.message });
20140
+ sendSafeError(res, e);
19899
20141
  }
19900
20142
  });
19901
20143
  function extractScoreCommentCompletion(payload) {
@@ -19988,6 +20230,19 @@ function registerCoursewareRoutes(ctx) {
19988
20230
  try {
19989
20231
  const { attemptId } = req.params;
19990
20232
  const { eventType, payload } = req.body;
20233
+ const token = getCookieToken(req);
20234
+ const session = req.session || (token ? getValidSession(token) : null);
20235
+ if (!session) {
20236
+ return res.status(401).json({ error: "Authentication required" });
20237
+ }
20238
+ if (session.role !== "teacher" && session.role !== "administrator") {
20239
+ const attemptRow = kernelContainer.db.prepare(
20240
+ "SELECT student_id FROM courseware_attempt WHERE id = ?"
20241
+ ).get(attemptId);
20242
+ if (attemptRow && attemptRow.student_id !== session.userId) {
20243
+ return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
20244
+ }
20245
+ }
19991
20246
  const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
19992
20247
  kernelContainer.db.prepare(
19993
20248
  "INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
@@ -20051,13 +20306,26 @@ function registerCoursewareRoutes(ctx) {
20051
20306
  });
20052
20307
  res.json({ success: true });
20053
20308
  } catch (e) {
20054
- res.status(500).json({ error: e.message });
20309
+ sendSafeError(res, e);
20055
20310
  }
20056
20311
  });
20057
20312
  app.post("/api/courseware/attempts/:attemptId/submit", async (req, res) => {
20058
20313
  try {
20059
20314
  const { attemptId } = req.params;
20060
20315
  let { score, comment, completion, status, extra = {} } = req.body;
20316
+ const token = getCookieToken(req);
20317
+ const session = req.session || (token ? getValidSession(token) : null);
20318
+ if (!session) {
20319
+ return res.status(401).json({ error: "Authentication required to submit attempt scores" });
20320
+ }
20321
+ if (session.role !== "teacher" && session.role !== "administrator") {
20322
+ const attemptRow = kernelContainer.db.prepare(
20323
+ "SELECT student_id FROM courseware_attempt WHERE id = ?"
20324
+ ).get(attemptId);
20325
+ if (attemptRow && attemptRow.student_id !== session.userId) {
20326
+ return res.status(403).json({ error: "Forbidden: Cannot submit scores for another student" });
20327
+ }
20328
+ }
20061
20329
  const extracted = extractScoreCommentCompletion({ ...req.body, ...extra });
20062
20330
  if (score === void 0 || score === null) score = extracted.score;
20063
20331
  if (comment === void 0 || comment === null) comment = extracted.comment;
@@ -20076,6 +20344,7 @@ function registerCoursewareRoutes(ctx) {
20076
20344
  parsedCompletion = num;
20077
20345
  }
20078
20346
  }
20347
+ const actorId = session.userId || getActorId(req) || "student";
20079
20348
  const cmd = kernelContainer.commandBus.createCommand("courseware.submit_attempt", {
20080
20349
  attemptId,
20081
20350
  score: parsedScore,
@@ -20083,12 +20352,12 @@ function registerCoursewareRoutes(ctx) {
20083
20352
  completion: parsedCompletion,
20084
20353
  status,
20085
20354
  extra
20086
- }, "student-demo");
20355
+ }, actorId);
20087
20356
  const result = await kernelContainer.commandBus.execute(cmd);
20088
20357
  io.emit("courseware-attempt-updated", { attemptId, type: "submit" });
20089
20358
  res.json(result);
20090
20359
  } catch (e) {
20091
- res.status(500).json({ error: e.message });
20360
+ sendSafeError(res, e);
20092
20361
  }
20093
20362
  });
20094
20363
  app.get("/api/courseware/attempts", (req, res) => {
@@ -20113,7 +20382,7 @@ function registerCoursewareRoutes(ctx) {
20113
20382
  `).all();
20114
20383
  res.json(rows);
20115
20384
  } catch (e) {
20116
- res.status(500).json({ error: e.message });
20385
+ sendSafeError(res, e);
20117
20386
  }
20118
20387
  });
20119
20388
  app.post("/api/courseware/debug", (req, res) => {
@@ -20121,12 +20390,12 @@ function registerCoursewareRoutes(ctx) {
20121
20390
  const { msg, url, student, courseware } = req.body;
20122
20391
  const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
20123
20392
  console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
20124
- const logFile2 = import_path9.default.join(process.cwd(), "client_debug.log");
20125
- import_fs9.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
20393
+ const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
20394
+ import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
20126
20395
  `);
20127
20396
  res.json({ success: true });
20128
20397
  } catch (e) {
20129
- res.status(500).json({ error: e.message });
20398
+ sendSafeError(res, e);
20130
20399
  }
20131
20400
  });
20132
20401
  app.get("/api/courseware/attempts/:attemptId/raw", async (req, res) => {
@@ -20136,7 +20405,7 @@ function registerCoursewareRoutes(ctx) {
20136
20405
  const result = await kernelContainer.commandBus.execute(cmd);
20137
20406
  res.json(result);
20138
20407
  } catch (e) {
20139
- res.status(500).json({ error: e.message });
20408
+ sendSafeError(res, e);
20140
20409
  }
20141
20410
  });
20142
20411
  app.get("/api/courseware/attempts/:attemptId/progress", (req, res) => {
@@ -20157,7 +20426,7 @@ function registerCoursewareRoutes(ctx) {
20157
20426
  progress: { score: result.score, comment: result.comment, completion: result.completion, extra }
20158
20427
  });
20159
20428
  } catch (e) {
20160
- res.status(500).json({ error: e.message });
20429
+ sendSafeError(res, e);
20161
20430
  }
20162
20431
  });
20163
20432
  app.post("/api/courseware/attempts/:attemptId/promote", async (req, res) => {
@@ -20248,7 +20517,7 @@ function registerCoursewareRoutes(ctx) {
20248
20517
  });
20249
20518
  res.json({ success: true, assignmentId, score: finalScore });
20250
20519
  } catch (e) {
20251
- res.status(500).json({ error: e.message });
20520
+ sendSafeError(res, e);
20252
20521
  }
20253
20522
  });
20254
20523
  app.get("/api/courseware/:id", (req, res) => {
@@ -20265,30 +20534,16 @@ function registerCoursewareRoutes(ctx) {
20265
20534
  const html = injectLmsSdk(node.content || "", req, { id: node.id, name: node.name, uuid: node.id });
20266
20535
  res.send(html);
20267
20536
  } catch (e) {
20268
- res.status(500).send(e.message);
20537
+ sendSafeError(res, e);
20269
20538
  }
20270
20539
  });
20271
20540
  }
20272
20541
 
20273
20542
  // server/routes/bridge.ts
20274
- var import_path10 = __toESM(require("path"), 1);
20275
- var import_fs10 = __toESM(require("fs"), 1);
20543
+ var import_path11 = __toESM(require("path"), 1);
20544
+ var import_fs11 = __toESM(require("fs"), 1);
20276
20545
  function registerBridgeRoutes(ctx) {
20277
- const {
20278
- app,
20279
- io,
20280
- loginLimiter,
20281
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20282
- lessonActiveSegments: lessonActiveSegments2,
20283
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20284
- buildAgentFinalMessage: buildAgentFinalMessage2,
20285
- normalizeToolSchema: normalizeToolSchema2,
20286
- buildOpenAITools: buildOpenAITools2,
20287
- executeAgentToolCall: executeAgentToolCall2,
20288
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20289
- runGeminiAgentChat: runGeminiAgentChat2,
20290
- runOpenAIAgentChat: runOpenAIAgentChat2
20291
- } = ctx;
20546
+ const { app } = ctx;
20292
20547
  app.get("/bridge.js", (_req, res) => {
20293
20548
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
20294
20549
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -20309,23 +20564,23 @@ function registerBridgeRoutes(ctx) {
20309
20564
  if (!subpath || subpath === "") {
20310
20565
  subpath = courseware.entry;
20311
20566
  }
20312
- const storageDir = import_path10.default.resolve(process.cwd(), "storage", "courseware", uuid);
20313
- const filePath = import_path10.default.resolve(storageDir, subpath);
20567
+ const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", uuid);
20568
+ const filePath = import_path11.default.resolve(storageDir, subpath);
20314
20569
  if (!filePath.startsWith(storageDir)) {
20315
20570
  return res.status(403).send("Access denied");
20316
20571
  }
20317
- if (!import_fs10.default.existsSync(filePath)) {
20572
+ if (!import_fs11.default.existsSync(filePath)) {
20318
20573
  return res.status(404).send(`File not found: ${subpath}`);
20319
20574
  }
20320
- const stat = import_fs10.default.statSync(filePath);
20575
+ const stat = import_fs11.default.statSync(filePath);
20321
20576
  if (stat.isDirectory()) {
20322
- const indexHtml = import_path10.default.join(filePath, "index.html");
20323
- if (import_fs10.default.existsSync(indexHtml)) {
20577
+ const indexHtml = import_path11.default.join(filePath, "index.html");
20578
+ if (import_fs11.default.existsSync(indexHtml)) {
20324
20579
  return res.redirect(`/runtime/${uuid}/${subpath.endsWith("/") ? subpath : subpath + "/"}index.html`);
20325
20580
  }
20326
20581
  return res.status(404).send("Directory index not found");
20327
20582
  }
20328
- const ext = import_path10.default.extname(filePath).toLowerCase();
20583
+ const ext = import_path11.default.extname(filePath).toLowerCase();
20329
20584
  let contentType = "text/plain; charset=utf-8";
20330
20585
  if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
20331
20586
  else if (ext === ".css") contentType = "text/css; charset=utf-8";
@@ -20341,14 +20596,14 @@ function registerBridgeRoutes(ctx) {
20341
20596
  res.setHeader("Access-Control-Allow-Origin", "*");
20342
20597
  const isHtml = ext === ".html" || ext === ".htm";
20343
20598
  if (isHtml) {
20344
- let html = import_fs10.default.readFileSync(filePath, "utf8");
20599
+ let html = import_fs11.default.readFileSync(filePath, "utf8");
20345
20600
  html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
20346
20601
  return res.send(html);
20347
20602
  } else {
20348
20603
  return res.sendFile(filePath);
20349
20604
  }
20350
20605
  } catch (e) {
20351
- res.status(500).send(e.message);
20606
+ sendSafeError(res, e, 500, "Failed to load courseware runtime file");
20352
20607
  }
20353
20608
  });
20354
20609
  }
@@ -20356,27 +20611,89 @@ function registerBridgeRoutes(ctx) {
20356
20611
  // server/routes/lessons.ts
20357
20612
  var import_genai3 = require("@google/genai");
20358
20613
  var import_uuid16 = require("uuid");
20614
+ function checkLessonOwnership(req, lessonId) {
20615
+ const token = getCookieToken(req);
20616
+ const session = req.session || (token ? getValidSession(token) : null);
20617
+ if (!session) {
20618
+ return { allowed: false, status: 401, error: "Authentication required" };
20619
+ }
20620
+ const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
20621
+ const isTeacherOrAdmin = isAdmin || session.role === "teacher";
20622
+ if (!isTeacherOrAdmin) {
20623
+ return { allowed: false, status: 403, error: "Forbidden: Only teachers or administrators can modify lessons" };
20624
+ }
20625
+ const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
20626
+ if (!lesson) {
20627
+ return { allowed: false, status: 404, error: "Lesson not found" };
20628
+ }
20629
+ if (isAdmin) {
20630
+ return { allowed: true, status: 200, lesson };
20631
+ }
20632
+ if (!lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
20633
+ return { allowed: true, status: 200, lesson };
20634
+ }
20635
+ return {
20636
+ allowed: false,
20637
+ status: 403,
20638
+ error: "Forbidden: You do not have permission to modify this lesson because it was created by another teacher"
20639
+ };
20640
+ }
20641
+ function requireWhiteboardWriteAccess() {
20642
+ return (req, res, next) => {
20643
+ const { id } = req.params;
20644
+ const token = getCookieToken(req);
20645
+ const session = req.session || (token ? getValidSession(token) : null);
20646
+ if (!session) {
20647
+ return res.status(401).json({ success: false, error: "Authentication required" });
20648
+ }
20649
+ req.session = session;
20650
+ const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
20651
+ if (id.startsWith("assignment-")) {
20652
+ if (isAdmin || session.role === "teacher") {
20653
+ return next();
20654
+ }
20655
+ if (session.role === "student") {
20656
+ const studentMatch = id.match(/-student-([^-]+)$/);
20657
+ const targetStudentId = studentMatch ? studentMatch[1] : null;
20658
+ const currentStudentId = session.studentId || session.userId;
20659
+ if (targetStudentId && currentStudentId && targetStudentId === currentStudentId) {
20660
+ return next();
20661
+ }
20662
+ return res.status(403).json({
20663
+ success: false,
20664
+ error: "Forbidden: You do not have permission to modify another student's assignment whiteboard"
20665
+ });
20666
+ }
20667
+ return res.status(403).json({ success: false, error: "Forbidden: Invalid role for whiteboard write" });
20668
+ }
20669
+ const isTeacherOrAdmin = isAdmin || session.role === "teacher";
20670
+ if (!isTeacherOrAdmin) {
20671
+ return res.status(403).json({
20672
+ success: false,
20673
+ error: "Forbidden: Students cannot modify classroom whiteboards"
20674
+ });
20675
+ }
20676
+ const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
20677
+ if (!lesson) {
20678
+ return res.status(404).json({ success: false, error: "Lesson not found" });
20679
+ }
20680
+ if (isAdmin || !lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
20681
+ return next();
20682
+ }
20683
+ return res.status(403).json({
20684
+ success: false,
20685
+ error: "Forbidden: You do not have permission to modify this lesson whiteboard because it was created by another teacher"
20686
+ });
20687
+ };
20688
+ }
20359
20689
  function registerLessonsRoutes(ctx) {
20360
- const {
20361
- app,
20362
- io,
20363
- loginLimiter,
20364
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20365
- lessonActiveSegments: lessonActiveSegments2,
20366
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20367
- buildAgentFinalMessage: buildAgentFinalMessage2,
20368
- normalizeToolSchema: normalizeToolSchema2,
20369
- buildOpenAITools: buildOpenAITools2,
20370
- executeAgentToolCall: executeAgentToolCall2,
20371
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20372
- runGeminiAgentChat: runGeminiAgentChat2,
20373
- runOpenAIAgentChat: runOpenAIAgentChat2
20374
- } = ctx;
20690
+ const { app, io } = ctx;
20375
20691
  app.get("/api/lessons", (req, res) => {
20376
20692
  const lessons = kernelContainer.db.prepare(`
20377
- SELECT l.*,
20693
+ SELECT l.*, u.name as creator_name,
20378
20694
  (SELECT COUNT(*) FROM student_lesson_progress WHERE lesson_id = l.id) as enrollment_count
20379
20695
  FROM lessons l
20696
+ LEFT JOIN users u ON l.creator_id = u.id
20380
20697
  ORDER BY l.created_at DESC
20381
20698
  `).all();
20382
20699
  res.json(lessons);
@@ -20392,7 +20709,7 @@ function registerLessonsRoutes(ctx) {
20392
20709
  `).all(lessonId);
20393
20710
  res.json(rows);
20394
20711
  } catch (err) {
20395
- res.status(500).json({ error: err.message });
20712
+ sendSafeError(res, err);
20396
20713
  }
20397
20714
  });
20398
20715
  app.get("/api/lessons/:lessonId/eval-grades", (req, res) => {
@@ -20436,7 +20753,7 @@ function registerLessonsRoutes(ctx) {
20436
20753
  }
20437
20754
  res.json(result);
20438
20755
  } catch (err) {
20439
- res.status(500).json({ error: err.message });
20756
+ sendSafeError(res, err);
20440
20757
  }
20441
20758
  });
20442
20759
  app.get("/api/eval-submissions/:submissionId/reviews", (req, res) => {
@@ -20450,7 +20767,7 @@ function registerLessonsRoutes(ctx) {
20450
20767
  `).all(submissionId);
20451
20768
  res.json(rows);
20452
20769
  } catch (err) {
20453
- res.status(500).json({ error: err.message });
20770
+ sendSafeError(res, err);
20454
20771
  }
20455
20772
  });
20456
20773
  app.get("/api/lessons/:lessonId/students/:studentId/eval-status", (req, res) => {
@@ -20487,43 +20804,55 @@ function registerLessonsRoutes(ctx) {
20487
20804
  grade
20488
20805
  });
20489
20806
  } catch (err) {
20490
- res.status(500).json({ error: err.message });
20807
+ sendSafeError(res, err);
20491
20808
  }
20492
20809
  });
20493
- app.post("/api/lessons", async (req, res) => {
20810
+ app.post("/api/lessons", requireAuth("teacher", "administrator"), async (req, res) => {
20494
20811
  try {
20495
20812
  const { title, content } = req.body;
20813
+ const session = req.session;
20814
+ const creatorId = session?.userId || session?.username || "usr_teacher";
20815
+ const actorId = getActorId(req) || "teacher";
20496
20816
  const cmd = kernelContainer.commandBus.createCommand(
20497
20817
  "lesson.create",
20498
- { title, content },
20499
- "user-frontend",
20818
+ { title, content, creatorId },
20819
+ actorId,
20500
20820
  { approved: true }
20501
20821
  );
20502
20822
  const result = await kernelContainer.commandBus.execute(cmd);
20503
20823
  res.json({ success: true, result });
20504
20824
  } catch (e) {
20505
- res.status(500).json({ error: e.message });
20825
+ sendSafeError(res, e);
20506
20826
  }
20507
20827
  });
20508
- app.put("/api/lessons/:id/timeline", async (req, res) => {
20828
+ app.put("/api/lessons/:id/timeline", requireAuth("teacher", "administrator"), async (req, res) => {
20509
20829
  try {
20510
20830
  const { id } = req.params;
20831
+ const ownership = checkLessonOwnership(req, id);
20832
+ if (!ownership.allowed) {
20833
+ return res.status(ownership.status).json({ success: false, error: ownership.error });
20834
+ }
20511
20835
  const { timeline } = req.body;
20836
+ const actorId = getActorId(req) || "teacher";
20512
20837
  const cmd = kernelContainer.commandBus.createCommand(
20513
20838
  "lesson.update_timeline",
20514
20839
  { lessonId: id, timeline },
20515
- "user-frontend",
20840
+ actorId,
20516
20841
  { approved: true }
20517
20842
  );
20518
20843
  const result = await kernelContainer.commandBus.execute(cmd);
20519
20844
  res.json({ success: true, result });
20520
20845
  } catch (e) {
20521
- res.status(500).json({ error: e.message });
20846
+ sendSafeError(res, e);
20522
20847
  }
20523
20848
  });
20524
- app.put("/api/lessons/:id/progress-mode", async (req, res) => {
20849
+ app.put("/api/lessons/:id/progress-mode", requireAuth("teacher", "administrator"), async (req, res) => {
20525
20850
  try {
20526
20851
  const { id } = req.params;
20852
+ const ownership = checkLessonOwnership(req, id);
20853
+ if (!ownership.allowed) {
20854
+ return res.status(ownership.status).json({ success: false, error: ownership.error });
20855
+ }
20527
20856
  const { progressMode, progressConditions } = req.body;
20528
20857
  const conditionsStr = typeof progressConditions === "string" ? progressConditions : JSON.stringify(progressConditions || null);
20529
20858
  kernelContainer.db.prepare("UPDATE lessons SET progress_mode = ?, progress_conditions = ?, updated_at = ? WHERE id = ?").run(progressMode || "manual", conditionsStr, Date.now(), id);
@@ -20534,7 +20863,7 @@ function registerLessonsRoutes(ctx) {
20534
20863
  });
20535
20864
  res.json({ success: true });
20536
20865
  } catch (e) {
20537
- res.status(500).json({ error: e.message });
20866
+ sendSafeError(res, e);
20538
20867
  }
20539
20868
  });
20540
20869
  app.get("/api/lessons/:id/whiteboard", (req, res) => {
@@ -20560,14 +20889,13 @@ function registerLessonsRoutes(ctx) {
20560
20889
  }
20561
20890
  res.json(elements);
20562
20891
  });
20563
- app.post("/api/lessons/:id/whiteboard/reset", async (req, res) => {
20892
+ app.post("/api/lessons/:id/whiteboard/reset", requireWhiteboardWriteAccess(), async (req, res) => {
20564
20893
  try {
20565
20894
  const id = req.params.id;
20566
20895
  if (id.startsWith("assignment-")) {
20567
20896
  const deleteStmt = kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?");
20568
20897
  deleteStmt.run(id);
20569
- res.json({ success: true, message: "Assignment whiteboard reset to empty" });
20570
- return;
20898
+ return res.json({ success: true, message: "Assignment whiteboard reset to empty" });
20571
20899
  }
20572
20900
  const snapshotId = `snapshot-${id}`;
20573
20901
  const hasSnapshot = kernelContainer.db.prepare("SELECT count(*) as count FROM whiteboard_elements WHERE lesson_id = ?").get(snapshotId);
@@ -20590,94 +20918,77 @@ function registerLessonsRoutes(ctx) {
20590
20918
  res.json({ success: true, message: "Lesson whiteboard cleared (no snapshot)" });
20591
20919
  }
20592
20920
  } catch (e) {
20593
- res.status(500).json({ error: e.message });
20921
+ sendSafeError(res, e);
20594
20922
  }
20595
20923
  });
20596
- app.post("/api/lessons/:id/whiteboard", async (req, res) => {
20924
+ app.post("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
20597
20925
  try {
20598
20926
  const { id } = req.params;
20599
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20600
- return res.status(403).json({ error: "Forbidden: Only teachers can draw on classroom whiteboards" });
20601
- }
20602
20927
  const { type, data } = req.body;
20928
+ const actorId = getActorId(req) || "user-frontend";
20603
20929
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.draw", {
20604
20930
  lessonId: id,
20605
20931
  type,
20606
20932
  data: JSON.stringify(data)
20607
- }, "user-frontend", { approved: true });
20933
+ }, actorId, { approved: true });
20608
20934
  const result = await kernelContainer.commandBus.execute(cmd);
20609
20935
  res.json(result);
20610
20936
  } catch (e) {
20611
- res.status(500).json({ error: e.message });
20937
+ sendSafeError(res, e);
20612
20938
  }
20613
20939
  });
20614
- app.put("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
20940
+ app.put("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
20615
20941
  try {
20616
- const { id } = req.params;
20617
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20618
- return res.status(403).json({ error: "Forbidden: Only teachers can update elements on classroom whiteboards" });
20619
- }
20942
+ const { id, elementId } = req.params;
20620
20943
  const { data } = req.body;
20944
+ const actorId = getActorId(req) || "user-frontend";
20621
20945
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.update", {
20622
20946
  lessonId: id,
20623
- elementId: req.params.elementId,
20947
+ elementId,
20624
20948
  data: JSON.stringify(data)
20625
- }, "user-frontend", { approved: true });
20949
+ }, actorId, { approved: true });
20626
20950
  const result = await kernelContainer.commandBus.execute(cmd);
20627
20951
  res.json(result);
20628
20952
  } catch (e) {
20629
- res.status(500).json({ error: e.message });
20953
+ sendSafeError(res, e);
20630
20954
  }
20631
20955
  });
20632
- app.delete("/api/lessons/:id/whiteboard", async (req, res) => {
20956
+ app.delete("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
20633
20957
  try {
20634
20958
  const { id } = req.params;
20635
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20636
- return res.status(403).json({ error: "Forbidden: Only teachers can clear the classroom whiteboard" });
20637
- }
20959
+ const actorId = getActorId(req) || "user-frontend";
20638
20960
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.clear", {
20639
20961
  lessonId: id
20640
- }, "user-frontend", { approved: true });
20962
+ }, actorId, { approved: true });
20641
20963
  const result = await kernelContainer.commandBus.execute(cmd);
20642
20964
  res.json(result);
20643
20965
  } catch (e) {
20644
- res.status(500).json({ error: e.message });
20966
+ sendSafeError(res, e);
20645
20967
  }
20646
20968
  });
20647
- app.delete("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
20969
+ app.delete("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
20648
20970
  try {
20649
- const { id } = req.params;
20650
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20651
- return res.status(403).json({ error: "Forbidden: Only teachers can delete elements from classroom whiteboards" });
20652
- }
20971
+ const { id, elementId } = req.params;
20972
+ const actorId = getActorId(req) || "user-frontend";
20653
20973
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.delete", {
20654
20974
  lessonId: id,
20655
- elementId: req.params.elementId
20656
- }, "user-frontend", { approved: true });
20975
+ elementId
20976
+ }, actorId, { approved: true });
20657
20977
  const result = await kernelContainer.commandBus.execute(cmd);
20658
20978
  res.json(result);
20659
20979
  } catch (e) {
20660
- res.status(500).json({ error: e.message });
20980
+ sendSafeError(res, e);
20661
20981
  }
20662
20982
  });
20663
- app.post("/api/lessons/:id/quiz-submit", async (req, res) => {
20983
+ app.post("/api/lessons/:id/quiz-submit", requireAuth("student", "teacher", "administrator"), async (req, res) => {
20664
20984
  try {
20665
20985
  const { id: lessonId } = req.params;
20666
20986
  const { elementId, answer } = req.body;
20667
20987
  if (!elementId || answer === void 0) {
20668
20988
  return res.status(400).json({ error: "Missing elementId or answer" });
20669
20989
  }
20670
- const token = getCookieToken(req);
20671
- let studentId = "guest";
20672
- if (token) {
20673
- const sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
20674
- if (sessionRow) {
20675
- const session = JSON.parse(sessionRow.session_data);
20676
- if (session.role === "student") {
20677
- studentId = session.studentId || session.userId || "guest";
20678
- }
20679
- }
20680
- }
20990
+ const session = req.session;
20991
+ const studentId = session.studentId || session.userId || "guest";
20681
20992
  const row = kernelContainer.db.prepare(
20682
20993
  "SELECT data FROM whiteboard_elements WHERE id = ? AND lesson_id = ?"
20683
20994
  ).get(elementId, lessonId);
@@ -20701,15 +21012,12 @@ function registerLessonsRoutes(ctx) {
20701
21012
  io.to(`lesson-${lessonId}`).emit("whiteboard-sync", { type: "element-updated", elementId });
20702
21013
  res.json({ success: true, isCorrect, score, studentId });
20703
21014
  } catch (e) {
20704
- res.status(500).json({ error: e.message });
21015
+ sendSafeError(res, e);
20705
21016
  }
20706
21017
  });
20707
- app.get("/api/lessons/:id/quiz-submissions", (req, res) => {
21018
+ app.get("/api/lessons/:id/quiz-submissions", requireAuth("teacher", "administrator"), (req, res) => {
20708
21019
  try {
20709
21020
  const { id: lessonId } = req.params;
20710
- if (!checkIsTeacherOrAdmin(req)) {
20711
- return res.status(403).json({ error: "Forbidden" });
20712
- }
20713
21021
  const elements = kernelContainer.db.prepare(
20714
21022
  "SELECT id, type, data FROM whiteboard_elements WHERE lesson_id = ? AND type = ?"
20715
21023
  ).all(lessonId, "quiz");
@@ -20730,10 +21038,10 @@ function registerLessonsRoutes(ctx) {
20730
21038
  });
20731
21039
  res.json({ success: true, quizzes });
20732
21040
  } catch (e) {
20733
- res.status(500).json({ error: e.message });
21041
+ sendSafeError(res, e);
20734
21042
  }
20735
21043
  });
20736
- app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
21044
+ app.post("/api/lessons/:id/ai-tutor", requireAuth("student", "teacher", "administrator"), async (req, res) => {
20737
21045
  try {
20738
21046
  const { elements } = req.body;
20739
21047
  const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
@@ -20762,12 +21070,16 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20762
21070
  res.json({ success: true, hint });
20763
21071
  } catch (e) {
20764
21072
  console.error("AI Tutor error:", e);
20765
- res.status(500).json({ error: e.message });
21073
+ sendSafeError(res, e);
20766
21074
  }
20767
21075
  });
20768
- app.delete("/api/lessons/:id", async (req, res) => {
21076
+ app.delete("/api/lessons/:id", requireAuth("teacher", "administrator"), async (req, res) => {
20769
21077
  try {
20770
21078
  const { id } = req.params;
21079
+ const ownership = checkLessonOwnership(req, id);
21080
+ if (!ownership.allowed) {
21081
+ return res.status(ownership.status).json({ success: false, error: ownership.error });
21082
+ }
20771
21083
  kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?").run(id);
20772
21084
  kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE lesson_id = ?").run(id);
20773
21085
  kernelContainer.db.prepare("DELETE FROM schedules WHERE lesson_id = ?").run(id);
@@ -20778,7 +21090,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20778
21090
  }
20779
21091
  res.json({ success: true });
20780
21092
  } catch (e) {
20781
- res.status(500).json({ error: e.message });
21093
+ sendSafeError(res, e);
20782
21094
  }
20783
21095
  });
20784
21096
  app.get("/api/lessons/:id/stats", (req, res) => {
@@ -20798,22 +21110,24 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20798
21110
  ).get(id).count;
20799
21111
  res.json({ whiteboardCount, scheduleCount, enrollmentCount, assignmentCount });
20800
21112
  } catch (e) {
20801
- res.status(500).json({ error: e.message });
21113
+ sendSafeError(res, e);
20802
21114
  }
20803
21115
  });
20804
- app.post("/api/lessons/:id/clone", async (req, res) => {
21116
+ app.post("/api/lessons/:id/clone", requireAuth("teacher", "administrator"), async (req, res) => {
20805
21117
  try {
20806
21118
  const { id } = req.params;
20807
21119
  const original = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
20808
21120
  if (!original) {
20809
21121
  return res.status(404).json({ error: "Lesson not found" });
20810
21122
  }
21123
+ const session = req.session;
21124
+ const creatorId = session?.userId || session?.username || "usr_teacher";
20811
21125
  const newId = (0, import_uuid16.v7)();
20812
21126
  const now = Date.now();
20813
21127
  const newTitle = `\u526F\u672C-${original.title}`;
20814
21128
  kernelContainer.db.prepare(
20815
- "INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
20816
- ).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, now, now);
21129
+ "INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
21130
+ ).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, creatorId, now, now);
20817
21131
  const whiteboardElements = kernelContainer.db.prepare(
20818
21132
  "SELECT * FROM whiteboard_elements WHERE lesson_id = ?"
20819
21133
  ).all(id);
@@ -20823,40 +21137,29 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20823
21137
  for (const el of whiteboardElements) {
20824
21138
  insertElement.run((0, import_uuid16.v7)(), newId, el.type, el.data, now);
20825
21139
  }
20826
- const cloned = kernelContainer.db.prepare(
20827
- "SELECT l.*, 0 as enrollment_count FROM lessons l WHERE l.id = ?"
20828
- ).get(newId);
21140
+ const cloned = kernelContainer.db.prepare(`
21141
+ SELECT l.*, u.name as creator_name, 0 as enrollment_count
21142
+ FROM lessons l
21143
+ LEFT JOIN users u ON l.creator_id = u.id
21144
+ WHERE l.id = ?
21145
+ `).get(newId);
20829
21146
  res.json({ success: true, lesson: cloned });
20830
21147
  } catch (e) {
20831
- res.status(500).json({ error: e.message });
21148
+ sendSafeError(res, e);
20832
21149
  }
20833
21150
  });
20834
21151
  }
20835
21152
 
20836
21153
  // server/routes/workspace.ts
20837
- var import_path11 = __toESM(require("path"), 1);
21154
+ var import_path12 = __toESM(require("path"), 1);
20838
21155
  function registerWorkspaceRoutes(ctx) {
20839
- const {
20840
- app,
20841
- io,
20842
- loginLimiter,
20843
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20844
- lessonActiveSegments: lessonActiveSegments2,
20845
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20846
- buildAgentFinalMessage: buildAgentFinalMessage2,
20847
- normalizeToolSchema: normalizeToolSchema2,
20848
- buildOpenAITools: buildOpenAITools2,
20849
- executeAgentToolCall: executeAgentToolCall2,
20850
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20851
- runGeminiAgentChat: runGeminiAgentChat2,
20852
- runOpenAIAgentChat: runOpenAIAgentChat2
20853
- } = ctx;
20854
- app.get("/api/events", (req, res) => {
21156
+ const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
21157
+ app.get("/api/events", requireAuth("administrator"), (req, res) => {
20855
21158
  try {
20856
21159
  const events = kernelContainer.db.prepare("SELECT * FROM events ORDER BY timestamp DESC LIMIT 50").all();
20857
21160
  res.json(events);
20858
21161
  } catch (e) {
20859
- res.status(500).json({ error: e.message });
21162
+ sendSafeError(res, e);
20860
21163
  }
20861
21164
  });
20862
21165
  app.get("/api/mfe/remotes", (req, res) => {
@@ -20888,12 +21191,15 @@ function registerWorkspaceRoutes(ctx) {
20888
21191
  MF_REMOTE_CACHE2.set(name, result);
20889
21192
  res.json({ success: true, result });
20890
21193
  } catch (e) {
20891
- res.status(500).json({ success: false, error: e.message });
21194
+ sendSafeError(res, e);
20892
21195
  }
20893
21196
  });
20894
- app.get("/api/vfs", (req, res) => {
21197
+ app.get("/api/vfs", requireAuth(), (req, res) => {
20895
21198
  try {
20896
21199
  const parentId = req.query.parentId === "null" ? null : req.query.parentId || null;
21200
+ const isStaff = checkIsTeacherOrAdmin(req);
21201
+ const token = getCookieToken(req);
21202
+ const session = req.session || (token ? getValidSession(token) : null);
20897
21203
  let nodes = [];
20898
21204
  if (parentId === "virtual-lessons") {
20899
21205
  const lessons = kernelContainer.db.prepare("SELECT id, title, content FROM lessons").all();
@@ -20902,12 +21208,23 @@ function registerWorkspaceRoutes(ctx) {
20902
21208
  const assignments = kernelContainer.db.prepare("SELECT a.id, a.title, c.name as cname, a.content FROM assignments a JOIN classes c ON a.class_id = c.id").all();
20903
21209
  nodes = assignments.map((a) => ({ id: `assgn-${a.id}`, parent_id: "virtual-assignments", type: "file", name: `[${a.cname}] ${a.title}.md`, content: a.content }));
20904
21210
  } else if (parentId === "virtual-submissions") {
20905
- const submissions = kernelContainer.db.prepare(`
20906
- SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
20907
- FROM assignment_submissions sub
20908
- JOIN assignments a ON sub.assignment_id = a.id
20909
- JOIN students s ON sub.student_id = s.id
20910
- `).all();
21211
+ let submissions = [];
21212
+ if (isStaff) {
21213
+ submissions = kernelContainer.db.prepare(`
21214
+ SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
21215
+ FROM assignment_submissions sub
21216
+ JOIN assignments a ON sub.assignment_id = a.id
21217
+ JOIN students s ON sub.student_id = s.id
21218
+ `).all();
21219
+ } else if (session?.userId) {
21220
+ submissions = kernelContainer.db.prepare(`
21221
+ SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
21222
+ FROM assignment_submissions sub
21223
+ JOIN assignments a ON sub.assignment_id = a.id
21224
+ JOIN students s ON sub.student_id = s.id
21225
+ WHERE sub.student_id = ?
21226
+ `).all(session.userId);
21227
+ }
20911
21228
  nodes = submissions.map((sub) => ({
20912
21229
  id: `sub-${sub.id}`,
20913
21230
  parent_id: "virtual-submissions",
@@ -20934,7 +21251,7 @@ ${sub.content}`
20934
21251
  }
20935
21252
  res.json(nodes);
20936
21253
  } catch (e) {
20937
- res.status(500).json({ error: e.message });
21254
+ sendSafeError(res, e);
20938
21255
  }
20939
21256
  });
20940
21257
  app.get("/files/*", (req, res) => {
@@ -20968,7 +21285,7 @@ ${sub.content}`
20968
21285
  }
20969
21286
  const filename = parts[parts.length - 1];
20970
21287
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
20971
- const ext = import_path11.default.extname(filename).toLowerCase();
21288
+ const ext = import_path12.default.extname(filename).toLowerCase();
20972
21289
  const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
20973
21290
  const content = foundNode.content || "";
20974
21291
  if (binaryExtensions.includes(ext)) {
@@ -20980,29 +21297,15 @@ ${sub.content}`
20980
21297
  }
20981
21298
  res.send(content);
20982
21299
  } catch (e) {
20983
- res.status(500).send(e.message);
21300
+ sendSafeError(res, e);
20984
21301
  }
20985
21302
  });
20986
21303
  }
20987
21304
 
20988
21305
  // server/routes/processes.ts
20989
21306
  function registerProcessesRoutes(ctx) {
20990
- const {
20991
- app,
20992
- io,
20993
- loginLimiter,
20994
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20995
- lessonActiveSegments: lessonActiveSegments2,
20996
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20997
- buildAgentFinalMessage: buildAgentFinalMessage2,
20998
- normalizeToolSchema: normalizeToolSchema2,
20999
- buildOpenAITools: buildOpenAITools2,
21000
- executeAgentToolCall: executeAgentToolCall2,
21001
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
21002
- runGeminiAgentChat: runGeminiAgentChat2,
21003
- runOpenAIAgentChat: runOpenAIAgentChat2
21004
- } = ctx;
21005
- app.get("/api/approvals", (req, res) => {
21307
+ const { app } = ctx;
21308
+ app.get("/api/approvals", requireAuth("administrator"), (req, res) => {
21006
21309
  try {
21007
21310
  const list = kernelContainer.db.prepare("SELECT * FROM pending_commands ORDER BY created_at DESC").all();
21008
21311
  res.json(list);
@@ -21010,71 +21313,54 @@ function registerProcessesRoutes(ctx) {
21010
21313
  res.status(500).json({ error: e.message });
21011
21314
  }
21012
21315
  });
21013
- app.post("/api/approvals/:id/approve", async (req, res) => {
21316
+ app.post("/api/approvals/:id/approve", requireAuth("administrator"), async (req, res) => {
21014
21317
  try {
21015
21318
  const pending = kernelContainer.db.prepare("SELECT * FROM pending_commands WHERE id = ?").get(req.params.id);
21016
21319
  if (!pending) return res.status(404).json({ error: "Not found" });
21017
- let payload = JSON.parse(pending.payload);
21018
- if (req.body && req.body.payloadOverride) {
21019
- payload = { ...payload, ...req.body.payloadOverride };
21020
- }
21320
+ const payload = JSON.parse(pending.payload);
21321
+ const approverId = getActorId(req) || "admin";
21021
21322
  const cmd = kernelContainer.commandBus.createCommand(
21022
21323
  pending.command_type,
21023
21324
  payload,
21024
- pending.actor_id,
21325
+ approverId,
21025
21326
  { approved: true }
21026
- // Bypass high risk check now
21027
21327
  );
21028
21328
  const result = await kernelContainer.commandBus.execute(cmd);
21029
21329
  kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(pending.id);
21030
21330
  res.json({ success: true, result });
21031
21331
  } catch (e) {
21032
- res.status(500).json({ error: e.message });
21332
+ sendSafeError(res, e);
21033
21333
  }
21034
21334
  });
21035
- app.post("/api/approvals/:id/reject", async (req, res) => {
21335
+ app.post("/api/approvals/:id/reject", requireAuth("administrator"), async (req, res) => {
21036
21336
  try {
21037
21337
  kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(req.params.id);
21038
21338
  res.json({ success: true });
21039
21339
  } catch (e) {
21040
- res.status(500).json({ error: e.message });
21340
+ sendSafeError(res, e);
21041
21341
  }
21042
21342
  });
21043
- app.get("/api/processes", (req, res) => {
21343
+ app.get("/api/processes", requireAuth("administrator"), (req, res) => {
21044
21344
  try {
21045
21345
  const list = kernelContainer.db.prepare("SELECT id, name, status, created_at, updated_at FROM processes WHERE status = 'running' ORDER BY created_at DESC").all();
21046
21346
  res.json(list);
21047
21347
  } catch (e) {
21048
- res.status(500).json({ error: e.message });
21348
+ sendSafeError(res, e);
21049
21349
  }
21050
21350
  });
21051
- app.get("/api/processes/:id/logs", (req, res) => {
21351
+ app.get("/api/processes/:id/logs", requireAuth("administrator"), (req, res) => {
21052
21352
  try {
21053
21353
  const dbRow = kernelContainer.db.prepare("SELECT logs FROM processes WHERE id = ?").get(req.params.id);
21054
21354
  res.json(dbRow || { logs: "" });
21055
21355
  } catch (e) {
21056
- res.status(500).json({ error: e.message });
21356
+ sendSafeError(res, e);
21057
21357
  }
21058
21358
  });
21059
21359
  }
21060
21360
 
21061
21361
  // server/routes/admin.ts
21062
21362
  function registerAdminRoutes(ctx) {
21063
- const {
21064
- app,
21065
- io,
21066
- loginLimiter,
21067
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
21068
- lessonActiveSegments: lessonActiveSegments2,
21069
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
21070
- buildAgentFinalMessage: buildAgentFinalMessage2,
21071
- normalizeToolSchema: normalizeToolSchema2,
21072
- buildOpenAITools: buildOpenAITools2,
21073
- executeAgentToolCall: executeAgentToolCall2,
21074
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
21075
- runGeminiAgentChat: runGeminiAgentChat2,
21076
- runOpenAIAgentChat: runOpenAIAgentChat2
21077
- } = ctx;
21363
+ const { app } = ctx;
21078
21364
  app.post("/api/admin/seed-demo", requireAuth("administrator"), (req, res) => {
21079
21365
  try {
21080
21366
  const db2 = kernelContainer.db;
@@ -21138,7 +21424,7 @@ function registerAdminRoutes(ctx) {
21138
21424
  }
21139
21425
  res.json({ success: true, classId: DEMO_CLASS_ID, scheduleId: DEMO_SCHEDULE_ID, lessonId });
21140
21426
  } catch (e) {
21141
- res.status(500).json({ error: e.message });
21427
+ sendSafeError(res, e);
21142
21428
  }
21143
21429
  });
21144
21430
  app.post("/api/classes/import", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21188,7 +21474,7 @@ function registerAdminRoutes(ctx) {
21188
21474
  }
21189
21475
  res.json({ success: true, imported });
21190
21476
  } catch (e) {
21191
- res.status(500).json({ error: e.message });
21477
+ sendSafeError(res, e);
21192
21478
  }
21193
21479
  });
21194
21480
  app.post("/api/students/import", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21224,32 +21510,18 @@ function registerAdminRoutes(ctx) {
21224
21510
  }
21225
21511
  res.json({ success: true, imported });
21226
21512
  } catch (e) {
21227
- res.status(500).json({ error: e.message });
21513
+ sendSafeError(res, e);
21228
21514
  }
21229
21515
  });
21230
21516
  }
21231
21517
 
21232
21518
  // server/routes/roster.ts
21233
- var import_path12 = __toESM(require("path"), 1);
21234
- var import_fs11 = __toESM(require("fs"), 1);
21519
+ var import_path13 = __toESM(require("path"), 1);
21520
+ var import_fs12 = __toESM(require("fs"), 1);
21235
21521
  var import_crypto10 = __toESM(require("crypto"), 1);
21236
21522
  var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
21237
21523
  function registerRosterRoutes(ctx) {
21238
- const {
21239
- app,
21240
- io,
21241
- loginLimiter,
21242
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
21243
- lessonActiveSegments: lessonActiveSegments2,
21244
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
21245
- buildAgentFinalMessage: buildAgentFinalMessage2,
21246
- normalizeToolSchema: normalizeToolSchema2,
21247
- buildOpenAITools: buildOpenAITools2,
21248
- executeAgentToolCall: executeAgentToolCall2,
21249
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
21250
- runGeminiAgentChat: runGeminiAgentChat2,
21251
- runOpenAIAgentChat: runOpenAIAgentChat2
21252
- } = ctx;
21524
+ const { app, io, loginLimiter } = ctx;
21253
21525
  app.get("/api/classes", (req, res) => {
21254
21526
  try {
21255
21527
  const classes = kernelContainer.db.prepare(`
@@ -21283,10 +21555,10 @@ function registerRosterRoutes(ctx) {
21283
21555
  `).all(req.params.id);
21284
21556
  res.json(students);
21285
21557
  } catch (e) {
21286
- res.status(500).json({ error: e.message });
21558
+ sendSafeError(res, e);
21287
21559
  }
21288
21560
  });
21289
- app.post("/api/classes", (req, res) => {
21561
+ app.post("/api/classes", requireAuth("teacher", "administrator"), (req, res) => {
21290
21562
  try {
21291
21563
  const { name, description } = req.body;
21292
21564
  const classId = Math.random().toString(36).slice(2);
@@ -21298,10 +21570,10 @@ function registerRosterRoutes(ctx) {
21298
21570
  );
21299
21571
  res.json({ success: true, id: classId });
21300
21572
  } catch (e) {
21301
- res.status(500).json({ error: e.message });
21573
+ sendSafeError(res, e);
21302
21574
  }
21303
21575
  });
21304
- app.put("/api/classes/:id", (req, res) => {
21576
+ app.put("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
21305
21577
  try {
21306
21578
  const { name, description, class_passcode } = req.body;
21307
21579
  if (name) kernelContainer.db.prepare("UPDATE classes SET name = ? WHERE id = ?").run(name, req.params.id);
@@ -21309,10 +21581,10 @@ function registerRosterRoutes(ctx) {
21309
21581
  if (class_passcode !== void 0) kernelContainer.db.prepare("UPDATE classes SET class_passcode = ? WHERE id = ?").run(class_passcode, req.params.id);
21310
21582
  res.json({ success: true });
21311
21583
  } catch (e) {
21312
- res.status(500).json({ error: e.message });
21584
+ sendSafeError(res, e);
21313
21585
  }
21314
21586
  });
21315
- app.delete("/api/classes/:id", (req, res) => {
21587
+ app.delete("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
21316
21588
  try {
21317
21589
  const classId = req.params.id;
21318
21590
  const db2 = kernelContainer.db;
@@ -21354,7 +21626,7 @@ function registerRosterRoutes(ctx) {
21354
21626
  deleteTransaction();
21355
21627
  res.json({ success: true });
21356
21628
  } catch (e) {
21357
- res.status(500).json({ error: e.message });
21629
+ sendSafeError(res, e);
21358
21630
  }
21359
21631
  });
21360
21632
  app.get("/api/db-status", (req, res) => {
@@ -21419,7 +21691,8 @@ function registerRosterRoutes(ctx) {
21419
21691
  }
21420
21692
  return res.status(500).json({ status: "disconnected", error: "Unexpected response from SQLite" });
21421
21693
  } catch (e) {
21422
- return res.status(500).json({ status: "disconnected", error: e.message });
21694
+ const exposed = process.env.NODE_ENV === "production" ? "Database connection error" : e.message;
21695
+ return res.status(500).json({ status: "disconnected", error: exposed });
21423
21696
  }
21424
21697
  });
21425
21698
  app.get("/api/auth/session", (req, res) => {
@@ -21434,7 +21707,7 @@ function registerRosterRoutes(ctx) {
21434
21707
  }
21435
21708
  res.json({ session });
21436
21709
  } catch (e) {
21437
- res.status(500).json({ error: e.message });
21710
+ sendSafeError(res, e);
21438
21711
  }
21439
21712
  });
21440
21713
  app.post("/api/auth/logout", (req, res) => {
@@ -21443,10 +21716,12 @@ function registerRosterRoutes(ctx) {
21443
21716
  if (token) {
21444
21717
  kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
21445
21718
  }
21446
- res.setHeader("Set-Cookie", `edu_os_token=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`);
21719
+ const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
21720
+ const secureFlag = isSecure ? "; Secure" : "";
21721
+ res.setHeader("Set-Cookie", `edu_os_token=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax${secureFlag}`);
21447
21722
  res.json({ success: true });
21448
21723
  } catch (e) {
21449
- res.status(500).json({ error: e.message });
21724
+ sendSafeError(res, e);
21450
21725
  }
21451
21726
  });
21452
21727
  app.post("/api/auth/change-password", (req, res) => {
@@ -21505,7 +21780,7 @@ function registerRosterRoutes(ctx) {
21505
21780
  }
21506
21781
  res.status(400).json({ error: "Unsupported role" });
21507
21782
  } catch (e) {
21508
- res.status(500).json({ error: e.message });
21783
+ sendSafeError(res, e);
21509
21784
  }
21510
21785
  });
21511
21786
  app.post("/api/auth/profile", (req, res) => {
@@ -21536,7 +21811,7 @@ function registerRosterRoutes(ctx) {
21536
21811
  }
21537
21812
  res.json({ success: true, name });
21538
21813
  } catch (e) {
21539
- res.status(500).json({ error: e.message });
21814
+ sendSafeError(res, e);
21540
21815
  }
21541
21816
  });
21542
21817
  app.post("/api/auth/avatar", (req, res) => {
@@ -21549,7 +21824,7 @@ function registerRosterRoutes(ctx) {
21549
21824
  if (!filename || !base64Data) {
21550
21825
  return res.status(400).json({ error: "Filename and base64Data are required" });
21551
21826
  }
21552
- const ext = import_path12.default.extname(filename).toLowerCase();
21827
+ const ext = import_path13.default.extname(filename).toLowerCase();
21553
21828
  const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
21554
21829
  if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
21555
21830
  return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
@@ -21562,11 +21837,11 @@ function registerRosterRoutes(ctx) {
21562
21837
  if (!validateMagicBytes(fileBuffer, filename)) {
21563
21838
  return res.status(400).json({ error: "File content does not match the declared image type" });
21564
21839
  }
21565
- const avatarDir = import_path12.default.join(process.cwd(), "uploads", "avatars");
21566
- import_fs11.default.mkdirSync(avatarDir, { recursive: true });
21840
+ const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
21841
+ import_fs12.default.mkdirSync(avatarDir, { recursive: true });
21567
21842
  const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
21568
- const filePath = import_path12.default.join(avatarDir, uniqueName);
21569
- import_fs11.default.writeFileSync(filePath, fileBuffer);
21843
+ const filePath = import_path13.default.join(avatarDir, uniqueName);
21844
+ import_fs12.default.writeFileSync(filePath, fileBuffer);
21570
21845
  const avatarUrl = `/uploads/avatars/${uniqueName}`;
21571
21846
  let oldAvatar = null;
21572
21847
  if (session.role === "student") {
@@ -21580,7 +21855,7 @@ function registerRosterRoutes(ctx) {
21580
21855
  }
21581
21856
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
21582
21857
  try {
21583
- import_fs11.default.unlinkSync(import_path12.default.join(process.cwd(), oldAvatar));
21858
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
21584
21859
  } catch {
21585
21860
  }
21586
21861
  }
@@ -21595,7 +21870,7 @@ function registerRosterRoutes(ctx) {
21595
21870
  }
21596
21871
  res.json({ success: true, avatar: avatarUrl });
21597
21872
  } catch (e) {
21598
- res.status(500).json({ error: e.message });
21873
+ sendSafeError(res, e);
21599
21874
  }
21600
21875
  });
21601
21876
  app.delete("/api/auth/avatar", (req, res) => {
@@ -21616,7 +21891,7 @@ function registerRosterRoutes(ctx) {
21616
21891
  }
21617
21892
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
21618
21893
  try {
21619
- import_fs11.default.unlinkSync(import_path12.default.join(process.cwd(), oldAvatar));
21894
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
21620
21895
  } catch {
21621
21896
  }
21622
21897
  }
@@ -21631,7 +21906,7 @@ function registerRosterRoutes(ctx) {
21631
21906
  }
21632
21907
  res.json({ success: true });
21633
21908
  } catch (e) {
21634
- res.status(500).json({ error: e.message });
21909
+ sendSafeError(res, e);
21635
21910
  }
21636
21911
  });
21637
21912
  app.post("/api/auth/login", loginLimiter, (req, res) => {
@@ -21726,7 +22001,10 @@ function registerRosterRoutes(ctx) {
21726
22001
  const now = Date.now();
21727
22002
  const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
21728
22003
  kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
21729
- res.setHeader("Set-Cookie", `edu_os_token=${sessionToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`);
22004
+ const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
22005
+ const secureFlag = isSecure ? "; Secure" : "";
22006
+ const maxAgeSeconds = 7 * 24 * 60 * 60;
22007
+ res.setHeader("Set-Cookie", `edu_os_token=${sessionToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}${secureFlag}`);
21730
22008
  return res.json({
21731
22009
  success: true,
21732
22010
  session: sessionData
@@ -21734,7 +22012,7 @@ function registerRosterRoutes(ctx) {
21734
22012
  }
21735
22013
  res.status(400).json({ error: "Unsupported entry type" });
21736
22014
  } catch (e) {
21737
- res.status(500).json({ error: e.message });
22015
+ sendSafeError(res, e);
21738
22016
  }
21739
22017
  });
21740
22018
  app.get("/api/students/:id/read_notifications", (req, res) => {
@@ -21742,10 +22020,10 @@ function registerRosterRoutes(ctx) {
21742
22020
  const rows = kernelContainer.db.prepare("SELECT notification_id FROM student_read_notifications WHERE student_id = ?").all(req.params.id);
21743
22021
  res.json(rows.map((r) => r.notification_id));
21744
22022
  } catch (e) {
21745
- res.status(500).json({ error: e.message });
22023
+ sendSafeError(res, e);
21746
22024
  }
21747
22025
  });
21748
- app.post("/api/students/:id/read_notifications", (req, res) => {
22026
+ app.post("/api/students/:id/read_notifications", requireAuth(), (req, res) => {
21749
22027
  try {
21750
22028
  const { notificationId } = req.body;
21751
22029
  if (!notificationId) {
@@ -21758,10 +22036,10 @@ function registerRosterRoutes(ctx) {
21758
22036
  });
21759
22037
  res.json({ success: true });
21760
22038
  } catch (e) {
21761
- res.status(500).json({ error: e.message });
22039
+ sendSafeError(res, e);
21762
22040
  }
21763
22041
  });
21764
- app.post("/api/classes/:classId/lock_lesson", (req, res) => {
22042
+ app.post("/api/classes/:classId/lock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
21765
22043
  try {
21766
22044
  const { lessonId } = req.body;
21767
22045
  if (!lessonId) {
@@ -21778,7 +22056,7 @@ function registerRosterRoutes(ctx) {
21778
22056
  res.status(550).json({ error: e.message });
21779
22057
  }
21780
22058
  });
21781
- app.post("/api/classes/:classId/unlock_lesson", (req, res) => {
22059
+ app.post("/api/classes/:classId/unlock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
21782
22060
  try {
21783
22061
  kernelContainer.db.prepare("UPDATE students SET locked_lesson_id = NULL WHERE id IN (SELECT student_id FROM class_students WHERE class_id = ?)").run(req.params.classId);
21784
22062
  io.emit("class-lock-status-changed", {
@@ -21848,7 +22126,7 @@ function registerRosterRoutes(ctx) {
21848
22126
  const labs = kernelContainer.db.prepare("SELECT * FROM computer_labs ORDER BY created_at DESC").all();
21849
22127
  res.json(labs);
21850
22128
  } catch (e) {
21851
- res.status(500).json({ error: e.message });
22129
+ sendSafeError(res, e);
21852
22130
  }
21853
22131
  });
21854
22132
  app.post("/api/labs", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21860,7 +22138,7 @@ function registerRosterRoutes(ctx) {
21860
22138
  ).run(id, room_number, parseInt(rows), parseInt(cols), Date.now());
21861
22139
  res.json({ success: true, id, room_number, rows, cols });
21862
22140
  } catch (e) {
21863
- res.status(500).json({ error: e.message });
22141
+ sendSafeError(res, e);
21864
22142
  }
21865
22143
  });
21866
22144
  app.put("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21871,7 +22149,7 @@ function registerRosterRoutes(ctx) {
21871
22149
  ).run(room_number, parseInt(rows), parseInt(cols), req.params.id);
21872
22150
  res.json({ success: true });
21873
22151
  } catch (e) {
21874
- res.status(500).json({ error: e.message });
22152
+ sendSafeError(res, e);
21875
22153
  }
21876
22154
  });
21877
22155
  app.delete("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21880,7 +22158,7 @@ function registerRosterRoutes(ctx) {
21880
22158
  kernelContainer.db.prepare("DELETE FROM student_seats WHERE lab_id = ?").run(req.params.id);
21881
22159
  res.json({ success: true });
21882
22160
  } catch (e) {
21883
- res.status(500).json({ error: e.message });
22161
+ sendSafeError(res, e);
21884
22162
  }
21885
22163
  });
21886
22164
  app.get("/api/classes/:classId/seats", (req, res) => {
@@ -21890,7 +22168,7 @@ function registerRosterRoutes(ctx) {
21890
22168
  const seats = kernelContainer.db.prepare("SELECT * FROM student_seats WHERE class_id = ?").all(req.params.classId);
21891
22169
  res.json({ lab_id: labId, seats });
21892
22170
  } catch (e) {
21893
- res.status(500).json({ error: e.message });
22171
+ sendSafeError(res, e);
21894
22172
  }
21895
22173
  });
21896
22174
  app.post("/api/classes/:classId/seats", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21908,10 +22186,10 @@ function registerRosterRoutes(ctx) {
21908
22186
  }
21909
22187
  res.json({ success: true });
21910
22188
  } catch (e) {
21911
- res.status(500).json({ error: e.message });
22189
+ sendSafeError(res, e);
21912
22190
  }
21913
22191
  });
21914
- app.post("/api/students", (req, res) => {
22192
+ app.post("/api/students", requireAuth("teacher", "administrator"), (req, res) => {
21915
22193
  try {
21916
22194
  const { name, email, password, student_number } = req.body;
21917
22195
  const studentId = Math.random().toString(36).slice(2);
@@ -21930,10 +22208,10 @@ function registerRosterRoutes(ctx) {
21930
22208
  );
21931
22209
  res.json({ success: true, id: studentId, student_number: finalNum, tempPassword: password || "123456" });
21932
22210
  } catch (e) {
21933
- res.status(500).json({ error: e.message });
22211
+ sendSafeError(res, e);
21934
22212
  }
21935
22213
  });
21936
- app.put("/api/students/:id", (req, res) => {
22214
+ app.put("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
21937
22215
  try {
21938
22216
  const { name, email, password, locked_lesson_id, private_notes, student_number } = req.body;
21939
22217
  if (name) kernelContainer.db.prepare("UPDATE students SET name = ? WHERE id = ?").run(name, req.params.id);
@@ -21947,10 +22225,10 @@ function registerRosterRoutes(ctx) {
21947
22225
  if (student_number !== void 0) kernelContainer.db.prepare("UPDATE students SET student_number = ? WHERE id = ?").run(student_number, req.params.id);
21948
22226
  res.json({ success: true });
21949
22227
  } catch (e) {
21950
- res.status(500).json({ error: e.message });
22228
+ sendSafeError(res, e);
21951
22229
  }
21952
22230
  });
21953
- app.delete("/api/students/:id", (req, res) => {
22231
+ app.delete("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
21954
22232
  try {
21955
22233
  kernelContainer.db.prepare("DELETE FROM class_students WHERE student_id = ?").run(req.params.id);
21956
22234
  kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE student_id = ?").run(req.params.id);
@@ -21966,7 +22244,7 @@ function registerRosterRoutes(ctx) {
21966
22244
  kernelContainer.db.prepare("DELETE FROM students WHERE id = ?").run(req.params.id);
21967
22245
  res.json({ success: true });
21968
22246
  } catch (e) {
21969
- res.status(500).json({ error: e.message });
22247
+ sendSafeError(res, e);
21970
22248
  }
21971
22249
  });
21972
22250
  app.get("/api/students/:id/export", (req, res) => {
@@ -22003,14 +22281,11 @@ function registerRosterRoutes(ctx) {
22003
22281
  exportedAt: (/* @__PURE__ */ new Date()).toISOString()
22004
22282
  });
22005
22283
  } catch (e) {
22006
- res.status(500).json({ error: e.message });
22284
+ sendSafeError(res, e);
22007
22285
  }
22008
22286
  });
22009
- app.delete("/api/students/:id/gdpr-delete", (req, res) => {
22287
+ app.delete("/api/students/:id/gdpr-delete", requireAuth("teacher", "administrator"), (req, res) => {
22010
22288
  try {
22011
- if (!checkIsTeacherOrAdmin(req)) {
22012
- return res.status(403).json({ error: "Only teachers and administrators can perform GDPR deletion" });
22013
- }
22014
22289
  const studentId = req.params.id;
22015
22290
  const { confirm } = req.body;
22016
22291
  if (confirm !== true) {
@@ -22031,7 +22306,7 @@ function registerRosterRoutes(ctx) {
22031
22306
  console.log(`[GDPR] Complete data deletion for student ${studentId}`);
22032
22307
  res.json({ success: true, message: "All student data has been permanently deleted." });
22033
22308
  } catch (e) {
22034
- res.status(500).json({ error: e.message });
22309
+ sendSafeError(res, e);
22035
22310
  }
22036
22311
  });
22037
22312
  app.get("/api/students/:id/progress", (req, res) => {
@@ -22044,11 +22319,16 @@ function registerRosterRoutes(ctx) {
22044
22319
  `).all(req.params.id);
22045
22320
  res.json(progress);
22046
22321
  } catch (e) {
22047
- res.status(500).json({ error: e.message });
22322
+ sendSafeError(res, e);
22048
22323
  }
22049
22324
  });
22050
- app.post("/api/students/:id/progress", (req, res) => {
22325
+ app.post("/api/students/:id/progress", requireAuth(), (req, res) => {
22051
22326
  try {
22327
+ const session = req.session;
22328
+ const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
22329
+ if (!isPrivileged && session?.userId !== req.params.id) {
22330
+ return res.status(403).json({ error: "Cannot update progress for another student" });
22331
+ }
22052
22332
  const { lessonId, completed, progressPercent, completedSegments } = req.body;
22053
22333
  const completedSegmentsStr = typeof completedSegments === "string" ? completedSegments : JSON.stringify(completedSegments || []);
22054
22334
  kernelContainer.db.prepare(`
@@ -22075,10 +22355,10 @@ function registerRosterRoutes(ctx) {
22075
22355
  });
22076
22356
  res.json({ success: true });
22077
22357
  } catch (e) {
22078
- res.status(500).json({ error: e.message });
22358
+ sendSafeError(res, e);
22079
22359
  }
22080
22360
  });
22081
- app.post("/api/classes/:id/students", (req, res) => {
22361
+ app.post("/api/classes/:id/students", requireAuth("teacher", "administrator"), (req, res) => {
22082
22362
  try {
22083
22363
  const { studentId } = req.body;
22084
22364
  kernelContainer.db.prepare("INSERT OR IGNORE INTO class_students (class_id, student_id, joined_at) VALUES (?, ?, ?)").run(
@@ -22088,7 +22368,7 @@ function registerRosterRoutes(ctx) {
22088
22368
  );
22089
22369
  res.json({ success: true });
22090
22370
  } catch (e) {
22091
- res.status(500).json({ error: e.message });
22371
+ sendSafeError(res, e);
22092
22372
  }
22093
22373
  });
22094
22374
  app.post("/api/classes/:id/students/bulk-enroll", requireAuth("teacher", "administrator"), (req, res) => {
@@ -22130,7 +22410,7 @@ function registerRosterRoutes(ctx) {
22130
22410
  }
22131
22411
  res.json({ success: true, count: results.length, results });
22132
22412
  } catch (e) {
22133
- res.status(500).json({ error: e.message });
22413
+ sendSafeError(res, e);
22134
22414
  }
22135
22415
  });
22136
22416
  app.get("/api/classes/:id/progress", (req, res) => {
@@ -22145,7 +22425,7 @@ function registerRosterRoutes(ctx) {
22145
22425
  `).all(req.params.id);
22146
22426
  res.json(progress);
22147
22427
  } catch (e) {
22148
- res.status(500).json({ error: e.message });
22428
+ sendSafeError(res, e);
22149
22429
  }
22150
22430
  });
22151
22431
  app.get("/api/classes/:classId/lessons/:lessonId/progress", (req, res) => {
@@ -22165,15 +22445,15 @@ function registerRosterRoutes(ctx) {
22165
22445
  `).all(req.params.lessonId, req.params.lessonId, req.params.classId);
22166
22446
  res.json(progress);
22167
22447
  } catch (e) {
22168
- res.status(500).json({ error: e.message });
22448
+ sendSafeError(res, e);
22169
22449
  }
22170
22450
  });
22171
- app.delete("/api/classes/:classId/students/:studentId", (req, res) => {
22451
+ app.delete("/api/classes/:classId/students/:studentId", requireAuth("teacher", "administrator"), (req, res) => {
22172
22452
  try {
22173
22453
  kernelContainer.db.prepare("DELETE FROM class_students WHERE class_id = ? AND student_id = ?").run(req.params.classId, req.params.studentId);
22174
22454
  res.json({ success: true });
22175
22455
  } catch (e) {
22176
- res.status(500).json({ error: e.message });
22456
+ sendSafeError(res, e);
22177
22457
  }
22178
22458
  });
22179
22459
  app.get("/api/classes/:classId/dashboard", (req, res) => {
@@ -22216,11 +22496,16 @@ function registerRosterRoutes(ctx) {
22216
22496
  `).all(req.params.classId, req.params.classId);
22217
22497
  res.json({ assignments, recentSubmissions, performance: performance2, rollcallStats });
22218
22498
  } catch (e) {
22219
- res.status(500).json({ error: e.message });
22499
+ sendSafeError(res, e);
22220
22500
  }
22221
22501
  });
22222
- app.get("/api/students/:id/dashboard", (req, res) => {
22502
+ app.get("/api/students/:id/dashboard", requireAuth(), (req, res) => {
22223
22503
  try {
22504
+ const session = req.session;
22505
+ const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
22506
+ if (!isPrivileged && session?.userId !== req.params.id) {
22507
+ return res.status(403).json({ error: "Cannot access another student dashboard" });
22508
+ }
22224
22509
  const studentId = req.params.id;
22225
22510
  const studentClasses = kernelContainer.db.prepare(`
22226
22511
  SELECT c.*
@@ -22295,7 +22580,7 @@ function registerRosterRoutes(ctx) {
22295
22580
  `).get(studentId);
22296
22581
  res.json({ classes: studentClasses, schedules, assignments, progress, rollcalls, profile });
22297
22582
  } catch (e) {
22298
- res.status(500).json({ error: e.message });
22583
+ sendSafeError(res, e);
22299
22584
  }
22300
22585
  });
22301
22586
  }
@@ -22303,30 +22588,16 @@ function registerRosterRoutes(ctx) {
22303
22588
  // server/routes/assignments.ts
22304
22589
  var import_genai4 = require("@google/genai");
22305
22590
  function registerAssignmentsRoutes(ctx) {
22306
- const {
22307
- app,
22308
- io,
22309
- loginLimiter,
22310
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22311
- lessonActiveSegments: lessonActiveSegments2,
22312
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22313
- buildAgentFinalMessage: buildAgentFinalMessage2,
22314
- normalizeToolSchema: normalizeToolSchema2,
22315
- buildOpenAITools: buildOpenAITools2,
22316
- executeAgentToolCall: executeAgentToolCall2,
22317
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22318
- runGeminiAgentChat: runGeminiAgentChat2,
22319
- runOpenAIAgentChat: runOpenAIAgentChat2
22320
- } = ctx;
22591
+ const { app } = ctx;
22321
22592
  app.get("/api/classes/:classId/assignments", (req, res) => {
22322
22593
  try {
22323
22594
  const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
22324
22595
  res.json(assignments);
22325
22596
  } catch (e) {
22326
- res.status(500).json({ error: e.message });
22597
+ sendSafeError(res, e);
22327
22598
  }
22328
22599
  });
22329
- app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
22600
+ app.post("/api/classes/:classId/assignments/generate", requireAuth("teacher", "administrator"), async (req, res) => {
22330
22601
  try {
22331
22602
  const { topic, lessonId } = req.body;
22332
22603
  const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
@@ -22351,10 +22622,10 @@ function registerAssignmentsRoutes(ctx) {
22351
22622
  );
22352
22623
  res.json({ success: true, assignment: { id, class_id: req.params.classId, lesson_id: lessonId || null, title: gen.title, description: gen.description, content: gen.content } });
22353
22624
  } catch (e) {
22354
- res.status(500).json({ error: e.message });
22625
+ sendSafeError(res, e);
22355
22626
  }
22356
22627
  });
22357
- app.post("/api/classes/:classId/assignments/suggest", async (req, res) => {
22628
+ app.post("/api/classes/:classId/assignments/suggest", requireAuth("teacher", "administrator"), async (req, res) => {
22358
22629
  try {
22359
22630
  const { lessonId } = req.body;
22360
22631
  const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
@@ -22418,10 +22689,10 @@ Generate the response in the specified JSON schema.`;
22418
22689
  const text = response.text || "{}";
22419
22690
  res.json(JSON.parse(text));
22420
22691
  } catch (e) {
22421
- res.status(500).json({ error: e.message });
22692
+ sendSafeError(res, e);
22422
22693
  }
22423
22694
  });
22424
- app.post("/api/classes/:classId/assignments/create-suggested-quiz", async (req, res) => {
22695
+ app.post("/api/classes/:classId/assignments/create-suggested-quiz", requireAuth("teacher", "administrator"), async (req, res) => {
22425
22696
  try {
22426
22697
  const { title, description, questions, learningObjectives, timeLimit, lessonId } = req.body;
22427
22698
  const id = "ast-" + Date.now().toString(36);
@@ -22442,12 +22713,17 @@ Generate the response in the specified JSON schema.`;
22442
22713
  );
22443
22714
  res.json({ success: true, assignmentId: id });
22444
22715
  } catch (e) {
22445
- res.status(500).json({ error: e.message });
22716
+ sendSafeError(res, e);
22446
22717
  }
22447
22718
  });
22448
- app.post("/api/assignments/:id/submissions", (req, res) => {
22719
+ app.post("/api/assignments/:id/submissions", requireAuth(), (req, res) => {
22449
22720
  try {
22450
22721
  const { studentId, content } = req.body;
22722
+ const session = req.session;
22723
+ const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
22724
+ if (!isPrivileged && session?.userId !== studentId) {
22725
+ return res.status(403).json({ error: "Cannot submit assignment on behalf of another student" });
22726
+ }
22451
22727
  kernelContainer.db.prepare(`
22452
22728
  INSERT INTO assignment_submissions (assignment_id, student_id, content, submitted_at, status)
22453
22729
  VALUES (?, ?, ?, ?, 'submitted')
@@ -22455,7 +22731,7 @@ Generate the response in the specified JSON schema.`;
22455
22731
  `).run(req.params.id, studentId, content, Date.now());
22456
22732
  res.json({ success: true });
22457
22733
  } catch (e) {
22458
- res.status(500).json({ error: e.message });
22734
+ sendSafeError(res, e);
22459
22735
  }
22460
22736
  });
22461
22737
  app.get("/api/assignments/:id/submissions", (req, res) => {
@@ -22469,10 +22745,10 @@ Generate the response in the specified JSON schema.`;
22469
22745
  `).all(req.params.id);
22470
22746
  res.json(submissions);
22471
22747
  } catch (e) {
22472
- res.status(500).json({ error: e.message });
22748
+ sendSafeError(res, e);
22473
22749
  }
22474
22750
  });
22475
- app.post("/api/assignments/:id/submissions/:studentId/grade", async (req, res) => {
22751
+ app.post("/api/assignments/:id/submissions/:studentId/grade", requireAuth("teacher", "administrator"), async (req, res) => {
22476
22752
  try {
22477
22753
  const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
22478
22754
  const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
@@ -22554,7 +22830,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
22554
22830
  });
22555
22831
  res.json({ success: true, pendingApproval: true, message: "Grade generated and sent for approval.", score: grade.score, feedback: grade.feedback });
22556
22832
  } catch (e) {
22557
- res.status(500).json({ error: e.message });
22833
+ sendSafeError(res, e);
22558
22834
  }
22559
22835
  });
22560
22836
  }
@@ -22562,21 +22838,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
22562
22838
  // server/routes/schedules.ts
22563
22839
  var import_genai5 = require("@google/genai");
22564
22840
  function registerSchedulesRoutes(ctx) {
22565
- const {
22566
- app,
22567
- io,
22568
- loginLimiter,
22569
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22570
- lessonActiveSegments: lessonActiveSegments2,
22571
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22572
- buildAgentFinalMessage: buildAgentFinalMessage2,
22573
- normalizeToolSchema: normalizeToolSchema2,
22574
- buildOpenAITools: buildOpenAITools2,
22575
- executeAgentToolCall: executeAgentToolCall2,
22576
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22577
- runGeminiAgentChat: runGeminiAgentChat2,
22578
- runOpenAIAgentChat: runOpenAIAgentChat2
22579
- } = ctx;
22841
+ const { app } = ctx;
22580
22842
  app.get("/api/schedules/today", (req, res) => {
22581
22843
  try {
22582
22844
  const clientDate = req.query.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -22631,7 +22893,7 @@ function registerSchedulesRoutes(ctx) {
22631
22893
  res.status(500).json({ error: e.message });
22632
22894
  }
22633
22895
  });
22634
- app.post("/api/classes/:classId/schedules", (req, res) => {
22896
+ app.post("/api/classes/:classId/schedules", requireAuth("teacher", "administrator"), (req, res) => {
22635
22897
  try {
22636
22898
  const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
22637
22899
  const id = "sch-" + Date.now().toString(36);
@@ -22664,7 +22926,7 @@ function registerSchedulesRoutes(ctx) {
22664
22926
  res.status(500).json({ error: e.message });
22665
22927
  }
22666
22928
  });
22667
- app.put("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
22929
+ app.put("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
22668
22930
  try {
22669
22931
  const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
22670
22932
  kernelContainer.db.prepare(`
@@ -22685,7 +22947,7 @@ function registerSchedulesRoutes(ctx) {
22685
22947
  res.status(500).json({ error: e.message });
22686
22948
  }
22687
22949
  });
22688
- app.delete("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
22950
+ app.delete("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
22689
22951
  try {
22690
22952
  kernelContainer.db.prepare("DELETE FROM schedules WHERE id = ? AND class_id = ?").run(req.params.scheduleId, req.params.classId);
22691
22953
  kernelContainer.db.prepare("DELETE FROM attendance WHERE schedule_id = ?").run(req.params.scheduleId);
@@ -22694,7 +22956,7 @@ function registerSchedulesRoutes(ctx) {
22694
22956
  res.status(500).json({ error: e.message });
22695
22957
  }
22696
22958
  });
22697
- app.post("/api/classes/:classId/schedules/batch", (req, res) => {
22959
+ app.post("/api/classes/:classId/schedules/batch", requireAuth("teacher", "administrator"), (req, res) => {
22698
22960
  try {
22699
22961
  const { schedules } = req.body;
22700
22962
  const db2 = kernelContainer.db;
@@ -22723,7 +22985,7 @@ function registerSchedulesRoutes(ctx) {
22723
22985
  res.status(500).json({ error: e.message });
22724
22986
  }
22725
22987
  });
22726
- app.post("/api/timetable/ocr", async (req, res) => {
22988
+ app.post("/api/timetable/ocr", requireAuth("teacher", "administrator"), async (req, res) => {
22727
22989
  const startTime = Date.now();
22728
22990
  console.log(`[OCR Start] Starting timetable OCR. Payload size: ${req.body.imageBase64?.length || 0} bytes. Lang: ${req.body.lang || "zh"}`);
22729
22991
  try {
@@ -22891,24 +23153,10 @@ function registerSchedulesRoutes(ctx) {
22891
23153
  }
22892
23154
 
22893
23155
  // server/routes/grading.ts
22894
- init_interfaces();
22895
23156
  var import_genai6 = require("@google/genai");
23157
+ init_interfaces();
22896
23158
  function registerGradingRoutes(ctx) {
22897
- const {
22898
- app,
22899
- io,
22900
- loginLimiter,
22901
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22902
- lessonActiveSegments: lessonActiveSegments2,
22903
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22904
- buildAgentFinalMessage: buildAgentFinalMessage2,
22905
- normalizeToolSchema: normalizeToolSchema2,
22906
- buildOpenAITools: buildOpenAITools2,
22907
- executeAgentToolCall: executeAgentToolCall2,
22908
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22909
- runGeminiAgentChat: runGeminiAgentChat2,
22910
- runOpenAIAgentChat: runOpenAIAgentChat2
22911
- } = ctx;
23159
+ const { app } = ctx;
22912
23160
  app.get("/api/classes/:classId/attendance-summary", (req, res) => {
22913
23161
  try {
22914
23162
  const classId = req.params.classId;
@@ -23022,7 +23270,7 @@ function registerGradingRoutes(ctx) {
23022
23270
  res.status(500).json({ error: e.message });
23023
23271
  }
23024
23272
  });
23025
- app.post("/api/schedules/:scheduleId/attendance", (req, res) => {
23273
+ app.post("/api/schedules/:scheduleId/attendance", requireAuth("teacher", "administrator"), (req, res) => {
23026
23274
  try {
23027
23275
  const { studentId, status } = req.body;
23028
23276
  kernelContainer.db.prepare(`
@@ -23052,7 +23300,7 @@ function registerGradingRoutes(ctx) {
23052
23300
  res.status(500).json({ error: e.message });
23053
23301
  }
23054
23302
  });
23055
- app.post("/api/classes/:classId/grade-weights", (req, res) => {
23303
+ app.post("/api/classes/:classId/grade-weights", requireAuth("teacher", "administrator"), (req, res) => {
23056
23304
  try {
23057
23305
  const { attendance_weight, progress_weight, assignment_weight, exam_weight } = req.body;
23058
23306
  const total = Number(attendance_weight) + Number(progress_weight) + Number(assignment_weight) + Number(exam_weight);
@@ -23086,7 +23334,7 @@ function registerGradingRoutes(ctx) {
23086
23334
  res.status(500).json({ error: e.message });
23087
23335
  }
23088
23336
  });
23089
- app.post("/api/classes/:classId/exams", (req, res) => {
23337
+ app.post("/api/classes/:classId/exams", requireAuth("teacher", "administrator"), (req, res) => {
23090
23338
  try {
23091
23339
  const { title, description, max_score } = req.body;
23092
23340
  if (!title) return res.status(400).json({ error: "Title is required" });
@@ -23108,7 +23356,7 @@ function registerGradingRoutes(ctx) {
23108
23356
  res.status(500).json({ error: e.message });
23109
23357
  }
23110
23358
  });
23111
- app.post("/api/exams/:examId/scores", (req, res) => {
23359
+ app.post("/api/exams/:examId/scores", requireAuth("teacher", "administrator"), (req, res) => {
23112
23360
  try {
23113
23361
  const { scores } = req.body;
23114
23362
  if (!Array.isArray(scores)) return res.status(400).json({ error: "Scores array is required" });
@@ -23264,11 +23512,8 @@ function registerGradingRoutes(ctx) {
23264
23512
  res.status(500).json({ error: e.message });
23265
23513
  }
23266
23514
  });
23267
- app.post("/api/grade-sync", async (req, res) => {
23515
+ app.post("/api/grade-sync", requireAuth("teacher", "administrator"), async (req, res) => {
23268
23516
  try {
23269
- if (!checkIsTeacherOrAdmin(req)) {
23270
- return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
23271
- }
23272
23517
  const { lessonId, studentId, grade } = req.body;
23273
23518
  if (!lessonId || !studentId || grade === void 0) {
23274
23519
  return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
@@ -23289,11 +23534,8 @@ function registerGradingRoutes(ctx) {
23289
23534
  res.status(500).json({ success: false, error: e.message });
23290
23535
  }
23291
23536
  });
23292
- app.post("/api/students/:studentId/points", async (req, res) => {
23537
+ app.post("/api/students/:studentId/points", requireAuth("teacher", "administrator"), async (req, res) => {
23293
23538
  try {
23294
- if (!checkIsTeacherOrAdmin(req)) {
23295
- return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
23296
- }
23297
23539
  const { studentId } = req.params;
23298
23540
  const { classId, dimensionId, deltaPoints, reason, pluginId } = req.body;
23299
23541
  if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
@@ -23325,7 +23567,7 @@ function registerGradingRoutes(ctx) {
23325
23567
  res.status(500).json({ success: false, error: e.message });
23326
23568
  }
23327
23569
  });
23328
- app.post("/api/classes/:classId/semester-reports/archive", (req, res) => {
23570
+ app.post("/api/classes/:classId/semester-reports/archive", requireAuth("teacher", "administrator"), (req, res) => {
23329
23571
  try {
23330
23572
  const { semesterName, reports } = req.body;
23331
23573
  if (!semesterName) return res.status(400).json({ error: "semesterName is required" });
@@ -23378,7 +23620,7 @@ function registerGradingRoutes(ctx) {
23378
23620
  res.status(500).json({ error: e.message });
23379
23621
  }
23380
23622
  });
23381
- app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", async (req, res) => {
23623
+ app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", requireAuth("teacher", "administrator"), async (req, res) => {
23382
23624
  try {
23383
23625
  const { classId, studentId } = req.params;
23384
23626
  const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\uFFFD?", providerId } = req.body;
@@ -23478,8 +23720,8 @@ ${examsText}
23478
23720
 
23479
23721
  // server/routes/plugins.ts
23480
23722
  var import_express2 = __toESM(require("express"), 1);
23481
- var import_path13 = __toESM(require("path"), 1);
23482
- var import_fs12 = __toESM(require("fs"), 1);
23723
+ var import_path14 = __toESM(require("path"), 1);
23724
+ var import_fs13 = __toESM(require("fs"), 1);
23483
23725
 
23484
23726
  // server/services/version-fetcher.ts
23485
23727
  var import_child_process2 = require("child_process");
@@ -23661,13 +23903,6 @@ function isSafeExternalUrl(urlStr) {
23661
23903
  function registerPluginsRoutes(ctx) {
23662
23904
  const {
23663
23905
  app,
23664
- io,
23665
- loginLimiter,
23666
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
23667
- lessonActiveSegments: lessonActiveSegments2,
23668
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
23669
- buildAgentFinalMessage: buildAgentFinalMessage2,
23670
- normalizeToolSchema: normalizeToolSchema2,
23671
23906
  buildOpenAITools: buildOpenAITools2,
23672
23907
  executeAgentToolCall: executeAgentToolCall2,
23673
23908
  buildOpenAIChatUrl: buildOpenAIChatUrl2,
@@ -23676,29 +23911,26 @@ function registerPluginsRoutes(ctx) {
23676
23911
  } = ctx;
23677
23912
  app.get("/api/docs/plugin-guide", (req, res) => {
23678
23913
  try {
23679
- const docPath = import_path13.default.join(process.cwd(), "docs_plugin_guide.md");
23680
- if (!import_fs12.default.existsSync(docPath)) {
23914
+ const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
23915
+ if (!import_fs13.default.existsSync(docPath)) {
23681
23916
  return res.status(404).json({ success: false, error: "Document not found" });
23682
23917
  }
23683
- const content = import_fs12.default.readFileSync(docPath, "utf-8");
23918
+ const content = import_fs13.default.readFileSync(docPath, "utf-8");
23684
23919
  res.json({ success: true, content });
23685
23920
  } catch (e) {
23686
- res.status(500).json({ success: false, error: e.message });
23921
+ sendSafeError(res, e);
23687
23922
  }
23688
23923
  });
23689
- app.get("/api/admin/logs", (req, res) => {
23690
- if (!checkIsTeacherOrAdmin(req)) {
23691
- return res.status(403).json({ success: false, error: "Access denied: teachers or admins only" });
23692
- }
23924
+ app.get("/api/admin/logs", requireAuth("administrator"), (req, res) => {
23693
23925
  try {
23694
23926
  const limit = parseInt(req.query.limit) || 200;
23695
23927
  const component = req.query.component;
23696
23928
  const levelFilter = req.query.level;
23697
- const logFile2 = import_path13.default.resolve(process.cwd(), "logs", "openlearn.log");
23698
- if (!import_fs12.default.existsSync(logFile2)) {
23929
+ const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
23930
+ if (!import_fs13.default.existsSync(logFile2)) {
23699
23931
  return res.json({ success: true, logs: [] });
23700
23932
  }
23701
- const content = import_fs12.default.readFileSync(logFile2, "utf-8");
23933
+ const content = import_fs13.default.readFileSync(logFile2, "utf-8");
23702
23934
  const lines = content.split("\n").filter(Boolean);
23703
23935
  let parsedLogs = lines.map((line) => {
23704
23936
  try {
@@ -23729,7 +23961,7 @@ function registerPluginsRoutes(ctx) {
23729
23961
  const sliceStart = Math.max(0, parsedLogs.length - limit);
23730
23962
  res.json({ success: true, logs: parsedLogs.slice(sliceStart) });
23731
23963
  } catch (e) {
23732
- res.status(500).json({ success: false, error: e.message });
23964
+ sendSafeError(res, e);
23733
23965
  }
23734
23966
  });
23735
23967
  app.get("/api/plugins", (req, res) => {
@@ -23754,7 +23986,7 @@ function registerPluginsRoutes(ctx) {
23754
23986
  isSystem: manifestId.startsWith("@openlearn/") || found.pluginId.startsWith("@openlearn/")
23755
23987
  });
23756
23988
  } catch (e) {
23757
- res.status(500).json({ success: false, error: e.message });
23989
+ sendSafeError(res, e);
23758
23990
  }
23759
23991
  });
23760
23992
  app.get("/api/plugins/market", async (req, res) => {
@@ -23780,7 +24012,7 @@ function registerPluginsRoutes(ctx) {
23780
24012
  }
23781
24013
  res.json({ success: true, market: results });
23782
24014
  } catch (e) {
23783
- res.status(500).json({ success: false, error: e.message });
24015
+ sendSafeError(res, e);
23784
24016
  }
23785
24017
  });
23786
24018
  app.post("/api/plugins/:id(*)/check-update", async (req, res) => {
@@ -23802,7 +24034,7 @@ function registerPluginsRoutes(ctx) {
23802
24034
  const result = await checkVersion(src, manifest.version || "0.0.0");
23803
24035
  res.json({ success: true, ...result });
23804
24036
  } catch (e) {
23805
- res.status(500).json({ success: false, error: e.message });
24037
+ sendSafeError(res, e);
23806
24038
  }
23807
24039
  });
23808
24040
  app.post("/api/plugins/:id(*)/one-click-update", requireAuth("administrator"), async (req, res) => {
@@ -23841,9 +24073,9 @@ function registerPluginsRoutes(ctx) {
23841
24073
  });
23842
24074
  }
23843
24075
  } else {
23844
- const zipPath = import_path13.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
23845
- if (import_fs12.default.existsSync(zipPath)) {
23846
- zipBuffer = import_fs12.default.readFileSync(zipPath);
24076
+ const zipPath = import_path14.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
24077
+ if (import_fs13.default.existsSync(zipPath)) {
24078
+ zipBuffer = import_fs13.default.readFileSync(zipPath);
23847
24079
  } else {
23848
24080
  return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
23849
24081
  }
@@ -23863,7 +24095,7 @@ function registerPluginsRoutes(ctx) {
23863
24095
  });
23864
24096
  } catch (err) {
23865
24097
  console.error(err);
23866
- res.status(500).json({ success: false, error: err.message });
24098
+ sendSafeError(res, err);
23867
24099
  }
23868
24100
  });
23869
24101
  app.get("/api/plugins/:id(*)/contributions", (req, res) => {
@@ -23872,7 +24104,7 @@ function registerPluginsRoutes(ctx) {
23872
24104
  const summary = kernelContainer.pluginHost.listContributions(rawId);
23873
24105
  res.json({ success: true, result: summary });
23874
24106
  } catch (e) {
23875
- res.status(500).json({ success: false, error: e.message });
24107
+ sendSafeError(res, e);
23876
24108
  }
23877
24109
  });
23878
24110
  app.get("/api/plugins/:id(*)/config", (req, res) => {
@@ -23892,7 +24124,7 @@ function registerPluginsRoutes(ctx) {
23892
24124
  }
23893
24125
  });
23894
24126
  } catch (e) {
23895
- res.status(500).json({ success: false, error: e.message });
24127
+ sendSafeError(res, e);
23896
24128
  }
23897
24129
  });
23898
24130
  app.post("/api/plugins/:id(*)/config", requireAuth("administrator"), (req, res) => {
@@ -23911,7 +24143,7 @@ function registerPluginsRoutes(ctx) {
23911
24143
  kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
23912
24144
  res.json({ success: true });
23913
24145
  } catch (e) {
23914
- res.status(500).json({ success: false, error: e.message });
24146
+ sendSafeError(res, e);
23915
24147
  }
23916
24148
  });
23917
24149
  app.post("/api/plugins/:id(*)/toggle", requireAuth("administrator"), async (req, res) => {
@@ -23925,7 +24157,7 @@ function registerPluginsRoutes(ctx) {
23925
24157
  const result = await kernelContainer.commandBus.execute(cmd);
23926
24158
  res.json(result);
23927
24159
  } catch (err) {
23928
- res.status(500).json({ success: false, error: err.message });
24160
+ sendSafeError(res, err);
23929
24161
  }
23930
24162
  });
23931
24163
  app.delete("/api/plugins/:id(*)", requireAuth("administrator"), async (req, res) => {
@@ -23939,7 +24171,7 @@ function registerPluginsRoutes(ctx) {
23939
24171
  const result = await kernelContainer.commandBus.execute(cmd);
23940
24172
  res.json(result);
23941
24173
  } catch (err) {
23942
- res.status(500).json({ success: false, error: err.message });
24174
+ sendSafeError(res, err);
23943
24175
  }
23944
24176
  });
23945
24177
  app.get("/api/plugins/:id(*)", async (req, res) => {
@@ -23968,7 +24200,7 @@ function registerPluginsRoutes(ctx) {
23968
24200
  res.json(result);
23969
24201
  } catch (err) {
23970
24202
  console.error(err);
23971
- res.status(500).json({ success: false, error: err.message });
24203
+ sendSafeError(res, err);
23972
24204
  }
23973
24205
  });
23974
24206
  app.post("/api/plugins/upload-zip", requireAuth("administrator"), async (req, res) => {
@@ -23983,7 +24215,7 @@ function registerPluginsRoutes(ctx) {
23983
24215
  res.json(result);
23984
24216
  } catch (err) {
23985
24217
  console.error(err);
23986
- res.status(500).json({ success: false, error: err.message });
24218
+ sendSafeError(res, err);
23987
24219
  }
23988
24220
  });
23989
24221
  app.post("/api/plugins/upload-zip-raw", requireAuth("administrator"), import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
@@ -24025,7 +24257,7 @@ function registerPluginsRoutes(ctx) {
24025
24257
  });
24026
24258
  } catch (err) {
24027
24259
  console.error(err);
24028
- res.status(500).json({ success: false, error: err.message });
24260
+ sendSafeError(res, err);
24029
24261
  }
24030
24262
  });
24031
24263
  app.post(
@@ -24058,7 +24290,7 @@ function registerPluginsRoutes(ctx) {
24058
24290
  });
24059
24291
  } catch (err) {
24060
24292
  console.error(err);
24061
- res.status(500).json({ success: false, error: err.message });
24293
+ sendSafeError(res, err);
24062
24294
  }
24063
24295
  }
24064
24296
  );
@@ -24102,10 +24334,10 @@ function registerPluginsRoutes(ctx) {
24102
24334
  res.json({ success: true, result });
24103
24335
  } catch (err) {
24104
24336
  console.error("[execute-command]", err.message);
24105
- res.status(500).json({ success: false, error: err.message });
24337
+ sendSafeError(res, err);
24106
24338
  }
24107
24339
  });
24108
- app.get("/api/ai-providers", (req, res) => {
24340
+ app.get("/api/ai-providers", requireAuth("teacher", "administrator"), (req, res) => {
24109
24341
  try {
24110
24342
  const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
24111
24343
  const masked = providers.map((p) => ({
@@ -24114,7 +24346,7 @@ function registerPluginsRoutes(ctx) {
24114
24346
  }));
24115
24347
  res.json(masked);
24116
24348
  } catch (e) {
24117
- res.status(500).json({ error: e.message });
24349
+ sendSafeError(res, e);
24118
24350
  }
24119
24351
  });
24120
24352
  app.post("/api/ai-providers", requireAuth("administrator"), (req, res) => {
@@ -24129,7 +24361,7 @@ function registerPluginsRoutes(ctx) {
24129
24361
  kernelContainer.db.prepare("INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, name, api_url, encryptedKey, model_name, now, now);
24130
24362
  res.json({ success: true, id });
24131
24363
  } catch (e) {
24132
- res.status(500).json({ error: e.message });
24364
+ sendSafeError(res, e);
24133
24365
  }
24134
24366
  });
24135
24367
  app.put("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
@@ -24149,7 +24381,7 @@ function registerPluginsRoutes(ctx) {
24149
24381
  kernelContainer.db.prepare("UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?").run(name, api_url, finalKey, model_name, now, req.params.id);
24150
24382
  res.json({ success: true });
24151
24383
  } catch (e) {
24152
- res.status(500).json({ error: e.message });
24384
+ sendSafeError(res, e);
24153
24385
  }
24154
24386
  });
24155
24387
  app.delete("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
@@ -24157,7 +24389,7 @@ function registerPluginsRoutes(ctx) {
24157
24389
  kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
24158
24390
  res.json({ success: true });
24159
24391
  } catch (e) {
24160
- res.status(500).json({ error: e.message });
24392
+ sendSafeError(res, e);
24161
24393
  }
24162
24394
  });
24163
24395
  app.get("/api/site-settings", (req, res) => {
@@ -24169,7 +24401,7 @@ function registerPluginsRoutes(ctx) {
24169
24401
  logoUrl: row?.logo_url || null
24170
24402
  });
24171
24403
  } catch (e) {
24172
- res.status(500).json({ error: e.message });
24404
+ sendSafeError(res, e);
24173
24405
  }
24174
24406
  });
24175
24407
  app.put("/api/site-settings", requireAuth("administrator"), (req, res) => {
@@ -24184,7 +24416,7 @@ function registerPluginsRoutes(ctx) {
24184
24416
  siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
24185
24417
  });
24186
24418
  } catch (e) {
24187
- res.status(500).json({ error: e.message });
24419
+ sendSafeError(res, e);
24188
24420
  }
24189
24421
  });
24190
24422
  app.post("/api/ai-providers/test", requireAuth("administrator"), async (req, res) => {
@@ -24233,7 +24465,7 @@ function registerPluginsRoutes(ctx) {
24233
24465
  res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
24234
24466
  }
24235
24467
  } catch (e) {
24236
- res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
24468
+ sendSafeError(res, e, 500, "Connection failed");
24237
24469
  }
24238
24470
  });
24239
24471
  }
@@ -24253,6 +24485,15 @@ async function startServer() {
24253
24485
  environment: process.env.NODE_ENV || "development",
24254
24486
  config: { port: Number(process.env.PORT) || 9e3 }
24255
24487
  });
24488
+ try {
24489
+ const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
24490
+ const migrations = loadMigrationsFromDirectory(migrationsDir);
24491
+ if (migrations.length > 0) {
24492
+ runMigrations(kernelContainer.db, migrations);
24493
+ }
24494
+ } catch (err) {
24495
+ console.error("[Migration] Failed to run database migrations:", err);
24496
+ }
24256
24497
  await runStartupMigrations(kernelContainer.db);
24257
24498
  await kernelContainer.ready;
24258
24499
  const activityRegistry = new ActivityRegistry2();
@@ -24263,23 +24504,27 @@ async function startServer() {
24263
24504
  kernelContainer.pluginHost.setExpressApp(app);
24264
24505
  const PORT = parseInt(process.env.PORT || "9000", 10);
24265
24506
  app.set("trust proxy", 1);
24507
+ const frameAllowedOrigins = process.env.ALLOWED_FRAME_ORIGINS ? process.env.ALLOWED_FRAME_ORIGINS.split(",").map((s) => s.trim()) : ["http://localhost:*", "http://127.0.0.1:*"];
24266
24508
  app.use((0, import_helmet.default)({
24267
24509
  contentSecurityPolicy: {
24268
24510
  directives: {
24269
24511
  defaultSrc: ["'self'"],
24270
- scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https:", "blob:", "data:"],
24271
- styleSrc: ["'self'", "'unsafe-inline'", "https:", "https://fonts.googleapis.com"],
24512
+ // 移除通配 https: data:,禁止加载全网任意第三方未授权脚本
24513
+ scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"],
24514
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
24272
24515
  imgSrc: ["'self'", "data:", "blob:", "https:"],
24273
24516
  connectSrc: ["'self'", "ws:", "wss:", "https:"],
24274
- fontSrc: ["'self'", "data:", "https:", "https://fonts.gstatic.com"],
24275
- frameSrc: ["'self'", "blob:", "data:", "http://localhost", "http://127.0.0.1", "http:", "https:"],
24276
- objectSrc: ["'none'"]
24517
+ fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
24518
+ // 移除通配 http: https:,限制 iframe 仅能加载本地、沙箱或受信任课件源
24519
+ frameSrc: ["'self'", "blob:", "data:", ...frameAllowedOrigins],
24520
+ objectSrc: ["'none'"],
24521
+ baseUri: ["'self'"]
24277
24522
  }
24278
24523
  },
24279
24524
  crossOriginOpenerPolicy: false,
24280
24525
  crossOriginEmbedderPolicy: false,
24281
24526
  crossOriginResourcePolicy: { policy: "cross-origin" },
24282
- // 允许沙箱 iframe(opaque origin)加载静态资�?
24527
+ // 允许沙箱 iframe(opaque origin)加载静态资?
24283
24528
  originAgentCluster: false,
24284
24529
  strictTransportSecurity: false
24285
24530
  // �? HTTP 部署,禁�? HSTS(否则浏览器缓存后强�? HTTPS,导�? ERR_CONNECTION_REFUSED�?
@@ -24294,8 +24539,8 @@ async function startServer() {
24294
24539
  });
24295
24540
  app.use(import_express3.default.json({ limit: "10mb" }));
24296
24541
  app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
24297
- app.use("/uploads", import_express3.default.static(import_path14.default.join(process.cwd(), "uploads")));
24298
- app.use("/plugins", import_express3.default.static(import_path14.default.join(process.cwd(), "plugins")));
24542
+ app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
24543
+ app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
24299
24544
  app.use((req, res, next) => {
24300
24545
  const origin = req.headers.origin;
24301
24546
  if (origin === "null" || origin === void 0) {
@@ -24325,14 +24570,58 @@ async function startServer() {
24325
24570
  activityRegistry
24326
24571
  };
24327
24572
  const httpServer = (0, import_http.createServer)(app);
24328
- const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",") : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
24573
+ const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()) : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
24329
24574
  const io = new import_socket.Server(httpServer, {
24330
24575
  cors: {
24331
- origin: allowedOrigins.length > 0 ? allowedOrigins : "*",
24576
+ origin: (origin, callback) => {
24577
+ if (!origin) return callback(null, true);
24578
+ if (allowedOrigins.length === 0) {
24579
+ return callback(new Error("CORS not allowed"));
24580
+ }
24581
+ if (allowedOrigins.includes(origin) || allowedOrigins.includes("*")) {
24582
+ return callback(null, true);
24583
+ }
24584
+ return callback(new Error("Not allowed by CORS"));
24585
+ },
24332
24586
  methods: ["GET", "POST"],
24333
24587
  credentials: true
24334
24588
  }
24335
24589
  });
24590
+ io.use((socket, next) => {
24591
+ try {
24592
+ const cookieHeader = socket.handshake.headers.cookie;
24593
+ let token = null;
24594
+ if (cookieHeader) {
24595
+ const parts = cookieHeader.split(";");
24596
+ for (const part of parts) {
24597
+ const trimmed = part.trim();
24598
+ if (trimmed.startsWith("edu_os_token=")) {
24599
+ token = trimmed.substring("edu_os_token=".length);
24600
+ break;
24601
+ }
24602
+ }
24603
+ }
24604
+ if (!token && socket.handshake.auth?.token) {
24605
+ token = socket.handshake.auth.token;
24606
+ }
24607
+ if (process.env.NODE_ENV === "test" && !token) {
24608
+ return next();
24609
+ }
24610
+ if (!token) {
24611
+ return next(new Error("Authentication required: missing edu_os_token"));
24612
+ }
24613
+ const session = getValidSession(token);
24614
+ if (!session) {
24615
+ return next(new Error("Authentication required: session expired or invalid"));
24616
+ }
24617
+ socket.data.session = session;
24618
+ socket.data.userId = session.userId;
24619
+ socket.data.role = session.role;
24620
+ next();
24621
+ } catch (err) {
24622
+ next(new Error(`Authentication error: ${err.message}`));
24623
+ }
24624
+ });
24336
24625
  kernelContainer.pluginHost.setSocketIO(io);
24337
24626
  ctx.io = io;
24338
24627
  registerOsRoutes(ctx);
@@ -24361,10 +24650,10 @@ async function startServer() {
24361
24650
  });
24362
24651
  app.use(vite.middlewares);
24363
24652
  } else {
24364
- const distPath = __dirname || import_path14.default.join(process.cwd(), "dist");
24653
+ const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
24365
24654
  app.use(import_express3.default.static(distPath));
24366
24655
  app.get("*", (req, res) => {
24367
- res.sendFile(import_path14.default.join(distPath, "index.html"));
24656
+ res.sendFile(import_path15.default.join(distPath, "index.html"));
24368
24657
  });
24369
24658
  }
24370
24659
  httpServer.on("error", (err) => {
@@ -24384,8 +24673,8 @@ async function startServer() {
24384
24673
  const startTime = Date.now();
24385
24674
  let platformVersion = "0.0.0";
24386
24675
  try {
24387
- const pkgPath = import_path14.default.join(process.cwd(), "package.json");
24388
- const pkg = JSON.parse(import_fs13.default.readFileSync(pkgPath, "utf-8"));
24676
+ const pkgPath = import_path15.default.join(process.cwd(), "package.json");
24677
+ const pkg = JSON.parse(import_fs14.default.readFileSync(pkgPath, "utf-8"));
24389
24678
  platformVersion = pkg.version ?? platformVersion;
24390
24679
  } catch {
24391
24680
  }
@@ -24401,7 +24690,7 @@ async function startServer() {
24401
24690
  res.status(503).json({ status: "not_ready", error: e.message });
24402
24691
  }
24403
24692
  });
24404
- app.get("/metrics", (_req, res) => {
24693
+ app.get("/metrics", requireAuth("administrator"), (_req, res) => {
24405
24694
  const mem = process.memoryUsage();
24406
24695
  res.json({
24407
24696
  uptime: Math.floor((Date.now() - startTime) / 1e3),