openlearn-next 0.2.9 → 0.3.2

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