opencami 1.8.2 → 1.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -14
- package/bin/opencami.js +15 -6
- package/dist/client/assets/{CSPContext-DeJH85nm.js → CSPContext-6t3O1emU.js} +1 -1
- package/dist/client/assets/{DirectionContext-CxhRpXkm.js → DirectionContext-C6goXEY_.js} +1 -1
- package/dist/client/assets/_sessionKey-B5Viv43f.js +23 -0
- package/dist/client/assets/agents-BmE6QOwl.js +2 -0
- package/dist/client/assets/agents-screen-pHUzJxX5.js +1 -0
- package/dist/client/assets/bots-BeOkwrXr.js +2 -0
- package/dist/client/assets/{bots-screen-Be3cfGgq.js → bots-screen-B79bAYvf.js} +1 -1
- package/dist/client/assets/{button-D9Plv7hu.js → button-CK8tKQ-Z.js} +1 -1
- package/dist/client/assets/{composite-B2KCZKKL.js → composite-feK0c-xF.js} +1 -1
- package/dist/client/assets/{connect-DuJfnyNK.js → connect-02tmQV_v.js} +1 -1
- package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/client/assets/{dashboard-00GpXm5V.js → dashboard-DQ0zDQKd.js} +1 -1
- package/dist/client/assets/event-BsD1rqGT.js +1 -0
- package/dist/client/assets/file-explorer-screen-Ds7LeJTd.js +1 -0
- package/dist/client/assets/files-e40B1zFy.js +2 -0
- package/dist/client/assets/go-CxLEBnE3.js +1 -0
- package/dist/client/assets/{index-Yo5UhdZV.js → index-lK3yGoTI.js} +1 -1
- package/dist/client/assets/{index-DtGzE-ea.js → index-rljDU_1M.js} +2 -2
- package/dist/client/assets/keyboard-shortcuts-dialog-Bb_GOr9L.js +1 -0
- package/dist/client/assets/main-Dq6jpr6-.js +210 -0
- package/dist/client/assets/{markdown-DtWnt4NA.js → markdown-C7_Aipwd.js} +37 -37
- package/dist/client/assets/memory-C7UG-1le.js +2 -0
- package/dist/client/assets/memory-screen-CUFBWsq5.js +1 -0
- package/dist/client/assets/menu-n6L--M9R.js +1 -0
- package/dist/client/assets/{opencami-logo-Bmge6-FB.js → opencami-logo-zuSBm5Br.js} +1 -1
- package/dist/client/assets/php-Dhbhpdrm.js +1 -0
- package/dist/client/assets/proxy-BU8Bw1Vt.js +9 -0
- package/dist/client/assets/{react-DODKNyyU.js → react-BLyCEWpN.js} +1 -1
- package/dist/client/assets/ruby-NiQIzKut.js +1 -0
- package/dist/client/assets/search-dialog-yB4w5ajo.js +1 -0
- package/dist/client/assets/session-export-dialog-qbZgd2Zo.js +1 -0
- package/dist/client/assets/settings-dialog-CHJbvpgk.js +1 -0
- package/dist/client/assets/skills-DoKPPhNY.js +2 -0
- package/dist/client/assets/{skills-panel-DSiH-DLs.js → skills-panel-BH27r3nC.js} +1 -1
- package/dist/client/assets/styles-CXV5jZiD.css +1 -0
- package/dist/client/assets/{swift-Dg5xB15N.js → swift-D82vCrfD.js} +1 -1
- package/dist/client/assets/switch-BD3a0LRm.js +1 -0
- package/dist/client/assets/tabs-DI1e-kzz.js +1 -0
- package/dist/client/assets/tooltip-BbH3QWvK.js +1 -0
- package/dist/client/assets/use-file-explorer-state-DBfLeAyz.js +12 -0
- package/dist/client/assets/{useBaseUiId-KQTzRPLp.js → useBaseUiId-MgM4ouhx.js} +1 -1
- package/dist/client/assets/{useCompositeItem-BPY2_hF_.js → useCompositeItem-OhltNFdZ.js} +1 -1
- package/dist/client/assets/{useControlled-B5pEEz2V.js → useControlled-BQxTgsOd.js} +1 -1
- package/dist/client/assets/{useMutation-BsQD6FKe.js → useMutation-12DyB3Ox.js} +1 -1
- package/dist/client/assets/useOnFirstRender-7qoaK5sI.js +1 -0
- package/dist/client/assets/{useQuery-CmAJuY2W.js → useQuery-Ctiljcrr.js} +1 -1
- package/dist/server/assets/{_sessionKey-Bq_fl7uv.js → _sessionKey-CH8wIyED.js} +3 -3
- package/dist/server/assets/{_tanstack-start-manifest_v-BMCAWon2.js → _tanstack-start-manifest_v-C5HBDfQB.js} +1 -1
- package/dist/server/assets/{index-C2hVqxBl.js → index-NcNCVGTL.js} +4 -3
- package/dist/server/assets/{router-bN_iTo0B.js → router-Brzpnz55.js} +449 -70
- package/dist/server/assets/{search-dialog-DReM5ZD2.js → search-dialog-BNhjVvKc.js} +5 -4
- package/dist/server/assets/{settings-dialog-BUOrQN3Z.js → settings-dialog-CWcmfDiV.js} +5 -4
- package/dist/server/server.js +195 -38
- package/package.json +4 -1
- package/dist/client/assets/_sessionKey-CQE0brGK.js +0 -23
- package/dist/client/assets/agents-CMTFd_sG.js +0 -2
- package/dist/client/assets/agents-screen-BNQGEqcW.js +0 -1
- package/dist/client/assets/bots-B6oGzCxP.js +0 -2
- package/dist/client/assets/csharp-K5feNrxe.js +0 -1
- package/dist/client/assets/event-DD8Cz4O9.js +0 -1
- package/dist/client/assets/file-explorer-screen-CxwemBES.js +0 -1
- package/dist/client/assets/files-DyBJVXBu.js +0 -2
- package/dist/client/assets/go-Dn2_MT6a.js +0 -1
- package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +0 -1
- package/dist/client/assets/main-CgwdHc9W.js +0 -210
- package/dist/client/assets/memory-l756yiNq.js +0 -2
- package/dist/client/assets/memory-screen-BQtVRuzE.js +0 -1
- package/dist/client/assets/menu-BsS6CDf_.js +0 -1
- package/dist/client/assets/php-CDn_0X-4.js +0 -1
- package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +0 -1
- package/dist/client/assets/proxy-CYZeDXoy.js +0 -9
- package/dist/client/assets/ruby-FDmvQDUv.js +0 -1
- package/dist/client/assets/search-dialog-DW91SK30.js +0 -1
- package/dist/client/assets/session-export-dialog-CliO9Ob-.js +0 -1
- package/dist/client/assets/settings-dialog-C1u52aju.js +0 -1
- package/dist/client/assets/skills-8T_avaVb.js +0 -2
- package/dist/client/assets/styles-DvaLh0o1.css +0 -1
- package/dist/client/assets/switch-DbgQPO6i.js +0 -1
- package/dist/client/assets/tabs-BsAvZnlD.js +0 -1
- package/dist/client/assets/tooltip-DLmutB5C.js +0 -1
- package/dist/client/assets/use-file-explorer-state-Cg_yDYJl.js +0 -12
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { createRootRoute, Outlet, HeadContent, Scripts, createFileRoute, lazyRouteComponent, redirect, createRouter } from "@tanstack/react-router";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import
|
|
6
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
-
import path, { join, resolve, relative, extname } from "node:path";
|
|
4
|
+
import crypto, { randomUUID } from "node:crypto";
|
|
5
|
+
import fs, { readFileSync, existsSync } from "node:fs";
|
|
8
6
|
import os, { homedir } from "node:os";
|
|
7
|
+
import path, { join, resolve, relative, extname } from "node:path";
|
|
8
|
+
import WebSocket from "ws";
|
|
9
9
|
import { json } from "@tanstack/router-core/ssr/client";
|
|
10
10
|
import { PassThrough, Readable } from "node:stream";
|
|
11
|
-
import { execSync } from "node:child_process";
|
|
11
|
+
import { execFile, execSync } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
12
13
|
import { readFile, mkdir, writeFile, rename, stat, readdir, rm, realpath, lstat } from "node:fs/promises";
|
|
13
14
|
import { posix } from "path";
|
|
14
|
-
const appCss = "/assets/styles-
|
|
15
|
+
const appCss = "/assets/styles-CXV5jZiD.css";
|
|
15
16
|
const swRegisterScript = `
|
|
16
17
|
(() => {
|
|
17
18
|
// Skip PWA service worker inside Capacitor native shell — they conflict
|
|
@@ -369,11 +370,11 @@ const $$splitComponentImporter$2 = () => import("./agents-CmQ4vvXm.js");
|
|
|
369
370
|
const Route$t = createFileRoute("/agents")({
|
|
370
371
|
component: lazyRouteComponent($$splitComponentImporter$2, "component")
|
|
371
372
|
});
|
|
372
|
-
const $$splitComponentImporter$1 = () => import("./index-
|
|
373
|
+
const $$splitComponentImporter$1 = () => import("./index-NcNCVGTL.js");
|
|
373
374
|
const Route$s = createFileRoute("/")({
|
|
374
375
|
component: lazyRouteComponent($$splitComponentImporter$1, "component")
|
|
375
376
|
});
|
|
376
|
-
const $$splitComponentImporter = () => import("./_sessionKey-
|
|
377
|
+
const $$splitComponentImporter = () => import("./_sessionKey-CH8wIyED.js").then((n) => n.$);
|
|
377
378
|
const Route$r = createFileRoute("/chat/$sessionKey")({
|
|
378
379
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
379
380
|
});
|
|
@@ -388,26 +389,202 @@ function getGatewayConfig() {
|
|
|
388
389
|
}
|
|
389
390
|
return { url, token, password };
|
|
390
391
|
}
|
|
391
|
-
|
|
392
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
393
|
+
function base64UrlEncode(buf) {
|
|
394
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
395
|
+
}
|
|
396
|
+
function derivePublicKeyRaw(publicKeyPem) {
|
|
397
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
398
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
399
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
400
|
+
}
|
|
401
|
+
return spki;
|
|
402
|
+
}
|
|
403
|
+
function fingerprintPublicKey(publicKeyPem) {
|
|
404
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
405
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
406
|
+
}
|
|
407
|
+
function ensureDir(filePath) {
|
|
408
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
function resolveDeviceIdentityPath() {
|
|
411
|
+
return path.join(os.homedir(), ".opencami", "identity", "device.json");
|
|
412
|
+
}
|
|
413
|
+
function resolveDeviceTokensPath() {
|
|
414
|
+
return path.join(os.homedir(), ".opencami", "identity", "device-tokens.json");
|
|
415
|
+
}
|
|
416
|
+
function hashGatewayUrl(url) {
|
|
417
|
+
return crypto.createHash("sha256").update(url.trim()).digest("hex");
|
|
418
|
+
}
|
|
419
|
+
function loadDeviceToken(deviceId, gatewayUrl) {
|
|
420
|
+
const filePath = resolveDeviceTokensPath();
|
|
421
|
+
try {
|
|
422
|
+
if (!fs.existsSync(filePath)) return "";
|
|
423
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
424
|
+
const parsed = JSON.parse(raw);
|
|
425
|
+
const key = `${deviceId}:${hashGatewayUrl(gatewayUrl)}`;
|
|
426
|
+
const token = parsed[key];
|
|
427
|
+
return typeof token === "string" ? token : "";
|
|
428
|
+
} catch {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function storeDeviceToken(deviceId, gatewayUrl, token) {
|
|
433
|
+
if (!token) return;
|
|
434
|
+
const filePath = resolveDeviceTokensPath();
|
|
435
|
+
const key = `${deviceId}:${hashGatewayUrl(gatewayUrl)}`;
|
|
436
|
+
try {
|
|
437
|
+
let parsed = {};
|
|
438
|
+
if (fs.existsSync(filePath)) {
|
|
439
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
440
|
+
parsed = JSON.parse(raw);
|
|
441
|
+
}
|
|
442
|
+
parsed[key] = token;
|
|
443
|
+
ensureDir(filePath);
|
|
444
|
+
fs.writeFileSync(filePath, `${JSON.stringify(parsed, null, 2)}
|
|
445
|
+
`, { mode: 384 });
|
|
446
|
+
try {
|
|
447
|
+
fs.chmodSync(filePath, 384);
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function loadOrCreateDeviceIdentity(filePath = resolveDeviceIdentityPath()) {
|
|
454
|
+
try {
|
|
455
|
+
if (fs.existsSync(filePath)) {
|
|
456
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
457
|
+
const parsed = JSON.parse(raw);
|
|
458
|
+
if (parsed?.version === 1 && typeof parsed.deviceId === "string" && typeof parsed.publicKeyPem === "string" && typeof parsed.privateKeyPem === "string") {
|
|
459
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
460
|
+
if (derivedId && derivedId !== parsed.deviceId) {
|
|
461
|
+
const updated = { ...parsed, deviceId: derivedId };
|
|
462
|
+
fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}
|
|
463
|
+
`, { mode: 384 });
|
|
464
|
+
try {
|
|
465
|
+
fs.chmodSync(filePath, 384);
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
469
|
+
}
|
|
470
|
+
return { deviceId: parsed.deviceId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
476
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
477
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
478
|
+
const deviceId = fingerprintPublicKey(publicKeyPem);
|
|
479
|
+
ensureDir(filePath);
|
|
480
|
+
const stored = {
|
|
481
|
+
version: 1,
|
|
482
|
+
deviceId,
|
|
483
|
+
publicKeyPem,
|
|
484
|
+
privateKeyPem,
|
|
485
|
+
createdAtMs: Date.now()
|
|
486
|
+
};
|
|
487
|
+
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
|
|
488
|
+
`, { mode: 384 });
|
|
489
|
+
try {
|
|
490
|
+
fs.chmodSync(filePath, 384);
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
return { deviceId, publicKeyPem, privateKeyPem };
|
|
494
|
+
}
|
|
495
|
+
function signDevicePayload(privateKeyPem, payload) {
|
|
496
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
497
|
+
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
|
|
498
|
+
}
|
|
499
|
+
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
|
|
500
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
501
|
+
}
|
|
502
|
+
function buildDeviceAuthPayload(args) {
|
|
503
|
+
const scopes = args.scopes.join(",");
|
|
504
|
+
const token = args.token ?? "";
|
|
505
|
+
return ["v2", args.deviceId, args.clientId, args.clientMode, args.role, scopes, String(args.signedAtMs), token, args.nonce].join("|");
|
|
506
|
+
}
|
|
507
|
+
function loadOrCreateInstanceId() {
|
|
508
|
+
const filePath = path.join(os.homedir(), ".opencami", "identity", "instance-id.txt");
|
|
509
|
+
try {
|
|
510
|
+
if (fs.existsSync(filePath)) {
|
|
511
|
+
const v2 = fs.readFileSync(filePath, "utf8").trim();
|
|
512
|
+
if (v2) return v2;
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
const v = randomUUID();
|
|
517
|
+
ensureDir(filePath);
|
|
518
|
+
fs.writeFileSync(filePath, `${v}
|
|
519
|
+
`, { mode: 384 });
|
|
520
|
+
try {
|
|
521
|
+
fs.chmodSync(filePath, 384);
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
return v;
|
|
525
|
+
}
|
|
526
|
+
function buildConnectParams(url, token, password, nonce) {
|
|
527
|
+
const clientId = "openclaw-control-ui";
|
|
528
|
+
const clientMode = "webchat";
|
|
529
|
+
const role = "operator";
|
|
530
|
+
const scopes = ["operator.admin", "operator.approvals", "operator.pairing"];
|
|
531
|
+
if (!nonce) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
"OpenClaw did not send connect.challenge nonce in time. If you are connecting cross-origin, ensure your origin is allowed (gateway.controlUi.allowedOrigins)."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const identity = loadOrCreateDeviceIdentity();
|
|
537
|
+
const storedToken = loadDeviceToken(identity.deviceId, url);
|
|
538
|
+
const signedAtMs = Date.now();
|
|
539
|
+
const payload = buildDeviceAuthPayload({
|
|
540
|
+
deviceId: identity.deviceId,
|
|
541
|
+
clientId,
|
|
542
|
+
clientMode,
|
|
543
|
+
role,
|
|
544
|
+
scopes,
|
|
545
|
+
signedAtMs,
|
|
546
|
+
token: token || null,
|
|
547
|
+
nonce
|
|
548
|
+
});
|
|
549
|
+
const signature = signDevicePayload(identity.privateKeyPem, payload);
|
|
392
550
|
return {
|
|
393
551
|
minProtocol: 3,
|
|
394
552
|
maxProtocol: 3,
|
|
395
553
|
client: {
|
|
396
|
-
id:
|
|
554
|
+
id: clientId,
|
|
397
555
|
displayName: "OpenCami",
|
|
398
556
|
version: "dev",
|
|
399
557
|
platform: process.platform,
|
|
400
|
-
mode:
|
|
401
|
-
instanceId:
|
|
558
|
+
mode: clientMode,
|
|
559
|
+
instanceId: loadOrCreateInstanceId()
|
|
402
560
|
},
|
|
561
|
+
caps: [],
|
|
403
562
|
auth: {
|
|
404
563
|
token: token || void 0,
|
|
405
|
-
password: password || void 0
|
|
564
|
+
password: password || void 0,
|
|
565
|
+
deviceToken: storedToken || void 0
|
|
406
566
|
},
|
|
407
567
|
role: "operator",
|
|
408
|
-
scopes
|
|
568
|
+
scopes,
|
|
569
|
+
device: {
|
|
570
|
+
id: identity.deviceId,
|
|
571
|
+
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
|
572
|
+
signature,
|
|
573
|
+
signedAt: signedAtMs,
|
|
574
|
+
nonce
|
|
575
|
+
},
|
|
576
|
+
userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
|
|
577
|
+
locale: process.env.LANG || "en"
|
|
409
578
|
};
|
|
410
579
|
}
|
|
580
|
+
class GatewayResponseError extends Error {
|
|
581
|
+
code;
|
|
582
|
+
constructor(message, code) {
|
|
583
|
+
super(message);
|
|
584
|
+
this.name = "GatewayResponseError";
|
|
585
|
+
this.code = code;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
411
588
|
class PersistentGatewayConnection {
|
|
412
589
|
ws = null;
|
|
413
590
|
connected = false;
|
|
@@ -417,6 +594,8 @@ class PersistentGatewayConnection {
|
|
|
417
594
|
reconnectDelay = 1e3;
|
|
418
595
|
maxReconnectDelay = 3e4;
|
|
419
596
|
destroyed = false;
|
|
597
|
+
_devicePending = false;
|
|
598
|
+
_deviceId = "";
|
|
420
599
|
// Event listeners keyed by sessionKey — each sessionKey can have multiple listeners
|
|
421
600
|
sessionListeners = /* @__PURE__ */ new Map();
|
|
422
601
|
// Listeners that receive ALL events (for debugging or global subscriptions)
|
|
@@ -426,6 +605,12 @@ class PersistentGatewayConnection {
|
|
|
426
605
|
get isConnected() {
|
|
427
606
|
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
428
607
|
}
|
|
608
|
+
getDeviceStatus() {
|
|
609
|
+
return {
|
|
610
|
+
deviceId: this._deviceId,
|
|
611
|
+
isPending: this._devicePending
|
|
612
|
+
};
|
|
613
|
+
}
|
|
429
614
|
/** Ensure the persistent connection is up and authenticated. */
|
|
430
615
|
async ensureConnected() {
|
|
431
616
|
if (this.isConnected) return;
|
|
@@ -440,7 +625,8 @@ class PersistentGatewayConnection {
|
|
|
440
625
|
async _connect() {
|
|
441
626
|
if (this.destroyed) throw new Error("Connection destroyed");
|
|
442
627
|
const { url, token, password } = getGatewayConfig();
|
|
443
|
-
const
|
|
628
|
+
const origin = process.env.OPENCAMI_ORIGIN?.trim();
|
|
629
|
+
const ws = origin ? new WebSocket(url, { headers: { Origin: origin } }) : new WebSocket(url);
|
|
444
630
|
this.ws = ws;
|
|
445
631
|
await new Promise((resolve2, reject) => {
|
|
446
632
|
const onOpen = () => {
|
|
@@ -458,19 +644,105 @@ class PersistentGatewayConnection {
|
|
|
458
644
|
ws.on("open", onOpen);
|
|
459
645
|
ws.on("error", onError);
|
|
460
646
|
});
|
|
647
|
+
const nonce = await new Promise((resolve2) => {
|
|
648
|
+
let done = false;
|
|
649
|
+
const timer = setTimeout(() => {
|
|
650
|
+
if (done) return;
|
|
651
|
+
done = true;
|
|
652
|
+
resolve2("");
|
|
653
|
+
}, 3e3);
|
|
654
|
+
const onMessage = (data) => {
|
|
655
|
+
try {
|
|
656
|
+
const str = typeof data === "string" ? data : data.toString();
|
|
657
|
+
const parsed = JSON.parse(str);
|
|
658
|
+
if (parsed.type === "event" && parsed.event === "connect.challenge") {
|
|
659
|
+
const n = parsed.payload?.nonce;
|
|
660
|
+
if (typeof n === "string" && n.length > 0) {
|
|
661
|
+
clearTimeout(timer);
|
|
662
|
+
ws.off("message", onMessage);
|
|
663
|
+
if (done) return;
|
|
664
|
+
done = true;
|
|
665
|
+
resolve2(n);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
ws.on("message", onMessage);
|
|
672
|
+
});
|
|
461
673
|
ws.on("message", (data) => this._onMessage(data));
|
|
462
674
|
ws.on("close", () => this._onClose());
|
|
463
675
|
ws.on("error", () => {
|
|
464
676
|
});
|
|
465
677
|
const connectId = randomUUID();
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
id
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
678
|
+
const shouldFallback = process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "1" || process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "true";
|
|
679
|
+
try {
|
|
680
|
+
const connectParams = buildConnectParams(url, token, password, nonce);
|
|
681
|
+
this._deviceId = connectParams.device?.id ?? "";
|
|
682
|
+
ws.send(
|
|
683
|
+
JSON.stringify({
|
|
684
|
+
type: "req",
|
|
685
|
+
id: connectId,
|
|
686
|
+
method: "connect",
|
|
687
|
+
params: connectParams
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
const hello = await this._waitForRes(connectId, 1e4);
|
|
691
|
+
if (hello?.auth?.deviceToken) {
|
|
692
|
+
const identity = loadOrCreateDeviceIdentity();
|
|
693
|
+
storeDeviceToken(identity.deviceId, url, hello.auth.deviceToken);
|
|
694
|
+
}
|
|
695
|
+
const grantedScopes = hello?.auth?.scopes;
|
|
696
|
+
if (Array.isArray(grantedScopes) && !grantedScopes.includes("operator.read")) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Gateway connected but missing required scope: operator.read (granted: ${grantedScopes.join(", ")})`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
this._devicePending = false;
|
|
702
|
+
} catch (err) {
|
|
703
|
+
const code = err instanceof GatewayResponseError ? err.code : "";
|
|
704
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
705
|
+
if (code === "device_pending" || message.toLowerCase().includes("pending")) {
|
|
706
|
+
this._devicePending = true;
|
|
707
|
+
}
|
|
708
|
+
if (!shouldFallback) throw err;
|
|
709
|
+
console.warn(
|
|
710
|
+
"[gateway-ws] Device auth connect failed; retrying without device identity (fallback enabled):",
|
|
711
|
+
err instanceof Error ? err.message : err
|
|
712
|
+
);
|
|
713
|
+
const fallbackId = randomUUID();
|
|
714
|
+
const fallbackParams = {
|
|
715
|
+
minProtocol: 3,
|
|
716
|
+
maxProtocol: 3,
|
|
717
|
+
client: {
|
|
718
|
+
id: "openclaw-control-ui",
|
|
719
|
+
displayName: "OpenCami",
|
|
720
|
+
version: "dev",
|
|
721
|
+
platform: process.platform,
|
|
722
|
+
mode: "webchat",
|
|
723
|
+
instanceId: loadOrCreateInstanceId()
|
|
724
|
+
},
|
|
725
|
+
caps: [],
|
|
726
|
+
auth: {
|
|
727
|
+
token: token || void 0,
|
|
728
|
+
password: password || void 0
|
|
729
|
+
},
|
|
730
|
+
role: "operator",
|
|
731
|
+
scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
|
|
732
|
+
userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
|
|
733
|
+
locale: process.env.LANG || "en"
|
|
734
|
+
};
|
|
735
|
+
ws.send(
|
|
736
|
+
JSON.stringify({
|
|
737
|
+
type: "req",
|
|
738
|
+
id: fallbackId,
|
|
739
|
+
method: "connect",
|
|
740
|
+
params: fallbackParams
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
await this._waitForRes(fallbackId, 1e4);
|
|
744
|
+
this._devicePending = false;
|
|
745
|
+
}
|
|
474
746
|
this.connected = true;
|
|
475
747
|
this.reconnectDelay = 1e3;
|
|
476
748
|
console.log("[gateway-ws] Persistent connection established");
|
|
@@ -487,7 +759,7 @@ class PersistentGatewayConnection {
|
|
|
487
759
|
if (parsed.ok) {
|
|
488
760
|
pending.resolve(parsed.payload);
|
|
489
761
|
} else {
|
|
490
|
-
pending.reject(new
|
|
762
|
+
pending.reject(new GatewayResponseError(parsed.error?.message ?? "gateway error", parsed.error?.code));
|
|
491
763
|
}
|
|
492
764
|
}
|
|
493
765
|
return;
|
|
@@ -532,9 +804,9 @@ class PersistentGatewayConnection {
|
|
|
532
804
|
}
|
|
533
805
|
_extractSessionKey(event) {
|
|
534
806
|
const payload = event.payload;
|
|
535
|
-
if (typeof payload
|
|
536
|
-
if (typeof payload
|
|
537
|
-
if (payload
|
|
807
|
+
if (typeof payload.sessionKey === "string") return payload.sessionKey;
|
|
808
|
+
if (typeof payload.session === "string") return payload.session;
|
|
809
|
+
if (payload.data && typeof payload.data?.sessionKey === "string") {
|
|
538
810
|
return payload.data.sessionKey;
|
|
539
811
|
}
|
|
540
812
|
return null;
|
|
@@ -652,6 +924,10 @@ function subscribeGatewayEvents(sessionKey, listener) {
|
|
|
652
924
|
const conn = getPersistentConnection();
|
|
653
925
|
return conn.subscribe(sessionKey, listener);
|
|
654
926
|
}
|
|
927
|
+
function getDeviceStatus() {
|
|
928
|
+
const conn = getPersistentConnection();
|
|
929
|
+
return conn.getDeviceStatus();
|
|
930
|
+
}
|
|
655
931
|
async function gatewayConnectCheck() {
|
|
656
932
|
const conn = getPersistentConnection();
|
|
657
933
|
await conn.ensureConnected();
|
|
@@ -1116,9 +1392,34 @@ const RECOMMENDED_SKILLS = [
|
|
|
1116
1392
|
"clawd-docs-v2",
|
|
1117
1393
|
"therapy-mode"
|
|
1118
1394
|
];
|
|
1119
|
-
|
|
1395
|
+
const execFileAsync = promisify(execFile);
|
|
1396
|
+
const ALLOWED_SORTS = /* @__PURE__ */ new Set(["trending", "downloads", "installs", "newest"]);
|
|
1397
|
+
const SLUG_PATTERN = /^[a-zA-Z0-9/_-]+$/;
|
|
1398
|
+
function parsePositiveInt(input, fallback, max) {
|
|
1399
|
+
if (!input) return fallback;
|
|
1400
|
+
const parsed = Number.parseInt(input, 10);
|
|
1401
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
1402
|
+
return Math.min(parsed, max);
|
|
1403
|
+
}
|
|
1404
|
+
function parseSort(input) {
|
|
1405
|
+
if (!input) return "trending";
|
|
1406
|
+
const normalized = input.trim().toLowerCase();
|
|
1407
|
+
return ALLOWED_SORTS.has(normalized) ? normalized : "trending";
|
|
1408
|
+
}
|
|
1409
|
+
function parseSlug(input) {
|
|
1410
|
+
if (typeof input !== "string") return null;
|
|
1411
|
+
const slug = input.trim();
|
|
1412
|
+
if (!slug || !SLUG_PATTERN.test(slug)) return null;
|
|
1413
|
+
return slug;
|
|
1414
|
+
}
|
|
1415
|
+
async function runCmd(args) {
|
|
1120
1416
|
try {
|
|
1121
|
-
|
|
1417
|
+
const { stdout } = await execFileAsync("clawhub", args, {
|
|
1418
|
+
encoding: "utf-8",
|
|
1419
|
+
timeout: 3e4,
|
|
1420
|
+
maxBuffer: 1024 * 1024 * 8
|
|
1421
|
+
});
|
|
1422
|
+
return stdout.trim();
|
|
1122
1423
|
} catch (err) {
|
|
1123
1424
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1124
1425
|
throw new Error(`Command failed: ${msg}`);
|
|
@@ -1165,15 +1466,13 @@ const Route$n = createFileRoute("/api/skills")({
|
|
|
1165
1466
|
const url = new URL(request.url);
|
|
1166
1467
|
const action = url.searchParams.get("action") || "installed";
|
|
1167
1468
|
if (action === "installed") {
|
|
1168
|
-
const output = runCmd("
|
|
1469
|
+
const output = await runCmd(["list"]);
|
|
1169
1470
|
return json({ ok: true, skills: parseInstalledSkills(output) });
|
|
1170
1471
|
}
|
|
1171
1472
|
if (action === "explore") {
|
|
1172
|
-
const sort = url.searchParams.get("sort")
|
|
1173
|
-
const limit = url.searchParams.get("limit")
|
|
1174
|
-
const
|
|
1175
|
-
const safeLimit = String(parseInt(limit, 10) || 25);
|
|
1176
|
-
const raw = runCmd(`clawhub explore --json --limit ${safeLimit} --sort ${safeSort}`);
|
|
1473
|
+
const sort = parseSort(url.searchParams.get("sort"));
|
|
1474
|
+
const limit = parsePositiveInt(url.searchParams.get("limit"), 25, 200);
|
|
1475
|
+
const raw = await runCmd(["explore", "--json", "--limit", String(limit), "--sort", sort]);
|
|
1177
1476
|
const output = raw.substring(raw.indexOf("{"));
|
|
1178
1477
|
try {
|
|
1179
1478
|
const data = JSON.parse(output);
|
|
@@ -1184,11 +1483,9 @@ const Route$n = createFileRoute("/api/skills")({
|
|
|
1184
1483
|
}
|
|
1185
1484
|
if (action === "search") {
|
|
1186
1485
|
const q = url.searchParams.get("q") || "";
|
|
1187
|
-
const limit = url.searchParams.get("limit")
|
|
1486
|
+
const limit = parsePositiveInt(url.searchParams.get("limit"), 10, 200);
|
|
1188
1487
|
if (!q.trim()) return json({ ok: true, skills: [] });
|
|
1189
|
-
const
|
|
1190
|
-
const safeQ = q.replace(/"/g, '\\"');
|
|
1191
|
-
const raw = runCmd(`clawhub search "${safeQ}" --limit ${safeLimit}`);
|
|
1488
|
+
const raw = await runCmd(["search", q, "--limit", String(limit)]);
|
|
1192
1489
|
const output = raw.replace(/^- Searching\n?/, "");
|
|
1193
1490
|
return json({ ok: true, skills: parseSearchResults(output) });
|
|
1194
1491
|
}
|
|
@@ -1202,7 +1499,7 @@ const Route$n = createFileRoute("/api/skills")({
|
|
|
1202
1499
|
for (const sort of ["downloads", "installs", "newest", "trending"]) {
|
|
1203
1500
|
if (found.size >= slugSet.size) break;
|
|
1204
1501
|
try {
|
|
1205
|
-
const raw = runCmd(
|
|
1502
|
+
const raw = await runCmd(["explore", "--json", "--limit", "200", "--sort", sort]);
|
|
1206
1503
|
const output = raw.substring(raw.indexOf("{"));
|
|
1207
1504
|
const data = JSON.parse(output);
|
|
1208
1505
|
const items = Array.isArray(data) ? data : data.items || [];
|
|
@@ -1230,15 +1527,14 @@ const Route$n = createFileRoute("/api/skills")({
|
|
|
1230
1527
|
try {
|
|
1231
1528
|
const body = await request.json().catch(() => ({}));
|
|
1232
1529
|
const action = typeof body.action === "string" ? body.action : "";
|
|
1233
|
-
const slug =
|
|
1234
|
-
if (!slug) return json({ ok: false, error: "slug is required" }, { status: 400 });
|
|
1235
|
-
const safeSlug = slug.replace(/[^a-zA-Z0-9/_-]/g, "");
|
|
1530
|
+
const slug = parseSlug(body.slug);
|
|
1531
|
+
if (!slug) return json({ ok: false, error: "Valid slug is required" }, { status: 400 });
|
|
1236
1532
|
if (action === "install") {
|
|
1237
|
-
const output = runCmd(
|
|
1533
|
+
const output = await runCmd(["install", slug, "--no-input"]);
|
|
1238
1534
|
return json({ ok: true, output });
|
|
1239
1535
|
}
|
|
1240
1536
|
if (action === "update") {
|
|
1241
|
-
const output = runCmd(
|
|
1537
|
+
const output = await runCmd(["update", slug, "--no-input"]);
|
|
1242
1538
|
return json({ ok: true, output });
|
|
1243
1539
|
}
|
|
1244
1540
|
return json({ ok: false, error: "Unknown action" }, { status: 400 });
|
|
@@ -1501,12 +1797,38 @@ const Route$k = createFileRoute("/api/ping")({
|
|
|
1501
1797
|
GET: async () => {
|
|
1502
1798
|
try {
|
|
1503
1799
|
await gatewayConnectCheck();
|
|
1504
|
-
|
|
1800
|
+
const status = getDeviceStatus();
|
|
1801
|
+
if (status.isPending) {
|
|
1802
|
+
return json(
|
|
1803
|
+
{
|
|
1804
|
+
ok: false,
|
|
1805
|
+
error: "device pending approval",
|
|
1806
|
+
deviceId: status.deviceId,
|
|
1807
|
+
approveCommand: `openclaw devices approve ${status.deviceId}`
|
|
1808
|
+
},
|
|
1809
|
+
{ status: 503 }
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
return json({ ok: true, deviceId: status.deviceId, isPending: false });
|
|
1505
1813
|
} catch (err) {
|
|
1814
|
+
const status = getDeviceStatus();
|
|
1815
|
+
if (status.isPending) {
|
|
1816
|
+
return json(
|
|
1817
|
+
{
|
|
1818
|
+
ok: false,
|
|
1819
|
+
error: "device pending approval",
|
|
1820
|
+
deviceId: status.deviceId,
|
|
1821
|
+
approveCommand: `openclaw devices approve ${status.deviceId}`
|
|
1822
|
+
},
|
|
1823
|
+
{ status: 503 }
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1506
1826
|
return json(
|
|
1507
1827
|
{
|
|
1508
1828
|
ok: false,
|
|
1509
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1829
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1830
|
+
deviceId: status.deviceId,
|
|
1831
|
+
isPending: status.isPending
|
|
1510
1832
|
},
|
|
1511
1833
|
{ status: 503 }
|
|
1512
1834
|
);
|
|
@@ -1755,27 +2077,73 @@ Rules:
|
|
|
1755
2077
|
}
|
|
1756
2078
|
return [];
|
|
1757
2079
|
}
|
|
1758
|
-
async function testApiKey(
|
|
2080
|
+
async function testApiKey(options) {
|
|
2081
|
+
const llmOptions = typeof options === "string" ? { apiKey: options } : options;
|
|
1759
2082
|
try {
|
|
1760
2083
|
await chatCompletion(
|
|
1761
2084
|
[{ role: "user", content: "Hi" }],
|
|
1762
|
-
{
|
|
2085
|
+
{ ...llmOptions, maxTokens: 1, timeoutMs: 5e3 }
|
|
1763
2086
|
);
|
|
1764
2087
|
return true;
|
|
1765
2088
|
} catch {
|
|
1766
2089
|
return false;
|
|
1767
2090
|
}
|
|
1768
2091
|
}
|
|
2092
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
2093
|
+
const PRESET_BASE_URL_ORIGINS = /* @__PURE__ */ new Set([
|
|
2094
|
+
"https://api.openai.com",
|
|
2095
|
+
"https://openrouter.ai",
|
|
2096
|
+
"https://api.kilo.ai",
|
|
2097
|
+
"http://localhost:11434",
|
|
2098
|
+
"http://127.0.0.1:11434"
|
|
2099
|
+
]);
|
|
2100
|
+
function getOrigin(rawBaseUrl) {
|
|
2101
|
+
try {
|
|
2102
|
+
return new URL(rawBaseUrl).origin;
|
|
2103
|
+
} catch {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function isAllowedClientBaseUrl(rawBaseUrl) {
|
|
2108
|
+
const parsed = new URL(rawBaseUrl);
|
|
2109
|
+
if (!["http:", "https:"].includes(parsed.protocol)) return false;
|
|
2110
|
+
if (parsed.username || parsed.password) return false;
|
|
2111
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
2112
|
+
const isLocalHost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
2113
|
+
if (!isLocalHost && parsed.protocol !== "https:") return false;
|
|
2114
|
+
const origin = parsed.origin;
|
|
2115
|
+
if (PRESET_BASE_URL_ORIGINS.has(origin)) return true;
|
|
2116
|
+
const envBaseUrl = process.env.LLM_BASE_URL?.trim();
|
|
2117
|
+
const envOrigin = envBaseUrl ? getOrigin(envBaseUrl) : null;
|
|
2118
|
+
return Boolean(envOrigin && envOrigin === origin);
|
|
2119
|
+
}
|
|
2120
|
+
function detectProvider(rawBaseUrl) {
|
|
2121
|
+
const baseUrl = rawBaseUrl?.toLowerCase() || "";
|
|
2122
|
+
if (baseUrl.includes("openrouter.ai")) return "openrouter";
|
|
2123
|
+
if (baseUrl.includes("kilo.ai")) return "kilocode";
|
|
2124
|
+
return "openai";
|
|
2125
|
+
}
|
|
1769
2126
|
function getLlmConfig(request) {
|
|
1770
2127
|
const headerKey = request.headers.get("X-OpenAI-API-Key");
|
|
1771
2128
|
const headerBaseUrl = request.headers.get("X-LLM-Base-URL")?.trim() || null;
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2129
|
+
const envBaseUrl = process.env.LLM_BASE_URL?.trim() || null;
|
|
2130
|
+
if (headerBaseUrl) {
|
|
2131
|
+
const origin = getOrigin(headerBaseUrl);
|
|
2132
|
+
if (!origin || !isAllowedClientBaseUrl(headerBaseUrl)) {
|
|
2133
|
+
return {
|
|
2134
|
+
apiKey: null,
|
|
2135
|
+
baseUrl: null,
|
|
2136
|
+
model: null,
|
|
2137
|
+
error: "Disallowed X-LLM-Base-URL value"
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const baseUrl = headerBaseUrl || envBaseUrl || DEFAULT_OPENAI_BASE_URL;
|
|
2142
|
+
const provider = detectProvider(baseUrl);
|
|
2143
|
+
const envKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : provider === "kilocode" ? process.env.KILOCODE_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : process.env.OPENAI_API_KEY?.trim();
|
|
1776
2144
|
const apiKey = headerKey?.trim() || envKey || null;
|
|
1777
2145
|
const model = request.headers.get("X-LLM-Model")?.trim() || process.env.LLM_MODEL?.trim() || null;
|
|
1778
|
-
return { apiKey, baseUrl, model };
|
|
2146
|
+
return { apiKey, baseUrl, model, error: null };
|
|
1779
2147
|
}
|
|
1780
2148
|
function generateHeuristicTitle(message) {
|
|
1781
2149
|
let text = message.replace(/```[\s\S]*?```/g, " ");
|
|
@@ -1844,6 +2212,12 @@ const Route$g = createFileRoute("/api/llm-features")({
|
|
|
1844
2212
|
});
|
|
1845
2213
|
}
|
|
1846
2214
|
const llmConfig = getLlmConfig(request);
|
|
2215
|
+
if (llmConfig.error) {
|
|
2216
|
+
return json({
|
|
2217
|
+
ok: false,
|
|
2218
|
+
error: llmConfig.error
|
|
2219
|
+
}, { status: 400 });
|
|
2220
|
+
}
|
|
1847
2221
|
if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
|
|
1848
2222
|
const title = generateHeuristicTitle(message);
|
|
1849
2223
|
return json({
|
|
@@ -1884,6 +2258,12 @@ const Route$g = createFileRoute("/api/llm-features")({
|
|
|
1884
2258
|
});
|
|
1885
2259
|
}
|
|
1886
2260
|
const llmConfig = getLlmConfig(request);
|
|
2261
|
+
if (llmConfig.error) {
|
|
2262
|
+
return json({
|
|
2263
|
+
ok: false,
|
|
2264
|
+
error: llmConfig.error
|
|
2265
|
+
}, { status: 400 });
|
|
2266
|
+
}
|
|
1887
2267
|
if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
|
|
1888
2268
|
return json({
|
|
1889
2269
|
ok: true,
|
|
@@ -1914,6 +2294,12 @@ const Route$g = createFileRoute("/api/llm-features")({
|
|
|
1914
2294
|
}
|
|
1915
2295
|
case "test": {
|
|
1916
2296
|
const llmConfig = getLlmConfig(request);
|
|
2297
|
+
if (llmConfig.error) {
|
|
2298
|
+
return json({
|
|
2299
|
+
ok: false,
|
|
2300
|
+
error: llmConfig.error
|
|
2301
|
+
}, { status: 400 });
|
|
2302
|
+
}
|
|
1917
2303
|
if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
|
|
1918
2304
|
return json({
|
|
1919
2305
|
ok: false,
|
|
@@ -1921,7 +2307,11 @@ const Route$g = createFileRoute("/api/llm-features")({
|
|
|
1921
2307
|
});
|
|
1922
2308
|
}
|
|
1923
2309
|
try {
|
|
1924
|
-
const valid = await testApiKey(
|
|
2310
|
+
const valid = await testApiKey({
|
|
2311
|
+
apiKey: llmConfig.apiKey || "",
|
|
2312
|
+
...llmConfig.baseUrl ? { baseUrl: llmConfig.baseUrl } : {},
|
|
2313
|
+
...llmConfig.model ? { model: llmConfig.model } : {}
|
|
2314
|
+
});
|
|
1925
2315
|
return json({
|
|
1926
2316
|
ok: true,
|
|
1927
2317
|
valid
|
|
@@ -3095,10 +3485,8 @@ const Route$5 = createFileRoute("/api/files/info")({
|
|
|
3095
3485
|
);
|
|
3096
3486
|
}
|
|
3097
3487
|
const path2 = validatePath(rawPath, "Path parameter");
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3100
|
-
const stats = await stat(absolutePath);
|
|
3101
|
-
return json({ size: stats.size });
|
|
3488
|
+
const fileInfo = await getFileInfo(path2);
|
|
3489
|
+
return json({ size: fileInfo.size });
|
|
3102
3490
|
} catch (err) {
|
|
3103
3491
|
const error = err;
|
|
3104
3492
|
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
|
|
@@ -3194,11 +3582,6 @@ function encodeFilename(filename) {
|
|
|
3194
3582
|
const encodedFilename = encodeURIComponent(filename);
|
|
3195
3583
|
return `filename*=UTF-8''${encodedFilename}`;
|
|
3196
3584
|
}
|
|
3197
|
-
function isStaticAsset(filename) {
|
|
3198
|
-
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
3199
|
-
const staticExts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "css", "js", "woff", "woff2", "ttf", "otf"];
|
|
3200
|
-
return staticExts.includes(ext);
|
|
3201
|
-
}
|
|
3202
3585
|
const Route$4 = createFileRoute("/api/files/download")({
|
|
3203
3586
|
server: {
|
|
3204
3587
|
handlers: {
|
|
@@ -3226,13 +3609,9 @@ const Route$4 = createFileRoute("/api/files/download")({
|
|
|
3226
3609
|
const headers = {
|
|
3227
3610
|
"Content-Type": contentType,
|
|
3228
3611
|
"Content-Disposition": contentDisposition,
|
|
3229
|
-
"Content-Length": content.byteLength.toString()
|
|
3612
|
+
"Content-Length": content.byteLength.toString(),
|
|
3613
|
+
"Cache-Control": "private, no-store"
|
|
3230
3614
|
};
|
|
3231
|
-
if (isStaticAsset(fileInfo.name)) {
|
|
3232
|
-
headers["Cache-Control"] = "public, max-age=31536000";
|
|
3233
|
-
} else {
|
|
3234
|
-
headers["Cache-Control"] = "no-cache";
|
|
3235
|
-
}
|
|
3236
3615
|
return new Response(content, { headers });
|
|
3237
3616
|
} catch (err) {
|
|
3238
3617
|
const error = err;
|