openlearn-next 0.7.10 → 0.7.13

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 (64) hide show
  1. package/README.md +9 -2
  2. package/dist/assets/{ActivityWorkspaceWidget-BU88GGUG.js → ActivityWorkspaceWidget-9DeXkxGk.js} +1 -1
  3. package/dist/assets/AdminDirectoryView-5gupeGAR.js +5 -0
  4. package/dist/assets/{AssessmentCenter-BoSWbJMt.js → AssessmentCenter-CPaqy0Xa.js} +1 -1
  5. package/dist/assets/{AssignmentFilePreview-D_POvLmv.js → AssignmentFilePreview-7fHtUuUL.js} +2 -2
  6. package/dist/assets/{BatchPickerModal-DiHd_6Tg.js → BatchPickerModal-0yIe1DXi.js} +1 -1
  7. package/dist/assets/{ClassesView-CWKqzWH0.js → ClassesView-CGpoW0IC.js} +5 -5
  8. package/dist/assets/{ClassroomCountdownWidget-B5-KUVU8.js → ClassroomCountdownWidget-B_5--dn_.js} +1 -1
  9. package/dist/assets/{CloudDriveModal-CjcvdDiw.js → CloudDriveModal-4Ts5Duh5.js} +1 -1
  10. package/dist/assets/{ComputerLabView-CNTcmWAh.js → ComputerLabView-DAEaKTBj.js} +1 -1
  11. package/dist/assets/{CourseManagement-ha_iSnLU.js → CourseManagement-CxIBH7lA.js} +1 -1
  12. package/dist/assets/{CourseWizardModal-oVT9mYSG.js → CourseWizardModal-R0xpa_Aw.js} +1 -1
  13. package/dist/assets/{CoursewareHubPanel-BuKp0mmW.js → CoursewareHubPanel-CpHptmSa.js} +1 -1
  14. package/dist/assets/{Dashboard-Vb9coh9_.js → Dashboard-DeBfP_Nc.js} +2 -2
  15. package/dist/assets/{ExportWeightModal-BEesz6rd.js → ExportWeightModal-DlnCOBlL.js} +1 -1
  16. package/dist/assets/{HelpTour-D3QcGDUz.js → HelpTour-Cztpqw_J.js} +1 -1
  17. package/dist/assets/{HelpView--Um9DFYx.js → HelpView-LPLAY9bq.js} +2 -2
  18. package/dist/assets/{ImportLessonsModal-DXGopj4j.js → ImportLessonsModal-BopEHmU_.js} +1 -1
  19. package/dist/assets/ImportModal-CYU3aMYY.js +3 -0
  20. package/dist/assets/{InteractiveCoursewareViewer-DzgF9_LU.js → InteractiveCoursewareViewer-CGc3P4HF.js} +1 -1
  21. package/dist/assets/InteractiveWhiteboard-DiyHrIGn.js +19 -0
  22. package/dist/assets/{LazyCourseware-6yb_QiYS.js → LazyCourseware-Bj1jm0nC.js} +2 -2
  23. package/dist/assets/LazyWhiteboard-NPD1EoyO.js +2 -0
  24. package/dist/assets/LessonEditorView-DqreBtFx.js +1 -0
  25. package/dist/assets/LiveClassroomView-eTc6Fb9g.js +64 -0
  26. package/dist/assets/{NotificationDetailModal-mUeIQP4R.js → NotificationDetailModal-B7G7ra9a.js} +1 -1
  27. package/dist/assets/{PluginView-CT3-zS2n.js → PluginView-DP6s-L70.js} +1 -1
  28. package/dist/assets/{ProcessLogsModal-DmsOrZnV.js → ProcessLogsModal-Dq5C4s7j.js} +1 -1
  29. package/dist/assets/{QuizGeneratorModal-ViKxNX5Q.js → QuizGeneratorModal-CoX9f8Wp.js} +1 -1
  30. package/dist/assets/RevealPresentationWrapper-wwlcfNWB.js +48 -0
  31. package/dist/assets/{StudentAssignmentEvalPanel-BRxHMABY.js → StudentAssignmentEvalPanel-DHb-asHQ.js} +1 -1
  32. package/dist/assets/{StudentAssignmentView-g-ewJIS3.js → StudentAssignmentView-AJpjQK3f.js} +1 -1
  33. package/dist/assets/{StudentLessonView-Dbvr3Xlo.js → StudentLessonView-Djd1T1QF.js} +1 -1
  34. package/dist/assets/{StudentView-20vfEk4v.js → StudentView-D41z7GY5.js} +2 -2
  35. package/dist/assets/{SystemResourceLibraryModal-CDKeaKf8.js → SystemResourceLibraryModal-CxmQh1HA.js} +1 -1
  36. package/dist/assets/TeacherView-DqSWaNuI.js +2 -0
  37. package/dist/assets/TimetableView-DrFs_xgc.js +6 -0
  38. package/dist/assets/{UTIF-BtQhea94.js → UTIF-u3Ct70K4.js} +1 -1
  39. package/dist/assets/index-7CK1B0tE.js +131 -0
  40. package/dist/assets/index-BaGZmpr6.css +1 -0
  41. package/dist/assets/{index.es-CijVATXs.js → index.es-CcQ2kcDj.js} +1 -1
  42. package/dist/assets/pluginParsers-BN3lQ7PJ.js +1 -0
  43. package/dist/assets/{student-workspace-DstiW0pM.js → student-workspace-DkTkcAGK.js} +1 -1
  44. package/dist/assets/{vendor-icons-DLOPlM4i.js → vendor-icons-CqLdl4B_.js} +1 -1
  45. package/dist/assets/{vendor-pdf-DVWGghIL.js → vendor-pdf-BMEc_Clb.js} +3 -3
  46. package/dist/assets/{vendor-pptx-BqU7v5zS.js → vendor-pptx-C0qaOH5T.js} +178 -178
  47. package/dist/index.html +3 -3
  48. package/dist/server.cjs +1666 -1846
  49. package/migrations/045_ai_providers_owner_and_scope.sql +20 -0
  50. package/migrations/README.md +1 -0
  51. package/migrations/schema-golden.json +13 -3
  52. package/package.json +5 -2
  53. package/dist/assets/AdminDirectoryView-Cdsk9Mdr.js +0 -5
  54. package/dist/assets/ImportModal-h5ShIMzO.js +0 -2
  55. package/dist/assets/InteractiveWhiteboard-DJqgP_tU.js +0 -65
  56. package/dist/assets/LazyWhiteboard-CsF9GB6J.js +0 -2
  57. package/dist/assets/LessonEditorView-CZTZ7Q0B.js +0 -1
  58. package/dist/assets/LiveClassroomView-D2ugupT9.js +0 -61
  59. package/dist/assets/TeacherView-KIKuhXGF.js +0 -2
  60. package/dist/assets/TimetableView-7V2AwWml.js +0 -6
  61. package/dist/assets/index-C4cHQZyx.css +0 -1
  62. package/dist/assets/index-XduPa2-x.js +0 -131
  63. package/dist/assets/pluginParsers-BO6U1E-h.js +0 -1
  64. package/dist/assets/vendor-reveal-DcQQzuI9.css +0 -1
package/dist/server.cjs CHANGED
@@ -3304,6 +3304,95 @@ var init_ai_event_bus = __esm({
3304
3304
  }
3305
3305
  });
3306
3306
 
3307
+ // packages/core/di/ai-provider-resolver.ts
3308
+ function hasOwnerColumns(db2) {
3309
+ try {
3310
+ const cols = db2.prepare("PRAGMA table_info(ai_providers)").all();
3311
+ return cols.some((c) => c.name === "owner_id");
3312
+ } catch {
3313
+ return false;
3314
+ }
3315
+ }
3316
+ function resolveAccessibleAiProvider(db2, options = {}) {
3317
+ const { providerId, userId, lang = "zh" } = options;
3318
+ const supportsOwner = hasOwnerColumns(db2);
3319
+ let provider;
3320
+ if (providerId) {
3321
+ const row = supportsOwner ? db2.prepare(
3322
+ "SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at FROM ai_providers WHERE id = ?"
3323
+ ).get(providerId) : db2.prepare("SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE id = ?").get(providerId);
3324
+ if (!row) {
3325
+ throw new AiProviderError(
3326
+ lang === "zh" ? "\u672A\u627E\u5230\u6307\u5B9A\u7684 AI \u63D0\u4F9B\u5546" : "Specified AI Provider not found",
3327
+ 404,
3328
+ "PROVIDER_NOT_FOUND"
3329
+ );
3330
+ }
3331
+ if (supportsOwner) {
3332
+ const isShared = Number(row.is_shared) === 1 || row.owner_id === null;
3333
+ const isOwner = Boolean(userId && row.owner_id === userId);
3334
+ if (!isShared && !isOwner) {
3335
+ throw new AiProviderError(
3336
+ lang === "zh" ? "\u65E0\u6743\u4F7F\u7528\u8BE5 AI \u63D0\u4F9B\u5546\uFF08\u975E\u672C\u4EBA\u79C1\u6709\u4E14\u672A\u5171\u4EAB\uFF09" : "Forbidden: You do not have access to this private AI provider",
3337
+ 403,
3338
+ "PROVIDER_ACCESS_DENIED"
3339
+ );
3340
+ }
3341
+ }
3342
+ provider = row;
3343
+ } else {
3344
+ if (!supportsOwner) {
3345
+ provider = db2.prepare(
3346
+ "SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' ORDER BY created_at DESC LIMIT 1"
3347
+ ).get();
3348
+ } else if (userId) {
3349
+ provider = db2.prepare(
3350
+ `SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at
3351
+ FROM ai_providers
3352
+ WHERE (owner_id = ? OR is_shared = 1 OR owner_id IS NULL)
3353
+ AND api_key IS NOT NULL AND api_key != ''
3354
+ ORDER BY (CASE WHEN owner_id = ? THEN 0 WHEN is_shared = 1 THEN 1 ELSE 2 END), created_at DESC
3355
+ LIMIT 1`
3356
+ ).get(userId, userId);
3357
+ } else {
3358
+ provider = db2.prepare(
3359
+ `SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at
3360
+ FROM ai_providers
3361
+ WHERE (is_shared = 1 OR owner_id IS NULL)
3362
+ AND api_key IS NOT NULL AND api_key != ''
3363
+ ORDER BY (CASE WHEN is_shared = 1 THEN 0 ELSE 1 END), created_at DESC
3364
+ LIMIT 1`
3365
+ ).get();
3366
+ }
3367
+ }
3368
+ if (provider?.api_key) {
3369
+ provider.api_key = decryptApiKey(provider.api_key);
3370
+ }
3371
+ if (!provider || !provider.api_key || !provider.api_key.trim()) {
3372
+ throw new AiProviderError(
3373
+ lang === "zh" ? "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002" : "No active AI Provider configured. Please add and configure an AI Provider in settings.",
3374
+ 400,
3375
+ "NO_AI_PROVIDER"
3376
+ );
3377
+ }
3378
+ return provider;
3379
+ }
3380
+ var AiProviderError;
3381
+ var init_ai_provider_resolver = __esm({
3382
+ "packages/core/di/ai-provider-resolver.ts"() {
3383
+ "use strict";
3384
+ init_api_key_crypto();
3385
+ AiProviderError = class extends Error {
3386
+ constructor(message, status = 400, code = "AI_PROVIDER_ERROR") {
3387
+ super(message);
3388
+ this.name = "AiProviderError";
3389
+ this.status = status;
3390
+ this.code = code;
3391
+ }
3392
+ };
3393
+ }
3394
+ });
3395
+
3307
3396
  // packages/core/di/ai-service.ts
3308
3397
  var AIService;
