openlearn-next 0.2.5 → 0.2.7

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 (23) hide show
  1. package/README.md +34 -136
  2. package/dist/assets/{CloudDriveModal-DgDurUBT.js → CloudDriveModal-TFWlPh8x.js} +1 -1
  3. package/dist/assets/{Dashboard-BnF7YAQi.js → Dashboard-vSYAYoji.js} +2 -2
  4. package/dist/assets/{HelpView-BP581QYZ.js → HelpView-D3WIxMoB.js} +2 -2
  5. package/dist/assets/{InteractiveCoursewareViewer-EIRNulEo.js → InteractiveCoursewareViewer-01PS40_x.js} +1 -1
  6. package/dist/assets/{InteractiveWhiteboard-D360IHbP.js → InteractiveWhiteboard-bmT1LKD6.js} +8 -8
  7. package/dist/assets/{LazyCourseware-qE4CY67E.js → LazyCourseware-Bcxn_20f.js} +1 -1
  8. package/dist/assets/{LazyWhiteboard-B5JmqHZ5.js → LazyWhiteboard-CJA7aRIM.js} +1 -1
  9. package/dist/assets/{LessonEditorView-C0czhtmf.js → LessonEditorView-hdJblhmb.js} +1 -1
  10. package/dist/assets/{LiveClassroomView-CrTY3jhP.js → LiveClassroomView-ByyIOLJC.js} +1 -1
  11. package/dist/assets/{PluginView-QS2b6nSF.js → PluginView-B3ZolNGQ.js} +2 -2
  12. package/dist/assets/{StudentAssignmentView-YxVJ5wkl.js → StudentAssignmentView-DDY7C092.js} +1 -1
  13. package/dist/assets/{StudentLessonView-D1gYdLM1.js → StudentLessonView-BcQ7q4QF.js} +1 -1
  14. package/dist/assets/{StudentPreviewModal-CG8bgUeI.js → StudentPreviewModal-ETJBF1Vc.js} +1 -1
  15. package/dist/assets/{StudentView-CmSL3cDw.js → StudentView-BOqPntx-.js} +1 -1
  16. package/dist/assets/{SystemResourceLibraryModal-Dnh7F5Vm.js → SystemResourceLibraryModal-tg8xnblT.js} +1 -1
  17. package/dist/assets/{TeacherView-BqXb0izg.js → TeacherView-CrbllHIm.js} +1 -1
  18. package/dist/assets/{index-Cq_aKTfc.js → index-CDTw8wSY.js} +1 -1
  19. package/dist/assets/index-eXp-qZdm.js +129 -0
  20. package/dist/index.html +1 -1
  21. package/dist/server.cjs +100 -37
  22. package/package.json +1 -1
  23. package/dist/assets/index-BxA9vp--.js +0 -129
package/dist/index.html CHANGED
@@ -15,7 +15,7 @@
15
15
  }
16
16
  }
17
17
  </script>
18
- <script type="module" crossorigin src="/assets/index-BxA9vp--.js"></script>
18
+ <script type="module" crossorigin src="/assets/index-eXp-qZdm.js"></script>
19
19
  <link rel="stylesheet" crossorigin href="/assets/index-CATki2cp.css">
20
20
  </head>
21
21
  <body>
