openlearn-next 0.2.8 → 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 (73) hide show
  1. package/dist/assets/AdminDirectoryView-0XV86Ues.js +1 -0
  2. package/dist/assets/BatchPickerModal-of9a5EJz.js +1 -0
  3. package/dist/assets/ClassesView-CefnsjZ8.js +19 -0
  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-DQ91dQio.js → Dashboard-LWbFbup4.js} +2 -2
  10. package/dist/assets/ExportWeightModal-Cyg1zMEd.js +1 -0
  11. package/dist/assets/HelpTour-CHPIK2ae.js +1 -0
  12. package/dist/assets/{HelpView-DLAKVpwf.js → HelpView-CvpgBpdr.js} +5 -5
  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-B6rDWBFb.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-BxrpnlWV.js +1 -0
  22. package/dist/assets/{PluginView-DCY4yi2h.js → PluginView-12qYzmnq.js} +3 -3
  23. package/dist/assets/{ProcessLogsModal-B-PY780w.js → ProcessLogsModal-01S7zsw0.js} +1 -1
  24. package/dist/assets/QuizGeneratorModal-DfZI40Ii.js +1 -0
  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-pptx-B0hlo0eh.js → vendor-pptx-B_NheIKl.js} +5 -5
  41. package/dist/assets/vendor-utils-BEhuP0Zq.js +1 -0
  42. package/dist/index.html +2 -2
  43. package/dist/server.cjs +1079 -699
  44. package/package.json +9 -6
  45. package/dist/assets/AdminDirectoryView-CD7tW4mc.js +0 -1
  46. package/dist/assets/BatchPickerModal-Bt8Qx0Av.js +0 -1
  47. package/dist/assets/ClassesView-B1B3wlkx.js +0 -19
  48. package/dist/assets/CloudDriveModal-BSZaWIex.js +0 -1
  49. package/dist/assets/ComputerLabView-B6-twB0u.js +0 -1
  50. package/dist/assets/CourseManagement-Bnezm9Vv.js +0 -1
  51. package/dist/assets/CourseWizardModal-BvfTvcQT.js +0 -3
  52. package/dist/assets/CoursewareHubPanel-Cb1mAqHS.js +0 -1
  53. package/dist/assets/ExportWeightModal-al1iIVxb.js +0 -1
  54. package/dist/assets/HelpTour-DwM9UBiA.js +0 -1
  55. package/dist/assets/ImportLessonsModal-DwhvqzY_.js +0 -1
  56. package/dist/assets/ImportModal-CmUt1YeX.js +0 -1
  57. package/dist/assets/InteractiveCoursewareViewer-DWnzq20c.js +0 -1
  58. package/dist/assets/InteractiveWhiteboard-CQ-4meYj.js +0 -102
  59. package/dist/assets/LessonEditorView-DixwX1Kw.js +0 -1
  60. package/dist/assets/LiveClassroomView-j93YOxSh.js +0 -1
  61. package/dist/assets/NotificationDetailModal-CQ9PIqkD.js +0 -1
  62. package/dist/assets/QuizGeneratorModal-BvTcmD68.js +0 -1
  63. package/dist/assets/StudentAssignmentView-4R0WxStx.js +0 -1
  64. package/dist/assets/StudentLessonView-DZ0i9-tX.js +0 -1
  65. package/dist/assets/StudentPreviewModal-CfsB1dPY.js +0 -1
  66. package/dist/assets/StudentView-D7GtUiYR.js +0 -2
  67. package/dist/assets/SystemResourceLibraryModal-Ilnthwpw.js +0 -1
  68. package/dist/assets/TeacherView-DjnjsQ_3.js +0 -2
  69. package/dist/assets/index-CATki2cp.css +0 -1
  70. package/dist/assets/index-D8x6bUeG.js +0 -150
  71. package/dist/assets/index-wdWIWwbg.js +0 -1
  72. package/dist/assets/vendor-content-V6uLfjnH.js +0 -3
  73. 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
  }
