@ccpocket/bridge 1.75.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 +3 -0
- package/dist/index.js +36 -6
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +31 -0
- package/dist/parser.js +42 -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 +9 -0
- package/dist/websocket.js +91 -1
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -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";
|
|
@@ -483,6 +484,7 @@ export class BridgeWebSocketServer {
|
|
|
483
484
|
static DEFAULT_FILE_LIST_MAX_ENTRIES = 5000;
|
|
484
485
|
static DEFAULT_FILE_LIST_MAX_BYTES = 512 * 1024;
|
|
485
486
|
static DEFAULT_FILE_DOWNLOAD_MAX_BYTES = 512 * 1024 * 1024;
|
|
487
|
+
static DEFAULT_FILE_UPLOAD_MAX_BYTES = 512 * 1024 * 1024;
|
|
486
488
|
static DEFAULT_DELTA_BATCH_MS = 100;
|
|
487
489
|
static DEFAULT_DELTA_BATCH_MAX_CHARS = 4096;
|
|
488
490
|
wss;
|
|
@@ -491,6 +493,7 @@ export class BridgeWebSocketServer {
|
|
|
491
493
|
allowedDirs;
|
|
492
494
|
imageStore;
|
|
493
495
|
mediaStore;
|
|
496
|
+
uploadStore;
|
|
494
497
|
galleryStore;
|
|
495
498
|
projectHistory;
|
|
496
499
|
debugTraceStore;
|
|
@@ -535,6 +538,7 @@ export class BridgeWebSocketServer {
|
|
|
535
538
|
fileListMaxEntries;
|
|
536
539
|
fileListMaxBytes;
|
|
537
540
|
fileDownloadMaxBytes;
|
|
541
|
+
fileUploadMaxBytes;
|
|
538
542
|
deltaBatchMs;
|
|
539
543
|
deltaBatchMaxChars;
|
|
540
544
|
deltaBatches = new Map();
|
|
@@ -543,11 +547,12 @@ export class BridgeWebSocketServer {
|
|
|
543
547
|
pendingClaudeResumeInputs = new WeakMap();
|
|
544
548
|
resumeOperations = new Map();
|
|
545
549
|
constructor(options) {
|
|
546
|
-
const { server, apiKey, allowedDirs, imageStore, mediaStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, promptHistoryStore, platform, fileListMaxEntries, fileListMaxBytes, fileDownloadMaxBytes, 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;
|
|
547
551
|
this.apiKey = apiKey ?? null;
|
|
548
552
|
this.allowedDirs = allowedDirs ?? [];
|
|
549
553
|
this.imageStore = imageStore ?? null;
|
|
550
554
|
this.mediaStore = mediaStore ?? null;
|
|
555
|
+
this.uploadStore = uploadStore ?? null;
|
|
551
556
|
this.galleryStore = galleryStore ?? null;
|
|
552
557
|
this.projectHistory = projectHistory ?? null;
|
|
553
558
|
this.debugTraceStore = debugTraceStore ?? new DebugTraceStore();
|
|
@@ -560,6 +565,7 @@ export class BridgeWebSocketServer {
|
|
|
560
565
|
this.fileListMaxEntries = normalizePositiveLimit(fileListMaxEntries, positiveEnvInt("BRIDGE_FILE_LIST_MAX_ENTRIES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_ENTRIES));
|
|
561
566
|
this.fileListMaxBytes = normalizePositiveLimit(fileListMaxBytes, positiveEnvInt("BRIDGE_FILE_LIST_MAX_BYTES", BridgeWebSocketServer.DEFAULT_FILE_LIST_MAX_BYTES));
|
|
562
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);
|
|
563
569
|
this.deltaBatchMs = normalizeNonNegativeLimit(deltaBatchMs, nonNegativeEnvInt("BRIDGE_DELTA_BATCH_MS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MS));
|
|
564
570
|
this.deltaBatchMaxChars = normalizePositiveLimit(deltaBatchMaxChars, positiveEnvInt("BRIDGE_DELTA_BATCH_MAX_CHARS", BridgeWebSocketServer.DEFAULT_DELTA_BATCH_MAX_CHARS));
|
|
565
571
|
this.archiveStore = new ArchiveStore();
|
|
@@ -714,6 +720,77 @@ export class BridgeWebSocketServer {
|
|
|
714
720
|
this.sendFileDownloadError(ws, request, "file_download_failed", "Unable to prepare the file download.");
|
|
715
721
|
}
|
|
716
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
|
+
}
|
|
717
794
|
/** Build a user-friendly error for disallowed project paths. */
|
|
718
795
|
buildPathNotAllowedError(projectPath) {
|
|
719
796
|
return {
|
|
@@ -4136,6 +4213,19 @@ export class BridgeWebSocketServer {
|
|
|
4136
4213
|
void this.prepareFileDownload(ws, msg);
|
|
4137
4214
|
break;
|
|
4138
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
|
+
}
|
|
4139
4229
|
case "read_file":
|
|
4140
4230
|
case "read_media_file": {
|
|
4141
4231
|
const absPath = resolve(msg.projectPath, msg.filePath);
|