openlearn-next 0.7.9 → 0.7.10

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 (41) hide show
  1. package/README.md +4 -2
  2. package/cli-data.mjs +1 -1
  3. package/dist/assets/{ActivityWorkspaceWidget-DbspLRxB.js → ActivityWorkspaceWidget-BU88GGUG.js} +1 -1
  4. package/dist/assets/{AdminDirectoryView-C2ZFIwHx.js → AdminDirectoryView-Cdsk9Mdr.js} +1 -1
  5. package/dist/assets/{AssessmentCenter-DbJAcTvr.js → AssessmentCenter-BoSWbJMt.js} +1 -1
  6. package/dist/assets/{AssignmentFilePreview-DEVTb4YJ.js → AssignmentFilePreview-D_POvLmv.js} +1 -1
  7. package/dist/assets/{BatchPickerModal-czw6eA6e.js → BatchPickerModal-DiHd_6Tg.js} +1 -1
  8. package/dist/assets/{ClassesView-B1F0tTtB.js → ClassesView-CWKqzWH0.js} +1 -1
  9. package/dist/assets/{ClassroomCountdownWidget-C9pv3GLy.js → ClassroomCountdownWidget-B5-KUVU8.js} +1 -1
  10. package/dist/assets/{CloudDriveModal-goDB6dlC.js → CloudDriveModal-CjcvdDiw.js} +1 -1
  11. package/dist/assets/{ComputerLabView-Csqiqvw2.js → ComputerLabView-CNTcmWAh.js} +1 -1
  12. package/dist/assets/{CourseManagement-BD-4z2OZ.js → CourseManagement-ha_iSnLU.js} +1 -1
  13. package/dist/assets/{CourseWizardModal-NXwhuph_.js → CourseWizardModal-oVT9mYSG.js} +1 -1
  14. package/dist/assets/{Dashboard-CFDWNYSw.js → Dashboard-Vb9coh9_.js} +1 -1
  15. package/dist/assets/{ExportWeightModal-dBxYVUP4.js → ExportWeightModal-BEesz6rd.js} +1 -1
  16. package/dist/assets/{HelpView-DSZC-1F0.js → HelpView--Um9DFYx.js} +1 -1
  17. package/dist/assets/{ImportLessonsModal-m_s44eus.js → ImportLessonsModal-DXGopj4j.js} +1 -1
  18. package/dist/assets/{InteractiveCoursewareViewer-C7faFOFD.js → InteractiveCoursewareViewer-DzgF9_LU.js} +1 -1
  19. package/dist/assets/{InteractiveWhiteboard-10fno-bJ.js → InteractiveWhiteboard-DJqgP_tU.js} +7 -7
  20. package/dist/assets/{LazyCourseware-DNn0O8lA.js → LazyCourseware-6yb_QiYS.js} +2 -2
  21. package/dist/assets/{LazyWhiteboard-BxpydfnE.js → LazyWhiteboard-CsF9GB6J.js} +2 -2
  22. package/dist/assets/{LessonEditorView-CMCgnrJD.js → LessonEditorView-CZTZ7Q0B.js} +1 -1
  23. package/dist/assets/{LiveClassroomView-BA0us3bY.js → LiveClassroomView-D2ugupT9.js} +1 -1
  24. package/dist/assets/{NotificationDetailModal-BMrt09PY.js → NotificationDetailModal-mUeIQP4R.js} +1 -1
  25. package/dist/assets/{PluginView-B6gCF1ZB.js → PluginView-CT3-zS2n.js} +1 -1
  26. package/dist/assets/{QuizGeneratorModal-BNW6tZ78.js → QuizGeneratorModal-ViKxNX5Q.js} +1 -1
  27. package/dist/assets/{StudentAssignmentEvalPanel-DMwBSOXf.js → StudentAssignmentEvalPanel-BRxHMABY.js} +1 -1
  28. package/dist/assets/{StudentAssignmentView-cQpPme4_.js → StudentAssignmentView-g-ewJIS3.js} +1 -1
  29. package/dist/assets/{StudentLessonView-CpGgYV-b.js → StudentLessonView-Dbvr3Xlo.js} +1 -1
  30. package/dist/assets/{StudentView-BfXi6-NQ.js → StudentView-20vfEk4v.js} +2 -2
  31. package/dist/assets/{SystemResourceLibraryModal-d4vfqM_9.js → SystemResourceLibraryModal-CDKeaKf8.js} +1 -1
  32. package/dist/assets/{TeacherView-DSJCvFE-.js → TeacherView-KIKuhXGF.js} +2 -2
  33. package/dist/assets/{TimetableView-D9Rhnvfj.js → TimetableView-7V2AwWml.js} +1 -1
  34. package/dist/assets/{index-DCnGJbif.css → index-C4cHQZyx.css} +1 -1
  35. package/dist/assets/{index-DuxQtTr9.js → index-XduPa2-x.js} +31 -31
  36. package/dist/assets/{pluginParsers-HRw6CVog.js → pluginParsers-BO6U1E-h.js} +1 -1
  37. package/dist/assets/{student-workspace-DO1mSl_I.js → student-workspace-DstiW0pM.js} +1 -1
  38. package/dist/env-validation.cjs +14 -6
  39. package/dist/index.html +2 -2
  40. package/dist/server.cjs +681 -346
  41. package/package.json +2 -2
package/dist/server.cjs CHANGED
@@ -507,6 +507,74 @@ var init_plugin_metrics = __esm({
507
507
  }
508
508
  });
509
509
 
510
+ // packages/core/capability/catalog.ts
511
+ function validateManifestCapabilities(capabilities) {
512
+ const unknown = [];
513
+ for (const cap of capabilities) {
514
+ if (typeof cap !== "string" || !KNOWN_CAPABILITIES.has(cap)) {
515
+ unknown.push(typeof cap === "string" ? cap : String(cap));
516
+ }
517
+ }
518
+ return { valid: unknown.length === 0, unknown };
519
+ }
520
+ var CAP_API_WRITE, CAP_API_ADMIN, CAP_GRADES_WRITE, CAP_AGENT_TOOL, CAP_AI_INVOKE, CAP_TASK_REGISTER, CAP_STUDENT_DATA_READ, CAP_STUDENT_DATA_WRITE, CAP, CAPABILITIES, KNOWN_CAPABILITIES;
521
+ var init_catalog = __esm({
522
+ "packages/core/capability/catalog.ts"() {
523
+ "use strict";
524
+ CAP_API_WRITE = "api:write";
525
+ CAP_API_ADMIN = "api:admin";
526
+ CAP_GRADES_WRITE = "grades:write";
527
+ CAP_AGENT_TOOL = "agent:tool";
528
+ CAP_AI_INVOKE = "ai:invoke";
529
+ CAP_TASK_REGISTER = "task:register";
530
+ CAP_STUDENT_DATA_READ = "student-data:read";
531
+ CAP_STUDENT_DATA_WRITE = "student-data:write";
532
+ CAP = {
533
+ // ── 资源型(ActionDescriptor.capabilityRequired)──────────────────────
534
+ ASSIGNMENT_MANAGE: "assignment:manage",
535
+ ASSIGNMENT_READ: "assignment:read",
536
+ ASSIGNMENT_REVIEW: "assignment:review",
537
+ ASSIGNMENT_SUBMIT: "assignment:submit",
538
+ ASSIGNMENT_WRITE: "assignment:write",
539
+ LESSON_DELETE: "lesson:delete",
540
+ LESSON_READ: "lesson:read",
541
+ LESSON_WRITE: "lesson:write",
542
+ MANAGEMENT_READ: "management:read",
543
+ MANAGEMENT_WRITE: "management:write",
544
+ PLUGIN_READ: "plugin:read",
545
+ PLUGIN_WRITE: "plugin:write",
546
+ PROCESS_READ: "process:read",
547
+ PROCESS_WRITE: "process:write",
548
+ STUDENT_WRITE: "student:write",
549
+ VFS_READ: "vfs:read",
550
+ VFS_WRITE: "vfs:write",
551
+ WHITEBOARD_READ: "whiteboard:read",
552
+ WHITEBOARD_WRITE: "whiteboard:write",
553
+ // ── 历史上只出现在 capabilitiesProposed、从未作为 capabilityRequired ──
554
+ // 保留是为了不让内置 management 插件在升级后突然被标成「未知串」。
555
+ CLASS_READ: "class:read",
556
+ CLASS_WRITE: "class:write",
557
+ STUDENT_READ: "student:read",
558
+ // ── 方法级(method-policy / 服务代理的 Security Barrier 3)───────────
559
+ API_WRITE: CAP_API_WRITE,
560
+ API_ADMIN: CAP_API_ADMIN,
561
+ GRADES_WRITE: CAP_GRADES_WRITE,
562
+ AGENT_TOOL: CAP_AGENT_TOOL,
563
+ AI_INVOKE: CAP_AI_INVOKE,
564
+ TASK_REGISTER: CAP_TASK_REGISTER,
565
+ STUDENT_DATA_READ: CAP_STUDENT_DATA_READ,
566
+ STUDENT_DATA_WRITE: CAP_STUDENT_DATA_WRITE,
567
+ // ── 平台内部 ─────────────────────────────────────────────────────────
568
+ // 片段命令(segment gate)用,插件片段可声明;不在 AI 工具箱里。
569
+ SEGMENT_GATE_EVAL: "segment:gate_eval"
570
+ };
571
+ CAPABILITIES = Object.freeze(
572
+ Array.from(new Set(Object.values(CAP)))
573
+ );
574
+ KNOWN_CAPABILITIES = new Set(CAPABILITIES);
575
+ }
576
+ });
577
+
510
578
  // packages/core/esm-loader/manifest-schema.ts
511
579
  var import_zod, classroomToolSchema, helpDocSchema, quickActivitySchema, timelineSegmentSchema, paletteItemSchema, cockpitWidgetSchema, stageCardSchema, classroomTopbarActionSchema, classroomAttributionAwardSchema, studentCompetencyDimensionSchema, studentProfileWidgetSchema, canvasWidgetSchema, barometerMetricSchema, peerReviewRubricSchema, peerReviewBadgeSchema, preclassPasscodeActionSchema, preclassPasscodeAddonSchema, commandPaletteItemSchema, quickInsertItemSchema, aiPromptTemplateSchema, aiSkillProviderSchema, classTabSchema, classBatchActionSchema, studentRowPanelSchema, editorHeaderActionSchema, postClassWidgetSchema, coursewareViewerToolbarSchema, coursewareViewerOverlaySchema, portalTelemetrySchema, portalCourseBadgeSchema, portalTeachingModeSchema, portalInsightSchema, portalPreflightSchema, portalLaunchActionSchema, countdownWidgetSchema, countdownActionSchema, studentCountdownSchema, studentQuickActionsItemSchema, studentQuickActionsActionSchema, studentQuickActionsFabSchema, notificationTabsSchema, pacingDashboardSchema, diagnosticFeedSchema, collabCanvasSchema, topbarPillSchema, attributionActionSchema, leaderboardActionSchema, studentProfileActionSchema, studentProfileTimelineItemSchema, classroomHeaderActionSchema, classroomAgendaActionSchema, whiteboardDockPluginSchema, classroomAuditEventSchema, peerReviewActionSchema, peerReviewShowcaseWidgetSchema, seatingToolbarSchema, seatingLegendSchema, seatingSeatBadgeSchema, seatingSeatActionsSchema, seatingSummarySchema, whiteboardAutosaveStatusSchema, whiteboardAutosaveActionSchema, teacherPanelSchema, studentFullscreenSchema, globalSettingSchema, navUserMenuSchema, frontendSlotEntrySchema, frontendSlotsShape, pluginLocalesSchema, contributesSchema, deploySchema, apiRouteSchema, apiSchema, requiresItemSchema, manifestSchema, requiresItemV3Schema, manifestSchemaV3;
