@ccpocket/bridge 1.74.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +36 -6
- package/dist/index.js.map +1 -1
- package/dist/media-store.d.ts +2 -1
- package/dist/media-store.js +39 -3
- package/dist/media-store.js.map +1 -1
- package/dist/parser.d.ts +44 -0
- package/dist/parser.js +58 -0
- package/dist/parser.js.map +1 -1
- package/dist/upload-store.d.ts +84 -0
- package/dist/upload-store.js +559 -0
- package/dist/upload-store.js.map +1 -0
- package/dist/websocket.d.ts +15 -0
- package/dist/websocket.js +215 -2
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { lstat, readFile, readlink, realpath, stat, unlink } from "node:fs/promises";
|
|
5
|
-
import { resolve, extname } from "node:path";
|
|
5
|
+
import { resolve, extname, posix, win32 } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { WebSocketServer, WebSocket } from "ws";
|
|
8
8
|
import { SessionManager, MAX_HISTORY_PER_SESSION, } from "./session.js";
|
|
@@ -11,6 +11,7 @@ import { codexErrorMessage, CodexRpcError, CodexProcess, } from "./codex-process
|
|
|
11
11
|
import { stopManagedCodexAppServers } from "./codex-transport.js";
|
|
12
12
|
import { parseClientMessage, } from "./parser.js";
|
|
13
13
|
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, codexUserTurnUuid, codexThreadToSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, getCodexSessionIndexMetadata, loadCodexSessionNames, renameClaudeSession, renameCodexSession, saveCodexSessionProfile, } from "./sessions-index.js";
|
|
14
|
+
import { isSafeUploadFileName, UploadStoreError, } from "./upload-store.js";
|
|
14
15
|
import { formatResumePerformanceLog, summarizeResumeHistory, } from "./resume-metrics.js";
|
|
15
16
|
import { ArchiveStore } from "./archive-store.js";
|
|
16
17
|
import { WorktreeStore } from "./worktree-store.js";
|
|
@@ -394,6 +395,44 @@ function normalizeNonNegativeLimit(value, fallback) {
|
|
|
394
395
|
? value
|
|
395
396
|
: fallback;
|
|
396
397
|
}
|
|
398
|
+
export function downloadMimeType(filePath) {
|
|
399
|
+
const extension = extname(filePath).toLowerCase();
|
|
400
|
+
const mimeTypes = {
|
|
401
|
+
".aac": "audio/aac",
|
|
402
|
+
".avi": "video/x-msvideo",
|
|
403
|
+
".bmp": "image/bmp",
|
|
404
|
+
".csv": "text/csv",
|
|
405
|
+
".doc": "application/msword",
|
|
406
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
407
|
+
".gif": "image/gif",
|
|
408
|
+
".gz": "application/gzip",
|
|
409
|
+
".html": "text/html",
|
|
410
|
+
".jpeg": "image/jpeg",
|
|
411
|
+
".jpg": "image/jpeg",
|
|
412
|
+
".json": "application/json",
|
|
413
|
+
".m4a": "audio/mp4",
|
|
414
|
+
".md": "text/markdown",
|
|
415
|
+
".mov": "video/quicktime",
|
|
416
|
+
".mp3": "audio/mpeg",
|
|
417
|
+
".mp4": "video/mp4",
|
|
418
|
+
".ogg": "audio/ogg",
|
|
419
|
+
".pdf": "application/pdf",
|
|
420
|
+
".png": "image/png",
|
|
421
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
422
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
423
|
+
".svg": "image/svg+xml",
|
|
424
|
+
".tar": "application/x-tar",
|
|
425
|
+
".txt": "text/plain",
|
|
426
|
+
".wav": "audio/wav",
|
|
427
|
+
".webm": "video/webm",
|
|
428
|
+
".webp": "image/webp",
|
|
429
|
+
".xls": "application/vnd.ms-excel",
|
|
430
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
431
|
+
".xml": "application/xml",
|
|
432
|
+
".zip": "application/zip",
|
|
433
|
+
};
|
|
434
|
+
return mimeTypes[extension] ?? "application/octet-stream";
|
|
435
|
+
}
|
|
397
436
|
function codexThreadToRecentSession(thread, indexed) {
|
|
398
437
|
// thread/list only exposes a single preview blob; prefer the real
|
|
399
438
|
// first/last/summary texts parsed from the rollout file so display-mode
|
|
@@ -444,6 +483,8 @@ export class BridgeWebSocketServer {
|
|
|
444
483
|
static CONNECT_METADATA_REFRESH_COOLDOWN_MS = 5 * 60 * 1000;
|
|
445
484
|
static DEFAULT_FILE_LIST_MAX_ENTRIES = 5000;
|
|
446
485
|
static DEFAULT_FILE_LIST_MAX_BYTES = 512 * 1024;
|
|
486
|
+
static DEFAULT_FILE_DOWNLOAD_MAX_BYTES = 512 * 1024 * 1024;
|
|
487
|
+
static DEFAULT_FILE_UPLOAD_MAX_BYTES = 512 * 1024 * 1024;
|
|
447
488
|
static DEFAULT_DELTA_BATCH_MS = 100;
|
|
448
489
|
static DEFAULT_DELTA_BATCH_MAX_CHARS = 4096;
|
|
449
490
|
wss;
|
|
@@ -452,6 +493,7 @@ export class BridgeWebSocketServer {
|
|
|
452
493
|
allowedDirs;
|
|
453
494
|
imageStore;
|
|
454
495
|
mediaStore;
|
|
496
|
+
uploadStore;
|
|
455
497
|
galleryStore;
|
|
456
498
|
projectHistory;
|
|
457
499
|
debugTraceStore;
|
|
@@ -495,6 +537,8 @@ export class BridgeWebSocketServer {
|
|
|
495
537
|
failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
|
|
496
538
|
fileListMaxEntries;
|
|
497
539
|
fileListMaxBytes;
|
|
540
|
+
fileDownloadMaxBytes;
|
|
541
|
+
fileUploadMaxBytes;
|
|
498
542
|
deltaBatchMs;
|
|
499
543
|
deltaBatchMaxChars;
|
|
500
544
|
deltaBatches = new Map();
|
|
@@ -503,11 +547,12 @@ export class BridgeWebSocketServer {
|
|
|
503
547
|
pendingClaudeResumeInputs = new WeakMap();
|
|
504
548
|
resumeOperations = new Map();
|
|
505
549
|
constructor(options) {
|
|
506
|
-
const { server, apiKey, allowedDirs, imageStore, mediaStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
550
|
+
const { server, apiKey, allowedDirs, imageStore, mediaStore, uploadStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, fileUploadMaxBytes, deltaBatchMs, deltaBatchMaxChars, } = options;
|
|
507
551
|
this.apiKey = apiKey ?? null;
|
|
508
552
|
this.allowedDirs = allowedDirs ?? [];
|
|
509
553
|
this.imageStore = imageStore ?? null;
|
|
510
554
|
this.mediaStore = mediaStore ?? null;
|
|
555
|
+
this.uploadStore = uploadStore ?? null;
|
|
511
556
|
this.galleryStore = galleryStore ?? null;
|
|
512
557
|
this.projectHistory = projectHistory ?? null;
|
|
513
558
|
this.debugTraceStore = debugTraceStore ?? new DebugTraceStore();
|
|
@@ -519,6 +564,8 @@ export class BridgeWebSocketServer {
|
|
|
519
564
|
this.platform = platform ?? process.platform;
|
|
520
565
|
this.fileListMaxEntries = normalizePositiveLimit(fileListMaxEntries, positiveEnvInt("BRIDGE_FILE_LIST_MAX_ENTRIES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_ENTRIES));
|
|
521
566
|
this.fileListMaxBytes = normalizePositiveLimit(fileListMaxBytes, positiveEnvInt("BRIDGE_FILE_LIST_MAX_BYTES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_BYTES));
|
|
567
|
+
this.fileDownloadMaxBytes = normalizePositiveLimit(fileDownloadMaxBytes, positiveEnvInt("BRIDGE_FILE_DOWNLOAD_MAX_SIZE_MB", BridgeWebSocketServer.DEFAULT_FILE_DOWNLOAD_MAX_BYTES / 1024 / 1024) * 1024 * 1024);
|
|
568
|
+
this.fileUploadMaxBytes = normalizePositiveLimit(fileUploadMaxBytes, positiveEnvInt("BRIDGE_FILE_UPLOAD_MAX_SIZE_MB", BridgeWebSocketServer.DEFAULT_FILE_UPLOAD_MAX_BYTES / 1024 / 1024) * 1024 * 1024);
|
|
522
569
|
this.deltaBatchMs = normalizeNonNegativeLimit(deltaBatchMs, nonNegativeEnvInt("BRIDGE_DELTA_BATCH_MS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MS));
|
|
523
570
|
this.deltaBatchMaxChars = normalizePositiveLimit(deltaBatchMaxChars, positiveEnvInt("BRIDGE_DELTA_BATCH_MAX_CHARS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MAX_CHARS));
|
|
524
571
|
this.archiveStore = new ArchiveStore();
|
|
@@ -595,6 +642,155 @@ export class BridgeWebSocketServer {
|
|
|
595
642
|
}
|
|
596
643
|
return false;
|
|
597
644
|
}
|
|
645
|
+
sendFileDownloadError(ws, request, errorCode, message) {
|
|
646
|
+
this.send(ws, {
|
|
647
|
+
type: "error",
|
|
648
|
+
errorCode,
|
|
649
|
+
message,
|
|
650
|
+
path: request.filePath,
|
|
651
|
+
requestId: request.requestId,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async prepareFileDownload(ws, request) {
|
|
655
|
+
const pathApi = this.platform === "win32" ? win32 : posix;
|
|
656
|
+
const projectPath = pathApi.resolve(request.projectPath);
|
|
657
|
+
if (pathApi.isAbsolute(request.filePath)) {
|
|
658
|
+
this.sendFileDownloadError(ws, request, "file_download_not_allowed", "Only project-relative file paths can be downloaded.");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const requestedPath = pathApi.resolve(projectPath, request.filePath);
|
|
662
|
+
if (!this.isPathAllowed(projectPath) ||
|
|
663
|
+
!isPathWithinAllowedDirectory(requestedPath, projectPath, this.platform)) {
|
|
664
|
+
this.sendFileDownloadError(ws, request, "file_download_not_allowed", "The requested file is outside the current project.");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
let canonicalProjectPath;
|
|
668
|
+
try {
|
|
669
|
+
canonicalProjectPath = await realpath(projectPath);
|
|
670
|
+
const projectStat = await stat(canonicalProjectPath);
|
|
671
|
+
if (!projectStat.isDirectory())
|
|
672
|
+
throw new Error("not a directory");
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
this.sendFileDownloadError(ws, request, "file_download_not_allowed", "The current project is unavailable or not allowed.");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
let canonicalFilePath;
|
|
679
|
+
try {
|
|
680
|
+
canonicalFilePath = await realpath(requestedPath);
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
this.sendFileDownloadError(ws, request, "file_download_not_found", "File not found.");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (!isPathWithinAllowedDirectory(canonicalFilePath, canonicalProjectPath, this.platform) ||
|
|
687
|
+
!(await this.isCanonicalPathAllowed(canonicalFilePath))) {
|
|
688
|
+
this.sendFileDownloadError(ws, request, "file_download_not_allowed", "The requested file resolves outside the current project.");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const fileStat = await stat(canonicalFilePath);
|
|
693
|
+
if (!fileStat.isFile()) {
|
|
694
|
+
this.sendFileDownloadError(ws, request, "file_download_not_file", "Only regular files can be downloaded.");
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (fileStat.size > this.fileDownloadMaxBytes) {
|
|
698
|
+
const maxSizeMb = Math.max(1, Math.ceil(this.fileDownloadMaxBytes / 1024 / 1024));
|
|
699
|
+
this.sendFileDownloadError(ws, request, "file_download_too_large", `File is too large to download. Maximum size is ${maxSizeMb} MB.`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!this.mediaStore) {
|
|
703
|
+
this.sendFileDownloadError(ws, request, "file_download_unavailable", "File downloads are unavailable on this Bridge.");
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const fileName = pathApi.basename(requestedPath);
|
|
707
|
+
const mimeType = downloadMimeType(fileName);
|
|
708
|
+
const ref = await this.mediaStore.register(canonicalFilePath, mimeType, fileStat.size, fileName);
|
|
709
|
+
this.send(ws, {
|
|
710
|
+
type: "file_download_ready",
|
|
711
|
+
requestId: request.requestId,
|
|
712
|
+
filePath: request.filePath,
|
|
713
|
+
fileName,
|
|
714
|
+
mimeType: ref.mimeType,
|
|
715
|
+
sizeBytes: ref.sizeBytes,
|
|
716
|
+
downloadUrl: ref.url,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
this.sendFileDownloadError(ws, request, "file_download_failed", "Unable to prepare the file download.");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
sendFileUploadError(ws, requestId, errorCode, message, path) {
|
|
724
|
+
this.send(ws, { type: "error", errorCode, message, requestId, path });
|
|
725
|
+
}
|
|
726
|
+
async prepareFileUpload(ws, request) {
|
|
727
|
+
const pathApi = this.platform === "win32" ? win32 : posix;
|
|
728
|
+
const projectPath = pathApi.resolve(request.projectPath);
|
|
729
|
+
if (!this.uploadStore ||
|
|
730
|
+
pathApi.isAbsolute(request.directoryPath) ||
|
|
731
|
+
!isSafeUploadFileName(request.fileName)) {
|
|
732
|
+
this.sendFileUploadError(ws, request.requestId, !this.uploadStore ? "file_upload_unavailable" : "file_upload_not_allowed", !this.uploadStore
|
|
733
|
+
? "File uploads are unavailable on this Bridge."
|
|
734
|
+
: "The upload destination or file name is not allowed.", request.directoryPath);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (request.sizeBytes > this.fileUploadMaxBytes) {
|
|
738
|
+
this.sendFileUploadError(ws, request.requestId, "file_upload_too_large", `File is too large to upload. Maximum size is ${Math.ceil(this.fileUploadMaxBytes / 1024 / 1024)} MB.`, request.fileName);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const requestedDirectory = pathApi.resolve(projectPath, request.directoryPath || ".");
|
|
742
|
+
if (!this.isPathAllowed(projectPath) ||
|
|
743
|
+
!isPathWithinAllowedDirectory(requestedDirectory, projectPath, this.platform)) {
|
|
744
|
+
this.sendFileUploadError(ws, request.requestId, "file_upload_not_allowed", "The upload destination is outside the current project.", request.directoryPath);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const canonicalProject = await realpath(projectPath);
|
|
749
|
+
const canonicalDirectory = await realpath(requestedDirectory);
|
|
750
|
+
const directoryStat = await stat(canonicalDirectory);
|
|
751
|
+
if (!directoryStat.isDirectory() ||
|
|
752
|
+
!isPathWithinAllowedDirectory(canonicalDirectory, canonicalProject, this.platform) ||
|
|
753
|
+
!(await this.isCanonicalPathAllowed(canonicalDirectory))) {
|
|
754
|
+
throw new UploadStoreError("file_upload_directory_changed", "The upload destination is not allowed.");
|
|
755
|
+
}
|
|
756
|
+
const ref = await this.uploadStore.register({
|
|
757
|
+
directoryPath: canonicalDirectory,
|
|
758
|
+
relativeDirectoryPath: pathApi
|
|
759
|
+
.relative(projectPath, requestedDirectory)
|
|
760
|
+
.split(pathApi.sep)
|
|
761
|
+
.join("/"),
|
|
762
|
+
fileName: request.fileName,
|
|
763
|
+
sizeBytes: request.sizeBytes,
|
|
764
|
+
conflictPolicy: request.conflictPolicy,
|
|
765
|
+
});
|
|
766
|
+
this.send(ws, {
|
|
767
|
+
type: "file_upload_ready",
|
|
768
|
+
requestId: request.requestId,
|
|
769
|
+
fileName: request.fileName,
|
|
770
|
+
sizeBytes: request.sizeBytes,
|
|
771
|
+
uploadUrl: ref.url,
|
|
772
|
+
uploadToken: ref.token,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
const known = error instanceof UploadStoreError ? error : null;
|
|
777
|
+
this.sendFileUploadError(ws, request.requestId, known?.code ?? "file_upload_directory_not_found", known?.message ?? "The upload destination was not found or is not allowed.", request.directoryPath);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async finalizeFileUpload(ws, request) {
|
|
781
|
+
if (!this.uploadStore) {
|
|
782
|
+
this.sendFileUploadError(ws, request.requestId, "file_upload_unavailable", "File uploads are unavailable on this Bridge.");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const result = await this.uploadStore.finalize(request.uploadToken, request.sha256);
|
|
787
|
+
this.send(ws, { type: "file_upload_complete", requestId: request.requestId, ...result });
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
const known = error instanceof UploadStoreError ? error : null;
|
|
791
|
+
this.sendFileUploadError(ws, request.requestId, known?.code ?? "file_upload_failed", known?.message ?? "Unable to finish the file upload.");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
598
794
|
/** Build a user-friendly error for disallowed project paths. */
|
|
599
795
|
buildPathNotAllowedError(projectPath) {
|
|
600
796
|
return {
|
|
@@ -4013,6 +4209,23 @@ export class BridgeWebSocketServer {
|
|
|
4013
4209
|
}
|
|
4014
4210
|
break;
|
|
4015
4211
|
}
|
|
4212
|
+
case "prepare_file_download": {
|
|
4213
|
+
void this.prepareFileDownload(ws, msg);
|
|
4214
|
+
break;
|
|
4215
|
+
}
|
|
4216
|
+
case "prepare_file_upload": {
|
|
4217
|
+
void this.prepareFileUpload(ws, msg);
|
|
4218
|
+
break;
|
|
4219
|
+
}
|
|
4220
|
+
case "finalize_file_upload": {
|
|
4221
|
+
void this.finalizeFileUpload(ws, msg);
|
|
4222
|
+
break;
|
|
4223
|
+
}
|
|
4224
|
+
case "cancel_file_upload": {
|
|
4225
|
+
if (this.uploadStore)
|
|
4226
|
+
void this.uploadStore.cancel(msg.uploadToken);
|
|
4227
|
+
break;
|
|
4228
|
+
}
|
|
4016
4229
|
case "read_file":
|
|
4017
4230
|
case "read_media_file": {
|
|
4018
4231
|
const absPath = resolve(msg.projectPath, msg.filePath);
|