package/dist/server.cjs CHANGED
@@ -2659,6 +2659,21 @@ parentPort.on('message', async function(msg) {
2659
2659
  },
2660
2660
  db: dbApi,
2661
2661
  require: function(moduleName) {
2662
+ var BLOCKED_NATIVE_MODULES = [
2663
+ 'child_process', 'node:child_process',
2664
+ 'fs', 'node:fs', 'fs/promises', 'node:fs/promises',
2665
+ 'net', 'node:net',
2666
+ 'http', 'node:http', 'https', 'node:https', 'http2', 'node:http2',
2667
+ 'dgram', 'node:dgram', 'dns', 'node:dns',
2668
+ 'cluster', 'node:cluster',
2669
+ 'worker_threads', 'node:worker_threads',
2670
+ 'vm', 'node:vm',
2671
+ 'v8', 'node:v8',
2672
+ 'wasi', 'node:wasi'
2673
+ ];
2674
+ if (BLOCKED_NATIVE_MODULES.indexOf(moduleName) !== -1) {
2675
+ throw new Error('[SecurityError] Direct access to Node.js native module "' + moduleName + '" is forbidden in plugin worker sandbox.');
2676
+ }
2662
2677
  if (PLUGIN_SHARED_MODULES.indexOf(moduleName) !== -1) {
2663
2678
  try {
2664
2679
  return requireFn(moduleName);
@@ -3374,7 +3389,16 @@ var CapabilityGuard = class {
3374
3389
  constructor() {
3375
3390
  this.actorCapabilities = /* @__PURE__ */ new Map();
3376
3391
  this.actorCapabilities.set("user-demo", ["*:*:*"]);
3377
- this.actorCapabilities.set("user-frontend", ["*:*:*"]);
3392
+ this.actorCapabilities.set("user-frontend", [
3393
+ "lesson:*",
3394
+ "whiteboard:*",
3395
+ "management:*",
3396
+ "quiz:*",
3397
+ "vfs:*",
3398
+ "process:*",
3399
+ "plugin:*"
3400
+ ]);
3401
+ this.actorCapabilities.set("anonymous", []);
3378
3402
  this.actorCapabilities.set("agent-system-0", [
3379
3403
  "lesson:write",
3380
3404
  "lesson:delete",
@@ -3388,8 +3412,8 @@ var CapabilityGuard = class {
3388
3412
  "process:write",
3389
3413
  "process:read"
3390
3414
  ]);
3391
- this.actorCapabilities.set("teacher-demo", ["lesson:write", "lesson:read"]);
3392
- this.actorCapabilities.set("student-demo", ["student:write"]);
3415
+ this.actorCapabilities.set("teacher-demo", ["lesson:*", "whiteboard:*", "management:*", "quiz:*", "vfs:*"]);
3416
+ this.actorCapabilities.set("student-demo", ["student:write", "lesson:read", "whiteboard:read"]);
3393
3417
  }
3394
3418
  grant(actorId, cap) {
3395
3419
  const caps = this.actorCapabilities.get(actorId) || [];
@@ -3402,8 +3426,28 @@ var CapabilityGuard = class {
3402
3426
  this.actorCapabilities.delete(actorId);
3403
3427
  }
3404
3428
  check(actorId, requiredCap) {
3405
- const isAdmin = actorId === "role:administrator" || actorId?.endsWith(":administrator") || actorId === "admin-demo";
3429
+ const isAdmin = actorId === "role:administrator" || actorId?.endsWith(":administrator") || actorId === "admin" || actorId === "usr_admin" || actorId === "admin-demo";
3406
3430
  if (isAdmin) return true;
3431
+ if (actorId?.endsWith(":teacher")) {
3432
+ const teacherCaps = ["lesson:*", "whiteboard:*", "management:*", "quiz:*", "vfs:*", "process:*", "plugin:*"];
3433
+ const [reqRes2, reqAct2] = requiredCap.split(":");
3434
+ if (teacherCaps.some((c) => {
3435
+ const [res, act] = c.split(":");
3436
+ return (res === reqRes2 || res === "*") && (act === reqAct2 || act === "*");
3437
+ })) {
3438
+ return true;
3439
+ }
3440
+ }
3441
+ if (actorId?.endsWith(":student")) {
3442
+ const studentCaps = ["student:write", "lesson:read", "whiteboard:read"];
3443
+ const [reqRes2, reqAct2] = requiredCap.split(":");
3444
+ if (studentCaps.some((c) => {
3445
+ const [res, act] = c.split(":");
3446
+ return (res === reqRes2 || res === "*") && (act === reqAct2 || act === "*");
3447
+ })) {
3448
+ return true;
3449
+ }
3450
+ }
3407
3451
  const caps = this.actorCapabilities.get(actorId) || [];
3408
3452
  if (caps.includes("*:*:*") || caps.includes("*")) return true;
3409
3453
  if (caps.includes(requiredCap)) return true;
@@ -3638,7 +3682,7 @@ var classroomToolSchema = import_zod.z.object({
3638
3682
  icon: import_zod.z.string().optional(),
3639
3683
  description: import_zod.z.string().optional(),
3640
3684
  commandType: import_zod.z.string().min(1),
3641
- payload: import_zod.z.record(import_zod.z.unknown()).optional()
3685
+ payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
3642
3686
  });
3643
3687
  var contributesSchema = import_zod.z.object({
3644
3688
  "classroom.tool": import_zod.z.array(classroomToolSchema).optional(),
@@ -3665,7 +3709,10 @@ var contributesSchema = import_zod.z.object({
3665
3709
  label: import_zod.z.string().min(1),
3666
3710
  icon: import_zod.z.string().optional()
3667
3711
  })).optional()
3668
- }).optional();
3712
+ // v0.2.6: 锚点槽位(anchor:*)为开放命名空间,由宿主公布锚点 id。
3713
+ // .passthrough() 保留任意 anchor:* 键(运行时透传),避免被 zod 默认 strip。
3714
+ // 锚点条目的运行时校验由前端注册时(ExtensionPointConfig)完成。
3715
+ }).passthrough().optional();
3669
3716
  var deploySchema = import_zod.z.object({
3670
3717
  script: import_zod.z.string().min(1, { error: "deploy.script \u4E0D\u80FD\u4E3A\u7A7A" }).optional(),
3671
3718
  staticRoute: import_zod.z.string().optional(),
@@ -3689,7 +3736,7 @@ var manifestSchema = import_zod.z.object({
3689
3736
  pluginDependencies: import_zod.z.array(import_zod.z.string().min(1)).optional(),
3690
3737
  provides: import_zod.z.array(import_zod.z.string().min(1)).optional(),
3691
3738
  configuration: import_zod.z.object({
3692
- properties: import_zod.z.record(import_zod.z.object({
3739
+ properties: import_zod.z.record(import_zod.z.string(), import_zod.z.object({
3693
3740
  type: import_zod.z.enum(["string", "number", "boolean", "integer"]),
3694
3741
  default: import_zod.z.unknown().optional(),
3695
3742
  description: import_zod.z.string().optional(),
@@ -3774,6 +3821,7 @@ if (process.env.OPENLEARN_DB_PATH) {
3774
3821
  var db = new import_better_sqlite3.default(dbPath);
3775
3822
  db.pragma("journal_mode = WAL");
3776
3823
  db.pragma("synchronous = NORMAL");
3824
+ db.pragma("foreign_keys = ON");
3777
3825
  db.exec(`
3778
3826
  CREATE TABLE IF NOT EXISTS events (
3779
3827
  id TEXT PRIMARY KEY,
@@ -4116,6 +4164,17 @@ db.exec(`
4116
4164
  status TEXT NOT NULL DEFAULT 'draft',
4117
4165
  graded_at INTEGER
4118
4166
  );
4167
+
4168
+ -- Performance Indexes
4169
+ CREATE INDEX IF NOT EXISTS idx_whiteboard_lesson ON whiteboard_elements(lesson_id);
4170
+ CREATE INDEX IF NOT EXISTS idx_class_students_class ON class_students(class_id);
4171
+ CREATE INDEX IF NOT EXISTS idx_class_students_student ON class_students(student_id);
4172
+ CREATE INDEX IF NOT EXISTS idx_schedules_class_date ON schedules(class_id, scheduled_date);
4173
+ CREATE INDEX IF NOT EXISTS idx_courseware_attempt_cw_st ON courseware_attempt(courseware_id, student_id);
4174
+ CREATE INDEX IF NOT EXISTS idx_submission_result_attempt ON submission_result(attempt_id);
4175
+ CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(type, timestamp);
4176
+ CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
4177
+ CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
4119
4178
  `);
4120
4179
  try {
4121
4180
  db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
@@ -4250,7 +4309,7 @@ var VfsPlugin = {
4250
4309
  "@openlearn/core:IDatabase@^1.0.0"
4251
4310
  ],
4252
4311
  capabilitiesProposed: ["vfs:read", "vfs:write"],
4253
- engines: { openlearn: "^2.5.0" }
4312
+ engines: { openlearn: "^0.2.5" }
4254
4313
  },
4255
4314
  activate: async (ctx) => {
4256
4315
  const commandBus = ctx.services.commandBus;
@@ -4416,7 +4475,7 @@ var ProcessPlugin = {
4416
4475
  "@openlearn/core:IDatabase@^1.0.0"
4417
4476
  ],
4418
4477
  capabilitiesProposed: ["process:read", "process:write"],
4419
- engines: { openlearn: "^2.5.0" }
4478
+ engines: { openlearn: "^0.2.5" }
4420
4479
  },
4421
4480
  activate: async (ctx) => {
4422
4481
  const commandBus = ctx.services.commandBus;
@@ -4547,7 +4606,7 @@ var ManagementPlugin = {
4547
4606
  "@openlearn/core:IDatabase@^1.0.0"
4548
4607
  ],
4549
4608
  capabilitiesProposed: ["class:read", "class:write", "student:read", "student:write"],
4550
- engines: { openlearn: "^2.5.0" }
4609
+ engines: { openlearn: "^0.2.5" }
4551
4610
  },
4552
4611
  activate: async (ctx) => {
4553
4612
  const commandBus = ctx.services.commandBus;
@@ -5446,7 +5505,7 @@ var AiSubmitInjectorPlugin = {
5446
5505
  "@openlearn/core:IAIService@^1.0.0"
5447
5506
  ],
5448
5507
  capabilitiesProposed: [],
5449
- engines: { openlearn: "^2.5.0" }
5508
+ engines: { openlearn: "^0.2.5" }
5450
5509
  },
5451
5510
  activate: async (ctx) => {
5452
5511
  const eventBus = ctx.services.eventBus;
@@ -5538,7 +5597,7 @@ var BuiltinPlugin = {
5538
5597
  "@openlearn/core:IPluginHost@^1.0.0"
5539
5598
  ],
5540
5599
  capabilitiesProposed: ["lesson:read", "lesson:write", "whiteboard:read", "whiteboard:write"],
5541
- engines: { openlearn: "^2.5.0" }
5600
+ engines: { openlearn: "^0.2.5" }
5542
5601
  },
5543
5602
  activate: async (ctx) => {
5544
5603
  const commandBus = ctx.services.commandBus;
@@ -6923,7 +6982,7 @@ var AiPlannerPlugin = {
6923
6982
  "@openlearn/core:IEventBusService@^1.0.0"
6924
6983
  ],
6925
6984
  capabilitiesProposed: ["process:write", "lesson:write", "assignment:write"],
6926
- engines: { openlearn: "^2.5.0" }
6985
+ engines: { openlearn: "^0.2.5" }
6927
6986
  },
6928
6987
  activate: async (ctx) => {
6929
6988
  const commandBus = ctx.services.commandBus;
@@ -7109,7 +7168,7 @@ var AssignmentEvalPlugin = {
7109
7168
  "@openlearn/core:ISemesterGradeService@^1.0.0"
7110
7169
  ],
7111
7170
  capabilitiesProposed: ["lesson:read", "lesson:write"],
7112
- engines: { openlearn: "^2.5.0" }
7171
+ engines: { openlearn: "^0.2.5" }
7113
7172
  },
7114
7173
  activate: async (ctx) => {
7115
7174
  const commandBus = ctx.services.commandBus;
@@ -7695,7 +7754,7 @@ var ContributionRegistry = class {
7695
7754
  register(pluginId, contributes) {
7696
7755
  this.unregister(pluginId);
7697
7756
  for (const [slot, configs] of Object.entries(contributes)) {
7698
- if (!configs || configs.length === 0) continue;
7757
+ if (!Array.isArray(configs) || configs.length === 0) continue;
7699
7758
  if (!this.contributions.has(slot)) {
7700
7759
  this.contributions.set(slot, /* @__PURE__ */ new Map());
7701
7760
  }
@@ -8057,7 +8116,7 @@ var PluginRuntimeComposition = class {
8057
8116
  this.workerManager = workerManager;
8058
8117
  this.id = "srv_plugin_runtime_composition";
8059
8118
  this.name = "PluginRuntimeComposition";
8060
- this.version = "0.2.3";
8119
+ this.version = "0.2.5";
8061
8120
  this._isStarted = false;
8062
8121
  }
8063
8122
  get isStarted() {
@@ -8111,7 +8170,7 @@ var PluginLifecycleManager = class {
8111
8170
  this.pluginHost = pluginHost;
8112
8171
  this.id = "srv_plugin_lifecycle_manager";
8113
8172
  this.name = "PluginLifecycleManager";
8114
- this.version = "0.2.3";
8173
+ this.version = "0.2.5";
8115
8174
  }
8116
8175
  getPluginState(pluginId) {
8117
8176
  return this.pluginHost.getPluginState(pluginId);
@@ -8158,7 +8217,7 @@ var PluginCapabilityGateway = class {
8158
8217
  this.capabilityRegistry = capabilityRegistry;
8159
8218
  this.id = "srv_plugin_capability_gateway";
8160
8219
  this.name = "PluginCapabilityGateway";
8161
- this.version = "0.2.3";
8220
+ this.version = "0.2.5";
8162
8221
  }
8163
8222
  listCapabilities() {
8164
8223
  const rawCaps = this.capabilityRegistry.listCapabilities();
@@ -8212,7 +8271,7 @@ var UnifiedExtensionRegistry = class {
8212
8271
  constructor() {
8213
8272
  this.id = "srv_unified_extension_registry";
8214
8273
  this.name = "UnifiedExtensionRegistry";
8215
- this.version = "0.2.3";
8274
+ this.version = "0.2.5";
8216
8275
  this._extensions = /* @__PURE__ */ new Map();
8217
8276
  }
8218
8277
  registerExtension(category, id, impl, meta) {
@@ -8317,7 +8376,7 @@ var PluginDistributionManager = class {
8317
8376
  this.pluginHost = pluginHost;
8318
8377
  this.id = "srv_plugin_distribution_manager";
8319
8378
  this.name = "PluginDistributionManager";
8320
- this.version = "0.2.3";
8379
+ this.version = "0.2.5";
8321
8380
  this._repositories = /* @__PURE__ */ new Map();
8322
8381
  }
8323
8382
  registerRepository(repo) {
@@ -8406,7 +8465,7 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
8406
8465
  }
8407
8466
  var ACTIVATION_TIMEOUT_MS = 5e3;
8408
8467
  var DEACTIVATION_TIMEOUT_MS = 5e3;
8409
- var OPENLEARN_VERSION = "0.2.3";
8468
+ var OPENLEARN_VERSION = "0.2.5";
8410
8469
  var PluginHost = class {
8411
8470
  constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
8412
8471
  this.serviceRegistry = serviceRegistry;
@@ -10431,7 +10490,7 @@ var HotReloadController = class {
10431
10490
  }
10432
10491
  if (this.enabled) return;
10433
10492
  this.enabled = true;
10434
- await this.fileWatcher.startWatch(this.watchDir, (event) => {
10493
+ await this.fileWatcher.startWatch(this.watchDir, async (event) => {
10435
10494
  this.handleFileChange(event.pluginId, event.filePath);
10436
10495
  });
10437
10496
  console.log("[HotReload] Hot reload enabled \u2014 watching for plugin changes");
@@ -16020,7 +16079,7 @@ var StartupTimeoutError = class extends PlatformBootstrapError {
16020
16079
  this.name = "StartupTimeoutError";
16021
16080
  }
16022
16081
  };
16023
- var PLATFORM_VERSION = "0.2.3";
16082
+ var PLATFORM_VERSION = "0.2.5";
16024
16083
  var BOOTSTRAP_STAGE_NAMES = Object.freeze([
16025
16084
  "Created" /* Created */,
16026
16085
  "Configuring" /* Configuring */,
@@ -16722,10 +16781,10 @@ var PluginCompositionModule = class {
16722
16781
  const realPluginHost = options?.infrastructureRefs?.get("pluginHost") ?? { name: "PluginHostService", isReady: true, pluginsCount: 0 };
16723
16782
  const realContributionRegistry = options?.infrastructureRefs?.get("contributionRegistry") ?? { name: "ContributionRegistry", slotsCount: 0 };
16724
16783
  const realRuntimeComposition = options?.infrastructureRefs?.get("runtimeComposition") ?? { name: "PluginRuntimeComposition", isStarted: true };
16725
- const realLifecycleManager = options?.infrastructureRefs?.get("lifecycleManager") ?? { name: "PluginLifecycleManager", version: "2.5.0" };
16726
- const realCapabilityGateway = options?.infrastructureRefs?.get("capabilityGateway") ?? { name: "PluginCapabilityGateway", version: "2.5.0" };
16727
- const realExtensionRegistry = options?.infrastructureRefs?.get("extensionRegistry") ?? { name: "UnifiedExtensionRegistry", version: "2.5.0" };
16728
- const realDistributionManager = options?.infrastructureRefs?.get("distributionManager") ?? { name: "PluginDistributionManager", version: "2.5.0" };
16784
+ const realLifecycleManager = options?.infrastructureRefs?.get("lifecycleManager") ?? { name: "PluginLifecycleManager", version: "0.2.5" };
16785
+ const realCapabilityGateway = options?.infrastructureRefs?.get("capabilityGateway") ?? { name: "PluginCapabilityGateway", version: "0.2.5" };
16786
+ const realExtensionRegistry = options?.infrastructureRefs?.get("extensionRegistry") ?? { name: "UnifiedExtensionRegistry", version: "0.2.5" };
16787
+ const realDistributionManager = options?.infrastructureRefs?.get("distributionManager") ?? { name: "PluginDistributionManager", version: "0.2.5" };
16729
16788
  registerOrReplace({
16730
16789
  id: "srv_plugin_host",
16731
16790
  lifetime: "Singleton",
@@ -16893,7 +16952,7 @@ var Kernel = class {
16893
16952
  throw new Error(`[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`);
16894
16953
  }
16895
16954
  }
16896
- const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || command.actorId?.endsWith(":administrator") || command.actorId?.endsWith(":admin") || command.actorId?.includes(":administrator") || command.actorId?.includes(":admin");
16955
+ const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || command.actorId?.endsWith(":administrator") || command.actorId?.endsWith(":admin");
16897
16956
  if (action.capabilityRequired && !isAdmin) {
16898
16957
  const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
16899
16958
  if (!allowed) {
@@ -18367,10 +18426,10 @@ function getCookieToken(req) {
18367
18426
  }
18368
18427
  function getActorId(req) {
18369
18428
  const token = getCookieToken(req);
18370
- if (!token) return "user-frontend";
18429
+ if (!token) return "anonymous";
18371
18430
  try {
18372
18431
  const session = getValidSession(token);
18373
- if (!session) return "user-frontend";
18432
+ if (!session) return "anonymous";
18374
18433
  let role = session.subRole || session.role;
18375
18434
  if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18376
18435
  role = "administrator";
@@ -18378,9 +18437,9 @@ function getActorId(req) {
18378
18437
  if (role) {
18379
18438
  return `user:${session.userId || "demo"}:${role}`;
18380
18439
  }
18381
- return "user-frontend";
18440
+ return "anonymous";
18382
18441
  } catch {
18383
- return "user-frontend";
18442
+ return "anonymous";
18384
18443
  }
18385
18444
  }
18386
18445
  function requireAuth(...roles) {
@@ -19163,7 +19222,7 @@ function registerOsRoutes(ctx) {
19163
19222
  res.status(500).json({ error: e.message });
19164
19223
  }
19165
19224
  });
19166
- app.post("/api/commands", async (req, res) => {
19225
+ app.post("/api/commands", requireAuth(), async (req, res) => {
19167
19226
  try {
19168
19227
  const { commandType, payload } = req.body;
19169
19228
  const cmd = kernelContainer.commandBus.createCommand(
@@ -23078,7 +23137,7 @@ function registerGradingRoutes(ctx) {
23078
23137
  if (!lessonId || !studentId || grade === void 0) {
23079
23138
  return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
23080
23139
  }
23081
- const gradeService = await kernelContainer.registry.resolve(ISemesterGradeServiceToken);
23140
+ const gradeService = await kernelContainer.serviceRegistry.resolve(ISemesterGradeServiceToken);
23082
23141
  await gradeService.saveSemesterGrade(lessonId, studentId, Math.round(Number(grade)));
23083
23142
  res.json({ success: true });
23084
23143
  } catch (e) {
@@ -23087,7 +23146,7 @@ function registerGradingRoutes(ctx) {
23087
23146
  });
23088
23147
  app.get("/api/classes/:classId/points-dimensions", async (req, res) => {
23089
23148
  try {
23090
- const dimensionRegistry = await kernelContainer.registry.resolve(IPointsDimensionRegistryToken);
23149
+ const dimensionRegistry = await kernelContainer.serviceRegistry.resolve(IPointsDimensionRegistryToken);
23091
23150
  const dimensions = dimensionRegistry.listDimensions();
23092
23151
  res.json({ success: true, dimensions });
23093
23152
  } catch (e) {
@@ -23104,7 +23163,7 @@ function registerGradingRoutes(ctx) {
23104
23163
  if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
23105
23164
  return res.status(400).json({ success: false, error: "classId, dimensionId, deltaPoints, and reason are required" });
23106
23165
  }
23107
- const ledgerService = await kernelContainer.registry.resolve(IPointsLedgerServiceToken);
23166
+ const ledgerService = await kernelContainer.serviceRegistry.resolve(IPointsLedgerServiceToken);
23108
23167
  const logItem = await ledgerService.addPoints(studentId, classId, dimensionId, Number(deltaPoints), reason, pluginId);
23109
23168
  kernelContainer.eventBus.publish({
23110
23169
  id: logItem.id,
@@ -23122,7 +23181,7 @@ function registerGradingRoutes(ctx) {
23122
23181
  try {
23123
23182
  const { studentId } = req.params;
23124
23183
  const { classId } = req.query;
23125
- const ledgerService = await kernelContainer.registry.resolve(IPointsLedgerServiceToken);
23184
+ const ledgerService = await kernelContainer.serviceRegistry.resolve(IPointsLedgerServiceToken);
23126
23185
  const logs = await ledgerService.getLogs(studentId, classId);
23127
23186
  const summary = await ledgerService.getStudentDimensionSummary(studentId, classId || "");
23128
23187
  res.json({ success: true, logs, summary });
@@ -24013,6 +24072,10 @@ function registerPluginsRoutes(ctx) {
24013
24072
  if (!cleanUrl.endsWith("/chat/completions")) {
24014
24073
  cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
24015
24074
  }
24075
+ const urlSafety = isSafeExternalUrl(cleanUrl);
24076
+ if (!urlSafety.safe) {
24077
+ return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
24078
+ }
24016
24079
  const response = await fetch(cleanUrl, {
24017
24080
  method: "POST",
24018
24081
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openlearn-next",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "A plugin-driven Educational OS / LMS platform with AI agent teaching assistant",
6
6
  "keywords": [