@@ -2605,7 +2651,9 @@ parentPort.on('message', async function(msg) {
2605
2651
  }
2606
2652
 
2607
2653
  // \u6784\u5EFA\u63D2\u4EF6\u81EA\u5EFA\u8868 API (dbApi)
2608
- var tablePrefix = 'plugin_' + workerData.pluginId.replace(/[^a-zA-Z0-9_]/g, '_') + '_';
2654
+ // \u8868\u524D\u7F00\u5FC5\u987B\u7528 manifestId\uFF08\u4E0E\u547D\u4EE4\u547D\u540D\u7A7A\u95F4 L525 \u53CA ServiceHost \u7684 DDL \u5B88\u536B\u4E00\u81F4\uFF09\uFF0C
2655
+ // \u5426\u5219 Worker \u63D2\u4EF6\u5728\u81EA\u5DF1\u547D\u540D\u7A7A\u95F4\u5EFA\u8868\u4F1A\u88AB\u8BEF\u5224\u4E3A\u8D8A\u6743 DDL\u3002
2656
+ var tablePrefix = 'plugin_' + (workerData.manifestId || workerData.pluginId).replace(/[^a-zA-Z0-9_]/g, '_') + '_';
2609
2657
  var dbService = rawServices['@openlearn/core:IDatabase'];
2610
2658
  var dbApi = dbService ? {
2611
2659
  ensureTable: function(tableName, schema) {
@@ -3076,7 +3124,11 @@ var init_worker_manager = __esm({
3076
3124
  workerData: { pluginId, manifestId: manifest.id, serviceTokens, pluginDir },
3077
3125
  eval: false,
3078
3126
  stdout: true,
3079
- stderr: true
3127
+ stderr: true,
3128
+ resourceLimits: {
3129
+ maxOldGenerationSizeMb: 128,
3130
+ maxYoungGenerationSizeMb: 32
3131
+ }
3080
3132
  });
3081
3133
  const pluginLogger = createLogger(`Plugin:${manifest.id || pluginId}`);
3082
3134
  worker.stdout.on("data", (chunk) => {
@@ -3108,7 +3160,8 @@ var init_worker_manager = __esm({
3108
3160
  // `manifest.id` (NOT the DB UUID `pluginId`) so the prefix
3109
3161
  // matches what the frontend `ctx.invokeCommand` produces —
3110
3162
  // see packages/core/plugin-host/plugin-namespace.ts.
3111
- manifest.id
3163
+ manifest.id,
3164
+ pluginId
3112
3165
  );
3113
3166
  const createdAt = Date.now();
3114
3167
  this.registry.register(pluginId, {
@@ -3281,8 +3334,8 @@ __export(server_exports, {
3281
3334
  });
3282
3335
  module.exports = __toCommonJS(server_exports);
3283
3336
  var import_express3 = __toESM(require("express"), 1);
3284
- var import_path14 = __toESM(require("path"), 1);
3285
- var import_fs13 = __toESM(require("fs"), 1);
3337
+ var import_path15 = __toESM(require("path"), 1);
3338
+ var import_fs14 = __toESM(require("fs"), 1);
3286
3339
  var import_dotenv = __toESM(require("dotenv"), 1);
3287
3340
  var import_vite = require("vite");
3288
3341
  var import_http = require("http");
@@ -3829,6 +3882,7 @@ function parseRequiresEntry(entry) {
3829
3882
  // packages/core/db/index.ts
3830
3883
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
3831
3884
  var import_path = __toESM(require("path"), 1);
3885
+ var import_os = __toESM(require("os"), 1);
3832
3886
  var import_fs = require("fs");
3833
3887
  var import_url = require("url");
3834
3888
  var import_crypto = __toESM(require("crypto"), 1);
@@ -3852,6 +3906,11 @@ var dbPath;
3852
3906
  if (process.env.OPENLEARN_DB_PATH) {
3853
3907
  dbPath = process.env.OPENLEARN_DB_PATH;
3854
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`);
3855
3914
  } else {
3856
3915
  if (typeof import_meta !== "undefined" && import_meta.url) {
3857
3916
  const __filename2 = (0, import_url.fileURLToPath)(import_meta.url);
@@ -3882,6 +3941,7 @@ db.exec(`
3882
3941
  timeline TEXT,
3883
3942
  progress_mode TEXT DEFAULT 'manual',
3884
3943
  progress_conditions TEXT,
3944
+ creator_id TEXT,
3885
3945
  created_at INTEGER NOT NULL,
3886
3946
  updated_at INTEGER NOT NULL
3887
3947
  );
@@ -4219,6 +4279,10 @@ db.exec(`
4219
4279
  CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
4220
4280
  CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
4221
4281
  `);
4282
+ try {
4283
+ db.exec("ALTER TABLE lessons ADD COLUMN creator_id TEXT");
4284
+ } catch {
4285
+ }
4222
4286
  try {
4223
4287
  db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
4224
4288
  } catch {
@@ -4315,6 +4379,9 @@ try {
4315
4379
  const insertStmt = db.prepare("INSERT INTO users (id, username, password_hash, role, name, created_at) VALUES (?, ?, ?, ?, ?, ?)");
4316
4380
  insertStmt.run("usr_admin", "admin", hashPassword("admin"), "administrator", "System Admin", Date.now());
4317
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
+ );
4318
4385
  }
4319
4386
  } catch (e) {
4320
4387
  console.error("Failed to seed default users:", e);
@@ -5669,13 +5736,15 @@ var BuiltinPlugin = {
5669
5736
  async execute(command) {
5670
5737
  const payload = command.payload;
5671
5738
  const lessonId = (0, import_uuid6.v7)();
5672
- const stmt = db2.prepare("INSERT INTO lessons (id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
5673
- 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());
5674
5743
  await eventBus.publish({
5675
5744
  id: (0, import_uuid6.v7)(),
5676
5745
  type: "lesson.created",
5677
5746
  source: "builtin.lesson",
5678
- payload: { id: lessonId, title: payload.title },
5747
+ payload: { id: lessonId, title: payload.title, creatorId },
5679
5748
  timestamp: Date.now(),
5680
5749
  correlationId: command.id
5681
5750
  });
@@ -6648,12 +6717,27 @@ var BuiltinPlugin = {
6648
6717
  const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
6649
6718
  const fileBuffer = Buffer.from(base64Content, "base64");
6650
6719
  if (ext === ".html" || ext === ".htm") {
6651
- const entryName = filename;
6652
- 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);
6653
6729
  const coursewareId = "cw_" + import_crypto3.default.randomBytes(8).toString("hex");
6654
6730
  db2.prepare(
6655
6731
  "INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
6656
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
+ }
6657
6741
  await eventBus.publish({
6658
6742
  id: (0, import_uuid6.v7)(),
6659
6743
  type: "courseware.uploaded",
@@ -6742,6 +6826,23 @@ var BuiltinPlugin = {
6742
6826
  db2.prepare(
6743
6827
  "INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
6744
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
+ }
6745
6846
  await eventBus.publish({
6746
6847
  id: (0, import_uuid6.v7)(),
6747
6848
  type: "courseware.uploaded",
@@ -7580,7 +7681,7 @@ var import_node_url = require("node:url");
7580
7681
  var esbuild = __toESM(require("esbuild"), 1);
7581
7682
  var import_jszip = __toESM(require("jszip"), 1);
7582
7683
  var fs3 = __toESM(require("fs"), 1);
7583
- var os = __toESM(require("os"), 1);
7684
+ var os2 = __toESM(require("os"), 1);
7584
7685
  var path4 = __toESM(require("path"), 1);
7585
7686
  var import_uuid10 = require("uuid");
7586
7687
  var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
@@ -7668,7 +7769,7 @@ async function validateAndBundleZip(zipBuffer) {
7668
7769
  );
7669
7770
  }
7670
7771
  const entryCode = await entryFile.async("string");
7671
- 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)()}`);
7672
7773
  try {
7673
7774
  fs3.mkdirSync(tmpDir, { recursive: true });
7674
7775
  for (const [name, file] of Object.entries(zip.files)) {
@@ -8458,11 +8559,11 @@ var PluginDistributionManager = class {
8458
8559
  return;
8459
8560
  }
8460
8561
  const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
8461
- const fs15 = await import("node:fs");
8462
- if (!fs15.existsSync(filePath)) {
8562
+ const fs16 = await import("node:fs");
8563
+ if (!fs16.existsSync(filePath)) {
8463
8564
  throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
8464
8565
  }
8465
- const code = fs15.readFileSync(filePath, "utf-8");
8566
+ const code = fs16.readFileSync(filePath, "utf-8");
8466
8567
  await this.pluginHost.reloadPlugin(pluginId, code);
8467
8568
  }
8468
8569
  async updateFromZip(zipBuffer, options = {}) {
@@ -8509,6 +8610,16 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
8509
8610
  var ACTIVATION_TIMEOUT_MS = 5e3;
8510
8611
  var DEACTIVATION_TIMEOUT_MS = 5e3;
8511
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
+ }
8512
8623
  var PluginHost = class {
8513
8624
  constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
8514
8625
  this.serviceRegistry = serviceRegistry;
@@ -8554,7 +8665,7 @@ var PluginHost = class {
8554
8665
  const pluginDir = this.getPluginDir(p.id);
8555
8666
  const absDir = import_path4.default.join(pluginDir, m.deploy.staticDir);
8556
8667
  if (import_fs5.default.existsSync(absDir)) {
8557
- this.expressApp.use(m.deploy.staticRoute, import_express.default.static(absDir));
8668
+ this.expressApp.use(m.deploy.staticRoute, ...createPluginStaticMiddleware(absDir));
8558
8669
  this._registeredRoutes.set(m.id, m.deploy.staticRoute);
8559
8670
  console.log(`[PluginHost] Restored static route "${m.deploy.staticRoute}" for plugin "${m.id}"`);
8560
8671
  }
@@ -9764,10 +9875,15 @@ var PluginHost = class {
9764
9875
  import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, "frontend.js"), frontendCode, "utf-8");
9765
9876
  }
9766
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
+ }
9767
9883
  const deployFile = zip.file(manifest.deploy.script);
9768
9884
  if (deployFile) {
9769
9885
  const deployCode = await deployFile.async("string");
9770
- import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, manifest.deploy.script), deployCode, "utf-8");
9886
+ import_fs5.default.writeFileSync(resolvedScript, deployCode, "utf-8");
9771
9887
  }
9772
9888
  }
9773
9889
  this.emitProgress(manifest.id, "extracting", "Extracting assets...");
@@ -9777,20 +9893,29 @@ var PluginHost = class {
9777
9893
  if (storageEntries.length > 0) {
9778
9894
  console.log(`[PluginHost] Extracting ${storageEntries.length} static asset files for plugin "${manifest.id}"...`);
9779
9895
  const dirs = /* @__PURE__ */ new Set();
9780
- for (const name of storageEntries) {
9781
- 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));
9782
9903
  }
9783
9904
  for (const dir of dirs) {
9784
- import_fs5.default.mkdirSync(import_path4.default.join(pluginDir, dir), { recursive: true });
9905
+ import_fs5.default.mkdirSync(dir, { recursive: true });
9785
9906
  }
9786
9907
  const BATCH_SIZE = 10;
9787
9908
  for (let i = 0; i < storageEntries.length; i += BATCH_SIZE) {
9788
9909
  const batch = storageEntries.slice(i, i + BATCH_SIZE);
9789
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
+ }
9790
9916
  const file = zip.file(name);
9791
9917
  if (file) {
9792
9918
  const content = await file.async("nodebuffer");
9793
- const destPath = import_path4.default.join(pluginDir, name);
9794
9919
  import_fs5.default.writeFileSync(destPath, content);
9795
9920
  }
9796
9921
  }));
@@ -9808,7 +9933,7 @@ var PluginHost = class {
9808
9933
  };
9809
9934
  import_fs5.default.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
9810
9935
  const { execSync: execSync2 } = await import("node:child_process");
9811
- 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", {
9812
9937
  cwd: pluginDir,
9813
9938
  stdio: "ignore"
9814
9939
  });
@@ -9818,30 +9943,36 @@ var PluginHost = class {
9818
9943
  }
9819
9944
  }
9820
9945
  if (manifest.deploy?.script) {
9821
- let deployScriptPath = import_path4.default.join(pluginDir, manifest.deploy.script);
9822
- if (!import_fs5.default.existsSync(deployScriptPath)) {
9823
- const altPath = import_path4.default.join(process.cwd(), "v2_plugins", "scratch-editor-deploy", manifest.deploy.script);
9824
- if (import_fs5.default.existsSync(altPath)) deployScriptPath = altPath;
9825
- }
9826
- if (import_fs5.default.existsSync(deployScriptPath)) {
9827
- console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
9828
- try {
9829
- const { execSync: execSync2 } = await import("node:child_process");
9830
- execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
9831
- console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
9832
- } catch (deployErr) {
9833
- console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
9834
- throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
9835
- }
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
+ );
9836
9950
  } else {
9837
- 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
+ }
9838
9969
  }
9839
9970
  }
9840
9971
  if (manifest.deploy?.staticRoute && manifest.deploy?.staticDir && this.expressApp) {
9841
9972
  const route = manifest.deploy.staticRoute;
9842
9973
  const absDir = import_path4.default.join(pluginDir, manifest.deploy.staticDir);
9843
9974
  if (import_fs5.default.existsSync(absDir)) {
9844
- this.expressApp.use(route, import_express.default.static(absDir));
9975
+ this.expressApp.use(route, ...createPluginStaticMiddleware(absDir));
9845
9976
  this._registeredRoutes.set(manifest.id, route);
9846
9977
  console.log(`[PluginHost] Registered static route "${route}" for plugin "${manifest.id}"`);
9847
9978
  } else {
@@ -10578,8 +10709,8 @@ var HotReloadController = class {
10578
10709
  const timer = setTimeout(async () => {
10579
10710
  this.debounceTimers.delete(pluginId);
10580
10711
  try {
10581
- const fs15 = await import("fs/promises");
10582
- const newSourceCode = await fs15.readFile(filePath, "utf-8");
10712
+ const fs16 = await import("fs/promises");
10713
+ const newSourceCode = await fs16.readFile(filePath, "utf-8");
10583
10714
  console.log(
10584
10715
  `[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`
10585
10716
  );
@@ -17370,13 +17501,22 @@ function setupPresence({ io, eventBus }) {
17370
17501
  };
17371
17502
  io.on("connection", (socket) => {
17372
17503
  let registeredStudentId = null;
17504
+ const session = socket.data?.session;
17505
+ const isTeacherOrAdmin = session?.role === "teacher" || session?.role === "administrator";
17373
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
+ }
17374
17511
  registeredStudentId = data.studentId;
17375
17512
  onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
17376
17513
  console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
17377
17514
  broadcastPresence();
17378
17515
  });
17379
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
+ }
17380
17520
  activeStudentLessons.set(data.studentId, data.lessonId);
17381
17521
  socket.join(data.lessonId);
17382
17522
  console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
@@ -17390,6 +17530,9 @@ function setupPresence({ io, eventBus }) {
17390
17530
  }
17391
17531
  });
17392
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
+ }
17393
17536
  const oldRoom = activeStudentLessons.get(data.studentId);
17394
17537
  if (oldRoom) {
17395
17538
  socket.leave(oldRoom);
@@ -17426,10 +17569,18 @@ function setupPresence({ io, eventBus }) {
17426
17569
  }
17427
17570
  );
17428
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
+ }
17429
17576
  lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
17430
17577
  io.to(data.lessonId).emit("student-active-segment-changed", data);
17431
17578
  });
17432
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
+ }
17433
17584
  console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
17434
17585
  const studentOnlineInfo = onlineStudents.get(data.studentId);
17435
17586
  if (studentOnlineInfo) {
@@ -17537,6 +17688,83 @@ async function runStartupMigrations(db2) {
17537
17688
  }
17538
17689
  }
17539
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
+
17540
17768
  // server/ai-agent.ts
17541
17769
  var import_genai2 = require("@google/genai");
17542
17770
  var import_crypto4 = __toESM(require("crypto"), 1);
@@ -17848,6 +18076,90 @@ var runOpenAIAgentChat = async (provider, request) => {
17848
18076
  };
17849
18077
  };
17850
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
+
17851
18163
  // packages/core/bootstrap/adapter/bootstrap-registration.ts
17852
18164
  var BootstrapRegistration = class {
17853
18165
  static registerConfiguration(builder, context) {
@@ -18332,28 +18644,28 @@ var IActivityRegistryToken = new Token(
18332
18644
  );
18333
18645
 
18334
18646
  // server/routes/os.ts
18335
- var import_path8 = __toESM(require("path"), 1);
18336
- var import_fs8 = __toESM(require("fs"), 1);
18647
+ var import_path9 = __toESM(require("path"), 1);
18648
+ var import_fs9 = __toESM(require("fs"), 1);
18337
18649
  var import_child_process = require("child_process");
18338
18650
  var import_crypto7 = __toESM(require("crypto"), 1);
18339
18651
  var import_xss = require("xss");
18340
18652
 
18341
18653
  // server/utils/crypto.ts
18342
18654
  var import_crypto5 = __toESM(require("crypto"), 1);
18343
- var import_fs7 = __toESM(require("fs"), 1);
18344
- var import_path6 = __toESM(require("path"), 1);
18655
+ var import_fs8 = __toESM(require("fs"), 1);
18656
+ var import_path7 = __toESM(require("path"), 1);
18345
18657
  var _encryptionKey = null;
18346
18658
  function getEncryptionKey() {
18347
18659
  if (_encryptionKey) return _encryptionKey;
18348
- const envPath = import_path6.default.resolve(process.cwd(), ".env");
18660
+ const envPath = import_path7.default.resolve(process.cwd(), ".env");
18349
18661
  const keyHex = process.env.ENCRYPTION_KEY;
18350
18662
  if (keyHex && keyHex.trim() !== "") {
18351
18663
  _encryptionKey = Buffer.from(keyHex.trim(), "hex");
18352
18664
  return _encryptionKey;
18353
18665
  }
18354
- if (import_fs7.default.existsSync(envPath)) {
18666
+ if (import_fs8.default.existsSync(envPath)) {
18355
18667
  try {
18356
- const content = import_fs7.default.readFileSync(envPath, "utf-8");
18668
+ const content = import_fs8.default.readFileSync(envPath, "utf-8");
18357
18669
  const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
18358
18670
  if (match && match[1].trim() !== "") {
18359
18671
  const fileKeyHex = match[1].trim();
@@ -18367,8 +18679,8 @@ function getEncryptionKey() {
18367
18679
  }
18368
18680
  const newKey = import_crypto5.default.randomBytes(32).toString("hex");
18369
18681
  try {
18370
- if (import_fs7.default.existsSync(envPath)) {
18371
- 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");
18372
18684
  if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
18373
18685
  console.warn(
18374
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."
@@ -18377,12 +18689,12 @@ function getEncryptionKey() {
18377
18689
  _encryptionKey = Buffer.from(newKey, "hex");
18378
18690
  return _encryptionKey;
18379
18691
  }
18380
- import_fs7.default.appendFileSync(envPath, `
18692
+ import_fs8.default.appendFileSync(envPath, `
18381
18693
  ENCRYPTION_KEY=${newKey}
18382
18694
  `);
18383
18695
  console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
18384
18696
  } else {
18385
- import_fs7.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
18697
+ import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
18386
18698
  `);
18387
18699
  console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
18388
18700
  }
@@ -18435,142 +18747,58 @@ function detectPromptInjection(input) {
18435
18747
  return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
18436
18748
  }
18437
18749
 
18438
- // server/middleware/auth.ts
18439
- function getValidSession(token) {
18440
- 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'
18441
18757
  try {
18442
- sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
18443
- } catch {
18444
- return null;
18445
- }
18446
- if (!sessionRow) return null;
18447
- const now = Date.now();
18448
- if (sessionRow.expires_at && sessionRow.expires_at < now) {
18449
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18450
- 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) {}
18451
18771
  }
18452
- const idleTimeout = 24 * 60 * 60 * 1e3;
18453
- if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
18454
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
18455
- return null;
18456
- }
18457
- kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
18458
- return JSON.parse(sessionRow.session_data);
18459
- }
18460
- function getCookieToken(req) {
18461
- const rc = req.headers.cookie;
18462
- if (!rc) return null;
18463
- const parts = rc.split(";");
18464
- for (const part of parts) {
18465
- const trimmed = part.trim();
18466
- if (trimmed.startsWith("edu_os_token=")) {
18467
- return trimmed.substring("edu_os_token=".length);
18468
- }
18469
- }
18470
- return null;
18471
- }
18472
- function getActorId(req) {
18473
- const token = getCookieToken(req);
18474
- if (!token) return "anonymous";
18475
- try {
18476
- const session = getValidSession(token);
18477
- if (!session) return "anonymous";
18478
- let role = session.subRole || session.role;
18479
- if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18480
- role = "administrator";
18481
- }
18482
- if (role) {
18483
- return `user:${session.userId || "demo"}:${role}`;
18484
- }
18485
- return "anonymous";
18486
- } catch {
18487
- return "anonymous";
18488
- }
18489
- }
18490
- function requireAuth(...roles) {
18491
- return (req, res, next) => {
18492
- const token = getCookieToken(req);
18493
- if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
18494
- const session = getValidSession(token);
18495
- if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
18496
- if (roles.length > 0) {
18497
- let userRole = session.role;
18498
- if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
18499
- userRole = "administrator";
18500
- }
18501
- const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
18502
- if (!effectiveRoles.includes(userRole)) {
18503
- return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
18504
- }
18505
- }
18506
- req.session = session;
18507
- next();
18508
- };
18509
- }
18510
- function checkIsTeacherOrAdmin(req) {
18511
- const token = getCookieToken(req);
18512
- if (!token) return false;
18513
- try {
18514
- const session = getValidSession(token);
18515
- if (!session) return false;
18516
- return session.role === "teacher" || session.role === "administrator";
18517
- } catch {
18518
- return false;
18519
- }
18520
- }
18521
-
18522
- // server/routes/shared.ts
18523
- var import_path7 = __toESM(require("path"), 1);
18524
- var import_crypto6 = __toESM(require("crypto"), 1);
18525
-
18526
- // server/utils/bridge-sdk.ts
18527
- var BRIDGE_SDK_CODE = `(function() {
18528
- // Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
18529
- try {
18530
- Object.defineProperty(document, 'cookie', {
18531
- get: function() { return ""; },
18532
- set: function(val) {},
18533
- configurable: true
18534
- });
18535
- } catch (e) {
18536
- try {
18537
- Object.defineProperty(Document.prototype, 'cookie', {
18538
- get: function() { return ""; },
18539
- set: function(val) {},
18540
- configurable: true
18541
- });
18542
- } catch (err) {}
18543
- }
18544
-
18545
- // \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
18546
- function __makeSafePostMessage(realTarget, label) {
18547
- return function(message, targetOrigin, transfer) {
18548
- try {
18549
- if (message && typeof message === 'object') {
18550
- if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
18551
- message.attempt_id = window.__LMS_STUDENT__.attempt_id;
18552
- }
18553
- if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
18554
- message.uuid = window.__LMS_COURSEWARE__.uuid;
18555
- }
18556
- }
18557
- } catch (e) {}
18558
-
18559
- var origin = targetOrigin;
18560
- if (origin === 'null' || origin === null || origin === undefined) {
18561
- console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
18562
- origin = '*';
18563
- }
18564
- try {
18565
- return realTarget.postMessage.call(realTarget, message, origin, transfer);
18566
- } catch (err) {
18567
- if (err.name === 'SyntaxError' && origin !== '*') {
18568
- console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
18569
- return realTarget.postMessage.call(realTarget, message, '*', transfer);
18570
- }
18571
- throw err;
18572
- }
18573
- };
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
+ };
18574
18802
  }
18575
18803
 
18576
18804
  // \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
@@ -18681,6 +18909,61 @@ var BRIDGE_SDK_CODE = `(function() {
18681
18909
  }
18682
18910
  } catch (e) {}
18683
18911
 
18912
+ // \u2500\u2500 Bidirectional host\u2192courseware command bus & Theme Bridge \u2500\u2500
18913
+ var __lmsHandlers = {};
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
+
18940
+ window.addEventListener('message', function(event) {
18941
+ var d = event.data;
18942
+ if (!d || typeof d !== 'object') return;
18943
+ if (d.type === 'LMS_HOST_COMMAND' && d.event) {
18944
+ if (d.event === 'theme:changed' && d.payload) {
18945
+ __applyThemeTokens(d.payload);
18946
+ }
18947
+ var cbs = __lmsHandlers[d.event] || [];
18948
+ for (var i = 0; i < cbs.length; i++) {
18949
+ try { cbs[i](d.payload); } catch (e) {}
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
+ }
18957
+ } else if (d.type === 'LMS_PROGRESS_RESPONSE' && d.requestId) {
18958
+ var pending = __lmsPendingRequests[d.requestId];
18959
+ if (pending) {
18960
+ delete __lmsPendingRequests[d.requestId];
18961
+ clearTimeout(pending.timer);
18962
+ pending.resolve(d.progress || null);
18963
+ }
18964
+ }
18965
+ });
18966
+
18684
18967
  window.LMS = {
18685
18968
  submit(data) {
18686
18969
  window.parent.postMessage({
@@ -18712,6 +18995,9 @@ var BRIDGE_SDK_CODE = `(function() {
18712
18995
  getCourseware() {
18713
18996
  return window.__LMS_COURSEWARE__;
18714
18997
  },
18998
+ getTheme() {
18999
+ return window.__LMS_THEME__ || null;
19000
+ },
18715
19001
  log(event, data) {
18716
19002
  window.parent.postMessage({
18717
19003
  type: "LMS_LOG",
@@ -18720,6 +19006,43 @@ var BRIDGE_SDK_CODE = `(function() {
18720
19006
  event: event,
18721
19007
  payload: data
18722
19008
  }, "*");
19009
+ },
19010
+ on(event, callback) {
19011
+ (__lmsHandlers[event] = __lmsHandlers[event] || []).push(callback);
19012
+ return function() {
19013
+ var arr = __lmsHandlers[event] || [];
19014
+ var idx = arr.indexOf(callback);
19015
+ if (idx >= 0) arr.splice(idx, 1);
19016
+ };
19017
+ },
19018
+ off(event, callback) {
19019
+ var arr = __lmsHandlers[event] || [];
19020
+ var idx = arr.indexOf(callback);
19021
+ if (idx >= 0) arr.splice(idx, 1);
19022
+ },
19023
+ setConfig(config) {
19024
+ window.parent.postMessage({
19025
+ type: "LMS_CONFIG",
19026
+ uuid: window.__LMS_COURSEWARE__?.uuid,
19027
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
19028
+ config: config
19029
+ }, "*");
19030
+ },
19031
+ getProgress() {
19032
+ return new Promise(function(resolve) {
19033
+ var requestId = 'prog_' + Math.random().toString(36).slice(2) + '_' + Date.now();
19034
+ var timer = setTimeout(function() {
19035
+ delete __lmsPendingRequests[requestId];
19036
+ resolve(null);
19037
+ }, 5000);
19038
+ __lmsPendingRequests[requestId] = { resolve: resolve, timer: timer };
19039
+ window.parent.postMessage({
19040
+ type: "LMS_GET_PROGRESS",
19041
+ uuid: window.__LMS_COURSEWARE__?.uuid,
19042
+ attempt_id: window.__LMS_STUDENT__?.attempt_id,
19043
+ requestId: requestId
19044
+ }, "*");
19045
+ });
18723
19046
  }
18724
19047
  };
18725
19048
 
@@ -19087,7 +19410,7 @@ function validateMagicBytes(buffer, fileName) {
19087
19410
  ".webp": [[82, 73, 70, 70]]
19088
19411
  // RIFF
19089
19412
  };
19090
- const ext = import_path7.default.extname(fileName || "").toLowerCase();
19413
+ const ext = import_path8.default.extname(fileName || "").toLowerCase();
19091
19414
  const signatures = MAGIC_BYTES[ext];
19092
19415
  if (!signatures) return true;
19093
19416
  return signatures.some(
@@ -19162,6 +19485,8 @@ window.__LMS_COURSEWARE__ = {
19162
19485
  html = html.replace(/<script[^>]*src='[^']*frog-sdk\.js'[^>]*><\/script>/gi, "<!-- Removed frog-sdk.js to prevent sandboxed iframe crash -->");
19163
19486
  html = html.replace(/<script[^>]*src="[^"]*init-frog\.js"[^>]*><\/script>/gi, "<!-- Removed init-frog.js to prevent sandboxed iframe crash -->");
19164
19487
  html = html.replace(/<script[^>]*src='[^']*init-frog\.js'[^>]*><\/script>/gi, "<!-- Removed init-frog.js to prevent sandboxed iframe crash -->");
19488
+ html = html.replace(/<base\b[^>]*>/gi, "<!-- Removed base tag -->");
19489
+ html = html.replace(/<meta[^>]*http-equiv\s*=\s*["']?refresh["']?[^>]*>/gi, "<!-- Removed meta refresh -->");
19165
19490
  if (html.toLowerCase().includes("<head>")) {
19166
19491
  html = html.replace(/<head>/i, `<head>${injection}`);
19167
19492
  } else if (html.toLowerCase().includes("<html>")) {
@@ -19188,20 +19513,21 @@ var generateStudentNumber = (db2) => {
19188
19513
  return `S${nextSeq.toString().padStart(3, "0")}`;
19189
19514
  };
19190
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
+
19191
19527
  // server/routes/os.ts
19192
19528
  function registerOsRoutes(ctx) {
19193
19529
  const {
19194
19530
  app,
19195
- io,
19196
- loginLimiter,
19197
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19198
- lessonActiveSegments: lessonActiveSegments2,
19199
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19200
- buildAgentFinalMessage: buildAgentFinalMessage2,
19201
- normalizeToolSchema: normalizeToolSchema2,
19202
- buildOpenAITools: buildOpenAITools2,
19203
- executeAgentToolCall: executeAgentToolCall2,
19204
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19205
19531
  runGeminiAgentChat: runGeminiAgentChat2,
19206
19532
  runOpenAIAgentChat: runOpenAIAgentChat2,
19207
19533
  activityRegistry
@@ -19212,7 +19538,7 @@ function registerOsRoutes(ctx) {
19212
19538
  if (!filename || !base64Data) {
19213
19539
  return res.status(400).json({ error: "Filename and base64Data are required" });
19214
19540
  }
19215
- const ext = import_path8.default.extname(filename).toLowerCase();
19541
+ const ext = import_path9.default.extname(filename).toLowerCase();
19216
19542
  if (BLOCKED_EXTENSIONS.includes(ext)) {
19217
19543
  return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
19218
19544
  }
@@ -19224,13 +19550,13 @@ function registerOsRoutes(ctx) {
19224
19550
  if (!validateMagicBytes(fileBuffer, filename)) {
19225
19551
  return res.status(400).json({ error: "File content does not match the declared file type." });
19226
19552
  }
19227
- const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
19228
- if (!import_fs8.default.existsSync(uploadsDir)) {
19229
- 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 });
19230
19556
  }
19231
19557
  const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
19232
- const filePath = import_path8.default.join(uploadsDir, uniqueName);
19233
- import_fs8.default.writeFileSync(filePath, fileBuffer);
19558
+ const filePath = import_path9.default.join(uploadsDir, uniqueName);
19559
+ import_fs9.default.writeFileSync(filePath, fileBuffer);
19234
19560
  let slideCount = 1;
19235
19561
  if (ext === ".pdf") {
19236
19562
  try {
@@ -19264,7 +19590,7 @@ function registerOsRoutes(ctx) {
19264
19590
  });
19265
19591
  } catch (e) {
19266
19592
  console.error("Upload error:", e);
19267
- res.status(500).json({ error: e.message });
19593
+ sendSafeError(res, e);
19268
19594
  }
19269
19595
  });
19270
19596
  app.post("/api/commands", requireAuth(), async (req, res) => {
@@ -19278,7 +19604,7 @@ function registerOsRoutes(ctx) {
19278
19604
  const result = await kernelContainer.commandBus.execute(cmd);
19279
19605
  res.json({ success: true, result });
19280
19606
  } catch (err) {
19281
- res.status(500).json({ success: false, error: err.message });
19607
+ sendSafeError(res, err);
19282
19608
  }
19283
19609
  });
19284
19610
  app.get("/api/commands/registered", (req, res) => {
@@ -19286,7 +19612,7 @@ function registerOsRoutes(ctx) {
19286
19612
  const actions = kernelContainer.actionRegistry.getAllActions();
19287
19613
  res.json(actions);
19288
19614
  } catch (err) {
19289
- res.status(500).json({ error: err.message });
19615
+ sendSafeError(res, err);
19290
19616
  }
19291
19617
  });
19292
19618
  app.get("/api/activities", async (req, res) => {
@@ -19295,7 +19621,7 @@ function registerOsRoutes(ctx) {
19295
19621
  const list = role === "all" ? activityRegistry.listDescriptors() : activityRegistry.listByRole(role).map((p) => p.descriptor);
19296
19622
  res.json({ activities: list });
19297
19623
  } catch (err) {
19298
- res.status(500).json({ error: err.message });
19624
+ sendSafeError(res, err);
19299
19625
  }
19300
19626
  });
19301
19627
  app.post("/api/activities/:id/start", async (req, res) => {
@@ -19317,7 +19643,7 @@ function registerOsRoutes(ctx) {
19317
19643
  res.json({ ok: true, ...result });
19318
19644
  } catch (err) {
19319
19645
  const status = err?.code === "PERMISSION_DENIED" ? 403 : 500;
19320
- res.status(status).json({ ok: false, error: err.message });
19646
+ sendSafeError(res, err, status);
19321
19647
  }
19322
19648
  });
19323
19649
  app.get("/api/activities/running", (_req, res) => {
@@ -19333,7 +19659,7 @@ function registerOsRoutes(ctx) {
19333
19659
  }));
19334
19660
  res.json({ activities });
19335
19661
  } catch (err) {
19336
- res.status(500).json({ error: err.message });
19662
+ sendSafeError(res, err);
19337
19663
  }
19338
19664
  });
19339
19665
  app.post("/api/activities/:id/finish", async (req, res) => {
@@ -19352,7 +19678,7 @@ function registerOsRoutes(ctx) {
19352
19678
  await provider.finish(context);
19353
19679
  res.json({ ok: true });
19354
19680
  } catch (err) {
19355
- res.status(500).json({ ok: false, error: err.message });
19681
+ sendSafeError(res, err);
19356
19682
  }
19357
19683
  });
19358
19684
  app.post("/api/activities/:id/pause", async (req, res) => {
@@ -19371,7 +19697,7 @@ function registerOsRoutes(ctx) {
19371
19697
  await provider.pause(context);
19372
19698
  res.json({ ok: true });
19373
19699
  } catch (err) {
19374
- res.status(500).json({ ok: false, error: err.message });
19700
+ sendSafeError(res, err);
19375
19701
  }
19376
19702
  });
19377
19703
  app.post("/api/activities/:id/resume", async (req, res) => {
@@ -19390,7 +19716,7 @@ function registerOsRoutes(ctx) {
19390
19716
  await provider.resume(context);
19391
19717
  res.json({ ok: true });
19392
19718
  } catch (err) {
19393
- res.status(500).json({ ok: false, error: err.message });
19719
+ sendSafeError(res, err);
19394
19720
  }
19395
19721
  });
19396
19722
  app.post("/api/agent/chat", async (req, res) => {
@@ -19448,7 +19774,7 @@ function registerOsRoutes(ctx) {
19448
19774
  });
19449
19775
  } catch (err) {
19450
19776
  console.error(err);
19451
- res.status(500).json({ success: false, error: err.message });
19777
+ sendSafeError(res, err);
19452
19778
  }
19453
19779
  });
19454
19780
  app.get("/api/agent/conversations", (req, res) => {
@@ -19468,7 +19794,7 @@ function registerOsRoutes(ctx) {
19468
19794
  messages: rows.map((r) => ({ role: r.role, content: r.content, createdAt: r.created_at }))
19469
19795
  });
19470
19796
  } catch (e) {
19471
- res.status(500).json({ error: e.message });
19797
+ sendSafeError(res, e);
19472
19798
  }
19473
19799
  });
19474
19800
  app.delete("/api/agent/conversations", (req, res) => {
@@ -19484,34 +19810,20 @@ function registerOsRoutes(ctx) {
19484
19810
  kernelContainer.db.prepare("DELETE FROM agent_conversations WHERE conv_key = ?").run(convKey);
19485
19811
  res.json({ success: true });
19486
19812
  } catch (e) {
19487
- res.status(500).json({ error: e.message });
19813
+ sendSafeError(res, e);
19488
19814
  }
19489
19815
  });
19490
19816
  }
19491
19817
 
19492
19818
  // server/routes/resources.ts
19493
19819
  function registerResourcesRoutes(ctx) {
19494
- const {
19495
- app,
19496
- io,
19497
- loginLimiter,
19498
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19499
- lessonActiveSegments: lessonActiveSegments2,
19500
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19501
- buildAgentFinalMessage: buildAgentFinalMessage2,
19502
- normalizeToolSchema: normalizeToolSchema2,
19503
- buildOpenAITools: buildOpenAITools2,
19504
- executeAgentToolCall: executeAgentToolCall2,
19505
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19506
- runGeminiAgentChat: runGeminiAgentChat2,
19507
- runOpenAIAgentChat: runOpenAIAgentChat2
19508
- } = ctx;
19820
+ const { app } = ctx;
19509
19821
  app.get("/api/resources", (req, res) => {
19510
19822
  try {
19511
19823
  const resources = kernelContainer.db.prepare("SELECT id, name, type, created_at FROM system_resources ORDER BY created_at DESC").all();
19512
19824
  res.json(resources);
19513
19825
  } catch (e) {
19514
- res.status(500).json({ error: e.message });
19826
+ sendSafeError(res, e);
19515
19827
  }
19516
19828
  });
19517
19829
  app.get("/api/resources/:id", (req, res) => {
@@ -19526,6 +19838,7 @@ function registerResourcesRoutes(ctx) {
19526
19838
  ).run(resource.id, resource.id, resource.name, "html", "index.html", resource.created_at || Date.now());
19527
19839
  }
19528
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:;");
19529
19842
  let html2 = resource.content || "";
19530
19843
  const baseTag2 = `<base href="/api/resources/${req.params.id}/">`;
19531
19844
  if (html2.toLowerCase().includes("<head>")) {
@@ -19570,7 +19883,7 @@ function registerResourcesRoutes(ctx) {
19570
19883
  html = injectLmsSdk(html, req, { id: resource.id, name: resource.name, uuid: resource.id });
19571
19884
  return res.send(html);
19572
19885
  } catch (e) {
19573
- res.status(500).send(e.message);
19886
+ sendSafeError(res, e);
19574
19887
  }
19575
19888
  });
19576
19889
  app.get("/api/resources/:id/*", (req, res) => {
@@ -19656,15 +19969,16 @@ function registerResourcesRoutes(ctx) {
19656
19969
  } else {
19657
19970
  let content = fileObj.content;
19658
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:;");
19659
19973
  content = injectLmsSdk(content, req, { id: resource.id, name: resource.name, uuid: resource.id });
19660
19974
  }
19661
19975
  return res.send(content);
19662
19976
  }
19663
19977
  } catch (e) {
19664
- res.status(500).send(e.message);
19978
+ sendSafeError(res, e);
19665
19979
  }
19666
19980
  });
19667
- app.post("/api/resources", async (req, res) => {
19981
+ app.post("/api/resources", requireAuth("teacher", "administrator"), async (req, res) => {
19668
19982
  try {
19669
19983
  const { name, type, content } = req.body;
19670
19984
  if (!name || !type) {
@@ -19721,15 +20035,15 @@ function registerResourcesRoutes(ctx) {
19721
20035
  }
19722
20036
  res.json({ success: true, id, name, type });
19723
20037
  } catch (e) {
19724
- res.status(500).json({ error: e.message });
20038
+ sendSafeError(res, e);
19725
20039
  }
19726
20040
  });
19727
- app.delete("/api/resources/:id", (req, res) => {
20041
+ app.delete("/api/resources/:id", requireAuth("teacher", "administrator"), (req, res) => {
19728
20042
  try {
19729
20043
  kernelContainer.db.prepare("DELETE FROM system_resources WHERE id = ?").run(req.params.id);
19730
20044
  res.json({ success: true });
19731
20045
  } catch (e) {
19732
- res.status(500).json({ error: e.message });
20046
+ sendSafeError(res, e);
19733
20047
  }
19734
20048
  });
19735
20049
  const RESOURCE_HANDLERS = {
@@ -19779,61 +20093,51 @@ function registerResourcesRoutes(ctx) {
19779
20093
  }
19780
20094
 
19781
20095
  // server/routes/courseware.ts
19782
- var import_path9 = __toESM(require("path"), 1);
19783
- var import_fs9 = __toESM(require("fs"), 1);
20096
+ var import_path10 = __toESM(require("path"), 1);
20097
+ var import_fs10 = __toESM(require("fs"), 1);
19784
20098
  var import_crypto9 = __toESM(require("crypto"), 1);
19785
20099
  function registerCoursewareRoutes(ctx) {
19786
- const {
19787
- app,
19788
- io,
19789
- loginLimiter,
19790
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
19791
- lessonActiveSegments: lessonActiveSegments2,
19792
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
19793
- buildAgentFinalMessage: buildAgentFinalMessage2,
19794
- normalizeToolSchema: normalizeToolSchema2,
19795
- buildOpenAITools: buildOpenAITools2,
19796
- executeAgentToolCall: executeAgentToolCall2,
19797
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
19798
- runGeminiAgentChat: runGeminiAgentChat2,
19799
- runOpenAIAgentChat: runOpenAIAgentChat2
19800
- } = ctx;
19801
- 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) => {
19802
20102
  try {
19803
20103
  const { name, filename, base64Data } = req.body;
19804
- 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);
19805
20106
  const result = await kernelContainer.commandBus.execute(cmd);
19806
20107
  res.json(result);
19807
20108
  } catch (e) {
19808
- res.status(500).json({ error: e.message });
20109
+ sendSafeError(res, e);
19809
20110
  }
19810
20111
  });
19811
- app.post("/api/courseware/confirm", async (req, res) => {
20112
+ app.post("/api/courseware/confirm", requireAuth("teacher", "administrator"), async (req, res) => {
19812
20113
  try {
19813
20114
  const { uuid, name, entry } = req.body;
19814
- 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);
19815
20117
  const result = await kernelContainer.commandBus.execute(cmd);
19816
20118
  res.json(result);
19817
20119
  } catch (e) {
19818
- res.status(500).json({ error: e.message });
20120
+ sendSafeError(res, e);
19819
20121
  }
19820
20122
  });
19821
- app.get("/api/courseware", async (req, res) => {
20123
+ app.get("/api/courseware", requireAuth(), async (req, res) => {
19822
20124
  try {
19823
- const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, "teacher-demo");
20125
+ const actorId = getActorId(req) || "system";
20126
+ const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId);
19824
20127
  const result = await kernelContainer.commandBus.execute(cmd);
19825
20128
  res.json(result);
19826
20129
  } catch (e) {
19827
- res.status(500).json({ error: e.message });
20130
+ sendSafeError(res, e);
19828
20131
  }
19829
20132
  });
19830
- app.delete("/api/courseware/:id", async (req, res) => {
20133
+ app.delete("/api/courseware/:id", requireAuth("teacher", "administrator"), async (req, res) => {
19831
20134
  try {
19832
- 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);
19833
20137
  const result = await kernelContainer.commandBus.execute(cmd);
19834
20138
  res.json(result);
19835
20139
  } catch (e) {
19836
- res.status(500).json({ error: e.message });
20140
+ sendSafeError(res, e);
19837
20141
  }
19838
20142
  });
19839
20143
  function extractScoreCommentCompletion(payload) {
@@ -19926,6 +20230,19 @@ function registerCoursewareRoutes(ctx) {
19926
20230
  try {
19927
20231
  const { attemptId } = req.params;
19928
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
+ }
19929
20246
  const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
19930
20247
  kernelContainer.db.prepare(
19931
20248
  "INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
@@ -19979,15 +20296,36 @@ function registerCoursewareRoutes(ctx) {
19979
20296
  }
19980
20297
  }
19981
20298
  io.emit("courseware-attempt-updated", { attemptId, type: "log" });
20299
+ void kernelContainer.eventBus.publish({
20300
+ id: "evt_" + import_crypto9.default.randomBytes(8).toString("hex"),
20301
+ type: "courseware.event_logged",
20302
+ source: "builtin.courseware",
20303
+ payload: { attemptId, eventType, payload },
20304
+ timestamp: Date.now(),
20305
+ correlationId: attemptId
20306
+ });
19982
20307
  res.json({ success: true });
19983
20308
  } catch (e) {
19984
- res.status(500).json({ error: e.message });
20309
+ sendSafeError(res, e);
19985
20310
  }
19986
20311
  });
19987
20312
  app.post("/api/courseware/attempts/:attemptId/submit", async (req, res) => {
19988
20313
  try {
19989
20314
  const { attemptId } = req.params;
19990
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
+ }
19991
20329
  const extracted = extractScoreCommentCompletion({ ...req.body, ...extra });
19992
20330
  if (score === void 0 || score === null) score = extracted.score;
19993
20331
  if (comment === void 0 || comment === null) comment = extracted.comment;
@@ -20006,6 +20344,7 @@ function registerCoursewareRoutes(ctx) {
20006
20344
  parsedCompletion = num;
20007
20345
  }
20008
20346
  }
20347
+ const actorId = session.userId || getActorId(req) || "student";
20009
20348
  const cmd = kernelContainer.commandBus.createCommand("courseware.submit_attempt", {
20010
20349
  attemptId,
20011
20350
  score: parsedScore,
@@ -20013,12 +20352,12 @@ function registerCoursewareRoutes(ctx) {
20013
20352
  completion: parsedCompletion,
20014
20353
  status,
20015
20354
  extra
20016
- }, "student-demo");
20355
+ }, actorId);
20017
20356
  const result = await kernelContainer.commandBus.execute(cmd);
20018
20357
  io.emit("courseware-attempt-updated", { attemptId, type: "submit" });
20019
20358
  res.json(result);
20020
20359
  } catch (e) {
20021
- res.status(500).json({ error: e.message });
20360
+ sendSafeError(res, e);
20022
20361
  }
20023
20362
  });
20024
20363
  app.get("/api/courseware/attempts", (req, res) => {
@@ -20043,7 +20382,7 @@ function registerCoursewareRoutes(ctx) {
20043
20382
  `).all();
20044
20383
  res.json(rows);
20045
20384
  } catch (e) {
20046
- res.status(500).json({ error: e.message });
20385
+ sendSafeError(res, e);
20047
20386
  }
20048
20387
  });
20049
20388
  app.post("/api/courseware/debug", (req, res) => {
@@ -20051,12 +20390,12 @@ function registerCoursewareRoutes(ctx) {
20051
20390
  const { msg, url, student, courseware } = req.body;
20052
20391
  const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
20053
20392
  console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
20054
- const logFile2 = import_path9.default.join(process.cwd(), "client_debug.log");
20055
- 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}
20056
20395
  `);
20057
20396
  res.json({ success: true });
20058
20397
  } catch (e) {
20059
- res.status(500).json({ error: e.message });
20398
+ sendSafeError(res, e);
20060
20399
  }
20061
20400
  });
20062
20401
  app.get("/api/courseware/attempts/:attemptId/raw", async (req, res) => {
@@ -20066,7 +20405,28 @@ function registerCoursewareRoutes(ctx) {
20066
20405
  const result = await kernelContainer.commandBus.execute(cmd);
20067
20406
  res.json(result);
20068
20407
  } catch (e) {
20069
- res.status(500).json({ error: e.message });
20408
+ sendSafeError(res, e);
20409
+ }
20410
+ });
20411
+ app.get("/api/courseware/attempts/:attemptId/progress", (req, res) => {
20412
+ try {
20413
+ const { attemptId } = req.params;
20414
+ const result = kernelContainer.db.prepare(
20415
+ "SELECT score, comment, completion, extra_json FROM submission_result WHERE attempt_id = ?"
20416
+ ).get(attemptId);
20417
+ if (!result) {
20418
+ return res.json({ progress: null });
20419
+ }
20420
+ let extra = {};
20421
+ try {
20422
+ extra = JSON.parse(result.extra_json || "{}");
20423
+ } catch {
20424
+ }
20425
+ res.json({
20426
+ progress: { score: result.score, comment: result.comment, completion: result.completion, extra }
20427
+ });
20428
+ } catch (e) {
20429
+ sendSafeError(res, e);
20070
20430
  }
20071
20431
  });
20072
20432
  app.post("/api/courseware/attempts/:attemptId/promote", async (req, res) => {
@@ -20157,7 +20517,7 @@ function registerCoursewareRoutes(ctx) {
20157
20517
  });
20158
20518
  res.json({ success: true, assignmentId, score: finalScore });
20159
20519
  } catch (e) {
20160
- res.status(500).json({ error: e.message });
20520
+ sendSafeError(res, e);
20161
20521
  }
20162
20522
  });
20163
20523
  app.get("/api/courseware/:id", (req, res) => {
@@ -20174,30 +20534,16 @@ function registerCoursewareRoutes(ctx) {
20174
20534
  const html = injectLmsSdk(node.content || "", req, { id: node.id, name: node.name, uuid: node.id });
20175
20535
  res.send(html);
20176
20536
  } catch (e) {
20177
- res.status(500).send(e.message);
20537
+ sendSafeError(res, e);
20178
20538
  }
20179
20539
  });
20180
20540
  }
20181
20541
 
20182
20542
  // server/routes/bridge.ts
20183
- var import_path10 = __toESM(require("path"), 1);
20184
- var import_fs10 = __toESM(require("fs"), 1);
20543
+ var import_path11 = __toESM(require("path"), 1);
20544
+ var import_fs11 = __toESM(require("fs"), 1);
20185
20545
  function registerBridgeRoutes(ctx) {
20186
- const {
20187
- app,
20188
- io,
20189
- loginLimiter,
20190
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20191
- lessonActiveSegments: lessonActiveSegments2,
20192
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20193
- buildAgentFinalMessage: buildAgentFinalMessage2,
20194
- normalizeToolSchema: normalizeToolSchema2,
20195
- buildOpenAITools: buildOpenAITools2,
20196
- executeAgentToolCall: executeAgentToolCall2,
20197
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20198
- runGeminiAgentChat: runGeminiAgentChat2,
20199
- runOpenAIAgentChat: runOpenAIAgentChat2
20200
- } = ctx;
20546
+ const { app } = ctx;
20201
20547
  app.get("/bridge.js", (_req, res) => {
20202
20548
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
20203
20549
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -20218,23 +20564,23 @@ function registerBridgeRoutes(ctx) {
20218
20564
  if (!subpath || subpath === "") {
20219
20565
  subpath = courseware.entry;
20220
20566
  }
20221
- const storageDir = import_path10.default.resolve(process.cwd(), "storage", "courseware", uuid);
20222
- 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);
20223
20569
  if (!filePath.startsWith(storageDir)) {
20224
20570
  return res.status(403).send("Access denied");
20225
20571
  }
20226
- if (!import_fs10.default.existsSync(filePath)) {
20572
+ if (!import_fs11.default.existsSync(filePath)) {
20227
20573
  return res.status(404).send(`File not found: ${subpath}`);
20228
20574
  }
20229
- const stat = import_fs10.default.statSync(filePath);
20575
+ const stat = import_fs11.default.statSync(filePath);
20230
20576
  if (stat.isDirectory()) {
20231
- const indexHtml = import_path10.default.join(filePath, "index.html");
20232
- if (import_fs10.default.existsSync(indexHtml)) {
20577
+ const indexHtml = import_path11.default.join(filePath, "index.html");
20578
+ if (import_fs11.default.existsSync(indexHtml)) {
20233
20579
  return res.redirect(`/runtime/${uuid}/${subpath.endsWith("/") ? subpath : subpath + "/"}index.html`);
20234
20580
  }
20235
20581
  return res.status(404).send("Directory index not found");
20236
20582
  }
20237
- const ext = import_path10.default.extname(filePath).toLowerCase();
20583
+ const ext = import_path11.default.extname(filePath).toLowerCase();
20238
20584
  let contentType = "text/plain; charset=utf-8";
20239
20585
  if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
20240
20586
  else if (ext === ".css") contentType = "text/css; charset=utf-8";
@@ -20250,14 +20596,14 @@ function registerBridgeRoutes(ctx) {
20250
20596
  res.setHeader("Access-Control-Allow-Origin", "*");
20251
20597
  const isHtml = ext === ".html" || ext === ".htm";
20252
20598
  if (isHtml) {
20253
- let html = import_fs10.default.readFileSync(filePath, "utf8");
20599
+ let html = import_fs11.default.readFileSync(filePath, "utf8");
20254
20600
  html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
20255
20601
  return res.send(html);
20256
20602
  } else {
20257
20603
  return res.sendFile(filePath);
20258
20604
  }
20259
20605
  } catch (e) {
20260
- res.status(500).send(e.message);
20606
+ sendSafeError(res, e, 500, "Failed to load courseware runtime file");
20261
20607
  }
20262
20608
  });
20263
20609
  }
@@ -20265,27 +20611,89 @@ function registerBridgeRoutes(ctx) {
20265
20611
  // server/routes/lessons.ts
20266
20612
  var import_genai3 = require("@google/genai");
20267
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
+ }
20268
20689
  function registerLessonsRoutes(ctx) {
20269
- const {
20270
- app,
20271
- io,
20272
- loginLimiter,
20273
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20274
- lessonActiveSegments: lessonActiveSegments2,
20275
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20276
- buildAgentFinalMessage: buildAgentFinalMessage2,
20277
- normalizeToolSchema: normalizeToolSchema2,
20278
- buildOpenAITools: buildOpenAITools2,
20279
- executeAgentToolCall: executeAgentToolCall2,
20280
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20281
- runGeminiAgentChat: runGeminiAgentChat2,
20282
- runOpenAIAgentChat: runOpenAIAgentChat2
20283
- } = ctx;
20690
+ const { app, io } = ctx;
20284
20691
  app.get("/api/lessons", (req, res) => {
20285
20692
  const lessons = kernelContainer.db.prepare(`
20286
- SELECT l.*,
20693
+ SELECT l.*, u.name as creator_name,
20287
20694
  (SELECT COUNT(*) FROM student_lesson_progress WHERE lesson_id = l.id) as enrollment_count
20288
20695
  FROM lessons l
20696
+ LEFT JOIN users u ON l.creator_id = u.id
20289
20697
  ORDER BY l.created_at DESC
20290
20698
  `).all();
20291
20699
  res.json(lessons);
@@ -20301,7 +20709,7 @@ function registerLessonsRoutes(ctx) {
20301
20709
  `).all(lessonId);
20302
20710
  res.json(rows);
20303
20711
  } catch (err) {
20304
- res.status(500).json({ error: err.message });
20712
+ sendSafeError(res, err);
20305
20713
  }
20306
20714
  });
20307
20715
  app.get("/api/lessons/:lessonId/eval-grades", (req, res) => {
@@ -20345,7 +20753,7 @@ function registerLessonsRoutes(ctx) {
20345
20753
  }
20346
20754
  res.json(result);
20347
20755
  } catch (err) {
20348
- res.status(500).json({ error: err.message });
20756
+ sendSafeError(res, err);
20349
20757
  }
20350
20758
  });
20351
20759
  app.get("/api/eval-submissions/:submissionId/reviews", (req, res) => {
@@ -20359,7 +20767,7 @@ function registerLessonsRoutes(ctx) {
20359
20767
  `).all(submissionId);
20360
20768
  res.json(rows);
20361
20769
  } catch (err) {
20362
- res.status(500).json({ error: err.message });
20770
+ sendSafeError(res, err);
20363
20771
  }
20364
20772
  });
20365
20773
  app.get("/api/lessons/:lessonId/students/:studentId/eval-status", (req, res) => {
@@ -20396,43 +20804,55 @@ function registerLessonsRoutes(ctx) {
20396
20804
  grade
20397
20805
  });
20398
20806
  } catch (err) {
20399
- res.status(500).json({ error: err.message });
20807
+ sendSafeError(res, err);
20400
20808
  }
20401
20809
  });
20402
- app.post("/api/lessons", async (req, res) => {
20810
+ app.post("/api/lessons", requireAuth("teacher", "administrator"), async (req, res) => {
20403
20811
  try {
20404
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";
20405
20816
  const cmd = kernelContainer.commandBus.createCommand(
20406
20817
  "lesson.create",
20407
- { title, content },
20408
- "user-frontend",
20818
+ { title, content, creatorId },
20819
+ actorId,
20409
20820
  { approved: true }
20410
20821
  );
20411
20822
  const result = await kernelContainer.commandBus.execute(cmd);
20412
20823
  res.json({ success: true, result });
20413
20824
  } catch (e) {
20414
- res.status(500).json({ error: e.message });
20825
+ sendSafeError(res, e);
20415
20826
  }
20416
20827
  });
20417
- app.put("/api/lessons/:id/timeline", async (req, res) => {
20828
+ app.put("/api/lessons/:id/timeline", requireAuth("teacher", "administrator"), async (req, res) => {
20418
20829
  try {
20419
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
+ }
20420
20835
  const { timeline } = req.body;
20836
+ const actorId = getActorId(req) || "teacher";
20421
20837
  const cmd = kernelContainer.commandBus.createCommand(
20422
20838
  "lesson.update_timeline",
20423
20839
  { lessonId: id, timeline },
20424
- "user-frontend",
20840
+ actorId,
20425
20841
  { approved: true }
20426
20842
  );
20427
20843
  const result = await kernelContainer.commandBus.execute(cmd);
20428
20844
  res.json({ success: true, result });
20429
20845
  } catch (e) {
20430
- res.status(500).json({ error: e.message });
20846
+ sendSafeError(res, e);
20431
20847
  }
20432
20848
  });
20433
- app.put("/api/lessons/:id/progress-mode", async (req, res) => {
20849
+ app.put("/api/lessons/:id/progress-mode", requireAuth("teacher", "administrator"), async (req, res) => {
20434
20850
  try {
20435
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
+ }
20436
20856
  const { progressMode, progressConditions } = req.body;
20437
20857
  const conditionsStr = typeof progressConditions === "string" ? progressConditions : JSON.stringify(progressConditions || null);
20438
20858
  kernelContainer.db.prepare("UPDATE lessons SET progress_mode = ?, progress_conditions = ?, updated_at = ? WHERE id = ?").run(progressMode || "manual", conditionsStr, Date.now(), id);
@@ -20443,7 +20863,7 @@ function registerLessonsRoutes(ctx) {
20443
20863
  });
20444
20864
  res.json({ success: true });
20445
20865
  } catch (e) {
20446
- res.status(500).json({ error: e.message });
20866
+ sendSafeError(res, e);
20447
20867
  }
20448
20868
  });
20449
20869
  app.get("/api/lessons/:id/whiteboard", (req, res) => {
@@ -20469,14 +20889,13 @@ function registerLessonsRoutes(ctx) {
20469
20889
  }
20470
20890
  res.json(elements);
20471
20891
  });
20472
- app.post("/api/lessons/:id/whiteboard/reset", async (req, res) => {
20892
+ app.post("/api/lessons/:id/whiteboard/reset", requireWhiteboardWriteAccess(), async (req, res) => {
20473
20893
  try {
20474
20894
  const id = req.params.id;
20475
20895
  if (id.startsWith("assignment-")) {
20476
20896
  const deleteStmt = kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?");
20477
20897
  deleteStmt.run(id);
20478
- res.json({ success: true, message: "Assignment whiteboard reset to empty" });
20479
- return;
20898
+ return res.json({ success: true, message: "Assignment whiteboard reset to empty" });
20480
20899
  }
20481
20900
  const snapshotId = `snapshot-${id}`;
20482
20901
  const hasSnapshot = kernelContainer.db.prepare("SELECT count(*) as count FROM whiteboard_elements WHERE lesson_id = ?").get(snapshotId);
@@ -20499,94 +20918,77 @@ function registerLessonsRoutes(ctx) {
20499
20918
  res.json({ success: true, message: "Lesson whiteboard cleared (no snapshot)" });
20500
20919
  }
20501
20920
  } catch (e) {
20502
- res.status(500).json({ error: e.message });
20921
+ sendSafeError(res, e);
20503
20922
  }
20504
20923
  });
20505
- app.post("/api/lessons/:id/whiteboard", async (req, res) => {
20924
+ app.post("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
20506
20925
  try {
20507
20926
  const { id } = req.params;
20508
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20509
- return res.status(403).json({ error: "Forbidden: Only teachers can draw on classroom whiteboards" });
20510
- }
20511
20927
  const { type, data } = req.body;
20928
+ const actorId = getActorId(req) || "user-frontend";
20512
20929
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.draw", {
20513
20930
  lessonId: id,
20514
20931
  type,
20515
20932
  data: JSON.stringify(data)
20516
- }, "user-frontend", { approved: true });
20933
+ }, actorId, { approved: true });
20517
20934
  const result = await kernelContainer.commandBus.execute(cmd);
20518
20935
  res.json(result);
20519
20936
  } catch (e) {
20520
- res.status(500).json({ error: e.message });
20937
+ sendSafeError(res, e);
20521
20938
  }
20522
20939
  });
20523
- app.put("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
20940
+ app.put("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
20524
20941
  try {
20525
- const { id } = req.params;
20526
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20527
- return res.status(403).json({ error: "Forbidden: Only teachers can update elements on classroom whiteboards" });
20528
- }
20942
+ const { id, elementId } = req.params;
20529
20943
  const { data } = req.body;
20944
+ const actorId = getActorId(req) || "user-frontend";
20530
20945
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.update", {
20531
20946
  lessonId: id,
20532
- elementId: req.params.elementId,
20947
+ elementId,
20533
20948
  data: JSON.stringify(data)
20534
- }, "user-frontend", { approved: true });
20949
+ }, actorId, { approved: true });
20535
20950
  const result = await kernelContainer.commandBus.execute(cmd);
20536
20951
  res.json(result);
20537
20952
  } catch (e) {
20538
- res.status(500).json({ error: e.message });
20953
+ sendSafeError(res, e);
20539
20954
  }
20540
20955
  });
20541
- app.delete("/api/lessons/:id/whiteboard", async (req, res) => {
20956
+ app.delete("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
20542
20957
  try {
20543
20958
  const { id } = req.params;
20544
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20545
- return res.status(403).json({ error: "Forbidden: Only teachers can clear the classroom whiteboard" });
20546
- }
20959
+ const actorId = getActorId(req) || "user-frontend";
20547
20960
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.clear", {
20548
20961
  lessonId: id
20549
- }, "user-frontend", { approved: true });
20962
+ }, actorId, { approved: true });
20550
20963
  const result = await kernelContainer.commandBus.execute(cmd);
20551
20964
  res.json(result);
20552
20965
  } catch (e) {
20553
- res.status(500).json({ error: e.message });
20966
+ sendSafeError(res, e);
20554
20967
  }
20555
20968
  });
20556
- app.delete("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
20969
+ app.delete("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
20557
20970
  try {
20558
- const { id } = req.params;
20559
- if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
20560
- return res.status(403).json({ error: "Forbidden: Only teachers can delete elements from classroom whiteboards" });
20561
- }
20971
+ const { id, elementId } = req.params;
20972
+ const actorId = getActorId(req) || "user-frontend";
20562
20973
  const cmd = kernelContainer.commandBus.createCommand("whiteboard.delete", {
20563
20974
  lessonId: id,
20564
- elementId: req.params.elementId
20565
- }, "user-frontend", { approved: true });
20975
+ elementId
20976
+ }, actorId, { approved: true });
20566
20977
  const result = await kernelContainer.commandBus.execute(cmd);
20567
20978
  res.json(result);
20568
20979
  } catch (e) {
20569
- res.status(500).json({ error: e.message });
20980
+ sendSafeError(res, e);
20570
20981
  }
20571
20982
  });
20572
- 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) => {
20573
20984
  try {
20574
20985
  const { id: lessonId } = req.params;
20575
20986
  const { elementId, answer } = req.body;
20576
20987
  if (!elementId || answer === void 0) {
20577
20988
  return res.status(400).json({ error: "Missing elementId or answer" });
20578
20989
  }
20579
- const token = getCookieToken(req);
20580
- let studentId = "guest";
20581
- if (token) {
20582
- const sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
20583
- if (sessionRow) {
20584
- const session = JSON.parse(sessionRow.session_data);
20585
- if (session.role === "student") {
20586
- studentId = session.studentId || session.userId || "guest";
20587
- }
20588
- }
20589
- }
20990
+ const session = req.session;
20991
+ const studentId = session.studentId || session.userId || "guest";
20590
20992
  const row = kernelContainer.db.prepare(
20591
20993
  "SELECT data FROM whiteboard_elements WHERE id = ? AND lesson_id = ?"
20592
20994
  ).get(elementId, lessonId);
@@ -20610,15 +21012,12 @@ function registerLessonsRoutes(ctx) {
20610
21012
  io.to(`lesson-${lessonId}`).emit("whiteboard-sync", { type: "element-updated", elementId });
20611
21013
  res.json({ success: true, isCorrect, score, studentId });
20612
21014
  } catch (e) {
20613
- res.status(500).json({ error: e.message });
21015
+ sendSafeError(res, e);
20614
21016
  }
20615
21017
  });
20616
- app.get("/api/lessons/:id/quiz-submissions", (req, res) => {
21018
+ app.get("/api/lessons/:id/quiz-submissions", requireAuth("teacher", "administrator"), (req, res) => {
20617
21019
  try {
20618
21020
  const { id: lessonId } = req.params;
20619
- if (!checkIsTeacherOrAdmin(req)) {
20620
- return res.status(403).json({ error: "Forbidden" });
20621
- }
20622
21021
  const elements = kernelContainer.db.prepare(
20623
21022
  "SELECT id, type, data FROM whiteboard_elements WHERE lesson_id = ? AND type = ?"
20624
21023
  ).all(lessonId, "quiz");
@@ -20639,10 +21038,10 @@ function registerLessonsRoutes(ctx) {
20639
21038
  });
20640
21039
  res.json({ success: true, quizzes });
20641
21040
  } catch (e) {
20642
- res.status(500).json({ error: e.message });
21041
+ sendSafeError(res, e);
20643
21042
  }
20644
21043
  });
20645
- 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) => {
20646
21045
  try {
20647
21046
  const { elements } = req.body;
20648
21047
  const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
@@ -20671,12 +21070,16 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20671
21070
  res.json({ success: true, hint });
20672
21071
  } catch (e) {
20673
21072
  console.error("AI Tutor error:", e);
20674
- res.status(500).json({ error: e.message });
21073
+ sendSafeError(res, e);
20675
21074
  }
20676
21075
  });
20677
- app.delete("/api/lessons/:id", async (req, res) => {
21076
+ app.delete("/api/lessons/:id", requireAuth("teacher", "administrator"), async (req, res) => {
20678
21077
  try {
20679
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
+ }
20680
21083
  kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?").run(id);
20681
21084
  kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE lesson_id = ?").run(id);
20682
21085
  kernelContainer.db.prepare("DELETE FROM schedules WHERE lesson_id = ?").run(id);
@@ -20687,7 +21090,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20687
21090
  }
20688
21091
  res.json({ success: true });
20689
21092
  } catch (e) {
20690
- res.status(500).json({ error: e.message });
21093
+ sendSafeError(res, e);
20691
21094
  }
20692
21095
  });
20693
21096
  app.get("/api/lessons/:id/stats", (req, res) => {
@@ -20707,22 +21110,24 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20707
21110
  ).get(id).count;
20708
21111
  res.json({ whiteboardCount, scheduleCount, enrollmentCount, assignmentCount });
20709
21112
  } catch (e) {
20710
- res.status(500).json({ error: e.message });
21113
+ sendSafeError(res, e);
20711
21114
  }
20712
21115
  });
20713
- app.post("/api/lessons/:id/clone", async (req, res) => {
21116
+ app.post("/api/lessons/:id/clone", requireAuth("teacher", "administrator"), async (req, res) => {
20714
21117
  try {
20715
21118
  const { id } = req.params;
20716
21119
  const original = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
20717
21120
  if (!original) {
20718
21121
  return res.status(404).json({ error: "Lesson not found" });
20719
21122
  }
21123
+ const session = req.session;
21124
+ const creatorId = session?.userId || session?.username || "usr_teacher";
20720
21125
  const newId = (0, import_uuid16.v7)();
20721
21126
  const now = Date.now();
20722
21127
  const newTitle = `\u526F\u672C-${original.title}`;
20723
21128
  kernelContainer.db.prepare(
20724
- "INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
20725
- ).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);
20726
21131
  const whiteboardElements = kernelContainer.db.prepare(
20727
21132
  "SELECT * FROM whiteboard_elements WHERE lesson_id = ?"
20728
21133
  ).all(id);
@@ -20732,40 +21137,29 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
20732
21137
  for (const el of whiteboardElements) {
20733
21138
  insertElement.run((0, import_uuid16.v7)(), newId, el.type, el.data, now);
20734
21139
  }
20735
- const cloned = kernelContainer.db.prepare(
20736
- "SELECT l.*, 0 as enrollment_count FROM lessons l WHERE l.id = ?"
20737
- ).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);
20738
21146
  res.json({ success: true, lesson: cloned });
20739
21147
  } catch (e) {
20740
- res.status(500).json({ error: e.message });
21148
+ sendSafeError(res, e);
20741
21149
  }
20742
21150
  });
20743
21151
  }
20744
21152
 
20745
21153
  // server/routes/workspace.ts
20746
- var import_path11 = __toESM(require("path"), 1);
21154
+ var import_path12 = __toESM(require("path"), 1);
20747
21155
  function registerWorkspaceRoutes(ctx) {
20748
- const {
20749
- app,
20750
- io,
20751
- loginLimiter,
20752
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20753
- lessonActiveSegments: lessonActiveSegments2,
20754
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20755
- buildAgentFinalMessage: buildAgentFinalMessage2,
20756
- normalizeToolSchema: normalizeToolSchema2,
20757
- buildOpenAITools: buildOpenAITools2,
20758
- executeAgentToolCall: executeAgentToolCall2,
20759
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20760
- runGeminiAgentChat: runGeminiAgentChat2,
20761
- runOpenAIAgentChat: runOpenAIAgentChat2
20762
- } = ctx;
20763
- 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) => {
20764
21158
  try {
20765
21159
  const events = kernelContainer.db.prepare("SELECT * FROM events ORDER BY timestamp DESC LIMIT 50").all();
20766
21160
  res.json(events);
20767
21161
  } catch (e) {
20768
- res.status(500).json({ error: e.message });
21162
+ sendSafeError(res, e);
20769
21163
  }
20770
21164
  });
20771
21165
  app.get("/api/mfe/remotes", (req, res) => {
@@ -20797,12 +21191,15 @@ function registerWorkspaceRoutes(ctx) {
20797
21191
  MF_REMOTE_CACHE2.set(name, result);
20798
21192
  res.json({ success: true, result });
20799
21193
  } catch (e) {
20800
- res.status(500).json({ success: false, error: e.message });
21194
+ sendSafeError(res, e);
20801
21195
  }
20802
21196
  });
20803
- app.get("/api/vfs", (req, res) => {
21197
+ app.get("/api/vfs", requireAuth(), (req, res) => {
20804
21198
  try {
20805
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);
20806
21203
  let nodes = [];
20807
21204
  if (parentId === "virtual-lessons") {
20808
21205
  const lessons = kernelContainer.db.prepare("SELECT id, title, content FROM lessons").all();
@@ -20811,12 +21208,23 @@ function registerWorkspaceRoutes(ctx) {
20811
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();
20812
21209
  nodes = assignments.map((a) => ({ id: `assgn-${a.id}`, parent_id: "virtual-assignments", type: "file", name: `[${a.cname}] ${a.title}.md`, content: a.content }));
20813
21210
  } else if (parentId === "virtual-submissions") {
20814
- const submissions = kernelContainer.db.prepare(`
20815
- SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
20816
- FROM assignment_submissions sub
20817
- JOIN assignments a ON sub.assignment_id = a.id
20818
- JOIN students s ON sub.student_id = s.id
20819
- `).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
+ }
20820
21228
  nodes = submissions.map((sub) => ({
20821
21229
  id: `sub-${sub.id}`,
20822
21230
  parent_id: "virtual-submissions",
@@ -20843,7 +21251,7 @@ ${sub.content}`
20843
21251
  }
20844
21252
  res.json(nodes);
20845
21253
  } catch (e) {
20846
- res.status(500).json({ error: e.message });
21254
+ sendSafeError(res, e);
20847
21255
  }
20848
21256
  });
20849
21257
  app.get("/files/*", (req, res) => {
@@ -20877,7 +21285,7 @@ ${sub.content}`
20877
21285
  }
20878
21286
  const filename = parts[parts.length - 1];
20879
21287
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
20880
- const ext = import_path11.default.extname(filename).toLowerCase();
21288
+ const ext = import_path12.default.extname(filename).toLowerCase();
20881
21289
  const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
20882
21290
  const content = foundNode.content || "";
20883
21291
  if (binaryExtensions.includes(ext)) {
@@ -20889,29 +21297,15 @@ ${sub.content}`
20889
21297
  }
20890
21298
  res.send(content);
20891
21299
  } catch (e) {
20892
- res.status(500).send(e.message);
21300
+ sendSafeError(res, e);
20893
21301
  }
20894
21302
  });
20895
21303
  }
20896
21304
 
20897
21305
  // server/routes/processes.ts
20898
21306
  function registerProcessesRoutes(ctx) {
20899
- const {
20900
- app,
20901
- io,
20902
- loginLimiter,
20903
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20904
- lessonActiveSegments: lessonActiveSegments2,
20905
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20906
- buildAgentFinalMessage: buildAgentFinalMessage2,
20907
- normalizeToolSchema: normalizeToolSchema2,
20908
- buildOpenAITools: buildOpenAITools2,
20909
- executeAgentToolCall: executeAgentToolCall2,
20910
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20911
- runGeminiAgentChat: runGeminiAgentChat2,
20912
- runOpenAIAgentChat: runOpenAIAgentChat2
20913
- } = ctx;
20914
- app.get("/api/approvals", (req, res) => {
21307
+ const { app } = ctx;
21308
+ app.get("/api/approvals", requireAuth("administrator"), (req, res) => {
20915
21309
  try {
20916
21310
  const list = kernelContainer.db.prepare("SELECT * FROM pending_commands ORDER BY created_at DESC").all();
20917
21311
  res.json(list);
@@ -20919,71 +21313,54 @@ function registerProcessesRoutes(ctx) {
20919
21313
  res.status(500).json({ error: e.message });
20920
21314
  }
20921
21315
  });
20922
- app.post("/api/approvals/:id/approve", async (req, res) => {
21316
+ app.post("/api/approvals/:id/approve", requireAuth("administrator"), async (req, res) => {
20923
21317
  try {
20924
21318
  const pending = kernelContainer.db.prepare("SELECT * FROM pending_commands WHERE id = ?").get(req.params.id);
20925
21319
  if (!pending) return res.status(404).json({ error: "Not found" });
20926
- let payload = JSON.parse(pending.payload);
20927
- if (req.body && req.body.payloadOverride) {
20928
- payload = { ...payload, ...req.body.payloadOverride };
20929
- }
21320
+ const payload = JSON.parse(pending.payload);
21321
+ const approverId = getActorId(req) || "admin";
20930
21322
  const cmd = kernelContainer.commandBus.createCommand(
20931
21323
  pending.command_type,
20932
21324
  payload,
20933
- pending.actor_id,
21325
+ approverId,
20934
21326
  { approved: true }
20935
- // Bypass high risk check now
20936
21327
  );
20937
21328
  const result = await kernelContainer.commandBus.execute(cmd);
20938
21329
  kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(pending.id);
20939
21330
  res.json({ success: true, result });
20940
21331
  } catch (e) {
20941
- res.status(500).json({ error: e.message });
21332
+ sendSafeError(res, e);
20942
21333
  }
20943
21334
  });
20944
- app.post("/api/approvals/:id/reject", async (req, res) => {
21335
+ app.post("/api/approvals/:id/reject", requireAuth("administrator"), async (req, res) => {
20945
21336
  try {
20946
21337
  kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(req.params.id);
20947
21338
  res.json({ success: true });
20948
21339
  } catch (e) {
20949
- res.status(500).json({ error: e.message });
21340
+ sendSafeError(res, e);
20950
21341
  }
20951
21342
  });
20952
- app.get("/api/processes", (req, res) => {
21343
+ app.get("/api/processes", requireAuth("administrator"), (req, res) => {
20953
21344
  try {
20954
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();
20955
21346
  res.json(list);
20956
21347
  } catch (e) {
20957
- res.status(500).json({ error: e.message });
21348
+ sendSafeError(res, e);
20958
21349
  }
20959
21350
  });
20960
- app.get("/api/processes/:id/logs", (req, res) => {
21351
+ app.get("/api/processes/:id/logs", requireAuth("administrator"), (req, res) => {
20961
21352
  try {
20962
21353
  const dbRow = kernelContainer.db.prepare("SELECT logs FROM processes WHERE id = ?").get(req.params.id);
20963
21354
  res.json(dbRow || { logs: "" });
20964
21355
  } catch (e) {
20965
- res.status(500).json({ error: e.message });
21356
+ sendSafeError(res, e);
20966
21357
  }
20967
21358
  });
20968
21359
  }
20969
21360
 
20970
21361
  // server/routes/admin.ts
20971
21362
  function registerAdminRoutes(ctx) {
20972
- const {
20973
- app,
20974
- io,
20975
- loginLimiter,
20976
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
20977
- lessonActiveSegments: lessonActiveSegments2,
20978
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
20979
- buildAgentFinalMessage: buildAgentFinalMessage2,
20980
- normalizeToolSchema: normalizeToolSchema2,
20981
- buildOpenAITools: buildOpenAITools2,
20982
- executeAgentToolCall: executeAgentToolCall2,
20983
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
20984
- runGeminiAgentChat: runGeminiAgentChat2,
20985
- runOpenAIAgentChat: runOpenAIAgentChat2
20986
- } = ctx;
21363
+ const { app } = ctx;
20987
21364
  app.post("/api/admin/seed-demo", requireAuth("administrator"), (req, res) => {
20988
21365
  try {
20989
21366
  const db2 = kernelContainer.db;
@@ -21047,7 +21424,7 @@ function registerAdminRoutes(ctx) {
21047
21424
  }
21048
21425
  res.json({ success: true, classId: DEMO_CLASS_ID, scheduleId: DEMO_SCHEDULE_ID, lessonId });
21049
21426
  } catch (e) {
21050
- res.status(500).json({ error: e.message });
21427
+ sendSafeError(res, e);
21051
21428
  }
21052
21429
  });
21053
21430
  app.post("/api/classes/import", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21097,7 +21474,7 @@ function registerAdminRoutes(ctx) {
21097
21474
  }
21098
21475
  res.json({ success: true, imported });
21099
21476
  } catch (e) {
21100
- res.status(500).json({ error: e.message });
21477
+ sendSafeError(res, e);
21101
21478
  }
21102
21479
  });
21103
21480
  app.post("/api/students/import", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21133,32 +21510,18 @@ function registerAdminRoutes(ctx) {
21133
21510
  }
21134
21511
  res.json({ success: true, imported });
21135
21512
  } catch (e) {
21136
- res.status(500).json({ error: e.message });
21513
+ sendSafeError(res, e);
21137
21514
  }
21138
21515
  });
21139
21516
  }
21140
21517
 
21141
21518
  // server/routes/roster.ts
21142
- var import_path12 = __toESM(require("path"), 1);
21143
- var import_fs11 = __toESM(require("fs"), 1);
21519
+ var import_path13 = __toESM(require("path"), 1);
21520
+ var import_fs12 = __toESM(require("fs"), 1);
21144
21521
  var import_crypto10 = __toESM(require("crypto"), 1);
21145
21522
  var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
21146
21523
  function registerRosterRoutes(ctx) {
21147
- const {
21148
- app,
21149
- io,
21150
- loginLimiter,
21151
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
21152
- lessonActiveSegments: lessonActiveSegments2,
21153
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
21154
- buildAgentFinalMessage: buildAgentFinalMessage2,
21155
- normalizeToolSchema: normalizeToolSchema2,
21156
- buildOpenAITools: buildOpenAITools2,
21157
- executeAgentToolCall: executeAgentToolCall2,
21158
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
21159
- runGeminiAgentChat: runGeminiAgentChat2,
21160
- runOpenAIAgentChat: runOpenAIAgentChat2
21161
- } = ctx;
21524
+ const { app, io, loginLimiter } = ctx;
21162
21525
  app.get("/api/classes", (req, res) => {
21163
21526
  try {
21164
21527
  const classes = kernelContainer.db.prepare(`
@@ -21192,10 +21555,10 @@ function registerRosterRoutes(ctx) {
21192
21555
  `).all(req.params.id);
21193
21556
  res.json(students);
21194
21557
  } catch (e) {
21195
- res.status(500).json({ error: e.message });
21558
+ sendSafeError(res, e);
21196
21559
  }
21197
21560
  });
21198
- app.post("/api/classes", (req, res) => {
21561
+ app.post("/api/classes", requireAuth("teacher", "administrator"), (req, res) => {
21199
21562
  try {
21200
21563
  const { name, description } = req.body;
21201
21564
  const classId = Math.random().toString(36).slice(2);
@@ -21207,10 +21570,10 @@ function registerRosterRoutes(ctx) {
21207
21570
  );
21208
21571
  res.json({ success: true, id: classId });
21209
21572
  } catch (e) {
21210
- res.status(500).json({ error: e.message });
21573
+ sendSafeError(res, e);
21211
21574
  }
21212
21575
  });
21213
- app.put("/api/classes/:id", (req, res) => {
21576
+ app.put("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
21214
21577
  try {
21215
21578
  const { name, description, class_passcode } = req.body;
21216
21579
  if (name) kernelContainer.db.prepare("UPDATE classes SET name = ? WHERE id = ?").run(name, req.params.id);
@@ -21218,10 +21581,10 @@ function registerRosterRoutes(ctx) {
21218
21581
  if (class_passcode !== void 0) kernelContainer.db.prepare("UPDATE classes SET class_passcode = ? WHERE id = ?").run(class_passcode, req.params.id);
21219
21582
  res.json({ success: true });
21220
21583
  } catch (e) {
21221
- res.status(500).json({ error: e.message });
21584
+ sendSafeError(res, e);
21222
21585
  }
21223
21586
  });
21224
- app.delete("/api/classes/:id", (req, res) => {
21587
+ app.delete("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
21225
21588
  try {
21226
21589
  const classId = req.params.id;
21227
21590
  const db2 = kernelContainer.db;
@@ -21263,7 +21626,7 @@ function registerRosterRoutes(ctx) {
21263
21626
  deleteTransaction();
21264
21627
  res.json({ success: true });
21265
21628
  } catch (e) {
21266
- res.status(500).json({ error: e.message });
21629
+ sendSafeError(res, e);
21267
21630
  }
21268
21631
  });
21269
21632
  app.get("/api/db-status", (req, res) => {
@@ -21328,7 +21691,8 @@ function registerRosterRoutes(ctx) {
21328
21691
  }
21329
21692
  return res.status(500).json({ status: "disconnected", error: "Unexpected response from SQLite" });
21330
21693
  } catch (e) {
21331
- 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 });
21332
21696
  }
21333
21697
  });
21334
21698
  app.get("/api/auth/session", (req, res) => {
@@ -21343,7 +21707,7 @@ function registerRosterRoutes(ctx) {
21343
21707
  }
21344
21708
  res.json({ session });
21345
21709
  } catch (e) {
21346
- res.status(500).json({ error: e.message });
21710
+ sendSafeError(res, e);
21347
21711
  }
21348
21712
  });
21349
21713
  app.post("/api/auth/logout", (req, res) => {
@@ -21352,10 +21716,12 @@ function registerRosterRoutes(ctx) {
21352
21716
  if (token) {
21353
21717
  kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
21354
21718
  }
21355
- 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}`);
21356
21722
  res.json({ success: true });
21357
21723
  } catch (e) {
21358
- res.status(500).json({ error: e.message });
21724
+ sendSafeError(res, e);
21359
21725
  }
21360
21726
  });
21361
21727
  app.post("/api/auth/change-password", (req, res) => {
@@ -21414,7 +21780,7 @@ function registerRosterRoutes(ctx) {
21414
21780
  }
21415
21781
  res.status(400).json({ error: "Unsupported role" });
21416
21782
  } catch (e) {
21417
- res.status(500).json({ error: e.message });
21783
+ sendSafeError(res, e);
21418
21784
  }
21419
21785
  });
21420
21786
  app.post("/api/auth/profile", (req, res) => {
@@ -21445,7 +21811,7 @@ function registerRosterRoutes(ctx) {
21445
21811
  }
21446
21812
  res.json({ success: true, name });
21447
21813
  } catch (e) {
21448
- res.status(500).json({ error: e.message });
21814
+ sendSafeError(res, e);
21449
21815
  }
21450
21816
  });
21451
21817
  app.post("/api/auth/avatar", (req, res) => {
@@ -21458,7 +21824,7 @@ function registerRosterRoutes(ctx) {
21458
21824
  if (!filename || !base64Data) {
21459
21825
  return res.status(400).json({ error: "Filename and base64Data are required" });
21460
21826
  }
21461
- const ext = import_path12.default.extname(filename).toLowerCase();
21827
+ const ext = import_path13.default.extname(filename).toLowerCase();
21462
21828
  const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
21463
21829
  if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
21464
21830
  return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
@@ -21471,11 +21837,11 @@ function registerRosterRoutes(ctx) {
21471
21837
  if (!validateMagicBytes(fileBuffer, filename)) {
21472
21838
  return res.status(400).json({ error: "File content does not match the declared image type" });
21473
21839
  }
21474
- const avatarDir = import_path12.default.join(process.cwd(), "uploads", "avatars");
21475
- 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 });
21476
21842
  const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
21477
- const filePath = import_path12.default.join(avatarDir, uniqueName);
21478
- import_fs11.default.writeFileSync(filePath, fileBuffer);
21843
+ const filePath = import_path13.default.join(avatarDir, uniqueName);
21844
+ import_fs12.default.writeFileSync(filePath, fileBuffer);
21479
21845
  const avatarUrl = `/uploads/avatars/${uniqueName}`;
21480
21846
  let oldAvatar = null;
21481
21847
  if (session.role === "student") {
@@ -21489,7 +21855,7 @@ function registerRosterRoutes(ctx) {
21489
21855
  }
21490
21856
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
21491
21857
  try {
21492
- import_fs11.default.unlinkSync(import_path12.default.join(process.cwd(), oldAvatar));
21858
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
21493
21859
  } catch {
21494
21860
  }
21495
21861
  }
@@ -21504,7 +21870,7 @@ function registerRosterRoutes(ctx) {
21504
21870
  }
21505
21871
  res.json({ success: true, avatar: avatarUrl });
21506
21872
  } catch (e) {
21507
- res.status(500).json({ error: e.message });
21873
+ sendSafeError(res, e);
21508
21874
  }
21509
21875
  });
21510
21876
  app.delete("/api/auth/avatar", (req, res) => {
@@ -21525,7 +21891,7 @@ function registerRosterRoutes(ctx) {
21525
21891
  }
21526
21892
  if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
21527
21893
  try {
21528
- import_fs11.default.unlinkSync(import_path12.default.join(process.cwd(), oldAvatar));
21894
+ import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
21529
21895
  } catch {
21530
21896
  }
21531
21897
  }
@@ -21540,7 +21906,7 @@ function registerRosterRoutes(ctx) {
21540
21906
  }
21541
21907
  res.json({ success: true });
21542
21908
  } catch (e) {
21543
- res.status(500).json({ error: e.message });
21909
+ sendSafeError(res, e);
21544
21910
  }
21545
21911
  });
21546
21912
  app.post("/api/auth/login", loginLimiter, (req, res) => {
@@ -21635,7 +22001,10 @@ function registerRosterRoutes(ctx) {
21635
22001
  const now = Date.now();
21636
22002
  const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
21637
22003
  kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
21638
- 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}`);
21639
22008
  return res.json({
21640
22009
  success: true,
21641
22010
  session: sessionData
@@ -21643,7 +22012,7 @@ function registerRosterRoutes(ctx) {
21643
22012
  }
21644
22013
  res.status(400).json({ error: "Unsupported entry type" });
21645
22014
  } catch (e) {
21646
- res.status(500).json({ error: e.message });
22015
+ sendSafeError(res, e);
21647
22016
  }
21648
22017
  });
21649
22018
  app.get("/api/students/:id/read_notifications", (req, res) => {
@@ -21651,10 +22020,10 @@ function registerRosterRoutes(ctx) {
21651
22020
  const rows = kernelContainer.db.prepare("SELECT notification_id FROM student_read_notifications WHERE student_id = ?").all(req.params.id);
21652
22021
  res.json(rows.map((r) => r.notification_id));
21653
22022
  } catch (e) {
21654
- res.status(500).json({ error: e.message });
22023
+ sendSafeError(res, e);
21655
22024
  }
21656
22025
  });
21657
- app.post("/api/students/:id/read_notifications", (req, res) => {
22026
+ app.post("/api/students/:id/read_notifications", requireAuth(), (req, res) => {
21658
22027
  try {
21659
22028
  const { notificationId } = req.body;
21660
22029
  if (!notificationId) {
@@ -21667,10 +22036,10 @@ function registerRosterRoutes(ctx) {
21667
22036
  });
21668
22037
  res.json({ success: true });
21669
22038
  } catch (e) {
21670
- res.status(500).json({ error: e.message });
22039
+ sendSafeError(res, e);
21671
22040
  }
21672
22041
  });
21673
- app.post("/api/classes/:classId/lock_lesson", (req, res) => {
22042
+ app.post("/api/classes/:classId/lock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
21674
22043
  try {
21675
22044
  const { lessonId } = req.body;
21676
22045
  if (!lessonId) {
@@ -21687,7 +22056,7 @@ function registerRosterRoutes(ctx) {
21687
22056
  res.status(550).json({ error: e.message });
21688
22057
  }
21689
22058
  });
21690
- app.post("/api/classes/:classId/unlock_lesson", (req, res) => {
22059
+ app.post("/api/classes/:classId/unlock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
21691
22060
  try {
21692
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);
21693
22062
  io.emit("class-lock-status-changed", {
@@ -21757,7 +22126,7 @@ function registerRosterRoutes(ctx) {
21757
22126
  const labs = kernelContainer.db.prepare("SELECT * FROM computer_labs ORDER BY created_at DESC").all();
21758
22127
  res.json(labs);
21759
22128
  } catch (e) {
21760
- res.status(500).json({ error: e.message });
22129
+ sendSafeError(res, e);
21761
22130
  }
21762
22131
  });
21763
22132
  app.post("/api/labs", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21769,7 +22138,7 @@ function registerRosterRoutes(ctx) {
21769
22138
  ).run(id, room_number, parseInt(rows), parseInt(cols), Date.now());
21770
22139
  res.json({ success: true, id, room_number, rows, cols });
21771
22140
  } catch (e) {
21772
- res.status(500).json({ error: e.message });
22141
+ sendSafeError(res, e);
21773
22142
  }
21774
22143
  });
21775
22144
  app.put("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21780,7 +22149,7 @@ function registerRosterRoutes(ctx) {
21780
22149
  ).run(room_number, parseInt(rows), parseInt(cols), req.params.id);
21781
22150
  res.json({ success: true });
21782
22151
  } catch (e) {
21783
- res.status(500).json({ error: e.message });
22152
+ sendSafeError(res, e);
21784
22153
  }
21785
22154
  });
21786
22155
  app.delete("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21789,7 +22158,7 @@ function registerRosterRoutes(ctx) {
21789
22158
  kernelContainer.db.prepare("DELETE FROM student_seats WHERE lab_id = ?").run(req.params.id);
21790
22159
  res.json({ success: true });
21791
22160
  } catch (e) {
21792
- res.status(500).json({ error: e.message });
22161
+ sendSafeError(res, e);
21793
22162
  }
21794
22163
  });
21795
22164
  app.get("/api/classes/:classId/seats", (req, res) => {
@@ -21799,7 +22168,7 @@ function registerRosterRoutes(ctx) {
21799
22168
  const seats = kernelContainer.db.prepare("SELECT * FROM student_seats WHERE class_id = ?").all(req.params.classId);
21800
22169
  res.json({ lab_id: labId, seats });
21801
22170
  } catch (e) {
21802
- res.status(500).json({ error: e.message });
22171
+ sendSafeError(res, e);
21803
22172
  }
21804
22173
  });
21805
22174
  app.post("/api/classes/:classId/seats", requireAuth("teacher", "administrator"), (req, res) => {
@@ -21817,10 +22186,10 @@ function registerRosterRoutes(ctx) {
21817
22186
  }
21818
22187
  res.json({ success: true });
21819
22188
  } catch (e) {
21820
- res.status(500).json({ error: e.message });
22189
+ sendSafeError(res, e);
21821
22190
  }
21822
22191
  });
21823
- app.post("/api/students", (req, res) => {
22192
+ app.post("/api/students", requireAuth("teacher", "administrator"), (req, res) => {
21824
22193
  try {
21825
22194
  const { name, email, password, student_number } = req.body;
21826
22195
  const studentId = Math.random().toString(36).slice(2);
@@ -21839,10 +22208,10 @@ function registerRosterRoutes(ctx) {
21839
22208
  );
21840
22209
  res.json({ success: true, id: studentId, student_number: finalNum, tempPassword: password || "123456" });
21841
22210
  } catch (e) {
21842
- res.status(500).json({ error: e.message });
22211
+ sendSafeError(res, e);
21843
22212
  }
21844
22213
  });
21845
- app.put("/api/students/:id", (req, res) => {
22214
+ app.put("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
21846
22215
  try {
21847
22216
  const { name, email, password, locked_lesson_id, private_notes, student_number } = req.body;
21848
22217
  if (name) kernelContainer.db.prepare("UPDATE students SET name = ? WHERE id = ?").run(name, req.params.id);
@@ -21856,10 +22225,10 @@ function registerRosterRoutes(ctx) {
21856
22225
  if (student_number !== void 0) kernelContainer.db.prepare("UPDATE students SET student_number = ? WHERE id = ?").run(student_number, req.params.id);
21857
22226
  res.json({ success: true });
21858
22227
  } catch (e) {
21859
- res.status(500).json({ error: e.message });
22228
+ sendSafeError(res, e);
21860
22229
  }
21861
22230
  });
21862
- app.delete("/api/students/:id", (req, res) => {
22231
+ app.delete("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
21863
22232
  try {
21864
22233
  kernelContainer.db.prepare("DELETE FROM class_students WHERE student_id = ?").run(req.params.id);
21865
22234
  kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE student_id = ?").run(req.params.id);
@@ -21875,7 +22244,7 @@ function registerRosterRoutes(ctx) {
21875
22244
  kernelContainer.db.prepare("DELETE FROM students WHERE id = ?").run(req.params.id);
21876
22245
  res.json({ success: true });
21877
22246
  } catch (e) {
21878
- res.status(500).json({ error: e.message });
22247
+ sendSafeError(res, e);
21879
22248
  }
21880
22249
  });
21881
22250
  app.get("/api/students/:id/export", (req, res) => {
@@ -21912,14 +22281,11 @@ function registerRosterRoutes(ctx) {
21912
22281
  exportedAt: (/* @__PURE__ */ new Date()).toISOString()
21913
22282
  });
21914
22283
  } catch (e) {
21915
- res.status(500).json({ error: e.message });
22284
+ sendSafeError(res, e);
21916
22285
  }
21917
22286
  });
21918
- app.delete("/api/students/:id/gdpr-delete", (req, res) => {
22287
+ app.delete("/api/students/:id/gdpr-delete", requireAuth("teacher", "administrator"), (req, res) => {
21919
22288
  try {
21920
- if (!checkIsTeacherOrAdmin(req)) {
21921
- return res.status(403).json({ error: "Only teachers and administrators can perform GDPR deletion" });
21922
- }
21923
22289
  const studentId = req.params.id;
21924
22290
  const { confirm } = req.body;
21925
22291
  if (confirm !== true) {
@@ -21940,7 +22306,7 @@ function registerRosterRoutes(ctx) {
21940
22306
  console.log(`[GDPR] Complete data deletion for student ${studentId}`);
21941
22307
  res.json({ success: true, message: "All student data has been permanently deleted." });
21942
22308
  } catch (e) {
21943
- res.status(500).json({ error: e.message });
22309
+ sendSafeError(res, e);
21944
22310
  }
21945
22311
  });
21946
22312
  app.get("/api/students/:id/progress", (req, res) => {
@@ -21953,11 +22319,16 @@ function registerRosterRoutes(ctx) {
21953
22319
  `).all(req.params.id);
21954
22320
  res.json(progress);
21955
22321
  } catch (e) {
21956
- res.status(500).json({ error: e.message });
22322
+ sendSafeError(res, e);
21957
22323
  }
21958
22324
  });
21959
- app.post("/api/students/:id/progress", (req, res) => {
22325
+ app.post("/api/students/:id/progress", requireAuth(), (req, res) => {
21960
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
+ }
21961
22332
  const { lessonId, completed, progressPercent, completedSegments } = req.body;
21962
22333
  const completedSegmentsStr = typeof completedSegments === "string" ? completedSegments : JSON.stringify(completedSegments || []);
21963
22334
  kernelContainer.db.prepare(`
@@ -21984,10 +22355,10 @@ function registerRosterRoutes(ctx) {
21984
22355
  });
21985
22356
  res.json({ success: true });
21986
22357
  } catch (e) {
21987
- res.status(500).json({ error: e.message });
22358
+ sendSafeError(res, e);
21988
22359
  }
21989
22360
  });
21990
- app.post("/api/classes/:id/students", (req, res) => {
22361
+ app.post("/api/classes/:id/students", requireAuth("teacher", "administrator"), (req, res) => {
21991
22362
  try {
21992
22363
  const { studentId } = req.body;
21993
22364
  kernelContainer.db.prepare("INSERT OR IGNORE INTO class_students (class_id, student_id, joined_at) VALUES (?, ?, ?)").run(
@@ -21997,7 +22368,7 @@ function registerRosterRoutes(ctx) {
21997
22368
  );
21998
22369
  res.json({ success: true });
21999
22370
  } catch (e) {
22000
- res.status(500).json({ error: e.message });
22371
+ sendSafeError(res, e);
22001
22372
  }
22002
22373
  });
22003
22374
  app.post("/api/classes/:id/students/bulk-enroll", requireAuth("teacher", "administrator"), (req, res) => {
@@ -22039,7 +22410,7 @@ function registerRosterRoutes(ctx) {
22039
22410
  }
22040
22411
  res.json({ success: true, count: results.length, results });
22041
22412
  } catch (e) {
22042
- res.status(500).json({ error: e.message });
22413
+ sendSafeError(res, e);
22043
22414
  }
22044
22415
  });
22045
22416
  app.get("/api/classes/:id/progress", (req, res) => {
@@ -22054,7 +22425,7 @@ function registerRosterRoutes(ctx) {
22054
22425
  `).all(req.params.id);
22055
22426
  res.json(progress);
22056
22427
  } catch (e) {
22057
- res.status(500).json({ error: e.message });
22428
+ sendSafeError(res, e);
22058
22429
  }
22059
22430
  });
22060
22431
  app.get("/api/classes/:classId/lessons/:lessonId/progress", (req, res) => {
@@ -22074,15 +22445,15 @@ function registerRosterRoutes(ctx) {
22074
22445
  `).all(req.params.lessonId, req.params.lessonId, req.params.classId);
22075
22446
  res.json(progress);
22076
22447
  } catch (e) {
22077
- res.status(500).json({ error: e.message });
22448
+ sendSafeError(res, e);
22078
22449
  }
22079
22450
  });
22080
- app.delete("/api/classes/:classId/students/:studentId", (req, res) => {
22451
+ app.delete("/api/classes/:classId/students/:studentId", requireAuth("teacher", "administrator"), (req, res) => {
22081
22452
  try {
22082
22453
  kernelContainer.db.prepare("DELETE FROM class_students WHERE class_id = ? AND student_id = ?").run(req.params.classId, req.params.studentId);
22083
22454
  res.json({ success: true });
22084
22455
  } catch (e) {
22085
- res.status(500).json({ error: e.message });
22456
+ sendSafeError(res, e);
22086
22457
  }
22087
22458
  });
22088
22459
  app.get("/api/classes/:classId/dashboard", (req, res) => {
@@ -22125,11 +22496,16 @@ function registerRosterRoutes(ctx) {
22125
22496
  `).all(req.params.classId, req.params.classId);
22126
22497
  res.json({ assignments, recentSubmissions, performance: performance2, rollcallStats });
22127
22498
  } catch (e) {
22128
- res.status(500).json({ error: e.message });
22499
+ sendSafeError(res, e);
22129
22500
  }
22130
22501
  });
22131
- app.get("/api/students/:id/dashboard", (req, res) => {
22502
+ app.get("/api/students/:id/dashboard", requireAuth(), (req, res) => {
22132
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
+ }
22133
22509
  const studentId = req.params.id;
22134
22510
  const studentClasses = kernelContainer.db.prepare(`
22135
22511
  SELECT c.*
@@ -22204,7 +22580,7 @@ function registerRosterRoutes(ctx) {
22204
22580
  `).get(studentId);
22205
22581
  res.json({ classes: studentClasses, schedules, assignments, progress, rollcalls, profile });
22206
22582
  } catch (e) {
22207
- res.status(500).json({ error: e.message });
22583
+ sendSafeError(res, e);
22208
22584
  }
22209
22585
  });
22210
22586
  }
@@ -22212,30 +22588,16 @@ function registerRosterRoutes(ctx) {
22212
22588
  // server/routes/assignments.ts
22213
22589
  var import_genai4 = require("@google/genai");
22214
22590
  function registerAssignmentsRoutes(ctx) {
22215
- const {
22216
- app,
22217
- io,
22218
- loginLimiter,
22219
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22220
- lessonActiveSegments: lessonActiveSegments2,
22221
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22222
- buildAgentFinalMessage: buildAgentFinalMessage2,
22223
- normalizeToolSchema: normalizeToolSchema2,
22224
- buildOpenAITools: buildOpenAITools2,
22225
- executeAgentToolCall: executeAgentToolCall2,
22226
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22227
- runGeminiAgentChat: runGeminiAgentChat2,
22228
- runOpenAIAgentChat: runOpenAIAgentChat2
22229
- } = ctx;
22591
+ const { app } = ctx;
22230
22592
  app.get("/api/classes/:classId/assignments", (req, res) => {
22231
22593
  try {
22232
22594
  const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
22233
22595
  res.json(assignments);
22234
22596
  } catch (e) {
22235
- res.status(500).json({ error: e.message });
22597
+ sendSafeError(res, e);
22236
22598
  }
22237
22599
  });
22238
- app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
22600
+ app.post("/api/classes/:classId/assignments/generate", requireAuth("teacher", "administrator"), async (req, res) => {
22239
22601
  try {
22240
22602
  const { topic, lessonId } = req.body;
22241
22603
  const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
@@ -22260,10 +22622,10 @@ function registerAssignmentsRoutes(ctx) {
22260
22622
  );
22261
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 } });
22262
22624
  } catch (e) {
22263
- res.status(500).json({ error: e.message });
22625
+ sendSafeError(res, e);
22264
22626
  }
22265
22627
  });
22266
- app.post("/api/classes/:classId/assignments/suggest", async (req, res) => {
22628
+ app.post("/api/classes/:classId/assignments/suggest", requireAuth("teacher", "administrator"), async (req, res) => {
22267
22629
  try {
22268
22630
  const { lessonId } = req.body;
22269
22631
  const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
@@ -22327,10 +22689,10 @@ Generate the response in the specified JSON schema.`;
22327
22689
  const text = response.text || "{}";
22328
22690
  res.json(JSON.parse(text));
22329
22691
  } catch (e) {
22330
- res.status(500).json({ error: e.message });
22692
+ sendSafeError(res, e);
22331
22693
  }
22332
22694
  });
22333
- 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) => {
22334
22696
  try {
22335
22697
  const { title, description, questions, learningObjectives, timeLimit, lessonId } = req.body;
22336
22698
  const id = "ast-" + Date.now().toString(36);
@@ -22351,12 +22713,17 @@ Generate the response in the specified JSON schema.`;
22351
22713
  );
22352
22714
  res.json({ success: true, assignmentId: id });
22353
22715
  } catch (e) {
22354
- res.status(500).json({ error: e.message });
22716
+ sendSafeError(res, e);
22355
22717
  }
22356
22718
  });
22357
- app.post("/api/assignments/:id/submissions", (req, res) => {
22719
+ app.post("/api/assignments/:id/submissions", requireAuth(), (req, res) => {
22358
22720
  try {
22359
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
+ }
22360
22727
  kernelContainer.db.prepare(`
22361
22728
  INSERT INTO assignment_submissions (assignment_id, student_id, content, submitted_at, status)
22362
22729
  VALUES (?, ?, ?, ?, 'submitted')
@@ -22364,7 +22731,7 @@ Generate the response in the specified JSON schema.`;
22364
22731
  `).run(req.params.id, studentId, content, Date.now());
22365
22732
  res.json({ success: true });
22366
22733
  } catch (e) {
22367
- res.status(500).json({ error: e.message });
22734
+ sendSafeError(res, e);
22368
22735
  }
22369
22736
  });
22370
22737
  app.get("/api/assignments/:id/submissions", (req, res) => {
@@ -22378,10 +22745,10 @@ Generate the response in the specified JSON schema.`;
22378
22745
  `).all(req.params.id);
22379
22746
  res.json(submissions);
22380
22747
  } catch (e) {
22381
- res.status(500).json({ error: e.message });
22748
+ sendSafeError(res, e);
22382
22749
  }
22383
22750
  });
22384
- 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) => {
22385
22752
  try {
22386
22753
  const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
22387
22754
  const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
@@ -22463,7 +22830,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
22463
22830
  });
22464
22831
  res.json({ success: true, pendingApproval: true, message: "Grade generated and sent for approval.", score: grade.score, feedback: grade.feedback });
22465
22832
  } catch (e) {
22466
- res.status(500).json({ error: e.message });
22833
+ sendSafeError(res, e);
22467
22834
  }
22468
22835
  });
22469
22836
  }
@@ -22471,21 +22838,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
22471
22838
  // server/routes/schedules.ts
22472
22839
  var import_genai5 = require("@google/genai");
22473
22840
  function registerSchedulesRoutes(ctx) {
22474
- const {
22475
- app,
22476
- io,
22477
- loginLimiter,
22478
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22479
- lessonActiveSegments: lessonActiveSegments2,
22480
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22481
- buildAgentFinalMessage: buildAgentFinalMessage2,
22482
- normalizeToolSchema: normalizeToolSchema2,
22483
- buildOpenAITools: buildOpenAITools2,
22484
- executeAgentToolCall: executeAgentToolCall2,
22485
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22486
- runGeminiAgentChat: runGeminiAgentChat2,
22487
- runOpenAIAgentChat: runOpenAIAgentChat2
22488
- } = ctx;
22841
+ const { app } = ctx;
22489
22842
  app.get("/api/schedules/today", (req, res) => {
22490
22843
  try {
22491
22844
  const clientDate = req.query.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -22540,7 +22893,7 @@ function registerSchedulesRoutes(ctx) {
22540
22893
  res.status(500).json({ error: e.message });
22541
22894
  }
22542
22895
  });
22543
- app.post("/api/classes/:classId/schedules", (req, res) => {
22896
+ app.post("/api/classes/:classId/schedules", requireAuth("teacher", "administrator"), (req, res) => {
22544
22897
  try {
22545
22898
  const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
22546
22899
  const id = "sch-" + Date.now().toString(36);
@@ -22573,7 +22926,7 @@ function registerSchedulesRoutes(ctx) {
22573
22926
  res.status(500).json({ error: e.message });
22574
22927
  }
22575
22928
  });
22576
- app.put("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
22929
+ app.put("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
22577
22930
  try {
22578
22931
  const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
22579
22932
  kernelContainer.db.prepare(`
@@ -22594,7 +22947,7 @@ function registerSchedulesRoutes(ctx) {
22594
22947
  res.status(500).json({ error: e.message });
22595
22948
  }
22596
22949
  });
22597
- app.delete("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
22950
+ app.delete("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
22598
22951
  try {
22599
22952
  kernelContainer.db.prepare("DELETE FROM schedules WHERE id = ? AND class_id = ?").run(req.params.scheduleId, req.params.classId);
22600
22953
  kernelContainer.db.prepare("DELETE FROM attendance WHERE schedule_id = ?").run(req.params.scheduleId);
@@ -22603,7 +22956,7 @@ function registerSchedulesRoutes(ctx) {
22603
22956
  res.status(500).json({ error: e.message });
22604
22957
  }
22605
22958
  });
22606
- app.post("/api/classes/:classId/schedules/batch", (req, res) => {
22959
+ app.post("/api/classes/:classId/schedules/batch", requireAuth("teacher", "administrator"), (req, res) => {
22607
22960
  try {
22608
22961
  const { schedules } = req.body;
22609
22962
  const db2 = kernelContainer.db;
@@ -22632,7 +22985,7 @@ function registerSchedulesRoutes(ctx) {
22632
22985
  res.status(500).json({ error: e.message });
22633
22986
  }
22634
22987
  });
22635
- app.post("/api/timetable/ocr", async (req, res) => {
22988
+ app.post("/api/timetable/ocr", requireAuth("teacher", "administrator"), async (req, res) => {
22636
22989
  const startTime = Date.now();
22637
22990
  console.log(`[OCR Start] Starting timetable OCR. Payload size: ${req.body.imageBase64?.length || 0} bytes. Lang: ${req.body.lang || "zh"}`);
22638
22991
  try {
@@ -22800,24 +23153,10 @@ function registerSchedulesRoutes(ctx) {
22800
23153
  }
22801
23154
 
22802
23155
  // server/routes/grading.ts
22803
- init_interfaces();
22804
23156
  var import_genai6 = require("@google/genai");
23157
+ init_interfaces();
22805
23158
  function registerGradingRoutes(ctx) {
22806
- const {
22807
- app,
22808
- io,
22809
- loginLimiter,
22810
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
22811
- lessonActiveSegments: lessonActiveSegments2,
22812
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
22813
- buildAgentFinalMessage: buildAgentFinalMessage2,
22814
- normalizeToolSchema: normalizeToolSchema2,
22815
- buildOpenAITools: buildOpenAITools2,
22816
- executeAgentToolCall: executeAgentToolCall2,
22817
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
22818
- runGeminiAgentChat: runGeminiAgentChat2,
22819
- runOpenAIAgentChat: runOpenAIAgentChat2
22820
- } = ctx;
23159
+ const { app } = ctx;
22821
23160
  app.get("/api/classes/:classId/attendance-summary", (req, res) => {
22822
23161
  try {
22823
23162
  const classId = req.params.classId;
@@ -22931,7 +23270,7 @@ function registerGradingRoutes(ctx) {
22931
23270
  res.status(500).json({ error: e.message });
22932
23271
  }
22933
23272
  });
22934
- app.post("/api/schedules/:scheduleId/attendance", (req, res) => {
23273
+ app.post("/api/schedules/:scheduleId/attendance", requireAuth("teacher", "administrator"), (req, res) => {
22935
23274
  try {
22936
23275
  const { studentId, status } = req.body;
22937
23276
  kernelContainer.db.prepare(`
@@ -22961,7 +23300,7 @@ function registerGradingRoutes(ctx) {
22961
23300
  res.status(500).json({ error: e.message });
22962
23301
  }
22963
23302
  });
22964
- app.post("/api/classes/:classId/grade-weights", (req, res) => {
23303
+ app.post("/api/classes/:classId/grade-weights", requireAuth("teacher", "administrator"), (req, res) => {
22965
23304
  try {
22966
23305
  const { attendance_weight, progress_weight, assignment_weight, exam_weight } = req.body;
22967
23306
  const total = Number(attendance_weight) + Number(progress_weight) + Number(assignment_weight) + Number(exam_weight);
@@ -22995,7 +23334,7 @@ function registerGradingRoutes(ctx) {
22995
23334
  res.status(500).json({ error: e.message });
22996
23335
  }
22997
23336
  });
22998
- app.post("/api/classes/:classId/exams", (req, res) => {
23337
+ app.post("/api/classes/:classId/exams", requireAuth("teacher", "administrator"), (req, res) => {
22999
23338
  try {
23000
23339
  const { title, description, max_score } = req.body;
23001
23340
  if (!title) return res.status(400).json({ error: "Title is required" });
@@ -23017,7 +23356,7 @@ function registerGradingRoutes(ctx) {
23017
23356
  res.status(500).json({ error: e.message });
23018
23357
  }
23019
23358
  });
23020
- app.post("/api/exams/:examId/scores", (req, res) => {
23359
+ app.post("/api/exams/:examId/scores", requireAuth("teacher", "administrator"), (req, res) => {
23021
23360
  try {
23022
23361
  const { scores } = req.body;
23023
23362
  if (!Array.isArray(scores)) return res.status(400).json({ error: "Scores array is required" });
@@ -23173,11 +23512,8 @@ function registerGradingRoutes(ctx) {
23173
23512
  res.status(500).json({ error: e.message });
23174
23513
  }
23175
23514
  });
23176
- app.post("/api/grade-sync", async (req, res) => {
23515
+ app.post("/api/grade-sync", requireAuth("teacher", "administrator"), async (req, res) => {
23177
23516
  try {
23178
- if (!checkIsTeacherOrAdmin(req)) {
23179
- return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
23180
- }
23181
23517
  const { lessonId, studentId, grade } = req.body;
23182
23518
  if (!lessonId || !studentId || grade === void 0) {
23183
23519
  return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
@@ -23198,11 +23534,8 @@ function registerGradingRoutes(ctx) {
23198
23534
  res.status(500).json({ success: false, error: e.message });
23199
23535
  }
23200
23536
  });
23201
- app.post("/api/students/:studentId/points", async (req, res) => {
23537
+ app.post("/api/students/:studentId/points", requireAuth("teacher", "administrator"), async (req, res) => {
23202
23538
  try {
23203
- if (!checkIsTeacherOrAdmin(req)) {
23204
- return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
23205
- }
23206
23539
  const { studentId } = req.params;
23207
23540
  const { classId, dimensionId, deltaPoints, reason, pluginId } = req.body;
23208
23541
  if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
@@ -23234,7 +23567,7 @@ function registerGradingRoutes(ctx) {
23234
23567
  res.status(500).json({ success: false, error: e.message });
23235
23568
  }
23236
23569
  });
23237
- app.post("/api/classes/:classId/semester-reports/archive", (req, res) => {
23570
+ app.post("/api/classes/:classId/semester-reports/archive", requireAuth("teacher", "administrator"), (req, res) => {
23238
23571
  try {
23239
23572
  const { semesterName, reports } = req.body;
23240
23573
  if (!semesterName) return res.status(400).json({ error: "semesterName is required" });
@@ -23287,7 +23620,7 @@ function registerGradingRoutes(ctx) {
23287
23620
  res.status(500).json({ error: e.message });
23288
23621
  }
23289
23622
  });
23290
- 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) => {
23291
23624
  try {
23292
23625
  const { classId, studentId } = req.params;
23293
23626
  const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\uFFFD?", providerId } = req.body;
@@ -23387,8 +23720,8 @@ ${examsText}
23387
23720
 
23388
23721
  // server/routes/plugins.ts
23389
23722
  var import_express2 = __toESM(require("express"), 1);
23390
- var import_path13 = __toESM(require("path"), 1);
23391
- var import_fs12 = __toESM(require("fs"), 1);
23723
+ var import_path14 = __toESM(require("path"), 1);
23724
+ var import_fs13 = __toESM(require("fs"), 1);
23392
23725
 
23393
23726
  // server/services/version-fetcher.ts
23394
23727
  var import_child_process2 = require("child_process");
@@ -23570,13 +23903,6 @@ function isSafeExternalUrl(urlStr) {
23570
23903
  function registerPluginsRoutes(ctx) {
23571
23904
  const {
23572
23905
  app,
23573
- io,
23574
- loginLimiter,
23575
- MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
23576
- lessonActiveSegments: lessonActiveSegments2,
23577
- buildAgentSystemInstruction: buildAgentSystemInstruction2,
23578
- buildAgentFinalMessage: buildAgentFinalMessage2,
23579
- normalizeToolSchema: normalizeToolSchema2,
23580
23906
  buildOpenAITools: buildOpenAITools2,
23581
23907
  executeAgentToolCall: executeAgentToolCall2,
23582
23908
  buildOpenAIChatUrl: buildOpenAIChatUrl2,
@@ -23585,29 +23911,26 @@ function registerPluginsRoutes(ctx) {
23585
23911
  } = ctx;
23586
23912
  app.get("/api/docs/plugin-guide", (req, res) => {
23587
23913
  try {
23588
- const docPath = import_path13.default.join(process.cwd(), "docs_plugin_guide.md");
23589
- 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)) {
23590
23916
  return res.status(404).json({ success: false, error: "Document not found" });
23591
23917
  }
23592
- const content = import_fs12.default.readFileSync(docPath, "utf-8");
23918
+ const content = import_fs13.default.readFileSync(docPath, "utf-8");
23593
23919
  res.json({ success: true, content });
23594
23920
  } catch (e) {
23595
- res.status(500).json({ success: false, error: e.message });
23921
+ sendSafeError(res, e);
23596
23922
  }
23597
23923
  });
23598
- app.get("/api/admin/logs", (req, res) => {
23599
- if (!checkIsTeacherOrAdmin(req)) {
23600
- return res.status(403).json({ success: false, error: "Access denied: teachers or admins only" });
23601
- }
23924
+ app.get("/api/admin/logs", requireAuth("administrator"), (req, res) => {
23602
23925
  try {
23603
23926
  const limit = parseInt(req.query.limit) || 200;
23604
23927
  const component = req.query.component;
23605
23928
  const levelFilter = req.query.level;
23606
- const logFile2 = import_path13.default.resolve(process.cwd(), "logs", "openlearn.log");
23607
- 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)) {
23608
23931
  return res.json({ success: true, logs: [] });
23609
23932
  }
23610
- const content = import_fs12.default.readFileSync(logFile2, "utf-8");
23933
+ const content = import_fs13.default.readFileSync(logFile2, "utf-8");
23611
23934
  const lines = content.split("\n").filter(Boolean);
23612
23935
  let parsedLogs = lines.map((line) => {
23613
23936
  try {
@@ -23638,7 +23961,7 @@ function registerPluginsRoutes(ctx) {
23638
23961
  const sliceStart = Math.max(0, parsedLogs.length - limit);
23639
23962
  res.json({ success: true, logs: parsedLogs.slice(sliceStart) });
23640
23963
  } catch (e) {
23641
- res.status(500).json({ success: false, error: e.message });
23964
+ sendSafeError(res, e);
23642
23965
  }
23643
23966
  });
23644
23967
  app.get("/api/plugins", (req, res) => {
@@ -23663,7 +23986,7 @@ function registerPluginsRoutes(ctx) {
23663
23986
  isSystem: manifestId.startsWith("@openlearn/") || found.pluginId.startsWith("@openlearn/")
23664
23987
  });
23665
23988
  } catch (e) {
23666
- res.status(500).json({ success: false, error: e.message });
23989
+ sendSafeError(res, e);
23667
23990
  }
23668
23991
  });
23669
23992
  app.get("/api/plugins/market", async (req, res) => {
@@ -23689,7 +24012,7 @@ function registerPluginsRoutes(ctx) {
23689
24012
  }
23690
24013
  res.json({ success: true, market: results });
23691
24014
  } catch (e) {
23692
- res.status(500).json({ success: false, error: e.message });
24015
+ sendSafeError(res, e);
23693
24016
  }
23694
24017
  });
23695
24018
  app.post("/api/plugins/:id(*)/check-update", async (req, res) => {
@@ -23711,7 +24034,7 @@ function registerPluginsRoutes(ctx) {
23711
24034
  const result = await checkVersion(src, manifest.version || "0.0.0");
23712
24035
  res.json({ success: true, ...result });
23713
24036
  } catch (e) {
23714
- res.status(500).json({ success: false, error: e.message });
24037
+ sendSafeError(res, e);
23715
24038
  }
23716
24039
  });
23717
24040
  app.post("/api/plugins/:id(*)/one-click-update", requireAuth("administrator"), async (req, res) => {
@@ -23750,9 +24073,9 @@ function registerPluginsRoutes(ctx) {
23750
24073
  });
23751
24074
  }
23752
24075
  } else {
23753
- const zipPath = import_path13.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
23754
- if (import_fs12.default.existsSync(zipPath)) {
23755
- 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);
23756
24079
  } else {
23757
24080
  return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
23758
24081
  }
@@ -23772,7 +24095,7 @@ function registerPluginsRoutes(ctx) {
23772
24095
  });
23773
24096
  } catch (err) {
23774
24097
  console.error(err);
23775
- res.status(500).json({ success: false, error: err.message });
24098
+ sendSafeError(res, err);
23776
24099
  }
23777
24100
  });
23778
24101
  app.get("/api/plugins/:id(*)/contributions", (req, res) => {
@@ -23781,7 +24104,7 @@ function registerPluginsRoutes(ctx) {
23781
24104
  const summary = kernelContainer.pluginHost.listContributions(rawId);
23782
24105
  res.json({ success: true, result: summary });
23783
24106
  } catch (e) {
23784
- res.status(500).json({ success: false, error: e.message });
24107
+ sendSafeError(res, e);
23785
24108
  }
23786
24109
  });
23787
24110
  app.get("/api/plugins/:id(*)/config", (req, res) => {
@@ -23801,7 +24124,7 @@ function registerPluginsRoutes(ctx) {
23801
24124
  }
23802
24125
  });
23803
24126
  } catch (e) {
23804
- res.status(500).json({ success: false, error: e.message });
24127
+ sendSafeError(res, e);
23805
24128
  }
23806
24129
  });
23807
24130
  app.post("/api/plugins/:id(*)/config", requireAuth("administrator"), (req, res) => {
@@ -23820,7 +24143,7 @@ function registerPluginsRoutes(ctx) {
23820
24143
  kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
23821
24144
  res.json({ success: true });
23822
24145
  } catch (e) {
23823
- res.status(500).json({ success: false, error: e.message });
24146
+ sendSafeError(res, e);
23824
24147
  }
23825
24148
  });
23826
24149
  app.post("/api/plugins/:id(*)/toggle", requireAuth("administrator"), async (req, res) => {
@@ -23834,7 +24157,7 @@ function registerPluginsRoutes(ctx) {
23834
24157
  const result = await kernelContainer.commandBus.execute(cmd);
23835
24158
  res.json(result);
23836
24159
  } catch (err) {
23837
- res.status(500).json({ success: false, error: err.message });
24160
+ sendSafeError(res, err);
23838
24161
  }
23839
24162
  });
23840
24163
  app.delete("/api/plugins/:id(*)", requireAuth("administrator"), async (req, res) => {
@@ -23848,7 +24171,7 @@ function registerPluginsRoutes(ctx) {
23848
24171
  const result = await kernelContainer.commandBus.execute(cmd);
23849
24172
  res.json(result);
23850
24173
  } catch (err) {
23851
- res.status(500).json({ success: false, error: err.message });
24174
+ sendSafeError(res, err);
23852
24175
  }
23853
24176
  });
23854
24177
  app.get("/api/plugins/:id(*)", async (req, res) => {
@@ -23877,7 +24200,7 @@ function registerPluginsRoutes(ctx) {
23877
24200
  res.json(result);
23878
24201
  } catch (err) {
23879
24202
  console.error(err);
23880
- res.status(500).json({ success: false, error: err.message });
24203
+ sendSafeError(res, err);
23881
24204
  }
23882
24205
  });
23883
24206
  app.post("/api/plugins/upload-zip", requireAuth("administrator"), async (req, res) => {
@@ -23892,7 +24215,7 @@ function registerPluginsRoutes(ctx) {
23892
24215
  res.json(result);
23893
24216
  } catch (err) {
23894
24217
  console.error(err);
23895
- res.status(500).json({ success: false, error: err.message });
24218
+ sendSafeError(res, err);
23896
24219
  }
23897
24220
  });
23898
24221
  app.post("/api/plugins/upload-zip-raw", requireAuth("administrator"), import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
@@ -23934,7 +24257,7 @@ function registerPluginsRoutes(ctx) {
23934
24257
  });
23935
24258
  } catch (err) {
23936
24259
  console.error(err);
23937
- res.status(500).json({ success: false, error: err.message });
24260
+ sendSafeError(res, err);
23938
24261
  }
23939
24262
  });
23940
24263
  app.post(
@@ -23967,7 +24290,7 @@ function registerPluginsRoutes(ctx) {
23967
24290
  });
23968
24291
  } catch (err) {
23969
24292
  console.error(err);
23970
- res.status(500).json({ success: false, error: err.message });
24293
+ sendSafeError(res, err);
23971
24294
  }
23972
24295
  }
23973
24296
  );
@@ -24011,10 +24334,10 @@ function registerPluginsRoutes(ctx) {
24011
24334
  res.json({ success: true, result });
24012
24335
  } catch (err) {
24013
24336
  console.error("[execute-command]", err.message);
24014
- res.status(500).json({ success: false, error: err.message });
24337
+ sendSafeError(res, err);
24015
24338
  }
24016
24339
  });
24017
- app.get("/api/ai-providers", (req, res) => {
24340
+ app.get("/api/ai-providers", requireAuth("teacher", "administrator"), (req, res) => {
24018
24341
  try {
24019
24342
  const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
24020
24343
  const masked = providers.map((p) => ({
@@ -24023,7 +24346,7 @@ function registerPluginsRoutes(ctx) {
24023
24346
  }));
24024
24347
  res.json(masked);
24025
24348
  } catch (e) {
24026
- res.status(500).json({ error: e.message });
24349
+ sendSafeError(res, e);
24027
24350
  }
24028
24351
  });
24029
24352
  app.post("/api/ai-providers", requireAuth("administrator"), (req, res) => {
@@ -24038,7 +24361,7 @@ function registerPluginsRoutes(ctx) {
24038
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);
24039
24362
  res.json({ success: true, id });
24040
24363
  } catch (e) {
24041
- res.status(500).json({ error: e.message });
24364
+ sendSafeError(res, e);
24042
24365
  }
24043
24366
  });
24044
24367
  app.put("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
@@ -24058,7 +24381,7 @@ function registerPluginsRoutes(ctx) {
24058
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);
24059
24382
  res.json({ success: true });
24060
24383
  } catch (e) {
24061
- res.status(500).json({ error: e.message });
24384
+ sendSafeError(res, e);
24062
24385
  }
24063
24386
  });
24064
24387
  app.delete("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
@@ -24066,7 +24389,7 @@ function registerPluginsRoutes(ctx) {
24066
24389
  kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
24067
24390
  res.json({ success: true });
24068
24391
  } catch (e) {
24069
- res.status(500).json({ error: e.message });
24392
+ sendSafeError(res, e);
24070
24393
  }
24071
24394
  });
24072
24395
  app.get("/api/site-settings", (req, res) => {
@@ -24078,7 +24401,7 @@ function registerPluginsRoutes(ctx) {
24078
24401
  logoUrl: row?.logo_url || null
24079
24402
  });
24080
24403
  } catch (e) {
24081
- res.status(500).json({ error: e.message });
24404
+ sendSafeError(res, e);
24082
24405
  }
24083
24406
  });
24084
24407
  app.put("/api/site-settings", requireAuth("administrator"), (req, res) => {
@@ -24093,7 +24416,7 @@ function registerPluginsRoutes(ctx) {
24093
24416
  siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
24094
24417
  });
24095
24418
  } catch (e) {
24096
- res.status(500).json({ error: e.message });
24419
+ sendSafeError(res, e);
24097
24420
  }
24098
24421
  });
24099
24422
  app.post("/api/ai-providers/test", requireAuth("administrator"), async (req, res) => {
@@ -24142,7 +24465,7 @@ function registerPluginsRoutes(ctx) {
24142
24465
  res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
24143
24466
  }
24144
24467
  } catch (e) {
24145
- res.status(500).json({ success: false, error: `Connection failed: ${e.message}` });
24468
+ sendSafeError(res, e, 500, "Connection failed");
24146
24469
  }
24147
24470
  });
24148
24471
  }
@@ -24162,6 +24485,15 @@ async function startServer() {
24162
24485
  environment: process.env.NODE_ENV || "development",
24163
24486
  config: { port: Number(process.env.PORT) || 9e3 }
24164
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
+ }
24165
24497
  await runStartupMigrations(kernelContainer.db);
24166
24498
  await kernelContainer.ready;
24167
24499
  const activityRegistry = new ActivityRegistry2();
@@ -24172,23 +24504,27 @@ async function startServer() {
24172
24504
  kernelContainer.pluginHost.setExpressApp(app);
24173
24505
  const PORT = parseInt(process.env.PORT || "9000", 10);
24174
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:*"];
24175
24508
  app.use((0, import_helmet.default)({
24176
24509
  contentSecurityPolicy: {
24177
24510
  directives: {
24178
24511
  defaultSrc: ["'self'"],
24179
- scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https:", "blob:", "data:"],
24180
- 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"],
24181
24515
  imgSrc: ["'self'", "data:", "blob:", "https:"],
24182
24516
  connectSrc: ["'self'", "ws:", "wss:", "https:"],
24183
- fontSrc: ["'self'", "data:", "https:", "https://fonts.gstatic.com"],
24184
- frameSrc: ["'self'", "blob:", "data:", "http://localhost", "http://127.0.0.1", "http:", "https:"],
24185
- 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'"]
24186
24522
  }
24187
24523
  },
24188
24524
  crossOriginOpenerPolicy: false,
24189
24525
  crossOriginEmbedderPolicy: false,
24190
24526
  crossOriginResourcePolicy: { policy: "cross-origin" },
24191
- // 允许沙箱 iframe(opaque origin)加载静态资�?
24527
+ // 允许沙箱 iframe(opaque origin)加载静态资?
24192
24528
  originAgentCluster: false,
24193
24529
  strictTransportSecurity: false
24194
24530
  // �? HTTP 部署,禁�? HSTS(否则浏览器缓存后强�? HTTPS,导�? ERR_CONNECTION_REFUSED�?
@@ -24203,8 +24539,8 @@ async function startServer() {
24203
24539
  });
24204
24540
  app.use(import_express3.default.json({ limit: "10mb" }));
24205
24541
  app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
24206
- app.use("/uploads", import_express3.default.static(import_path14.default.join(process.cwd(), "uploads")));
24207
- 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")));
24208
24544
  app.use((req, res, next) => {
24209
24545
  const origin = req.headers.origin;
24210
24546
  if (origin === "null" || origin === void 0) {
@@ -24234,14 +24570,58 @@ async function startServer() {
24234
24570
  activityRegistry
24235
24571
  };
24236
24572
  const httpServer = (0, import_http.createServer)(app);
24237
- 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"];
24238
24574
  const io = new import_socket.Server(httpServer, {
24239
24575
  cors: {
24240
- 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
+ },
24241
24586
  methods: ["GET", "POST"],
24242
24587
  credentials: true
24243
24588
  }
24244
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
+ });
24245
24625
  kernelContainer.pluginHost.setSocketIO(io);
24246
24626
  ctx.io = io;
24247
24627
  registerOsRoutes(ctx);
@@ -24270,10 +24650,10 @@ async function startServer() {
24270
24650
  });
24271
24651
  app.use(vite.middlewares);
24272
24652
  } else {
24273
- const distPath = __dirname || import_path14.default.join(process.cwd(), "dist");
24653
+ const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
24274
24654
  app.use(import_express3.default.static(distPath));
24275
24655
  app.get("*", (req, res) => {
24276
- res.sendFile(import_path14.default.join(distPath, "index.html"));
24656
+ res.sendFile(import_path15.default.join(distPath, "index.html"));
24277
24657
  });
24278
24658
  }
24279
24659
  httpServer.on("error", (err) => {
@@ -24293,8 +24673,8 @@ async function startServer() {
24293
24673
  const startTime = Date.now();
24294
24674
  let platformVersion = "0.0.0";
24295
24675
  try {
24296
- const pkgPath = import_path14.default.join(process.cwd(), "package.json");
24297
- 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"));
24298
24678
  platformVersion = pkg.version ?? platformVersion;
24299
24679
  } catch {
24300
24680
  }
@@ -24310,7 +24690,7 @@ async function startServer() {
24310
24690
  res.status(503).json({ status: "not_ready", error: e.message });
24311
24691
  }
24312
24692
  });
24313
- app.get("/metrics", (_req, res) => {
24693
+ app.get("/metrics", requireAuth("administrator"), (_req, res) => {
24314
24694
  const mem = process.memoryUsage();
24315
24695
  res.json({
24316
24696
  uptime: Math.floor((Date.now() - startTime) / 1e3),