3309
3398
  var init_ai_service = __esm({
@@ -3312,6 +3401,7 @@ var init_ai_service = __esm({
3312
3401
  init_provider_gateway();
3313
3402
  init_ai_event_bus();
3314
3403
  init_api_key_crypto();
3404
+ init_ai_provider_resolver();
3315
3405
  AIService = class {
3316
3406
  constructor(db2) {
3317
3407
  this.db = db2;
@@ -3359,16 +3449,11 @@ var init_ai_service = __esm({
3359
3449
  this._contextDelegates.unregister?.(id);
3360
3450
  }
3361
3451
  async generateText(prompt, options) {
3362
- const provider = this.db.prepare(
3363
- "SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' LIMIT 1"
3364
- ).get();
3365
- if (!provider || !provider.api_key) {
3366
- throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002");
3367
- }
3368
- const apiKey = decryptApiKey(provider.api_key);
3369
- if (!apiKey || !apiKey.trim()) {
3370
- throw new Error("AI Provider API Key \u89E3\u5BC6\u4E3A\u7A7A\uFF0C\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58\u5BC6\u94A5\u3002");
3371
- }
3452
+ const provider = resolveAccessibleAiProvider(this.db, {
3453
+ userId: options?.userId,
3454
+ providerId: options?.providerId
3455
+ });
3456
+ const apiKey = provider.api_key;
3372
3457
  if (looksLikeCiphertext(apiKey)) {
3373
3458
  throw new Error(
3374
3459
  "AI Provider API Key \u89E3\u5BC6\u5931\u8D25\uFF1AENCRYPTION_KEY \u4E0E\u52A0\u5BC6\u65F6\u4E0D\u4E00\u81F4\uFF08\u5E38\u89C1\u4E8E PM2 \u5C06 ENCRYPTION_KEY \u7F6E\u7A7A\u6216\u5BC6\u94A5\u88AB\u8F6E\u6362\uFF09\u3002\u8BF7\u6062\u590D\u539F ENCRYPTION_KEY\uFF0C\u6216\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58 API Key\u3002"
@@ -9211,6 +9296,7 @@ var init_di = __esm({
9211
9296
  init_storage_service();
9212
9297
  init_ai_service();
9213
9298
  init_courseware_runtime_script_registry();
9299
+ init_ai_provider_resolver();
9214
9300
  init_container();
9215
9301
  }
9216
9302
  });
@@ -13409,8 +13495,8 @@ var ManagementPlugin = {
13409
13495
  });
13410
13496
  await commandBus.registerHandler(classTemplateDownloadCmd, {
13411
13497
  async execute() {
13412
- const headers = "Class Name,Class Desc,Student Name,Student Email";
13413
- const sampleRow = "Class 101,Introduction to English,John Doe,john@example.com\nClass 101,Introduction to English,Jane Smith,jane@example.com";
13498
+ const headers = "Class Name,Class Desc,Student Number,Student Name,Student Email,Initial Password,Student Notes,Gender";
13499
+ const sampleRow = "Class 101,Introduction to English,20260101,John Doe,john@example.com,,,Male\nClass 101,Introduction to English,20260102,Jane Smith,jane@example.com,,,Female";
13414
13500
  const content = `${headers}
13415
13501
  ${sampleRow}`;
13416
13502
  const fileId = (0, import_uuid4.v7)();
@@ -13442,8 +13528,8 @@ ${sampleRow}`;
13442
13528
  });
13443
13529
  await commandBus.registerHandler(studentTemplateDownloadCmd, {
13444
13530
  async execute() {
13445
- const headers = "Student Name,Student Email";
13446
- const sampleRow = "Alice Cooper,alice@example.com\nBob Dylan,bob@example.com";
13531
+ const headers = "Student Number,Student Name,Student Email,Initial Password,Student Notes,Gender";
13532
+ const sampleRow = "20260101,Alice Cooper,alice@example.com,,,Male\n20260102,Bob Dylan,bob@example.com,,,Female";
13447
13533
  const content = `${headers}
13448
13534
  ${sampleRow}`;
13449
13535
  const fileId = (0, import_uuid4.v7)();
@@ -21204,7 +21290,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
21204
21290
  var import_node_path13 = __toESM(require("node:path"), 1);
21205
21291
  var import_node_url5 = require("node:url");
21206
21292
  var import_meta7 = {};
21207
- var FALLBACK_VERSION = "0.7.10";
21293
+ var FALLBACK_VERSION = "0.7.13";
21208
21294
  function readPlatformVersion() {
21209
21295
  const bases = [];
21210
21296
  if (typeof __dirname !== "undefined") bases.push(__dirname);
@@ -29897,1298 +29983,366 @@ var ServiceRegistryKernel = class {
29897
29983
  }
29898
29984
  };
29899
29985
 
29900
- // packages/core/bootstrap/composition/composition-validator.ts
29901
- var CompositionValidator = class {
29902
- static validate(options, pipeline, modules) {
29903
- const errors = [];
29904
- const warnings = [];
29905
- if (options.config && options.config.port !== void 0) {
29906
- if (options.config.port <= 0 || options.config.port > 65535) {
29907
- errors.push({
29908
- code: "INVALID_PORT",
29909
- message: `Port must be a valid number between 1 and 65535. Received: ${options.config.port}`,
29910
- target: "config.port"
29911
- });
29912
- }
29913
- }
29914
- if (!pipeline || pipeline.stages.length === 0) {
29915
- errors.push({
29916
- code: "EMPTY_PIPELINE",
29917
- message: "Bootstrap pipeline is missing or contains zero stages.",
29918
- target: "pipeline"
29919
- });
29986
+ // packages/core/kernel/index.ts
29987
+ var import_path12 = __toESM(require("path"), 1);
29988
+ var Kernel = class {
29989
+ /**
29990
+ * @param opts.pluginsDir - 插件产物目录。生产默认 `<cwd>/plugins`;
29991
+ * 测试**必须**显式传入临时目录,否则插件产物会落进工作树。
29992
+ *
29993
+ * ## 为什么要开这个口子(H-1)
29994
+ *
29995
+ * 原本这里硬编码 `path.resolve(process.cwd(), 'plugins')` 且不可覆盖。
29996
+ * 后果不只是「测试会弄脏工作树」,更根本的是 **Kernel 在这个维度上不可测** ——
29997
+ * 没有任何办法在测试里把插件产物引到别处,于是污染一直无人察觉:
29998
+ * 实测仓库 `plugins/` 下积累了 **1705 个孤儿目录 / 24MB**,且每次 `pnpm test` 继续增长。
29999
+ * 该目录被 `.gitignore` 忽略,所以 `git status` 完全看不见。
30000
+ *
30001
+ * 加可选参数后,生产行为**逐字节不变**(默认值相同),而测试可把产物引到
30002
+ * `os.tmpdir()`。`vitest.setup.ts` 的全局守卫会把「仍写工作树」变成硬失败,
30003
+ * 防止将来有人再引入同类调用。
30004
+ */
30005
+ constructor(opts) {
30006
+ this.db = db;
30007
+ this.readPool = readPool;
30008
+ this.getReadDb = getReadDb;
30009
+ this.queryRead = queryRead;
30010
+ this.queryReadOne = queryReadOne;
30011
+ this.checkpoint = checkpoint;
30012
+ this.eventBus = new EventBus();
30013
+ this.capabilityGuard = new CapabilityGuard();
30014
+ this.serviceRegistry = new ServiceRegistry();
30015
+ this.storageService = new StorageService(this.db);
30016
+ this.aiService = new AIService(this.db);
30017
+ this.aiRuntime = new AIRuntimeKernel();
30018
+ this.aiCapability = new AICapabilityKernel(this.aiRuntime);
30019
+ this.capabilityFrameworkRuntime = new CapabilityRuntimeKernel(this.aiCapability);
30020
+ this.capabilityGovernance = new CapabilityGovernanceKernel();
30021
+ this.platformServiceRegistryKernel = new ServiceRegistryKernel();
30022
+ this.analyticsEngine = new AnalyticsEngineKernel();
30023
+ this.collaborationEngine = new CollaborationEngineKernel();
30024
+ this.presenceEngine = new PresenceEngineKernel();
30025
+ this.classroomRuntime = new ClassroomRuntimeKernel();
30026
+ this.lessonRuntime = new LessonRuntime({ eventBus: this.eventBus });
30027
+ this.lessonRuntime.aiInterface.setAIService(this.aiService);
30028
+ this.commandBus = new CommandBus(this.eventBus);
30029
+ this.actionRegistry = new ActionRegistry();
30030
+ this.processManager = new ProcessManager(this);
30031
+ this.esmLoader = new NodeEsmLoader();
30032
+ const pluginsDir = opts?.pluginsDir ?? import_path12.default.resolve(process.cwd(), "plugins");
30033
+ try {
30034
+ import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
30035
+ } catch (err) {
30036
+ const detail = err instanceof Error ? err.message : String(err);
30037
+ throw new Error(
30038
+ `[Kernel] FATAL: \u65E0\u6CD5\u521B\u5EFA\u63D2\u4EF6\u76EE\u5F55 ${pluginsDir}\uFF08${detail}\uFF09\u3002\u5BB9\u5668\u8BF7\u628A OPENLEARN_DATA_DIR \u6216 OPENLEARN_PLUGINS_DIR \u6307\u5230\u53EF\u5199\u5377\uFF0C\u4F8B\u5982 /data/plugins\u3002`
30039
+ );
29920
30040
  }
29921
- const moduleIds = /* @__PURE__ */ new Set();
29922
- for (const mod of modules) {
29923
- if (!mod.id) {
29924
- errors.push({
29925
- code: "INVALID_MODULE",
29926
- message: `Module '${mod.name}' has no unique ID.`,
29927
- target: "module.id"
29928
- });
29929
- } else if (moduleIds.has(mod.id)) {
29930
- errors.push({
29931
- code: "DUPLICATE_MODULE",
29932
- message: `Duplicate composition module detected: '${mod.id}'.`,
29933
- target: "module.id"
29934
- });
30041
+ this.pluginHost = new PluginHost(this.serviceRegistry, this.esmLoader, this.db, pluginsDir);
30042
+ setPluginNamespaceResolver((namespace) => this.pluginHost.findByManifestId(namespace) ? namespace : void 0);
30043
+ this.workerManager = new WorkerManager(this.serviceRegistry, this.capabilityGuard, this.db);
30044
+ this.pluginHost.setWorkerManager(this.workerManager);
30045
+ const capabilityRegistry = this.aiCapability.registry;
30046
+ this.pluginRuntimeComposition = new PluginRuntimeComposition(this.pluginHost, this.workerManager);
30047
+ this.pluginLifecycleManager = new PluginLifecycleManager(this.pluginHost);
30048
+ this.pluginDistributionManager = new PluginDistributionManager(this.pluginHost);
30049
+ this.unifiedExtensionRegistry = new UnifiedExtensionRegistry();
30050
+ this.capabilityRegistry = capabilityRegistry;
30051
+ this.pluginCapabilityGateway = new PluginCapabilityGateway(capabilityRegistry);
30052
+ this.serviceRegistry.register(IEventBusServiceToken3, this.eventBus);
30053
+ this.serviceRegistry.register(ICapabilityServiceToken, this.capabilityGuard);
30054
+ this.serviceRegistry.register(IStorageServiceToken, this.storageService);
30055
+ this.serviceRegistry.register(ICommandBusServiceToken4, this.commandBus);
30056
+ this.serviceRegistry.register(IActionRegistryServiceToken4, this.actionRegistry);
30057
+ this.serviceRegistry.register(IProcessServiceToken2, this.processManager);
30058
+ this.serviceRegistry.register(IAIServiceToken, this.aiService);
30059
+ this.serviceRegistry.register(IDatabaseToken4, this.db);
30060
+ this.serviceRegistry.register(IPluginHostToken, this.pluginHost);
30061
+ this.serviceRegistry.register(IPluginLifecycleManagerToken, this.pluginLifecycleManager);
30062
+ this.serviceRegistry.register(IPluginDistributionManagerToken, this.pluginDistributionManager);
30063
+ this.serviceRegistry.register(IPluginRuntimeCompositionToken, this.pluginRuntimeComposition);
30064
+ this.serviceRegistry.register(IUnifiedExtensionRegistryToken, this.unifiedExtensionRegistry);
30065
+ this.serviceRegistry.register(IPluginCapabilityGatewayToken, this.pluginCapabilityGateway);
30066
+ this.serviceRegistry.register(ICapabilityRegistryToken, this.capabilityRegistry);
30067
+ this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
30068
+ this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
30069
+ this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
30070
+ this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
30071
+ this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
30072
+ this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
30073
+ getRuntimeKernel: async () => this.classroomRuntime
30074
+ });
30075
+ this.serviceRegistry.register(IPresenceEngineServiceToken, {
30076
+ getPresenceEngine: async () => this.presenceEngine
30077
+ });
30078
+ this.serviceRegistry.register(ITeachingCollaborationServiceToken, {
30079
+ getCollaborationEngine: async () => this.collaborationEngine
30080
+ });
30081
+ this.serviceRegistry.register(ILearningAnalyticsServiceToken, {
30082
+ getAnalyticsEngine: async () => this.analyticsEngine
30083
+ });
30084
+ this.serviceRegistry.register(IAICapabilityServiceToken, {
30085
+ getCapabilityKernel: async () => this.aiCapability
30086
+ });
30087
+ this.serviceRegistry.register(ICapabilityRuntimeServiceToken, {
30088
+ getRuntimeKernel: async () => this.capabilityFrameworkRuntime
30089
+ });
30090
+ this.serviceRegistry.register(ICapabilityGovernanceServiceToken, {
30091
+ getGovernanceKernel: async () => this.capabilityGovernance
30092
+ });
30093
+ this.serviceRegistry.register(IPlatformServiceRegistryToken, {
30094
+ getServiceRegistryKernel: async () => this.platformServiceRegistryKernel
30095
+ });
30096
+ this.serviceRegistry.register(IAuthSessionBridgeToken, new AuthSessionBridgeService(this.db));
30097
+ this.serviceRegistry.register(ICoursewareRuntimeScriptRegistryToken, new CoursewareRuntimeScriptRegistry());
30098
+ this.serviceRegistry.register(IStageGuardServiceToken, defaultStageGuardPipeline);
30099
+ this.serviceRegistry.register(IAssessmentGraderRegistryToken, assessmentGraderRegistry);
30100
+ this.commandBus.setInterceptor(async (command) => {
30101
+ const action = this.actionRegistry.getActionByCommandType(command.type);
30102
+ if (action) {
30103
+ if (action.inputSchema) {
30104
+ const errors = validateJsonSchema(command.payload, action.inputSchema);
30105
+ if (errors.length > 0) {
30106
+ throw new Error(
30107
+ `[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`
30108
+ );
30109
+ }
30110
+ }
30111
+ const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || typeof command.actorId === "string" && command.actorId.startsWith("user:") && (command.actorId.endsWith(":administrator") || command.actorId.endsWith(":admin"));
30112
+ if (action.capabilityRequired && !isAdmin) {
30113
+ const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
30114
+ if (!allowed) {
30115
+ const err = new Error(
30116
+ `[CapabilityGuard] Access Denied: Actor ${command.actorId} missing capability ${action.capabilityRequired} for ${command.type}`
30117
+ );
30118
+ err.status = 403;
30119
+ err.code = "COMMAND_CAPABILITY_DENIED";
30120
+ throw err;
30121
+ }
30122
+ }
30123
+ const pluginSuppliedApproval = typeof command.actorId === "string" && command.actorId.startsWith("plugin:");
30124
+ if (action.isHighRisk && (command.metadata?.approved !== true || pluginSuppliedApproval)) {
30125
+ if (isAdmin && !pluginSuppliedApproval) {
30126
+ console.log(
30127
+ `[Security] Command ${command.type} initiated by Administrator (${command.actorId}). Bypassing human approval.`
30128
+ );
30129
+ } else {
30130
+ const stmt = this.db.prepare(
30131
+ "INSERT INTO pending_commands (id, command_type, payload, actor_id, created_at) VALUES (?, ?, ?, ?, ?)"
30132
+ );
30133
+ stmt.run(command.id, command.type, JSON.stringify(command.payload), command.actorId, Date.now());
30134
+ this.eventBus.publish({
30135
+ id: (0, import_uuid15.v7)(),
30136
+ type: "approval.requested",
30137
+ source: "kernel.security",
30138
+ payload: { commandId: command.id, commandType: command.type },
30139
+ timestamp: Date.now(),
30140
+ correlationId: command.id
30141
+ });
30142
+ throw new Error(
30143
+ `[Security] Command ${command.type} requires human approval. It has been queued to pending actions.`
30144
+ );
30145
+ }
30146
+ }
29935
30147
  } else {
29936
- moduleIds.add(mod.id);
30148
+ const err = new Error(
30149
+ `[Security] Command "${command.type}" has no registered action descriptor and is denied by default. Only commands registered via actionRegistry.register() may be dispatched. If you own this command, register an ActionDescriptor (capabilityRequired + inputSchema).`
30150
+ );
30151
+ err.status = 403;
30152
+ err.code = "COMMAND_NOT_REGISTERED";
30153
+ throw err;
30154
+ }
30155
+ });
30156
+ Promise.resolve().then(() => (init_context_builder(), context_builder_exports)).then((m) => m.bootstrapSharedModules()).catch((err) => {
30157
+ console.warn("[Kernel] Failed to bootstrap shared modules:", err.message);
30158
+ });
30159
+ this.ready = this.bootstrapSystemPlugins().catch((err) => {
30160
+ console.error("[Kernel] Critical system plugin bootstrap failed:", err);
30161
+ process.exit(1);
30162
+ });
30163
+ if (process.env.NODE_ENV === "development") {
30164
+ const watchDir = this.pluginHost.getPluginsDir();
30165
+ try {
30166
+ const hotReload = new HotReloadController(this.pluginHost, watchDir);
30167
+ this.pluginHost.setHotReloadController(hotReload);
30168
+ hotReload.start().catch((err) => {
30169
+ console.warn("[Kernel] Hot reload initialization failed:", err.message);
30170
+ });
30171
+ } catch (err) {
30172
+ console.warn("[Kernel] Hot reload initialization failed:", err.message);
29937
30173
  }
29938
30174
  }
29939
- return {
29940
- isValid: errors.length === 0,
29941
- errors: Object.freeze(errors),
29942
- warnings: Object.freeze(warnings)
29943
- };
29944
30175
  }
29945
- };
29946
-
29947
- // packages/core/bootstrap/builder/builder-validation-engine.ts
29948
- var BuilderValidationEngine = class {
29949
- static validate(config, environment, stages) {
29950
- const errors = [];
29951
- const warnings = [];
29952
- if (!["development", "production", "test"].includes(environment)) {
29953
- errors.push({
29954
- code: "INVALID_ENVIRONMENT",
29955
- message: `Environment must be one of 'development', 'production', 'test'. Received: '${environment}'`,
29956
- field: "environment"
29957
- });
30176
+ async bootstrapSystemPlugins() {
30177
+ await this.migratePluginsToFilesystem();
30178
+ const systemPlugins = [
30179
+ { id: "@openlearn/plugin-vfs", mod: VfsPlugin, name: "Virtual File System Plugin", critical: true },
30180
+ { id: "@openlearn/plugin-process", mod: ProcessPlugin, name: "Background Process Plugin", critical: true },
30181
+ { id: "@openlearn/plugin-management", mod: ManagementPlugin, name: "LMS Management Plugin", critical: true },
30182
+ { id: "@openlearn/plugin-builtin", mod: BuiltinPlugin, name: "Classroom Builtin Plugin", critical: true },
30183
+ { id: "@openlearn/plugin-ai-planner", mod: AiPlannerPlugin, name: "AI Planner Plugin", critical: false },
30184
+ {
30185
+ id: "@openlearn/plugin-ai-submit-injector",
30186
+ mod: AiSubmitInjectorPlugin,
30187
+ name: "AI Submit Injector Plugin",
30188
+ critical: false
30189
+ },
30190
+ {
30191
+ id: "@openlearn/plugin-assignment-eval",
30192
+ mod: AssignmentEvalPlugin,
30193
+ name: "Assignment Evaluation and Peer Review Plugin",
30194
+ critical: false
30195
+ }
30196
+ ];
30197
+ for (const plugin of systemPlugins) {
30198
+ try {
30199
+ let row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(plugin.id);
30200
+ if (!row) {
30201
+ this.db.prepare(
30202
+ "INSERT INTO plugins (id, name, manifest, source_code, file_path, status, created_at, loader_version, execution_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
30203
+ ).run(
30204
+ plugin.id,
30205
+ plugin.name,
30206
+ JSON.stringify(plugin.mod.manifest),
30207
+ "",
30208
+ // source_code: system plugins are preloaded in-memory
30209
+ null,
30210
+ // file_path: system plugins have no file
30211
+ "installed",
30212
+ Date.now(),
30213
+ "esm",
30214
+ "inline"
30215
+ );
30216
+ }
30217
+ this.pluginHost.registerPreloadedPlugin(
30218
+ plugin.id,
30219
+ plugin.mod
30220
+ );
30221
+ await this.pluginHost.activatePlugin(plugin.id);
30222
+ } catch (err) {
30223
+ if (plugin.critical) {
30224
+ console.error(`[Kernel] Failed to bootstrap critical system plugin ${plugin.name}:`, err);
30225
+ throw err;
30226
+ } else {
30227
+ console.warn(`[Kernel] Soft-fail: Failed to bootstrap AI system plugin ${plugin.name}:`, err);
30228
+ }
30229
+ }
29958
30230
  }
29959
- if (!config.port || config.port <= 0 || config.port > 65535) {
29960
- errors.push({
29961
- code: "INVALID_PORT",
29962
- message: `Port must be a valid number between 1 and 65535. Received: ${config.port}`,
29963
- field: "port"
29964
- });
30231
+ try {
30232
+ await this.pluginHost.restoreActivePlugins();
30233
+ } catch (err) {
30234
+ console.error("[Kernel] Failed to restore active plugins:", err);
29965
30235
  }
29966
- if (!stages || stages.length === 0) {
29967
- errors.push({
29968
- code: "MISSING_STAGES",
29969
- message: "Bootstrap pipeline contains no registered stages.",
29970
- field: "stages"
29971
- });
29972
- } else {
29973
- const stageIds = /* @__PURE__ */ new Set();
29974
- for (const stage of stages) {
29975
- if (!stage.id) {
29976
- errors.push({
29977
- code: "INVALID_STAGE",
29978
- message: `Stage '${stage.name}' has no unique ID.`
29979
- });
29980
- } else if (stageIds.has(stage.id)) {
29981
- errors.push({
29982
- code: "DUPLICATE_STAGE",
29983
- message: `Duplicate stage registration detected for stage ID '${stage.id}'.`,
29984
- field: "stage.id"
29985
- });
29986
- } else {
29987
- stageIds.add(stage.id);
30236
+ }
30237
+ /**
30238
+ * Phase 7: 将 DB 中存有 source_code 的旧插件迁移到文件系统(幂等)
30239
+ */
30240
+ async migratePluginsToFilesystem() {
30241
+ const plugins = this.db.prepare("SELECT id, source_code, manifest FROM plugins WHERE source_code != '' AND source_code IS NOT NULL").all();
30242
+ if (plugins.length === 0) return;
30243
+ console.log(`[Migration] Found ${plugins.length} plugin(s) to migrate to filesystem`);
30244
+ const pluginsDir = this.pluginHost.getPluginsDir();
30245
+ for (const p of plugins) {
30246
+ const pluginDir = import_path12.default.join(pluginsDir, p.id);
30247
+ const indexPath = import_path12.default.join(pluginDir, "index.js");
30248
+ const manifestPath = import_path12.default.join(pluginDir, "manifest.json");
30249
+ if (import_fs13.default.existsSync(indexPath)) {
30250
+ this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
30251
+ continue;
30252
+ }
30253
+ try {
30254
+ import_fs13.default.mkdirSync(pluginDir, { recursive: true });
30255
+ import_fs13.default.writeFileSync(indexPath, p.source_code, "utf-8");
30256
+ if (!import_fs13.default.existsSync(manifestPath)) {
30257
+ import_fs13.default.writeFileSync(manifestPath, p.manifest, "utf-8");
29988
30258
  }
30259
+ this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
30260
+ console.log(`[Migration] Plugin "${p.id}" migrated to ${indexPath}`);
30261
+ } catch (err) {
30262
+ console.error(`[Migration] Failed to migrate plugin "${p.id}":`, err);
29989
30263
  }
29990
30264
  }
29991
- return {
29992
- isValid: errors.length === 0,
29993
- errors: Object.freeze(errors),
29994
- warnings: Object.freeze(warnings)
29995
- };
30265
+ console.log("[Migration] Plugin migration complete");
29996
30266
  }
29997
- };
29998
-
29999
- // packages/core/bootstrap/types/index.ts
30000
- var PlatformBootstrapError = class extends Error {
30001
- constructor(message, stage, cause) {
30002
- super(`[PlatformBootstrapError${stage ? `:${stage}` : ""}] ${message}`);
30003
- this.stage = stage;
30004
- this.cause = cause;
30005
- this.name = "PlatformBootstrapError";
30267
+ // Subscribe to all events and log them to DB
30268
+ initAuditLog() {
30269
+ let stmt = null;
30270
+ let hasLessonIdColumn = null;
30271
+ this.eventBus.subscribe("*", (event) => {
30272
+ const values = [
30273
+ event.id,
30274
+ event.type,
30275
+ event.source,
30276
+ JSON.stringify(redactPasscodeMaterial(event.payload)),
30277
+ event.timestamp,
30278
+ event.correlationId || null
30279
+ ];
30280
+ if (hasLessonIdColumn === null) {
30281
+ const columns = this.db.prepare("PRAGMA table_info(events)").all();
30282
+ hasLessonIdColumn = columns.some((c) => c.name === "lesson_id");
30283
+ stmt = this.db.prepare(hasLessonIdColumn ? INSERT_EVENT_WITH_LESSON : INSERT_EVENT_LEGACY);
30284
+ }
30285
+ if (hasLessonIdColumn) {
30286
+ stmt.run(...values, extractEventLessonId(event.payload));
30287
+ } else {
30288
+ stmt.run(...values);
30289
+ }
30290
+ });
30006
30291
  }
30007
30292
  };
30008
- var ConfigurationError = class extends PlatformBootstrapError {
30009
- constructor(message, cause) {
30010
- super(message, "Configuring" /* Configuring */, cause);
30011
- this.name = "ConfigurationError";
30293
+ var INSERT_EVENT_WITH_LESSON = "INSERT INTO events (id, type, source, payload, timestamp, correlationId, lesson_id) VALUES (?, ?, ?, ?, ?, ?, ?)";
30294
+ var INSERT_EVENT_LEGACY = "INSERT INTO events (id, type, source, payload, timestamp, correlationId) VALUES (?, ?, ?, ?, ?, ?)";
30295
+ function extractEventLessonId(payload) {
30296
+ if (!payload || typeof payload !== "object") return null;
30297
+ const record = payload;
30298
+ for (const key of ["lessonId", "lesson_id"]) {
30299
+ const value = record[key];
30300
+ if (typeof value === "string" && value.length > 0) return value;
30012
30301
  }
30013
- };
30014
- var StartupTimeoutError = class extends PlatformBootstrapError {
30015
- constructor(message, stage, cause) {
30016
- super(message, stage, cause);
30017
- this.name = "StartupTimeoutError";
30302
+ return null;
30303
+ }
30304
+ function resolvePluginsDirOverride() {
30305
+ const raw = process.env.OPENLEARN_PLUGINS_DIR;
30306
+ if (typeof raw === "string" && raw.length > 0) return raw;
30307
+ const dataDir = process.env.OPENLEARN_DATA_DIR?.trim();
30308
+ if (dataDir) return import_path12.default.join(import_path12.default.resolve(dataDir), "plugins");
30309
+ if (process.env.VITEST) {
30310
+ throw new Error(
30311
+ "[H-1] \u6D4B\u8BD5\u73AF\u5883\u4E0B\u672A\u80FD\u786E\u5B9A\u63D2\u4EF6\u76EE\u5F55\uFF0C\u62D2\u7EDD\u56DE\u9000\u5230 process.cwd()/plugins\u3002\n \u539F\u56E0\uFF1A\u56DE\u9000\u4F1A\u628A\u63D2\u4EF6\u4EA7\u7269\u5199\u8FDB\u4ED3\u5E93\u5DE5\u4F5C\u6811\uFF1B\u8BE5\u76EE\u5F55\u88AB .gitignore \u5FFD\u7565\uFF0C\u6C61\u67D3\u5B8C\u5168\u9690\u5F62\u3002\n \u4FEE\u6CD5\uFF08\u62E9\u4E00\uFF09\uFF1A\n 1. \u8BE5\u6D4B\u8BD5\u81EA\u5DF1 new Kernel({ pluginsDir })\uFF0C\u7528 __tests__/helpers/plugins-dir \u7684\n createPluginsDir('\u7528\u9014') \u53D6\u5F97\uFF0C\u5E76\u5728 afterEach \u91CC cleanupPluginsDir\uFF1B\n 2. \u82E5\u7528\u7684\u662F kernelContainer \u5355\u4F8B\uFF0C\u786E\u8BA4\u5B83\u662F\u5728 vitest.setup.ts \u8D4B\u503C\n OPENLEARN_PLUGINS_DIR \u4E4B\u540E\u624D\u88AB\u9996\u6B21\u8BBF\u95EE\u3002"
30312
+ );
30018
30313
  }
30019
- };
30020
- var PLATFORM_VERSION2 = PLATFORM_VERSION;
30021
- var BOOTSTRAP_STAGE_NAMES = Object.freeze([
30022
- "Created" /* Created */,
30023
- "Configuring" /* Configuring */,
30024
- "Registering" /* Registering */,
30025
- "Initializing" /* Initializing */,
30026
- "Activating" /* Activating */,
30027
- "Ready" /* Ready */,
30028
- "ShuttingDown" /* ShuttingDown */,
30029
- "Disposed" /* Disposed */
30030
- ]);
30031
- var DEFAULT_BOOTSTRAP_CONFIG = Object.freeze({
30032
- environment: "development",
30033
- mode: "standalone",
30034
- port: 9e3,
30035
- debug: false,
30036
- pluginsDir: "./plugins",
30037
- dbPath: "./packages/core/db/educational_os.db"
30038
- });
30039
-
30040
- // packages/core/bootstrap/pipeline/pipeline-executor.ts
30041
- var PipelineExecutor = class {
30042
- constructor() {
30043
- this.listeners = /* @__PURE__ */ new Set();
30314
+ return void 0;
30315
+ }
30316
+ var _kernelContainer;
30317
+ function ensureKernelContainer() {
30318
+ if (!_kernelContainer) {
30319
+ _kernelContainer = new Kernel({ pluginsDir: resolvePluginsDirOverride() });
30320
+ _kernelContainer.initAuditLog();
30044
30321
  }
30045
- addListener(listener) {
30046
- this.listeners.add(listener);
30047
- return () => {
30048
- this.listeners.delete(listener);
30049
- };
30322
+ return _kernelContainer;
30323
+ }
30324
+ var kernelContainer = new Proxy({}, {
30325
+ get(target, prop, receiver) {
30326
+ return Reflect.get(ensureKernelContainer(), prop, receiver);
30327
+ },
30328
+ set(target, prop, value, receiver) {
30329
+ return Reflect.set(ensureKernelContainer(), prop, value, receiver);
30050
30330
  }
30051
- emit(event) {
30052
- for (const listener of Array.from(this.listeners)) {
30053
- try {
30054
- listener(event);
30055
- } catch (err) {
30056
- console.error("[PipelineExecutor] Listener error:", err);
30057
- }
30058
- }
30059
- }
30060
- async execute(stages, context) {
30061
- const startTime = Date.now();
30062
- const results = [];
30063
- const executedStages = [];
30064
- this.emit({ type: "PipelineStarted", timestamp: startTime });
30065
- console.log("[Platform] Starting Bootstrap Pipeline execution...");
30066
- for (const stage of stages) {
30067
- if (context.isCancelled) {
30068
- console.warn(`[Platform] Pipeline cancelled before stage ${stage.name}`);
30069
- const totalDurationMs2 = Date.now() - startTime;
30070
- return {
30071
- totalDurationMs: totalDurationMs2,
30072
- status: "Aborted",
30073
- stageResults: Object.freeze(results)
30074
- };
30075
- }
30076
- console.log(`[Platform] Entering ${stage.name}`);
30077
- this.emit({
30078
- type: "StageStarted",
30079
- timestamp: Date.now(),
30080
- stageId: stage.id,
30081
- stageName: String(stage.name)
30082
- });
30083
- const stageStart = Date.now();
30084
- try {
30085
- if (stage.timeoutMs && stage.timeoutMs > 0) {
30086
- await this.executeWithTimeout(stage, context, stage.timeoutMs);
30087
- } else {
30088
- await stage.execute(context);
30089
- }
30090
- const durationMs = Date.now() - stageStart;
30091
- console.log(`[Platform] Completed ${stage.name} (${durationMs} ms)`);
30092
- results.push({
30093
- stageId: stage.id,
30094
- stageName: String(stage.name),
30095
- durationMs,
30096
- status: "Success"
30097
- });
30098
- executedStages.push(stage);
30099
- this.emit({
30100
- type: "StageCompleted",
30101
- timestamp: Date.now(),
30102
- stageId: stage.id,
30103
- stageName: String(stage.name),
30104
- durationMs
30105
- });
30106
- } catch (err) {
30107
- const error = err instanceof Error ? err : new Error(String(err));
30108
- const durationMs = Date.now() - stageStart;
30109
- console.error(`[Platform] Failed ${stage.name} (${durationMs} ms): ${error.message}`);
30110
- results.push({
30111
- stageId: stage.id,
30112
- stageName: String(stage.name),
30113
- durationMs,
30114
- status: "Failed",
30115
- error
30116
- });
30117
- this.emit({
30118
- type: "StageFailed",
30119
- timestamp: Date.now(),
30120
- stageId: stage.id,
30121
- stageName: String(stage.name),
30122
- durationMs,
30123
- error
30124
- });
30125
- await this.rollbackExecutedStages(executedStages, context);
30126
- const totalDurationMs2 = Date.now() - startTime;
30127
- const pipelineResult = {
30128
- totalDurationMs: totalDurationMs2,
30129
- status: "Failed",
30130
- stageResults: Object.freeze(results),
30131
- failedStage: stage.id,
30132
- error
30133
- };
30134
- this.emit({
30135
- type: "PipelineCompleted",
30136
- timestamp: Date.now(),
30137
- durationMs: totalDurationMs2,
30138
- error
30139
- });
30140
- return pipelineResult;
30141
- }
30142
- }
30143
- const totalDurationMs = Date.now() - startTime;
30144
- console.log(`[Platform] Ready. Total startup duration: ${totalDurationMs} ms.`);
30145
- const finalResult = {
30146
- totalDurationMs,
30147
- status: "Success",
30148
- stageResults: Object.freeze(results)
30149
- };
30150
- this.emit({
30151
- type: "PipelineCompleted",
30152
- timestamp: Date.now(),
30153
- durationMs: totalDurationMs
30154
- });
30155
- return finalResult;
30156
- }
30157
- async executeWithTimeout(stage, context, timeoutMs) {
30158
- let timer;
30159
- const timeoutPromise = new Promise((_, reject) => {
30160
- timer = setTimeout(() => {
30161
- reject(new StartupTimeoutError(`Stage execution timed out after ${timeoutMs} ms`));
30162
- }, timeoutMs);
30163
- });
30164
- try {
30165
- await Promise.race([stage.execute(context), timeoutPromise]);
30166
- } finally {
30167
- if (timer) {
30168
- clearTimeout(timer);
30169
- }
30170
- }
30171
- }
30172
- async rollbackExecutedStages(executedStages, context) {
30173
- console.log("[Platform] Triggering stage rollbacks...");
30174
- for (let i = executedStages.length - 1; i >= 0; i--) {
30175
- const stage = executedStages[i];
30176
- if (stage.rollback) {
30177
- try {
30178
- console.log(`[Platform] Rolling back ${stage.name}`);
30179
- await stage.rollback(context);
30180
- } catch (rbErr) {
30181
- console.error(`[Platform] Error during rollback of ${stage.name}:`, rbErr);
30182
- }
30183
- }
30184
- }
30185
- }
30186
- };
30187
-
30188
- // packages/core/bootstrap/pipeline/stages/standard-stages.ts
30189
- var StartupStageImpl = class {
30190
- constructor() {
30191
- this.id = "stage_startup";
30192
- this.name = "Created" /* Created */;
30193
- this.description = "Environment & configuration validation";
30194
- }
30195
- async execute(context) {
30196
- context.setStage("Configuring" /* Configuring */);
30197
- }
30198
- };
30199
- var RegistrationStageImpl = class {
30200
- constructor() {
30201
- this.id = "stage_registration";
30202
- this.name = "Registering" /* Registering */;
30203
- this.description = "Core Service Contracts & DI Tokens registration";
30204
- }
30205
- async execute(context) {
30206
- context.setStage("Registering" /* Registering */);
30207
- }
30208
- };
30209
- var InitializationStageImpl = class {
30210
- constructor() {
30211
- this.id = "stage_initialization";
30212
- this.name = "Initializing" /* Initializing */;
30213
- this.description = "Subsystem Kernels initialization";
30214
- }
30215
- async execute(context) {
30216
- context.setStage("Initializing" /* Initializing */);
30217
- }
30218
- };
30219
- var ActivationStageImpl = class {
30220
- constructor() {
30221
- this.id = "stage_activation";
30222
- this.name = "Activating" /* Activating */;
30223
- this.description = "Plugin Host discovery & ESM plugins activation";
30224
- }
30225
- async execute(context) {
30226
- context.setStage("Activating" /* Activating */);
30227
- }
30228
- };
30229
- var ReadyStageImpl = class {
30230
- constructor() {
30231
- this.id = "stage_ready";
30232
- this.name = "Ready" /* Ready */;
30233
- this.description = "HTTP & Socket.IO server readiness";
30234
- }
30235
- async execute(context) {
30236
- context.setStage("Ready" /* Ready */);
30237
- }
30238
- };
30239
-
30240
- // packages/core/bootstrap/pipeline/bootstrap-pipeline.ts
30241
- var BootstrapPipeline = class {
30242
- constructor(stages) {
30243
- this._stages = [];
30244
- this.executor = new PipelineExecutor();
30245
- if (stages && stages.length > 0) {
30246
- this._stages = [...stages];
30247
- } else {
30248
- this._stages = [
30249
- new StartupStageImpl(),
30250
- new RegistrationStageImpl(),
30251
- new InitializationStageImpl(),
30252
- new ActivationStageImpl(),
30253
- new ReadyStageImpl()
30254
- ];
30255
- }
30256
- }
30257
- get stages() {
30258
- return Object.freeze(this._stages);
30259
- }
30260
- addStage(stage) {
30261
- this._stages.push(stage);
30262
- return this;
30263
- }
30264
- addListener(listener) {
30265
- return this.executor.addListener(listener);
30266
- }
30267
- async run(context) {
30268
- const result = await this.execute(context);
30269
- if (result.status === "Failed") {
30270
- throw result.error || new Error(`Bootstrap Pipeline failed at stage: ${result.failedStage}`);
30271
- }
30272
- }
30273
- async execute(context) {
30274
- return this.executor.execute(this._stages, context);
30275
- }
30276
- };
30277
-
30278
- // packages/core/bootstrap/builder/platform-builder.ts
30279
- var DefaultPlatformLogger = class {
30280
- info(message, ...args) {
30281
- console.log(`[PlatformBuilder:INFO] ${message}`, ...args);
30282
- }
30283
- warn(message, ...args) {
30284
- console.warn(`[PlatformBuilder:WARN] ${message}`, ...args);
30285
- }
30286
- error(message, ...args) {
30287
- console.error(`[PlatformBuilder:ERROR] ${message}`, ...args);
30288
- }
30289
- debug(message, ...args) {
30290
- console.debug(`[PlatformBuilder:DEBUG] ${message}`, ...args);
30291
- }
30292
- };
30293
- var PlatformBuilder = class _PlatformBuilder {
30294
- constructor(options) {
30295
- this._state = "Created";
30296
- this._config = { ...DEFAULT_BOOTSTRAP_CONFIG };
30297
- this._logger = new DefaultPlatformLogger();
30298
- this._environment = "development";
30299
- this._metadata = /* @__PURE__ */ new Map();
30300
- this._services = /* @__PURE__ */ new Map();
30301
- this._capabilities = /* @__PURE__ */ new Map();
30302
- this._extensions = /* @__PURE__ */ new Map();
30303
- this._pipeline = new BootstrapPipeline();
30304
- this._state = "Created";
30305
- this._logger.info("Builder Started");
30306
- if (options) {
30307
- if (options.config) this.withConfiguration(options.config);
30308
- if (options.logger) this.withLogger(options.logger);
30309
- if (options.environment) this.withEnvironment(options.environment);
30310
- if (options.metadata) {
30311
- for (const [k, v] of Object.entries(options.metadata)) {
30312
- this.withMetadata(k, v);
30313
- }
30314
- }
30315
- }
30316
- }
30317
- static create(options) {
30318
- return new _PlatformBuilder(options);
30319
- }
30320
- get state() {
30321
- return this._state;
30322
- }
30323
- transitionTo(newState) {
30324
- if (this._state === "Disposed") {
30325
- throw new Error("PlatformBuilder has been disposed and cannot accept further transitions.");
30326
- }
30327
- if (this._state === "Built" && newState !== "Disposed") {
30328
- throw new Error(`Invalid state transition from Built to ${newState}. Builder is already built.`);
30329
- }
30330
- this._state = newState;
30331
- }
30332
- withConfiguration(config) {
30333
- this.transitionTo("Configuring");
30334
- this._config = { ...this._config, ...config };
30335
- if (config.environment) {
30336
- this._environment = config.environment;
30337
- }
30338
- this._logger.info("Configuration Loaded");
30339
- return this;
30340
- }
30341
- withLogger(logger2) {
30342
- this.transitionTo("Configuring");
30343
- this._logger = logger2;
30344
- return this;
30345
- }
30346
- withEnvironment(env) {
30347
- this.transitionTo("Configuring");
30348
- this._environment = env;
30349
- this._config = { ...this._config, environment: env };
30350
- return this;
30351
- }
30352
- withMetadata(key, value) {
30353
- this.transitionTo("Configuring");
30354
- this._metadata.set(key, value);
30355
- return this;
30356
- }
30357
- addBootstrapStage(stage) {
30358
- this.transitionTo("Configuring");
30359
- this._pipeline.addStage(stage);
30360
- return this;
30361
- }
30362
- addService(serviceId, instance) {
30363
- this.transitionTo("Configuring");
30364
- this._services.set(serviceId, instance);
30365
- return this;
30366
- }
30367
- addCapability(capabilityId, descriptor) {
30368
- this.transitionTo("Configuring");
30369
- this._capabilities.set(capabilityId, descriptor);
30370
- return this;
30371
- }
30372
- addExtension(extensionId, extensionSpec) {
30373
- this.transitionTo("Configuring");
30374
- this._extensions.set(extensionId, extensionSpec);
30375
- return this;
30376
- }
30377
- buildResult() {
30378
- const buildStart = Date.now();
30379
- this.transitionTo("Validating");
30380
- this._logger.info("Validation Started");
30381
- const validation = BuilderValidationEngine.validate(this._config, this._environment, this._pipeline.stages);
30382
- this._logger.info("Validation Completed");
30383
- if (!validation.isValid) {
30384
- this._logger.error(`Validation Failed with ${validation.errors.length} errors.`);
30385
- throw new ConfigurationError(
30386
- `PlatformBuilder validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
30387
- );
30388
- }
30389
- this.transitionTo("Building");
30390
- this._logger.info("Build Started");
30391
- const platformContext = {
30392
- platformId: `plt_${globalThis.crypto.randomUUID()}`,
30393
- version: PLATFORM_VERSION2,
30394
- namespace: "openlearn.core",
30395
- mode: this._config.mode,
30396
- environment: {
30397
- type: this._environment,
30398
- isDevelopment: this._environment === "development",
30399
- isProduction: this._environment === "production",
30400
- isTest: this._environment === "test",
30401
- isPluginSandbox: false
30402
- },
30403
- config: this._config,
30404
- featureFlags: {
30405
- isEnabled: () => true,
30406
- getFlag: (_, def) => def,
30407
- getAllFlags: () => ({})
30408
- },
30409
- runtimeMetadata: {
30410
- os: process.platform,
30411
- nodeVersion: process.version,
30412
- arch: process.arch,
30413
- processId: process.pid,
30414
- memoryUsage: process.memoryUsage(),
30415
- buildVersion: PLATFORM_VERSION2
30416
- },
30417
- bootstrapState: "Bootstrapping",
30418
- logger: this._logger,
30419
- diagnostics: {
30420
- getMetrics: () => ({ uptime: 0 }),
30421
- getSystemStatus: () => ({ isHealthy: true, activeServicesCount: this._services.size, uptimeSeconds: 0 })
30422
- },
30423
- serviceLocator: {
30424
- hasService: (id) => this._services.has(id),
30425
- getService: (id) => this._services.get(id)
30426
- },
30427
- capabilityCatalog: {
30428
- hasCapability: (id) => this._capabilities.has(id),
30429
- getCapability: (id) => this._capabilities.get(id)
30430
- },
30431
- extensionCatalog: {
30432
- hasExtension: (id) => this._extensions.has(id),
30433
- getExtension: (id) => this._extensions.get(id)
30434
- },
30435
- eventDispatcher: {
30436
- dispatch: async () => {
30437
- }
30438
- },
30439
- shutdownToken: {
30440
- isRequested: false,
30441
- onShutdown: () => {
30442
- }
30443
- }
30444
- };
30445
- const buildDurationMs = Date.now() - buildStart;
30446
- this.transitionTo("Built");
30447
- this._logger.info(`Build Completed in ${buildDurationMs} ms`);
30448
- const result = {
30449
- platformContext,
30450
- pipeline: this._pipeline,
30451
- buildDurationMs,
30452
- validation,
30453
- metadata: Object.fromEntries(this._metadata.entries()),
30454
- builderVersion: PLATFORM_VERSION2
30455
- };
30456
- return result;
30457
- }
30458
- async build() {
30459
- const res = this.buildResult();
30460
- return {
30461
- currentStage: "Created" /* Created */,
30462
- context: res.platformContext,
30463
- pipeline: res.pipeline,
30464
- start: async () => {
30465
- },
30466
- shutdown: async () => {
30467
- },
30468
- dispose: async () => {
30469
- }
30470
- };
30471
- }
30472
- async start() {
30473
- const kernel = await this.build();
30474
- await kernel.start();
30475
- return kernel;
30476
- }
30477
- async shutdown() {
30478
- this.transitionTo("Disposed");
30479
- }
30480
- dispose() {
30481
- this._state = "Disposed";
30482
- this._services.clear();
30483
- this._capabilities.clear();
30484
- this._extensions.clear();
30485
- this._metadata.clear();
30486
- }
30487
- };
30488
-
30489
- // packages/core/bootstrap/composition/platform-composition-root.ts
30490
- var PlatformCompositionRoot = class _PlatformCompositionRoot {
30491
- constructor() {
30492
- this._state = "Created";
30493
- this._modules = [];
30494
- this._state = "Created";
30495
- }
30496
- static create() {
30497
- return new _PlatformCompositionRoot();
30498
- }
30499
- get state() {
30500
- return this._state;
30501
- }
30502
- registerModule(module2) {
30503
- if (this._state === "Disposed" || this._state === "Composed") {
30504
- throw new Error(`Cannot register composition module in state: ${this._state}`);
30505
- }
30506
- this._modules.push(module2);
30507
- return this;
30508
- }
30509
- compose(options) {
30510
- const startTime = Date.now();
30511
- this._state = "Validating";
30512
- const opts = options || {};
30513
- const builder = PlatformBuilder.create();
30514
- if (opts.config) builder.withConfiguration(opts.config);
30515
- if (opts.logger) builder.withLogger(opts.logger);
30516
- if (opts.environment) builder.withEnvironment(opts.environment);
30517
- const builderResult = builder.buildResult();
30518
- const validation = CompositionValidator.validate(opts, builderResult.pipeline, this._modules);
30519
- if (!validation.isValid) {
30520
- throw new ConfigurationError(
30521
- `CompositionRoot validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
30522
- );
30523
- }
30524
- this._state = "Composing";
30525
- for (const mod of this._modules) {
30526
- mod.compose(opts);
30527
- }
30528
- const durationMs = Date.now() - startTime;
30529
- this._state = "Composed";
30530
- return {
30531
- context: builderResult.platformContext,
30532
- pipeline: builderResult.pipeline,
30533
- durationMs,
30534
- validation
30535
- };
30536
- }
30537
- dispose() {
30538
- this._state = "Disposed";
30539
- this._modules = [];
30540
- }
30541
- };
30542
-
30543
- // packages/core/bootstrap/permission/permission-registry.ts
30544
- var PermissionRegistry = class {
30545
- constructor() {
30546
- this.descriptors = /* @__PURE__ */ new Map();
30547
- }
30548
- register(descriptor) {
30549
- if (!descriptor || !descriptor.id) {
30550
- throw new Error("PermissionRegistry Error: PermissionDescriptor must have a valid ID.");
30551
- }
30552
- if (this.descriptors.has(descriptor.id)) {
30553
- throw new Error(`PermissionRegistry Collision: Permission '${descriptor.id}' is already registered.`);
30554
- }
30555
- this.descriptors.set(descriptor.id, Object.freeze({ ...descriptor }));
30556
- }
30557
- unregister(permissionId) {
30558
- return this.descriptors.delete(permissionId);
30559
- }
30560
- get(permissionId) {
30561
- return this.descriptors.get(permissionId);
30562
- }
30563
- exists(permissionId) {
30564
- return this.descriptors.has(permissionId);
30565
- }
30566
- list() {
30567
- return Object.freeze(Array.from(this.descriptors.values()));
30568
- }
30569
- clear() {
30570
- this.descriptors.clear();
30571
- }
30572
- };
30573
-
30574
- // packages/core/bootstrap/permission/permission-evaluator.ts
30575
- var PermissionEvaluator = class {
30576
- static async evaluate(subject, permission, explicitGrants, descriptor, providers) {
30577
- const evaluatedAt = Date.now();
30578
- const grantKey = `${subject}::${permission}`;
30579
- if (explicitGrants.has(grantKey)) {
30580
- const policy = explicitGrants.get(grantKey);
30581
- if (policy === "Allow") {
30582
- return { allowed: true, policy: "Allow", reason: "Explicitly granted to subject", evaluatedAt };
30583
- }
30584
- if (policy === "Deny") {
30585
- return { allowed: false, policy: "Deny", reason: "Explicitly denied to subject", evaluatedAt };
30586
- }
30587
- }
30588
- if (providers && providers.length > 0) {
30589
- for (const provider of providers) {
30590
- const policy = await provider.getPolicy(subject, permission);
30591
- if (policy === "Allow") {
30592
- return { allowed: true, policy: "Allow", reason: `Granted by provider '${provider.id}'`, evaluatedAt };
30593
- }
30594
- if (policy === "Deny") {
30595
- return { allowed: false, policy: "Deny", reason: `Denied by provider '${provider.id}'`, evaluatedAt };
30596
- }
30597
- }
30598
- }
30599
- if (descriptor && descriptor.defaultPolicy) {
30600
- const defaultPol = descriptor.defaultPolicy;
30601
- if (defaultPol === "Allow") {
30602
- return { allowed: true, policy: "Allow", reason: "Allowed by descriptor default policy", evaluatedAt };
30603
- }
30604
- if (defaultPol === "Deny") {
30605
- return { allowed: false, policy: "Deny", reason: "Denied by descriptor default policy", evaluatedAt };
30606
- }
30607
- }
30608
- return {
30609
- allowed: false,
30610
- policy: "Default",
30611
- reason: "No explicit allow policy resolved; default deny",
30612
- evaluatedAt
30613
- };
30614
- }
30615
- };
30616
-
30617
- // packages/core/bootstrap/permission/permission-manager.ts
30618
- var PermissionManager = class {
30619
- constructor() {
30620
- this.registry = new PermissionRegistry();
30621
- this.grants = /* @__PURE__ */ new Map();
30622
- this.providers = [];
30623
- }
30624
- register(descriptor) {
30625
- this.registry.register(descriptor);
30626
- }
30627
- unregister(permissionId) {
30628
- return this.registry.unregister(permissionId);
30629
- }
30630
- grant(subject, permissionId, policy = "Allow") {
30631
- const key = `${subject}::${permissionId}`;
30632
- this.grants.set(key, policy);
30633
- }
30634
- revoke(subject, permissionId) {
30635
- const key = `${subject}::${permissionId}`;
30636
- this.grants.delete(key);
30637
- }
30638
- addProvider(provider) {
30639
- if (!this.providers.some((p) => p.id === provider.id)) {
30640
- this.providers.push(provider);
30641
- }
30642
- }
30643
- removeProvider(providerId) {
30644
- this.providers = this.providers.filter((p) => p.id !== providerId);
30645
- }
30646
- exists(permissionId) {
30647
- return this.registry.exists(permissionId);
30648
- }
30649
- list() {
30650
- return this.registry.list();
30651
- }
30652
- async check(subject, target, permissionId, metadata) {
30653
- const timestamp = Date.now();
30654
- const descriptor = this.registry.get(permissionId);
30655
- const result = await PermissionEvaluator.evaluate(subject, permissionId, this.grants, descriptor, this.providers);
30656
- const context = {
30657
- subject,
30658
- target,
30659
- permission: permissionId,
30660
- metadata,
30661
- timestamp,
30662
- result
30663
- };
30664
- return context;
30665
- }
30666
- async require(subject, target, permissionId, metadata) {
30667
- const context = await this.check(subject, target, permissionId, metadata);
30668
- if (!context.result || !context.result.allowed) {
30669
- const reason = context.result?.reason || "Permission Denied";
30670
- throw new Error(
30671
- `Infrastructure Permission Exception: Subject '${subject}' is denied '${permissionId}' on target '${target}'. Reason: ${reason}`
30672
- );
30673
- }
30674
- return context;
30675
- }
30676
- clear() {
30677
- this.registry.clear();
30678
- this.grants.clear();
30679
- this.providers = [];
30680
- }
30681
- };
30682
-
30683
- // packages/core/bootstrap/composition/plugin-composition-module.ts
30684
- var _sharedPlatformRegistry;
30685
- function getPlatformServiceRegistry() {
30686
- if (!_sharedPlatformRegistry) {
30687
- _sharedPlatformRegistry = new PlatformServiceRegistry();
30688
- }
30689
- return _sharedPlatformRegistry;
30690
- }
30691
- var PluginCompositionModule = class {
30692
- constructor() {
30693
- this.id = "mod_plugin_composition";
30694
- this.name = "PluginCompositionModule";
30695
- }
30696
- compose(options) {
30697
- const serviceRegistry = getPlatformServiceRegistry();
30698
- const registerOrReplace = (config) => {
30699
- const { id, instance } = config;
30700
- if (serviceRegistry.has(id)) {
30701
- serviceRegistry.replace(id, instance);
30702
- } else {
30703
- serviceRegistry.register({ id }, instance);
30704
- }
30705
- };
30706
- const capabilityRegistry = new CapabilityRegistry();
30707
- const permissionManager = new PermissionManager();
30708
- const eventBus = new EventBus();
30709
- const realPluginHost = options?.infrastructureRefs?.get("pluginHost") ?? {
30710
- name: "PluginHostService",
30711
- isReady: true,
30712
- pluginsCount: 0
30713
- };
30714
- const realContributionRegistry = options?.infrastructureRefs?.get("contributionRegistry") ?? {
30715
- name: "ContributionRegistry",
30716
- slotsCount: 0
30717
- };
30718
- const realRuntimeComposition = options?.infrastructureRefs?.get("runtimeComposition") ?? {
30719
- name: "PluginRuntimeComposition",
30720
- isStarted: true
30721
- };
30722
- const realLifecycleManager = options?.infrastructureRefs?.get("lifecycleManager") ?? {
30723
- name: "PluginLifecycleManager",
30724
- version: PLATFORM_VERSION
30725
- };
30726
- const realCapabilityGateway = options?.infrastructureRefs?.get("capabilityGateway") ?? {
30727
- name: "PluginCapabilityGateway",
30728
- version: PLATFORM_VERSION
30729
- };
30730
- const realExtensionRegistry = options?.infrastructureRefs?.get("extensionRegistry") ?? {
30731
- name: "UnifiedExtensionRegistry",
30732
- version: PLATFORM_VERSION
30733
- };
30734
- const realDistributionManager = options?.infrastructureRefs?.get("distributionManager") ?? {
30735
- name: "PluginDistributionManager",
30736
- version: PLATFORM_VERSION
30737
- };
30738
- registerOrReplace({
30739
- id: "srv_plugin_host",
30740
- lifetime: "Singleton",
30741
- description: "OpenLearn Plugin Host Service Engine",
30742
- instance: realPluginHost
30743
- });
30744
- registerOrReplace({
30745
- id: "srv_plugin_contribution_registry",
30746
- lifetime: "Singleton",
30747
- description: "Plugin UI & Slot Contribution Registry",
30748
- instance: realContributionRegistry
30749
- });
30750
- registerOrReplace({
30751
- id: "srv_plugin_runtime_composition",
30752
- lifetime: "Singleton",
30753
- description: "OpenLearn Plugin Runtime Composition Facade",
30754
- instance: realRuntimeComposition
30755
- });
30756
- registerOrReplace({
30757
- id: "srv_plugin_lifecycle_manager",
30758
- lifetime: "Singleton",
30759
- description: "OpenLearn Unified Plugin Lifecycle Manager",
30760
- instance: realLifecycleManager
30761
- });
30762
- registerOrReplace({
30763
- id: "srv_plugin_capability_gateway",
30764
- lifetime: "Singleton",
30765
- description: "OpenLearn Unified Plugin Capability Gateway",
30766
- instance: realCapabilityGateway
30767
- });
30768
- registerOrReplace({
30769
- id: "srv_unified_extension_registry",
30770
- lifetime: "Singleton",
30771
- description: "OpenLearn Unified Extension Registry Foundation",
30772
- instance: realExtensionRegistry
30773
- });
30774
- registerOrReplace({
30775
- id: "srv_plugin_distribution_manager",
30776
- lifetime: "Singleton",
30777
- description: "OpenLearn Plugin Distribution Manager Foundation",
30778
- instance: realDistributionManager
30779
- });
30780
- permissionManager.register({
30781
- id: "perm_plugin_execute",
30782
- name: "Plugin Execution Permission",
30783
- category: "Infrastructure",
30784
- description: "Allows plugin sandbox execution within Platform",
30785
- defaultPolicy: "Allow"
30786
- });
30787
- permissionManager.register({
30788
- id: "perm_plugin_install",
30789
- name: "Plugin Installation Permission",
30790
- category: "Infrastructure",
30791
- description: "Allows installing new plugin packages into storage",
30792
- defaultPolicy: "Allow"
30793
- });
30794
- eventBus.publish({
30795
- id: `evt_${globalThis.crypto.randomUUID()}`,
30796
- type: "PluginHostInitialized",
30797
- source: "PluginCompositionModule",
30798
- payload: { timestamp: Date.now(), status: "Ready" },
30799
- timestamp: Date.now()
30800
- });
30801
- eventBus.publish({
30802
- id: `evt_${globalThis.crypto.randomUUID()}`,
30803
- type: "PluginLoaded",
30804
- source: "PluginCompositionModule",
30805
- payload: { timestamp: Date.now(), pluginsLoaded: 0 },
30806
- timestamp: Date.now()
30807
- });
30808
- eventBus.publish({
30809
- id: `evt_${globalThis.crypto.randomUUID()}`,
30810
- type: "PluginActivated",
30811
- source: "PluginCompositionModule",
30812
- payload: { timestamp: Date.now(), activePlugins: [] },
30813
- timestamp: Date.now()
30814
- });
30815
- }
30816
- };
30817
-
30818
- // packages/core/kernel/index.ts
30819
- var import_path12 = __toESM(require("path"), 1);
30820
- var Kernel = class {
30821
- /**
30822
- * @param opts.pluginsDir - 插件产物目录。生产默认 `<cwd>/plugins`;
30823
- * 测试**必须**显式传入临时目录,否则插件产物会落进工作树。
30824
- *
30825
- * ## 为什么要开这个口子(H-1)
30826
- *
30827
- * 原本这里硬编码 `path.resolve(process.cwd(), 'plugins')` 且不可覆盖。
30828
- * 后果不只是「测试会弄脏工作树」,更根本的是 **Kernel 在这个维度上不可测** ——
30829
- * 没有任何办法在测试里把插件产物引到别处,于是污染一直无人察觉:
30830
- * 实测仓库 `plugins/` 下积累了 **1705 个孤儿目录 / 24MB**,且每次 `pnpm test` 继续增长。
30831
- * 该目录被 `.gitignore` 忽略,所以 `git status` 完全看不见。
30832
- *
30833
- * 加可选参数后,生产行为**逐字节不变**(默认值相同),而测试可把产物引到
30834
- * `os.tmpdir()`。`vitest.setup.ts` 的全局守卫会把「仍写工作树」变成硬失败,
30835
- * 防止将来有人再引入同类调用。
30836
- */
30837
- constructor(opts) {
30838
- this.db = db;
30839
- this.readPool = readPool;
30840
- this.getReadDb = getReadDb;
30841
- this.queryRead = queryRead;
30842
- this.queryReadOne = queryReadOne;
30843
- this.checkpoint = checkpoint;
30844
- this.eventBus = new EventBus();
30845
- this.capabilityGuard = new CapabilityGuard();
30846
- this.serviceRegistry = new ServiceRegistry();
30847
- this.storageService = new StorageService(this.db);
30848
- this.aiService = new AIService(this.db);
30849
- this.aiRuntime = new AIRuntimeKernel();
30850
- this.aiCapability = new AICapabilityKernel(this.aiRuntime);
30851
- this.capabilityFrameworkRuntime = new CapabilityRuntimeKernel(this.aiCapability);
30852
- this.capabilityGovernance = new CapabilityGovernanceKernel();
30853
- this.platformServiceRegistryKernel = new ServiceRegistryKernel();
30854
- this.analyticsEngine = new AnalyticsEngineKernel();
30855
- this.collaborationEngine = new CollaborationEngineKernel();
30856
- this.presenceEngine = new PresenceEngineKernel();
30857
- this.classroomRuntime = new ClassroomRuntimeKernel();
30858
- this.lessonRuntime = new LessonRuntime({ eventBus: this.eventBus });
30859
- this.lessonRuntime.aiInterface.setAIService(this.aiService);
30860
- this.commandBus = new CommandBus(this.eventBus);
30861
- this.actionRegistry = new ActionRegistry();
30862
- this.processManager = new ProcessManager(this);
30863
- this.esmLoader = new NodeEsmLoader();
30864
- const pluginsDir = opts?.pluginsDir ?? import_path12.default.resolve(process.cwd(), "plugins");
30865
- try {
30866
- import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
30867
- } catch (err) {
30868
- const detail = err instanceof Error ? err.message : String(err);
30869
- throw new Error(
30870
- `[Kernel] FATAL: \u65E0\u6CD5\u521B\u5EFA\u63D2\u4EF6\u76EE\u5F55 ${pluginsDir}\uFF08${detail}\uFF09\u3002\u5BB9\u5668\u8BF7\u628A OPENLEARN_DATA_DIR \u6216 OPENLEARN_PLUGINS_DIR \u6307\u5230\u53EF\u5199\u5377\uFF0C\u4F8B\u5982 /data/plugins\u3002`
30871
- );
30872
- }
30873
- this.pluginHost = new PluginHost(this.serviceRegistry, this.esmLoader, this.db, pluginsDir);
30874
- setPluginNamespaceResolver((namespace) => this.pluginHost.findByManifestId(namespace) ? namespace : void 0);
30875
- this.workerManager = new WorkerManager(this.serviceRegistry, this.capabilityGuard, this.db);
30876
- this.pluginHost.setWorkerManager(this.workerManager);
30877
- const capabilityRegistry = this.aiCapability.registry;
30878
- this.pluginRuntimeComposition = new PluginRuntimeComposition(this.pluginHost, this.workerManager);
30879
- this.pluginLifecycleManager = new PluginLifecycleManager(this.pluginHost);
30880
- this.pluginDistributionManager = new PluginDistributionManager(this.pluginHost);
30881
- this.unifiedExtensionRegistry = new UnifiedExtensionRegistry();
30882
- this.capabilityRegistry = capabilityRegistry;
30883
- this.pluginCapabilityGateway = new PluginCapabilityGateway(capabilityRegistry);
30884
- const infrastructureRefs = /* @__PURE__ */ new Map([
30885
- ["pluginHost", this.pluginHost],
30886
- ["contributionRegistry", this.pluginHost.getContributionRegistry()],
30887
- ["runtimeComposition", this.pluginRuntimeComposition],
30888
- ["lifecycleManager", this.pluginLifecycleManager],
30889
- ["capabilityGateway", this.pluginCapabilityGateway],
30890
- ["extensionRegistry", this.unifiedExtensionRegistry],
30891
- ["distributionManager", this.pluginDistributionManager]
30892
- ]);
30893
- try {
30894
- PlatformCompositionRoot.create().registerModule(new PluginCompositionModule()).compose({ infrastructureRefs });
30895
- } catch (e) {
30896
- console.warn("[P7-A2] Plugin composition failed (non-fatal):", e.message);
30897
- }
30898
- this.serviceRegistry.register(IEventBusServiceToken3, this.eventBus);
30899
- this.serviceRegistry.register(ICapabilityServiceToken, this.capabilityGuard);
30900
- this.serviceRegistry.register(IStorageServiceToken, this.storageService);
30901
- this.serviceRegistry.register(ICommandBusServiceToken4, this.commandBus);
30902
- this.serviceRegistry.register(IActionRegistryServiceToken4, this.actionRegistry);
30903
- this.serviceRegistry.register(IProcessServiceToken2, this.processManager);
30904
- this.serviceRegistry.register(IAIServiceToken, this.aiService);
30905
- this.serviceRegistry.register(IDatabaseToken4, this.db);
30906
- this.serviceRegistry.register(IPluginHostToken, this.pluginHost);
30907
- this.serviceRegistry.register(IPluginLifecycleManagerToken, this.pluginLifecycleManager);
30908
- this.serviceRegistry.register(IPluginDistributionManagerToken, this.pluginDistributionManager);
30909
- this.serviceRegistry.register(IPluginRuntimeCompositionToken, this.pluginRuntimeComposition);
30910
- this.serviceRegistry.register(IUnifiedExtensionRegistryToken, this.unifiedExtensionRegistry);
30911
- this.serviceRegistry.register(IPluginCapabilityGatewayToken, this.pluginCapabilityGateway);
30912
- this.serviceRegistry.register(ICapabilityRegistryToken, this.capabilityRegistry);
30913
- this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
30914
- this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
30915
- this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
30916
- this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
30917
- this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
30918
- this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
30919
- getRuntimeKernel: async () => this.classroomRuntime
30920
- });
30921
- this.serviceRegistry.register(IPresenceEngineServiceToken, {
30922
- getPresenceEngine: async () => this.presenceEngine
30923
- });
30924
- this.serviceRegistry.register(ITeachingCollaborationServiceToken, {
30925
- getCollaborationEngine: async () => this.collaborationEngine
30926
- });
30927
- this.serviceRegistry.register(ILearningAnalyticsServiceToken, {
30928
- getAnalyticsEngine: async () => this.analyticsEngine
30929
- });
30930
- this.serviceRegistry.register(IAICapabilityServiceToken, {
30931
- getCapabilityKernel: async () => this.aiCapability
30932
- });
30933
- this.serviceRegistry.register(ICapabilityRuntimeServiceToken, {
30934
- getRuntimeKernel: async () => this.capabilityFrameworkRuntime
30935
- });
30936
- this.serviceRegistry.register(ICapabilityGovernanceServiceToken, {
30937
- getGovernanceKernel: async () => this.capabilityGovernance
30938
- });
30939
- this.serviceRegistry.register(IPlatformServiceRegistryToken, {
30940
- getServiceRegistryKernel: async () => this.platformServiceRegistryKernel
30941
- });
30942
- this.serviceRegistry.register(IAuthSessionBridgeToken, new AuthSessionBridgeService(this.db));
30943
- this.serviceRegistry.register(ICoursewareRuntimeScriptRegistryToken, new CoursewareRuntimeScriptRegistry());
30944
- this.serviceRegistry.register(IStageGuardServiceToken, defaultStageGuardPipeline);
30945
- this.serviceRegistry.register(IAssessmentGraderRegistryToken, assessmentGraderRegistry);
30946
- this.commandBus.setInterceptor(async (command) => {
30947
- const action = this.actionRegistry.getActionByCommandType(command.type);
30948
- if (action) {
30949
- if (action.inputSchema) {
30950
- const errors = validateJsonSchema(command.payload, action.inputSchema);
30951
- if (errors.length > 0) {
30952
- throw new Error(
30953
- `[PayloadValidationError] Invalid command payload for ${command.type}: ${errors.join("; ")}`
30954
- );
30955
- }
30956
- }
30957
- const isAdmin = command.actorId === "role:administrator" || command.actorId === "admin" || command.actorId === "usr_admin" || command.actorId === "admin-demo" || typeof command.actorId === "string" && command.actorId.startsWith("user:") && (command.actorId.endsWith(":administrator") || command.actorId.endsWith(":admin"));
30958
- if (action.capabilityRequired && !isAdmin) {
30959
- const allowed = this.capabilityGuard.check(command.actorId, action.capabilityRequired);
30960
- if (!allowed) {
30961
- const err = new Error(
30962
- `[CapabilityGuard] Access Denied: Actor ${command.actorId} missing capability ${action.capabilityRequired} for ${command.type}`
30963
- );
30964
- err.status = 403;
30965
- err.code = "COMMAND_CAPABILITY_DENIED";
30966
- throw err;
30967
- }
30968
- }
30969
- const pluginSuppliedApproval = typeof command.actorId === "string" && command.actorId.startsWith("plugin:");
30970
- if (action.isHighRisk && (command.metadata?.approved !== true || pluginSuppliedApproval)) {
30971
- if (isAdmin && !pluginSuppliedApproval) {
30972
- console.log(
30973
- `[Security] Command ${command.type} initiated by Administrator (${command.actorId}). Bypassing human approval.`
30974
- );
30975
- } else {
30976
- const stmt = this.db.prepare(
30977
- "INSERT INTO pending_commands (id, command_type, payload, actor_id, created_at) VALUES (?, ?, ?, ?, ?)"
30978
- );
30979
- stmt.run(command.id, command.type, JSON.stringify(command.payload), command.actorId, Date.now());
30980
- this.eventBus.publish({
30981
- id: (0, import_uuid15.v7)(),
30982
- type: "approval.requested",
30983
- source: "kernel.security",
30984
- payload: { commandId: command.id, commandType: command.type },
30985
- timestamp: Date.now(),
30986
- correlationId: command.id
30987
- });
30988
- throw new Error(
30989
- `[Security] Command ${command.type} requires human approval. It has been queued to pending actions.`
30990
- );
30991
- }
30992
- }
30993
- } else {
30994
- const err = new Error(
30995
- `[Security] Command "${command.type}" has no registered action descriptor and is denied by default. Only commands registered via actionRegistry.register() may be dispatched. If you own this command, register an ActionDescriptor (capabilityRequired + inputSchema).`
30996
- );
30997
- err.status = 403;
30998
- err.code = "COMMAND_NOT_REGISTERED";
30999
- throw err;
31000
- }
31001
- });
31002
- Promise.resolve().then(() => (init_context_builder(), context_builder_exports)).then((m) => m.bootstrapSharedModules()).catch((err) => {
31003
- console.warn("[Kernel] Failed to bootstrap shared modules:", err.message);
31004
- });
31005
- this.ready = this.bootstrapSystemPlugins().catch((err) => {
31006
- console.error("[Kernel] Critical system plugin bootstrap failed:", err);
31007
- process.exit(1);
31008
- });
31009
- if (process.env.NODE_ENV === "development") {
31010
- const watchDir = this.pluginHost.getPluginsDir();
31011
- try {
31012
- const hotReload = new HotReloadController(this.pluginHost, watchDir);
31013
- this.pluginHost.setHotReloadController(hotReload);
31014
- hotReload.start().catch((err) => {
31015
- console.warn("[Kernel] Hot reload initialization failed:", err.message);
31016
- });
31017
- } catch (err) {
31018
- console.warn("[Kernel] Hot reload initialization failed:", err.message);
31019
- }
31020
- }
31021
- }
31022
- async bootstrapSystemPlugins() {
31023
- await this.migratePluginsToFilesystem();
31024
- const systemPlugins = [
31025
- { id: "@openlearn/plugin-vfs", mod: VfsPlugin, name: "Virtual File System Plugin", critical: true },
31026
- { id: "@openlearn/plugin-process", mod: ProcessPlugin, name: "Background Process Plugin", critical: true },
31027
- { id: "@openlearn/plugin-management", mod: ManagementPlugin, name: "LMS Management Plugin", critical: true },
31028
- { id: "@openlearn/plugin-builtin", mod: BuiltinPlugin, name: "Classroom Builtin Plugin", critical: true },
31029
- { id: "@openlearn/plugin-ai-planner", mod: AiPlannerPlugin, name: "AI Planner Plugin", critical: false },
31030
- {
31031
- id: "@openlearn/plugin-ai-submit-injector",
31032
- mod: AiSubmitInjectorPlugin,
31033
- name: "AI Submit Injector Plugin",
31034
- critical: false
31035
- },
31036
- {
31037
- id: "@openlearn/plugin-assignment-eval",
31038
- mod: AssignmentEvalPlugin,
31039
- name: "Assignment Evaluation and Peer Review Plugin",
31040
- critical: false
31041
- }
31042
- ];
31043
- for (const plugin of systemPlugins) {
31044
- try {
31045
- let row = this.db.prepare("SELECT id FROM plugins WHERE id = ?").get(plugin.id);
31046
- if (!row) {
31047
- this.db.prepare(
31048
- "INSERT INTO plugins (id, name, manifest, source_code, file_path, status, created_at, loader_version, execution_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
31049
- ).run(
31050
- plugin.id,
31051
- plugin.name,
31052
- JSON.stringify(plugin.mod.manifest),
31053
- "",
31054
- // source_code: system plugins are preloaded in-memory
31055
- null,
31056
- // file_path: system plugins have no file
31057
- "installed",
31058
- Date.now(),
31059
- "esm",
31060
- "inline"
31061
- );
31062
- }
31063
- this.pluginHost.registerPreloadedPlugin(
31064
- plugin.id,
31065
- plugin.mod
31066
- );
31067
- await this.pluginHost.activatePlugin(plugin.id);
31068
- } catch (err) {
31069
- if (plugin.critical) {
31070
- console.error(`[Kernel] Failed to bootstrap critical system plugin ${plugin.name}:`, err);
31071
- throw err;
31072
- } else {
31073
- console.warn(`[Kernel] Soft-fail: Failed to bootstrap AI system plugin ${plugin.name}:`, err);
31074
- }
31075
- }
31076
- }
31077
- try {
31078
- await this.pluginHost.restoreActivePlugins();
31079
- } catch (err) {
31080
- console.error("[Kernel] Failed to restore active plugins:", err);
31081
- }
31082
- }
31083
- /**
31084
- * Phase 7: 将 DB 中存有 source_code 的旧插件迁移到文件系统(幂等)
31085
- */
31086
- async migratePluginsToFilesystem() {
31087
- const plugins = this.db.prepare("SELECT id, source_code, manifest FROM plugins WHERE source_code != '' AND source_code IS NOT NULL").all();
31088
- if (plugins.length === 0) return;
31089
- console.log(`[Migration] Found ${plugins.length} plugin(s) to migrate to filesystem`);
31090
- const pluginsDir = this.pluginHost.getPluginsDir();
31091
- for (const p of plugins) {
31092
- const pluginDir = import_path12.default.join(pluginsDir, p.id);
31093
- const indexPath = import_path12.default.join(pluginDir, "index.js");
31094
- const manifestPath = import_path12.default.join(pluginDir, "manifest.json");
31095
- if (import_fs13.default.existsSync(indexPath)) {
31096
- this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
31097
- continue;
31098
- }
31099
- try {
31100
- import_fs13.default.mkdirSync(pluginDir, { recursive: true });
31101
- import_fs13.default.writeFileSync(indexPath, p.source_code, "utf-8");
31102
- if (!import_fs13.default.existsSync(manifestPath)) {
31103
- import_fs13.default.writeFileSync(manifestPath, p.manifest, "utf-8");
31104
- }
31105
- this.db.prepare("UPDATE plugins SET source_code = ?, file_path = ? WHERE id = ?").run("", indexPath, p.id);
31106
- console.log(`[Migration] Plugin "${p.id}" migrated to ${indexPath}`);
31107
- } catch (err) {
31108
- console.error(`[Migration] Failed to migrate plugin "${p.id}":`, err);
31109
- }
31110
- }
31111
- console.log("[Migration] Plugin migration complete");
31112
- }
31113
- // Subscribe to all events and log them to DB
31114
- initAuditLog() {
31115
- let stmt = null;
31116
- let hasLessonIdColumn = null;
31117
- this.eventBus.subscribe("*", (event) => {
31118
- const values = [
31119
- event.id,
31120
- event.type,
31121
- event.source,
31122
- JSON.stringify(redactPasscodeMaterial(event.payload)),
31123
- event.timestamp,
31124
- event.correlationId || null
31125
- ];
31126
- if (hasLessonIdColumn === null) {
31127
- const columns = this.db.prepare("PRAGMA table_info(events)").all();
31128
- hasLessonIdColumn = columns.some((c) => c.name === "lesson_id");
31129
- stmt = this.db.prepare(hasLessonIdColumn ? INSERT_EVENT_WITH_LESSON : INSERT_EVENT_LEGACY);
31130
- }
31131
- if (hasLessonIdColumn) {
31132
- stmt.run(...values, extractEventLessonId(event.payload));
31133
- } else {
31134
- stmt.run(...values);
31135
- }
31136
- });
31137
- }
31138
- };
31139
- var INSERT_EVENT_WITH_LESSON = "INSERT INTO events (id, type, source, payload, timestamp, correlationId, lesson_id) VALUES (?, ?, ?, ?, ?, ?, ?)";
31140
- var INSERT_EVENT_LEGACY = "INSERT INTO events (id, type, source, payload, timestamp, correlationId) VALUES (?, ?, ?, ?, ?, ?)";
31141
- function extractEventLessonId(payload) {
31142
- if (!payload || typeof payload !== "object") return null;
31143
- const record = payload;
31144
- for (const key of ["lessonId", "lesson_id"]) {
31145
- const value = record[key];
31146
- if (typeof value === "string" && value.length > 0) return value;
31147
- }
31148
- return null;
31149
- }
31150
- function resolvePluginsDirOverride() {
31151
- const raw = process.env.OPENLEARN_PLUGINS_DIR;
31152
- if (typeof raw === "string" && raw.length > 0) return raw;
31153
- const dataDir = process.env.OPENLEARN_DATA_DIR?.trim();
31154
- if (dataDir) return import_path12.default.join(import_path12.default.resolve(dataDir), "plugins");
31155
- if (process.env.VITEST) {
31156
- throw new Error(
31157
- "[H-1] \u6D4B\u8BD5\u73AF\u5883\u4E0B\u672A\u80FD\u786E\u5B9A\u63D2\u4EF6\u76EE\u5F55\uFF0C\u62D2\u7EDD\u56DE\u9000\u5230 process.cwd()/plugins\u3002\n \u539F\u56E0\uFF1A\u56DE\u9000\u4F1A\u628A\u63D2\u4EF6\u4EA7\u7269\u5199\u8FDB\u4ED3\u5E93\u5DE5\u4F5C\u6811\uFF1B\u8BE5\u76EE\u5F55\u88AB .gitignore \u5FFD\u7565\uFF0C\u6C61\u67D3\u5B8C\u5168\u9690\u5F62\u3002\n \u4FEE\u6CD5\uFF08\u62E9\u4E00\uFF09\uFF1A\n 1. \u8BE5\u6D4B\u8BD5\u81EA\u5DF1 new Kernel({ pluginsDir })\uFF0C\u7528 __tests__/helpers/plugins-dir \u7684\n createPluginsDir('\u7528\u9014') \u53D6\u5F97\uFF0C\u5E76\u5728 afterEach \u91CC cleanupPluginsDir\uFF1B\n 2. \u82E5\u7528\u7684\u662F kernelContainer \u5355\u4F8B\uFF0C\u786E\u8BA4\u5B83\u662F\u5728 vitest.setup.ts \u8D4B\u503C\n OPENLEARN_PLUGINS_DIR \u4E4B\u540E\u624D\u88AB\u9996\u6B21\u8BBF\u95EE\u3002"
31158
- );
31159
- }
31160
- return void 0;
31161
- }
31162
- var _kernelContainer;
31163
- function ensureKernelContainer() {
31164
- if (!_kernelContainer) {
31165
- _kernelContainer = new Kernel({ pluginsDir: resolvePluginsDirOverride() });
31166
- _kernelContainer.initAuditLog();
31167
- }
31168
- return _kernelContainer;
31169
- }
31170
- var kernelContainer = new Proxy({}, {
31171
- get(target, prop, receiver) {
31172
- return Reflect.get(ensureKernelContainer(), prop, receiver);
31173
- },
31174
- set(target, prop, value, receiver) {
31175
- return Reflect.set(ensureKernelContainer(), prop, value, receiver);
31176
- }
31177
- });
31178
- function validateJsonSchema(data, schema) {
31179
- if (!schema) return [];
31180
- const errors = [];
31181
- const type = schema.type;
31182
- if (type === "OBJECT") {
31183
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
31184
- errors.push(`Expected object, got ${typeof data}`);
31185
- return errors;
31186
- }
31187
- if (schema.required && Array.isArray(schema.required)) {
31188
- for (const req of schema.required) {
31189
- if (!(req in data) || data[req] === void 0) {
31190
- errors.push(`Missing required property "${req}"`);
31191
- }
30331
+ });
30332
+ function validateJsonSchema(data, schema) {
30333
+ if (!schema) return [];
30334
+ const errors = [];
30335
+ const type = schema.type;
30336
+ if (type === "OBJECT") {
30337
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
30338
+ errors.push(`Expected object, got ${typeof data}`);
30339
+ return errors;
30340
+ }
30341
+ if (schema.required && Array.isArray(schema.required)) {
30342
+ for (const req of schema.required) {
30343
+ if (!(req in data) || data[req] === void 0) {
30344
+ errors.push(`Missing required property "${req}"`);
30345
+ }
31192
30346
  }
31193
30347
  }
31194
30348
  if (schema.properties && typeof schema.properties === "object") {
@@ -32769,6 +31923,299 @@ var import_helmet = __toESM(require("helmet"), 1);
32769
31923
 
32770
31924
  // server/security/rate-limit.ts
32771
31925
  var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
31926
+
31927
+ // server/security/route-scope.ts
31928
+ var AUTH_MIDDLEWARE_MARKER = Symbol.for("openlearn.routeScope.authMiddleware");
31929
+ var DATA_SCOPE_MARKER = Symbol.for("openlearn.routeScope.dataScope");
31930
+ function markAuthMiddleware(middleware, roles) {
31931
+ Object.defineProperty(middleware, AUTH_MIDDLEWARE_MARKER, {
31932
+ value: { roles: [...roles] },
31933
+ enumerable: false,
31934
+ configurable: true
31935
+ });
31936
+ return middleware;
31937
+ }
31938
+ function markDataScopeMiddleware(middleware, scope) {
31939
+ Object.defineProperty(middleware, DATA_SCOPE_MARKER, {
31940
+ value: scope,
31941
+ enumerable: false,
31942
+ configurable: true
31943
+ });
31944
+ return middleware;
31945
+ }
31946
+ function readMarker(handle, marker) {
31947
+ if (typeof handle !== "function") return void 0;
31948
+ return handle[marker];
31949
+ }
31950
+ function collectAuthRoutes(app) {
31951
+ const router = app._router ?? app.router;
31952
+ const stack = router?.stack ?? [];
31953
+ const result = [];
31954
+ for (const layer of stack) {
31955
+ const route = layer?.route;
31956
+ if (!route) continue;
31957
+ const handles = (route.stack ?? []).map((entry) => entry.handle);
31958
+ const authMeta = handles.map((handle) => readMarker(handle, AUTH_MIDDLEWARE_MARKER)).find(Boolean);
31959
+ if (!authMeta) continue;
31960
+ const scope = handles.map((handle) => readMarker(handle, DATA_SCOPE_MARKER)).find(Boolean);
31961
+ const methods = Object.keys(route.methods ?? {}).filter((method) => method !== "_all" && route.methods[method]).map((method) => method.toUpperCase());
31962
+ for (const method of methods) {
31963
+ result.push({
31964
+ method,
31965
+ path: String(route.path),
31966
+ scope,
31967
+ roles: [...authMeta.roles]
31968
+ });
31969
+ }
31970
+ }
31971
+ return result;
31972
+ }
31973
+ function isWhitelisted(route, whitelist) {
31974
+ return whitelist.some(
31975
+ (entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
31976
+ );
31977
+ }
31978
+ function normalizePath2(path48) {
31979
+ if (!path48) return "/";
31980
+ return path48.endsWith("/") && path48.length > 1 ? path48.slice(0, -1) : path48;
31981
+ }
31982
+ function auditRouteScopes(app, whitelist = []) {
31983
+ const declared = [];
31984
+ const roleGated = [];
31985
+ const undeclared = [];
31986
+ for (const route of collectAuthRoutes(app)) {
31987
+ if (route.scope) {
31988
+ declared.push(route);
31989
+ continue;
31990
+ }
31991
+ if (route.roles.length > 0) {
31992
+ roleGated.push(route);
31993
+ continue;
31994
+ }
31995
+ if (isWhitelisted(route, whitelist)) continue;
31996
+ undeclared.push(route);
31997
+ }
31998
+ return { declared, roleGated, undeclared };
31999
+ }
32000
+ function formatRouteScopeViolation(route) {
32001
+ return `${route.method} ${route.path}`;
32002
+ }
32003
+ function assertRouteDataScopes(app, options = {}) {
32004
+ const logger2 = options.logger ?? console;
32005
+ const audit = auditRouteScopes(app, options.whitelist ?? []);
32006
+ if (audit.undeclared.length > 0) {
32007
+ const lines = audit.undeclared.map(formatRouteScopeViolation);
32008
+ const message = `[RouteScope] ${audit.undeclared.length} \u4E2A\u5DF2\u8BA4\u8BC1\u8DEF\u7531\u672A\u58F0\u660E\u6570\u636E\u8303\u56F4\uFF08requireScopedAuth\uFF09\uFF1A
32009
+ - ${lines.join("\n - ")}`;
32010
+ if (options.enforce) {
32011
+ logger2.error(message);
32012
+ throw new Error(message);
32013
+ }
32014
+ logger2.warn(message);
32015
+ }
32016
+ if (audit.declared.length > 0) {
32017
+ logger2.info?.(
32018
+ `[RouteScope] \u5DF2\u58F0\u660E\u6570\u636E\u8303\u56F4 ${audit.declared.length} \u6761\uFF0C\u89D2\u8272\u95E8\u7981\u8DEF\u7531 ${audit.roleGated.length} \u6761\uFF0C\u672A\u58F0\u660E ${audit.undeclared.length} \u6761\uFF08\u767D\u540D\u5355 ${options.whitelist?.length ?? 0} \u6761\uFF09\u3002`
32019
+ );
32020
+ }
32021
+ return audit;
32022
+ }
32023
+
32024
+ // server/utils/password-gate.ts
32025
+ var PASSWORD_GATE_SKIP_ENV = "OPENLEARN_SKIP_PASSWORD_CHANGE_GATE";
32026
+ var PASSWORD_GATE_SKIP_ENV_ALIAS = "DEV_BYPASS_PASSWORD_CHANGE_GATE";
32027
+ var productionWarningEmitted = false;
32028
+ function isTruthyFlag2(value) {
32029
+ if (typeof value !== "string") return false;
32030
+ const normalized = value.trim().toLowerCase();
32031
+ return normalized === "1" || normalized === "true";
32032
+ }
32033
+ function isPasswordChangeGateSkipped(env = process.env) {
32034
+ const requested = isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV]) || isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV_ALIAS]);
32035
+ if (!requested) return false;
32036
+ if ((env.NODE_ENV ?? "") === "production") {
32037
+ if (!productionWarningEmitted) {
32038
+ productionWarningEmitted = true;
32039
+ console.warn(
32040
+ `[SECURITY] ${PASSWORD_GATE_SKIP_ENV}/${PASSWORD_GATE_SKIP_ENV_ALIAS} \u5DF2\u914D\u7F6E\uFF0C\u4F46\u751F\u4EA7\u73AF\u5883\uFF08NODE_ENV=production\uFF09\u4E25\u683C\u5FFD\u7565\u8BE5\u5F00\u5173\uFF1A\u5F3A\u5236\u6539\u5BC6\u9632\u7EBF\u4E0D\u5931\u6548\u3002\u8BF7\u7ACB\u5373\u4ECE\u751F\u4EA7\u73AF\u5883\u79FB\u9664\u8BE5\u53D8\u91CF\u3002`
32041
+ );
32042
+ }
32043
+ return false;
32044
+ }
32045
+ return true;
32046
+ }
32047
+
32048
+ // server/services/session-role.ts
32049
+ var SESSION_ROLE_WHITELIST = /* @__PURE__ */ new Set(["administrator", "teacher", "student", "anonymous"]);
32050
+ function resolveSessionRole(session) {
32051
+ if (!session) return "anonymous";
32052
+ for (const candidate of [session.subRole, session.role]) {
32053
+ if (typeof candidate !== "string" || candidate.length === 0) continue;
32054
+ const normalized = candidate === "admin" ? "administrator" : candidate;
32055
+ if (SESSION_ROLE_WHITELIST.has(normalized)) return normalized;
32056
+ }
32057
+ return "anonymous";
32058
+ }
32059
+ function isAdministrator(session) {
32060
+ return resolveSessionRole(session) === "administrator";
32061
+ }
32062
+
32063
+ // server/middleware/auth.ts
32064
+ var SESSION_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
32065
+ function getValidSession(token) {
32066
+ let sessionRow;
32067
+ try {
32068
+ sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
32069
+ } catch {
32070
+ return null;
32071
+ }
32072
+ if (!sessionRow) return null;
32073
+ const now = Date.now();
32074
+ if (sessionRow.expires_at && sessionRow.expires_at < now) {
32075
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
32076
+ return null;
32077
+ }
32078
+ const idleTimeout = 24 * 60 * 60 * 1e3;
32079
+ if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
32080
+ kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
32081
+ return null;
32082
+ }
32083
+ if (!sessionRow.updated_at || now - sessionRow.updated_at > SESSION_REFRESH_INTERVAL_MS) {
32084
+ kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
32085
+ }
32086
+ let session;
32087
+ try {
32088
+ session = JSON.parse(sessionRow.session_data);
32089
+ } catch {
32090
+ return null;
32091
+ }
32092
+ if (session && !session.userId && session.studentId) {
32093
+ session.userId = session.studentId;
32094
+ }
32095
+ if (session.role && !["teacher", "student", "administrator", "admin"].includes(session.role)) {
32096
+ session.role = "anonymous";
32097
+ }
32098
+ return session;
32099
+ }
32100
+ function getCookieToken(req) {
32101
+ const rc = req.headers.cookie;
32102
+ if (!rc) return null;
32103
+ const parts = rc.split(";");
32104
+ for (const part of parts) {
32105
+ const trimmed2 = part.trim();
32106
+ if (trimmed2.startsWith("edu_os_token=")) {
32107
+ return trimmed2.substring("edu_os_token=".length);
32108
+ }
32109
+ }
32110
+ return null;
32111
+ }
32112
+ function getActorId(req) {
32113
+ const token = getCookieToken(req);
32114
+ if (!token) return "anonymous";
32115
+ try {
32116
+ const session = getValidSession(token);
32117
+ if (!session) return "anonymous";
32118
+ const role = resolveSessionRole(session);
32119
+ const rawUserId = session.userId || session.studentId || "demo";
32120
+ const safeUserId = String(rawUserId).replace(/[:\s]/g, "_").slice(0, 64);
32121
+ return `user:${safeUserId}:${role}`;
32122
+ } catch {
32123
+ return "anonymous";
32124
+ }
32125
+ }
32126
+ function requireAuth(...roles) {
32127
+ const middleware = (req, res, next) => {
32128
+ const token = getCookieToken(req);
32129
+ if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
32130
+ const session = getValidSession(token);
32131
+ if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
32132
+ if (roles.length > 0) {
32133
+ const userRole = resolveSessionRole(session);
32134
+ const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
32135
+ if (!effectiveRoles.includes(userRole)) {
32136
+ return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
32137
+ }
32138
+ }
32139
+ req.session = session;
32140
+ next();
32141
+ };
32142
+ return markAuthMiddleware(middleware, roles);
32143
+ }
32144
+ var PASSWORD_CHANGE_EXEMPT_PATHS = /* @__PURE__ */ new Set([
32145
+ "/api/auth/change-password",
32146
+ "/api/auth/logout",
32147
+ "/api/auth/session",
32148
+ "/api/auth/me",
32149
+ "/api/client-errors"
32150
+ ]);
32151
+ function enforcePasswordChanged(req, res, next) {
32152
+ if (process.env.PLAYWRIGHT_TEST) {
32153
+ return next();
32154
+ }
32155
+ if (isPasswordChangeGateSkipped()) {
32156
+ return next();
32157
+ }
32158
+ const path48 = req.path || "";
32159
+ if (!path48.startsWith("/api/")) {
32160
+ return next();
32161
+ }
32162
+ if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path48)) {
32163
+ return next();
32164
+ }
32165
+ const token = getCookieToken(req);
32166
+ if (token) {
32167
+ const session = getValidSession(token);
32168
+ if (session && session.mustChangePassword) {
32169
+ res.status(403).json({
32170
+ success: false,
32171
+ code: "FORBIDDEN_DEFAULT_PASSWORD",
32172
+ error: "Default password must be changed first"
32173
+ });
32174
+ return;
32175
+ }
32176
+ }
32177
+ next();
32178
+ }
32179
+ function socketAuthMiddleware(socket, next) {
32180
+ try {
32181
+ const cookieHeader = socket.handshake.headers.cookie;
32182
+ let token = null;
32183
+ if (cookieHeader) {
32184
+ const parts = cookieHeader.split(";");
32185
+ for (const part of parts) {
32186
+ const trimmed2 = part.trim();
32187
+ if (trimmed2.startsWith("edu_os_token=")) {
32188
+ token = trimmed2.substring("edu_os_token=".length);
32189
+ break;
32190
+ }
32191
+ }
32192
+ }
32193
+ if (!token && socket.handshake.auth?.token) {
32194
+ token = socket.handshake.auth.token;
32195
+ }
32196
+ if (process.env.NODE_ENV === "test" && !token) {
32197
+ return next();
32198
+ }
32199
+ if (!token) {
32200
+ return next(new Error("Authentication required: missing edu_os_token"));
32201
+ }
32202
+ const session = getValidSession(token);
32203
+ if (!session) {
32204
+ return next(new Error("Authentication required: session expired or invalid"));
32205
+ }
32206
+ if (session.mustChangePassword && !isPasswordChangeGateSkipped()) {
32207
+ return next(new Error("FORBIDDEN_DEFAULT_PASSWORD"));
32208
+ }
32209
+ socket.data.session = session;
32210
+ socket.data.userId = session.userId;
32211
+ socket.data.role = session.role;
32212
+ next();
32213
+ } catch (err) {
32214
+ next(new Error(`Authentication error: ${err.message}`));
32215
+ }
32216
+ }
32217
+
32218
+ // server/security/rate-limit.ts
32772
32219
  function loginRateLimitKey(req) {
32773
32220
  const body = req.body ?? {};
32774
32221
  const raw = body.username ?? body.studentId ?? body.studentName ?? "";
@@ -32785,6 +32232,18 @@ function writeRateLimitKey(req) {
32785
32232
  return ip;
32786
32233
  }
32787
32234
  }
32235
+ function sessionRateLimitKey(req) {
32236
+ try {
32237
+ const token = getCookieToken(req);
32238
+ const session = token ? getValidSession(token) : null;
32239
+ if (session) {
32240
+ const identity = String(session.userId || session.studentId || session.username || "").trim();
32241
+ if (identity) return `session:${identity.slice(0, 64)}`;
32242
+ }
32243
+ } catch {
32244
+ }
32245
+ return `ip:${writeRateLimitKey(req)}`;
32246
+ }
32788
32247
  var limiterValidation = {
32789
32248
  xForwardedForHeader: false,
32790
32249
  trustProxy: false,
@@ -32823,6 +32282,17 @@ function createAiLimiter(max) {
32823
32282
  validate: limiterValidation
32824
32283
  });
32825
32284
  }
32285
+ function createSessionWriteLimiter(max) {
32286
+ return (0, import_express_rate_limit.default)({
32287
+ windowMs: 60 * 1e3,
32288
+ max,
32289
+ keyGenerator: sessionRateLimitKey,
32290
+ message: { error: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002Too many requests, please try again later." },
32291
+ standardHeaders: true,
32292
+ legacyHeaders: false,
32293
+ validate: limiterValidation
32294
+ });
32295
+ }
32826
32296
 
32827
32297
  // server/security/trust-proxy.ts
32828
32298
  function resolveTrustProxy(raw) {
@@ -34199,297 +33669,6 @@ function startBackupScheduler(deps) {
34199
33669
  };
34200
33670
  }
34201
33671
 
34202
- // server/security/route-scope.ts
34203
- var AUTH_MIDDLEWARE_MARKER = Symbol.for("openlearn.routeScope.authMiddleware");
34204
- var DATA_SCOPE_MARKER = Symbol.for("openlearn.routeScope.dataScope");
34205
- function markAuthMiddleware(middleware, roles) {
34206
- Object.defineProperty(middleware, AUTH_MIDDLEWARE_MARKER, {
34207
- value: { roles: [...roles] },
34208
- enumerable: false,
34209
- configurable: true
34210
- });
34211
- return middleware;
34212
- }
34213
- function markDataScopeMiddleware(middleware, scope) {
34214
- Object.defineProperty(middleware, DATA_SCOPE_MARKER, {
34215
- value: scope,
34216
- enumerable: false,
34217
- configurable: true
34218
- });
34219
- return middleware;
34220
- }
34221
- function readMarker(handle, marker) {
34222
- if (typeof handle !== "function") return void 0;
34223
- return handle[marker];
34224
- }
34225
- function collectAuthRoutes(app) {
34226
- const router = app._router ?? app.router;
34227
- const stack = router?.stack ?? [];
34228
- const result = [];
34229
- for (const layer of stack) {
34230
- const route = layer?.route;
34231
- if (!route) continue;
34232
- const handles = (route.stack ?? []).map((entry) => entry.handle);
34233
- const authMeta = handles.map((handle) => readMarker(handle, AUTH_MIDDLEWARE_MARKER)).find(Boolean);
34234
- if (!authMeta) continue;
34235
- const scope = handles.map((handle) => readMarker(handle, DATA_SCOPE_MARKER)).find(Boolean);
34236
- const methods = Object.keys(route.methods ?? {}).filter((method) => method !== "_all" && route.methods[method]).map((method) => method.toUpperCase());
34237
- for (const method of methods) {
34238
- result.push({
34239
- method,
34240
- path: String(route.path),
34241
- scope,
34242
- roles: [...authMeta.roles]
34243
- });
34244
- }
34245
- }
34246
- return result;
34247
- }
34248
- function isWhitelisted(route, whitelist) {
34249
- return whitelist.some(
34250
- (entry) => entry.method.toUpperCase() === route.method && normalizePath2(entry.path) === normalizePath2(route.path)
34251
- );
34252
- }
34253
- function normalizePath2(path48) {
34254
- if (!path48) return "/";
34255
- return path48.endsWith("/") && path48.length > 1 ? path48.slice(0, -1) : path48;
34256
- }
34257
- function auditRouteScopes(app, whitelist = []) {
34258
- const declared = [];
34259
- const roleGated = [];
34260
- const undeclared = [];
34261
- for (const route of collectAuthRoutes(app)) {
34262
- if (route.scope) {
34263
- declared.push(route);
34264
- continue;
34265
- }
34266
- if (route.roles.length > 0) {
34267
- roleGated.push(route);
34268
- continue;
34269
- }
34270
- if (isWhitelisted(route, whitelist)) continue;
34271
- undeclared.push(route);
34272
- }
34273
- return { declared, roleGated, undeclared };
34274
- }
34275
- function formatRouteScopeViolation(route) {
34276
- return `${route.method} ${route.path}`;
34277
- }
34278
- function assertRouteDataScopes(app, options = {}) {
34279
- const logger2 = options.logger ?? console;
34280
- const audit = auditRouteScopes(app, options.whitelist ?? []);
34281
- if (audit.undeclared.length > 0) {
34282
- const lines = audit.undeclared.map(formatRouteScopeViolation);
34283
- const message = `[RouteScope] ${audit.undeclared.length} \u4E2A\u5DF2\u8BA4\u8BC1\u8DEF\u7531\u672A\u58F0\u660E\u6570\u636E\u8303\u56F4\uFF08requireScopedAuth\uFF09\uFF1A
34284
- - ${lines.join("\n - ")}`;
34285
- if (options.enforce) {
34286
- logger2.error(message);
34287
- throw new Error(message);
34288
- }
34289
- logger2.warn(message);
34290
- }
34291
- if (audit.declared.length > 0) {
34292
- logger2.info?.(
34293
- `[RouteScope] \u5DF2\u58F0\u660E\u6570\u636E\u8303\u56F4 ${audit.declared.length} \u6761\uFF0C\u89D2\u8272\u95E8\u7981\u8DEF\u7531 ${audit.roleGated.length} \u6761\uFF0C\u672A\u58F0\u660E ${audit.undeclared.length} \u6761\uFF08\u767D\u540D\u5355 ${options.whitelist?.length ?? 0} \u6761\uFF09\u3002`
34294
- );
34295
- }
34296
- return audit;
34297
- }
34298
-
34299
- // server/utils/password-gate.ts
34300
- var PASSWORD_GATE_SKIP_ENV = "OPENLEARN_SKIP_PASSWORD_CHANGE_GATE";
34301
- var PASSWORD_GATE_SKIP_ENV_ALIAS = "DEV_BYPASS_PASSWORD_CHANGE_GATE";
34302
- var productionWarningEmitted = false;
34303
- function isTruthyFlag2(value) {
34304
- if (typeof value !== "string") return false;
34305
- const normalized = value.trim().toLowerCase();
34306
- return normalized === "1" || normalized === "true";
34307
- }
34308
- function isPasswordChangeGateSkipped(env = process.env) {
34309
- const requested = isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV]) || isTruthyFlag2(env[PASSWORD_GATE_SKIP_ENV_ALIAS]);
34310
- if (!requested) return false;
34311
- if ((env.NODE_ENV ?? "") === "production") {
34312
- if (!productionWarningEmitted) {
34313
- productionWarningEmitted = true;
34314
- console.warn(
34315
- `[SECURITY] ${PASSWORD_GATE_SKIP_ENV}/${PASSWORD_GATE_SKIP_ENV_ALIAS} \u5DF2\u914D\u7F6E\uFF0C\u4F46\u751F\u4EA7\u73AF\u5883\uFF08NODE_ENV=production\uFF09\u4E25\u683C\u5FFD\u7565\u8BE5\u5F00\u5173\uFF1A\u5F3A\u5236\u6539\u5BC6\u9632\u7EBF\u4E0D\u5931\u6548\u3002\u8BF7\u7ACB\u5373\u4ECE\u751F\u4EA7\u73AF\u5883\u79FB\u9664\u8BE5\u53D8\u91CF\u3002`
34316
- );
34317
- }
34318
- return false;
34319
- }
34320
- return true;
34321
- }
34322
-
34323
- // server/services/session-role.ts
34324
- var SESSION_ROLE_WHITELIST = /* @__PURE__ */ new Set(["administrator", "teacher", "student", "anonymous"]);
34325
- function resolveSessionRole(session) {
34326
- if (!session) return "anonymous";
34327
- for (const candidate of [session.subRole, session.role]) {
34328
- if (typeof candidate !== "string" || candidate.length === 0) continue;
34329
- const normalized = candidate === "admin" ? "administrator" : candidate;
34330
- if (SESSION_ROLE_WHITELIST.has(normalized)) return normalized;
34331
- }
34332
- return "anonymous";
34333
- }
34334
- function isAdministrator(session) {
34335
- return resolveSessionRole(session) === "administrator";
34336
- }
34337
-
34338
- // server/middleware/auth.ts
34339
- var SESSION_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
34340
- function getValidSession(token) {
34341
- let sessionRow;
34342
- try {
34343
- sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
34344
- } catch {
34345
- return null;
34346
- }
34347
- if (!sessionRow) return null;
34348
- const now = Date.now();
34349
- if (sessionRow.expires_at && sessionRow.expires_at < now) {
34350
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
34351
- return null;
34352
- }
34353
- const idleTimeout = 24 * 60 * 60 * 1e3;
34354
- if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
34355
- kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
34356
- return null;
34357
- }
34358
- if (!sessionRow.updated_at || now - sessionRow.updated_at > SESSION_REFRESH_INTERVAL_MS) {
34359
- kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
34360
- }
34361
- let session;
34362
- try {
34363
- session = JSON.parse(sessionRow.session_data);
34364
- } catch {
34365
- return null;
34366
- }
34367
- if (session && !session.userId && session.studentId) {
34368
- session.userId = session.studentId;
34369
- }
34370
- if (session.role && !["teacher", "student", "administrator", "admin"].includes(session.role)) {
34371
- session.role = "anonymous";
34372
- }
34373
- return session;
34374
- }
34375
- function getCookieToken(req) {
34376
- const rc = req.headers.cookie;
34377
- if (!rc) return null;
34378
- const parts = rc.split(";");
34379
- for (const part of parts) {
34380
- const trimmed2 = part.trim();
34381
- if (trimmed2.startsWith("edu_os_token=")) {
34382
- return trimmed2.substring("edu_os_token=".length);
34383
- }
34384
- }
34385
- return null;
34386
- }
34387
- function getActorId(req) {
34388
- const token = getCookieToken(req);
34389
- if (!token) return "anonymous";
34390
- try {
34391
- const session = getValidSession(token);
34392
- if (!session) return "anonymous";
34393
- const role = resolveSessionRole(session);
34394
- const rawUserId = session.userId || session.studentId || "demo";
34395
- const safeUserId = String(rawUserId).replace(/[:\s]/g, "_").slice(0, 64);
34396
- return `user:${safeUserId}:${role}`;
34397
- } catch {
34398
- return "anonymous";
34399
- }
34400
- }
34401
- function requireAuth(...roles) {
34402
- const middleware = (req, res, next) => {
34403
- const token = getCookieToken(req);
34404
- if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
34405
- const session = getValidSession(token);
34406
- if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
34407
- if (roles.length > 0) {
34408
- const userRole = resolveSessionRole(session);
34409
- const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
34410
- if (!effectiveRoles.includes(userRole)) {
34411
- return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
34412
- }
34413
- }
34414
- req.session = session;
34415
- next();
34416
- };
34417
- return markAuthMiddleware(middleware, roles);
34418
- }
34419
- var PASSWORD_CHANGE_EXEMPT_PATHS = /* @__PURE__ */ new Set([
34420
- "/api/auth/change-password",
34421
- "/api/auth/logout",
34422
- "/api/auth/session",
34423
- "/api/auth/me",
34424
- "/api/client-errors"
34425
- ]);
34426
- function enforcePasswordChanged(req, res, next) {
34427
- if (process.env.PLAYWRIGHT_TEST) {
34428
- return next();
34429
- }
34430
- if (isPasswordChangeGateSkipped()) {
34431
- return next();
34432
- }
34433
- const path48 = req.path || "";
34434
- if (!path48.startsWith("/api/")) {
34435
- return next();
34436
- }
34437
- if (PASSWORD_CHANGE_EXEMPT_PATHS.has(path48)) {
34438
- return next();
34439
- }
34440
- const token = getCookieToken(req);
34441
- if (token) {
34442
- const session = getValidSession(token);
34443
- if (session && session.mustChangePassword) {
34444
- res.status(403).json({
34445
- success: false,
34446
- code: "FORBIDDEN_DEFAULT_PASSWORD",
34447
- error: "Default password must be changed first"
34448
- });
34449
- return;
34450
- }
34451
- }
34452
- next();
34453
- }
34454
- function socketAuthMiddleware(socket, next) {
34455
- try {
34456
- const cookieHeader = socket.handshake.headers.cookie;
34457
- let token = null;
34458
- if (cookieHeader) {
34459
- const parts = cookieHeader.split(";");
34460
- for (const part of parts) {
34461
- const trimmed2 = part.trim();
34462
- if (trimmed2.startsWith("edu_os_token=")) {
34463
- token = trimmed2.substring("edu_os_token=".length);
34464
- break;
34465
- }
34466
- }
34467
- }
34468
- if (!token && socket.handshake.auth?.token) {
34469
- token = socket.handshake.auth.token;
34470
- }
34471
- if (process.env.NODE_ENV === "test" && !token) {
34472
- return next();
34473
- }
34474
- if (!token) {
34475
- return next(new Error("Authentication required: missing edu_os_token"));
34476
- }
34477
- const session = getValidSession(token);
34478
- if (!session) {
34479
- return next(new Error("Authentication required: session expired or invalid"));
34480
- }
34481
- if (session.mustChangePassword && !isPasswordChangeGateSkipped()) {
34482
- return next(new Error("FORBIDDEN_DEFAULT_PASSWORD"));
34483
- }
34484
- socket.data.session = session;
34485
- socket.data.userId = session.userId;
34486
- socket.data.role = session.role;
34487
- next();
34488
- } catch (err) {
34489
- next(new Error(`Authentication error: ${err.message}`));
34490
- }
34491
- }
34492
-
34493
33672
  // server/middleware/csrf.ts
34494
33673
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
34495
33674
  var EXEMPT_PREFIXES = [
@@ -34712,7 +33891,7 @@ function sendSafeError(res, err, status = 500, fallbackMessage = "Internal serve
34712
33891
  }
34713
33892
  const isProd = process.env.NODE_ENV === "production";
34714
33893
  const exposedMessage = isProd && effectiveStatus >= 500 ? fallbackMessage : message;
34715
- const rawCode = err && typeof err === "object" && typeof err.code === "string" ? err.code : void 0;
33894
+ const rawCode = err && typeof err === "object" && typeof err.code === "string" ? err.code : err && typeof err === "object" && err.type === "entity.too.large" ? "PAYLOAD_TOO_LARGE" : void 0;
34716
33895
  const errorCode = effectiveStatus >= 400 && effectiveStatus < 500 && rawCode ? rawCode : void 0;
34717
33896
  return res.status(effectiveStatus).json({ success: false, ...errorCode ? { code: errorCode } : {}, error: exposedMessage });
34718
33897
  }
@@ -36170,15 +35349,28 @@ var LARGE_UPLOAD_ENDPOINTS = [
36170
35349
  "/api/upload",
36171
35350
  "/api/courseware/upload",
36172
35351
  "/api/courseware/inline",
36173
- "/api/plugins/upload-zip"
35352
+ "/api/plugins/upload-zip",
35353
+ "/api/resources"
36174
35354
  ];
36175
35355
  var LARGE_UPLOAD_LIMIT = "110mb";
35356
+ var MEDIUM_UPLOAD_ENDPOINTS = [
35357
+ "/api/auth/avatar",
35358
+ "/api/timetable/ocr",
35359
+ "/api/site-settings",
35360
+ "/api/agent/chat"
35361
+ ];
35362
+ var MEDIUM_UPLOAD_LIMIT = "30mb";
36176
35363
  function mountLargeBodyParsers(app) {
36177
35364
  app.use(
36178
35365
  LARGE_UPLOAD_ENDPOINTS,
36179
35366
  import_express2.default.json({ limit: LARGE_UPLOAD_LIMIT }),
36180
35367
  import_express2.default.urlencoded({ limit: LARGE_UPLOAD_LIMIT, extended: true })
36181
35368
  );
35369
+ app.use(
35370
+ MEDIUM_UPLOAD_ENDPOINTS,
35371
+ import_express2.default.json({ limit: MEDIUM_UPLOAD_LIMIT }),
35372
+ import_express2.default.urlencoded({ limit: MEDIUM_UPLOAD_LIMIT, extended: true })
35373
+ );
36182
35374
  app.use(
36183
35375
  COURSE_PACKAGE_IMPORT_PATH,
36184
35376
  import_express2.default.json({ limit: COURSE_PACKAGE_IMPORT_BODY_LIMIT_BYTES }),
@@ -36262,164 +35454,706 @@ window.addEventListener('message', async function (event) {
36262
35454
  post({ type: 'deactivated', id: data.id });
36263
35455
  return;
36264
35456
  }
36265
- if (data.type !== 'load') return;
36266
- targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
36267
- try {
36268
- const blob = new Blob([data.source], { type: 'text/javascript' });
36269
- const url = URL.createObjectURL(blob);
36270
- let mod;
36271
- try { mod = await import(url); } finally { URL.revokeObjectURL(url); }
36272
- const plugin = mod.default || mod;
36273
- if (typeof plugin.activate !== 'function') throw new Error('missing activate function');
36274
- activePlugin = plugin;
36275
- await Promise.race([
36276
- plugin.activate(makeCtx(data.pluginId, data.manifest)),
36277
- new Promise(function (_, reject) { setTimeout(function () { reject(new Error('Activation timeout (5000ms)')); }, 5000); })
36278
- ]);
36279
- post({ type: 'activated', id: data.id });
36280
- } catch (error) {
36281
- post({ type: 'activate-error', id: data.id, error: error && error.message ? error.message : String(error) });
35457
+ if (data.type !== 'load') return;
35458
+ targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
35459
+ try {
35460
+ const blob = new Blob([data.source], { type: 'text/javascript' });
35461
+ const url = URL.createObjectURL(blob);
35462
+ let mod;
35463
+ try { mod = await import(url); } finally { URL.revokeObjectURL(url); }
35464
+ const plugin = mod.default || mod;
35465
+ if (typeof plugin.activate !== 'function') throw new Error('missing activate function');
35466
+ activePlugin = plugin;
35467
+ await Promise.race([
35468
+ plugin.activate(makeCtx(data.pluginId, data.manifest)),
35469
+ new Promise(function (_, reject) { setTimeout(function () { reject(new Error('Activation timeout (5000ms)')); }, 5000); })
35470
+ ]);
35471
+ post({ type: 'activated', id: data.id });
35472
+ } catch (error) {
35473
+ post({ type: 'activate-error', id: data.id, error: error && error.message ? error.message : String(error) });
35474
+ }
35475
+ });
35476
+ post({ type: 'ready' });
35477
+ </script>
35478
+ </body></html>`;
35479
+
35480
+ // server/utils/plugin-view-bridge.ts
35481
+ var PLUGIN_VIEW_CSP = "sandbox allow-scripts allow-forms allow-downloads; frame-ancestors 'self'; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src 'self' 'unsafe-inline' data: blob: https:; style-src-attr 'unsafe-inline'; connect-src 'self' blob: data: https:;";
35482
+ var PLUGIN_VIEW_BRIDGE_JS = `(function () {
35483
+ 'use strict';
35484
+ const CHANNEL = 'ol-plugin-view';
35485
+ let targetOrigin = '*';
35486
+ const pending = new Map();
35487
+ const initCallbacks = [];
35488
+ const contextUpdateCallbacks = [];
35489
+ let currentContext = null;
35490
+
35491
+ function post(msg) {
35492
+ if (window.parent && window.parent !== window) {
35493
+ window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
35494
+ }
35495
+ }
35496
+
35497
+ function call(op, args) {
35498
+ const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
35499
+ return new Promise(function (resolve, reject) {
35500
+ pending.set(callId, { resolve: resolve, reject: reject });
35501
+ post({ type: 'call', callId: callId, op: op, args: args || [] });
35502
+ });
35503
+ }
35504
+
35505
+ window.addEventListener('message', function (event) {
35506
+ const data = event.data;
35507
+ if (!data || data.channel !== CHANNEL) return;
35508
+ if (event.source !== window.parent) return;
35509
+
35510
+ if (data.type === 'init') {
35511
+ currentContext = data.payload || {};
35512
+ targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
35513
+ for (let i = 0; i < initCallbacks.length; i++) {
35514
+ try {
35515
+ initCallbacks[i](currentContext);
35516
+ } catch (e) {
35517
+ console.error('[OpenLearnPluginBridge] init callback error:', e);
35518
+ }
35519
+ }
35520
+ return;
35521
+ }
35522
+
35523
+ if (data.type === 'context-update') {
35524
+ currentContext = Object.assign({}, currentContext, data.payload || {});
35525
+ for (let i = 0; i < contextUpdateCallbacks.length; i++) {
35526
+ try {
35527
+ contextUpdateCallbacks[i](currentContext);
35528
+ } catch (e) {
35529
+ console.error('[OpenLearnPluginBridge] contextUpdate callback error:', e);
35530
+ }
35531
+ }
35532
+ return;
35533
+ }
35534
+
35535
+ if (data.type === 'reply') {
35536
+ const waiter = pending.get(data.callId);
35537
+ if (!waiter) return;
35538
+ pending.delete(data.callId);
35539
+ if (data.ok) {
35540
+ waiter.resolve(data.value);
35541
+ } else {
35542
+ waiter.reject(new Error(data.error || 'bridge call failed'));
35543
+ }
35544
+ return;
35545
+ }
35546
+ });
35547
+
35548
+ const bridge = {
35549
+ CHANNEL: CHANNEL,
35550
+ onInit: function (callback) {
35551
+ if (typeof callback === 'function') {
35552
+ if (currentContext) {
35553
+ try {
35554
+ callback(currentContext);
35555
+ } catch (e) {
35556
+ console.error('[OpenLearnPluginBridge] onInit callback error:', e);
35557
+ }
35558
+ }
35559
+ initCallbacks.push(callback);
35560
+ }
35561
+ return bridge;
35562
+ },
35563
+ onContextUpdate: function (callback) {
35564
+ if (typeof callback === 'function') {
35565
+ contextUpdateCallbacks.push(callback);
35566
+ }
35567
+ return bridge;
35568
+ },
35569
+ getContext: function () {
35570
+ return currentContext;
35571
+ },
35572
+ ui: {
35573
+ toast: function (message, type) {
35574
+ return call('ui.toast', [message, type || 'info']);
35575
+ },
35576
+ showToast: function (message, type) {
35577
+ return call('ui.toast', [message, type || 'info']);
35578
+ },
35579
+ resize: function (height) {
35580
+ post({ type: 'resize', height: Number(height) || 0 });
35581
+ },
35582
+ },
35583
+ invokeCommand: function (type, payload) {
35584
+ return call('command.invoke', [type, payload]);
35585
+ },
35586
+ api: {
35587
+ get: function (path) {
35588
+ return call('api.get', [path]);
35589
+ },
35590
+ post: function (path, body) {
35591
+ return call('api.post', [path, body]);
35592
+ },
35593
+ del: function (path) {
35594
+ return call('api.del', [path]);
35595
+ },
35596
+ },
35597
+ storage: {
35598
+ get: function (key) {
35599
+ return call('storage.get', [key]);
35600
+ },
35601
+ set: function (key, value) {
35602
+ return call('storage.set', [key, value]);
35603
+ },
35604
+ delete: function (key) {
35605
+ return call('storage.delete', [key]);
35606
+ },
35607
+ },
35608
+ };
35609
+
35610
+ window.OpenLearnPluginBridge = bridge;
35611
+ // \u901A\u77E5\u5BBF\u4E3B\u89C6\u56FE\u6C99\u7BB1 iframe \u5DF2\u5C31\u7EEA
35612
+ post({ type: 'ready' });
35613
+ })();
35614
+ `;
35615
+
35616
+ // packages/core/bootstrap/types/index.ts
35617
+ var PlatformBootstrapError = class extends Error {
35618
+ constructor(message, stage, cause) {
35619
+ super(`[PlatformBootstrapError${stage ? `:${stage}` : ""}] ${message}`);
35620
+ this.stage = stage;
35621
+ this.cause = cause;
35622
+ this.name = "PlatformBootstrapError";
35623
+ }
35624
+ };
35625
+ var ConfigurationError = class extends PlatformBootstrapError {
35626
+ constructor(message, cause) {
35627
+ super(message, "Configuring" /* Configuring */, cause);
35628
+ this.name = "ConfigurationError";
35629
+ }
35630
+ };
35631
+ var StartupTimeoutError = class extends PlatformBootstrapError {
35632
+ constructor(message, stage, cause) {
35633
+ super(message, stage, cause);
35634
+ this.name = "StartupTimeoutError";
35635
+ }
35636
+ };
35637
+ var PLATFORM_VERSION2 = PLATFORM_VERSION;
35638
+ var BOOTSTRAP_STAGE_NAMES = Object.freeze([
35639
+ "Created" /* Created */,
35640
+ "Configuring" /* Configuring */,
35641
+ "Registering" /* Registering */,
35642
+ "Initializing" /* Initializing */,
35643
+ "Activating" /* Activating */,
35644
+ "Ready" /* Ready */,
35645
+ "ShuttingDown" /* ShuttingDown */,
35646
+ "Disposed" /* Disposed */
35647
+ ]);
35648
+ var DEFAULT_BOOTSTRAP_CONFIG = Object.freeze({
35649
+ environment: "development",
35650
+ mode: "standalone",
35651
+ port: 9e3,
35652
+ debug: false,
35653
+ pluginsDir: "./plugins",
35654
+ dbPath: "./packages/core/db/educational_os.db"
35655
+ });
35656
+
35657
+ // packages/core/bootstrap/pipeline/pipeline-executor.ts
35658
+ var PipelineExecutor = class {
35659
+ constructor() {
35660
+ this.listeners = /* @__PURE__ */ new Set();
35661
+ }
35662
+ addListener(listener) {
35663
+ this.listeners.add(listener);
35664
+ return () => {
35665
+ this.listeners.delete(listener);
35666
+ };
35667
+ }
35668
+ emit(event) {
35669
+ for (const listener of Array.from(this.listeners)) {
35670
+ try {
35671
+ listener(event);
35672
+ } catch (err) {
35673
+ console.error("[PipelineExecutor] Listener error:", err);
35674
+ }
35675
+ }
35676
+ }
35677
+ async execute(stages, context) {
35678
+ const startTime = Date.now();
35679
+ const results = [];
35680
+ const executedStages = [];
35681
+ this.emit({ type: "PipelineStarted", timestamp: startTime });
35682
+ console.log("[Platform] Starting Bootstrap Pipeline execution...");
35683
+ for (const stage of stages) {
35684
+ if (context.isCancelled) {
35685
+ console.warn(`[Platform] Pipeline cancelled before stage ${stage.name}`);
35686
+ const totalDurationMs2 = Date.now() - startTime;
35687
+ return {
35688
+ totalDurationMs: totalDurationMs2,
35689
+ status: "Aborted",
35690
+ stageResults: Object.freeze(results)
35691
+ };
35692
+ }
35693
+ console.log(`[Platform] Entering ${stage.name}`);
35694
+ this.emit({
35695
+ type: "StageStarted",
35696
+ timestamp: Date.now(),
35697
+ stageId: stage.id,
35698
+ stageName: String(stage.name)
35699
+ });
35700
+ const stageStart = Date.now();
35701
+ try {
35702
+ if (stage.timeoutMs && stage.timeoutMs > 0) {
35703
+ await this.executeWithTimeout(stage, context, stage.timeoutMs);
35704
+ } else {
35705
+ await stage.execute(context);
35706
+ }
35707
+ const durationMs = Date.now() - stageStart;
35708
+ console.log(`[Platform] Completed ${stage.name} (${durationMs} ms)`);
35709
+ results.push({
35710
+ stageId: stage.id,
35711
+ stageName: String(stage.name),
35712
+ durationMs,
35713
+ status: "Success"
35714
+ });
35715
+ executedStages.push(stage);
35716
+ this.emit({
35717
+ type: "StageCompleted",
35718
+ timestamp: Date.now(),
35719
+ stageId: stage.id,
35720
+ stageName: String(stage.name),
35721
+ durationMs
35722
+ });
35723
+ } catch (err) {
35724
+ const error = err instanceof Error ? err : new Error(String(err));
35725
+ const durationMs = Date.now() - stageStart;
35726
+ console.error(`[Platform] Failed ${stage.name} (${durationMs} ms): ${error.message}`);
35727
+ results.push({
35728
+ stageId: stage.id,
35729
+ stageName: String(stage.name),
35730
+ durationMs,
35731
+ status: "Failed",
35732
+ error
35733
+ });
35734
+ this.emit({
35735
+ type: "StageFailed",
35736
+ timestamp: Date.now(),
35737
+ stageId: stage.id,
35738
+ stageName: String(stage.name),
35739
+ durationMs,
35740
+ error
35741
+ });
35742
+ await this.rollbackExecutedStages(executedStages, context);
35743
+ const totalDurationMs2 = Date.now() - startTime;
35744
+ const pipelineResult = {
35745
+ totalDurationMs: totalDurationMs2,
35746
+ status: "Failed",
35747
+ stageResults: Object.freeze(results),
35748
+ failedStage: stage.id,
35749
+ error
35750
+ };
35751
+ this.emit({
35752
+ type: "PipelineCompleted",
35753
+ timestamp: Date.now(),
35754
+ durationMs: totalDurationMs2,
35755
+ error
35756
+ });
35757
+ return pipelineResult;
35758
+ }
35759
+ }
35760
+ const totalDurationMs = Date.now() - startTime;
35761
+ console.log(`[Platform] Ready. Total startup duration: ${totalDurationMs} ms.`);
35762
+ const finalResult = {
35763
+ totalDurationMs,
35764
+ status: "Success",
35765
+ stageResults: Object.freeze(results)
35766
+ };
35767
+ this.emit({
35768
+ type: "PipelineCompleted",
35769
+ timestamp: Date.now(),
35770
+ durationMs: totalDurationMs
35771
+ });
35772
+ return finalResult;
35773
+ }
35774
+ async executeWithTimeout(stage, context, timeoutMs) {
35775
+ let timer;
35776
+ const timeoutPromise = new Promise((_, reject) => {
35777
+ timer = setTimeout(() => {
35778
+ reject(new StartupTimeoutError(`Stage execution timed out after ${timeoutMs} ms`));
35779
+ }, timeoutMs);
35780
+ });
35781
+ try {
35782
+ await Promise.race([stage.execute(context), timeoutPromise]);
35783
+ } finally {
35784
+ if (timer) {
35785
+ clearTimeout(timer);
35786
+ }
35787
+ }
35788
+ }
35789
+ async rollbackExecutedStages(executedStages, context) {
35790
+ console.log("[Platform] Triggering stage rollbacks...");
35791
+ for (let i = executedStages.length - 1; i >= 0; i--) {
35792
+ const stage = executedStages[i];
35793
+ if (stage.rollback) {
35794
+ try {
35795
+ console.log(`[Platform] Rolling back ${stage.name}`);
35796
+ await stage.rollback(context);
35797
+ } catch (rbErr) {
35798
+ console.error(`[Platform] Error during rollback of ${stage.name}:`, rbErr);
35799
+ }
35800
+ }
35801
+ }
35802
+ }
35803
+ };
35804
+
35805
+ // packages/core/bootstrap/pipeline/stages/standard-stages.ts
35806
+ var StartupStageImpl = class {
35807
+ constructor() {
35808
+ this.id = "stage_startup";
35809
+ this.name = "Created" /* Created */;
35810
+ this.description = "Environment & configuration validation";
35811
+ }
35812
+ async execute(context) {
35813
+ context.setStage("Configuring" /* Configuring */);
35814
+ }
35815
+ };
35816
+ var RegistrationStageImpl = class {
35817
+ constructor() {
35818
+ this.id = "stage_registration";
35819
+ this.name = "Registering" /* Registering */;
35820
+ this.description = "Core Service Contracts & DI Tokens registration";
35821
+ }
35822
+ async execute(context) {
35823
+ context.setStage("Registering" /* Registering */);
35824
+ }
35825
+ };
35826
+ var InitializationStageImpl = class {
35827
+ constructor() {
35828
+ this.id = "stage_initialization";
35829
+ this.name = "Initializing" /* Initializing */;
35830
+ this.description = "Subsystem Kernels initialization";
35831
+ }
35832
+ async execute(context) {
35833
+ context.setStage("Initializing" /* Initializing */);
35834
+ }
35835
+ };
35836
+ var ActivationStageImpl = class {
35837
+ constructor() {
35838
+ this.id = "stage_activation";
35839
+ this.name = "Activating" /* Activating */;
35840
+ this.description = "Plugin Host discovery & ESM plugins activation";
35841
+ }
35842
+ async execute(context) {
35843
+ context.setStage("Activating" /* Activating */);
35844
+ }
35845
+ };
35846
+ var ReadyStageImpl = class {
35847
+ constructor() {
35848
+ this.id = "stage_ready";
35849
+ this.name = "Ready" /* Ready */;
35850
+ this.description = "HTTP & Socket.IO server readiness";
35851
+ }
35852
+ async execute(context) {
35853
+ context.setStage("Ready" /* Ready */);
35854
+ }
35855
+ };
35856
+
35857
+ // packages/core/bootstrap/pipeline/bootstrap-pipeline.ts
35858
+ var BootstrapPipeline = class {
35859
+ constructor(stages) {
35860
+ this._stages = [];
35861
+ this.executor = new PipelineExecutor();
35862
+ if (stages && stages.length > 0) {
35863
+ this._stages = [...stages];
35864
+ } else {
35865
+ this._stages = [
35866
+ new StartupStageImpl(),
35867
+ new RegistrationStageImpl(),
35868
+ new InitializationStageImpl(),
35869
+ new ActivationStageImpl(),
35870
+ new ReadyStageImpl()
35871
+ ];
35872
+ }
35873
+ }
35874
+ get stages() {
35875
+ return Object.freeze(this._stages);
35876
+ }
35877
+ addStage(stage) {
35878
+ this._stages.push(stage);
35879
+ return this;
36282
35880
  }
36283
- });
36284
- post({ type: 'ready' });
36285
- </script>
36286
- </body></html>`;
36287
-
36288
- // server/utils/plugin-view-bridge.ts
36289
- var PLUGIN_VIEW_CSP = "sandbox allow-scripts allow-forms allow-downloads; frame-ancestors 'self'; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src 'self' 'unsafe-inline' data: blob: https:; style-src-attr 'unsafe-inline'; connect-src 'self' blob: data: https:;";
36290
- var PLUGIN_VIEW_BRIDGE_JS = `(function () {
36291
- 'use strict';
36292
- const CHANNEL = 'ol-plugin-view';
36293
- let targetOrigin = '*';
36294
- const pending = new Map();
36295
- const initCallbacks = [];
36296
- const contextUpdateCallbacks = [];
36297
- let currentContext = null;
36298
-
36299
- function post(msg) {
36300
- if (window.parent && window.parent !== window) {
36301
- window.parent.postMessage(Object.assign({ channel: CHANNEL }, msg), targetOrigin);
35881
+ addListener(listener) {
35882
+ return this.executor.addListener(listener);
35883
+ }
35884
+ async run(context) {
35885
+ const result = await this.execute(context);
35886
+ if (result.status === "Failed") {
35887
+ throw result.error || new Error(`Bootstrap Pipeline failed at stage: ${result.failedStage}`);
36302
35888
  }
36303
35889
  }
36304
-
36305
- function call(op, args) {
36306
- const callId = Math.random().toString(16).slice(2) + Date.now().toString(16);
36307
- return new Promise(function (resolve, reject) {
36308
- pending.set(callId, { resolve: resolve, reject: reject });
36309
- post({ type: 'call', callId: callId, op: op, args: args || [] });
36310
- });
35890
+ async execute(context) {
35891
+ return this.executor.execute(this._stages, context);
36311
35892
  }
35893
+ };
36312
35894
 
36313
- window.addEventListener('message', function (event) {
36314
- const data = event.data;
36315
- if (!data || data.channel !== CHANNEL) return;
36316
- if (event.source !== window.parent) return;
36317
-
36318
- if (data.type === 'init') {
36319
- currentContext = data.payload || {};
36320
- targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
36321
- for (let i = 0; i < initCallbacks.length; i++) {
36322
- try {
36323
- initCallbacks[i](currentContext);
36324
- } catch (e) {
36325
- console.error('[OpenLearnPluginBridge] init callback error:', e);
35895
+ // packages/core/bootstrap/builder/builder-validation-engine.ts
35896
+ var BuilderValidationEngine = class {
35897
+ static validate(config, environment, stages) {
35898
+ const errors = [];
35899
+ const warnings = [];
35900
+ if (!["development", "production", "test"].includes(environment)) {
35901
+ errors.push({
35902
+ code: "INVALID_ENVIRONMENT",
35903
+ message: `Environment must be one of 'development', 'production', 'test'. Received: '${environment}'`,
35904
+ field: "environment"
35905
+ });
35906
+ }
35907
+ if (!config.port || config.port <= 0 || config.port > 65535) {
35908
+ errors.push({
35909
+ code: "INVALID_PORT",
35910
+ message: `Port must be a valid number between 1 and 65535. Received: ${config.port}`,
35911
+ field: "port"
35912
+ });
35913
+ }
35914
+ if (!stages || stages.length === 0) {
35915
+ errors.push({
35916
+ code: "MISSING_STAGES",
35917
+ message: "Bootstrap pipeline contains no registered stages.",
35918
+ field: "stages"
35919
+ });
35920
+ } else {
35921
+ const stageIds = /* @__PURE__ */ new Set();
35922
+ for (const stage of stages) {
35923
+ if (!stage.id) {
35924
+ errors.push({
35925
+ code: "INVALID_STAGE",
35926
+ message: `Stage '${stage.name}' has no unique ID.`
35927
+ });
35928
+ } else if (stageIds.has(stage.id)) {
35929
+ errors.push({
35930
+ code: "DUPLICATE_STAGE",
35931
+ message: `Duplicate stage registration detected for stage ID '${stage.id}'.`,
35932
+ field: "stage.id"
35933
+ });
35934
+ } else {
35935
+ stageIds.add(stage.id);
36326
35936
  }
36327
35937
  }
36328
- return;
36329
35938
  }
35939
+ return {
35940
+ isValid: errors.length === 0,
35941
+ errors: Object.freeze(errors),
35942
+ warnings: Object.freeze(warnings)
35943
+ };
35944
+ }
35945
+ };
36330
35946
 
36331
- if (data.type === 'context-update') {
36332
- currentContext = Object.assign({}, currentContext, data.payload || {});
36333
- for (let i = 0; i < contextUpdateCallbacks.length; i++) {
36334
- try {
36335
- contextUpdateCallbacks[i](currentContext);
36336
- } catch (e) {
36337
- console.error('[OpenLearnPluginBridge] contextUpdate callback error:', e);
35947
+ // packages/core/bootstrap/builder/platform-builder.ts
35948
+ var DefaultPlatformLogger = class {
35949
+ info(message, ...args) {
35950
+ console.log(`[PlatformBuilder:INFO] ${message}`, ...args);
35951
+ }
35952
+ warn(message, ...args) {
35953
+ console.warn(`[PlatformBuilder:WARN] ${message}`, ...args);
35954
+ }
35955
+ error(message, ...args) {
35956
+ console.error(`[PlatformBuilder:ERROR] ${message}`, ...args);
35957
+ }
35958
+ debug(message, ...args) {
35959
+ console.debug(`[PlatformBuilder:DEBUG] ${message}`, ...args);
35960
+ }
35961
+ };
35962
+ var PlatformBuilder = class _PlatformBuilder {
35963
+ constructor(options) {
35964
+ this._state = "Created";
35965
+ this._config = { ...DEFAULT_BOOTSTRAP_CONFIG };
35966
+ this._logger = new DefaultPlatformLogger();
35967
+ this._environment = "development";
35968
+ this._metadata = /* @__PURE__ */ new Map();
35969
+ this._services = /* @__PURE__ */ new Map();
35970
+ this._capabilities = /* @__PURE__ */ new Map();
35971
+ this._extensions = /* @__PURE__ */ new Map();
35972
+ this._pipeline = new BootstrapPipeline();
35973
+ this._state = "Created";
35974
+ this._logger.info("Builder Started");
35975
+ if (options) {
35976
+ if (options.config) this.withConfiguration(options.config);
35977
+ if (options.logger) this.withLogger(options.logger);
35978
+ if (options.environment) this.withEnvironment(options.environment);
35979
+ if (options.metadata) {
35980
+ for (const [k, v] of Object.entries(options.metadata)) {
35981
+ this.withMetadata(k, v);
36338
35982
  }
36339
35983
  }
36340
- return;
36341
35984
  }
36342
-
36343
- if (data.type === 'reply') {
36344
- const waiter = pending.get(data.callId);
36345
- if (!waiter) return;
36346
- pending.delete(data.callId);
36347
- if (data.ok) {
36348
- waiter.resolve(data.value);
36349
- } else {
36350
- waiter.reject(new Error(data.error || 'bridge call failed'));
36351
- }
36352
- return;
35985
+ }
35986
+ static create(options) {
35987
+ return new _PlatformBuilder(options);
35988
+ }
35989
+ get state() {
35990
+ return this._state;
35991
+ }
35992
+ transitionTo(newState) {
35993
+ if (this._state === "Disposed") {
35994
+ throw new Error("PlatformBuilder has been disposed and cannot accept further transitions.");
36353
35995
  }
36354
- });
36355
-
36356
- const bridge = {
36357
- CHANNEL: CHANNEL,
36358
- onInit: function (callback) {
36359
- if (typeof callback === 'function') {
36360
- if (currentContext) {
36361
- try {
36362
- callback(currentContext);
36363
- } catch (e) {
36364
- console.error('[OpenLearnPluginBridge] onInit callback error:', e);
36365
- }
36366
- }
36367
- initCallbacks.push(callback);
36368
- }
36369
- return bridge;
36370
- },
36371
- onContextUpdate: function (callback) {
36372
- if (typeof callback === 'function') {
36373
- contextUpdateCallbacks.push(callback);
36374
- }
36375
- return bridge;
36376
- },
36377
- getContext: function () {
36378
- return currentContext;
36379
- },
36380
- ui: {
36381
- toast: function (message, type) {
36382
- return call('ui.toast', [message, type || 'info']);
35996
+ if (this._state === "Built" && newState !== "Disposed") {
35997
+ throw new Error(`Invalid state transition from Built to ${newState}. Builder is already built.`);
35998
+ }
35999
+ this._state = newState;
36000
+ }
36001
+ withConfiguration(config) {
36002
+ this.transitionTo("Configuring");
36003
+ this._config = { ...this._config, ...config };
36004
+ if (config.environment) {
36005
+ this._environment = config.environment;
36006
+ }
36007
+ this._logger.info("Configuration Loaded");
36008
+ return this;
36009
+ }
36010
+ withLogger(logger2) {
36011
+ this.transitionTo("Configuring");
36012
+ this._logger = logger2;
36013
+ return this;
36014
+ }
36015
+ withEnvironment(env) {
36016
+ this.transitionTo("Configuring");
36017
+ this._environment = env;
36018
+ this._config = { ...this._config, environment: env };
36019
+ return this;
36020
+ }
36021
+ withMetadata(key, value) {
36022
+ this.transitionTo("Configuring");
36023
+ this._metadata.set(key, value);
36024
+ return this;
36025
+ }
36026
+ addBootstrapStage(stage) {
36027
+ this.transitionTo("Configuring");
36028
+ this._pipeline.addStage(stage);
36029
+ return this;
36030
+ }
36031
+ addService(serviceId, instance) {
36032
+ this.transitionTo("Configuring");
36033
+ this._services.set(serviceId, instance);
36034
+ return this;
36035
+ }
36036
+ addCapability(capabilityId, descriptor) {
36037
+ this.transitionTo("Configuring");
36038
+ this._capabilities.set(capabilityId, descriptor);
36039
+ return this;
36040
+ }
36041
+ addExtension(extensionId, extensionSpec) {
36042
+ this.transitionTo("Configuring");
36043
+ this._extensions.set(extensionId, extensionSpec);
36044
+ return this;
36045
+ }
36046
+ buildResult() {
36047
+ const buildStart = Date.now();
36048
+ this.transitionTo("Validating");
36049
+ this._logger.info("Validation Started");
36050
+ const validation = BuilderValidationEngine.validate(this._config, this._environment, this._pipeline.stages);
36051
+ this._logger.info("Validation Completed");
36052
+ if (!validation.isValid) {
36053
+ this._logger.error(`Validation Failed with ${validation.errors.length} errors.`);
36054
+ throw new ConfigurationError(
36055
+ `PlatformBuilder validation failed: ${validation.errors.map((e) => e.message).join("; ")}`
36056
+ );
36057
+ }
36058
+ this.transitionTo("Building");
36059
+ this._logger.info("Build Started");
36060
+ const platformContext = {
36061
+ platformId: `plt_${globalThis.crypto.randomUUID()}`,
36062
+ version: PLATFORM_VERSION2,
36063
+ namespace: "openlearn.core",
36064
+ mode: this._config.mode,
36065
+ environment: {
36066
+ type: this._environment,
36067
+ isDevelopment: this._environment === "development",
36068
+ isProduction: this._environment === "production",
36069
+ isTest: this._environment === "test",
36070
+ isPluginSandbox: false
36383
36071
  },
36384
- showToast: function (message, type) {
36385
- return call('ui.toast', [message, type || 'info']);
36072
+ config: this._config,
36073
+ featureFlags: {
36074
+ isEnabled: () => true,
36075
+ getFlag: (_, def) => def,
36076
+ getAllFlags: () => ({})
36386
36077
  },
36387
- resize: function (height) {
36388
- post({ type: 'resize', height: Number(height) || 0 });
36078
+ runtimeMetadata: {
36079
+ os: process.platform,
36080
+ nodeVersion: process.version,
36081
+ arch: process.arch,
36082
+ processId: process.pid,
36083
+ memoryUsage: process.memoryUsage(),
36084
+ buildVersion: PLATFORM_VERSION2
36389
36085
  },
36390
- },
36391
- invokeCommand: function (type, payload) {
36392
- return call('command.invoke', [type, payload]);
36393
- },
36394
- api: {
36395
- get: function (path) {
36396
- return call('api.get', [path]);
36086
+ bootstrapState: "Bootstrapping",
36087
+ logger: this._logger,
36088
+ diagnostics: {
36089
+ getMetrics: () => ({ uptime: 0 }),
36090
+ getSystemStatus: () => ({ isHealthy: true, activeServicesCount: this._services.size, uptimeSeconds: 0 })
36397
36091
  },
36398
- post: function (path, body) {
36399
- return call('api.post', [path, body]);
36092
+ serviceLocator: {
36093
+ hasService: (id) => this._services.has(id),
36094
+ getService: (id) => this._services.get(id)
36400
36095
  },
36401
- del: function (path) {
36402
- return call('api.del', [path]);
36096
+ capabilityCatalog: {
36097
+ hasCapability: (id) => this._capabilities.has(id),
36098
+ getCapability: (id) => this._capabilities.get(id)
36403
36099
  },
36404
- },
36405
- storage: {
36406
- get: function (key) {
36407
- return call('storage.get', [key]);
36100
+ extensionCatalog: {
36101
+ hasExtension: (id) => this._extensions.has(id),
36102
+ getExtension: (id) => this._extensions.get(id)
36408
36103
  },
36409
- set: function (key, value) {
36410
- return call('storage.set', [key, value]);
36104
+ eventDispatcher: {
36105
+ dispatch: async () => {
36106
+ }
36411
36107
  },
36412
- delete: function (key) {
36413
- return call('storage.delete', [key]);
36108
+ shutdownToken: {
36109
+ isRequested: false,
36110
+ onShutdown: () => {
36111
+ }
36112
+ }
36113
+ };
36114
+ const buildDurationMs = Date.now() - buildStart;
36115
+ this.transitionTo("Built");
36116
+ this._logger.info(`Build Completed in ${buildDurationMs} ms`);
36117
+ const result = {
36118
+ platformContext,
36119
+ pipeline: this._pipeline,
36120
+ buildDurationMs,
36121
+ validation,
36122
+ metadata: Object.fromEntries(this._metadata.entries()),
36123
+ builderVersion: PLATFORM_VERSION2
36124
+ };
36125
+ return result;
36126
+ }
36127
+ async build() {
36128
+ const res = this.buildResult();
36129
+ return {
36130
+ currentStage: "Created" /* Created */,
36131
+ context: res.platformContext,
36132
+ pipeline: res.pipeline,
36133
+ start: async () => {
36414
36134
  },
36415
- },
36416
- };
36417
-
36418
- window.OpenLearnPluginBridge = bridge;
36419
- // \u901A\u77E5\u5BBF\u4E3B\u89C6\u56FE\u6C99\u7BB1 iframe \u5DF2\u5C31\u7EEA
36420
- post({ type: 'ready' });
36421
- })();
36422
- `;
36135
+ shutdown: async () => {
36136
+ },
36137
+ dispose: async () => {
36138
+ }
36139
+ };
36140
+ }
36141
+ async start() {
36142
+ const kernel = await this.build();
36143
+ await kernel.start();
36144
+ return kernel;
36145
+ }
36146
+ async shutdown() {
36147
+ this.transitionTo("Disposed");
36148
+ }
36149
+ dispose() {
36150
+ this._state = "Disposed";
36151
+ this._services.clear();
36152
+ this._capabilities.clear();
36153
+ this._extensions.clear();
36154
+ this._metadata.clear();
36155
+ }
36156
+ };
36423
36157
 
36424
36158
  // packages/core/bootstrap/adapter/bootstrap-registration.ts
36425
36159
  var BootstrapRegistration = class {
@@ -38406,9 +38140,12 @@ var generateStudentNumber = (db2) => {
38406
38140
  return `S${nextSeq.toString().padStart(3, "0")}`;
38407
38141
  };
38408
38142
 
38143
+ // server/services/ai-provider-resolver.ts
38144
+ init_ai_provider_resolver();
38145
+
38409
38146
  // server/routes/os.ts
38410
38147
  function registerOsRoutes(ctx) {
38411
- const { app, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2, activityRegistry, aiLimiter } = ctx;
38148
+ const { app, runOpenAIAgentChat: runOpenAIAgentChat2, activityRegistry, aiLimiter } = ctx;
38412
38149
  app.get("/api/ping", (_req, res) => {
38413
38150
  res.json({ ok: true, ts: Date.now() });
38414
38151
  });
@@ -38627,13 +38364,10 @@ function registerOsRoutes(ctx) {
38627
38364
  }
38628
38365
  let callerRole;
38629
38366
  let userId;
38630
- const token = getCookieToken(req);
38631
- if (token) {
38632
- const session = getValidSession(token);
38633
- if (session) {
38634
- callerRole = session.role;
38635
- userId = session.userId;
38636
- }
38367
+ const session = req.session || (getCookieToken(req) ? getValidSession(getCookieToken(req)) : null);
38368
+ if (session) {
38369
+ callerRole = session.role;
38370
+ userId = session.userId || session.username;
38637
38371
  }
38638
38372
  const convKey = `agent:${userId || "anonymous"}:${currentLessonId || "global"}`;
38639
38373
  const MEMORY_TURNS = 20;
@@ -38643,19 +38377,17 @@ function registerOsRoutes(ctx) {
38643
38377
  content: r.content
38644
38378
  }));
38645
38379
  let provider;
38646
- if (providerId) {
38647
- provider = kernelContainer.db.prepare("SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE id = ?").get(providerId);
38648
- } else {
38649
- provider = kernelContainer.db.prepare(
38650
- "SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' LIMIT 1"
38651
- ).get();
38652
- }
38653
- if (provider?.api_key) provider.api_key = decryptApiKey(provider.api_key);
38654
- if (!provider || !provider.api_key || !provider.api_key.trim()) {
38655
- return res.status(400).json({
38380
+ try {
38381
+ provider = resolveAccessibleAiProvider(kernelContainer.db, {
38382
+ providerId,
38383
+ userId,
38384
+ lang
38385
+ });
38386
+ } catch (err) {
38387
+ return res.status(err.status || 400).json({
38656
38388
  success: false,
38657
- error: "NO_AI_PROVIDER",
38658
- message: lang === "zh" ? "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002" : 'No active AI Provider configured. Please add and configure an AI Provider in "System Management -> AI Provider Management".'
38389
+ error: err.code || "NO_AI_PROVIDER",
38390
+ message: err.message
38659
38391
  });
38660
38392
  }
38661
38393
  const actorId = getActorId(req);
@@ -42119,7 +41851,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
42119
41851
  * 的按学科硬编码字符串拼接,与课题内容无关。此方法用真实的 LLM 调用取代它。
42120
41852
  */
42121
41853
  async generateOutlineWithAi(params) {
42122
- const { title, category, description, timeline, lang = "zh" } = params;
41854
+ const { title, category, description, timeline, lang = "zh", providerId, userId } = params;
42123
41855
  const trimmedTitle = (title || "").trim();
42124
41856
  if (!trimmedTitle) {
42125
41857
  const err = new Error(lang === "zh" ? "\u8BFE\u7A0B\u8BFE\u9898\u4E0D\u80FD\u4E3A\u7A7A" : "Course title is required");
@@ -42159,7 +41891,12 @@ ${timelineText}` : "- \u8BFE\u5802\u65F6\u95F4\u8F74\u73AF\u8282\uFF1A\uFF08\u65
42159
41891
  - \`## \u{1F4E5} \u8BFE\u540E\u4F5C\u4E1A\`\uFF1A\u4E00\u4EFD\u660E\u786E\u7684\u8BFE\u540E\u4EFB\u52A1\u4E0E\u63D0\u4EA4\u8981\u6C42\u3002
42160
41892
  4. \u5185\u5BB9\u5FC5\u987B\u7D27\u6263\u8BFE\u9898\u672C\u8EAB\uFF0C\u7981\u6B62\u4F7F\u7528"\u516C\u5F0F A""\u6A21\u5757 1"\u4E4B\u7C7B\u7684\u5360\u4F4D\u7B26\u6216\u4E0E\u8BFE\u9898\u65E0\u5173\u7684\u901A\u7528\u5957\u8BDD\u3002
42161
41893
  5. \u7BC7\u5E45\u63A7\u5236\u5728 600-1000 \u5B57\uFF0C\u8BED\u8A00${lang === "zh" ? "\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587" : "use English"}\u3002`;
42162
- const raw = await this.aiService.generateText(prompt, { temperature: 0.6, maxTokens: 2048 });
41894
+ const raw = await this.aiService.generateText(prompt, {
41895
+ temperature: 0.6,
41896
+ maxTokens: 2048,
41897
+ userId,
41898
+ providerId
41899
+ });
42163
41900
  const outline = stripReasoningArtifacts(stripOuterCodeFence(String(raw || "").trim()));
42164
41901
  if (!outline) {
42165
41902
  const err = new Error(
@@ -42423,13 +42160,17 @@ function registerLessonsRoutes(ctx) {
42423
42160
  safeAiLimiter,
42424
42161
  async (req, res) => {
42425
42162
  try {
42426
- const { title, category, description, timeline, lang } = req.body || {};
42163
+ const { title, category, description, timeline, lang, providerId } = req.body || {};
42164
+ const session = req.session;
42165
+ const userId = session?.userId || session?.username;
42427
42166
  const content = await lessonService.generateOutlineWithAi({
42428
42167
  title,
42429
42168
  category,
42430
42169
  description,
42431
42170
  timeline,
42432
- lang
42171
+ lang,
42172
+ providerId,
42173
+ userId
42433
42174
  });
42434
42175
  res.json({ success: true, content });
42435
42176
  } catch (e) {
@@ -48515,7 +48256,7 @@ var ScheduleService = class {
48515
48256
  * Timetable OCR 课表图像识别与数据清洗
48516
48257
  */
48517
48258
  async processTimetableOcr(imageBase64, options = {}) {
48518
- const { lang = "zh", providerId } = options;
48259
+ const { lang = "zh", providerId, userId } = options;
48519
48260
  if (!imageBase64) {
48520
48261
  throw new Error("imageBase64 is required");
48521
48262
  }
@@ -48546,21 +48287,11 @@ var ScheduleService = class {
48546
48287
 
48547
48288
  \u8FD4\u56DE\u683C\u5F0F\u793A\u4F8B\uFF1A
48548
48289
  [{"dayOfWeek":1,"periodNumber":1,"className":"\u9AD8\u4E00(13)","subject":"\u4FE1\u606F","timeSlot":"08:00-08:40","location":"312","teacherName":""}]`;
48549
- let provider;
48550
- if (providerId) {
48551
- provider = this.db.prepare("SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE id = ?").get(providerId);
48552
- }
48553
- if (!provider) {
48554
- provider = this.db.prepare(
48555
- "SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' ORDER BY created_at DESC LIMIT 1"
48556
- ).get();
48557
- }
48558
- if (provider?.api_key) provider.api_key = decryptApiKey(provider.api_key);
48559
- if (!provider || !provider.api_key || !provider.api_key.trim()) {
48560
- throw new Error(
48561
- lang === "zh" ? "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002" : 'No AI provider configured. Please add and configure an AI Provider in "System Management -> AI Provider Management".'
48562
- );
48563
- }
48290
+ const provider = resolveAccessibleAiProvider(this.db, {
48291
+ providerId,
48292
+ userId,
48293
+ lang
48294
+ });
48564
48295
  let chatUrl = provider.api_url.trim();
48565
48296
  if (!chatUrl.endsWith("/chat/completions")) {
48566
48297
  chatUrl = chatUrl.endsWith("/") ? chatUrl + "chat/completions" : chatUrl + "/chat/completions";
@@ -48776,7 +48507,9 @@ function registerSchedulesRoutes(ctx) {
48776
48507
  const startTime = Date.now();
48777
48508
  try {
48778
48509
  const { imageBase64, lang = "zh", providerId } = req.body;
48779
- const result = await scheduleService.processTimetableOcr(imageBase64, { lang, providerId });
48510
+ const session = req.session;
48511
+ const userId = session?.userId || session?.username;
48512
+ const result = await scheduleService.processTimetableOcr(imageBase64, { lang, providerId, userId });
48780
48513
  res.json({
48781
48514
  success: true,
48782
48515
  entries: result.entries,
@@ -49162,7 +48895,7 @@ var GradingService = class {
49162
48895
  }
49163
48896
  // ── 6. AI 学期综合总评评语生成 ────────────────────────────────────────────────
49164
48897
  async generateSemesterAiEvaluation(params) {
49165
- const { classId, studentId, semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId } = params;
48898
+ const { classId, studentId, semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId, userId } = params;
49166
48899
  const student = this.db.prepare("SELECT name FROM students WHERE id = ?").get(studentId);
49167
48900
  if (!student) {
49168
48901
  throw new Error("Student not found");
@@ -49220,13 +48953,10 @@ ${examsText}
49220
48953
  - \u6307\u51FA\u5176\u53EF\u4EE5\u6539\u8FDB\u7684\u5730\u65B9\uFF08\u5982\u8FDB\u5EA6\u843D\u540E\u3001\u8003\u8BD5\u53D1\u6325\u4E0D\u4F73\u7B49\uFF09\uFF0C\u8BED\u6C14\u8981\u975E\u5E38\u6E29\u67D4\u3001\u59D4\u5A49\uFF0C\u7ED9\u4E88\u5176\u4FE1\u5FC3\u3002
49221
48954
  - \u5BF9\u672A\u6765\u7684\u671F\u8BB8\uFF0C\u6FC0\u52B1\u5B66\u751F\u5728\u4E0B\u5B66\u671F\u7EE7\u7EED\u52AA\u529B\u3002
49222
48955
  3. \u957F\u5EA6\u63A7\u5236\u5728 150-250 \u5B57\u4E4B\u95F4\u3002\u4E0D\u8981\u5305\u542B\u4EFB\u4F55 Markdown \u683C\u5F0F\uFF0C\u53EA\u8FD4\u56DE\u7EAF\u6587\u672C\u8BC4\u8BED\u3002`;
49223
- const provider = providerId ? this.db.prepare("SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE id = ?").get(providerId) : this.db.prepare(
49224
- "SELECT id, name, api_url, api_key, model_name FROM ai_providers WHERE api_key IS NOT NULL AND api_key != '' LIMIT 1"
49225
- ).get();
49226
- if (provider?.api_key) provider.api_key = decryptApiKey(provider.api_key);
49227
- if (!provider || !provider.api_key || !provider.api_key.trim()) {
49228
- throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002");
49229
- }
48956
+ const provider = resolveAccessibleAiProvider(this.db, {
48957
+ providerId,
48958
+ userId
48959
+ });
49230
48960
  let chatUrl = provider.api_url.trim();
49231
48961
  if (!chatUrl.endsWith("/chat/completions")) {
49232
48962
  chatUrl = chatUrl.endsWith("/") ? chatUrl + "chat/completions" : chatUrl + "/chat/completions";
@@ -49499,11 +49229,14 @@ function registerGradingRoutes(ctx) {
49499
49229
  const { classId, studentId } = req.params;
49500
49230
  if (gate(req, res, classId, "staff")) return;
49501
49231
  const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\u671F", providerId } = req.body;
49232
+ const session = req.session;
49233
+ const userId = session?.userId || session?.username;
49502
49234
  const aiEvaluation = await gradingService.generateSemesterAiEvaluation({
49503
49235
  classId,
49504
49236
  studentId,
49505
49237
  semesterName,
49506
- providerId
49238
+ providerId,
49239
+ userId
49507
49240
  });
49508
49241
  res.json({ success: true, aiEvaluation });
49509
49242
  } catch (e) {
@@ -49931,7 +49664,8 @@ async function pluginApiGatewayMiddleware(req, res, next) {
49931
49664
  }
49932
49665
  }
49933
49666
  const clientIp = req.ip || req.socket?.remoteAddress || "unknown-ip";
49934
- const rateLimitKey = `${clientIp}:${resolvedUuid || pluginId}:${method}:${normalizedSubPath}`;
49667
+ const identityKey = auth.isAuthenticated ? `u:${String(auth.userId || auth.username || auth.actorId).slice(0, 64)}` : `ip:${clientIp}`;
49668
+ const rateLimitKey = `${identityKey}:${resolvedUuid || pluginId}:${method}:${normalizedSubPath}`;
49935
49669
  const windowMs = matchedRule?.rateLimit?.windowMs ?? 60 * 1e3;
49936
49670
  const maxRequests = matchedRule?.rateLimit?.max ?? 120;
49937
49671
  if (!rateLimiter.consume(rateLimitKey, maxRequests, windowMs)) {
@@ -50920,16 +50654,9 @@ function listInstalledManifestVersions() {
50920
50654
  return installed;
50921
50655
  }
50922
50656
  function registerPluginsRoutes(ctx) {
50923
- const {
50924
- app,
50925
- aiLimiter,
50926
- buildOpenAITools: buildOpenAITools2,
50927
- executeAgentToolCall: executeAgentToolCall2,
50928
- buildOpenAIChatUrl: buildOpenAIChatUrl2,
50929
- runGeminiAgentChat: runGeminiAgentChat2,
50930
- runOpenAIAgentChat: runOpenAIAgentChat2
50931
- } = ctx;
50932
- const safeAiLimiter = typeof aiLimiter === "function" ? aiLimiter : (_req, _res, next) => next();
50657
+ const { app, aiLimiter } = ctx;
50658
+ const noopMiddleware = (_req, _res, next) => next();
50659
+ const safeAiLimiter = typeof aiLimiter === "function" ? aiLimiter : noopMiddleware;
50933
50660
  app.get("/api/docs/plugin-guide", (req, res) => {
50934
50661
  try {
50935
50662
  const docPath = import_path22.default.join(process.cwd(), "docs_plugin_guide.md");
@@ -51965,9 +51692,13 @@ function registerPluginsRoutes(ctx) {
51965
51692
  }
51966
51693
  });
51967
51694
  app.all("/api/plugins/:pluginId/*", pluginApiGatewayMiddleware);
51968
- app.get("/api/ai-providers", requireAuth("teacher", "administrator"), (req, res) => {
51695
+ app.get("/api/ai-providers", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
51969
51696
  try {
51970
- const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
51697
+ const session = req.session;
51698
+ const currentUserId = session?.userId || session?.username;
51699
+ const providers = kernelContainer.db.prepare(
51700
+ "SELECT id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at FROM ai_providers WHERE (owner_id = ? OR is_shared = 1) ORDER BY created_at DESC"
51701
+ ).all(currentUserId);
51971
51702
  const masked = providers.map((p) => {
51972
51703
  let apiKey;
51973
51704
  try {
@@ -51975,54 +51706,122 @@ function registerPluginsRoutes(ctx) {
51975
51706
  } catch {
51976
51707
  apiKey = "[UNDECRYPTABLE]";
51977
51708
  }
51978
- return { ...p, api_key: apiKey };
51709
+ return {
51710
+ ...p,
51711
+ api_key: apiKey,
51712
+ is_shared: Number(p.is_shared) === 1
51713
+ };
51979
51714
  });
51980
51715
  res.json(masked);
51981
51716
  } catch (e) {
51982
51717
  sendSafeError(res, e);
51983
51718
  }
51984
51719
  });
51985
- app.post("/api/ai-providers", requireAuth("administrator"), (req, res) => {
51720
+ app.post("/api/ai-providers", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
51986
51721
  try {
51987
- const { name, api_url, api_key, model_name } = req.body;
51722
+ const session = req.session;
51723
+ const currentUserId = session?.userId || session?.username;
51724
+ const isAdmin = isAdministrator2(session);
51725
+ const { name, api_url, api_key, model_name, is_shared } = req.body;
51988
51726
  if (!name || !api_url || !model_name) {
51989
51727
  return res.status(400).json({ error: "Missing name, api_url or model_name" });
51990
51728
  }
51729
+ const wantsShared = is_shared === 1 || is_shared === true || is_shared === "1";
51730
+ if (wantsShared && !isAdmin) {
51731
+ return res.status(403).json({ error: "Only administrators can create platform shared providers" });
51732
+ }
51991
51733
  const id = "prov_" + Date.now();
51992
51734
  const now = Date.now();
51993
51735
  const encryptedKey = api_key ? encryptApiKey(api_key) : "";
51994
51736
  kernelContainer.db.prepare(
51995
- "INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
51996
- ).run(id, name, api_url, encryptedKey, model_name, now, now);
51737
+ "INSERT INTO ai_providers (id, name, api_url, api_key, model_name, owner_id, is_shared, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
51738
+ ).run(
51739
+ id,
51740
+ name.trim(),
51741
+ api_url.trim(),
51742
+ encryptedKey,
51743
+ model_name.trim(),
51744
+ currentUserId,
51745
+ wantsShared && isAdmin ? 1 : 0,
51746
+ now,
51747
+ now
51748
+ );
51997
51749
  res.json({ success: true, id });
51998
51750
  } catch (e) {
51999
51751
  sendSafeError(res, e);
52000
51752
  }
52001
51753
  });
52002
- app.put("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
51754
+ app.put("/api/ai-providers/:id", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
52003
51755
  try {
52004
- const { name, api_url, api_key, model_name } = req.body;
51756
+ const session = req.session;
51757
+ const currentUserId = session?.userId || session?.username;
51758
+ const isAdmin = isAdministrator2(session);
51759
+ const { name, api_url, api_key, model_name, is_shared } = req.body;
52005
51760
  if (!name || !api_url || !model_name) {
52006
51761
  return res.status(400).json({ error: "Missing name, api_url or model_name" });
52007
51762
  }
51763
+ const existing = kernelContainer.db.prepare("SELECT id, api_key, owner_id, is_shared FROM ai_providers WHERE id = ?").get(req.params.id);
51764
+ if (!existing) {
51765
+ return res.status(404).json({ error: "AI provider not found" });
51766
+ }
51767
+ const isExistingShared = Number(existing.is_shared) === 1;
51768
+ if (isExistingShared) {
51769
+ if (!isAdmin) {
51770
+ return res.status(403).json({ error: "Forbidden: Only administrators can modify platform shared providers" });
51771
+ }
51772
+ } else {
51773
+ if (existing.owner_id !== currentUserId) {
51774
+ return res.status(403).json({
51775
+ error: "Forbidden: You do not have permission to modify another user's private AI provider"
51776
+ });
51777
+ }
51778
+ }
51779
+ let nextShared = existing.is_shared ?? 0;
51780
+ if (is_shared !== void 0) {
51781
+ const wantsShared = is_shared === 1 || is_shared === true || is_shared === "1";
51782
+ if (wantsShared !== (Number(existing.is_shared) === 1)) {
51783
+ if (!isAdmin) {
51784
+ return res.status(403).json({ error: "Forbidden: Only administrators can change platform shared status" });
51785
+ }
51786
+ nextShared = wantsShared ? 1 : 0;
51787
+ }
51788
+ }
52008
51789
  const now = Date.now();
52009
51790
  let finalKey;
52010
51791
  if (api_key && api_key.trim() !== "" && !api_key.includes("****")) {
52011
51792
  finalKey = encryptApiKey(api_key);
52012
51793
  } else {
52013
- const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ?").get(req.params.id);
52014
51794
  finalKey = existing?.api_key || "";
52015
51795
  }
52016
51796
  kernelContainer.db.prepare(
52017
- "UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?"
52018
- ).run(name, api_url, finalKey, model_name, now, req.params.id);
51797
+ "UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, is_shared = ?, updated_at = ? WHERE id = ?"
51798
+ ).run(name.trim(), api_url.trim(), finalKey, model_name.trim(), nextShared, now, req.params.id);
52019
51799
  res.json({ success: true });
52020
51800
  } catch (e) {
52021
51801
  sendSafeError(res, e);
52022
51802
  }
52023
51803
  });
52024
- app.delete("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
51804
+ app.delete("/api/ai-providers/:id", requireScopedAuth("self", "teacher", "administrator"), (req, res) => {
52025
51805
  try {
51806
+ const session = req.session;
51807
+ const currentUserId = session?.userId || session?.username;
51808
+ const isAdmin = isAdministrator2(session);
51809
+ const existing = kernelContainer.db.prepare("SELECT id, owner_id, is_shared FROM ai_providers WHERE id = ?").get(req.params.id);
51810
+ if (!existing) {
51811
+ return res.status(404).json({ error: "AI provider not found" });
51812
+ }
51813
+ const isExistingShared = Number(existing.is_shared) === 1;
51814
+ if (isExistingShared) {
51815
+ if (!isAdmin) {
51816
+ return res.status(403).json({ error: "Forbidden: Only administrators can delete platform shared providers" });
51817
+ }
51818
+ } else {
51819
+ if (existing.owner_id !== currentUserId) {
51820
+ return res.status(403).json({
51821
+ error: "Forbidden: You do not have permission to delete another user's private AI provider"
51822
+ });
51823
+ }
51824
+ }
52026
51825
  kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
52027
51826
  res.json({ success: true });
52028
51827
  } catch (e) {
@@ -52105,65 +51904,82 @@ function registerPluginsRoutes(ctx) {
52105
51904
  sendSafeError(res, e);
52106
51905
  }
52107
51906
  });
52108
- app.post("/api/ai-providers/test", requireAuth("administrator"), safeAiLimiter, async (req, res) => {
52109
- try {
52110
- const { api_url, api_key: providedKey, model_name } = req.body;
52111
- if (!api_url || !model_name) {
52112
- return res.status(400).json({ error: "api_url and model_name are required" });
52113
- }
52114
- let api_key = "";
52115
- if (providedKey && providedKey.includes("****")) {
52116
- const existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? LIMIT 1").get(api_url, model_name);
52117
- api_key = existing ? decryptApiKey(existing.api_key) : "";
52118
- } else if (providedKey) {
52119
- api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
52120
- }
52121
- let cleanUrl = api_url.trim();
52122
- if (!cleanUrl.endsWith("/chat/completions")) {
52123
- cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
52124
- }
52125
- const urlSafety = isSafeExternalUrl(cleanUrl);
52126
- if (!urlSafety.safe) {
52127
- return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
52128
- }
52129
- let response;
51907
+ app.post(
51908
+ "/api/ai-providers/test",
51909
+ requireScopedAuth("self", "teacher", "administrator"),
51910
+ safeAiLimiter,
51911
+ async (req, res) => {
52130
51912
  try {
52131
- response = await fetchWithSafeRedirects(cleanUrl, {
52132
- method: "POST",
52133
- headers: {
52134
- "Content-Type": "application/json",
52135
- Authorization: `Bearer ${api_key || ""}`
52136
- },
52137
- body: JSON.stringify({
52138
- model: model_name,
52139
- messages: [{ role: "user", content: "Say connected" }],
52140
- max_tokens: 5
52141
- }),
52142
- timeoutMs: 1e4
52143
- });
52144
- } catch (err) {
52145
- const isTimeout = err instanceof AIFetchTimeoutError || err?.name === "TimeoutError" || err?.name === "AbortError" || /abort|timeout/i.test(err?.message ?? "");
52146
- if (isTimeout) {
52147
- return res.status(504).json({
51913
+ const session = req.session;
51914
+ const currentUserId = session?.userId || session?.username;
51915
+ const { api_url, api_key: providedKey, model_name, id } = req.body;
51916
+ if (!api_url || !model_name) {
51917
+ return res.status(400).json({ error: "api_url and model_name are required" });
51918
+ }
51919
+ let api_key = "";
51920
+ if (providedKey && providedKey.includes("****")) {
51921
+ let existing;
51922
+ if (id) {
51923
+ existing = kernelContainer.db.prepare("SELECT api_key FROM ai_providers WHERE id = ? AND (owner_id = ? OR is_shared = 1)").get(id, currentUserId);
51924
+ } else {
51925
+ existing = kernelContainer.db.prepare(
51926
+ "SELECT api_key FROM ai_providers WHERE api_url = ? AND model_name = ? AND (owner_id = ? OR is_shared = 1) LIMIT 1"
51927
+ ).get(api_url, model_name, currentUserId);
51928
+ }
51929
+ if (!existing) {
51930
+ return res.status(403).json({ error: "Forbidden: Cannot access key for this provider" });
51931
+ }
51932
+ api_key = decryptApiKey(existing.api_key);
51933
+ } else if (providedKey) {
51934
+ api_key = providedKey.includes(":") ? decryptApiKey(providedKey) : providedKey;
51935
+ }
51936
+ let cleanUrl = api_url.trim();
51937
+ if (!cleanUrl.endsWith("/chat/completions")) {
51938
+ cleanUrl = cleanUrl.endsWith("/") ? cleanUrl + "chat/completions" : cleanUrl + "/chat/completions";
51939
+ }
51940
+ const urlSafety = isSafeExternalUrl(cleanUrl);
51941
+ if (!urlSafety.safe) {
51942
+ return res.status(400).json({ error: `Blocked unsafe AI provider URL: ${urlSafety.reason}` });
51943
+ }
51944
+ let response;
51945
+ try {
51946
+ response = await fetchWithSafeRedirects(cleanUrl, {
51947
+ method: "POST",
51948
+ headers: {
51949
+ "Content-Type": "application/json",
51950
+ Authorization: `Bearer ${api_key || ""}`
51951
+ },
51952
+ body: JSON.stringify({
51953
+ model: model_name,
51954
+ messages: [{ role: "user", content: "Say connected" }],
51955
+ max_tokens: 5
51956
+ }),
51957
+ timeoutMs: 1e4
51958
+ });
51959
+ } catch (err) {
51960
+ const isTimeout = err instanceof AIFetchTimeoutError || err?.name === "TimeoutError" || err?.name === "AbortError" || /abort|timeout/i.test(err?.message ?? "");
51961
+ if (isTimeout) {
51962
+ return res.status(504).json({
51963
+ success: false,
51964
+ error: "AI Provider request timed out after 10000ms. Please verify the URL or provider availability."
51965
+ });
51966
+ }
51967
+ throw err;
51968
+ }
51969
+ const responseText = await response.text();
51970
+ if (response.ok) {
51971
+ res.json({ success: true, message: "Successfully connected and received response." });
51972
+ } else {
51973
+ res.status(response.status).json({
52148
51974
  success: false,
52149
- error: "AI Provider request timed out after 10000ms. Please verify the URL or provider availability."
51975
+ error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}`
52150
51976
  });
52151
51977
  }
52152
- throw err;
52153
- }
52154
- const responseText = await response.text();
52155
- if (response.ok) {
52156
- res.json({ success: true, message: "Successfully connected and received response." });
52157
- } else {
52158
- res.status(response.status).json({
52159
- success: false,
52160
- error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}`
52161
- });
51978
+ } catch (e) {
51979
+ sendSafeError(res, e, 500, "Connection failed");
52162
51980
  }
52163
- } catch (e) {
52164
- sendSafeError(res, e, 500, "Connection failed");
52165
51981
  }
52166
- });
51982
+ );
52167
51983
  }
52168
51984
 
52169
51985
  // server/services/assessment-service.ts
@@ -58608,12 +58424,16 @@ async function startServer() {
58608
58424
  const defaultWriteLimit = process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 180;
58609
58425
  const writeLimiter = createWriteLimiter(defaultWriteLimit);
58610
58426
  const viewStateLimiter = createWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : isDev ? 600 : 120);
58427
+ const pluginApiLimiter = createSessionWriteLimiter(process.env.PLAYWRIGHT_TEST ? 1e4 : 600);
58611
58428
  const aiLimiter = createAiLimiter(process.env.PLAYWRIGHT_TEST ? 1e3 : 10);
58612
58429
  app.use((req, res, next) => {
58613
58430
  if (req.method === "POST" || req.method === "PUT" || req.method === "DELETE") {
58614
58431
  if (/^\/api\/classroom\/sessions\/[^/]+\/view-state\/?$/.test(req.path)) {
58615
58432
  return viewStateLimiter(req, res, next);
58616
58433
  }
58434
+ if (req.path.startsWith("/api/plugins/")) {
58435
+ return pluginApiLimiter(req, res, next);
58436
+ }
58617
58437
  return writeLimiter(req, res, next);
58618
58438
  }
58619
58439
  next();