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.
- package/README.md +4 -2
- package/cli-data.mjs +1 -1
- package/dist/assets/{ActivityWorkspaceWidget-DbspLRxB.js → ActivityWorkspaceWidget-BU88GGUG.js} +1 -1
- package/dist/assets/{AdminDirectoryView-C2ZFIwHx.js → AdminDirectoryView-Cdsk9Mdr.js} +1 -1
- package/dist/assets/{AssessmentCenter-DbJAcTvr.js → AssessmentCenter-BoSWbJMt.js} +1 -1
- package/dist/assets/{AssignmentFilePreview-DEVTb4YJ.js → AssignmentFilePreview-D_POvLmv.js} +1 -1
- package/dist/assets/{BatchPickerModal-czw6eA6e.js → BatchPickerModal-DiHd_6Tg.js} +1 -1
- package/dist/assets/{ClassesView-B1F0tTtB.js → ClassesView-CWKqzWH0.js} +1 -1
- package/dist/assets/{ClassroomCountdownWidget-C9pv3GLy.js → ClassroomCountdownWidget-B5-KUVU8.js} +1 -1
- package/dist/assets/{CloudDriveModal-goDB6dlC.js → CloudDriveModal-CjcvdDiw.js} +1 -1
- package/dist/assets/{ComputerLabView-Csqiqvw2.js → ComputerLabView-CNTcmWAh.js} +1 -1
- package/dist/assets/{CourseManagement-BD-4z2OZ.js → CourseManagement-ha_iSnLU.js} +1 -1
- package/dist/assets/{CourseWizardModal-NXwhuph_.js → CourseWizardModal-oVT9mYSG.js} +1 -1
- package/dist/assets/{Dashboard-CFDWNYSw.js → Dashboard-Vb9coh9_.js} +1 -1
- package/dist/assets/{ExportWeightModal-dBxYVUP4.js → ExportWeightModal-BEesz6rd.js} +1 -1
- package/dist/assets/{HelpView-DSZC-1F0.js → HelpView--Um9DFYx.js} +1 -1
- package/dist/assets/{ImportLessonsModal-m_s44eus.js → ImportLessonsModal-DXGopj4j.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-C7faFOFD.js → InteractiveCoursewareViewer-DzgF9_LU.js} +1 -1
- package/dist/assets/{InteractiveWhiteboard-10fno-bJ.js → InteractiveWhiteboard-DJqgP_tU.js} +7 -7
- package/dist/assets/{LazyCourseware-DNn0O8lA.js → LazyCourseware-6yb_QiYS.js} +2 -2
- package/dist/assets/{LazyWhiteboard-BxpydfnE.js → LazyWhiteboard-CsF9GB6J.js} +2 -2
- package/dist/assets/{LessonEditorView-CMCgnrJD.js → LessonEditorView-CZTZ7Q0B.js} +1 -1
- package/dist/assets/{LiveClassroomView-BA0us3bY.js → LiveClassroomView-D2ugupT9.js} +1 -1
- package/dist/assets/{NotificationDetailModal-BMrt09PY.js → NotificationDetailModal-mUeIQP4R.js} +1 -1
- package/dist/assets/{PluginView-B6gCF1ZB.js → PluginView-CT3-zS2n.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-BNW6tZ78.js → QuizGeneratorModal-ViKxNX5Q.js} +1 -1
- package/dist/assets/{StudentAssignmentEvalPanel-DMwBSOXf.js → StudentAssignmentEvalPanel-BRxHMABY.js} +1 -1
- package/dist/assets/{StudentAssignmentView-cQpPme4_.js → StudentAssignmentView-g-ewJIS3.js} +1 -1
- package/dist/assets/{StudentLessonView-CpGgYV-b.js → StudentLessonView-Dbvr3Xlo.js} +1 -1
- package/dist/assets/{StudentView-BfXi6-NQ.js → StudentView-20vfEk4v.js} +2 -2
- package/dist/assets/{SystemResourceLibraryModal-d4vfqM_9.js → SystemResourceLibraryModal-CDKeaKf8.js} +1 -1
- package/dist/assets/{TeacherView-DSJCvFE-.js → TeacherView-KIKuhXGF.js} +2 -2
- package/dist/assets/{TimetableView-D9Rhnvfj.js → TimetableView-7V2AwWml.js} +1 -1
- package/dist/assets/{index-DCnGJbif.css → index-C4cHQZyx.css} +1 -1
- package/dist/assets/{index-DuxQtTr9.js → index-XduPa2-x.js} +31 -31
- package/dist/assets/{pluginParsers-HRw6CVog.js → pluginParsers-BO6U1E-h.js} +1 -1
- package/dist/assets/{student-workspace-DO1mSl_I.js → student-workspace-DstiW0pM.js} +1 -1
- package/dist/env-validation.cjs +14 -6
- package/dist/index.html +2 -2
- package/dist/server.cjs +681 -346
- 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
|
|
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
|
-
|
|
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/
|
|
4248
|
-
function
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
-
|
|
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
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4261
|
-
|
|
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
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
|
|
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
|
|
4279
|
-
const
|
|
4280
|
-
if (
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
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
|
|
4288
|
-
var
|
|
4289
|
-
"packages/core/plugin-host/
|
|
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
|
-
|
|
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
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10881
|
+
CAP.LESSON_WRITE,
|
|
10882
|
+
CAP.LESSON_DELETE,
|
|
10883
|
+
CAP.WHITEBOARD_WRITE,
|
|
10699
10884
|
"quiz:write",
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
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", [
|
|
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
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
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
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
33110
|
+
warnings.push({
|
|
32812
33111
|
key: "OPENLEARN_ADMIN_PASSWORD",
|
|
32813
|
-
severity: "
|
|
32814
|
-
message:
|
|
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
|
-
|
|
33118
|
+
warnings.push({
|
|
32820
33119
|
key: "OPENLEARN_TEACHER_PASSWORD",
|
|
32821
|
-
severity: "
|
|
32822
|
-
message:
|
|
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;
|