@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/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);