openlearn-next 0.2.5 → 0.2.6

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.
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-B9cwmoaG.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(),
@@ -4116,6 +4163,17 @@ db.exec(`
4116
4163
  status TEXT NOT NULL DEFAULT 'draft',
4117
4164
  graded_at INTEGER
4118
4165
  );
4166
+
4167
+ -- Performance Indexes
4168
+ CREATE INDEX IF NOT EXISTS idx_whiteboard_lesson ON whiteboard_elements(lesson_id);
4169
+ CREATE INDEX IF NOT EXISTS idx_class_students_class ON class_students(class_id);
4170
+ CREATE INDEX IF NOT EXISTS idx_class_students_student ON class_students(student_id);
4171
+ CREATE INDEX IF NOT EXISTS idx_schedules_class_date ON schedules(class_id, scheduled_date);
4172
+ CREATE INDEX IF NOT EXISTS idx_courseware_attempt_cw_st ON courseware_attempt(courseware_id, student_id);
4173
+ CREATE INDEX IF NOT EXISTS idx_submission_result_attempt ON submission_result(attempt_id);
4174
+ CREATE INDEX IF NOT EXISTS idx_events_type_time ON events(type, timestamp);
4175
+ CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
4176
+ CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
4119
4177
  `);
4120
4178
  try {
4121
4179
  db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
@@ -4250,7 +4308,7 @@ var VfsPlugin = {
4250
4308
  "@openlearn/core:IDatabase@^1.0.0"
4251
4309
  ],
4252
4310
  capabilitiesProposed: ["vfs:read", "vfs:write"],
4253
- engines: { openlearn: "^2.5.0" }
4311
+ engines: { openlearn: "^0.2.5" }
4254
4312
  },
4255
4313
  activate: async (ctx) => {
4256
4314
  const commandBus = ctx.services.commandBus;
@@ -4416,7 +4474,7 @@ var ProcessPlugin = {
4416
4474
  "@openlearn/core:IDatabase@^1.0.0"
4417
4475
  ],
4418
4476
  capabilitiesProposed: ["process:read", "process:write"],
4419
- engines: { openlearn: "^2.5.0" }
4477
+ engines: { openlearn: "^0.2.5" }
4420
4478
  },
4421
4479
  activate: async (ctx) => {
4422
4480
  const commandBus = ctx.services.commandBus;
@@ -4547,7 +4605,7 @@ var ManagementPlugin = {
4547
4605
  "@openlearn/core:IDatabase@^1.0.0"
4548
4606
  ],
4549
4607
  capabilitiesProposed: ["class:read", "class:write", "student:read", "student:write"],
4550
- engines: { openlearn: "^2.5.0" }
4608
+ engines: { openlearn: "^0.2.5" }
4551
4609
  },
4552
4610
  activate: async (ctx) => {
4553
4611
  const commandBus = ctx.services.commandBus;
@@ -5446,7 +5504,7 @@ var AiSubmitInjectorPlugin = {
5446
5504
  "@openlearn/core:IAIService@^1.0.0"
5447
5505
  ],
5448
5506
  capabilitiesProposed: [],
5449
- engines: { openlearn: "^2.5.0" }
5507
+ engines: { openlearn: "^0.2.5" }
5450
5508
  },
5451
5509
  activate: async (ctx) => {
5452
5510
  const eventBus = ctx.services.eventBus;
@@ -5538,7 +5596,7 @@ var BuiltinPlugin = {
5538
5596
  "@openlearn/core:IPluginHost@^1.0.0"
5539
5597
  ],
5540
5598
  capabilitiesProposed: ["lesson:read", "lesson:write", "whiteboard:read", "whiteboard:write"],
5541
- engines: { openlearn: "^2.5.0" }
5599
+ engines: { openlearn: "^0.2.5" }
5542
5600
  },
