openlearn-next 0.3.18 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AdminDirectoryView-BtdFA-PR.js → AdminDirectoryView-CGo33RNh.js} +1 -1
- package/dist/assets/{BatchPickerModal-BQ_vsb5h.js → BatchPickerModal-MnNnNwFT.js} +1 -1
- package/dist/assets/{ClassesView-D_m_99Wk.js → ClassesView-CpblTKWg.js} +1 -1
- package/dist/assets/{CloudDriveModal-BHWCchwE.js → CloudDriveModal-BMjXCoju.js} +1 -1
- package/dist/assets/{ComputerLabView-n5hzKtEx.js → ComputerLabView-DGha_J8B.js} +1 -1
- package/dist/assets/CourseManagement-BqlboC1S.js +1 -0
- package/dist/assets/{CourseWizardModal-1F_Ksxgf.js → CourseWizardModal-DO1n4T2T.js} +1 -1
- package/dist/assets/{CoursewareHubPanel-B0JwbcQz.js → CoursewareHubPanel-CEK_QEc5.js} +1 -1
- package/dist/assets/{Dashboard-CgJNFUha.js → Dashboard-CfgOPb6z.js} +1 -1
- package/dist/assets/{ExportWeightModal-B2wHglNt.js → ExportWeightModal-d9A2R6Ur.js} +1 -1
- package/dist/assets/{HelpTour-C3G0EsYT.js → HelpTour-Dlc1vvy3.js} +1 -1
- package/dist/assets/{HelpView-Bgxlwpn_.js → HelpView-bHc3tQPD.js} +1 -1
- package/dist/assets/{ImportLessonsModal-xjfRYYa0.js → ImportLessonsModal-C25NubUA.js} +1 -1
- package/dist/assets/{ImportModal-ChLAX_xg.js → ImportModal-B2acbUT8.js} +1 -1
- package/dist/assets/{InteractiveCoursewareViewer-pi7sr3lt.js → InteractiveCoursewareViewer-BcXVoPUb.js} +1 -1
- package/dist/assets/{InteractiveWhiteboard-CsKeinZt.js → InteractiveWhiteboard-BxvOZE3l.js} +1 -1
- package/dist/assets/{LazyCourseware-Bw86DSzq.js → LazyCourseware-D3_YrYGR.js} +1 -1
- package/dist/assets/{LazyWhiteboard-XI8iSIjM.js → LazyWhiteboard-CFPDpdYV.js} +1 -1
- package/dist/assets/{LessonEditorView-3TnocL3v.js → LessonEditorView-CYHcC9EA.js} +1 -1
- package/dist/assets/{LiveClassroomView-CQGaFkzD.js → LiveClassroomView-BSPW9gVe.js} +1 -1
- package/dist/assets/{NotificationDetailModal-CI4IfD2v.js → NotificationDetailModal-D4w-m1D-.js} +1 -1
- package/dist/assets/{PluginView-LokCtvB8.js → PluginView-DogON4Dd.js} +1 -1
- package/dist/assets/{ProcessLogsModal-uJqQQFyw.js → ProcessLogsModal-MtK-Pe4R.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-Bul00fIH.js → QuizGeneratorModal-bCASRfsi.js} +1 -1
- package/dist/assets/{StudentAssignmentView-CaOXzBkt.js → StudentAssignmentView-B0b4spYC.js} +1 -1
- package/dist/assets/{StudentLessonView-6l-A6scB.js → StudentLessonView-SLcpNqtY.js} +1 -1
- package/dist/assets/{StudentPreviewModal-CnWC2qso.js → StudentPreviewModal-nO5C3nq5.js} +1 -1
- package/dist/assets/{StudentView-BtGPu4nl.js → StudentView-BtleJCxa.js} +1 -1
- package/dist/assets/{SystemResourceLibraryModal-CiRD12Rw.js → SystemResourceLibraryModal-D00BK5Xh.js} +1 -1
- package/dist/assets/{TeacherView-_iKPvpXI.js → TeacherView-BP04D9dn.js} +1 -1
- package/dist/assets/{TimetableView-B0yL3omc.js → TimetableView-Do_LWpV2.js} +4 -4
- package/dist/assets/{index-AdOtUso0.js → index-hTc1h9s2.js} +4 -4
- package/dist/assets/{vendor-icons-BHXru9a4.js → vendor-icons-B2DRJfZU.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/server.cjs +369 -231
- package/package.json +17 -17
- package/dist/assets/CourseManagement-DamuhaYN.js +0 -1
package/dist/server.cjs
CHANGED
|
@@ -549,15 +549,77 @@ var init_ai_event_bus = __esm({
|
|
|
549
549
|
}
|
|
550
550
|
});
|
|
551
551
|
|
|
552
|
-
// packages/core/di/
|
|
553
|
-
function
|
|
552
|
+
// packages/core/di/api-key-crypto.ts
|
|
553
|
+
function getEncryptionKey() {
|
|
554
|
+
if (_encryptionKey) return _encryptionKey;
|
|
555
|
+
const envPath = import_node_path.default.resolve(process.cwd(), ".env");
|
|
556
|
+
const keyHex = process.env.ENCRYPTION_KEY;
|
|
557
|
+
if (keyHex && keyHex.trim() !== "") {
|
|
558
|
+
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
559
|
+
return _encryptionKey;
|
|
560
|
+
}
|
|
561
|
+
if (import_node_fs.default.existsSync(envPath)) {
|
|
562
|
+
try {
|
|
563
|
+
const content = import_node_fs.default.readFileSync(envPath, "utf-8");
|
|
564
|
+
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
565
|
+
if (match && match[1].trim() !== "") {
|
|
566
|
+
const fileKeyHex = match[1].trim();
|
|
567
|
+
_encryptionKey = Buffer.from(fileKeyHex, "hex");
|
|
568
|
+
process.env.ENCRYPTION_KEY = fileKeyHex;
|
|
569
|
+
console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
|
|
570
|
+
return _encryptionKey;
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const newKey = import_node_crypto.default.randomBytes(32).toString("hex");
|
|
576
|
+
try {
|
|
577
|
+
if (import_node_fs.default.existsSync(envPath)) {
|
|
578
|
+
const existingContent = import_node_fs.default.readFileSync(envPath, "utf-8");
|
|
579
|
+
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
580
|
+
console.warn(
|
|
581
|
+
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
582
|
+
);
|
|
583
|
+
process.env.ENCRYPTION_KEY = newKey;
|
|
584
|
+
_encryptionKey = Buffer.from(newKey, "hex");
|
|
585
|
+
return _encryptionKey;
|
|
586
|
+
}
|
|
587
|
+
import_node_fs.default.appendFileSync(envPath, `
|
|
588
|
+
ENCRYPTION_KEY=${newKey}
|
|
589
|
+
`);
|
|
590
|
+
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
591
|
+
} else {
|
|
592
|
+
import_node_fs.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
593
|
+
`);
|
|
594
|
+
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
598
|
+
}
|
|
599
|
+
process.env.ENCRYPTION_KEY = newKey;
|
|
600
|
+
_encryptionKey = Buffer.from(newKey, "hex");
|
|
601
|
+
return _encryptionKey;
|
|
602
|
+
}
|
|
603
|
+
function encryptApiKey(plaintext) {
|
|
604
|
+
if (!plaintext) return "";
|
|
605
|
+
const key = getEncryptionKey();
|
|
606
|
+
const iv = import_node_crypto.default.randomBytes(16);
|
|
607
|
+
const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", key, iv);
|
|
608
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
609
|
+
const authTag = cipher.getAuthTag();
|
|
610
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
611
|
+
}
|
|
612
|
+
function looksLikeCiphertext(value) {
|
|
613
|
+
const parts = (value || "").split(":");
|
|
614
|
+
if (parts.length !== 3) return false;
|
|
615
|
+
return parts.every((p) => p.length > 0 && /^[0-9a-fA-F]+$/.test(p));
|
|
616
|
+
}
|
|
617
|
+
function decryptApiKey(encrypted) {
|
|
554
618
|
if (!encrypted) return "";
|
|
555
619
|
const parts = encrypted.split(":");
|
|
556
620
|
if (parts.length !== 3) return encrypted;
|
|
557
621
|
try {
|
|
558
|
-
const
|
|
559
|
-
if (!rawKey) return encrypted;
|
|
560
|
-
const key = rawKey.length === 64 ? Buffer.from(rawKey, "hex") : import_node_crypto.default.createHash("sha256").update(rawKey).digest();
|
|
622
|
+
const key = getEncryptionKey();
|
|
561
623
|
const iv = Buffer.from(parts[0], "hex");
|
|
562
624
|
const authTag = Buffer.from(parts[1], "hex");
|
|
563
625
|
const ciphertext = Buffer.from(parts[2], "hex");
|
|
@@ -565,15 +627,27 @@ function decryptKeyIfNeeded(encrypted) {
|
|
|
565
627
|
decipher.setAuthTag(authTag);
|
|
566
628
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
567
629
|
} catch {
|
|
630
|
+
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
568
631
|
return encrypted;
|
|
569
632
|
}
|
|
570
633
|
}
|
|
571
|
-
var import_node_crypto,
|
|
634
|
+
var import_node_crypto, import_node_fs, import_node_path, _encryptionKey;
|
|
635
|
+
var init_api_key_crypto = __esm({
|
|
636
|
+
"packages/core/di/api-key-crypto.ts"() {
|
|
637
|
+
import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
638
|
+
import_node_fs = __toESM(require("node:fs"), 1);
|
|
639
|
+
import_node_path = __toESM(require("node:path"), 1);
|
|
640
|
+
_encryptionKey = null;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// packages/core/di/ai-service.ts
|
|
645
|
+
var AIService;
|
|
572
646
|
var init_ai_service = __esm({
|
|
573
647
|
"packages/core/di/ai-service.ts"() {
|
|
574
|
-
import_node_crypto = __toESM(require("node:crypto"), 1);
|
|
575
648
|
init_provider_gateway();
|
|
576
649
|
init_ai_event_bus();
|
|
650
|
+
init_api_key_crypto();
|
|
577
651
|
AIService = class {
|
|
578
652
|
constructor(db2) {
|
|
579
653
|
this.db = db2;
|
|
@@ -586,11 +660,20 @@ var init_ai_service = __esm({
|
|
|
586
660
|
if (!provider || !provider.api_key) {
|
|
587
661
|
throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684 AI \u63D0\u4F9B\u5546\u3002\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u6DFB\u52A0\u5E76\u914D\u7F6E\u5927\u6A21\u578B\u670D\u52A1\u3002");
|
|
588
662
|
}
|
|
663
|
+
const apiKey = decryptApiKey(provider.api_key);
|
|
664
|
+
if (!apiKey || !apiKey.trim()) {
|
|
665
|
+
throw new Error("AI Provider API Key \u89E3\u5BC6\u4E3A\u7A7A\uFF0C\u8BF7\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58\u5BC6\u94A5\u3002");
|
|
666
|
+
}
|
|
667
|
+
if (looksLikeCiphertext(apiKey)) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
"AI Provider API Key \u89E3\u5BC6\u5931\u8D25\uFF1AENCRYPTION_KEY \u4E0E\u52A0\u5BC6\u65F6\u4E0D\u4E00\u81F4\uFF08\u5E38\u89C1\u4E8E PM2 \u5C06 ENCRYPTION_KEY \u7F6E\u7A7A\u6216\u5BC6\u94A5\u88AB\u8F6E\u6362\uFF09\u3002\u8BF7\u6062\u590D\u539F ENCRYPTION_KEY\uFF0C\u6216\u524D\u5F80\u300C\u7CFB\u7EDF\u7BA1\u7406 -> AI \u63D0\u4F9B\u5546\u7BA1\u7406\u300D\u91CD\u65B0\u4FDD\u5B58 API Key\u3002"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
589
672
|
const config = {
|
|
590
673
|
id: provider.id,
|
|
591
674
|
name: provider.name,
|
|
592
675
|
apiUrl: provider.api_url,
|
|
593
|
-
apiKey
|
|
676
|
+
apiKey,
|
|
594
677
|
modelName: provider.model_name
|
|
595
678
|
};
|
|
596
679
|
return this.gateway.generateText(prompt, options, config);
|
|
@@ -969,18 +1052,18 @@ var init_http_router = __esm({
|
|
|
969
1052
|
function createLogger(component) {
|
|
970
1053
|
return logger.child({ component });
|
|
971
1054
|
}
|
|
972
|
-
var import_pino,
|
|
1055
|
+
var import_pino, import_node_fs2, import_node_path2, level, logDir, logFile, streams, logger;
|
|
973
1056
|
var init_logger = __esm({
|
|
974
1057
|
"server/utils/logger.ts"() {
|
|
975
1058
|
import_pino = __toESM(require("pino"), 1);
|
|
976
|
-
|
|
977
|
-
|
|
1059
|
+
import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
1060
|
+
import_node_path2 = __toESM(require("node:path"), 1);
|
|
978
1061
|
level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug");
|
|
979
|
-
logDir =
|
|
980
|
-
if (!
|
|
981
|
-
|
|
1062
|
+
logDir = import_node_path2.default.resolve(process.cwd(), "logs");
|
|
1063
|
+
if (!import_node_fs2.default.existsSync(logDir)) {
|
|
1064
|
+
import_node_fs2.default.mkdirSync(logDir, { recursive: true });
|
|
982
1065
|
}
|
|
983
|
-
logFile =
|
|
1066
|
+
logFile = import_node_path2.default.join(logDir, "openlearn.log");
|
|
984
1067
|
streams = [];
|
|
985
1068
|
if (process.env.NODE_ENV !== "production") {
|
|
986
1069
|
streams.push({
|
|
@@ -1002,7 +1085,7 @@ var init_logger = __esm({
|
|
|
1002
1085
|
}
|
|
1003
1086
|
streams.push({
|
|
1004
1087
|
level,
|
|
1005
|
-
stream:
|
|
1088
|
+
stream: import_node_fs2.default.createWriteStream(logFile, { flags: "a" })
|
|
1006
1089
|
});
|
|
1007
1090
|
logger = (0, import_pino.default)({ level }, import_pino.default.multistream(streams));
|
|
1008
1091
|
}
|
|
@@ -3509,12 +3592,12 @@ parentPort.on('message', async function(msg) {
|
|
|
3509
3592
|
});
|
|
3510
3593
|
`;
|
|
3511
3594
|
}
|
|
3512
|
-
var import_node_worker_threads, import_fs4,
|
|
3595
|
+
var import_node_worker_threads, import_fs4, import_node_path3, import_uuid12, ALL_SERVICE_TOKENS, MAX_WORKERS, ACTIVATE_TIMEOUT_MS, ACTIVATE_PROGRESS_SLIDE_MS, WorkerRegistry, WorkerManager;
|
|
3513
3596
|
var init_worker_manager = __esm({
|
|
3514
3597
|
"packages/core/worker-runtime/worker-manager.ts"() {
|
|
3515
3598
|
import_node_worker_threads = require("node:worker_threads");
|
|
3516
3599
|
import_fs4 = __toESM(require("fs"), 1);
|
|
3517
|
-
|
|
3600
|
+
import_node_path3 = __toESM(require("node:path"), 1);
|
|
3518
3601
|
init_transport();
|
|
3519
3602
|
init_service_host();
|
|
3520
3603
|
init_logger();
|
|
@@ -3802,7 +3885,7 @@ var init_worker_manager = __esm({
|
|
|
3802
3885
|
const bootstrapDataUrl = `data:text/javascript;base64,${encodedBootstrap}`;
|
|
3803
3886
|
let worker;
|
|
3804
3887
|
try {
|
|
3805
|
-
const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(
|
|
3888
|
+
const resolvedPluginDir = pluginDir && import_fs4.default.existsSync(import_node_path3.default.join(pluginDir, "index.js")) ? pluginDir : void 0;
|
|
3806
3889
|
worker = new import_node_worker_threads.Worker(new URL(bootstrapDataUrl), {
|
|
3807
3890
|
// Pass both the DB id (`pluginId` — used as the actor/registry key)
|
|
3808
3891
|
// and `manifestId` (used as the namespace prefix for command types).
|
|
@@ -4047,7 +4130,7 @@ __export(server_exports, {
|
|
|
4047
4130
|
});
|
|
4048
4131
|
module.exports = __toCommonJS(server_exports);
|
|
4049
4132
|
var import_express3 = __toESM(require("express"), 1);
|
|
4050
|
-
var
|
|
4133
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
4051
4134
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
4052
4135
|
var import_os2 = __toESM(require("os"), 1);
|
|
4053
4136
|
var import_child_process3 = require("child_process");
|
|
@@ -8702,9 +8785,9 @@ var import_node_url = require("node:url");
|
|
|
8702
8785
|
|
|
8703
8786
|
// packages/core/esm-loader/install-utils.ts
|
|
8704
8787
|
var import_jszip = __toESM(require("jszip"), 1);
|
|
8705
|
-
var
|
|
8788
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
8706
8789
|
var os2 = __toESM(require("os"), 1);
|
|
8707
|
-
var
|
|
8790
|
+
var path5 = __toESM(require("path"), 1);
|
|
8708
8791
|
var import_uuid10 = require("uuid");
|
|
8709
8792
|
var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
|
|
8710
8793
|
async function bundlePlugin(entryCode, resolveDir) {
|
|
@@ -8727,12 +8810,12 @@ async function bundlePlugin(entryCode, resolveDir) {
|
|
|
8727
8810
|
setup(build) {
|
|
8728
8811
|
build.onResolve({ filter: /.*/ }, (args) => {
|
|
8729
8812
|
if (args.path.includes("core/di/interfaces")) {
|
|
8730
|
-
const tsPath =
|
|
8731
|
-
if (
|
|
8813
|
+
const tsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.ts");
|
|
8814
|
+
if (fs4.existsSync(tsPath)) {
|
|
8732
8815
|
return { path: tsPath };
|
|
8733
8816
|
}
|
|
8734
|
-
const jsPath =
|
|
8735
|
-
if (
|
|
8817
|
+
const jsPath = path5.resolve(process.cwd(), "packages/core/di/interfaces.js");
|
|
8818
|
+
if (fs4.existsSync(jsPath)) {
|
|
8736
8819
|
return { path: jsPath };
|
|
8737
8820
|
}
|
|
8738
8821
|
}
|
|
@@ -8788,22 +8871,22 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
8788
8871
|
throw new Error(`Entry file "${manifest.main}" specified in manifest not found in ZIP package`);
|
|
8789
8872
|
}
|
|
8790
8873
|
const entryCode = await entryFile.async("string");
|
|
8791
|
-
const tmpDir =
|
|
8874
|
+
const tmpDir = path5.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
|
|
8792
8875
|
try {
|
|
8793
|
-
|
|
8876
|
+
fs4.mkdirSync(tmpDir, { recursive: true });
|
|
8794
8877
|
for (const [name, file] of Object.entries(zip.files)) {
|
|
8795
8878
|
if (file.dir) continue;
|
|
8796
|
-
const filePath =
|
|
8797
|
-
const fileDir =
|
|
8798
|
-
|
|
8879
|
+
const filePath = path5.join(tmpDir, name);
|
|
8880
|
+
const fileDir = path5.dirname(filePath);
|
|
8881
|
+
fs4.mkdirSync(fileDir, { recursive: true });
|
|
8799
8882
|
const content = await file.async("nodebuffer");
|
|
8800
|
-
|
|
8883
|
+
fs4.writeFileSync(filePath, content);
|
|
8801
8884
|
}
|
|
8802
8885
|
const bundledCode = await bundlePlugin(entryCode, tmpDir);
|
|
8803
8886
|
return { manifest, bundledCode, entryFileName: manifest.main };
|
|
8804
8887
|
} finally {
|
|
8805
8888
|
try {
|
|
8806
|
-
|
|
8889
|
+
fs4.rmSync(tmpDir, { recursive: true, force: true });
|
|
8807
8890
|
} catch {
|
|
8808
8891
|
}
|
|
8809
8892
|
}
|
|
@@ -19743,93 +19826,14 @@ function registerOfficialActivities(registry, actionRegistry) {
|
|
|
19743
19826
|
var IActivityRegistryToken = new Token("@openlearn/activity-ecosystem:IActivityRegistry");
|
|
19744
19827
|
|
|
19745
19828
|
// server/routes/os.ts
|
|
19746
|
-
var
|
|
19747
|
-
var
|
|
19829
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
19830
|
+
var import_fs8 = __toESM(require("fs"), 1);
|
|
19748
19831
|
var import_child_process = require("child_process");
|
|
19749
|
-
var
|
|
19832
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
19750
19833
|
var import_xss = require("xss");
|
|
19751
19834
|
|
|
19752
19835
|
// server/utils/crypto.ts
|
|
19753
|
-
|
|
19754
|
-
var import_fs8 = __toESM(require("fs"), 1);
|
|
19755
|
-
var import_path7 = __toESM(require("path"), 1);
|
|
19756
|
-
var _encryptionKey = null;
|
|
19757
|
-
function getEncryptionKey() {
|
|
19758
|
-
if (_encryptionKey) return _encryptionKey;
|
|
19759
|
-
const envPath = import_path7.default.resolve(process.cwd(), ".env");
|
|
19760
|
-
const keyHex = process.env.ENCRYPTION_KEY;
|
|
19761
|
-
if (keyHex && keyHex.trim() !== "") {
|
|
19762
|
-
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
19763
|
-
return _encryptionKey;
|
|
19764
|
-
}
|
|
19765
|
-
if (import_fs8.default.existsSync(envPath)) {
|
|
19766
|
-
try {
|
|
19767
|
-
const content = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
19768
|
-
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
19769
|
-
if (match && match[1].trim() !== "") {
|
|
19770
|
-
const fileKeyHex = match[1].trim();
|
|
19771
|
-
_encryptionKey = Buffer.from(fileKeyHex, "hex");
|
|
19772
|
-
process.env.ENCRYPTION_KEY = fileKeyHex;
|
|
19773
|
-
console.log("[Crypto] ENCRYPTION_KEY loaded from .env file");
|
|
19774
|
-
return _encryptionKey;
|
|
19775
|
-
}
|
|
19776
|
-
} catch {
|
|
19777
|
-
}
|
|
19778
|
-
}
|
|
19779
|
-
const newKey = import_crypto5.default.randomBytes(32).toString("hex");
|
|
19780
|
-
try {
|
|
19781
|
-
if (import_fs8.default.existsSync(envPath)) {
|
|
19782
|
-
const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
19783
|
-
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
19784
|
-
console.warn(
|
|
19785
|
-
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
19786
|
-
);
|
|
19787
|
-
process.env.ENCRYPTION_KEY = newKey;
|
|
19788
|
-
_encryptionKey = Buffer.from(newKey, "hex");
|
|
19789
|
-
return _encryptionKey;
|
|
19790
|
-
}
|
|
19791
|
-
import_fs8.default.appendFileSync(envPath, `
|
|
19792
|
-
ENCRYPTION_KEY=${newKey}
|
|
19793
|
-
`);
|
|
19794
|
-
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
19795
|
-
} else {
|
|
19796
|
-
import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
19797
|
-
`);
|
|
19798
|
-
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
19799
|
-
}
|
|
19800
|
-
} catch (e) {
|
|
19801
|
-
console.warn("[Crypto] Could not persist ENCRYPTION_KEY, using in-memory fallback");
|
|
19802
|
-
}
|
|
19803
|
-
process.env.ENCRYPTION_KEY = newKey;
|
|
19804
|
-
_encryptionKey = Buffer.from(newKey, "hex");
|
|
19805
|
-
return _encryptionKey;
|
|
19806
|
-
}
|
|
19807
|
-
function encryptApiKey(plaintext) {
|
|
19808
|
-
if (!plaintext) return "";
|
|
19809
|
-
const key = getEncryptionKey();
|
|
19810
|
-
const iv = import_crypto5.default.randomBytes(16);
|
|
19811
|
-
const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
|
|
19812
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
19813
|
-
const authTag = cipher.getAuthTag();
|
|
19814
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
19815
|
-
}
|
|
19816
|
-
function decryptApiKey(encrypted) {
|
|
19817
|
-
if (!encrypted) return "";
|
|
19818
|
-
const parts = encrypted.split(":");
|
|
19819
|
-
if (parts.length !== 3) return encrypted;
|
|
19820
|
-
try {
|
|
19821
|
-
const key = getEncryptionKey();
|
|
19822
|
-
const iv = Buffer.from(parts[0], "hex");
|
|
19823
|
-
const authTag = Buffer.from(parts[1], "hex");
|
|
19824
|
-
const ciphertext = Buffer.from(parts[2], "hex");
|
|
19825
|
-
const decipher = import_crypto5.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
19826
|
-
decipher.setAuthTag(authTag);
|
|
19827
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
|
|
19828
|
-
} catch {
|
|
19829
|
-
console.warn("[Crypto] Failed to decrypt API key, treating as plaintext");
|
|
19830
|
-
return encrypted;
|
|
19831
|
-
}
|
|
19832
|
-
}
|
|
19836
|
+
init_api_key_crypto();
|
|
19833
19837
|
function maskApiKey(key) {
|
|
19834
19838
|
if (!key || key.length <= 8) return key ? "****" : "";
|
|
19835
19839
|
return key.substring(0, 4) + "****" + key.substring(key.length - 4);
|
|
@@ -19847,8 +19851,8 @@ function detectPromptInjection(input) {
|
|
|
19847
19851
|
}
|
|
19848
19852
|
|
|
19849
19853
|
// server/routes/shared.ts
|
|
19850
|
-
var
|
|
19851
|
-
var
|
|
19854
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
19855
|
+
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
19852
19856
|
|
|
19853
19857
|
// server/utils/bridge-sdk.ts
|
|
19854
19858
|
var BRIDGE_SDK_CODE = `(function() {
|
|
@@ -20315,7 +20319,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20315
20319
|
const text = (el.textContent || el.innerText || '').trim();
|
|
20316
20320
|
if (text) {
|
|
20317
20321
|
logData.push("Selector '" + selector + "' matched text: '" + text + "'");
|
|
20318
|
-
const fractionMatch = text.match(/(
|
|
20322
|
+
const fractionMatch = text.match(/(\\d+(\\.\\d+)?)\\s*[\\/|\u4E4B]\\s*(\\d+)/);
|
|
20319
20323
|
if (fractionMatch) {
|
|
20320
20324
|
const num = parseFloat(fractionMatch[1]);
|
|
20321
20325
|
const den = parseFloat(fractionMatch[3]);
|
|
@@ -20325,7 +20329,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20325
20329
|
return { score: pct, log: logData };
|
|
20326
20330
|
}
|
|
20327
20331
|
}
|
|
20328
|
-
const match = text.match(
|
|
20332
|
+
const match = text.match(/\\d+(\\.\\d+)?/);
|
|
20329
20333
|
if (match) {
|
|
20330
20334
|
const num = parseFloat(match[0]);
|
|
20331
20335
|
if (!isNaN(num)) {
|
|
@@ -20366,7 +20370,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20366
20370
|
if (txt) {
|
|
20367
20371
|
const hasKey = txt.includes('\u5F97\u5206') || txt.includes('\u5206\u6570') || txt.includes('\u6210\u7EE9') || txt.toLowerCase().includes('score') || txt.toLowerCase().includes('points');
|
|
20368
20372
|
if (hasKey) {
|
|
20369
|
-
const m = txt.match(
|
|
20373
|
+
const m = txt.match(/\\d+(\\.\\d+)?/);
|
|
20370
20374
|
if (m) {
|
|
20371
20375
|
const val = parseFloat(m[0]);
|
|
20372
20376
|
logData.push("Fallback leaf <" + el.tagName + "> '" + txt + "' parsed: " + val);
|
|
@@ -20384,6 +20388,139 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20384
20388
|
return { score: null, log: logData };
|
|
20385
20389
|
}
|
|
20386
20390
|
|
|
20391
|
+
// --- RESULT-SCREEN AUTO SUBMIT ---------------------------------------
|
|
20392
|
+
// \u90E8\u5206\u8BFE\u4EF6\u7B54\u5B8C\u9898\u540E\u76F4\u63A5\u5207\u5230\u300C\u7ED3\u7B97 / \u7ED3\u679C\u9875\u300D\uFF0C\u5E76\u4E0D\u70B9\u51FB\u4EFB\u4F55\u63D0\u4EA4\u6309\u94AE\u3002
|
|
20393
|
+
// \u8FD9\u91CC\u76D1\u542C\u7ED3\u7B97\u9875\u51FA\u73B0\uFF0C\u81EA\u52A8\u6293\u53D6\u5206\u6570\u5E76\u4E0A\u62A5\u4E00\u6B21\u3002
|
|
20394
|
+
// \u53E3\u5F84\u4F18\u5148\u300C\u6B63\u786E\u9898\u6570 X/Y\u300D\u2192 \u767E\u5206\u5236\uFF0C\u5426\u5219\u56DE\u843D\u53EF\u89C1\u7684\u5206\u6570\u5143\u7D20\u3002
|
|
20395
|
+
var __lmsResultSubmitted = false;
|
|
20396
|
+
var __lmsResultScheduled = false;
|
|
20397
|
+
var __lmsResultPolling = false;
|
|
20398
|
+
|
|
20399
|
+
// \u5F3A\u7ED3\u675F\u4FE1\u53F7\uFF1A\u53EA\u8BA4\u7ED3\u7B97/\u7ED3\u679C\u9875\u6587\u6848\uFF0C\u907F\u514D\u7B54\u9898\u8FC7\u7A0B\u4E2D\u8BEF\u89E6\u53D1
|
|
20400
|
+
var __LMS_DONE_RE = /(\u95EF\u5173|\u6311\u6218|\u7B54\u9898|\u6D4B\u8BD5|\u6E38\u620F|\u672C\u8F6E|\u672C\u5173)(\u7ED3\u675F|\u5B8C\u6210|\u6210\u529F)|\u901A\u5173|\u7ED3\u7B97|\u67E5\u770B\u89E3\u6790|\u6B63\u786E\u7387|\u6700\u7EC8\u5F97\u5206|\u603B\u5F97\u5206|\u5168\u90E8\u7B54\u5B8C|\u7B54\u9898\u5B8C\u6BD5/;
|
|
20401
|
+
// \u7528 [0-9] / [^0-9] \u4EE3\u66FF \\d / \\s\uFF0C\u89C4\u907F\u6A21\u677F\u5B57\u7B26\u4E32\u7684\u8F6C\u4E49\u9677\u9631
|
|
20402
|
+
var __LMS_RATIO_RE = /([0-9]+(?:[.][0-9]+)?)[^0-9]*[/\uFF0F|\u4E4B][^0-9]*([0-9]+)/;
|
|
20403
|
+
var __LMS_NUM_RE = /[0-9]+(?:[.][0-9]+)?/;
|
|
20404
|
+
|
|
20405
|
+
function __lmsVisibleText() {
|
|
20406
|
+
try {
|
|
20407
|
+
if (!document.body) return '';
|
|
20408
|
+
if (typeof document.body.innerText === 'string') return document.body.innerText;
|
|
20409
|
+
return document.body.textContent || '';
|
|
20410
|
+
} catch (e) { return ''; }
|
|
20411
|
+
}
|
|
20412
|
+
|
|
20413
|
+
function __lmsIsShown(el) {
|
|
20414
|
+
try {
|
|
20415
|
+
if (!el || !el.getBoundingClientRect) return false;
|
|
20416
|
+
var rect = el.getBoundingClientRect();
|
|
20417
|
+
if (rect.width <= 0 && rect.height <= 0) return false;
|
|
20418
|
+
var node = el;
|
|
20419
|
+
while (node && node.nodeType === 1) {
|
|
20420
|
+
var st = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
20421
|
+
if (st && (st.display === 'none' || st.visibility === 'hidden')) return false;
|
|
20422
|
+
node = node.parentElement;
|
|
20423
|
+
}
|
|
20424
|
+
return true;
|
|
20425
|
+
} catch (e) { return false; }
|
|
20426
|
+
}
|
|
20427
|
+
|
|
20428
|
+
// \u4EC5\u4ECE\u300C\u53EF\u89C1\u300D\u7684\u5206\u6570\u5143\u7D20\u53D6\u503C\uFF0C\u907F\u514D\u7ED3\u7B97\u9875\u5C1A\u672A\u5C55\u5F00\u65F6\u8BFB\u5230\u9690\u85CF\u7684\u521D\u59CB\u503C 0
|
|
20429
|
+
function __lmsFindVisibleScore() {
|
|
20430
|
+
var selectors = [
|
|
20431
|
+
'#finalScore', '#score', '#scoreDisplay', '.score', '.final-score',
|
|
20432
|
+
'[id*="score" i]', '[id*="point" i]', '[id*="grade" i]'
|
|
20433
|
+
];
|
|
20434
|
+
for (var i = 0; i < selectors.length; i++) {
|
|
20435
|
+
var nodes = [];
|
|
20436
|
+
try { nodes = document.querySelectorAll(selectors[i]); } catch (e) { nodes = []; }
|
|
20437
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
20438
|
+
if (!__lmsIsShown(nodes[j])) continue;
|
|
20439
|
+
var txt = (nodes[j].textContent || '').trim();
|
|
20440
|
+
if (!txt) continue;
|
|
20441
|
+
var frac = txt.match(__LMS_RATIO_RE);
|
|
20442
|
+
if (frac) {
|
|
20443
|
+
var fn = parseFloat(frac[1]);
|
|
20444
|
+
var fd = parseFloat(frac[2]);
|
|
20445
|
+
if (fd > 0 && fn >= 0 && fn <= fd) return Math.round((fn / fd) * 10000) / 100;
|
|
20446
|
+
}
|
|
20447
|
+
var num = txt.match(__LMS_NUM_RE);
|
|
20448
|
+
if (num) return parseFloat(num[0]);
|
|
20449
|
+
}
|
|
20450
|
+
}
|
|
20451
|
+
return null;
|
|
20452
|
+
}
|
|
20453
|
+
|
|
20454
|
+
function __lmsExtractResultScore() {
|
|
20455
|
+
// 1) \u6B63\u786E\u9898\u6570 X/Y \u2192 \u767E\u5206\u5236\uFF08\u53E3\u5F84\u6700\u7A33\u5B9A\uFF09
|
|
20456
|
+
var ratioSelectors = [
|
|
20457
|
+
'#correctCount', '[id*="correct" i]', '[id*="accuracy" i]',
|
|
20458
|
+
'[class*="correct" i]', '[class*="accuracy" i]', '[id*="ratio" i]'
|
|
20459
|
+
];
|
|
20460
|
+
for (var i = 0; i < ratioSelectors.length; i++) {
|
|
20461
|
+
var nodes = [];
|
|
20462
|
+
try { nodes = document.querySelectorAll(ratioSelectors[i]); } catch (e) { nodes = []; }
|
|
20463
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
20464
|
+
if (!__lmsIsShown(nodes[j])) continue;
|
|
20465
|
+
var m = (nodes[j].textContent || '').trim().match(__LMS_RATIO_RE);
|
|
20466
|
+
if (m) {
|
|
20467
|
+
var num = parseFloat(m[1]);
|
|
20468
|
+
var den = parseFloat(m[2]);
|
|
20469
|
+
if (den > 0 && num >= 0 && num <= den) return Math.round((num / den) * 10000) / 100;
|
|
20470
|
+
}
|
|
20471
|
+
}
|
|
20472
|
+
}
|
|
20473
|
+
// 2) \u56DE\u843D\uFF1A\u53EF\u89C1\u5206\u6570\u5143\u7D20
|
|
20474
|
+
return __lmsFindVisibleScore();
|
|
20475
|
+
}
|
|
20476
|
+
|
|
20477
|
+
function __lmsMaybeSubmitResult() {
|
|
20478
|
+
if (__lmsResultSubmitted || __lmsResultPolling) return;
|
|
20479
|
+
if (!__LMS_DONE_RE.test(__lmsVisibleText())) return;
|
|
20480
|
+
|
|
20481
|
+
__lmsResultPolling = true;
|
|
20482
|
+
// \u7ED3\u7B97\u9875 DOM \u53EF\u80FD\u5206\u6B65\u6E32\u67D3\uFF0C\u77ED\u8F6E\u8BE2\u53D6\u6700\u540E\u4E00\u6B21\u6709\u6548\u5206\u6570
|
|
20483
|
+
var tries = 0;
|
|
20484
|
+
var delays = [0, 120, 250, 400, 700, 1100];
|
|
20485
|
+
var latest = null;
|
|
20486
|
+
|
|
20487
|
+
function poll() {
|
|
20488
|
+
var s = __lmsExtractResultScore();
|
|
20489
|
+
if (s !== null && !isNaN(s)) latest = s;
|
|
20490
|
+
tries++;
|
|
20491
|
+
if (tries < delays.length) {
|
|
20492
|
+
setTimeout(poll, delays[tries]);
|
|
20493
|
+
return;
|
|
20494
|
+
}
|
|
20495
|
+
__lmsResultPolling = false;
|
|
20496
|
+
if (latest !== null && !__lmsResultSubmitted) {
|
|
20497
|
+
__lmsResultSubmitted = true;
|
|
20498
|
+
logToServer('Result screen detected, auto-submitting score: ' + latest);
|
|
20499
|
+
window.LMS.submit({ score: latest, completion: 1.0, comment: '\u7ED3\u7B97\u9875\u81EA\u52A8\u63D0\u53D6\u5F97\u5206' });
|
|
20500
|
+
}
|
|
20501
|
+
}
|
|
20502
|
+
poll();
|
|
20503
|
+
}
|
|
20504
|
+
|
|
20505
|
+
function initResultWatcher() {
|
|
20506
|
+
try {
|
|
20507
|
+
if (!document.body) return;
|
|
20508
|
+
var observer = new MutationObserver(function() {
|
|
20509
|
+
if (__lmsResultSubmitted || __lmsResultScheduled) return;
|
|
20510
|
+
__lmsResultScheduled = true;
|
|
20511
|
+
setTimeout(function() {
|
|
20512
|
+
__lmsResultScheduled = false;
|
|
20513
|
+
__lmsMaybeSubmitResult();
|
|
20514
|
+
}, 600);
|
|
20515
|
+
});
|
|
20516
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
20517
|
+
// \u5C11\u6570\u8BFE\u4EF6\u9996\u5C4F\u5373\u7ED3\u7B97
|
|
20518
|
+
setTimeout(__lmsMaybeSubmitResult, 800);
|
|
20519
|
+
} catch (e) {
|
|
20520
|
+
logToServer('Error in initResultWatcher: ' + e.message);
|
|
20521
|
+
}
|
|
20522
|
+
}
|
|
20523
|
+
|
|
20387
20524
|
function attachListeners() {
|
|
20388
20525
|
try {
|
|
20389
20526
|
const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], .btn, .button');
|
|
@@ -20477,6 +20614,7 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
20477
20614
|
});
|
|
20478
20615
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
20479
20616
|
attachListeners();
|
|
20617
|
+
initResultWatcher();
|
|
20480
20618
|
} catch (e) {
|
|
20481
20619
|
logToServer("Error in initAutoSubmit: " + e.message);
|
|
20482
20620
|
}
|
|
@@ -20509,7 +20647,7 @@ function validateMagicBytes(buffer, fileName) {
|
|
|
20509
20647
|
".webp": [[82, 73, 70, 70]]
|
|
20510
20648
|
// RIFF
|
|
20511
20649
|
};
|
|
20512
|
-
const ext =
|
|
20650
|
+
const ext = import_path7.default.extname(fileName || "").toLowerCase();
|
|
20513
20651
|
const signatures = MAGIC_BYTES[ext];
|
|
20514
20652
|
if (!signatures) return true;
|
|
20515
20653
|
return signatures.some((sig) => sig.every((byte, i) => buffer[i] === byte));
|
|
@@ -20531,7 +20669,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20531
20669
|
const classRow = kernelContainer.db.prepare("SELECT class_id FROM class_students WHERE student_id = ? LIMIT 1").get(session.studentId);
|
|
20532
20670
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, session.studentId, "active");
|
|
20533
20671
|
if (!attempt) {
|
|
20534
|
-
const attemptId = "att_" +
|
|
20672
|
+
const attemptId = "att_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20535
20673
|
kernelContainer.db.prepare(
|
|
20536
20674
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20537
20675
|
).run(attemptId, cwInfo.id, session.studentId, Date.now(), "active");
|
|
@@ -20546,7 +20684,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20546
20684
|
} else if (session.role === "teacher" || session.role === "administrator") {
|
|
20547
20685
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "teacher", "active");
|
|
20548
20686
|
if (!attempt) {
|
|
20549
|
-
const attemptId = "att_teacher_" +
|
|
20687
|
+
const attemptId = "att_teacher_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20550
20688
|
kernelContainer.db.prepare(
|
|
20551
20689
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20552
20690
|
).run(attemptId, cwInfo.id, "teacher", Date.now(), "active");
|
|
@@ -20564,7 +20702,7 @@ function injectLmsSdk(htmlContent, req, cwInfo) {
|
|
|
20564
20702
|
if (studentInfo.attempt_id === "guest-attempt") {
|
|
20565
20703
|
let attempt = kernelContainer.db.prepare("SELECT id FROM courseware_attempt WHERE courseware_id = ? AND student_id = ? AND status = ?").get(cwInfo.id, "guest", "active");
|
|
20566
20704
|
if (!attempt) {
|
|
20567
|
-
const attemptId = "att_guest_" +
|
|
20705
|
+
const attemptId = "att_guest_" + import_crypto5.default.randomBytes(8).toString("hex");
|
|
20568
20706
|
kernelContainer.db.prepare(
|
|
20569
20707
|
"INSERT INTO courseware_attempt (id, courseware_id, student_id, started_at, status) VALUES (?, ?, ?, ?, ?)"
|
|
20570
20708
|
).run(attemptId, cwInfo.id, "guest", Date.now(), "active");
|
|
@@ -20648,7 +20786,7 @@ function registerOsRoutes(ctx) {
|
|
|
20648
20786
|
if (!filename || !base64Data) {
|
|
20649
20787
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
20650
20788
|
}
|
|
20651
|
-
const ext =
|
|
20789
|
+
const ext = import_path8.default.extname(filename).toLowerCase();
|
|
20652
20790
|
if (BLOCKED_EXTENSIONS.includes(ext)) {
|
|
20653
20791
|
return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
|
|
20654
20792
|
}
|
|
@@ -20660,13 +20798,13 @@ function registerOsRoutes(ctx) {
|
|
|
20660
20798
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
20661
20799
|
return res.status(400).json({ error: "File content does not match the declared file type." });
|
|
20662
20800
|
}
|
|
20663
|
-
const uploadsDir =
|
|
20664
|
-
if (!
|
|
20665
|
-
|
|
20801
|
+
const uploadsDir = import_path8.default.join(process.cwd(), "uploads");
|
|
20802
|
+
if (!import_fs8.default.existsSync(uploadsDir)) {
|
|
20803
|
+
import_fs8.default.mkdirSync(uploadsDir, { recursive: true });
|
|
20666
20804
|
}
|
|
20667
|
-
const uniqueName = `${Date.now()}-${
|
|
20668
|
-
const filePath =
|
|
20669
|
-
|
|
20805
|
+
const uniqueName = `${Date.now()}-${import_crypto6.default.randomBytes(4).toString("hex")}${ext}`;
|
|
20806
|
+
const filePath = import_path8.default.join(uploadsDir, uniqueName);
|
|
20807
|
+
import_fs8.default.writeFileSync(filePath, fileBuffer);
|
|
20670
20808
|
let slideCount = 1;
|
|
20671
20809
|
if (ext === ".pdf") {
|
|
20672
20810
|
try {
|
|
@@ -20879,8 +21017,8 @@ function registerOsRoutes(ctx) {
|
|
|
20879
21017
|
});
|
|
20880
21018
|
if (result && typeof result.agentText === "string") {
|
|
20881
21019
|
const now = Date.now();
|
|
20882
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
20883
|
-
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" +
|
|
21020
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "user", message, now);
|
|
21021
|
+
kernelContainer.db.prepare("INSERT INTO agent_conversations (id, conv_key, role, content, created_at) VALUES (?, ?, ?, ?, ?)").run("ac_" + import_crypto6.default.randomUUID(), convKey, "assistant", result.agentText, now + 1);
|
|
20884
21022
|
}
|
|
20885
21023
|
res.json({
|
|
20886
21024
|
success: true,
|
|
@@ -20929,8 +21067,8 @@ function registerOsRoutes(ctx) {
|
|
|
20929
21067
|
}
|
|
20930
21068
|
|
|
20931
21069
|
// server/routes/resources.ts
|
|
20932
|
-
var
|
|
20933
|
-
var
|
|
21070
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
21071
|
+
var import_fs9 = __toESM(require("fs"), 1);
|
|
20934
21072
|
function registerResourcesRoutes(ctx) {
|
|
20935
21073
|
const { app } = ctx;
|
|
20936
21074
|
app.get("/api/resources", (req, res) => {
|
|
@@ -20946,23 +21084,23 @@ function registerResourcesRoutes(ctx) {
|
|
|
20946
21084
|
const resource = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(req.params.id);
|
|
20947
21085
|
if (!resource) return res.status(404).send("Resource not found");
|
|
20948
21086
|
if (resource.type === "html") {
|
|
20949
|
-
const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ?
|
|
21087
|
+
const entryName = resource.name && (resource.name.endsWith(".html") || resource.name.endsWith(".htm")) ? import_path9.default.basename(resource.name.replace(/\\/g, "/")) : "index.html";
|
|
20950
21088
|
const existingCw2 = kernelContainer.db.prepare("SELECT id FROM courseware WHERE id = ?").get(resource.id);
|
|
20951
21089
|
if (!existingCw2) {
|
|
20952
21090
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "html", entryName, resource.created_at || Date.now());
|
|
20953
21091
|
}
|
|
20954
21092
|
try {
|
|
20955
|
-
const storageDir =
|
|
20956
|
-
if (!
|
|
20957
|
-
|
|
21093
|
+
const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
|
|
21094
|
+
if (!import_fs9.default.existsSync(storageDir)) {
|
|
21095
|
+
import_fs9.default.mkdirSync(storageDir, { recursive: true });
|
|
20958
21096
|
}
|
|
20959
|
-
const namedPath =
|
|
20960
|
-
if (!
|
|
20961
|
-
|
|
21097
|
+
const namedPath = import_path9.default.resolve(storageDir, entryName);
|
|
21098
|
+
if (!import_fs9.default.existsSync(namedPath)) {
|
|
21099
|
+
import_fs9.default.writeFileSync(namedPath, resource.content || "", "utf8");
|
|
20962
21100
|
}
|
|
20963
|
-
const indexHtmlPath =
|
|
20964
|
-
if (!
|
|
20965
|
-
|
|
21101
|
+
const indexHtmlPath = import_path9.default.resolve(storageDir, "index.html");
|
|
21102
|
+
if (!import_fs9.default.existsSync(indexHtmlPath)) {
|
|
21103
|
+
import_fs9.default.writeFileSync(indexHtmlPath, resource.content || "", "utf8");
|
|
20966
21104
|
}
|
|
20967
21105
|
} catch (storageErr) {
|
|
20968
21106
|
console.warn("[resources] Failed to write courseware storage cache:", storageErr);
|
|
@@ -21002,21 +21140,21 @@ function registerResourcesRoutes(ctx) {
|
|
|
21002
21140
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resource.id, resource.id, resource.name, "folder", indexFile.path, resource.created_at || Date.now());
|
|
21003
21141
|
}
|
|
21004
21142
|
try {
|
|
21005
|
-
const storageDir =
|
|
21006
|
-
if (!
|
|
21007
|
-
|
|
21143
|
+
const storageDir = import_path9.default.resolve(process.cwd(), "storage", "courseware", resource.id);
|
|
21144
|
+
if (!import_fs9.default.existsSync(storageDir)) {
|
|
21145
|
+
import_fs9.default.mkdirSync(storageDir, { recursive: true });
|
|
21008
21146
|
for (const f of files) {
|
|
21009
21147
|
if (f.path && f.content !== void 0) {
|
|
21010
21148
|
const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21011
|
-
const target =
|
|
21149
|
+
const target = import_path9.default.resolve(storageDir, cleanRel);
|
|
21012
21150
|
if (target.startsWith(storageDir)) {
|
|
21013
|
-
|
|
21151
|
+
import_fs9.default.mkdirSync(import_path9.default.dirname(target), { recursive: true });
|
|
21014
21152
|
const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
|
|
21015
21153
|
if (isBin) {
|
|
21016
21154
|
const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
|
|
21017
|
-
|
|
21155
|
+
import_fs9.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
|
|
21018
21156
|
} else {
|
|
21019
|
-
|
|
21157
|
+
import_fs9.default.writeFileSync(target, f.content, "utf8");
|
|
21020
21158
|
}
|
|
21021
21159
|
}
|
|
21022
21160
|
}
|
|
@@ -21239,9 +21377,9 @@ function registerResourcesRoutes(ctx) {
|
|
|
21239
21377
|
}
|
|
21240
21378
|
|
|
21241
21379
|
// server/routes/courseware.ts
|
|
21242
|
-
var
|
|
21243
|
-
var
|
|
21244
|
-
var
|
|
21380
|
+
var import_path10 = __toESM(require("path"), 1);
|
|
21381
|
+
var import_fs10 = __toESM(require("fs"), 1);
|
|
21382
|
+
var import_crypto8 = __toESM(require("crypto"), 1);
|
|
21245
21383
|
function registerCoursewareRoutes(ctx) {
|
|
21246
21384
|
const { app, io } = ctx;
|
|
21247
21385
|
app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
@@ -21391,7 +21529,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21391
21529
|
return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
|
|
21392
21530
|
}
|
|
21393
21531
|
}
|
|
21394
|
-
const rawId = "raw_" +
|
|
21532
|
+
const rawId = "raw_" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
21395
21533
|
kernelContainer.db.prepare(
|
|
21396
21534
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
21397
21535
|
).run(rawId, attemptId, eventType, JSON.stringify(payload), Date.now());
|
|
@@ -21419,7 +21557,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21419
21557
|
kernelContainer.db.prepare(
|
|
21420
21558
|
"INSERT INTO submission_result (id, attempt_id, score, comment, completion, extra_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
21421
21559
|
).run(
|
|
21422
|
-
"res_" +
|
|
21560
|
+
"res_" + import_crypto8.default.randomBytes(8).toString("hex"),
|
|
21423
21561
|
attemptId,
|
|
21424
21562
|
parsedScore,
|
|
21425
21563
|
comment || null,
|
|
@@ -21445,7 +21583,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21445
21583
|
}
|
|
21446
21584
|
io.emit("courseware-attempt-updated", { attemptId, type: "log" });
|
|
21447
21585
|
void kernelContainer.eventBus.publish({
|
|
21448
|
-
id: "evt_" +
|
|
21586
|
+
id: "evt_" + import_crypto8.default.randomBytes(8).toString("hex"),
|
|
21449
21587
|
type: "courseware.event_logged",
|
|
21450
21588
|
source: "builtin.courseware",
|
|
21451
21589
|
payload: { attemptId, eventType, payload },
|
|
@@ -21542,8 +21680,8 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21542
21680
|
const { msg, url, student, courseware } = req.body;
|
|
21543
21681
|
const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
|
|
21544
21682
|
console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
|
|
21545
|
-
const logFile2 =
|
|
21546
|
-
|
|
21683
|
+
const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
|
|
21684
|
+
import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
|
|
21547
21685
|
`);
|
|
21548
21686
|
res.json({ success: true });
|
|
21549
21687
|
} catch (e) {
|
|
@@ -21619,7 +21757,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21619
21757
|
let assignment = kernelContainer.db.prepare("SELECT id FROM assignments WHERE class_id = ? AND lesson_id = ? AND title = ?").get(classId, lessonId, assignmentTitle);
|
|
21620
21758
|
let assignmentId = assignment?.id;
|
|
21621
21759
|
if (!assignmentId) {
|
|
21622
|
-
assignmentId = "ast-cw-" +
|
|
21760
|
+
assignmentId = "ast-cw-" + import_crypto8.default.randomBytes(8).toString("hex");
|
|
21623
21761
|
kernelContainer.db.prepare(
|
|
21624
21762
|
"INSERT INTO assignments (id, class_id, lesson_id, title, description, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
21625
21763
|
).run(
|
|
@@ -21692,8 +21830,8 @@ function registerCoursewareRoutes(ctx) {
|
|
|
21692
21830
|
}
|
|
21693
21831
|
|
|
21694
21832
|
// server/routes/bridge.ts
|
|
21695
|
-
var
|
|
21696
|
-
var
|
|
21833
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
21834
|
+
var import_fs11 = __toESM(require("fs"), 1);
|
|
21697
21835
|
function registerBridgeRoutes(ctx) {
|
|
21698
21836
|
const { app } = ctx;
|
|
21699
21837
|
app.get("/bridge.js", (_req, res) => {
|
|
@@ -21713,7 +21851,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21713
21851
|
if (!courseware) {
|
|
21714
21852
|
const resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ?").get(uuid);
|
|
21715
21853
|
if (resRow) {
|
|
21716
|
-
const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ?
|
|
21854
|
+
const entryName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : "index.html";
|
|
21717
21855
|
const createdAt = resRow.created_at || Date.now();
|
|
21718
21856
|
kernelContainer.db.prepare("INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(resRow.id, resRow.id, resRow.name, resRow.type || "html", entryName, createdAt);
|
|
21719
21857
|
courseware = {
|
|
@@ -21729,21 +21867,21 @@ function registerBridgeRoutes(ctx) {
|
|
|
21729
21867
|
if (!courseware) {
|
|
21730
21868
|
return res.status(404).send("Courseware not found");
|
|
21731
21869
|
}
|
|
21732
|
-
const storageDir =
|
|
21733
|
-
if (!
|
|
21870
|
+
const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", courseware.uuid);
|
|
21871
|
+
if (!import_fs11.default.existsSync(storageDir) || import_fs11.default.readdirSync(storageDir).length === 0) {
|
|
21734
21872
|
let resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE id = ? OR id = ?").get(courseware.uuid, courseware.id);
|
|
21735
21873
|
if (!resRow && courseware.name) {
|
|
21736
21874
|
const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
|
|
21737
21875
|
resRow = kernelContainer.db.prepare("SELECT * FROM system_resources WHERE name = ? OR name = ?").get(courseware.name, nameCandidate);
|
|
21738
21876
|
}
|
|
21739
21877
|
if (resRow && resRow.content) {
|
|
21740
|
-
|
|
21878
|
+
import_fs11.default.mkdirSync(storageDir, { recursive: true });
|
|
21741
21879
|
if (resRow.type === "html") {
|
|
21742
|
-
const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ?
|
|
21743
|
-
const safeEntry =
|
|
21744
|
-
|
|
21880
|
+
const originalName = resRow.name && (resRow.name.endsWith(".html") || resRow.name.endsWith(".htm")) ? import_path11.default.basename(resRow.name.replace(/\\/g, "/")) : courseware.entry || "index.html";
|
|
21881
|
+
const safeEntry = import_path11.default.basename(originalName.replace(/\\/g, "/"));
|
|
21882
|
+
import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, safeEntry), resRow.content, "utf8");
|
|
21745
21883
|
if (safeEntry !== "index.html") {
|
|
21746
|
-
|
|
21884
|
+
import_fs11.default.writeFileSync(import_path11.default.resolve(storageDir, "index.html"), resRow.content, "utf8");
|
|
21747
21885
|
}
|
|
21748
21886
|
} else if (resRow.type === "folder") {
|
|
21749
21887
|
try {
|
|
@@ -21751,15 +21889,15 @@ function registerBridgeRoutes(ctx) {
|
|
|
21751
21889
|
for (const f of files) {
|
|
21752
21890
|
if (f.path && f.content !== void 0) {
|
|
21753
21891
|
const cleanRel = f.path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21754
|
-
const target =
|
|
21892
|
+
const target = import_path11.default.resolve(storageDir, cleanRel);
|
|
21755
21893
|
if (target.startsWith(storageDir)) {
|
|
21756
|
-
|
|
21894
|
+
import_fs11.default.mkdirSync(import_path11.default.dirname(target), { recursive: true });
|
|
21757
21895
|
const isBin = /\.(png|jpe?g|gif|webp|ico)$/i.test(cleanRel);
|
|
21758
21896
|
if (isBin) {
|
|
21759
21897
|
const cleanBase64 = f.content.replace(/^data:[^;]+;base64,/, "");
|
|
21760
|
-
|
|
21898
|
+
import_fs11.default.writeFileSync(target, Buffer.from(cleanBase64, "base64"));
|
|
21761
21899
|
} else {
|
|
21762
|
-
|
|
21900
|
+
import_fs11.default.writeFileSync(target, f.content, "utf8");
|
|
21763
21901
|
}
|
|
21764
21902
|
}
|
|
21765
21903
|
}
|
|
@@ -21773,35 +21911,35 @@ function registerBridgeRoutes(ctx) {
|
|
|
21773
21911
|
if (!subpath || subpath === "") {
|
|
21774
21912
|
subpath = courseware.entry || "index.html";
|
|
21775
21913
|
}
|
|
21776
|
-
let filePath =
|
|
21914
|
+
let filePath = import_path11.default.resolve(storageDir, subpath);
|
|
21777
21915
|
if (!filePath.startsWith(storageDir)) {
|
|
21778
21916
|
return res.status(403).send("Access denied");
|
|
21779
21917
|
}
|
|
21780
|
-
if (!
|
|
21781
|
-
const dirEntries =
|
|
21918
|
+
if (!import_fs11.default.existsSync(filePath) && import_fs11.default.existsSync(storageDir)) {
|
|
21919
|
+
const dirEntries = import_fs11.default.readdirSync(storageDir);
|
|
21782
21920
|
const isLookingForEntry = !subpath || subpath === "" || subpath === "index.html" || subpath === "index.htm" || subpath === courseware.entry;
|
|
21783
21921
|
if (isLookingForEntry) {
|
|
21784
21922
|
let candidate = dirEntries.find(
|
|
21785
|
-
(f) => f === courseware.entry &&
|
|
21923
|
+
(f) => f === courseware.entry && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
|
|
21786
21924
|
);
|
|
21787
21925
|
if (!candidate && courseware.name) {
|
|
21788
21926
|
const nameCandidate = courseware.name.endsWith(".html") ? courseware.name : `${courseware.name}.html`;
|
|
21789
21927
|
candidate = dirEntries.find(
|
|
21790
|
-
(f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) &&
|
|
21928
|
+
(f) => (f === nameCandidate || f.toLowerCase() === nameCandidate.toLowerCase()) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile()
|
|
21791
21929
|
);
|
|
21792
21930
|
}
|
|
21793
21931
|
if (!candidate) {
|
|
21794
21932
|
candidate = dirEntries.find((f) => {
|
|
21795
21933
|
const lower = f.toLowerCase();
|
|
21796
|
-
return (lower.endsWith(".html") || lower.endsWith(".htm")) &&
|
|
21934
|
+
return (lower.endsWith(".html") || lower.endsWith(".htm")) && import_fs11.default.statSync(import_path11.default.join(storageDir, f)).isFile();
|
|
21797
21935
|
});
|
|
21798
21936
|
}
|
|
21799
21937
|
if (candidate) {
|
|
21800
|
-
filePath =
|
|
21938
|
+
filePath = import_path11.default.resolve(storageDir, candidate);
|
|
21801
21939
|
try {
|
|
21802
|
-
const indexHtmlPath =
|
|
21803
|
-
if (!
|
|
21804
|
-
|
|
21940
|
+
const indexHtmlPath = import_path11.default.resolve(storageDir, "index.html");
|
|
21941
|
+
if (!import_fs11.default.existsSync(indexHtmlPath)) {
|
|
21942
|
+
import_fs11.default.copyFileSync(filePath, indexHtmlPath);
|
|
21805
21943
|
}
|
|
21806
21944
|
if (courseware.entry !== candidate) {
|
|
21807
21945
|
kernelContainer.db.prepare("UPDATE courseware SET entry = ? WHERE id = ?").run(candidate, courseware.id);
|
|
@@ -21812,9 +21950,9 @@ function registerBridgeRoutes(ctx) {
|
|
|
21812
21950
|
}
|
|
21813
21951
|
} else {
|
|
21814
21952
|
for (const entry of dirEntries) {
|
|
21815
|
-
const subDirPath =
|
|
21816
|
-
if (
|
|
21817
|
-
const subEntries =
|
|
21953
|
+
const subDirPath = import_path11.default.join(storageDir, entry);
|
|
21954
|
+
if (import_fs11.default.statSync(subDirPath).isDirectory()) {
|
|
21955
|
+
const subEntries = import_fs11.default.readdirSync(subDirPath);
|
|
21818
21956
|
const subCandidate = subEntries.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || subEntries.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
|
|
21819
21957
|
if (subCandidate) {
|
|
21820
21958
|
const redirectPath = `${entry}/${subCandidate}`;
|
|
@@ -21825,12 +21963,12 @@ function registerBridgeRoutes(ctx) {
|
|
|
21825
21963
|
}
|
|
21826
21964
|
}
|
|
21827
21965
|
}
|
|
21828
|
-
if (!
|
|
21966
|
+
if (!import_fs11.default.existsSync(filePath)) {
|
|
21829
21967
|
return res.status(404).send(`File not found: ${subpath}`);
|
|
21830
21968
|
}
|
|
21831
|
-
const stat =
|
|
21969
|
+
const stat = import_fs11.default.statSync(filePath);
|
|
21832
21970
|
if (stat.isDirectory()) {
|
|
21833
|
-
const dirFiles =
|
|
21971
|
+
const dirFiles = import_fs11.default.readdirSync(filePath);
|
|
21834
21972
|
const htmlFile = dirFiles.find((f) => f.toLowerCase() === "index.html" || f.toLowerCase() === "index.htm") || dirFiles.find((f) => f.toLowerCase().endsWith(".html") || f.toLowerCase().endsWith(".htm"));
|
|
21835
21973
|
if (htmlFile) {
|
|
21836
21974
|
const base = subpath ? subpath.endsWith("/") ? subpath : subpath + "/" : "";
|
|
@@ -21838,7 +21976,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21838
21976
|
}
|
|
21839
21977
|
return res.status(404).send("Directory index not found");
|
|
21840
21978
|
}
|
|
21841
|
-
const ext =
|
|
21979
|
+
const ext = import_path11.default.extname(filePath).toLowerCase();
|
|
21842
21980
|
let contentType = "text/plain; charset=utf-8";
|
|
21843
21981
|
if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
|
|
21844
21982
|
else if (ext === ".css") contentType = "text/css; charset=utf-8";
|
|
@@ -21858,7 +21996,7 @@ function registerBridgeRoutes(ctx) {
|
|
|
21858
21996
|
"Content-Security-Policy",
|
|
21859
21997
|
"sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; script-src-attr 'unsafe-inline'; style-src-attr 'unsafe-inline';"
|
|
21860
21998
|
);
|
|
21861
|
-
let html =
|
|
21999
|
+
let html = import_fs11.default.readFileSync(filePath, "utf8");
|
|
21862
22000
|
html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
|
|
21863
22001
|
return res.send(html);
|
|
21864
22002
|
} else {
|
|
@@ -22448,7 +22586,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
22448
22586
|
}
|
|
22449
22587
|
|
|
22450
22588
|
// server/routes/workspace.ts
|
|
22451
|
-
var
|
|
22589
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
22452
22590
|
function registerWorkspaceRoutes(ctx) {
|
|
22453
22591
|
const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
|
|
22454
22592
|
app.get("/api/events", requireAuth("administrator"), (req, res) => {
|
|
@@ -22597,7 +22735,7 @@ ${sub.content}`
|
|
|
22597
22735
|
}
|
|
22598
22736
|
const filename = parts[parts.length - 1];
|
|
22599
22737
|
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
|
|
22600
|
-
const ext =
|
|
22738
|
+
const ext = import_path12.default.extname(filename).toLowerCase();
|
|
22601
22739
|
const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
|
|
22602
22740
|
const content = foundNode.content || "";
|
|
22603
22741
|
if (binaryExtensions.includes(ext)) {
|
|
@@ -22839,9 +22977,9 @@ function registerAdminRoutes(ctx) {
|
|
|
22839
22977
|
}
|
|
22840
22978
|
|
|
22841
22979
|
// server/routes/roster.ts
|
|
22842
|
-
var
|
|
22843
|
-
var
|
|
22844
|
-
var
|
|
22980
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
22981
|
+
var import_fs12 = __toESM(require("fs"), 1);
|
|
22982
|
+
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
22845
22983
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
22846
22984
|
function registerRosterRoutes(ctx) {
|
|
22847
22985
|
const { app, io, loginLimiter } = ctx;
|
|
@@ -23095,7 +23233,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23095
23233
|
if (storedPwd.startsWith("$2")) {
|
|
23096
23234
|
matches = import_bcryptjs2.default.compareSync(oldPassword, storedPwd);
|
|
23097
23235
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
23098
|
-
matches =
|
|
23236
|
+
matches = import_crypto9.default.createHash("sha256").update(oldPassword).digest("hex") === storedPwd;
|
|
23099
23237
|
} else {
|
|
23100
23238
|
matches = storedPwd === oldPassword;
|
|
23101
23239
|
}
|
|
@@ -23152,7 +23290,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23152
23290
|
if (!filename || !base64Data) {
|
|
23153
23291
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
23154
23292
|
}
|
|
23155
|
-
const ext =
|
|
23293
|
+
const ext = import_path13.default.extname(filename).toLowerCase();
|
|
23156
23294
|
const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
23157
23295
|
if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
|
|
23158
23296
|
return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
|
|
@@ -23165,11 +23303,11 @@ function registerRosterRoutes(ctx) {
|
|
|
23165
23303
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
23166
23304
|
return res.status(400).json({ error: "File content does not match the declared image type" });
|
|
23167
23305
|
}
|
|
23168
|
-
const avatarDir =
|
|
23169
|
-
|
|
23170
|
-
const uniqueName = `${Date.now()}-${
|
|
23171
|
-
const filePath =
|
|
23172
|
-
|
|
23306
|
+
const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
|
|
23307
|
+
import_fs12.default.mkdirSync(avatarDir, { recursive: true });
|
|
23308
|
+
const uniqueName = `${Date.now()}-${import_crypto9.default.randomBytes(4).toString("hex")}${ext}`;
|
|
23309
|
+
const filePath = import_path13.default.join(avatarDir, uniqueName);
|
|
23310
|
+
import_fs12.default.writeFileSync(filePath, fileBuffer);
|
|
23173
23311
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
23174
23312
|
let oldAvatar = null;
|
|
23175
23313
|
if (session.role === "student") {
|
|
@@ -23183,7 +23321,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23183
23321
|
}
|
|
23184
23322
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
23185
23323
|
try {
|
|
23186
|
-
|
|
23324
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
23187
23325
|
} catch {
|
|
23188
23326
|
}
|
|
23189
23327
|
}
|
|
@@ -23219,7 +23357,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23219
23357
|
}
|
|
23220
23358
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
23221
23359
|
try {
|
|
23222
|
-
|
|
23360
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
23223
23361
|
} catch {
|
|
23224
23362
|
}
|
|
23225
23363
|
}
|
|
@@ -23286,7 +23424,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23286
23424
|
if (storedPwd.startsWith("$2")) {
|
|
23287
23425
|
matchesOwnPassword = import_bcryptjs2.default.compareSync(providedPassword, storedPwd);
|
|
23288
23426
|
} else if (/^[a-f0-9]{64}$/.test(storedPwd)) {
|
|
23289
|
-
const sha256Hash =
|
|
23427
|
+
const sha256Hash = import_crypto9.default.createHash("sha256").update(providedPassword).digest("hex");
|
|
23290
23428
|
if (sha256Hash === storedPwd) {
|
|
23291
23429
|
matchesOwnPassword = true;
|
|
23292
23430
|
kernelContainer.db.prepare("UPDATE students SET password = ? WHERE id = ?").run(hashPassword(providedPassword), studentObj.id);
|
|
@@ -23330,7 +23468,7 @@ function registerRosterRoutes(ctx) {
|
|
|
23330
23468
|
};
|
|
23331
23469
|
}
|
|
23332
23470
|
if (sessionData) {
|
|
23333
|
-
const sessionToken = "token_" +
|
|
23471
|
+
const sessionToken = "token_" + import_crypto9.default.randomBytes(16).toString("hex");
|
|
23334
23472
|
const now = Date.now();
|
|
23335
23473
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
23336
23474
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
@@ -25168,8 +25306,8 @@ ${examsText}
|
|
|
25168
25306
|
|
|
25169
25307
|
// server/routes/plugins.ts
|
|
25170
25308
|
var import_express2 = __toESM(require("express"), 1);
|
|
25171
|
-
var
|
|
25172
|
-
var
|
|
25309
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
25310
|
+
var import_fs13 = __toESM(require("fs"), 1);
|
|
25173
25311
|
var import_semver3 = __toESM(require("semver"), 1);
|
|
25174
25312
|
|
|
25175
25313
|
// server/services/version-fetcher.ts
|
|
@@ -25327,7 +25465,7 @@ async function checkVersion(source, installedVersion) {
|
|
|
25327
25465
|
}
|
|
25328
25466
|
|
|
25329
25467
|
// server/routes/plugin-api-gateway.ts
|
|
25330
|
-
var
|
|
25468
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
25331
25469
|
init_http_router();
|
|
25332
25470
|
var RESERVED_ACTIONS = /* @__PURE__ */ new Set(["config", "toggle", "contributions", "check-update", "one-click-update"]);
|
|
25333
25471
|
var FORBIDDEN_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -25461,7 +25599,7 @@ async function pluginApiGatewayMiddleware(req, res, next) {
|
|
|
25461
25599
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
25462
25600
|
return;
|
|
25463
25601
|
}
|
|
25464
|
-
const normalizedSubPath =
|
|
25602
|
+
const normalizedSubPath = import_node_path4.default.posix.normalize("/" + rawSubPath);
|
|
25465
25603
|
if (normalizedSubPath.includes("..")) {
|
|
25466
25604
|
res.status(400).json({ success: false, error: "Path traversal attempt detected" });
|
|
25467
25605
|
return;
|
|
@@ -25681,16 +25819,16 @@ data: ${JSON.stringify({ error: msg })}
|
|
|
25681
25819
|
|
|
25682
25820
|
// server/routes/plugins.ts
|
|
25683
25821
|
function findLocalPluginSource(manifestId) {
|
|
25684
|
-
const root =
|
|
25685
|
-
if (!
|
|
25822
|
+
const root = import_path14.default.resolve(process.cwd(), "v2_plugins");
|
|
25823
|
+
if (!import_fs13.default.existsSync(root)) return null;
|
|
25686
25824
|
let best = null;
|
|
25687
25825
|
try {
|
|
25688
|
-
for (const entry of
|
|
25826
|
+
for (const entry of import_fs13.default.readdirSync(root, { withFileTypes: true })) {
|
|
25689
25827
|
if (!entry.isDirectory()) continue;
|
|
25690
|
-
const manifestPath =
|
|
25691
|
-
if (!
|
|
25828
|
+
const manifestPath = import_path14.default.join(root, entry.name, "manifest.json");
|
|
25829
|
+
if (!import_fs13.default.existsSync(manifestPath)) continue;
|
|
25692
25830
|
try {
|
|
25693
|
-
const m = JSON.parse(
|
|
25831
|
+
const m = JSON.parse(import_fs13.default.readFileSync(manifestPath, "utf-8"));
|
|
25694
25832
|
if (m?.id !== manifestId || !m?.version) continue;
|
|
25695
25833
|
if (!best || import_semver3.default.gt(m.version, best.version)) best = { dir: entry.name, version: m.version };
|
|
25696
25834
|
} catch {
|
|
@@ -25726,11 +25864,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25726
25864
|
const { app, buildOpenAITools: buildOpenAITools2, executeAgentToolCall: executeAgentToolCall2, buildOpenAIChatUrl: buildOpenAIChatUrl2, runGeminiAgentChat: runGeminiAgentChat2, runOpenAIAgentChat: runOpenAIAgentChat2 } = ctx;
|
|
25727
25865
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
25728
25866
|
try {
|
|
25729
|
-
const docPath =
|
|
25730
|
-
if (!
|
|
25867
|
+
const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
25868
|
+
if (!import_fs13.default.existsSync(docPath)) {
|
|
25731
25869
|
return res.status(404).json({ success: false, error: "Document not found" });
|
|
25732
25870
|
}
|
|
25733
|
-
const content =
|
|
25871
|
+
const content = import_fs13.default.readFileSync(docPath, "utf-8");
|
|
25734
25872
|
res.json({ success: true, content });
|
|
25735
25873
|
} catch (e) {
|
|
25736
25874
|
sendSafeError(res, e);
|
|
@@ -25741,11 +25879,11 @@ function registerPluginsRoutes(ctx) {
|
|
|
25741
25879
|
const limit = parseInt(req.query.limit) || 200;
|
|
25742
25880
|
const component = req.query.component;
|
|
25743
25881
|
const levelFilter = req.query.level;
|
|
25744
|
-
const logFile2 =
|
|
25745
|
-
if (!
|
|
25882
|
+
const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
|
|
25883
|
+
if (!import_fs13.default.existsSync(logFile2)) {
|
|
25746
25884
|
return res.json({ success: true, logs: [] });
|
|
25747
25885
|
}
|
|
25748
|
-
const content =
|
|
25886
|
+
const content = import_fs13.default.readFileSync(logFile2, "utf-8");
|
|
25749
25887
|
const lines = content.split("\n").filter(Boolean);
|
|
25750
25888
|
let parsedLogs = lines.map((line) => {
|
|
25751
25889
|
try {
|
|
@@ -26307,7 +26445,7 @@ async function startServer() {
|
|
|
26307
26445
|
config: { port: Number(process.env.PORT) || 9e3 }
|
|
26308
26446
|
});
|
|
26309
26447
|
try {
|
|
26310
|
-
const migrationsDir =
|
|
26448
|
+
const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
|
|
26311
26449
|
const migrations = loadMigrationsFromDirectory(migrationsDir);
|
|
26312
26450
|
if (migrations.length > 0) {
|
|
26313
26451
|
runMigrations(kernelContainer.db, migrations);
|
|
@@ -26371,8 +26509,8 @@ async function startServer() {
|
|
|
26371
26509
|
});
|
|
26372
26510
|
app.use(import_express3.default.json({ limit: "10mb" }));
|
|
26373
26511
|
app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
|
|
26374
|
-
app.use("/uploads", import_express3.default.static(
|
|
26375
|
-
app.use("/plugins", import_express3.default.static(
|
|
26512
|
+
app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
|
|
26513
|
+
app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
|
|
26376
26514
|
const configuredOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
26377
26515
|
const isOriginAllowed = (origin, hostHeader) => {
|
|
26378
26516
|
if (!origin) return true;
|
|
@@ -26545,10 +26683,10 @@ async function startServer() {
|
|
|
26545
26683
|
});
|
|
26546
26684
|
app.use(vite.middlewares);
|
|
26547
26685
|
} else {
|
|
26548
|
-
const distPath = __dirname ||
|
|
26686
|
+
const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
|
|
26549
26687
|
app.use(import_express3.default.static(distPath));
|
|
26550
26688
|
app.get("*", (req, res) => {
|
|
26551
|
-
res.sendFile(
|
|
26689
|
+
res.sendFile(import_path15.default.join(distPath, "index.html"));
|
|
26552
26690
|
});
|
|
26553
26691
|
}
|
|
26554
26692
|
const HOST = process.env.HOST || "0.0.0.0";
|