openlearn-next 0.4.2 → 0.7.3

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 (149) hide show
  1. package/README.md +26 -21
  2. package/cli-data.mjs +535 -40
  3. package/cli-doctor.mjs +456 -17
  4. package/cli.mjs +214 -12
  5. package/dist/assets/AcademicYearFilter-DUUGpDKt.js +1 -0
  6. package/dist/assets/ActivityWorkspaceWidget-5PzyxF6a.js +1 -0
  7. package/dist/assets/AdminDirectoryView-BCtmQTEK.js +5 -0
  8. package/dist/assets/AssessmentCenter-B43ojHG-.js +1 -0
  9. package/dist/assets/BatchPickerModal-CbmjInp7.js +1 -0
  10. package/dist/assets/ClassesView-DEnMaDOt.js +32 -0
  11. package/dist/assets/ClassroomCountdownWidget-CHq4983w.js +2 -0
  12. package/dist/assets/CloudDriveModal-D2t3Uyzr.js +1 -0
  13. package/dist/assets/ComputerLabView-YtnoT5Rb.js +1 -0
  14. package/dist/assets/CourseManagement-DOXyJ_j-.js +1 -0
  15. package/dist/assets/CourseWizardModal-Dbi6Caw2.js +3 -0
  16. package/dist/assets/CoursewareHubPanel-CyxDIJAF.js +1 -0
  17. package/dist/assets/Dashboard-2rLu_ZBy.js +8 -0
  18. package/dist/assets/ExportWeightModal-BrmAh6VX.js +1 -0
  19. package/dist/assets/GLTFLoader-CZJbo_y1.js +1 -0
  20. package/dist/assets/HelpTour-D_q1z_MI.js +1 -0
  21. package/dist/assets/HelpView-CVBrKDrd.js +204 -0
  22. package/dist/assets/ImportLessonsModal-PhHtX31R.js +1 -0
  23. package/dist/assets/ImportModal-DqjqNeH4.js +2 -0
  24. package/dist/assets/InteractiveCoursewareViewer-Vi6lLz7a.js +1 -0
  25. package/dist/assets/InteractiveWhiteboard-lTzeayX5.js +65 -0
  26. package/dist/assets/LazyCourseware-KIt0rGtW.js +2 -0
  27. package/dist/assets/LazyWhiteboard-CCN3pYcW.js +2 -0
  28. package/dist/assets/LessonEditorView-D_GVhHuM.js +1 -0
  29. package/dist/assets/LiveClassroomView-BRsZ6QEq.js +61 -0
  30. package/dist/assets/NotificationDetailModal-tY5rxYCN.js +1 -0
  31. package/dist/assets/OrbitControls-CQjN_uwS.js +1 -0
  32. package/dist/assets/PluginView-CedpAW6d.js +77 -0
  33. package/dist/assets/ProcessLogsModal-Cfr-T9Gd.js +1 -0
  34. package/dist/assets/QuizGeneratorModal-bUA002kP.js +1 -0
  35. package/dist/assets/StudentAssignmentView-BhAz6Hfk.js +1 -0
  36. package/dist/assets/StudentLessonView-DC1yX4kG.js +1 -0
  37. package/dist/assets/StudentView-PHKxIV9T.js +2 -0
  38. package/dist/assets/SystemResourceLibraryModal-DgXYe6_x.js +1 -0
  39. package/dist/assets/TeacherView-DscJtOJ9.js +2 -0
  40. package/dist/assets/TimetableView-D2ozoj_D.js +6 -0
  41. package/dist/assets/UTIF-FfCsrHhe.js +1 -0
  42. package/dist/assets/__vite-optional-peer-dep_ai_ooxml-ui-BIHI7g3E.js +1 -0
  43. package/dist/assets/browser-DEI0QxZC.js +1 -0
  44. package/dist/assets/html2canvas-pro.esm-Bo50djkB.js +12 -0
  45. package/dist/assets/index-2676X8YB.js +131 -0
  46. package/dist/assets/index-C6hlh8pw.css +1 -0
  47. package/dist/assets/{index.es-C5eWEO4o.js → index.es-DyEF0CZ9.js} +1 -1
  48. package/dist/assets/pluginParsers-e4PSm2Iy.js +1 -0
  49. package/dist/assets/react_pptx-react-viewer-BIHI7g3E.js +1 -0
  50. package/dist/assets/teaching-modes-client-BeVaDdJ-.js +1 -0
  51. package/dist/assets/three.module-iOYB3rD4.js +4228 -0
  52. package/dist/assets/timeline-duration-BgdJCSrW.js +1 -0
  53. package/dist/assets/{vendor-charts-qLbgF1eL.js → vendor-charts-SrpBsvM3.js} +2 -2
  54. package/dist/assets/{vendor-icons-BRmhY6fg.js → vendor-icons-CXrZ3R2V.js} +2 -2
  55. package/dist/assets/{vendor-konva-CEOq4dcl.js → vendor-konva-Dne0Z-WC.js} +1 -1
  56. package/dist/assets/{vendor-pdf-D7rGevI1.js → vendor-pdf-C1Vh2Aqq.js} +56 -56
  57. package/dist/assets/vendor-pptx-Bzn6whfK.js +11311 -0
  58. package/dist/assets/vendor-pptx-DIWdXF1L.css +1 -0
  59. package/dist/assets/{vendor-react-B0t7H3hh.js → vendor-react-Kv_JATwr.js} +1 -1
  60. package/dist/assets/vendor-utils-aM7aYvPP.js +1 -0
  61. package/dist/cli-migrator.cjs +310 -0
  62. package/dist/env-validation.cjs +351 -0
  63. package/dist/index.html +13 -10
  64. package/dist/plugin-net-preload.cjs +263 -0
  65. package/dist/server.cjs +51140 -27229
  66. package/migrations/000_initial_schema.sql +410 -0
  67. package/migrations/001_add_execution_mode.sql +6 -0
  68. package/migrations/002_add_client_session_expiry.sql +6 -0
  69. package/migrations/003_classroom_runtime.sql +33 -0
  70. package/migrations/004_courseware_score_config.sql +26 -0
  71. package/migrations/005_assignment_hub.sql +155 -0
  72. package/migrations/006_classroom_event_bus.sql +28 -0
  73. package/migrations/007_interactive_classroom.sql +119 -0
  74. package/migrations/008_teaching_modes.sql +36 -0
  75. package/migrations/009_classroom_peer_review.sql +159 -0
  76. package/migrations/010_class_groups.sql +33 -0
  77. package/migrations/011_classroom_feed.sql +34 -0
  78. package/migrations/012_auto_record_score.sql +48 -0
  79. package/migrations/013_score_source_strategy.sql +81 -0
  80. package/migrations/014_performance_indexes.sql +21 -0
  81. package/migrations/015_demo_data_registry.sql +26 -0
  82. package/migrations/016_preset_polls_and_passcode_expiry.sql +33 -0
  83. package/migrations/017_pacing_signals_segment.sql +6 -0
  84. package/migrations/018_processes_plugin_owner.sql +26 -0
  85. package/migrations/019_plugins_version.sql +61 -0
  86. package/migrations/020_upload_max_file_mb.sql +14 -0
  87. package/migrations/021_class_owner_and_password_gate.sql +11 -0
  88. package/migrations/022_plugin_public_routes.sql +14 -0
  89. package/migrations/023_encrypt_class_passcodes.sql +18 -0
  90. package/migrations/024_segment_gates.sql +37 -0
  91. package/migrations/025_assignment_lookup_indexes.sql +11 -0
  92. package/migrations/026_plugin_grants.sql +21 -0
  93. package/migrations/027_academic_year.sql +26 -0
  94. package/migrations/028_classroom_indexes.sql +14 -0
  95. package/migrations/029_clean_lesson_think_artifacts.sql +13 -0
  96. package/migrations/030_backfill_academic_year.sql +30 -0
  97. package/migrations/031_assessment_core.sql +158 -0
  98. package/migrations/032_progress_verification.sql +17 -0
  99. package/migrations/033_student_lock_mode.sql +9 -0
  100. package/migrations/034_plugin_orphan_data.sql +27 -0
  101. package/migrations/035_icebreaker_checkins.sql +26 -0
  102. package/migrations/036_student_read_notifications_created_at.sql +19 -0
  103. package/migrations/037_plugin_config_scopes.sql +40 -0
  104. package/migrations/038_assessment_ai_grading_suggestion.sql +10 -0
  105. package/migrations/039_question_bank_sharing_and_papers.sql +14 -0
  106. package/migrations/040_assignment_grade_history.sql +37 -0
  107. package/migrations/041_course_package_idempotency.sql +33 -0
  108. package/migrations/042_assignment_grade_resubmit_audit.sql +12 -0
  109. package/migrations/043_mark_sha256_passwords.sql +31 -0
  110. package/migrations/README.md +61 -0
  111. package/migrations/schema-golden.json +640 -0
  112. package/package.json +54 -28
  113. package/dist/assets/AdminDirectoryView-DY--WM5Z.js +0 -1
  114. package/dist/assets/BatchPickerModal-BpWu0Lil.js +0 -1
  115. package/dist/assets/ClassesView-KbHEqJto.js +0 -22
  116. package/dist/assets/ClassroomCountdownWidget-96WFRO9A.js +0 -2
  117. package/dist/assets/CloudDriveModal-DEQ1nkNk.js +0 -1
  118. package/dist/assets/ComputerLabView-1qWDNRQ2.js +0 -1
  119. package/dist/assets/CourseManagement-D3ZpxY0l.js +0 -1
  120. package/dist/assets/CourseWizardModal-Ck_-M8pk.js +0 -3
  121. package/dist/assets/CoursewareHubPanel-CaEizhxl.js +0 -1
  122. package/dist/assets/Dashboard-CYbpbofV.js +0 -8
  123. package/dist/assets/ExportWeightModal-DW2bu9Zz.js +0 -1
  124. package/dist/assets/HelpTour-CMJbbGne.js +0 -1
  125. package/dist/assets/HelpView-CtzdqhBq.js +0 -204
  126. package/dist/assets/ImportLessonsModal-jBuIK453.js +0 -1
  127. package/dist/assets/ImportModal-D7L-KNhZ.js +0 -1
  128. package/dist/assets/InteractiveCoursewareViewer-Bj7ahXMs.js +0 -1
  129. package/dist/assets/InteractiveWhiteboard-B7JcJvDu.js +0 -77
  130. package/dist/assets/LazyCourseware-CuiPxhDg.js +0 -2
  131. package/dist/assets/LazyWhiteboard-BJwCEZAU.js +0 -2
  132. package/dist/assets/LessonEditorView-DFrOp2qp.js +0 -1
  133. package/dist/assets/LiveClassroomView-BZ5nGee3.js +0 -59
  134. package/dist/assets/NotificationDetailModal-Baqnc5pU.js +0 -1
  135. package/dist/assets/PluginView-CCbIuZ24.js +0 -66
  136. package/dist/assets/ProcessLogsModal-DmFXg9mG.js +0 -1
  137. package/dist/assets/QuizGeneratorModal-BBEDssp7.js +0 -1
  138. package/dist/assets/StudentAssignmentView-DgIjUsXz.js +0 -1
  139. package/dist/assets/StudentLessonView-CBJbI5x6.js +0 -1
  140. package/dist/assets/StudentPreviewModal-Bh8OZs0t.js +0 -1
  141. package/dist/assets/StudentView--uGxc5TJ.js +0 -2
  142. package/dist/assets/SystemResourceLibraryModal-BVCclcqA.js +0 -1
  143. package/dist/assets/TeacherView-D4Zsc0eZ.js +0 -2
  144. package/dist/assets/TimetableView-BQG-rEb5.js +0 -6
  145. package/dist/assets/index-BKBCBKvy.js +0 -217
  146. package/dist/assets/index-CvGu7LSj.css +0 -1
  147. package/dist/assets/purify.es-CYR4BTuT.js +0 -3
  148. package/dist/assets/vendor-pptx-C5-U7ieS.js +0 -39
  149. package/dist/assets/vendor-utils-m0Urvtws.js +0 -1
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/core/db/migrator.ts
31
+ var migrator_exports = {};
32
+ __export(migrator_exports, {
33
+ executeSqlStatements: () => executeSqlStatements,
34
+ getDefaultMigrationsDir: () => getDefaultMigrationsDir,
35
+ loadMigrationsFromDirectory: () => loadMigrationsFromDirectory,
36
+ parseMigrationSql: () => parseMigrationSql,
37
+ rollbackMigration: () => rollbackMigration,
38
+ runMigrations: () => runMigrations,
39
+ simpleChecksum: () => simpleChecksum,
40
+ splitSqlStatements: () => splitSqlStatements,
41
+ sqlMigration: () => sqlMigration
42
+ });
43
+ module.exports = __toCommonJS(migrator_exports);
44
+ var import_fs = __toESM(require("fs"), 1);
45
+ var import_path = __toESM(require("path"), 1);
46
+ var import_url = require("url");
47
+
48
+ // packages/core/db/backup-manager.ts
49
+ var import_node_fs2 = __toESM(require("node:fs"), 1);
50
+ var import_node_path = __toESM(require("node:path"), 1);
51
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
52
+
53
+ // packages/core/db/secure-files.ts
54
+ var import_node_fs = __toESM(require("node:fs"), 1);
55
+ var SECURE_FILE_MODE = 384;
56
+ var SECURE_DIR_MODE = 448;
57
+ var GROUP_OTHER_BITS = 63;
58
+ function currentMode(filePath) {
59
+ try {
60
+ return import_node_fs.default.statSync(filePath).mode & 511;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ function hardenFile(filePath) {
66
+ const mode = currentMode(filePath);
67
+ if (mode === null || (mode & GROUP_OTHER_BITS) === 0) return false;
68
+ try {
69
+ import_node_fs.default.chmodSync(filePath, SECURE_FILE_MODE);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function hardenDir(dirPath) {
76
+ const mode = currentMode(dirPath);
77
+ if (mode === null || (mode & GROUP_OTHER_BITS) === 0) return false;
78
+ try {
79
+ import_node_fs.default.chmodSync(dirPath, SECURE_DIR_MODE);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ function ensureSecureDir(dirPath) {
86
+ try {
87
+ import_node_fs.default.mkdirSync(dirPath, { recursive: true, mode: SECURE_DIR_MODE });
88
+ } catch {
89
+ }
90
+ hardenDir(dirPath);
91
+ }
92
+
93
+ // packages/core/db/backup-manager.ts
94
+ function getDefaultBackupDir() {
95
+ if (process.env.BACKUP_DIR) {
96
+ return import_node_path.default.resolve(process.cwd(), process.env.BACKUP_DIR);
97
+ }
98
+ return import_node_path.default.resolve(process.cwd(), "backups");
99
+ }
100
+ function pruneByPrefix(backupDir, prefix, keepCount) {
101
+ if (keepCount <= 0 || !import_node_fs2.default.existsSync(backupDir)) return 0;
102
+ const files = import_node_fs2.default.readdirSync(backupDir).filter((f) => f.startsWith(prefix) && f.endsWith(".db")).sort().reverse();
103
+ if (files.length <= keepCount) return 0;
104
+ let pruned = 0;
105
+ for (const file of files.slice(keepCount)) {
106
+ try {
107
+ import_node_fs2.default.unlinkSync(import_node_path.default.join(backupDir, file));
108
+ pruned++;
109
+ } catch {
110
+ }
111
+ }
112
+ return pruned;
113
+ }
114
+ function resolvePreMigrationKeep() {
115
+ const n = Number(process.env.PRE_MIGRATION_KEEP ?? 5);
116
+ const keep = Number.isFinite(n) ? Math.floor(n) : 5;
117
+ return Math.max(1, keep);
118
+ }
119
+ function pruneMigratorSnapshots(backupDir = getDefaultBackupDir(), keepCount) {
120
+ return pruneByPrefix(backupDir, "pre_migration_", Math.max(1, keepCount ?? resolvePreMigrationKeep()));
121
+ }
122
+
123
+ // packages/core/db/migrator.ts
124
+ var import_meta = {};
125
+ function splitSqlStatements(sql) {
126
+ return sql.split(";").map((s) => s.trim()).filter((s) => {
127
+ const content = s.replace(/--.*$/gm, "").trim();
128
+ return content.length > 0;
129
+ });
130
+ }
131
+ function parseMigrationSql(name, content) {
132
+ const upMatch = content.match(/--\s*UP([\s\S]*?)(?=--\s*DOWN|$)/i);
133
+ const downMatch = content.match(/--\s*DOWN([\s\S]*)$/i);
134
+ const up = upMatch ? upMatch[1].trim() : downMatch ? content.slice(0, downMatch.index).trim() : content.trim();
135
+ const down = downMatch ? downMatch[1].trim() : "";
136
+ return { name, up, down };
137
+ }
138
+ function loadMigrationsFromDirectory(dirPath) {
139
+ if (!import_fs.default.existsSync(dirPath)) {
140
+ return [];
141
+ }
142
+ const files = import_fs.default.readdirSync(dirPath).filter((f) => f.endsWith(".sql")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" }));
143
+ return files.map((file) => {
144
+ const fullPath = import_path.default.join(dirPath, file);
145
+ const content = import_fs.default.readFileSync(fullPath, "utf8");
146
+ const name = import_path.default.basename(file, ".sql");
147
+ return parseMigrationSql(name, content);
148
+ });
149
+ }
150
+ function executeSqlStatements(db, sql) {
151
+ const statements = splitSqlStatements(sql);
152
+ for (const stmt of statements) {
153
+ try {
154
+ db.exec(stmt);
155
+ } catch (err) {
156
+ if (err?.message && err.message.includes("duplicate column name")) {
157
+ continue;
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ }
163
+ function simpleChecksum(text) {
164
+ let hash = 0;
165
+ for (let i = 0; i < text.length; i++) {
166
+ const char = text.charCodeAt(i);
167
+ hash = (hash << 5) - hash + char;
168
+ hash |= 0;
169
+ }
170
+ return hash.toString(16);
171
+ }
172
+ function runMigrations(db, migrations) {
173
+ db.exec(`
174
+ CREATE TABLE IF NOT EXISTS _migrations (
175
+ name TEXT PRIMARY KEY,
176
+ applied_at INTEGER NOT NULL,
177
+ checksum TEXT NOT NULL
178
+ );
179
+ `);
180
+ const appliedRows = db.prepare("SELECT name, checksum FROM _migrations").all();
181
+ const applied = new Map(appliedRows.map((r) => [r.name, r.checksum]));
182
+ const byName = new Map(migrations.map((m) => [m.name, m]));
183
+ const drift = [];
184
+ for (const [name, stored] of applied) {
185
+ const migration = byName.get(name);
186
+ if (!migration) continue;
187
+ const actual = simpleChecksum(migration.up);
188
+ if (actual !== stored) {
189
+ drift.push({ name, stored, actual });
190
+ }
191
+ }
192
+ if (drift.length > 0) {
193
+ const detail = drift.map((d) => ` - ${d.name}: \u8BB0\u5F55 ${d.stored} \u2192 \u5F53\u524D\u6587\u4EF6 ${d.actual}`).join("\n");
194
+ const msg = `[Migration] \u5DF2\u6267\u884C\u8FC1\u79FB\u7684 UP \u811A\u672C\u5728\u5E94\u7528\u540E\u88AB\u4FEE\u6539\u8FC7\uFF1A
195
+ ${detail}
196
+ \u8FD9\u4E0D\u4F1A\u81EA\u52A8\u4FEE\u590D\u6570\u636E\u5E93 \u2014\u2014 \u5DF2\u6267\u884C\u7684\u90E8\u5206\u65E0\u6CD5\u56DE\u9000\u3002\u8BF7\u786E\u8BA4\u662F\u6709\u610F\u4FEE\u6539\uFF0C\u6216\u8865\u4E00\u6761\u65B0\u7684\u8FC1\u79FB\u6765\u4FEE\u6B63\u3002`;
197
+ if (process.env.MIGRATION_CHECKSUM_STRICT === "true") {
198
+ throw new Error(msg);
199
+ }
200
+ console.error(msg);
201
+ }
202
+ const pending = migrations.filter((m) => !applied.has(m.name));
203
+ if (pending.length > 0 && db.name && db.name !== ":memory:" && (process.env.FORCE_MIGRATION_SNAPSHOT === "true" || !process.env.VITEST)) {
204
+ try {
205
+ const backupDir = process.env.BACKUP_DIR || import_path.default.join(import_path.default.dirname(db.name), "backups");
206
+ ensureSecureDir(backupDir);
207
+ const now = /* @__PURE__ */ new Date();
208
+ const pad = (n) => String(n).padStart(2, "0");
209
+ const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
210
+ const backupFile = import_path.default.join(backupDir, `pre_migration_${timestamp}.db`);
211
+ const escaped = backupFile.replace(/'/g, "''");
212
+ db.exec(`VACUUM INTO '${escaped}'`);
213
+ hardenFile(backupFile);
214
+ try {
215
+ const pruned = pruneMigratorSnapshots(backupDir);
216
+ if (pruned > 0) console.log(`[Migration] Pruned ${pruned} old pre-migration snapshot(s)`);
217
+ } catch (pErr) {
218
+ console.warn("[Migration] Warning: failed to prune old pre-migration snapshots:", pErr);
219
+ }
220
+ console.log(`[Migration] Safety snapshot created before applying ${pending.length} migration(s): ${backupFile}`);
221
+ } catch (bErr) {
222
+ const message = bErr instanceof Error ? bErr.message : String(bErr);
223
+ if (!process.env.VITEST) {
224
+ throw new Error(`[Migration] \u8FC1\u79FB\u524D\u5FEB\u7167\u5931\u8D25\uFF0C\u5DF2\u4E2D\u6B62\u4E14\u672A\u5E94\u7528\u4EFB\u4F55\u5F85\u6267\u884C SQL: ${message}`, { cause: bErr });
225
+ }
226
+ console.warn("[Migration] Warning: Could not create pre-migration snapshot:", message);
227
+ }
228
+ }
229
+ for (const migration of migrations) {
230
+ if (applied.has(migration.name)) continue;
231
+ const applyOne = db.transaction(() => {
232
+ executeSqlStatements(db, migration.up);
233
+ db.prepare("INSERT INTO _migrations (name, applied_at, checksum) VALUES (?, ?, ?)").run(
234
+ migration.name,
235
+ Date.now(),
236
+ simpleChecksum(migration.up)
237
+ );
238
+ });
239
+ try {
240
+ applyOne();
241
+ } catch (err) {
242
+ console.error(`[Migration] FAILED: ${migration.name}`, err);
243
+ throw err;
244
+ }
245
+ }
246
+ return drift;
247
+ }
248
+ function rollbackMigration(db, migration) {
249
+ if (!migration.down || migration.down.trim() === "") {
250
+ throw new Error(`Migration ${migration.name} does not provide a DOWN rollback script.`);
251
+ }
252
+ executeSqlStatements(db, migration.down);
253
+ db.prepare("DELETE FROM _migrations WHERE name = ?").run(migration.name);
254
+ }
255
+ function sqlMigration(name, up, down = "") {
256
+ return { name, up, down };
257
+ }
258
+ function getDefaultMigrationsDir(options = {}) {
259
+ const env = options.env ?? process.env;
260
+ const cwd = options.cwd ?? process.cwd();
261
+ const hasSql = (dir) => {
262
+ try {
263
+ return import_fs.default.existsSync(dir) && import_fs.default.readdirSync(dir).some((f) => f.endsWith(".sql"));
264
+ } catch {
265
+ return false;
266
+ }
267
+ };
268
+ const override = env.OPENLEARN_MIGRATIONS_DIR;
269
+ if (override && hasSql(override)) return import_path.default.resolve(override);
270
+ const starts = options.startDirs ? [...options.startDirs] : [];
271
+ if (!options.startDirs && typeof __dirname !== "undefined") starts.push(__dirname);
272
+ try {
273
+ if (!options.startDirs && typeof import_meta !== "undefined" && import_meta.url) {
274
+ starts.push(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta.url)));
275
+ }
276
+ } catch {
277
+ }
278
+ for (const start of starts) {
279
+ let dir = import_path.default.resolve(start);
280
+ for (; ; ) {
281
+ const pkgPath = import_path.default.join(dir, "package.json");
282
+ if (import_fs.default.existsSync(pkgPath)) {
283
+ try {
284
+ const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
285
+ if (pkg.name === "openlearn-next") {
286
+ const candidate = import_path.default.join(dir, "migrations");
287
+ if (hasSql(candidate)) return candidate;
288
+ }
289
+ } catch {
290
+ }
291
+ }
292
+ const parent = import_path.default.dirname(dir);
293
+ if (parent === dir) break;
294
+ dir = parent;
295
+ }
296
+ }
297
+ return import_path.default.resolve(cwd, "migrations");
298
+ }
299
+ // Annotate the CommonJS export names for ESM import in node:
300
+ 0 && (module.exports = {
301
+ executeSqlStatements,
302
+ getDefaultMigrationsDir,
303
+ loadMigrationsFromDirectory,
304
+ parseMigrationSql,
305
+ rollbackMigration,
306
+ runMigrations,
307
+ simpleChecksum,
308
+ splitSqlStatements,
309
+ sqlMigration
310
+ });
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // server/security/env-validation.ts
31
+ var env_validation_exports = {};
32
+ __export(env_validation_exports, {
33
+ assertStartupEnv: () => assertStartupEnv,
34
+ formatEnvIssues: () => formatEnvIssues,
35
+ validateStartupEnv: () => validateStartupEnv
36
+ });
37
+ module.exports = __toCommonJS(env_validation_exports);
38
+ var import_node_net = __toESM(require("node:net"), 1);
39
+ var NODE_ENVS = ["development", "production", "test"];
40
+ var ISOLATE_KINDS = ["thread", "process"];
41
+ var PERMISSION_POLICIES = ["rw", "ro", "off"];
42
+ var LIVENESS_VALUES = ["on", "off"];
43
+ var TELEMETRY_VALUES = ["on", "off"];
44
+ var QUOTA_INT_KEYS = [
45
+ "OPENLEARN_PLUGIN_MAX_RSS_MB",
46
+ "OPENLEARN_PLUGIN_CPU_QUOTA_MS",
47
+ "OPENLEARN_PLUGIN_CPU_WINDOW_MS"
48
+ ];
49
+ var QUOTA_SWITCH_KEYS = ["OPENLEARN_PLUGIN_QUOTA"];
50
+ var LIVENESS_INT_KEYS = ["OPENLEARN_WORKER_LIVENESS_INTERVAL_MS", "OPENLEARN_WORKER_LIVENESS_GRACE_MS"];
51
+ var HEX_64 = /^[0-9a-fA-F]{64}$/;
52
+ function trimmed(env, key) {
53
+ const raw = env[key];
54
+ if (raw === void 0 || raw === null) return void 0;
55
+ const value = String(raw).trim();
56
+ return value === "" ? void 0 : value;
57
+ }
58
+ function unquote(value) {
59
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
60
+ return value.slice(1, -1).trim();
61
+ }
62
+ return value;
63
+ }
64
+ function isValidTrustProxyToken(token) {
65
+ const value = token.trim();
66
+ if (value === "") return false;
67
+ if (/^\d+$/.test(value)) return Number(value) >= 0;
68
+ if (value === "true" || value === "false") return true;
69
+ if (value === "loopback" || value === "linklocal" || value === "uniquelocal") return true;
70
+ if (import_node_net.default.isIP(value) !== 0) return true;
71
+ const cidr = /^(.+)\/(\d+)$/.exec(value);
72
+ if (cidr && import_node_net.default.isIP(cidr[1]) !== 0) {
73
+ const prefix = Number(cidr[2]);
74
+ const maxPrefix = import_node_net.default.isIPv6(cidr[1]) ? 128 : 32;
75
+ return prefix >= 0 && prefix <= maxPrefix;
76
+ }
77
+ return false;
78
+ }
79
+ function isValidOriginPattern(entry) {
80
+ const value = entry.trim();
81
+ if (value === "") return false;
82
+ if (value === "*") return true;
83
+ let parsed;
84
+ try {
85
+ parsed = new URL(value.replace(":*", ":0"));
86
+ } catch {
87
+ return false;
88
+ }
89
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
90
+ if (!parsed.hostname) return false;
91
+ return true;
92
+ }
93
+ function splitList(value) {
94
+ if (!value) return [];
95
+ return value.split(",").map((part) => part.trim()).filter(Boolean);
96
+ }
97
+ function validateStartupEnv(env = process.env) {
98
+ const errors = [];
99
+ const warnings = [];
100
+ const isProduction = env.NODE_ENV === "production";
101
+ const nodeEnv = trimmed(env, "NODE_ENV");
102
+ if (nodeEnv && !NODE_ENVS.includes(nodeEnv)) {
103
+ errors.push({
104
+ key: "NODE_ENV",
105
+ severity: "error",
106
+ message: `NODE_ENV \u5FC5\u987B\u662F ${NODE_ENVS.join(" / ")} \u4E4B\u4E00\uFF0C\u5B9E\u9645\u4E3A "${nodeEnv}"\u3002`
107
+ });
108
+ }
109
+ const port = trimmed(env, "PORT");
110
+ if (port && !/^\d+$/.test(port)) {
111
+ errors.push({ key: "PORT", severity: "error", message: `PORT \u5FC5\u987B\u662F\u6574\u6570\uFF0C\u5B9E\u9645\u4E3A "${port}"\u3002` });
112
+ } else if (port) {
113
+ const parsedPort = Number(port);
114
+ if (parsedPort < 1 || parsedPort > 65535) {
115
+ errors.push({
116
+ key: "PORT",
117
+ severity: "error",
118
+ message: `PORT \u5FC5\u987B\u5728 1-65535 \u4E4B\u95F4\uFF0C\u5B9E\u9645\u4E3A ${parsedPort}\u3002`
119
+ });
120
+ }
121
+ }
122
+ const trustProxy = trimmed(env, "TRUST_PROXY");
123
+ if (trustProxy) {
124
+ const tokens = splitList(trustProxy);
125
+ const invalid = tokens.filter((token) => !isValidTrustProxyToken(token));
126
+ if (tokens.length === 0 || invalid.length > 0) {
127
+ errors.push({
128
+ key: "TRUST_PROXY",
129
+ severity: "error",
130
+ message: `TRUST_PROXY \u542B\u65E0\u6CD5\u8BC6\u522B\u7684\u53D6\u503C ${invalid.length ? invalid.map((t) => `"${t}"`).join(", ") : `"${trustProxy}"`}\u3002 \u5141\u8BB8\uFF1A0/false\u30011/true\u3001\u975E\u8D1F\u6574\u6570\u3001loopback/linklocal/uniquelocal\u3001\u5177\u4F53 IP\u3001\u9017\u53F7\u5206\u9694\u7EC4\u5408\u3002`
131
+ });
132
+ }
133
+ if (isProduction && tokens.some((token) => token === "true" || token === "1")) {
134
+ warnings.push({
135
+ key: "TRUST_PROXY",
136
+ severity: "warning",
137
+ message: "\u751F\u4EA7\u73AF\u5883\u4FE1\u4EFB\u6240\u6709\u4E0A\u6E38\u4EE3\u7406\uFF08TRUST_PROXY=1/true\uFF09\u3002\u82E5\u670D\u52A1\u53EF\u88AB\u516C\u7F51\u76F4\u8FDE\uFF0C\u5BA2\u6237\u7AEF\u53EF\u4F2A\u9020 X-Forwarded-For \u7ED5\u8FC7\u9650\u6D41\u3002"
138
+ });
139
+ }
140
+ }
141
+ const encryptionKey = trimmed(env, "ENCRYPTION_KEY");
142
+ if (encryptionKey) {
143
+ const value = unquote(encryptionKey);
144
+ if (!HEX_64.test(value)) {
145
+ errors.push({
146
+ key: "ENCRYPTION_KEY",
147
+ severity: "error",
148
+ message: "ENCRYPTION_KEY \u5FC5\u987B\u662F 64 \u4F4D\u5341\u516D\u8FDB\u5236\uFF0832 \u5B57\u8282\uFF09\u3002\u4E0D\u8981\u4F7F\u7528\u77ED\u5BC6\u94A5\u6216\u5360\u4F4D\u7B26\u3002"
149
+ });
150
+ }
151
+ } else if (isProduction) {
152
+ warnings.push({
153
+ key: "ENCRYPTION_KEY",
154
+ severity: "warning",
155
+ message: "\u672A\u8BBE\u7F6E ENCRYPTION_KEY\uFF0C\u5C06\u4F7F\u7528/\u751F\u6210 storage/encryption-key\u3002\u751F\u4EA7\u73AF\u5883\u5EFA\u8BAE\u663E\u5F0F\u8BBE\u7F6E\u5E76\u79BB\u7EBF\u5907\u4EFD\uFF0C\u5426\u5219\u4E22\u5931\u8BE5\u6587\u4EF6\u5C06\u65E0\u6CD5\u89E3\u5BC6 AI Key \u4E0E\u73ED\u7EA7\u53E3\u4EE4\u3002"
156
+ });
157
+ }
158
+ const isolate = trimmed(env, "OPENLEARN_WORKER_ISOLATE");
159
+ if (isolate && !ISOLATE_KINDS.includes(isolate.toLowerCase())) {
160
+ errors.push({
161
+ key: "OPENLEARN_WORKER_ISOLATE",
162
+ severity: "error",
163
+ message: `OPENLEARN_WORKER_ISOLATE \u5FC5\u987B\u662F ${ISOLATE_KINDS.join(" / ")}\uFF0C\u5B9E\u9645\u4E3A "${isolate}"\u3002`
164
+ });
165
+ }
166
+ const permission = trimmed(env, "OPENLEARN_PLUGIN_PERMISSION");
167
+ if (permission && !PERMISSION_POLICIES.includes(permission.toLowerCase())) {
168
+ errors.push({
169
+ key: "OPENLEARN_PLUGIN_PERMISSION",
170
+ severity: "error",
171
+ message: `OPENLEARN_PLUGIN_PERMISSION \u5FC5\u987B\u662F ${PERMISSION_POLICIES.join(" / ")}\uFF0C\u5B9E\u9645\u4E3A "${permission}"\u3002 \u62FC\u9519\u4F1A\u9759\u9ED8\u56DE\u843D\u9ED8\u8BA4\u7B56\u7565\uFF0C\u56E0\u6B64\u62D2\u7EDD\u542F\u52A8\u800C\u4E0D\u662F\u7EE7\u7EED\u3002`
172
+ });
173
+ }
174
+ const heap = trimmed(env, "OPENLEARN_PLUGIN_MAX_HEAP_MB");
175
+ if (heap) {
176
+ const lower = heap.toLowerCase();
177
+ if (lower !== "off" && lower !== "none" && lower !== "0") {
178
+ const match = /^(\d+)$/.exec(heap);
179
+ const mb = match ? Number(match[1]) : NaN;
180
+ if (!match || mb < 32) {
181
+ errors.push({
182
+ key: "OPENLEARN_PLUGIN_MAX_HEAP_MB",
183
+ severity: "error",
184
+ message: `OPENLEARN_PLUGIN_MAX_HEAP_MB \u5FC5\u987B\u662F "off"/"none"/"0" \u6216 >= 32 \u7684\u6574\u6570 MB\uFF0C\u5B9E\u9645\u4E3A "${heap}"\u3002 \u5E26\u5355\u4F4D\uFF08\u5982 128MB\uFF09\u6216\u4F4E\u4E8E\u4E0B\u9650\u4F1A\u9759\u9ED8\u56DE\u843D\u9ED8\u8BA4\u503C\uFF0C\u56E0\u6B64\u62D2\u7EDD\u542F\u52A8\u800C\u4E0D\u662F\u7EE7\u7EED\u3002`
185
+ });
186
+ }
187
+ }
188
+ }
189
+ const liveness = trimmed(env, "OPENLEARN_WORKER_LIVENESS");
190
+ if (liveness && !LIVENESS_VALUES.includes(liveness.toLowerCase())) {
191
+ errors.push({
192
+ key: "OPENLEARN_WORKER_LIVENESS",
193
+ severity: "error",
194
+ message: `OPENLEARN_WORKER_LIVENESS \u53EA\u80FD\u662F on / off\uFF0C\u5B9E\u9645\u4E3A "${liveness}"\u3002`
195
+ });
196
+ }
197
+ const enforce = trimmed(env, "OPENLEARN_ENFORCE_PLUGIN_ISOLATION");
198
+ if (enforce && !["1", "0"].includes(enforce)) {
199
+ errors.push({
200
+ key: "OPENLEARN_ENFORCE_PLUGIN_ISOLATION",
201
+ severity: "error",
202
+ message: `OPENLEARN_ENFORCE_PLUGIN_ISOLATION \u53EA\u80FD\u662F "1" \u6216 "0"\uFF0C\u5B9E\u9645\u4E3A "${enforce}"\u3002 \u5224\u5B9A\u5904\u662F \`!== '1'\` \u2014\u2014 \u5199\u6210 true/yes \u4F1A\u88AB\u9759\u9ED8\u5F53\u6210"\u672A\u5F00\u542F"\uFF0C\u56E0\u6B64\u62D2\u7EDD\u542F\u52A8\u800C\u4E0D\u662F\u7EE7\u7EED\u3002`
203
+ });
204
+ }
205
+ const trustedInline = trimmed(env, "OPENLEARN_TRUSTED_INLINE_PLUGINS");
206
+ if (trustedInline) {
207
+ const ids = splitList(trustedInline);
208
+ if (ids.length === 0) {
209
+ warnings.push({
210
+ key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
211
+ severity: "warning",
212
+ message: "OPENLEARN_TRUSTED_INLINE_PLUGINS \u53EA\u542B\u7A7A\u767D/\u9017\u53F7\uFF0C\u7B49\u540C\u4E8E\u672A\u914D\u7F6E\u3002"
213
+ });
214
+ } else if (isProduction) {
215
+ warnings.push({
216
+ key: "OPENLEARN_TRUSTED_INLINE_PLUGINS",
217
+ severity: "warning",
218
+ message: `\u751F\u4EA7\u73AF\u5883\u4FE1\u4EFB\u4E86 ${ids.length} \u4E2A\u63D2\u4EF6\u5728**\u8FDB\u7A0B\u5185**\u8FD0\u884C\uFF1A${ids.join(", ")}\u3002 \u5B83\u4EEC\u4E0E\u5BBF\u4E3B\u5171\u4EAB\u73AF\u5883\u53D8\u91CF\u4E0E\u7F51\u7EDC\u6808\uFF0C\u51FA\u7AD9\u7F51\u7EDC\u62E6\u622A\u5BF9\u5B83\u4EEC\u4E0D\u751F\u6548\u3002 \u786E\u8BA4\u8FD9\u662F\u6709\u610F\u4E3A\u4E4B\uFF1B\u5426\u5219\u6E05\u7A7A\u8BE5\u53D8\u91CF\u8BA9\u5B83\u4EEC\u56DE\u5230 process \u5B50\u8FDB\u7A0B\u9694\u79BB\u3002`
219
+ });
220
+ }
221
+ }
222
+ for (const key of QUOTA_SWITCH_KEYS) {
223
+ const value = trimmed(env, key);
224
+ if (value && !LIVENESS_VALUES.includes(value.toLowerCase())) {
225
+ errors.push({
226
+ key,
227
+ severity: "error",
228
+ message: `${key} \u53EA\u80FD\u662F on / off\uFF0C\u5B9E\u9645\u4E3A "${value}"\u3002\u5199\u9519\u4F1A\u9759\u9ED8\u4FDD\u6301\u914D\u989D\u751F\u6548\uFF0C\u770B\u8D77\u6765\u50CF"\u5173\u6389\u4E86\u4F46\u6CA1\u7528"\u3002`
229
+ });
230
+ }
231
+ }
232
+ for (const key of QUOTA_INT_KEYS) {
233
+ const value = trimmed(env, key);
234
+ if (!value) continue;
235
+ const lower = value.toLowerCase();
236
+ if (lower === "off" || lower === "none" || lower === "0") continue;
237
+ if (!/^\d+$/.test(value) || Number(value) <= 0) {
238
+ errors.push({
239
+ key,
240
+ severity: "error",
241
+ message: `${key} \u5FC5\u987B\u662F "off"/"none"/"0" \u6216\u6B63\u6574\u6570\uFF08\u6BEB\u79D2 / MB\uFF09\uFF0C\u5B9E\u9645\u4E3A "${value}"\u3002 \u5E26\u5355\u4F4D\uFF08\u5982 20s\u3001512MB\uFF09\u4F1A\u9759\u9ED8\u56DE\u843D\u9ED8\u8BA4\u503C\uFF0C\u56E0\u6B64\u62D2\u7EDD\u542F\u52A8\u800C\u4E0D\u662F\u7EE7\u7EED\u3002`
242
+ });
243
+ } else if (key.endsWith("_WINDOW_MS") && Number(value) < 1e3) {
244
+ warnings.push({
245
+ key,
246
+ severity: "warning",
247
+ message: `${key}=${value} \u5C0F\u4E8E 1 \u79D2\uFF0C\u91C7\u6837\u7A97\u53E3\u8FC7\u77ED\u4F1A\u8BA9 CPU \u914D\u989D\u5224\u5B9A\u6296\u52A8\uFF08\u91C7\u6837\u672C\u8EAB\u662F 1s \u4E00\u6B21\uFF09\u3002`
248
+ });
249
+ }
250
+ }
251
+ for (const key of LIVENESS_INT_KEYS) {
252
+ const value = trimmed(env, key);
253
+ if (!value) continue;
254
+ if (!/^\d+$/.test(value) || Number(value) <= 0) {
255
+ errors.push({
256
+ key,
257
+ severity: "error",
258
+ message: `${key} \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08\u6BEB\u79D2\uFF09\uFF0C\u5B9E\u9645\u4E3A "${value}"\u3002\u975E\u6CD5\u503C\u4F1A\u9759\u9ED8\u56DE\u843D\u9ED8\u8BA4\u63A2\u6D3B\u8282\u594F\u3002`
259
+ });
260
+ }
261
+ }
262
+ const telemetry = trimmed(env, "BRIDGE_HOOK_TELEMETRY");
263
+ if (telemetry && !TELEMETRY_VALUES.includes(telemetry.toLowerCase())) {
264
+ errors.push({
265
+ key: "BRIDGE_HOOK_TELEMETRY",
266
+ severity: "error",
267
+ message: `BRIDGE_HOOK_TELEMETRY \u53EA\u80FD\u662F on / off\uFF0C\u5B9E\u9645\u4E3A "${telemetry}"\u3002`
268
+ });
269
+ }
270
+ const allowedOrigins = trimmed(env, "ALLOWED_ORIGINS");
271
+ if (allowedOrigins) {
272
+ const origins = splitList(allowedOrigins);
273
+ const invalid = origins.filter((origin) => !isValidOriginPattern(origin));
274
+ if (invalid.length > 0) {
275
+ errors.push({
276
+ key: "ALLOWED_ORIGINS",
277
+ severity: "error",
278
+ message: `ALLOWED_ORIGINS \u542B\u975E\u6CD5 origin\uFF1A${invalid.map((o) => `"${o}"`).join(", ")}\u3002`
279
+ });
280
+ }
281
+ if (origins.includes("*")) {
282
+ warnings.push({
283
+ key: "ALLOWED_ORIGINS",
284
+ severity: "warning",
285
+ message: 'ALLOWED_ORIGINS \u542B "*"\uFF0C\u5C06\u5173\u95ED\u51ED\u8BC1\u652F\u6301\u3002\u751F\u4EA7\u73AF\u5883\u5EFA\u8BAE\u6539\u4E3A\u663E\u5F0F\u6765\u6E90\u767D\u540D\u5355\u3002'
286
+ });
287
+ }
288
+ }
289
+ for (const key of ["ALLOWED_FRAME_ORIGINS", "LTI_ALLOWED_LMS_ORIGINS"]) {
290
+ const raw = trimmed(env, key);
291
+ if (!raw) continue;
292
+ const invalid = splitList(raw).filter((origin) => !isValidOriginPattern(origin));
293
+ if (invalid.length > 0) {
294
+ warnings.push({
295
+ key,
296
+ severity: "warning",
297
+ message: `${key} \u542B\u65E0\u6CD5\u89E3\u6790\u7684 origin\uFF1A${invalid.map((o) => `"${o}"`).join(", ")}\uFF0C\u5C06\u65E0\u6CD5\u5339\u914D\u4EFB\u4F55\u6765\u6E90\u3002`
298
+ });
299
+ }
300
+ }
301
+ if (isProduction) {
302
+ const adminPassword = trimmed(env, "OPENLEARN_ADMIN_PASSWORD");
303
+ if (adminPassword && adminPassword.toLowerCase() === "admin") {
304
+ errors.push({
305
+ key: "OPENLEARN_ADMIN_PASSWORD",
306
+ severity: "error",
307
+ 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'
308
+ });
309
+ }
310
+ const teacherPassword = trimmed(env, "OPENLEARN_TEACHER_PASSWORD");
311
+ if (teacherPassword && teacherPassword.toLowerCase() === "teacher") {
312
+ errors.push({
313
+ key: "OPENLEARN_TEACHER_PASSWORD",
314
+ severity: "error",
315
+ 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'
316
+ });
317
+ }
318
+ }
319
+ const metricsToken = trimmed(env, "METRICS_TOKEN");
320
+ if (metricsToken && metricsToken.length < 16) {
321
+ warnings.push({
322
+ key: "METRICS_TOKEN",
323
+ severity: "warning",
324
+ message: "METRICS_TOKEN \u77ED\u4E8E 16 \u4E2A\u5B57\u7B26\uFF0C\u5BB9\u6613\u88AB\u66B4\u529B\u731C\u6D4B\uFF0C\u5EFA\u8BAE\u52A0\u957F\u3002"
325
+ });
326
+ }
327
+ return { errors, warnings };
328
+ }
329
+ function formatEnvIssues(issues) {
330
+ return issues.map((issue) => ` [${issue.severity.toUpperCase()}] ${issue.key}: ${issue.message}`).join("\n");
331
+ }
332
+ function assertStartupEnv(env = process.env) {
333
+ const { errors, warnings } = validateStartupEnv(env);
334
+ if (warnings.length > 0) {
335
+ console.warn(`[Security] \u542F\u52A8\u671F\u73AF\u5883\u53D8\u91CF\u544A\u8B66\uFF1A
336
+ ${formatEnvIssues(warnings)}`);
337
+ }
338
+ if (errors.length > 0) {
339
+ const message = `[Security] FATAL: \u542F\u52A8\u671F\u73AF\u5883\u53D8\u91CF\u6821\u9A8C\u5931\u8D25\uFF0C\u62D2\u7EDD\u542F\u52A8\uFF08${errors.length} \u9879\uFF09\uFF1A
340
+ ` + formatEnvIssues(errors);
341
+ const error = new Error(message);
342
+ error.name = "StartupEnvValidationError";
343
+ throw error;
344
+ }
345
+ }
346
+ // Annotate the CommonJS export names for ESM import in node:
347
+ 0 && (module.exports = {
348
+ assertStartupEnv,
349
+ formatEnvIssues,
350
+ validateStartupEnv
351
+ });