512
580
  var init_manifest_schema = __esm({
@@ -1800,7 +1868,7 @@ var init_token = __esm({
1800
1868
  });
1801
1869
 
1802
1870
  // packages/core/di/interfaces.ts
1803
- var ICommandBusServiceToken4, IEventBusServiceToken3, IActionRegistryServiceToken4, ICapabilityServiceToken, IProcessServiceToken2, IStorageServiceToken, IAIServiceToken, IDatabaseToken4, IPluginHostToken, IPluginLifecycleManagerToken, IPluginDistributionManagerToken, IPluginRuntimeCompositionToken, IUnifiedExtensionRegistryToken, IPluginCapabilityGatewayToken, ICapabilityRegistryToken, IAuthSessionBridgeToken, ISemesterGradeServiceToken, IPointsDimensionRegistryToken, IPointsLedgerServiceToken, ILessonEngineServiceToken, IClassroomRuntimeServiceToken, IPresenceEngineServiceToken, ITeachingCollaborationServiceToken, ILearningAnalyticsServiceToken, IAICapabilityServiceToken, ICapabilityRuntimeServiceToken, ICapabilityGovernanceServiceToken, IPlatformServiceRegistryToken, ICoursewareRuntimeScriptRegistryToken, IClassroomLifecycleServiceToken, IInteractionRuntimeServiceToken, IClassroomCountdownServiceToken, IAssessmentGraderRegistryToken, IStageGuardServiceToken, PLUGIN_RESOLVABLE_TOKENS, PLUGIN_RESOLVABLE_TOKEN_NAMES, INetworkProxyServiceToken;
1871
+ var ICommandBusServiceToken4, IEventBusServiceToken3, IActionRegistryServiceToken4, ICapabilityServiceToken, IProcessServiceToken2, IStorageServiceToken, IAIServiceToken, IDatabaseToken4, IPluginHostToken, IPluginLifecycleManagerToken, IPluginDistributionManagerToken, IPluginRuntimeCompositionToken, IUnifiedExtensionRegistryToken, IPluginCapabilityGatewayToken, ICapabilityRegistryToken, IAuthSessionBridgeToken, ISemesterGradeServiceToken, IPointsDimensionRegistryToken, IPointsLedgerServiceToken, IClassRosterServiceToken, ILessonEngineServiceToken, IClassroomRuntimeServiceToken, IPresenceEngineServiceToken, ITeachingCollaborationServiceToken, ILearningAnalyticsServiceToken, IAICapabilityServiceToken, ICapabilityRuntimeServiceToken, ICapabilityGovernanceServiceToken, IPlatformServiceRegistryToken, ICoursewareRuntimeScriptRegistryToken, IClassroomLifecycleServiceToken, IInteractionRuntimeServiceToken, IClassroomCountdownServiceToken, IAssessmentGraderRegistryToken, IStageGuardServiceToken, PLUGIN_RESOLVABLE_TOKENS, PLUGIN_RESOLVABLE_TOKEN_NAMES, INetworkProxyServiceToken;
1804
1872
  var init_interfaces = __esm({
1805
1873
  "packages/core/di/interfaces.ts"() {
1806
1874
  "use strict";
@@ -1839,6 +1907,7 @@ var init_interfaces = __esm({
1839
1907
  "@openlearn/core:IPointsDimensionRegistry"
1840
1908
  );
1841
1909
  IPointsLedgerServiceToken = new Token("@openlearn/core:IPointsLedgerService");
1910
+ IClassRosterServiceToken = new Token("@openlearn/core:IClassRosterService");
1842
1911
  ILessonEngineServiceToken = new Token("@openlearn/core:ILessonEngineService");
1843
1912
  IClassroomRuntimeServiceToken = new Token(
1844
1913
  "@openlearn/core:IClassroomRuntimeService"
@@ -1889,6 +1958,7 @@ var init_interfaces = __esm({
1889
1958
  ISemesterGradeServiceToken,
1890
1959
  IPointsDimensionRegistryToken,
1891
1960
  IPointsLedgerServiceToken,
1961
+ IClassRosterServiceToken,
1892
1962
  ILessonEngineServiceToken,
1893
1963
  IClassroomRuntimeServiceToken,
1894
1964
  IPresenceEngineServiceToken,
@@ -4209,18 +4279,11 @@ function checkMethodPolicy(input) {
4209
4279
  if (tokens.uiService && token === tokens.uiService) return null;
4210
4280
  return null;
4211
4281
  }
4212
- var CAP_API_WRITE, CAP_API_ADMIN, CAP_GRADES_WRITE, CAP_AGENT_TOOL, CAP_AI_INVOKE, CAP_TASK_REGISTER, CAP_STUDENT_DATA_READ, CAP_STUDENT_DATA_WRITE, HIGH_RISK_PATH_PREFIXES, IRREVERSIBLE_PATH_SUFFIXES, ENTITY_DELETE_RE;
4282
+ var HIGH_RISK_PATH_PREFIXES, IRREVERSIBLE_PATH_SUFFIXES, ENTITY_DELETE_RE;
4213
4283
  var init_method_policy = __esm({
4214
4284
  "packages/core/plugin-host/method-policy.ts"() {
4215
4285
  "use strict";
4216
- CAP_API_WRITE = "api:write";
4217
- CAP_API_ADMIN = "api:admin";
4218
- CAP_GRADES_WRITE = "grades:write";
4219
- CAP_AGENT_TOOL = "agent:tool";
4220
- CAP_AI_INVOKE = "ai:invoke";
4221
- CAP_TASK_REGISTER = "task:register";
4222
- CAP_STUDENT_DATA_READ = "student-data:read";
4223
- CAP_STUDENT_DATA_WRITE = "student-data:write";
4286
+ init_catalog();
4224
4287
  HIGH_RISK_PATH_PREFIXES = Object.freeze([
4225
4288
  "/api/plugins",
4226
4289
  // 插件增删改、一键更新(可执行任意第三方代码)
@@ -4244,51 +4307,238 @@ var init_method_policy = __esm({
4244
4307
  }
4245
4308
  });
4246
4309
 
4247
- // packages/core/command-bus/plugin-command.ts
4248
- function bindPluginCommand(command, pluginActorId) {
4249
- const actor = typeof pluginActorId === "string" ? pluginActorId.trim() : "";
4250
- if (!actor) {
4251
- throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
4310
+ // packages/core/plugin-host/sql-guard.ts
4311
+ function sanitizeSql(sql) {
4312
+ let inSingleQuote = false;
4313
+ let inDoubleQuote = false;
4314
+ let inBacktick = false;
4315
+ let inBracket = false;
4316
+ let result = "";
4317
+ const len = sql.length;
4318
+ for (let i = 0; i < len; i++) {
4319
+ const ch = sql[i];
4320
+ const next = i + 1 < len ? sql[i + 1] : "";
4321
+ if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "-" && next === "-") {
4322
+ while (i < len && sql[i] !== "\n") i++;
4323
+ result += " ";
4324
+ continue;
4325
+ }
4326
+ if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "/" && next === "*") {
4327
+ i += 2;
4328
+ while (i < len && !(sql[i] === "*" && i + 1 < len && sql[i + 1] === "/")) {
4329
+ i++;
4330
+ }
4331
+ i++;
4332
+ result += " ";
4333
+ continue;
4334
+ }
4335
+ if (ch === "'" && !inDoubleQuote && !inBacktick && !inBracket) {
4336
+ if (inSingleQuote) {
4337
+ if (next === "'") {
4338
+ i++;
4339
+ } else {
4340
+ inSingleQuote = false;
4341
+ }
4342
+ } else {
4343
+ inSingleQuote = true;
4344
+ }
4345
+ result += " ";
4346
+ continue;
4347
+ }
4348
+ if (inSingleQuote) {
4349
+ result += " ";
4350
+ continue;
4351
+ }
4352
+ if (ch === "[" && !inDoubleQuote && !inBacktick) {
4353
+ inBracket = true;
4354
+ result += " ";
4355
+ continue;
4356
+ }
4357
+ if (ch === "]" && inBracket) {
4358
+ inBracket = false;
4359
+ result += " ";
4360
+ continue;
4361
+ }
4362
+ if (ch === "`" && !inDoubleQuote) {
4363
+ inBacktick = !inBacktick;
4364
+ result += " ";
4365
+ continue;
4366
+ }
4367
+ if (ch === '"' && !inBacktick) {
4368
+ inDoubleQuote = !inDoubleQuote;
4369
+ result += " ";
4370
+ continue;
4371
+ }
4372
+ result += ch;
4252
4373
  }
4253
- const raw = command && typeof command === "object" ? { ...command } : {};
4254
- const metadata = raw.metadata && typeof raw.metadata === "object" ? { ...raw.metadata } : {};
4255
- delete metadata.approved;
4256
- return { ...raw, actorId: actor, metadata };
4374
+ return result;
4257
4375
  }
4258
- function pluginActorIdFor(manifestId, pluginId) {
4259
- const id = (manifestId || pluginId || "").trim();
4260
- if (!id) {
4261
- throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
4376
+ function extractFlatTables(flatSql, tables) {
4377
+ const ddlRegex = /\b(?:CREATE|DROP|ALTER)\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?([a-zA-Z_][\w]*)/gi;
4378
+ for (const m of flatSql.matchAll(ddlRegex)) {
4379
+ if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
4380
+ tables.add(m[1].toLowerCase());
4381
+ }
4382
+ }
4383
+ const renameToRegex = /\bRENAME\s+TO\s+([a-zA-Z_][\w]*)/gi;
4384
+ for (const m of flatSql.matchAll(renameToRegex)) {
4385
+ if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
4386
+ tables.add(m[1].toLowerCase());
4387
+ }
4388
+ }
4389
+ const simpleClauseRegex = /\b(?:INTO|UPDATE(?:\s+OR\s+(?:ROLLBACK|ABORT|REPLACE|FAIL|IGNORE))?|JOIN)\s+([a-zA-Z_][\w]*)/gi;
4390
+ for (const m of flatSql.matchAll(simpleClauseRegex)) {
4391
+ if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
4392
+ tables.add(m[1].toLowerCase());
4393
+ }
4394
+ }
4395
+ const fromRegex = /\bFROM\s+([^;]+?)(?=\bWHERE\b|\bGROUP\b|\bHAVING\b|\bORDER\b|\bLIMIT\b|\bUNION\b|\bEXCEPT\b|\bINTERSECT\b|\bWINDOW\b|\bVALUES\b|\bSET\b|\bINTO\b|\bJOIN\b|\bLEFT\b|\bRIGHT\b|\bINNER\b|\bOUTER\b|\bCROSS\b|\bNATURAL\b|\bON\b|\bUSING\b|\bSELECT\b|;|$)/gi;
4396
+ for (const match of flatSql.matchAll(fromRegex)) {
4397
+ const clause = match[1];
4398
+ if (!clause) continue;
4399
+ const parts = clause.split(",");
4400
+ for (const part of parts) {
4401
+ const trimmed2 = part.trim();
4402
+ if (!trimmed2) continue;
4403
+ const tokenMatch = /^[a-zA-Z_][\w]*/.exec(trimmed2);
4404
+ if (tokenMatch) {
4405
+ const tableName = tokenMatch[0].toLowerCase();
4406
+ if (tableName !== SUBQUERY_PLACEHOLDER) {
4407
+ tables.add(tableName);
4408
+ }
4409
+ }
4410
+ }
4262
4411
  }
4263
- return id.startsWith("plugin:") ? id : `plugin:${id}`;
4264
4412
  }
4265
- var init_plugin_command = __esm({
4266
- "packages/core/command-bus/plugin-command.ts"() {
4267
- "use strict";
4413
+ function extractReferencedTables(cleanSql) {
4414
+ const tables = /* @__PURE__ */ new Set();
4415
+ let working = cleanSql;
4416
+ const parenRegex = /\(([^()]*)\)/g;
4417
+ let depth = 0;
4418
+ while (parenRegex.test(working) && depth++ < 100) {
4419
+ working = working.replace(parenRegex, (_full, inner) => {
4420
+ extractFlatTables(inner, tables);
4421
+ const trimmedInner = inner.trim();
4422
+ if (/^(?:SELECT|VALUES|WITH)\b/i.test(trimmedInner)) {
4423
+ return ` ${SUBQUERY_PLACEHOLDER} `;
4424
+ }
4425
+ return ` ${inner} `;
4426
+ });
4268
4427
  }
4269
- });
4270
-
4271
- // packages/core/plugin-host/plugin-namespace.ts
4272
- function resolvePluginCommandType(type, pluginId) {
4273
- const prefix = pluginId + ".";
4274
- if (type.startsWith(prefix)) return type;
4275
- if (UUID_V7_PREFIX.test(type)) return type;
4276
- return prefix + type;
4428
+ working = working.replace(/[()]/g, " ");
4429
+ extractFlatTables(working, tables);
4430
+ return tables;
4277
4431
  }
4278
- function stripPluginCommandPrefix(type, pluginId) {
4279
- const prefix = pluginId + ".";
4280
- if (type.startsWith(prefix)) return type.slice(prefix.length);
4281
- if (UUID_V7_PREFIX.test(type)) {
4282
- const idx = type.indexOf(".");
4283
- return type.slice(idx + 1);
4432
+ function assertPluginSqlAllowed(options) {
4433
+ const { sql, ownPrefix, allowedPrefixes = [], researchAllowed = false } = options;
4434
+ if (typeof sql !== "string" || sql.trim() === "") return;
4435
+ for (const opRegex of FORBIDDEN_OPERATIONS) {
4436
+ if (opRegex.test(sql)) {
4437
+ throw new Error(`Dangerous statement is forbidden: "${sql.trim()}"`);
4438
+ }
4439
+ }
4440
+ const clean = sanitizeSql(sql);
4441
+ const tables = extractReferencedTables(clean);
4442
+ if (/\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\b/i.test(clean) && tables.size === 0) {
4443
+ throw new Error(`Worker plugin SQL rejected: unable to resolve target table in "${sql.trim()}"`);
4444
+ }
4445
+ const isDdl = /^\s*(DROP|CREATE|ALTER|TRUNCATE)\b/i.test(clean);
4446
+ const words = clean.toLowerCase().match(/\b[a-zA-Z_][\w]*\b/g) || [];
4447
+ for (const word of words) {
4448
+ if (FORBIDDEN_CORE_TABLES_SET.has(word)) {
4449
+ if (tables.has(word)) {
4450
+ if (isDdl) {
4451
+ throw new Error(
4452
+ `Worker plugin is not permitted to perform DDL on table "${word}" (core security table is strictly forbidden)`
4453
+ );
4454
+ }
4455
+ throw new Error(
4456
+ `Worker plugin is not permitted to access table "${word}" (core security table is strictly forbidden)`
4457
+ );
4458
+ }
4459
+ }
4460
+ }
4461
+ const ownPrefixLower = ownPrefix ? ownPrefix.toLowerCase() : "";
4462
+ const validPrefixes = [ownPrefixLower, ...allowedPrefixes.map((p) => p.toLowerCase())].filter(Boolean);
4463
+ for (const table of tables) {
4464
+ if (table === "plugin_migrations") continue;
4465
+ if (researchAllowed && table.startsWith("plugin_research_")) continue;
4466
+ const isOwn = validPrefixes.some((p) => table.startsWith(p));
4467
+ if (isOwn) continue;
4468
+ if (table === "sqlite_master" || table === "sqlite_schema" || table === "sqlite_temp_master") {
4469
+ const strippedComments = sql.replace(/--.*$/gm, " ").replace(/\/\*[\s\S]*?\*\//g, " ").trim();
4470
+ const isAllowedMetaQuery = /^\s*SELECT\s+name\s+FROM\s+(?:sqlite_master|sqlite_schema)\s+WHERE\s+type\s*=\s*['"]table['"]\s+AND\s+name\s+LIKE\s+['"][A-Za-z0-9_%]+['"]\s*$/i.test(
4471
+ strippedComments
4472
+ ) && ownPrefixLower && strippedComments.toLowerCase().includes(ownPrefixLower);
4473
+ if (isAllowedMetaQuery) continue;
4474
+ throw new Error(`Direct access to sqlite metadata table "${table}" is forbidden`);
4475
+ }
4476
+ if (isDdl) {
4477
+ throw new Error(
4478
+ `Worker plugin is not permitted to perform DDL on table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
4479
+ );
4480
+ }
4481
+ throw new Error(
4482
+ `Worker plugin is not permitted to access table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
4483
+ );
4284
4484
  }
4285
- return type;
4286
4485
  }
4287
- var UUID_V7_PREFIX;
4288
- var init_plugin_namespace = __esm({
4289
- "packages/core/plugin-host/plugin-namespace.ts"() {
4486
+ var FORBIDDEN_CORE_TABLES, FORBIDDEN_CORE_TABLES_SET, FORBIDDEN_OPERATIONS, SUBQUERY_PLACEHOLDER;
4487
+ var init_sql_guard = __esm({
4488
+ "packages/core/plugin-host/sql-guard.ts"() {
4290
4489
  "use strict";
4291
- UUID_V7_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\./i;
4490
+ FORBIDDEN_CORE_TABLES = Object.freeze([
4491
+ "users",
4492
+ "client_sessions",
4493
+ "plugins",
4494
+ "plugin_storage",
4495
+ "ai_providers",
4496
+ "processes",
4497
+ "pending_commands",
4498
+ "classes",
4499
+ "students",
4500
+ "class_students",
4501
+ "lessons",
4502
+ "assignments",
4503
+ "assignment_submissions",
4504
+ "attendance",
4505
+ "schedules",
4506
+ "class_grade_weights",
4507
+ "exams",
4508
+ "exam_scores",
4509
+ "student_semester_reports",
4510
+ "student_rollcalls",
4511
+ "student_seats",
4512
+ "student_read_notifications",
4513
+ "student_lesson_progress",
4514
+ "courseware",
4515
+ "courseware_attempt",
4516
+ "system_resources",
4517
+ "events",
4518
+ "_migrations",
4519
+ "site_settings",
4520
+ "agent_conversations",
4521
+ "classroom_sessions",
4522
+ "lesson_quiz_submissions",
4523
+ "courseware_score_config",
4524
+ "demo_data_registry",
4525
+ "whiteboard_elements",
4526
+ "student_point_logs",
4527
+ "lianyun_research_activities",
4528
+ "lianyun_research_groups",
4529
+ "lianyun_research_submissions",
4530
+ "lianyun_research_reviews"
4531
+ ]);
4532
+ FORBIDDEN_CORE_TABLES_SET = new Set(FORBIDDEN_CORE_TABLES.map((t) => t.toLowerCase()));
4533
+ FORBIDDEN_OPERATIONS = [
4534
+ /\bATTACH(?:\s+DATABASE)?\b/i,
4535
+ /\bDETACH(?:\s+DATABASE)?\b/i,
4536
+ /\bPRAGMA\b/i,
4537
+ /\bVACUUM\b/i,
4538
+ /\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
4539
+ /\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
4540
+ ];
4541
+ SUBQUERY_PLACEHOLDER = "__ol_subquery__";
4292
4542
  }
4293
4543
  });
4294
4544
 
@@ -4318,6 +4568,108 @@ var init_plugin_tables = __esm({
4318
4568
  }
4319
4569
  });
4320
4570
 
4571
+ // packages/core/plugin-host/inline-db-guard.ts
4572
+ function isSqlGuardAllModesEnabled(env = process.env) {
4573
+ const raw = env.OPENLEARN_SQL_GUARD_ALL_MODES?.trim().toLowerCase();
4574
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
4575
+ }
4576
+ function createGuardedPluginDatabase(db2, pluginId, manifestId) {
4577
+ const ownPrefix = pluginTablePrefix(pluginId, manifestId);
4578
+ const allowedPrefixes = listAllowedPluginTablePrefixes(pluginId, manifestId);
4579
+ const assertSql = (sql) => {
4580
+ try {
4581
+ assertPluginSqlAllowed({ sql: typeof sql === "string" ? sql : String(sql ?? ""), ownPrefix, allowedPrefixes });
4582
+ } catch (err) {
4583
+ const message = err?.message ? String(err.message) : String(err);
4584
+ const wrapped = new Error(`${SQL_GUARD_ERROR_PREFIX}\uFF1A${message}\uFF08\u63D2\u4EF6 "${pluginId}"\uFF09`);
4585
+ wrapped.code = "PLUGIN_SQL_REJECTED";
4586
+ throw wrapped;
4587
+ }
4588
+ };
4589
+ return new Proxy(db2, {
4590
+ get(target, prop, receiver) {
4591
+ const value = Reflect.get(target, prop, receiver);
4592
+ if (typeof value !== "function") return value;
4593
+ if (prop === "prepare") {
4594
+ return (sql, ...rest) => {
4595
+ assertSql(sql);
4596
+ return value.call(target, sql, ...rest);
4597
+ };
4598
+ }
4599
+ if (prop === "exec") {
4600
+ return (sql, ...rest) => {
4601
+ assertSql(sql);
4602
+ return value.call(target, sql, ...rest);
4603
+ };
4604
+ }
4605
+ if (prop === "pragma") {
4606
+ return (source, ...rest) => {
4607
+ assertSql(`PRAGMA ${String(source ?? "")}`);
4608
+ return value.call(target, source, ...rest);
4609
+ };
4610
+ }
4611
+ return value.bind(target);
4612
+ }
4613
+ });
4614
+ }
4615
+ var SQL_GUARD_ERROR_PREFIX;
4616
+ var init_inline_db_guard = __esm({
4617
+ "packages/core/plugin-host/inline-db-guard.ts"() {
4618
+ "use strict";
4619
+ init_sql_guard();
4620
+ init_plugin_tables();
4621
+ SQL_GUARD_ERROR_PREFIX = "[PluginHost] SQL \u88AB\u62D2\u7EDD";
4622
+ }
4623
+ });
4624
+
4625
+ // packages/core/command-bus/plugin-command.ts
4626
+ function bindPluginCommand(command, pluginActorId) {
4627
+ const actor = typeof pluginActorId === "string" ? pluginActorId.trim() : "";
4628
+ if (!actor) {
4629
+ throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
4630
+ }
4631
+ const raw = command && typeof command === "object" ? { ...command } : {};
4632
+ const metadata = raw.metadata && typeof raw.metadata === "object" ? { ...raw.metadata } : {};
4633
+ delete metadata.approved;
4634
+ return { ...raw, actorId: actor, metadata };
4635
+ }
4636
+ function pluginActorIdFor(manifestId, pluginId) {
4637
+ const id = (manifestId || pluginId || "").trim();
4638
+ if (!id) {
4639
+ throw new Error("[Security] Refusing to dispatch a plugin command without a host-assigned actor");
4640
+ }
4641
+ return id.startsWith("plugin:") ? id : `plugin:${id}`;
4642
+ }
4643
+ var init_plugin_command = __esm({
4644
+ "packages/core/command-bus/plugin-command.ts"() {
4645
+ "use strict";
4646
+ }
4647
+ });
4648
+
4649
+ // packages/core/plugin-host/plugin-namespace.ts
4650
+ function resolvePluginCommandType(type, pluginId) {
4651
+ const prefix = pluginId + ".";
4652
+ if (type.startsWith(prefix)) return type;
4653
+ if (UUID_V7_PREFIX.test(type)) return type;
4654
+ return prefix + type;
4655
+ }
4656
+ function stripPluginCommandPrefix(type, pluginId) {
4657
+ const prefix = pluginId + ".";
4658
+ if (type.startsWith(prefix)) return type.slice(prefix.length);
4659
+ if (UUID_V7_PREFIX.test(type)) {
4660
+ const idx = type.indexOf(".");
4661
+ return type.slice(idx + 1);
4662
+ }
4663
+ return type;
4664
+ }
4665
+ var UUID_V7_PREFIX;
4666
+ var init_plugin_namespace = __esm({
4667
+ "packages/core/plugin-host/plugin-namespace.ts"() {
4668
+ "use strict";
4669
+ UUID_V7_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\./i;
4670
+ }
4671
+ });
4672
+
4321
4673
  // packages/core/plugin-host/context-builder.ts
4322
4674
  var context_builder_exports = {};
4323
4675
  __export(context_builder_exports, {
@@ -4720,15 +5072,51 @@ function wrapInteractionRuntimeService(service, tracker, pluginId) {
4720
5072
  listActivityProviders: createSafeFunction(() => service.listActivityProviders())
4721
5073
  };
4722
5074
  }
4723
- function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId) {
5075
+ function wrapPluginRuntimeService(raw, tokenName, tracker, pluginId, manifestId) {
4724
5076
  if (tokenName === IClassroomLifecycleServiceToken.name) {
4725
5077
  return wrapClassroomLifecycleService(raw, tracker, pluginId);
4726
5078
  }
4727
5079
  if (tokenName === IInteractionRuntimeServiceToken.name) {
4728
5080
  return wrapInteractionRuntimeService(raw, tracker, pluginId);
4729
5081
  }
5082
+ if (tokenName === "@openlearn/core:IDatabase" && isSqlGuardAllModesEnabled()) {
5083
+ return createGuardedPluginDatabase(raw, pluginId, manifestId);
5084
+ }
4730
5085
  return raw;
4731
5086
  }
5087
+ function wrapRosterService(service, pluginId, capabilities) {
5088
+ const CAP_ROSTER_READ = "management:read";
5089
+ const safe = (fn) => createSafeFunction(fn);
5090
+ const assertReadable = (method, detail) => {
5091
+ const hasCap = hasCapability(capabilities ?? [], CAP_ROSTER_READ);
5092
+ recordSecurityAudit("plugin.roster_read", {
5093
+ pluginId,
5094
+ method,
5095
+ ...detail,
5096
+ hasCapability: hasCap,
5097
+ ...hasCap ? {} : { denied: true }
5098
+ });
5099
+ if (hasCap) return;
5100
+ console.error(
5101
+ `[Security] \u63D2\u4EF6 "${pluginId}" \u8C03\u7528 roster.${method} \u88AB\u62D2\uFF1A\u7F3A\u5C11\u80FD\u529B "${CAP_ROSTER_READ}"\u3002\u8BF7\u5728 manifest.capabilitiesProposed \u58F0\u660E\u5B83\uFF08\u6559\u5E08\u89D2\u8272\u9ED8\u8BA4\u5177\u5907\uFF09\u3002`
5102
+ );
5103
+ throw new PluginCapabilityDeniedError(pluginId, CAP_ROSTER_READ, "read", method);
5104
+ };
5105
+ return {
5106
+ listClasses: safe(async () => {
5107
+ assertReadable("listClasses", {});
5108
+ return service.listClasses();
5109
+ }),
5110
+ listClassStudents: safe(async (classId) => {
5111
+ assertReadable("listClassStudents", { classId });
5112
+ return service.listClassStudents(classId);
5113
+ }),
5114
+ getStudent: safe(async (studentId) => {
5115
+ assertReadable("getStudent", { studentId });
5116
+ return service.getStudent(studentId);
5117
+ })
5118
+ };
5119
+ }
4732
5120
  function wrapPointsLedgerService(service, pluginId, capabilities) {
4733
5121
  const auditAccess = (studentId, method, operation, requiredCap) => {
4734
5122
  const hasCap = hasCapability(capabilities ?? [], requiredCap);
@@ -4741,6 +5129,9 @@ function wrapPointsLedgerService(service, pluginId, capabilities) {
4741
5129
  ...hasCap ? {} : { denied: true }
4742
5130
  });
4743
5131
  if (!hasCap) {
5132
+ console.error(
5133
+ `[Security] \u63D2\u4EF6 "${pluginId}" \u8C03\u7528 pointsLedger.${method} \u88AB\u62D2\uFF1A\u7F3A\u5C11\u80FD\u529B "${requiredCap}"\uFF08${operation} / student=${studentId}\uFF09\u3002\u8BF7\u5728 manifest.capabilitiesProposed \u58F0\u660E\u5B83\u3002`
5134
+ );
4744
5135
  throw new PluginCapabilityDeniedError(pluginId, requiredCap, operation, method);
4745
5136
  }
4746
5137
  };
@@ -4857,6 +5248,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4857
5248
  const aiService = await serviceRegistry.resolve(IAIServiceToken);
4858
5249
  const pointsDimensionRegistry = await serviceRegistry.tryResolve(IPointsDimensionRegistryToken);
4859
5250
  const pointsLedgerService = await serviceRegistry.tryResolve(IPointsLedgerServiceToken);
5251
+ const rosterService = await serviceRegistry.tryResolve(IClassRosterServiceToken);
4860
5252
  const wrappedCommandBus = wrapCommandBus(commandBusService, tracker, pluginId, manifest.id, actionRegistryService);
4861
5253
  const wrappedEventBus = wrapEventBus(eventBusService, tracker, pluginId);
4862
5254
  const wrappedProcessManager = wrapProcessManager(
@@ -4873,6 +5265,7 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4873
5265
  const wrappedAI = wrapAI(aiService, pluginId, tracker);
4874
5266
  const wrappedPointsDimension = pointsDimensionRegistry ? wrapPointsDimensionRegistry(pointsDimensionRegistry, tracker, pluginId) : null;
4875
5267
  const wrappedPointsLedger = pointsLedgerService ? wrapPointsLedgerService(pointsLedgerService, pluginId, manifest.capabilitiesProposed) : null;
5268
+ const wrappedRoster = rosterService ? wrapRosterService(rosterService, pluginId, manifest.capabilitiesProposed) : null;
4876
5269
  Object.setPrototypeOf(wrappedCommandBus, null);
4877
5270
  Object.setPrototypeOf(wrappedEventBus, null);
4878
5271
  Object.setPrototypeOf(wrappedProcessManager, null);
@@ -4900,7 +5293,8 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4900
5293
  storage: wrappedStorage,
4901
5294
  ai: wrappedAI,
4902
5295
  pointsDimension: wrappedPointsDimension,
4903
- pointsLedger: wrappedPointsLedger
5296
+ pointsLedger: wrappedPointsLedger,
5297
+ roster: wrappedRoster
4904
5298
  };
4905
5299
  if (skipTokens && skipTokens.size > 0) {
4906
5300
  const TOKEN_TO_SERVICE_KEY = {
@@ -4912,7 +5306,8 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
4912
5306
  "@openlearn/core:IStorageService": "storage",
4913
5307
  "@openlearn/core:IAIService": "ai",
4914
5308
  "@openlearn/core:IPointsDimensionRegistry": "pointsDimension",
4915
- "@openlearn/core:IPointsLedgerService": "pointsLedger"
5309
+ "@openlearn/core:IPointsLedgerService": "pointsLedger",
5310
+ "@openlearn/core:IClassRosterService": "roster"
4916
5311
  };
4917
5312
  for (const tokenName of skipTokens) {
4918
5313
  const serviceKey = TOKEN_TO_SERVICE_KEY[tokenName];
@@ -5023,9 +5418,9 @@ async function buildContext(serviceRegistry, tracker, pluginId, manifest, db2, s
5023
5418
  `[PluginHost] Plugin "${manifest.id}" cannot resolve "${token}" by name. Use the Token object imported from @openlearn/plugin-sdk, or one of the SDK-exported Token names.`
5024
5419
  );
5025
5420
  }
5026
- return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId));
5421
+ return serviceRegistry.resolveByName(token).then((raw) => wrapPluginRuntimeService(raw, token, tracker, pluginId, manifest.id));
5027
5422
  }
5028
- return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId));
5423
+ return serviceRegistry.resolve(token).then((raw) => wrapPluginRuntimeService(raw, token.name, tracker, pluginId, manifest.id));
5029
5424
  },
5030
5425
  // V3.2: 插件向 DI 容器提供服务(对应 manifest.provides)
5031
5426
  provide: async (token, instance) => {
@@ -5125,6 +5520,7 @@ var init_context_builder = __esm({
5125
5520
  init_audit_log();
5126
5521
  init_method_policy();
5127
5522
  init_errors2();
5523
+ init_inline_db_guard();
5128
5524
  init_extension_points();
5129
5525
  init_interfaces();
5130
5526
  init_plugin_command();
@@ -7518,241 +7914,6 @@ var init_event_forwarder = __esm({
7518
7914
  }
7519
7915
  });
7520
7916
 
7521
- // packages/core/plugin-host/sql-guard.ts
7522
- function sanitizeSql(sql) {
7523
- let inSingleQuote = false;
7524
- let inDoubleQuote = false;
7525
- let inBacktick = false;
7526
- let inBracket = false;
7527
- let result = "";
7528
- const len = sql.length;
7529
- for (let i = 0; i < len; i++) {
7530
- const ch = sql[i];
7531
- const next = i + 1 < len ? sql[i + 1] : "";
7532
- if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "-" && next === "-") {
7533
- while (i < len && sql[i] !== "\n") i++;
7534
- result += " ";
7535
- continue;
7536
- }
7537
- if (!inSingleQuote && !inDoubleQuote && !inBacktick && !inBracket && ch === "/" && next === "*") {
7538
- i += 2;
7539
- while (i < len && !(sql[i] === "*" && i + 1 < len && sql[i + 1] === "/")) {
7540
- i++;
7541
- }
7542
- i++;
7543
- result += " ";
7544
- continue;
7545
- }
7546
- if (ch === "'" && !inDoubleQuote && !inBacktick && !inBracket) {
7547
- if (inSingleQuote) {
7548
- if (next === "'") {
7549
- i++;
7550
- } else {
7551
- inSingleQuote = false;
7552
- }
7553
- } else {
7554
- inSingleQuote = true;
7555
- }
7556
- result += " ";
7557
- continue;
7558
- }
7559
- if (inSingleQuote) {
7560
- result += " ";
7561
- continue;
7562
- }
7563
- if (ch === "[" && !inDoubleQuote && !inBacktick) {
7564
- inBracket = true;
7565
- result += " ";
7566
- continue;
7567
- }
7568
- if (ch === "]" && inBracket) {
7569
- inBracket = false;
7570
- result += " ";
7571
- continue;
7572
- }
7573
- if (ch === "`" && !inDoubleQuote) {
7574
- inBacktick = !inBacktick;
7575
- result += " ";
7576
- continue;
7577
- }
7578
- if (ch === '"' && !inBacktick) {
7579
- inDoubleQuote = !inDoubleQuote;
7580
- result += " ";
7581
- continue;
7582
- }
7583
- result += ch;
7584
- }
7585
- return result;
7586
- }
7587
- function extractFlatTables(flatSql, tables) {
7588
- const ddlRegex = /\b(?:CREATE|DROP|ALTER)\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?([a-zA-Z_][\w]*)/gi;
7589
- for (const m of flatSql.matchAll(ddlRegex)) {
7590
- if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
7591
- tables.add(m[1].toLowerCase());
7592
- }
7593
- }
7594
- const renameToRegex = /\bRENAME\s+TO\s+([a-zA-Z_][\w]*)/gi;
7595
- for (const m of flatSql.matchAll(renameToRegex)) {
7596
- if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
7597
- tables.add(m[1].toLowerCase());
7598
- }
7599
- }
7600
- const simpleClauseRegex = /\b(?:INTO|UPDATE(?:\s+OR\s+(?:ROLLBACK|ABORT|REPLACE|FAIL|IGNORE))?|JOIN)\s+([a-zA-Z_][\w]*)/gi;
7601
- for (const m of flatSql.matchAll(simpleClauseRegex)) {
7602
- if (m[1] && m[1].toLowerCase() !== SUBQUERY_PLACEHOLDER) {
7603
- tables.add(m[1].toLowerCase());
7604
- }
7605
- }
7606
- const fromRegex = /\bFROM\s+([^;]+?)(?=\bWHERE\b|\bGROUP\b|\bHAVING\b|\bORDER\b|\bLIMIT\b|\bUNION\b|\bEXCEPT\b|\bINTERSECT\b|\bWINDOW\b|\bVALUES\b|\bSET\b|\bINTO\b|\bJOIN\b|\bLEFT\b|\bRIGHT\b|\bINNER\b|\bOUTER\b|\bCROSS\b|\bNATURAL\b|\bON\b|\bUSING\b|\bSELECT\b|;|$)/gi;
7607
- for (const match of flatSql.matchAll(fromRegex)) {
7608
- const clause = match[1];
7609
- if (!clause) continue;
7610
- const parts = clause.split(",");
7611
- for (const part of parts) {
7612
- const trimmed2 = part.trim();
7613
- if (!trimmed2) continue;
7614
- const tokenMatch = /^[a-zA-Z_][\w]*/.exec(trimmed2);
7615
- if (tokenMatch) {
7616
- const tableName = tokenMatch[0].toLowerCase();
7617
- if (tableName !== SUBQUERY_PLACEHOLDER) {
7618
- tables.add(tableName);
7619
- }
7620
- }
7621
- }
7622
- }
7623
- }
7624
- function extractReferencedTables(cleanSql) {
7625
- const tables = /* @__PURE__ */ new Set();
7626
- let working = cleanSql;
7627
- const parenRegex = /\(([^()]*)\)/g;
7628
- let depth = 0;
7629
- while (parenRegex.test(working) && depth++ < 100) {
7630
- working = working.replace(parenRegex, (_full, inner) => {
7631
- extractFlatTables(inner, tables);
7632
- const trimmedInner = inner.trim();
7633
- if (/^(?:SELECT|VALUES|WITH)\b/i.test(trimmedInner)) {
7634
- return ` ${SUBQUERY_PLACEHOLDER} `;
7635
- }
7636
- return ` ${inner} `;
7637
- });
7638
- }
7639
- working = working.replace(/[()]/g, " ");
7640
- extractFlatTables(working, tables);
7641
- return tables;
7642
- }
7643
- function assertPluginSqlAllowed(options) {
7644
- const { sql, ownPrefix, allowedPrefixes = [], researchAllowed = false } = options;
7645
- if (typeof sql !== "string" || sql.trim() === "") return;
7646
- for (const opRegex of FORBIDDEN_OPERATIONS) {
7647
- if (opRegex.test(sql)) {
7648
- throw new Error(`Dangerous statement is forbidden: "${sql.trim()}"`);
7649
- }
7650
- }
7651
- const clean = sanitizeSql(sql);
7652
- const tables = extractReferencedTables(clean);
7653
- if (/\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\b/i.test(clean) && tables.size === 0) {
7654
- throw new Error(`Worker plugin SQL rejected: unable to resolve target table in "${sql.trim()}"`);
7655
- }
7656
- const isDdl = /^\s*(DROP|CREATE|ALTER|TRUNCATE)\b/i.test(clean);
7657
- const words = clean.toLowerCase().match(/\b[a-zA-Z_][\w]*\b/g) || [];
7658
- for (const word of words) {
7659
- if (FORBIDDEN_CORE_TABLES_SET.has(word)) {
7660
- if (tables.has(word)) {
7661
- if (isDdl) {
7662
- throw new Error(
7663
- `Worker plugin is not permitted to perform DDL on table "${word}" (core security table is strictly forbidden)`
7664
- );
7665
- }
7666
- throw new Error(
7667
- `Worker plugin is not permitted to access table "${word}" (core security table is strictly forbidden)`
7668
- );
7669
- }
7670
- }
7671
- }
7672
- const ownPrefixLower = ownPrefix ? ownPrefix.toLowerCase() : "";
7673
- const validPrefixes = [ownPrefixLower, ...allowedPrefixes.map((p) => p.toLowerCase())].filter(Boolean);
7674
- for (const table of tables) {
7675
- if (table === "plugin_migrations") continue;
7676
- if (researchAllowed && table.startsWith("plugin_research_")) continue;
7677
- const isOwn = validPrefixes.some((p) => table.startsWith(p));
7678
- if (isOwn) continue;
7679
- if (table === "sqlite_master" || table === "sqlite_schema" || table === "sqlite_temp_master") {
7680
- const strippedComments = sql.replace(/--.*$/gm, " ").replace(/\/\*[\s\S]*?\*\//g, " ").trim();
7681
- const isAllowedMetaQuery = /^\s*SELECT\s+name\s+FROM\s+(?:sqlite_master|sqlite_schema)\s+WHERE\s+type\s*=\s*['"]table['"]\s+AND\s+name\s+LIKE\s+['"][A-Za-z0-9_%]+['"]\s*$/i.test(
7682
- strippedComments
7683
- ) && ownPrefixLower && strippedComments.toLowerCase().includes(ownPrefixLower);
7684
- if (isAllowedMetaQuery) continue;
7685
- throw new Error(`Direct access to sqlite metadata table "${table}" is forbidden`);
7686
- }
7687
- if (isDdl) {
7688
- throw new Error(
7689
- `Worker plugin is not permitted to perform DDL on table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
7690
- );
7691
- }
7692
- throw new Error(
7693
- `Worker plugin is not permitted to access table "${table}" (only ${ownPrefix || "<none>"} namespace is allowed)`
7694
- );
7695
- }
7696
- }
7697
- var FORBIDDEN_CORE_TABLES, FORBIDDEN_CORE_TABLES_SET, FORBIDDEN_OPERATIONS, SUBQUERY_PLACEHOLDER;
7698
- var init_sql_guard = __esm({
7699
- "packages/core/plugin-host/sql-guard.ts"() {
7700
- "use strict";
7701
- FORBIDDEN_CORE_TABLES = Object.freeze([
7702
- "users",
7703
- "client_sessions",
7704
- "plugins",
7705
- "plugin_storage",
7706
- "ai_providers",
7707
- "processes",
7708
- "pending_commands",
7709
- "classes",
7710
- "students",
7711
- "class_students",
7712
- "lessons",
7713
- "assignments",
7714
- "assignment_submissions",
7715
- "attendance",
7716
- "schedules",
7717
- "class_grade_weights",
7718
- "exams",
7719
- "exam_scores",
7720
- "student_semester_reports",
7721
- "student_rollcalls",
7722
- "student_seats",
7723
- "student_read_notifications",
7724
- "student_lesson_progress",
7725
- "courseware",
7726
- "courseware_attempt",
7727
- "system_resources",
7728
- "events",
7729
- "_migrations",
7730
- "site_settings",
7731
- "agent_conversations",
7732
- "classroom_sessions",
7733
- "lesson_quiz_submissions",
7734
- "courseware_score_config",
7735
- "demo_data_registry",
7736
- "whiteboard_elements",
7737
- "student_point_logs",
7738
- "lianyun_research_activities",
7739
- "lianyun_research_groups",
7740
- "lianyun_research_submissions",
7741
- "lianyun_research_reviews"
7742
- ]);
7743
- FORBIDDEN_CORE_TABLES_SET = new Set(FORBIDDEN_CORE_TABLES.map((t) => t.toLowerCase()));
7744
- FORBIDDEN_OPERATIONS = [
7745
- /\bATTACH(?:\s+DATABASE)?\b/i,
7746
- /\bDETACH(?:\s+DATABASE)?\b/i,
7747
- /\bPRAGMA\b/i,
7748
- /\bVACUUM\b/i,
7749
- /\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
7750
- /\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
7751
- ];
7752
- SUBQUERY_PLACEHOLDER = "__ol_subquery__";
7753
- }
7754
- });
7755
-
7756
7917
  // packages/core/worker-runtime/service-host.ts
7757
7918
  function sanitizeClonableValue(val, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
7758
7919
  if (depth > 12) return null;
@@ -10309,6 +10470,15 @@ var CommandBus = class {
10309
10470
  hasHandler(commandType) {
10310
10471
  return this.handlers.has(commandType) || this.legacyHandlers.has(commandType);
10311
10472
  }
10473
+ /**
10474
+ * 只读枚举已注册的命令类型(modern + legacy)。
10475
+ *
10476
+ * 供插件激活后的「handler 有、ActionDescriptor 没有」自检使用(Issue #369):
10477
+ * 自检要能看清「这次激活新增了哪些命令」,而不是去读私有 Map。
10478
+ */
10479
+ listRegisteredCommandTypes() {
10480
+ return Array.from(/* @__PURE__ */ new Set([...this.handlers.keys(), ...this.legacyHandlers.keys()]));
10481
+ }
10312
10482
  async execute(command) {
10313
10483
  const actorId = typeof command.actorId === "string" ? command.actorId.trim() : "";
10314
10484
  if (!actorId) {
@@ -10411,6 +10581,20 @@ var ActionRegistry = class {
10411
10581
  getActionByCommandType(commandType) {
10412
10582
  return Array.from(this.actions.values()).find((a) => a.commandType === commandType);
10413
10583
  }
10584
+ /** 已登记描述符覆盖的 commandType(去重、只读)。 */
10585
+ listRegisteredCommandTypes() {
10586
+ return Array.from(new Set(Array.from(this.actions.values()).map((a) => a.commandType))).sort();
10587
+ }
10588
+ /**
10589
+ * 给定命令类型里**没有** ActionDescriptor 的那些。
10590
+ *
10591
+ * 命令总线是 default-deny:这种命令会在用户点击时被 403 `COMMAND_NOT_REGISTERED` 拒绝。
10592
+ * 插件激活后用它把「注册了 handler 却忘了登记描述符」提前打成一条 WARN(Issue #369)。
10593
+ */
10594
+ listMissingDescriptors(commandTypes) {
10595
+ const registered = new Set(Array.from(this.actions.values()).map((a) => a.commandType));
10596
+ return Array.from(new Set(commandTypes)).filter((type) => !registered.has(type)).sort();
10597
+ }
10414
10598
  };
10415
10599
 
10416
10600
  // packages/core/capability/event/capability-event-bus.ts
@@ -10678,6 +10862,7 @@ var InvocationEngine = class {
10678
10862
  };
10679
10863
 
10680
10864
  // packages/core/capability/guard/capability-guard.ts
10865
+ init_catalog();
10681
10866
  var CapabilityGuard = class _CapabilityGuard {
10682
10867
  constructor() {
10683
10868
  this.actorCapabilities = /* @__PURE__ */ new Map();
@@ -10693,20 +10878,20 @@ var CapabilityGuard = class _CapabilityGuard {
10693
10878
  ]);
10694
10879
  this.actorCapabilities.set("anonymous", []);
10695
10880
  this.actorCapabilities.set("agent-system-0", [
10696
- "lesson:write",
10697
- "lesson:delete",
10698
- "whiteboard:write",
10881
+ CAP.LESSON_WRITE,
10882
+ CAP.LESSON_DELETE,
10883
+ CAP.WHITEBOARD_WRITE,
10699
10884
  "quiz:write",
10700
- "plugin:read",
10701
- "vfs:read",
10702
- "vfs:write",
10703
- "management:write",
10704
- "management:read",
10705
- "process:write",
10706
- "process:read"
10885
+ CAP.PLUGIN_READ,
10886
+ CAP.VFS_READ,
10887
+ CAP.VFS_WRITE,
10888
+ CAP.MANAGEMENT_WRITE,
10889
+ CAP.MANAGEMENT_READ,
10890
+ CAP.PROCESS_WRITE,
10891
+ CAP.PROCESS_READ
10707
10892
  ]);
10708
10893
  this.actorCapabilities.set("teacher-demo", ["lesson:*", "whiteboard:*", "management:*", "quiz:*", "vfs:*"]);
10709
- this.actorCapabilities.set("student-demo", ["student:write", "lesson:read", "whiteboard:read"]);
10894
+ this.actorCapabilities.set("student-demo", [CAP.STUDENT_WRITE, CAP.LESSON_READ, CAP.WHITEBOARD_READ]);
10710
10895
  }
10711
10896
  grant(actorId, cap) {
10712
10897
  const caps = this.actorCapabilities.get(actorId) || [];
@@ -10740,11 +10925,11 @@ var CapabilityGuard = class _CapabilityGuard {
10740
10925
  "vfs:*",
10741
10926
  "process:*",
10742
10927
  "plugin:*",
10743
- "student:write",
10744
- "assignment:read",
10745
- "assignment:submit",
10746
- "assignment:review",
10747
- "assignment:manage"
10928
+ CAP.STUDENT_WRITE,
10929
+ CAP.ASSIGNMENT_READ,
10930
+ CAP.ASSIGNMENT_SUBMIT,
10931
+ CAP.ASSIGNMENT_REVIEW,
10932
+ CAP.ASSIGNMENT_MANAGE
10748
10933
  ];
10749
10934
  const [reqRes2, reqAct2] = requiredCap.split(":");
10750
10935
  if (teacherCaps.some((c) => {
@@ -10756,12 +10941,12 @@ var CapabilityGuard = class _CapabilityGuard {
10756
10941
  }
10757
10942
  if (role === "student") {
10758
10943
  const studentCaps = [
10759
- "student:write",
10760
- "lesson:read",
10761
- "whiteboard:read",
10762
- "assignment:read",
10763
- "assignment:submit",
10764
- "assignment:review"
10944
+ CAP.STUDENT_WRITE,
10945
+ CAP.LESSON_READ,
10946
+ CAP.WHITEBOARD_READ,
10947
+ CAP.ASSIGNMENT_READ,
10948
+ CAP.ASSIGNMENT_SUBMIT,
10949
+ CAP.ASSIGNMENT_REVIEW
10765
10950
  ];
10766
10951
  const [reqRes2, reqAct2] = requiredCap.split(":");
10767
10952
  if (studentCaps.some((c) => {
@@ -12100,18 +12285,25 @@ function generateUnambiguousPassword(length = 20) {
12100
12285
  }
12101
12286
  return out;
12102
12287
  }
12288
+ function resolveSeedPasswordMode(env = process.env) {
12289
+ return env.OPENLEARN_SEED_PASSWORD_MODE?.trim().toLowerCase() === "random" ? "random" : "default";
12290
+ }
12103
12291
  function resolveSeedCredential(username, options) {
12104
12292
  if (options?.allowWellKnown) {
12105
- return { password: username, mustChange: false, generated: false };
12293
+ return { password: username, mustChange: false, generated: false, source: "playwright" };
12106
12294
  }
12107
12295
  const envValue = options?.envValue?.trim();
12108
12296
  if (envValue) {
12109
- return { password: envValue, mustChange: envValue === username, generated: false };
12297
+ return { password: envValue, mustChange: envValue === username, generated: false, source: "env" };
12110
12298
  }
12111
12299
  if (options?.vitestFallback) {
12112
- return { password: `Vitest-${username}-NotDefault9`, mustChange: false, generated: false };
12300
+ return { password: `Vitest-${username}-NotDefault9`, mustChange: false, generated: false, source: "vitest" };
12113
12301
  }
12114
- return { password: generateUnambiguousPassword(20), mustChange: true, generated: true };
12302
+ const mode = options?.mode ?? resolveSeedPasswordMode();
12303
+ if (mode === "random") {
12304
+ return { password: generateUnambiguousPassword(20), mustChange: true, generated: true, source: "random" };
12305
+ }
12306
+ return { password: username, mustChange: true, generated: false, source: "default" };
12115
12307
  }
12116
12308
  function resolveBootstrapCredentialDir() {
12117
12309
  if (process.env.OPENLEARN_DATA_DIR?.trim()) {
@@ -12135,6 +12327,11 @@ function persistGeneratedSeedCredentials(lines) {
12135
12327
  console.warn(`[SECURITY] \u65E0\u6CD5\u5199\u5165 ${filePath}\uFF0C\u8BF7\u4ECE\u4E0A\u65B9\u65E5\u5FD7\u6284\u5F55\u521D\u59CB\u53E3\u4EE4\u3002`, err);
12136
12328
  }
12137
12329
  }
12330
+ function logDefaultSeedCredentials(usernames) {
12331
+ console.warn(
12332
+ "[SECURITY] \u9996\u6B21\u542F\u52A8\u5DF2\u521B\u5EFA\u79CD\u5B50\u8D26\u53F7\uFF0C\u521D\u59CB\u53E3\u4EE4 = \u7528\u6237\u540D\uFF08\u9996\u6B21\u767B\u5F55\u540E\u5FC5\u987B\u4FEE\u6539\uFF09\uFF1A\n" + usernames.map((name) => ` ${name} / ${name}`).join("\n") + "\n \u6539\u5BC6\u524D\uFF0C\u9664\u300C\u6539\u5BC6 / \u767B\u51FA\u300D\u4EE5\u5916\u7684\u6240\u6709\u63A5\u53E3\u90FD\u4F1A\u88AB\u62D2\u7EDD\uFF08403 FORBIDDEN_DEFAULT_PASSWORD\uFF09\u3002\n \u5982\u9700\u968F\u673A\u521D\u59CB\u53E3\u4EE4\uFF0C\u8BBE\u7F6E OPENLEARN_SEED_PASSWORD_MODE=random \u540E\u91CD\u5EFA\u6570\u636E\u5E93\u3002"
12333
+ );
12334
+ }
12138
12335
  function verifyPassword(pwd, storedHash) {
12139
12336
  if (storedHash.startsWith("$2")) {
12140
12337
  return { valid: import_bcryptjs.default.compareSync(pwd, storedHash), needsUpgrade: false };
@@ -12248,16 +12445,19 @@ function initializeDatabase(instance) {
12248
12445
  const countObj = instance.prepare("SELECT COUNT(*) as cnt FROM users").get();
12249
12446
  const allowWellKnown = isPlaywrightTestFlag(process.env.PLAYWRIGHT_TEST);
12250
12447
  const vitestFallback = Boolean(process.env.VITEST) && !allowWellKnown;
12448
+ const seedMode = resolveSeedPasswordMode();
12251
12449
  if (countObj && countObj.cnt === 0) {
12252
12450
  const adminCred = resolveSeedCredential("admin", {
12253
12451
  envValue: process.env.OPENLEARN_ADMIN_PASSWORD,
12254
12452
  allowWellKnown,
12255
- vitestFallback
12453
+ vitestFallback,
12454
+ mode: seedMode
12256
12455
  });
12257
12456
  const teacherCred = resolveSeedCredential("teacher", {
12258
12457
  envValue: process.env.OPENLEARN_TEACHER_PASSWORD,
12259
12458
  allowWellKnown,
12260
- vitestFallback
12459
+ vitestFallback,
12460
+ mode: seedMode
12261
12461
  });
12262
12462
  const insertStmt = instance.prepare(
12263
12463
  "INSERT INTO users (id, username, password_hash, role, name, created_at, must_change_password) VALUES (?, ?, ?, ?, ?, ?, ?)"
@@ -12284,8 +12484,12 @@ function initializeDatabase(instance) {
12284
12484
  if (adminCred.generated) generated.push(`admin ${adminCred.password}`);
12285
12485
  if (teacherCred.generated) generated.push(`teacher ${teacherCred.password}`);
12286
12486
  if (generated.length > 0) persistGeneratedSeedCredentials(generated);
12287
- } else if (!allowWellKnown) {
12288
- const seeded = instance.prepare("SELECT id, username, password_hash FROM users WHERE username IN (?, ?)").all("admin", "teacher");
12487
+ const defaulted = [];
12488
+ if (adminCred.source === "default") defaulted.push("admin");
12489
+ if (teacherCred.source === "default") defaulted.push("teacher");
12490
+ if (defaulted.length > 0) logDefaultSeedCredentials(defaulted);
12491
+ } else {
12492
+ const seeded = allowWellKnown ? [] : instance.prepare("SELECT id, username, password_hash FROM users WHERE username IN (?, ?)").all("admin", "teacher");
12289
12493
  for (const user of seeded) {
12290
12494
  if (!verifyPassword(user.username, user.password_hash).valid) continue;
12291
12495
  instance.prepare("UPDATE users SET must_change_password = 1 WHERE id = ?").run(user.id);
@@ -14920,6 +15124,45 @@ var BuiltinPlugin = {
14920
15124
  "page_meta"
14921
15125
  // 分页与大纲元数据
14922
15126
  ];
15127
+ const normalizePluginElementData = (dataObj) => {
15128
+ const pluginId = typeof dataObj.pluginId === "string" ? dataObj.pluginId.trim() : "";
15129
+ if (!pluginId) {
15130
+ console.warn("[Whiteboard] plugin \u5143\u7D20\u672A\u58F0\u660E pluginId\uFF0C\u5C06\u53EA\u80FD\u6309 widgetId \u67E5\u627E\u7EC4\u4EF6\uFF1B\u5EFA\u8BAE\u8865\u4E0A manifest id\u3002");
15131
+ }
15132
+ const teacher = typeof dataObj.teacherWidgetId === "string" ? dataObj.teacherWidgetId.trim() : "";
15133
+ const student = typeof dataObj.studentWidgetId === "string" ? dataObj.studentWidgetId.trim() : "";
15134
+ if (!teacher && !student) {
15135
+ throw new Error(
15136
+ "plugin \u5143\u7D20\u5FC5\u987B\u81F3\u5C11\u58F0\u660E teacherWidgetId \u6216 studentWidgetId \u4E4B\u4E00\uFF0C\u5426\u5219\u6559\u5E08\u7AEF / \u5B66\u751F\u7AEF\u90FD\u4F1A\u505C\u5728\u52A0\u8F7D\u6001\u3002"
15137
+ );
15138
+ }
15139
+ if (!teacher) {
15140
+ dataObj.teacherWidgetId = student;
15141
+ console.warn(`[Whiteboard] plugin \u5143\u7D20\u53EA\u58F0\u660E\u4E86 studentWidgetId\uFF0C\u5DF2\u56DE\u9000 teacherWidgetId="${student}"\u3002`);
15142
+ } else if (!student) {
15143
+ dataObj.studentWidgetId = teacher;
15144
+ console.warn(`[Whiteboard] plugin \u5143\u7D20\u53EA\u58F0\u660E\u4E86 teacherWidgetId\uFF0C\u5DF2\u56DE\u9000 studentWidgetId="${teacher}"\u3002`);
15145
+ }
15146
+ };
15147
+ const buildElementData = (type, rawData, segmentId, page) => {
15148
+ let dataObj = null;
15149
+ try {
15150
+ const parsed = JSON.parse(rawData);
15151
+ dataObj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
15152
+ } catch {
15153
+ if (type === "plugin") {
15154
+ throw new Error(
15155
+ 'plugin \u5143\u7D20\u7684 data \u5FC5\u987B\u662F\u5408\u6CD5 JSON \u5B57\u7B26\u4E32\uFF08\u4F8B\u5982 {"pluginId":"ext-x","teacherWidgetId":"w"}\uFF09\u3002'
15156
+ );
15157
+ }
15158
+ }
15159
+ if (!dataObj) return rawData;
15160
+ if (segmentId && !dataObj.segmentId) dataObj.segmentId = segmentId;
15161
+ if (page !== void 0 && dataObj.page === void 0) dataObj.page = page;
15162
+ else if (dataObj.page === void 0) dataObj.page = 0;
15163
+ if (type === "plugin") normalizePluginElementData(dataObj);
15164
+ return JSON.stringify(dataObj);
15165
+ };
14923
15166
  const isAllowedElementType = (type) => {
14924
15167
  if (!type || typeof type !== "string") return false;
14925
15168
  if (KNOWN_ELEMENT_TYPES2.includes(type)) return true;
@@ -14968,20 +15211,7 @@ var BuiltinPlugin = {
14968
15211
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${payload.type}"\u3002${hint}`);
14969
15212
  }
14970
15213
  const elementId = (0, import_uuid7.v7)();
14971
- let dataStr = payload.data;
14972
- try {
14973
- const dataObj = JSON.parse(dataStr);
14974
- if (payload.segmentId && !dataObj.segmentId) {
14975
- dataObj.segmentId = payload.segmentId;
14976
- }
14977
- if (payload.page !== void 0 && dataObj.page === void 0) {
14978
- dataObj.page = payload.page;
14979
- } else if (dataObj.page === void 0) {
14980
- dataObj.page = 0;
14981
- }
14982
- dataStr = JSON.stringify(dataObj);
14983
- } catch {
14984
- }
15214
+ const dataStr = buildElementData(payload.type, payload.data, payload.segmentId, payload.page);
14985
15215
  const stmt = db2.prepare(
14986
15216
  "INSERT INTO whiteboard_elements (id, lesson_id, type, data, created_at) VALUES (?, ?, ?, ?, ?)"
14987
15217
  );
@@ -15210,15 +15440,7 @@ var BuiltinPlugin = {
15210
15440
  throw new Error(`\u4E0D\u652F\u6301\u7684\u5143\u7D20\u7C7B\u578B "${el.type}"\u3002\u5DF2\u77E5\u7C7B\u578B\uFF1A${KNOWN_ELEMENT_TYPES2.join(", ")}`);
15211
15441
  }
15212
15442
  const elementId = (0, import_uuid7.v7)();
15213
- let dataStr = el.data;
15214
- try {
15215
- const dataObj = JSON.parse(dataStr);
15216
- if (el.segmentId && !dataObj.segmentId) dataObj.segmentId = el.segmentId;
15217
- if (el.page !== void 0 && dataObj.page === void 0) dataObj.page = el.page;
15218
- else if (dataObj.page === void 0) dataObj.page = 0;
15219
- dataStr = JSON.stringify(dataObj);
15220
- } catch {
15221
- }
15443
+ const dataStr = buildElementData(el.type, el.data, el.segmentId, el.page);
15222
15444
  insertStmt.run(elementId, payload.lessonId, el.type, dataStr, Date.now());
15223
15445
  elementIds.push(elementId);
15224
15446
  }
@@ -18042,6 +18264,40 @@ var SemesterGradeService = class {
18042
18264
  }
18043
18265
  };
18044
18266
 
18267
+ // packages/core/di/roster-service.ts
18268
+ var ClassRosterService = class {
18269
+ constructor(db2) {
18270
+ this.db = db2;
18271
+ }
18272
+ async listClasses() {
18273
+ const rows = this.db.prepare(
18274
+ `SELECT c.id AS id, c.name AS name, COUNT(cs.student_id) AS student_count
18275
+ FROM classes c
18276
+ LEFT JOIN class_students cs ON cs.class_id = c.id
18277
+ GROUP BY c.id, c.name
18278
+ ORDER BY c.created_at DESC, c.name ASC`
18279
+ ).all();
18280
+ return rows.map((row) => ({ id: row.id, name: row.name, studentCount: Number(row.student_count) }));
18281
+ }
18282
+ async listClassStudents(classId) {
18283
+ const rows = this.db.prepare(
18284
+ `SELECT s.id AS id, s.name AS name, s.student_number AS student_number
18285
+ FROM class_students cs
18286
+ JOIN students s ON s.id = cs.student_id
18287
+ WHERE cs.class_id = ?
18288
+ ORDER BY s.name ASC`
18289
+ ).all(classId);
18290
+ return rows.map(toStudentRef);
18291
+ }
18292
+ async getStudent(studentId) {
18293
+ const row = this.db.prepare("SELECT id, name, student_number FROM students WHERE id = ?").get(studentId);
18294
+ return row ? toStudentRef(row) : null;
18295
+ }
18296
+ };
18297
+ function toStudentRef(row) {
18298
+ return { id: row.id, name: row.name, studentNumber: row.student_number ?? null };
18299
+ }
18300
+
18045
18301
  // packages/core/di/points-dimension-registry.ts
18046
18302
  var PointsDimensionRegistry = class {
18047
18303
  constructor() {
@@ -20253,6 +20509,7 @@ init_types();
20253
20509
  init_errors2();
20254
20510
  init_interfaces();
20255
20511
  init_plugin_grants();
20512
+ init_catalog();
20256
20513
  var PluginHostLifecycle = class extends PluginHostHttp {
20257
20514
  /**
20258
20515
  * 激活插件。
@@ -20310,12 +20567,47 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20310
20567
  const currentState = this.pluginStates.get(pluginId) ?? "installed" /* INSTALLED */;
20311
20568
  this.validateTransition(pluginId, currentState, "activating" /* ACTIVATING */);
20312
20569
  this.setPluginState(pluginId, "activating" /* ACTIVATING */);
20570
+ const commandTypesBefore = this.listHostCommandTypes();
20313
20571
  const preloaded = this.preloadedPlugins.get(pluginId);
20314
20572
  if (preloaded) {
20315
20573
  await this.activatePreloaded(pluginId, preloaded);
20574
+ this.auditMissingActionDescriptors(pluginId, commandTypesBefore);
20316
20575
  return;
20317
20576
  }
20318
20577
  await this.activateFromDatabase(pluginId, currentState);
20578
+ this.auditMissingActionDescriptors(pluginId, commandTypesBefore);
20579
+ }
20580
+ /** 宿主 CommandBus 当前注册的命令类型;解析失败返回空集(自检不能影响激活)。 */
20581
+ listHostCommandTypes() {
20582
+ try {
20583
+ const bus = this.serviceRegistry.resolve(ICommandBusServiceToken4);
20584
+ return new Set(bus.listRegisteredCommandTypes?.() ?? []);
20585
+ } catch {
20586
+ return /* @__PURE__ */ new Set();
20587
+ }
20588
+ }
20589
+ /**
20590
+ * 插件激活后的命令契约自检(Issue #369)。
20591
+ *
20592
+ * 命令总线是 default-deny:handler 有、ActionDescriptor 没有的命令会在用户点击时
20593
+ * 403 `COMMAND_NOT_REGISTERED`。静态扫描抓不到循环注册 / 对象表 / `regHandler()` 包装,
20594
+ * 所以改为激活后做一次运行时差集,把缺口提前打成立即可见的 WARN。
20595
+ *
20596
+ * 自检只服务可观测性:任何异常都不能影响激活结果,故整体 try/catch 且只打日志。
20597
+ */
20598
+ auditMissingActionDescriptors(pluginId, before) {
20599
+ try {
20600
+ if (this.inflightActivate.size > 1) return;
20601
+ const added = Array.from(this.listHostCommandTypes()).filter((type) => !before.has(type));
20602
+ if (added.length === 0) return;
20603
+ const registry4 = this.serviceRegistry.resolve(IActionRegistryServiceToken4);
20604
+ const missing = registry4.listMissingDescriptors?.(added) ?? [];
20605
+ if (missing.length === 0) return;
20606
+ console.warn(
20607
+ `[PluginHost] \u63D2\u4EF6 "${pluginId}" \u6709 ${missing.length} \u4E2A\u547D\u4EE4 handler \u6CA1\u6709 ActionDescriptor\uFF1A${missing.join(", ")}\u3002\u8FD9\u4E9B\u547D\u4EE4\u4F1A\u88AB default-deny \u4EE5 403 COMMAND_NOT_REGISTERED \u62D2\u7EDD\uFF0C\u8BF7\u4E3A\u6BCF\u4E2A registerHandler \u540C\u65F6\u6CE8\u518C actionRegistry.register(...)\u3002`
20608
+ );
20609
+ } catch {
20610
+ }
20319
20611
  }
20320
20612
  /**
20321
20613
  * preloaded 插件的激活(Phase 8,内置插件路径)。
@@ -20566,6 +20858,12 @@ var PluginHostLifecycle = class extends PluginHostHttp {
20566
20858
  async grantCapabilities(actorId, caps, opts) {
20567
20859
  if (caps.length === 0) return;
20568
20860
  try {
20861
+ const { unknown } = validateManifestCapabilities(caps);
20862
+ if (unknown.length > 0) {
20863
+ console.error(
20864
+ `[PluginHost] "${actorId}" \u7684 manifest \u58F0\u660E\u4E86\u672A\u77E5\u80FD\u529B\uFF1A${unknown.join(", ")}\u3002\u8FD9\u4E9B\u4E32\u4E0D\u4F1A\u751F\u6548\uFF08\u5B88\u536B\u53EA\u8BA4 ActionDescriptor.capabilityRequired\uFF09\u3002\u5408\u6CD5\u6E05\u5355\u89C1 packages/core/capability/catalog.ts \u4E0E docs/reference/plugin-capability-matrix.md\u3002`
20865
+ );
20866
+ }
20569
20867
  const capService = await this.serviceRegistry.resolve(ICapabilityServiceToken);
20570
20868
  for (const cap of caps) {
20571
20869
  if (typeof cap !== "string" || isBlockedPluginCapability(cap)) {
@@ -20906,7 +21204,7 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
20906
21204
  var import_node_path13 = __toESM(require("node:path"), 1);
20907
21205
  var import_node_url5 = require("node:url");
20908
21206
  var import_meta7 = {};
20909
- var FALLBACK_VERSION = "0.7.9";
21207
+ var FALLBACK_VERSION = "0.7.10";
20910
21208
  function readPlatformVersion() {
20911
21209
  const bases = [];
20912
21210
  if (typeof __dirname !== "undefined") bases.push(__dirname);
@@ -30615,6 +30913,7 @@ var Kernel = class {
30615
30913
  this.serviceRegistry.register(ISemesterGradeServiceToken, new SemesterGradeService(this.db));
30616
30914
  this.serviceRegistry.register(IPointsDimensionRegistryToken, new PointsDimensionRegistry());
30617
30915
  this.serviceRegistry.register(IPointsLedgerServiceToken, new PointsLedgerService(this.db));
30916
+ this.serviceRegistry.register(IClassRosterServiceToken, new ClassRosterService(this.db));
30618
30917
  this.serviceRegistry.register(ILessonEngineServiceToken, { getRuntime: async () => this.lessonRuntime });
30619
30918
  this.serviceRegistry.register(IClassroomRuntimeServiceToken, {
30620
30919
  getRuntimeKernel: async () => this.classroomRuntime
@@ -32808,21 +33107,29 @@ function validateStartupEnv(env = process.env) {
32808
33107
  if (isProduction) {
32809
33108
  const adminPassword = trimmed(env, "OPENLEARN_ADMIN_PASSWORD");
32810
33109
  if (adminPassword && adminPassword.toLowerCase() === "admin") {
32811
- errors.push({
33110
+ warnings.push({
32812
33111
  key: "OPENLEARN_ADMIN_PASSWORD",
32813
- severity: "error",
32814
- message: '\u751F\u4EA7\u73AF\u5883\u7981\u6B62\u628A OPENLEARN_ADMIN_PASSWORD \u8BBE\u4E3A\u7528\u6237\u540D "admin"\u3002\u8BF7\u8BBE\u7F6E\u5F3A\u53E3\u4EE4\u3002'
33112
+ severity: "warning",
33113
+ message: "OPENLEARN_ADMIN_PASSWORD \u4E0E\u7528\u6237\u540D\u76F8\u540C\u3002\u8FD9\u4E0D\u6BD4\u7559\u7A7A\u66F4\u5F31\uFF08\u7559\u7A7A\u65F6\u521D\u59CB\u53E3\u4EE4\u540C\u6837\u662F admin\uFF09\uFF0C\u4F46\u8BF7\u786E\u8BA4\u9996\u6B21\u767B\u5F55\u540E\u7ACB\u5373\u6539\u5BC6\uFF1B\u751F\u4EA7\u90E8\u7F72\u5EFA\u8BAE\u76F4\u63A5\u8BBE\u7F6E\u4E3A\u5F3A\u53E3\u4EE4\u3002"
32815
33114
  });
32816
33115
  }
32817
33116
  const teacherPassword = trimmed(env, "OPENLEARN_TEACHER_PASSWORD");
32818
33117
  if (teacherPassword && teacherPassword.toLowerCase() === "teacher") {
32819
- errors.push({
33118
+ warnings.push({
32820
33119
  key: "OPENLEARN_TEACHER_PASSWORD",
32821
- severity: "error",
32822
- message: '\u751F\u4EA7\u73AF\u5883\u7981\u6B62\u628A OPENLEARN_TEACHER_PASSWORD \u8BBE\u4E3A\u7528\u6237\u540D "teacher"\u3002\u8BF7\u8BBE\u7F6E\u5F3A\u53E3\u4EE4\u3002'
33120
+ severity: "warning",
33121
+ message: "OPENLEARN_TEACHER_PASSWORD \u4E0E\u7528\u6237\u540D\u76F8\u540C\u3002\u8BF7\u786E\u8BA4\u9996\u6B21\u767B\u5F55\u540E\u7ACB\u5373\u6539\u5BC6\uFF0C\u751F\u4EA7\u90E8\u7F72\u5EFA\u8BAE\u8BBE\u7F6E\u4E3A\u5F3A\u53E3\u4EE4\u3002"
32823
33122
  });
32824
33123
  }
32825
33124
  }
33125
+ const seedMode = trimmed(env, "OPENLEARN_SEED_PASSWORD_MODE");
33126
+ if (seedMode && !["default", "random"].includes(seedMode.toLowerCase())) {
33127
+ errors.push({
33128
+ key: "OPENLEARN_SEED_PASSWORD_MODE",
33129
+ severity: "error",
33130
+ message: `\u53EA\u652F\u6301 "default"\uFF08\u53E3\u4EE4 = \u7528\u6237\u540D\uFF0C\u9996\u6B21\u767B\u5F55\u5F3A\u5236\u6539\u5BC6\uFF09\u6216 "random"\uFF08\u968F\u673A\u53E3\u4EE4\u5199\u5165 bootstrap-credentials.txt\uFF09\uFF0C\u6536\u5230 "${seedMode}"\u3002`
33131
+ });
33132
+ }
32826
33133
  const metricsToken = trimmed(env, "METRICS_TOKEN");
32827
33134
  if (metricsToken && metricsToken.length < 16) {
32828
33135
  warnings.push({
@@ -45167,6 +45474,26 @@ function resolveWritableStudentId(session, rawId) {
45167
45474
  if (isTeacher(session) && teacherCanSeeStudent(kernelContainer.getReadDb(), session, studentId)) return studentId;
45168
45475
  return null;
45169
45476
  }
45477
+ var SEED_STATUS_TTL_MS = 15e3;
45478
+ var seedStatusCache = null;
45479
+ function readSeedDefaultStatus() {
45480
+ const now = Date.now();
45481
+ if (seedStatusCache && seedStatusCache.expiresAt > now) return seedStatusCache.payload;
45482
+ const accounts = [];
45483
+ try {
45484
+ const rows = kernelContainer.db.prepare("SELECT username, password_hash FROM users WHERE username IN (?, ?)").all("admin", "teacher");
45485
+ for (const row of rows) {
45486
+ if (verifyPassword(row.username, row.password_hash).valid) accounts.push(row.username);
45487
+ }
45488
+ } catch {
45489
+ }
45490
+ const payload = { active: accounts.length > 0, accounts };
45491
+ seedStatusCache = { payload, expiresAt: now + SEED_STATUS_TTL_MS };
45492
+ return payload;
45493
+ }
45494
+ function invalidateSeedStatusCache() {
45495
+ seedStatusCache = null;
45496
+ }
45170
45497
  function registerRosterRoutes(ctx) {
45171
45498
  const { app, io, loginLimiter } = ctx;
45172
45499
  const rosterService = new RosterService(kernelContainer.db, kernelContainer.getReadDb());
@@ -45448,6 +45775,7 @@ function registerRosterRoutes(ctx) {
45448
45775
  return res.status(400).json({ error: "New password must be different from the username" });
45449
45776
  }
45450
45777
  kernelContainer.db.prepare("UPDATE users SET password_hash = ?, must_change_password = 0 WHERE id = ?").run(hashPassword(newPassword), session.userId);
45778
+ invalidateSeedStatusCache();
45451
45779
  kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id != ? AND session_data LIKE ?").run(token, `%${session.userId}%`);
45452
45780
  try {
45453
45781
  const updatedSession = { ...session, mustChangePassword: false };
@@ -45615,6 +45943,13 @@ function registerRosterRoutes(ctx) {
45615
45943
  sendSafeError(res, e);
45616
45944
  }
45617
45945
  });
45946
+ app.get("/api/auth/seed-status", (_req, res) => {
45947
+ try {
45948
+ res.json(readSeedDefaultStatus());
45949
+ } catch (e) {
45950
+ sendSafeError(res, e);
45951
+ }
45952
+ });
45618
45953
  app.post("/api/auth/login", loginLimiter, (req, res) => {
45619
45954
  try {
45620
45955
  const { entrance, username, password, studentId } = req.body;