5543
5601
  activate: async (ctx) => {
5544
5602
  const commandBus = ctx.services.commandBus;
@@ -6923,7 +6981,7 @@ var AiPlannerPlugin = {
6923
6981
  "@openlearn/core:IEventBusService@^1.0.0"
6924
6982
  ],
6925
6983
  capabilitiesProposed: ["process:write", "lesson:write", "assignment:write"],
6926
- engines: { openlearn: "^2.5.0" }
6984
+ engines: { openlearn: "^0.2.5" }
6927
6985
  },
6928
6986
  activate: async (ctx) => {
6929
6987
  const commandBus = ctx.services.commandBus;
@@ -7109,7 +7167,7 @@ var AssignmentEvalPlugin = {
7109
7167
  "@openlearn/core:ISemesterGradeService@^1.0.0"
7110
7168
  ],
7111
7169
  capabilitiesProposed: ["lesson:read", "lesson:write"],
7112
- engines: { openlearn: "^2.5.0" }
7170
+ engines: { openlearn: "^0.2.5" }
7113
7171
  },
7114
7172
  activate: async (ctx) => {
7115
7173
  const commandBus = ctx.services.commandBus;
@@ -7695,7 +7753,7 @@ var ContributionRegistry = class {
7695
7753
  register(pluginId, contributes) {
7696
7754
  this.unregister(pluginId);
7697
7755
  for (const [slot, configs] of Object.entries(contributes)) {
7698
- if (!configs || configs.length === 0) continue;
7756
+ if (!Array.isArray(configs) || configs.length === 0) continue;
7699
7757
  if (!this.contributions.has(slot)) {
7700
7758
  this.contributions.set(slot, /* @__PURE__ */ new Map());
7701
7759
  }
@@ -8057,7 +8115,7 @@ var PluginRuntimeComposition = class {
8057
8115
  this.workerManager = workerManager;
8058
8116
  this.id = "srv_plugin_runtime_composition";
8059
8117
  this.name = "PluginRuntimeComposition";
8060
- this.version = "0.2.3";
8118
+ this.version = "0.2.5";
8061
8119
  this._isStarted = false;
8062
8120
  }
8063
8121
  get isStarted() {
@@ -8111,7 +8169,7 @@ var PluginLifecycleManager = class {
8111
8169
  this.pluginHost = pluginHost;
8112
8170
  this.id = "srv_plugin_lifecycle_manager";
8113
8171
  this.name = "PluginLifecycleManager";
8114
- this.version = "0.2.3";
8172
+ this.version = "0.2.5";
8115
8173
  }
8116
8174
  getPluginState(pluginId) {
8117
8175
  return this.pluginHost.getPluginState(pluginId);
@@ -8158,7 +8216,7 @@ var PluginCapabilityGateway = class {
8158
8216
  this.capabilityRegistry = capabilityRegistry;
8159
8217
  this.id = "srv_plugin_capability_gateway";
8160
8218
  this.name = "PluginCapabilityGateway";
8161
- this.version = "0.2.3";
8219
+ this.version = "0.2.5";
8162
8220
  }
8163
8221
  listCapabilities() {
8164
8222
  const rawCaps = this.capabilityRegistry.listCapabilities();
@@ -8212,7 +8270,7 @@ var UnifiedExtensionRegistry = class {
8212
8270
  constructor() {
8213
8271
  this.id = "srv_unified_extension_registry";
8214
8272
  this.name = "UnifiedExtensionRegistry";
8215
- this.version = "0.2.3";
8273
+ this.version = "0.2.5";
8216
8274
  this._extensions = /* @__PURE__ */ new Map();
8217
8275
  }
8218
8276
  registerExtension(category, id, impl, meta) {
@@ -8317,7 +8375,7 @@ var PluginDistributionManager = class {
8317
8375
  this.pluginHost = pluginHost;
8318
8376
  this.id = "srv_plugin_distribution_manager";
8319
8377
  this.name = "PluginDistributionManager";
8320
- this.version = "0.2.3";
8378
+ this.version = "0.2.5";
8321
8379
  this._repositories = /* @__PURE__ */ new Map();
8322
8380
  }
8323
8381
  registerRepository(repo) {
@@ -8406,7 +8464,7 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
8406
8464
  }
8407
8465
  var ACTIVATION_TIMEOUT_MS = 5e3;
8408
8466
  var DEACTIVATION_TIMEOUT_MS = 5e3;
8409
- var OPENLEARN_VERSION = "0.2.3";
8467
+ var OPENLEARN_VERSION = "0.2.5";
8410
8468
  var PluginHost = class {
8411
8469
  constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
8412
8470
  this.serviceRegistry = serviceRegistry;
@@ -10431,7 +10489,7 @@ var HotReloadController = class {
10431
10489
  }
10432
10490
  if (this.enabled) return;
10433
10491
  this.enabled = true;
10434
- await this.fileWatcher.startWatch(this.watchDir, (event) => {
10492
+ await this.fileWatcher.startWatch(this.watchDir, async (event) => {
10435
10493
  this.handleFileChange(event.pluginId, event.filePath);
10436
10494
  });
10437
10495
  console.log("[HotReload] Hot reload enabled \u2014 watching for plugin changes");
@@ -16020,7 +16078,7 @@ var StartupTimeoutError = class extends PlatformBootstrapError {
16020
16078
  this.name = "StartupTimeoutError";
16021
16079
  }
16022
16080
  };
16023
- var PLATFORM_VERSION = "0.2.3";
16081
+ var PLATFORM_VERSION = "0.2.5";
16024
16082
  var BOOTSTRAP_STAGE_NAMES = Object.freeze([
16025
16083
  "Created" /* Created */,
16026
16084
  "Configuring" /* Configuring */,
@@ -16722,10 +16780,10 @@ var PluginCompositionModule = class {
16722
16780
  const realPluginHost = options?.infrastructureRefs?.get("pluginHost") ?? { name: "PluginHostService", isReady: true, pluginsCount: 0 };
16723
16781
  const realContributionRegistry = options?.infrastructureRefs?.get("contributionRegistry") ?? { name: "ContributionRegistry", slotsCount: 0 };
16724
16782
  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" };
16783
+ const realLifecycleManager = options?.infrastructureRefs?.get("lifecycleManager") ?? { name: "PluginLifecycleManager", version: "0.2.5" };
16784
+ const realCapabilityGateway = options?.infrastructureRefs?.get("capabilityGateway") ?? { name: "PluginCapabilityGateway", version: "0.2.5" };
16785
+ const realExtensionRegistry = options?.infrastructureRefs?.get("extensionRegistry") ?? { name: "UnifiedExtensionRegistry", version: "0.2.5" };
16786
+ const realDistributionManager = options?.infrastructureRefs?.get("distributionManager") ?? { name: "PluginDistributionManager", version: "0.2.5" };
16729
16787
  registerOrReplace({
16730
16788
  id: "srv_plugin_host",
16731
16789
  lifetime: "Singleton",
@@ -16893,7 +16951,7 @@ var Kernel = class {
16893
16951
  throw new Error(`[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`);
16894
16952
  }
16895
16953
  }
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");
16954
+ 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
16955
  if (action.capabilityRequired && !isAdmin) {
16898
16956
  const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
16899
16957
  if (!allowed) {
@@ -18367,10 +18425,10 @@ function getCookieToken(req) {
18367
18425
  }
18368
18426
  function getActorId(req) {
18369
18427
  const token = getCookieToken(req);
18370
- if (!token) return "user-frontend";
18428
+ if (!token) return "anonymous";
18371
18429
  try {
18372
18430
  const session = getValidSession(token);
18373
- if (!session) return "user-frontend";
18431
+ if (!session) return "anonymous";
18374
18432
  let role = session.subRole || session.role;
18375
18433
  if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
18376
18434
  role = "administrator";
@@ -18378,9 +18436,9 @@ function getActorId(req) {
18378
18436
  if (role) {
18379
18437
  return `user:${session.userId || "demo"}:${role}`;
18380
18438
  }
18381
- return "user-frontend";
18439
+ return "anonymous";
18382
18440
  } catch {
18383
- return "user-frontend";
18441
+ return "anonymous";
18384
18442
  }
18385
18443
  }
18386
18444
  function requireAuth(...roles) {
@@ -19163,7 +19221,7 @@ function registerOsRoutes(ctx) {
19163
19221
  res.status(500).json({ error: e.message });
19164
19222
  }
19165
19223
  });
19166
- app.post("/api/commands", async (req, res) => {
19224
+ app.post("/api/commands", requireAuth(), async (req, res) => {
19167
19225
  try {
19168
19226
  const { commandType, payload } = req.body;
19169
19227
  const cmd = kernelContainer.commandBus.createCommand(
@@ -23078,7 +23136,7 @@ function registerGradingRoutes(ctx) {
23078
23136
  if (!lessonId || !studentId || grade === void 0) {
23079
23137
  return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
23080
23138
  }
23081
- const gradeService = await kernelContainer.registry.resolve(ISemesterGradeServiceToken);
23139
+ const gradeService = await kernelContainer.serviceRegistry.resolve(ISemesterGradeServiceToken);
23082
23140
  await gradeService.saveSemesterGrade(lessonId, studentId, Math.round(Number(grade)));
23083
23141
  res.json({ success: true });
23084
23142
  } catch (e) {
@@ -23087,7 +23145,7 @@ function registerGradingRoutes(ctx) {
23087
23145
  });
23088
23146
  app.get("/api/classes/:classId/points-dimensions", async (req, res) => {
23089
23147
  try {
23090
- const dimensionRegistry = await kernelContainer.registry.resolve(IPointsDimensionRegistryToken);
23148
+ const dimensionRegistry = await kernelContainer.serviceRegistry.resolve(IPointsDimensionRegistryToken);
23091
23149
  const dimensions = dimensionRegistry.listDimensions();
23092
23150
  res.json({ success: true, dimensions });
23093
23151
  } catch (e) {
@@ -23104,7 +23162,7 @@ function registerGradingRoutes(ctx) {
23104
23162
  if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
23105
23163
  return res.status(400).json({ success: false, error: "classId, dimensionId, deltaPoints, and reason are required" });
23106
23164
  }
23107
- const ledgerService = await kernelContainer.registry.resolve(IPointsLedgerServiceToken);
23165
+ const ledgerService = await kernelContainer.serviceRegistry.resolve(IPointsLedgerServiceToken);
23108
23166
  const logItem = await ledgerService.addPoints(studentId, classId, dimensionId, Number(deltaPoints), reason, pluginId);
23109
23167
  kernelContainer.eventBus.publish({
23110
23168
  id: logItem.id,
@@ -23122,7 +23180,7 @@ function registerGradingRoutes(ctx) {
23122
23180
  try {
23123
23181
  const { studentId } = req.params;
23124
23182
  const { classId } = req.query;
23125
- const ledgerService = await kernelContainer.registry.resolve(IPointsLedgerServiceToken);
23183
+ const ledgerService = await kernelContainer.serviceRegistry.resolve(IPointsLedgerServiceToken);
23126
23184
  const logs = await ledgerService.getLogs(studentId, classId);
23127
23185
  const summary = await ledgerService.getStudentDimensionSummary(studentId, classId || "");
23128
23186
  res.json({ success: true, logs, summary });
@@ -24013,6 +24071,10 @@ function registerPluginsRoutes(ctx) {
24013
24071
  if (!cleanUrl.endsWith("/chat/completions")) {
24014
24072
  cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
24015
24073
  }
24074
+ const urlSafety = isSafeExternalUrl(cleanUrl);
24075
+ if (!urlSafety.safe) {
24076
+ return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
24077
+ }
24016
24078
  const response = await fetch(cleanUrl, {
24017
24079
  method: "POST",
24018
24080
  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.6",
4
4
  "private": false,
5
5
  "description": "A plugin-driven Educational OS / LMS platform with AI agent teaching assistant",
6
6
  "keywords": [