@zero-transfer/sdk 0.4.2 → 0.4.7

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/index.cjs CHANGED
@@ -94,7 +94,6 @@ __export(index_exports, {
94
94
  createLocalProviderFactory: () => createLocalProviderFactory,
95
95
  createMemoryProviderFactory: () => createMemoryProviderFactory,
96
96
  createMemoryS3MultipartResumeStore: () => createMemoryS3MultipartResumeStore,
97
- createNativeSftpProviderFactory: () => createNativeSftpProviderFactory,
98
97
  createOAuthTokenSecretSource: () => createOAuthTokenSecretSource,
99
98
  createOneDriveProviderFactory: () => createOneDriveProviderFactory,
100
99
  createOutboxRoute: () => createOutboxRoute,
@@ -104,7 +103,7 @@ __export(index_exports, {
104
103
  createRemoteBrowser: () => createRemoteBrowser,
105
104
  createRemoteManifest: () => createRemoteManifest,
106
105
  createS3ProviderFactory: () => createS3ProviderFactory,
107
- createSftpProviderFactory: () => createNativeSftpProviderFactory,
106
+ createSftpProviderFactory: () => createSftpProviderFactory,
108
107
  createSyncPlan: () => createSyncPlan,
109
108
  createTransferClient: () => createTransferClient,
110
109
  createTransferJobsFromPlan: () => createTransferJobsFromPlan,
@@ -128,6 +127,7 @@ __export(index_exports, {
128
127
  inboxFailedPath: () => inboxFailedPath,
129
128
  inboxProcessedPath: () => inboxProcessedPath,
130
129
  isClassicProviderId: () => isClassicProviderId,
130
+ isMainModule: () => isMainModule,
131
131
  isSensitiveKey: () => isSensitiveKey,
132
132
  joinRemotePath: () => joinRemotePath,
133
133
  matchKnownHosts: () => matchKnownHosts,
@@ -160,6 +160,7 @@ __export(index_exports, {
160
160
  resolveSecret: () => resolveSecret,
161
161
  runConnectionDiagnostics: () => runConnectionDiagnostics,
162
162
  runRoute: () => runRoute,
163
+ runSshCommand: () => runSshCommand,
163
164
  serializeRemoteManifest: () => serializeRemoteManifest,
164
165
  signWebhookPayload: () => signWebhookPayload,
165
166
  sortRemoteEntries: () => sortRemoteEntries,
@@ -6273,7 +6274,7 @@ var SshTransportPacketUnprotector = class {
6273
6274
  }
6274
6275
  /**
6275
6276
  * Feeds raw encrypted bytes from the socket and returns any fully decoded payloads.
6276
- * Maintains internal framing state across calls pass each `data` event chunk directly.
6277
+ * Maintains internal framing state across calls - pass each `data` event chunk directly.
6277
6278
  */
6278
6279
  pushBytes(chunk) {
6279
6280
  this.framePendingRaw = import_node_buffer15.Buffer.concat([this.framePendingRaw, chunk]);
@@ -6781,7 +6782,7 @@ var SshTransportConnection = class {
6781
6782
  assertConnected() {
6782
6783
  if (!this.connected) {
6783
6784
  throw new ProtocolError({
6784
- message: "SshTransportConnection is not yet connected \u2014 call connect() first",
6785
+ message: "SshTransportConnection is not yet connected - call connect() first",
6785
6786
  protocol: "sftp",
6786
6787
  retryable: false
6787
6788
  });
@@ -7121,14 +7122,14 @@ function sftpStatusToError(status, path2) {
7121
7122
  case SFTP_STATUS.NO_SUCH_FILE:
7122
7123
  return new PathNotFoundError({
7123
7124
  details: { path: path2, sftpMessage: status.errorMessage },
7124
- message: `SFTP: no such file or directory${path2 !== void 0 ? ` \u2014 ${path2}` : ""}`,
7125
+ message: `SFTP: no such file or directory${path2 !== void 0 ? ` - ${path2}` : ""}`,
7125
7126
  protocol: "sftp",
7126
7127
  retryable: false
7127
7128
  });
7128
7129
  case SFTP_STATUS.PERMISSION_DENIED:
7129
7130
  return new PermissionDeniedError({
7130
7131
  details: { path: path2, sftpMessage: status.errorMessage },
7131
- message: `SFTP: permission denied${path2 !== void 0 ? ` \u2014 ${path2}` : ""}`,
7132
+ message: `SFTP: permission denied${path2 !== void 0 ? ` - ${path2}` : ""}`,
7132
7133
  protocol: "sftp",
7133
7134
  retryable: false
7134
7135
  });
@@ -7136,21 +7137,21 @@ function sftpStatusToError(status, path2) {
7136
7137
  case SFTP_STATUS.CONNECTION_LOST:
7137
7138
  return new ConnectionError({
7138
7139
  details: { sftpMessage: status.errorMessage, statusCode: status.statusCode },
7139
- message: `SFTP: connection error \u2014 ${status.errorMessage}`,
7140
+ message: `SFTP: connection error - ${status.errorMessage}`,
7140
7141
  protocol: "sftp",
7141
7142
  retryable: true
7142
7143
  });
7143
7144
  case SFTP_STATUS.OP_UNSUPPORTED:
7144
7145
  return new UnsupportedFeatureError({
7145
7146
  details: { sftpMessage: status.errorMessage },
7146
- message: `SFTP: operation unsupported \u2014 ${status.errorMessage}`,
7147
+ message: `SFTP: operation unsupported - ${status.errorMessage}`,
7147
7148
  protocol: "sftp",
7148
7149
  retryable: false
7149
7150
  });
7150
7151
  case SFTP_STATUS.BAD_MESSAGE:
7151
7152
  return new ProtocolError({
7152
7153
  details: { sftpMessage: status.errorMessage },
7153
- message: `SFTP: bad message \u2014 ${status.errorMessage}`,
7154
+ message: `SFTP: bad message - ${status.errorMessage}`,
7154
7155
  protocol: "sftp",
7155
7156
  retryable: false
7156
7157
  });
@@ -7158,7 +7159,7 @@ function sftpStatusToError(status, path2) {
7158
7159
  return new ZeroTransferError({
7159
7160
  code: "SFTP_FAILURE",
7160
7161
  details: { sftpMessage: status.errorMessage, statusCode: status.statusCode },
7161
- message: `SFTP: operation failed (status ${status.statusCode}) \u2014 ${status.errorMessage}`,
7162
+ message: `SFTP: operation failed (status ${status.statusCode}) - ${status.errorMessage}`,
7162
7163
  protocol: "sftp",
7163
7164
  retryable: false
7164
7165
  });
@@ -7568,7 +7569,7 @@ function buildNativeSftpCapabilities(maxConcurrency) {
7568
7569
  var NATIVE_SFTP_PROVIDER_CAPABILITIES = buildNativeSftpCapabilities(
7569
7570
  NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
7570
7571
  );
7571
- function createNativeSftpProviderFactory(options = {}) {
7572
+ function createSftpProviderFactory(options = {}) {
7572
7573
  validateNativeSftpOptions(options);
7573
7574
  const capabilities = buildNativeSftpCapabilities(
7574
7575
  options.maxConcurrency ?? NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
@@ -9261,6 +9262,9 @@ function concatChunks(chunks, totalSize) {
9261
9262
  // src/providers/cloud/OneDriveProvider.ts
9262
9263
  var ONEDRIVE_DRIVE_BASE = "https://graph.microsoft.com/v1.0/me/drive";
9263
9264
  var ONEDRIVE_CHECKSUM_CAPABILITIES = ["sha1", "sha256", "quickxorhash"];
9265
+ var ONEDRIVE_CHUNK_ALIGNMENT = 320 * 1024;
9266
+ var DEFAULT_ONEDRIVE_PART_SIZE = 10 * 1024 * 1024;
9267
+ var DEFAULT_ONEDRIVE_THRESHOLD = 4 * 1024 * 1024;
9264
9268
  function createOneDriveProviderFactory(options = {}) {
9265
9269
  const id = options.id ?? "one-drive";
9266
9270
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -9271,6 +9275,37 @@ function createOneDriveProviderFactory(options = {}) {
9271
9275
  retryable: false
9272
9276
  });
9273
9277
  }
9278
+ const multipartEnabled = options.multipart?.enabled ?? true;
9279
+ const partSizeBytes = options.multipart?.partSizeBytes ?? DEFAULT_ONEDRIVE_PART_SIZE;
9280
+ const thresholdBytes = options.multipart?.thresholdBytes ?? DEFAULT_ONEDRIVE_THRESHOLD;
9281
+ if (multipartEnabled) {
9282
+ if (!Number.isInteger(partSizeBytes) || partSizeBytes <= 0) {
9283
+ throw new ConfigurationError({
9284
+ details: { partSizeBytes },
9285
+ message: "OneDriveMultipartOptions.partSizeBytes must be a positive integer",
9286
+ retryable: false
9287
+ });
9288
+ }
9289
+ if (partSizeBytes % ONEDRIVE_CHUNK_ALIGNMENT !== 0) {
9290
+ throw new ConfigurationError({
9291
+ details: { partSizeBytes },
9292
+ message: `OneDrive multipart partSizeBytes must be a multiple of ${String(ONEDRIVE_CHUNK_ALIGNMENT)} bytes (320 KiB)`,
9293
+ retryable: false
9294
+ });
9295
+ }
9296
+ if (!Number.isInteger(thresholdBytes) || thresholdBytes < 0) {
9297
+ throw new ConfigurationError({
9298
+ details: { thresholdBytes },
9299
+ message: "OneDriveMultipartOptions.thresholdBytes must be a non-negative integer",
9300
+ retryable: false
9301
+ });
9302
+ }
9303
+ }
9304
+ const multipart = {
9305
+ enabled: multipartEnabled,
9306
+ partSizeBytes,
9307
+ thresholdBytes
9308
+ };
9274
9309
  const capabilities = {
9275
9310
  atomicRename: false,
9276
9311
  authentication: ["token", "oauth"],
@@ -9280,13 +9315,17 @@ function createOneDriveProviderFactory(options = {}) {
9280
9315
  list: true,
9281
9316
  maxConcurrency: 4,
9282
9317
  metadata: ["modifiedAt", "createdAt", "uniqueId"],
9283
- notes: [
9284
- "OneDrive provider performs single-shot uploads via PUT /content; resumable upload sessions are not yet supported."
9318
+ notes: multipartEnabled ? [
9319
+ `OneDrive upload session enabled by default (partSize=${String(multipart.partSizeBytes)}B, threshold=${String(multipart.thresholdBytes)}B).`,
9320
+ "Payloads at or below the threshold automatically fall back to single-shot PUT /content.",
9321
+ "Pass `multipart: { enabled: false }` to force the legacy single-shot behaviour."
9322
+ ] : [
9323
+ "OneDrive provider performs single-shot uploads via PUT /content; resumable upload sessions are disabled."
9285
9324
  ],
9286
9325
  provider: id,
9287
9326
  readStream: true,
9288
9327
  resumeDownload: true,
9289
- resumeUpload: false,
9328
+ resumeUpload: multipartEnabled,
9290
9329
  serverSideCopy: false,
9291
9330
  serverSideMove: false,
9292
9331
  stat: true,
@@ -9300,7 +9339,8 @@ function createOneDriveProviderFactory(options = {}) {
9300
9339
  defaultHeaders: { ...options.defaultHeaders ?? {} },
9301
9340
  driveBaseUrl,
9302
9341
  fetch: fetchImpl,
9303
- id
9342
+ id,
9343
+ multipart
9304
9344
  }),
9305
9345
  id
9306
9346
  };
@@ -9334,6 +9374,7 @@ var OneDriveProvider = class {
9334
9374
  driveBaseUrl: this.internals.driveBaseUrl,
9335
9375
  fetch: this.internals.fetch,
9336
9376
  id: this.internals.id,
9377
+ multipart: this.internals.multipart,
9337
9378
  token
9338
9379
  };
9339
9380
  if (profile.timeoutMs !== void 0) sessionOptions.timeoutMs = profile.timeoutMs;
@@ -9437,12 +9478,19 @@ var OneDriveTransferOperations = class {
9437
9478
  if (request.offset !== void 0 && request.offset > 0) {
9438
9479
  throw new UnsupportedFeatureError({
9439
9480
  details: { offset: request.offset },
9440
- message: "OneDrive provider does not yet support resumable upload sessions",
9481
+ message: "OneDrive provider does not yet support cross-attempt resume of upload sessions",
9441
9482
  retryable: false
9442
9483
  });
9443
9484
  }
9444
9485
  const normalized = normalizeRemotePath(request.endpoint.path);
9486
+ const multipart = this.options.multipart;
9445
9487
  const buffered = await collectChunks3(request.content);
9488
+ if (!multipart.enabled || buffered.byteLength <= multipart.thresholdBytes) {
9489
+ return this.singleShotPut(request, normalized, buffered);
9490
+ }
9491
+ return this.writeUploadSession(request, normalized, buffered);
9492
+ }
9493
+ async singleShotPut(request, normalized, buffered) {
9446
9494
  const url = `${this.options.driveBaseUrl}${itemSegment(normalized)}/content`;
9447
9495
  const response = await graphFetch(this.options, "PUT", url, {
9448
9496
  ...request.signal !== void 0 ? { signal: request.signal } : {},
@@ -9462,6 +9510,77 @@ var OneDriveTransferOperations = class {
9462
9510
  if (checksum !== void 0) result.checksum = checksum;
9463
9511
  return result;
9464
9512
  }
9513
+ async writeUploadSession(request, normalized, buffered) {
9514
+ const partSize = this.options.multipart.partSizeBytes;
9515
+ const total = buffered.byteLength;
9516
+ const sessionUrl = `${this.options.driveBaseUrl}${itemSegment(normalized)}/createUploadSession`;
9517
+ const initiate = await graphFetch(this.options, "POST", sessionUrl, {
9518
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
9519
+ body: new TextEncoder().encode(
9520
+ JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } })
9521
+ ),
9522
+ extraHeaders: { "content-type": "application/json" }
9523
+ });
9524
+ if (!initiate.ok) {
9525
+ throw mapOneDriveResponseError(initiate, normalized, await safeReadText3(initiate));
9526
+ }
9527
+ const initiateBody = await initiate.json();
9528
+ const uploadUrl = initiateBody.uploadUrl;
9529
+ if (typeof uploadUrl !== "string" || uploadUrl === "") {
9530
+ throw new ConnectionError({
9531
+ details: { path: normalized },
9532
+ message: "OneDrive createUploadSession returned no uploadUrl",
9533
+ retryable: true
9534
+ });
9535
+ }
9536
+ let bytesTransferred = 0;
9537
+ let finalItem;
9538
+ try {
9539
+ while (bytesTransferred < total) {
9540
+ request.throwIfAborted();
9541
+ const chunkEnd = Math.min(bytesTransferred + partSize, total);
9542
+ const chunk = buffered.subarray(bytesTransferred, chunkEnd);
9543
+ const response = await graphSessionFetch(this.options, uploadUrl, {
9544
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
9545
+ body: chunk,
9546
+ extraHeaders: {
9547
+ "content-length": String(chunk.byteLength),
9548
+ "content-range": `bytes ${String(bytesTransferred)}-${String(chunkEnd - 1)}/${String(total)}`,
9549
+ "content-type": "application/octet-stream"
9550
+ }
9551
+ });
9552
+ if (response.status === 202) {
9553
+ bytesTransferred = chunkEnd;
9554
+ request.reportProgress(bytesTransferred, total);
9555
+ continue;
9556
+ }
9557
+ if (response.status === 200 || response.status === 201) {
9558
+ bytesTransferred = chunkEnd;
9559
+ request.reportProgress(bytesTransferred, total);
9560
+ finalItem = await response.json();
9561
+ break;
9562
+ }
9563
+ throw mapOneDriveResponseError(response, normalized, await safeReadText3(response));
9564
+ }
9565
+ } catch (error) {
9566
+ void graphSessionFetch(this.options, uploadUrl, { method: "DELETE" }).catch(() => void 0);
9567
+ throw error;
9568
+ }
9569
+ if (finalItem === void 0) {
9570
+ throw new ConnectionError({
9571
+ details: { path: normalized },
9572
+ message: "OneDrive upload session did not return a final DriveItem",
9573
+ retryable: true
9574
+ });
9575
+ }
9576
+ const result = {
9577
+ bytesTransferred,
9578
+ totalBytes: bytesTransferred
9579
+ };
9580
+ const checksum = preferHash(finalItem.file?.hashes);
9581
+ if (checksum !== void 0) result.checksum = checksum;
9582
+ return result;
9583
+ }
9465
9584
  async fetchItem(normalized) {
9466
9585
  const url = `${this.options.driveBaseUrl}${itemSegment(normalized)}`;
9467
9586
  const response = await graphFetch(this.options, "GET", url);
@@ -9508,6 +9627,45 @@ async function graphFetch(options, method, url, fetchOptions = {}) {
9508
9627
  if (timer !== void 0) clearTimeout(timer);
9509
9628
  }
9510
9629
  }
9630
+ async function graphSessionFetch(options, uploadUrl, fetchOptions = {}) {
9631
+ const headers = {
9632
+ ...fetchOptions.extraHeaders ?? {}
9633
+ };
9634
+ const init = { headers, method: fetchOptions.method ?? "PUT" };
9635
+ if (fetchOptions.body !== void 0) {
9636
+ init.body = fetchOptions.body;
9637
+ }
9638
+ const controller = new AbortController();
9639
+ const upstream = fetchOptions.signal ?? null;
9640
+ if (upstream !== null) {
9641
+ if (upstream.aborted) controller.abort(upstream.reason);
9642
+ else upstream.addEventListener("abort", () => controller.abort(upstream.reason));
9643
+ }
9644
+ let timer;
9645
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
9646
+ timer = setTimeout(
9647
+ () => controller.abort(new Error("OneDrive upload session request timed out")),
9648
+ options.timeoutMs
9649
+ );
9650
+ }
9651
+ try {
9652
+ return await options.fetch(uploadUrl, { ...init, signal: controller.signal });
9653
+ } catch (error) {
9654
+ const safeUrl = redactSessionUrl(uploadUrl);
9655
+ throw new ConnectionError({
9656
+ cause: error,
9657
+ details: { url: safeUrl },
9658
+ message: `OneDrive upload session request to ${safeUrl} failed`,
9659
+ retryable: true
9660
+ });
9661
+ } finally {
9662
+ if (timer !== void 0) clearTimeout(timer);
9663
+ }
9664
+ }
9665
+ function redactSessionUrl(url) {
9666
+ const queryStart = url.indexOf("?");
9667
+ return queryStart === -1 ? url : `${url.slice(0, queryStart)}?<redacted>`;
9668
+ }
9511
9669
  function mapOneDriveResponseError(response, contextPath, bodyText) {
9512
9670
  const details = {
9513
9671
  bodyText: bodyText.slice(0, 500),
@@ -9629,8 +9787,12 @@ async function collectChunks3(source) {
9629
9787
  }
9630
9788
 
9631
9789
  // src/providers/cloud/AzureBlobProvider.ts
9790
+ var import_node_crypto11 = require("crypto");
9632
9791
  var AZURE_BLOB_API_VERSION = "2023-11-03";
9633
9792
  var AZURE_CHECKSUM_CAPABILITIES = ["md5"];
9793
+ var DEFAULT_AZURE_PART_SIZE = 8 * 1024 * 1024;
9794
+ var DEFAULT_AZURE_THRESHOLD = 8 * 1024 * 1024;
9795
+ var AZURE_MAX_PART_SIZE = 4e3 * 1024 * 1024;
9634
9796
  function createAzureBlobProviderFactory(options) {
9635
9797
  if (typeof options.container !== "string" || options.container === "") {
9636
9798
  throw new ConfigurationError({
@@ -9648,6 +9810,37 @@ function createAzureBlobProviderFactory(options) {
9648
9810
  }
9649
9811
  const endpoint = resolveAzureEndpoint(options);
9650
9812
  const apiVersion = options.apiVersion ?? AZURE_BLOB_API_VERSION;
9813
+ const multipartEnabled = options.multipart?.enabled ?? true;
9814
+ const partSizeBytes = options.multipart?.partSizeBytes ?? DEFAULT_AZURE_PART_SIZE;
9815
+ const thresholdBytes = options.multipart?.thresholdBytes ?? DEFAULT_AZURE_THRESHOLD;
9816
+ if (multipartEnabled) {
9817
+ if (!Number.isInteger(partSizeBytes) || partSizeBytes <= 0) {
9818
+ throw new ConfigurationError({
9819
+ details: { partSizeBytes },
9820
+ message: "AzureBlobMultipartOptions.partSizeBytes must be a positive integer",
9821
+ retryable: false
9822
+ });
9823
+ }
9824
+ if (partSizeBytes > AZURE_MAX_PART_SIZE) {
9825
+ throw new ConfigurationError({
9826
+ details: { maxBytes: AZURE_MAX_PART_SIZE, partSizeBytes },
9827
+ message: `AzureBlobMultipartOptions.partSizeBytes must not exceed ${String(AZURE_MAX_PART_SIZE)} bytes (4000 MiB)`,
9828
+ retryable: false
9829
+ });
9830
+ }
9831
+ if (!Number.isInteger(thresholdBytes) || thresholdBytes < 0) {
9832
+ throw new ConfigurationError({
9833
+ details: { thresholdBytes },
9834
+ message: "AzureBlobMultipartOptions.thresholdBytes must be a non-negative integer",
9835
+ retryable: false
9836
+ });
9837
+ }
9838
+ }
9839
+ const multipart = {
9840
+ enabled: multipartEnabled,
9841
+ partSizeBytes,
9842
+ thresholdBytes
9843
+ };
9651
9844
  const capabilities = {
9652
9845
  atomicRename: false,
9653
9846
  authentication: ["token", "oauth"],
@@ -9657,13 +9850,17 @@ function createAzureBlobProviderFactory(options) {
9657
9850
  list: true,
9658
9851
  maxConcurrency: 4,
9659
9852
  metadata: ["modifiedAt", "uniqueId"],
9660
- notes: [
9661
- "Azure Blob provider performs single-shot block-blob uploads via PUT; staged-block + Put Block List uploads are not yet supported."
9853
+ notes: multipartEnabled ? [
9854
+ `Azure Blob staged-block upload enabled by default (partSize=${String(multipart.partSizeBytes)}B, threshold=${String(multipart.thresholdBytes)}B).`,
9855
+ "Payloads at or below the threshold automatically fall back to single-shot block-blob PUT.",
9856
+ "Pass `multipart: { enabled: false }` to force the legacy single-shot behaviour."
9857
+ ] : [
9858
+ "Azure Blob provider performs single-shot block-blob uploads via PUT; entire object is buffered in memory before transmission."
9662
9859
  ],
9663
9860
  provider: id,
9664
9861
  readStream: true,
9665
9862
  resumeDownload: true,
9666
- resumeUpload: false,
9863
+ resumeUpload: multipartEnabled,
9667
9864
  serverSideCopy: false,
9668
9865
  serverSideMove: false,
9669
9866
  stat: true,
@@ -9680,6 +9877,7 @@ function createAzureBlobProviderFactory(options) {
9680
9877
  endpoint,
9681
9878
  fetch: fetchImpl,
9682
9879
  id,
9880
+ multipart,
9683
9881
  ...options.sasToken !== void 0 ? { sasToken: options.sasToken } : {}
9684
9882
  }),
9685
9883
  id
@@ -9725,7 +9923,8 @@ var AzureBlobProvider = class {
9725
9923
  defaultHeaders: this.internals.defaultHeaders,
9726
9924
  endpoint: this.internals.endpoint,
9727
9925
  fetch: this.internals.fetch,
9728
- id: this.internals.id
9926
+ id: this.internals.id,
9927
+ multipart: this.internals.multipart
9729
9928
  };
9730
9929
  if (bearerToken !== void 0) sessionOptions.bearerToken = bearerToken;
9731
9930
  if (this.internals.sasToken !== void 0) sessionOptions.sasToken = this.internals.sasToken;
@@ -9860,12 +10059,19 @@ var AzureBlobTransferOperations = class {
9860
10059
  if (request.offset !== void 0 && request.offset > 0) {
9861
10060
  throw new UnsupportedFeatureError({
9862
10061
  details: { offset: request.offset },
9863
- message: "Azure Blob provider does not yet support staged-block resumable uploads",
10062
+ message: "Azure Blob provider does not yet support cross-attempt resume of staged-block uploads",
9864
10063
  retryable: false
9865
10064
  });
9866
10065
  }
9867
10066
  const normalized = normalizeRemotePath(request.endpoint.path);
9868
- const buffered = await collectChunks4(request.content);
10067
+ const multipart = this.options.multipart;
10068
+ if (!multipart.enabled) {
10069
+ const buffered = await collectChunks4(request.content);
10070
+ return this.singleShotPut(request, normalized, buffered);
10071
+ }
10072
+ return this.writeStagedBlocks(request, normalized);
10073
+ }
10074
+ async singleShotPut(request, normalized, buffered) {
9869
10075
  const url = buildBlobUrl(this.options, normalized);
9870
10076
  const response = await azureFetch(this.options, "PUT", url, {
9871
10077
  ...request.signal !== void 0 ? { signal: request.signal } : {},
@@ -9887,6 +10093,92 @@ var AzureBlobTransferOperations = class {
9887
10093
  if (md5 !== null && md5 !== "") result.checksum = md5;
9888
10094
  return result;
9889
10095
  }
10096
+ async writeStagedBlocks(request, normalized) {
10097
+ const multipart = this.options.multipart;
10098
+ const partSize = multipart.partSizeBytes;
10099
+ const iterator = request.content[Symbol.asyncIterator]();
10100
+ const uploadNonce = generateUploadNonce();
10101
+ const initialBuffer = [];
10102
+ let initialSize = 0;
10103
+ while (initialSize <= multipart.thresholdBytes) {
10104
+ const next = await iterator.next();
10105
+ if (next.done === true) break;
10106
+ const chunk = next.value;
10107
+ if (chunk.byteLength === 0) continue;
10108
+ initialBuffer.push(chunk);
10109
+ initialSize += chunk.byteLength;
10110
+ }
10111
+ if (initialSize <= multipart.thresholdBytes) {
10112
+ return this.singleShotPut(request, normalized, concatChunks2(initialBuffer, initialSize));
10113
+ }
10114
+ const blockIds = [];
10115
+ let bytesTransferred = 0;
10116
+ let partNumber = 1;
10117
+ let buffer = [...initialBuffer];
10118
+ let bufferSize = initialSize;
10119
+ const flushBlocks = async (final) => {
10120
+ while (bufferSize >= partSize || final && bufferSize > 0) {
10121
+ const take = Math.min(bufferSize, partSize);
10122
+ const sliced = sliceFromBuffers(buffer, take);
10123
+ buffer = sliced.remaining;
10124
+ bufferSize -= sliced.bytes.byteLength;
10125
+ const blockId = encodeBlockId(uploadNonce, partNumber);
10126
+ const blockUrl = buildBlobUrl(this.options, normalized, {
10127
+ blockid: blockId,
10128
+ comp: "block"
10129
+ });
10130
+ const response = await azureFetch(this.options, "PUT", blockUrl, {
10131
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
10132
+ body: sliced.bytes,
10133
+ extraHeaders: { "content-type": "application/octet-stream" }
10134
+ });
10135
+ if (!response.ok) {
10136
+ throw mapAzureResponseError(response, normalized, await safeReadText4(response));
10137
+ }
10138
+ blockIds.push(blockId);
10139
+ bytesTransferred += sliced.bytes.byteLength;
10140
+ request.reportProgress(bytesTransferred, void 0);
10141
+ partNumber += 1;
10142
+ }
10143
+ };
10144
+ await flushBlocks(false);
10145
+ while (true) {
10146
+ request.throwIfAborted();
10147
+ const next = await iterator.next();
10148
+ if (next.done === true) break;
10149
+ if (next.value.byteLength === 0) continue;
10150
+ buffer.push(next.value);
10151
+ bufferSize += next.value.byteLength;
10152
+ await flushBlocks(false);
10153
+ }
10154
+ await flushBlocks(true);
10155
+ if (blockIds.length === 0) {
10156
+ throw new ConnectionError({
10157
+ message: "Azure Blob staged-block upload completed with zero blocks",
10158
+ retryable: false
10159
+ });
10160
+ }
10161
+ const commitUrl = buildBlobUrl(this.options, normalized, { comp: "blocklist" });
10162
+ const xmlBody = buildBlockListXml(blockIds);
10163
+ const commitResponse = await azureFetch(this.options, "PUT", commitUrl, {
10164
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
10165
+ body: new TextEncoder().encode(xmlBody),
10166
+ extraHeaders: {
10167
+ "content-type": "application/xml",
10168
+ "x-ms-blob-content-type": "application/octet-stream"
10169
+ }
10170
+ });
10171
+ if (!commitResponse.ok) {
10172
+ throw mapAzureResponseError(commitResponse, normalized, await safeReadText4(commitResponse));
10173
+ }
10174
+ const result = {
10175
+ bytesTransferred,
10176
+ totalBytes: bytesTransferred
10177
+ };
10178
+ const md5 = commitResponse.headers.get("content-md5");
10179
+ if (md5 !== null && md5 !== "") result.checksum = md5;
10180
+ return result;
10181
+ }
9890
10182
  };
9891
10183
  async function azureFetch(options, method, url, fetchOptions = {}) {
9892
10184
  const headers = {
@@ -9932,14 +10224,17 @@ function buildContainerUrl(options, params) {
9932
10224
  appendSas(search, options.sasToken);
9933
10225
  return `${options.endpoint}/${encodeURIComponent(options.container)}?${search.toString()}`;
9934
10226
  }
9935
- function buildBlobUrl(options, normalized) {
10227
+ function buildBlobUrl(options, normalized, extraParams) {
9936
10228
  const blobPath = normalized.replace(/^\/+/u, "");
9937
10229
  const encoded = blobPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
9938
10230
  const base = `${options.endpoint}/${encodeURIComponent(options.container)}/${encoded}`;
9939
- if (options.sasToken !== void 0 && options.sasToken !== "") {
9940
- return `${base}?${options.sasToken}`;
10231
+ const search = new URLSearchParams();
10232
+ if (extraParams !== void 0) {
10233
+ for (const [k, v] of Object.entries(extraParams)) search.set(k, v);
9941
10234
  }
9942
- return base;
10235
+ appendSas(search, options.sasToken);
10236
+ const query = search.toString();
10237
+ return query === "" ? base : `${base}?${query}`;
9943
10238
  }
9944
10239
  function appendSas(search, sasToken) {
9945
10240
  if (sasToken === void 0 || sasToken === "") return;
@@ -10091,11 +10386,63 @@ async function collectChunks4(source) {
10091
10386
  }
10092
10387
  return out;
10093
10388
  }
10389
+ function concatChunks2(chunks, totalSize) {
10390
+ const out = new Uint8Array(totalSize);
10391
+ let offset = 0;
10392
+ for (const chunk of chunks) {
10393
+ out.set(chunk, offset);
10394
+ offset += chunk.byteLength;
10395
+ }
10396
+ return out;
10397
+ }
10398
+ function sliceFromBuffers(buffers, size) {
10399
+ const out = new Uint8Array(size);
10400
+ let offset = 0;
10401
+ let i = 0;
10402
+ while (offset < size && i < buffers.length) {
10403
+ const chunk = buffers[i];
10404
+ if (chunk === void 0) {
10405
+ i += 1;
10406
+ continue;
10407
+ }
10408
+ const remaining = size - offset;
10409
+ if (chunk.byteLength <= remaining) {
10410
+ out.set(chunk, offset);
10411
+ offset += chunk.byteLength;
10412
+ i += 1;
10413
+ } else {
10414
+ out.set(chunk.subarray(0, remaining), offset);
10415
+ const leftover = chunk.subarray(remaining);
10416
+ const next = buffers.slice(i + 1);
10417
+ next.unshift(leftover);
10418
+ return { bytes: out, remaining: next };
10419
+ }
10420
+ }
10421
+ return { bytes: out.subarray(0, offset), remaining: buffers.slice(i) };
10422
+ }
10423
+ function encodeBlockId(nonce, partNumber) {
10424
+ const padded = String(partNumber).padStart(9, "0");
10425
+ const raw = `${nonce}-${padded}`;
10426
+ return Buffer.from(raw, "utf8").toString("base64");
10427
+ }
10428
+ function generateUploadNonce() {
10429
+ return (0, import_node_crypto11.randomBytes)(4).toString("hex");
10430
+ }
10431
+ function buildBlockListXml(blockIds) {
10432
+ const items = blockIds.map((id) => `<Latest>${escapeXml(id)}</Latest>`).join("");
10433
+ return `<?xml version="1.0" encoding="utf-8"?><BlockList>${items}</BlockList>`;
10434
+ }
10435
+ function escapeXml(value) {
10436
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
10437
+ }
10094
10438
 
10095
10439
  // src/providers/cloud/GcsProvider.ts
10096
10440
  var GCS_JSON_API_BASE = "https://storage.googleapis.com/storage/v1";
10097
10441
  var GCS_UPLOAD_API_BASE = "https://storage.googleapis.com/upload/storage/v1";
10098
10442
  var GCS_CHECKSUM_CAPABILITIES = ["md5", "crc32c"];
10443
+ var GCS_CHUNK_ALIGNMENT = 256 * 1024;
10444
+ var DEFAULT_GCS_PART_SIZE = 8 * 1024 * 1024;
10445
+ var DEFAULT_GCS_THRESHOLD = 8 * 1024 * 1024;
10099
10446
  function createGcsProviderFactory(options) {
10100
10447
  if (typeof options.bucket !== "string" || options.bucket === "") {
10101
10448
  throw new ConfigurationError({
@@ -10113,6 +10460,37 @@ function createGcsProviderFactory(options) {
10113
10460
  }
10114
10461
  const apiBaseUrl = (options.apiBaseUrl ?? GCS_JSON_API_BASE).replace(/\/+$/u, "");
10115
10462
  const uploadBaseUrl = (options.uploadBaseUrl ?? GCS_UPLOAD_API_BASE).replace(/\/+$/u, "");
10463
+ const multipartEnabled = options.multipart?.enabled ?? true;
10464
+ const partSizeBytes = options.multipart?.partSizeBytes ?? DEFAULT_GCS_PART_SIZE;
10465
+ const thresholdBytes = options.multipart?.thresholdBytes ?? DEFAULT_GCS_THRESHOLD;
10466
+ if (multipartEnabled) {
10467
+ if (!Number.isInteger(partSizeBytes) || partSizeBytes <= 0) {
10468
+ throw new ConfigurationError({
10469
+ details: { partSizeBytes },
10470
+ message: "GcsMultipartOptions.partSizeBytes must be a positive integer",
10471
+ retryable: false
10472
+ });
10473
+ }
10474
+ if (partSizeBytes % GCS_CHUNK_ALIGNMENT !== 0) {
10475
+ throw new ConfigurationError({
10476
+ details: { partSizeBytes },
10477
+ message: `GCS multipart partSizeBytes must be a multiple of ${String(GCS_CHUNK_ALIGNMENT)} bytes (256 KiB)`,
10478
+ retryable: false
10479
+ });
10480
+ }
10481
+ if (!Number.isInteger(thresholdBytes) || thresholdBytes < 0) {
10482
+ throw new ConfigurationError({
10483
+ details: { thresholdBytes },
10484
+ message: "GcsMultipartOptions.thresholdBytes must be a non-negative integer",
10485
+ retryable: false
10486
+ });
10487
+ }
10488
+ }
10489
+ const multipart = {
10490
+ enabled: multipartEnabled,
10491
+ partSizeBytes,
10492
+ thresholdBytes
10493
+ };
10116
10494
  const capabilities = {
10117
10495
  atomicRename: false,
10118
10496
  authentication: ["token", "oauth"],
@@ -10122,13 +10500,17 @@ function createGcsProviderFactory(options) {
10122
10500
  list: true,
10123
10501
  maxConcurrency: 4,
10124
10502
  metadata: ["modifiedAt", "createdAt", "uniqueId"],
10125
- notes: [
10126
- "GCS provider performs single-shot media uploads via /upload?uploadType=media; resumable upload sessions are not yet supported."
10503
+ notes: multipartEnabled ? [
10504
+ `GCS resumable-upload session enabled by default (partSize=${String(multipart.partSizeBytes)}B, threshold=${String(multipart.thresholdBytes)}B).`,
10505
+ "Payloads at or below the threshold automatically fall back to single-shot uploadType=media POST.",
10506
+ "Pass `multipart: { enabled: false }` to force the legacy single-shot behaviour."
10507
+ ] : [
10508
+ "GCS provider performs single-shot media uploads via /upload?uploadType=media; resumable upload sessions are disabled."
10127
10509
  ],
10128
10510
  provider: id,
10129
10511
  readStream: true,
10130
10512
  resumeDownload: true,
10131
- resumeUpload: false,
10513
+ resumeUpload: multipartEnabled,
10132
10514
  serverSideCopy: false,
10133
10515
  serverSideMove: false,
10134
10516
  stat: true,
@@ -10144,6 +10526,7 @@ function createGcsProviderFactory(options) {
10144
10526
  defaultHeaders: { ...options.defaultHeaders ?? {} },
10145
10527
  fetch: fetchImpl,
10146
10528
  id,
10529
+ multipart,
10147
10530
  uploadBaseUrl
10148
10531
  }),
10149
10532
  id
@@ -10179,6 +10562,7 @@ var GcsProvider = class {
10179
10562
  defaultHeaders: this.internals.defaultHeaders,
10180
10563
  fetch: this.internals.fetch,
10181
10564
  id: this.internals.id,
10565
+ multipart: this.internals.multipart,
10182
10566
  token,
10183
10567
  uploadBaseUrl: this.internals.uploadBaseUrl
10184
10568
  };
@@ -10300,13 +10684,20 @@ var GcsTransferOperations = class {
10300
10684
  if (request.offset !== void 0 && request.offset > 0) {
10301
10685
  throw new UnsupportedFeatureError({
10302
10686
  details: { offset: request.offset },
10303
- message: "GCS provider does not yet support resumable upload sessions",
10687
+ message: "GCS provider does not yet support cross-attempt resume of upload sessions",
10304
10688
  retryable: false
10305
10689
  });
10306
10690
  }
10307
10691
  const normalized = normalizeRemotePath(request.endpoint.path);
10308
10692
  const objectName = toGcsObjectName(normalized);
10309
- const buffered = await collectChunks5(request.content);
10693
+ const multipart = this.options.multipart;
10694
+ if (!multipart.enabled) {
10695
+ const buffered = await collectChunks5(request.content);
10696
+ return this.singleShotMedia(request, normalized, objectName, buffered);
10697
+ }
10698
+ return this.writeResumableSession(request, normalized, objectName);
10699
+ }
10700
+ async singleShotMedia(request, normalized, objectName, buffered) {
10310
10701
  const url = `${this.options.uploadBaseUrl}/b/${encodeURIComponent(this.options.bucket)}/o?uploadType=media&name=${encodeURIComponent(objectName)}`;
10311
10702
  const response = await gcsFetch(this.options, "POST", url, {
10312
10703
  ...request.signal !== void 0 ? { signal: request.signal } : {},
@@ -10325,6 +10716,124 @@ var GcsTransferOperations = class {
10325
10716
  if (typeof item.md5Hash === "string" && item.md5Hash !== "") result.checksum = item.md5Hash;
10326
10717
  return result;
10327
10718
  }
10719
+ async writeResumableSession(request, normalized, objectName) {
10720
+ const multipart = this.options.multipart;
10721
+ const partSize = multipart.partSizeBytes;
10722
+ const iterator = request.content[Symbol.asyncIterator]();
10723
+ const initialBuffer = [];
10724
+ let initialSize = 0;
10725
+ while (initialSize <= multipart.thresholdBytes) {
10726
+ const next = await iterator.next();
10727
+ if (next.done === true) break;
10728
+ const chunk = next.value;
10729
+ if (chunk.byteLength === 0) continue;
10730
+ initialBuffer.push(chunk);
10731
+ initialSize += chunk.byteLength;
10732
+ }
10733
+ if (initialSize <= multipart.thresholdBytes) {
10734
+ return this.singleShotMedia(
10735
+ request,
10736
+ normalized,
10737
+ objectName,
10738
+ concatChunks3(initialBuffer, initialSize)
10739
+ );
10740
+ }
10741
+ const initiateUrl = `${this.options.uploadBaseUrl}/b/${encodeURIComponent(this.options.bucket)}/o?uploadType=resumable&name=${encodeURIComponent(objectName)}`;
10742
+ const initiateResponse = await gcsFetch(this.options, "POST", initiateUrl, {
10743
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
10744
+ body: new TextEncoder().encode("{}"),
10745
+ extraHeaders: {
10746
+ "content-type": "application/json; charset=UTF-8",
10747
+ "x-upload-content-type": "application/octet-stream"
10748
+ }
10749
+ });
10750
+ if (!initiateResponse.ok) {
10751
+ throw mapGcsResponseError(initiateResponse, normalized, await safeReadText5(initiateResponse));
10752
+ }
10753
+ const sessionUri = initiateResponse.headers.get("location");
10754
+ if (sessionUri === null || sessionUri === "") {
10755
+ throw new ConnectionError({
10756
+ details: { path: normalized },
10757
+ message: "GCS resumable session initiation returned no Location header",
10758
+ retryable: true
10759
+ });
10760
+ }
10761
+ let bytesTransferred = 0;
10762
+ let buffer = [...initialBuffer];
10763
+ let bufferSize = initialSize;
10764
+ let sourceExhausted = false;
10765
+ let finalItem;
10766
+ const flushChunks = async (final) => {
10767
+ while (bufferSize >= partSize || final && bufferSize > 0) {
10768
+ const take = final ? bufferSize : partSize;
10769
+ const sliced = sliceFromBuffers2(buffer, take);
10770
+ buffer = sliced.remaining;
10771
+ bufferSize -= sliced.bytes.byteLength;
10772
+ const chunkStart = bytesTransferred;
10773
+ const chunkEnd = chunkStart + sliced.bytes.byteLength - 1;
10774
+ const totalRange = final ? String(chunkEnd + 1) : "*";
10775
+ const headers = {
10776
+ "content-length": String(sliced.bytes.byteLength),
10777
+ "content-range": `bytes ${String(chunkStart)}-${String(chunkEnd)}/${totalRange}`,
10778
+ "content-type": "application/octet-stream"
10779
+ };
10780
+ const response = await gcsSessionFetch(this.options, sessionUri, {
10781
+ ...request.signal !== void 0 ? { signal: request.signal } : {},
10782
+ body: sliced.bytes,
10783
+ extraHeaders: headers
10784
+ });
10785
+ if (response.status === 308) {
10786
+ bytesTransferred += sliced.bytes.byteLength;
10787
+ request.reportProgress(bytesTransferred, void 0);
10788
+ continue;
10789
+ }
10790
+ if (response.status === 200 || response.status === 201) {
10791
+ bytesTransferred += sliced.bytes.byteLength;
10792
+ request.reportProgress(bytesTransferred, bytesTransferred);
10793
+ finalItem = await response.json();
10794
+ return;
10795
+ }
10796
+ throw mapGcsResponseError(response, normalized, await safeReadText5(response));
10797
+ }
10798
+ };
10799
+ try {
10800
+ await flushChunks(false);
10801
+ while (!sourceExhausted) {
10802
+ request.throwIfAborted();
10803
+ const next = await iterator.next();
10804
+ if (next.done === true) {
10805
+ sourceExhausted = true;
10806
+ break;
10807
+ }
10808
+ if (next.value.byteLength === 0) continue;
10809
+ buffer.push(next.value);
10810
+ bufferSize += next.value.byteLength;
10811
+ await flushChunks(false);
10812
+ if (finalItem !== void 0) break;
10813
+ }
10814
+ if (finalItem === void 0) {
10815
+ await flushChunks(true);
10816
+ }
10817
+ } catch (error) {
10818
+ void gcsSessionFetch(this.options, sessionUri, { method: "DELETE" }).catch(() => void 0);
10819
+ throw error;
10820
+ }
10821
+ if (finalItem === void 0) {
10822
+ throw new ConnectionError({
10823
+ details: { path: normalized },
10824
+ message: "GCS resumable upload did not return a final object",
10825
+ retryable: true
10826
+ });
10827
+ }
10828
+ const result = {
10829
+ bytesTransferred,
10830
+ totalBytes: bytesTransferred
10831
+ };
10832
+ if (typeof finalItem.md5Hash === "string" && finalItem.md5Hash !== "") {
10833
+ result.checksum = finalItem.md5Hash;
10834
+ }
10835
+ return result;
10836
+ }
10328
10837
  };
10329
10838
  async function gcsFetch(options, method, url, fetchOptions = {}) {
10330
10839
  const headers = {
@@ -10483,6 +10992,81 @@ async function collectChunks5(source) {
10483
10992
  }
10484
10993
  return out;
10485
10994
  }
10995
+ function concatChunks3(chunks, totalSize) {
10996
+ const out = new Uint8Array(totalSize);
10997
+ let offset = 0;
10998
+ for (const chunk of chunks) {
10999
+ out.set(chunk, offset);
11000
+ offset += chunk.byteLength;
11001
+ }
11002
+ return out;
11003
+ }
11004
+ function sliceFromBuffers2(buffers, size) {
11005
+ const out = new Uint8Array(size);
11006
+ let offset = 0;
11007
+ let i = 0;
11008
+ while (offset < size && i < buffers.length) {
11009
+ const chunk = buffers[i];
11010
+ if (chunk === void 0) {
11011
+ i += 1;
11012
+ continue;
11013
+ }
11014
+ const remaining = size - offset;
11015
+ if (chunk.byteLength <= remaining) {
11016
+ out.set(chunk, offset);
11017
+ offset += chunk.byteLength;
11018
+ i += 1;
11019
+ } else {
11020
+ out.set(chunk.subarray(0, remaining), offset);
11021
+ const leftover = chunk.subarray(remaining);
11022
+ const next = buffers.slice(i + 1);
11023
+ next.unshift(leftover);
11024
+ return { bytes: out, remaining: next };
11025
+ }
11026
+ }
11027
+ return { bytes: out.subarray(0, offset), remaining: buffers.slice(i) };
11028
+ }
11029
+ async function gcsSessionFetch(options, sessionUri, fetchOptions = {}) {
11030
+ const headers = {
11031
+ ...options.defaultHeaders,
11032
+ ...fetchOptions.extraHeaders ?? {},
11033
+ authorization: `Bearer ${options.token}`
11034
+ };
11035
+ const init = { headers, method: fetchOptions.method ?? "PUT" };
11036
+ if (fetchOptions.body !== void 0) {
11037
+ init.body = fetchOptions.body;
11038
+ }
11039
+ const controller = new AbortController();
11040
+ const upstream = fetchOptions.signal ?? null;
11041
+ if (upstream !== null) {
11042
+ if (upstream.aborted) controller.abort(upstream.reason);
11043
+ else upstream.addEventListener("abort", () => controller.abort(upstream.reason));
11044
+ }
11045
+ let timer;
11046
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
11047
+ timer = setTimeout(
11048
+ () => controller.abort(new Error("GCS resumable session request timed out")),
11049
+ options.timeoutMs
11050
+ );
11051
+ }
11052
+ try {
11053
+ return await options.fetch(sessionUri, { ...init, signal: controller.signal });
11054
+ } catch (error) {
11055
+ const safeUrl = redactSessionUrl2(sessionUri);
11056
+ throw new ConnectionError({
11057
+ cause: error,
11058
+ details: { url: safeUrl },
11059
+ message: `GCS resumable session request to ${safeUrl} failed`,
11060
+ retryable: true
11061
+ });
11062
+ } finally {
11063
+ if (timer !== void 0) clearTimeout(timer);
11064
+ }
11065
+ }
11066
+ function redactSessionUrl2(url) {
11067
+ const queryStart = url.indexOf("?");
11068
+ return queryStart === -1 ? url : `${url.slice(0, queryStart)}?<redacted>`;
11069
+ }
10486
11070
 
10487
11071
  // src/providers/local/LocalProvider.ts
10488
11072
  var import_node_fs = require("fs");
@@ -10693,9 +11277,9 @@ async function collectTransferContent(request) {
10693
11277
  byteLength += clonedChunk.byteLength;
10694
11278
  request.reportProgress(byteLength, request.totalBytes);
10695
11279
  }
10696
- return concatChunks2(chunks, byteLength);
11280
+ return concatChunks4(chunks, byteLength);
10697
11281
  }
10698
- function concatChunks2(chunks, byteLength) {
11282
+ function concatChunks4(chunks, byteLength) {
10699
11283
  const content = new Uint8Array(byteLength);
10700
11284
  let offset = 0;
10701
11285
  for (const chunk of chunks) {
@@ -11269,9 +11853,9 @@ async function collectTransferContent2(request) {
11269
11853
  byteLength += clonedChunk.byteLength;
11270
11854
  request.reportProgress(byteLength, request.totalBytes);
11271
11855
  }
11272
- return concatChunks3(chunks, byteLength);
11856
+ return concatChunks5(chunks, byteLength);
11273
11857
  }
11274
- function concatChunks3(chunks, byteLength) {
11858
+ function concatChunks5(chunks, byteLength) {
11275
11859
  const content = new Uint8Array(byteLength);
11276
11860
  let offset = 0;
11277
11861
  for (const chunk of chunks) {
@@ -11954,12 +12538,12 @@ function parentOf(path2) {
11954
12538
  }
11955
12539
 
11956
12540
  // src/providers/web/S3Provider.ts
11957
- var import_node_crypto12 = require("crypto");
12541
+ var import_node_crypto13 = require("crypto");
11958
12542
  var import_promises3 = require("fs/promises");
11959
12543
  var import_node_path3 = require("path");
11960
12544
 
11961
12545
  // src/providers/web/awsSigv4.ts
11962
- var import_node_crypto11 = require("crypto");
12546
+ var import_node_crypto12 = require("crypto");
11963
12547
  function signSigV4(input) {
11964
12548
  const now = input.now ?? /* @__PURE__ */ new Date();
11965
12549
  const amzDate = formatAmzDate(now);
@@ -12029,13 +12613,13 @@ function encodeRfc3986(value) {
12029
12613
  );
12030
12614
  }
12031
12615
  function sha256Hex(data) {
12032
- return (0, import_node_crypto11.createHash)("sha256").update(data).digest("hex");
12616
+ return (0, import_node_crypto12.createHash)("sha256").update(data).digest("hex");
12033
12617
  }
12034
12618
  function hmac(key, data) {
12035
- return (0, import_node_crypto11.createHmac)("sha256", key).update(data, "utf8").digest();
12619
+ return (0, import_node_crypto12.createHmac)("sha256", key).update(data, "utf8").digest();
12036
12620
  }
12037
12621
  function hmacHex(key, data) {
12038
- return (0, import_node_crypto11.createHmac)("sha256", key).update(data, "utf8").digest("hex");
12622
+ return (0, import_node_crypto12.createHmac)("sha256", key).update(data, "utf8").digest("hex");
12039
12623
  }
12040
12624
 
12041
12625
  // src/providers/web/S3Provider.ts
@@ -12061,7 +12645,7 @@ function createFileSystemS3MultipartResumeStore(options) {
12061
12645
  });
12062
12646
  }
12063
12647
  const fileFor = (key) => {
12064
- const hash = (0, import_node_crypto12.createHash)("sha256").update(`${key.bucket}\0${key.jobId}\0${key.path}`).digest("hex");
12648
+ const hash = (0, import_node_crypto13.createHash)("sha256").update(`${key.bucket}\0${key.jobId}\0${key.path}`).digest("hex");
12065
12649
  return (0, import_node_path3.join)(directory, `${hash}.json`);
12066
12650
  };
12067
12651
  return {
@@ -12438,7 +13022,7 @@ var S3TransferOperations = class {
12438
13022
  const flushPart = async (final) => {
12439
13023
  while (bufferSize >= partSize || final && bufferSize > 0) {
12440
13024
  const take = final ? bufferSize : partSize;
12441
- const partBytes = sliceFromBuffers(buffer, take);
13025
+ const partBytes = sliceFromBuffers3(buffer, take);
12442
13026
  buffer = partBytes.remaining;
12443
13027
  bufferSize -= partBytes.bytes.byteLength;
12444
13028
  const partUrl = new URL(objectUrl.toString());
@@ -12627,7 +13211,7 @@ function concat(chunks, totalSize) {
12627
13211
  }
12628
13212
  return out;
12629
13213
  }
12630
- function sliceFromBuffers(buffers, size) {
13214
+ function sliceFromBuffers3(buffers, size) {
12631
13215
  const out = new Uint8Array(size);
12632
13216
  let offset = 0;
12633
13217
  let i = 0;
@@ -12659,11 +13243,11 @@ async function abortMultipart(options, objectUrl, uploadId) {
12659
13243
  }
12660
13244
  function buildCompleteMultipartBody(parts) {
12661
13245
  const partsXml = parts.map(
12662
- (part) => `<Part><PartNumber>${String(part.partNumber)}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`
13246
+ (part) => `<Part><PartNumber>${String(part.partNumber)}</PartNumber><ETag>${escapeXml2(part.etag)}</ETag></Part>`
12663
13247
  ).join("");
12664
13248
  return `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
12665
13249
  }
12666
- function escapeXml(value) {
13250
+ function escapeXml2(value) {
12667
13251
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
12668
13252
  }
12669
13253
  function parseListObjectsV2(xml, prefix) {
@@ -12743,7 +13327,7 @@ function getBuiltinCapabilityMatrix() {
12743
13327
  label: "FTPS"
12744
13328
  },
12745
13329
  {
12746
- capabilities: createNativeSftpProviderFactory().capabilities,
13330
+ capabilities: createSftpProviderFactory().capabilities,
12747
13331
  id: "sftp",
12748
13332
  label: "SFTP"
12749
13333
  },
@@ -12812,8 +13396,8 @@ function formatCapabilityMatrixMarkdown(matrix = getBuiltinCapabilityMatrix()) {
12812
13396
  const c = entry.capabilities;
12813
13397
  const yesNo = (value) => value ? "\u2705" : "\u274C";
12814
13398
  const sideways = `${yesNo(c.serverSideCopy)} / ${yesNo(c.serverSideMove)}`;
12815
- const checksums = c.checksum.length === 0 ? "\u2014" : c.checksum.join(", ");
12816
- const auth = c.authentication.length === 0 ? "\u2014" : c.authentication.join(", ");
13399
+ const checksums = c.checksum.length === 0 ? "-" : c.checksum.join(", ");
13400
+ const auth = c.authentication.length === 0 ? "-" : c.authentication.join(", ");
12817
13401
  return `| ${entry.label} | ${yesNo(c.list)} | ${yesNo(c.stat)} | ${yesNo(c.readStream)} | ${yesNo(c.writeStream)} | ${yesNo(c.resumeDownload)} | ${yesNo(c.resumeUpload)} | ${sideways} | ${checksums} | ${auth} |`;
12818
13402
  });
12819
13403
  return [header, divider, ...rows].join("\n");
@@ -13399,6 +13983,84 @@ function mapFtp550(details) {
13399
13983
  return new PermissionDeniedError(details);
13400
13984
  }
13401
13985
 
13986
+ // src/protocols/ssh/runSshCommand.ts
13987
+ var import_node_net3 = require("net");
13988
+ var DEFAULT_PORT = 22;
13989
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
13990
+ var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
13991
+ var DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
13992
+ async function runSshCommand(options) {
13993
+ const {
13994
+ host,
13995
+ port = DEFAULT_PORT,
13996
+ command,
13997
+ auth,
13998
+ transport: transportOptions,
13999
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
14000
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES
14001
+ } = options;
14002
+ const socket = await openTcpSocket(host, port, connectTimeoutMs);
14003
+ const transport = new SshTransportConnection({
14004
+ handshakeTimeoutMs: DEFAULT_HANDSHAKE_TIMEOUT_MS,
14005
+ ...transportOptions
14006
+ });
14007
+ try {
14008
+ const handshake = await transport.connect(socket);
14009
+ const authSession = new SshAuthSession(transport);
14010
+ await authSession.authenticate({
14011
+ credential: auth,
14012
+ sessionId: handshake.keyExchange.sessionId
14013
+ });
14014
+ const conn = new SshConnectionManager(transport);
14015
+ const channel = await conn.openExecChannel(command);
14016
+ const pump = conn.start();
14017
+ pump.catch(() => {
14018
+ });
14019
+ const chunks = [];
14020
+ let bytesReceived = 0;
14021
+ try {
14022
+ for await (const chunk of channel.receiveData()) {
14023
+ bytesReceived += chunk.length;
14024
+ if (bytesReceived > maxOutputBytes) {
14025
+ throw new Error(
14026
+ `runSshCommand: stdout exceeded ${maxOutputBytes} bytes (set maxOutputBytes to allow more)`
14027
+ );
14028
+ }
14029
+ chunks.push(chunk);
14030
+ }
14031
+ } finally {
14032
+ channel.close();
14033
+ }
14034
+ const stdout = Buffer.concat(chunks);
14035
+ return {
14036
+ stdout,
14037
+ stdoutText: stdout.toString("utf8"),
14038
+ bytesReceived
14039
+ };
14040
+ } finally {
14041
+ transport.disconnect();
14042
+ }
14043
+ }
14044
+ function openTcpSocket(host, port, timeoutMs) {
14045
+ return new Promise((resolve, reject) => {
14046
+ const socket = (0, import_node_net3.connect)({ host, port });
14047
+ const timer = setTimeout(() => {
14048
+ socket.destroy();
14049
+ reject(
14050
+ new Error(`runSshCommand: TCP connect to ${host}:${port} timed out after ${timeoutMs}ms`)
14051
+ );
14052
+ }, timeoutMs);
14053
+ socket.once("connect", () => {
14054
+ clearTimeout(timer);
14055
+ resolve(socket);
14056
+ });
14057
+ socket.once("error", (error) => {
14058
+ clearTimeout(timer);
14059
+ reject(error);
14060
+ });
14061
+ });
14062
+ }
14063
+
13402
14064
  // src/transfers/TransferPlan.ts
13403
14065
  function createTransferPlan(input) {
13404
14066
  const plan = {
@@ -14856,9 +15518,9 @@ function deepFreeze(value) {
14856
15518
  }
14857
15519
 
14858
15520
  // src/mft/webhooks.ts
14859
- var import_node_crypto13 = require("crypto");
15521
+ var import_node_crypto14 = require("crypto");
14860
15522
  function signWebhookPayload(payload, secret, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
14861
- const mac = (0, import_node_crypto13.createHmac)("sha256", secret);
15523
+ const mac = (0, import_node_crypto14.createHmac)("sha256", secret);
14862
15524
  mac.update(`${timestamp}.${payload}`);
14863
15525
  return { digest: mac.digest("hex"), timestamp };
14864
15526
  }
@@ -15437,6 +16099,19 @@ var defaultRunner = ({ client, route, signal }) => {
15437
16099
  const options = { client, route, signal };
15438
16100
  return runRoute(options);
15439
16101
  };
16102
+
16103
+ // src/utils/mainModule.ts
16104
+ var import_node_url = require("url");
16105
+ function isMainModule(importMetaUrl) {
16106
+ if (typeof process === "undefined" || !process.argv || process.argv.length < 2) {
16107
+ return false;
16108
+ }
16109
+ try {
16110
+ return process.argv[1] === (0, import_node_url.fileURLToPath)(importMetaUrl);
16111
+ } catch {
16112
+ return false;
16113
+ }
16114
+ }
15440
16115
  // Annotate the CommonJS export names for ESM import in node:
15441
16116
  0 && (module.exports = {
15442
16117
  AbortError,
@@ -15503,7 +16178,6 @@ var defaultRunner = ({ client, route, signal }) => {
15503
16178
  createLocalProviderFactory,
15504
16179
  createMemoryProviderFactory,
15505
16180
  createMemoryS3MultipartResumeStore,
15506
- createNativeSftpProviderFactory,
15507
16181
  createOAuthTokenSecretSource,
15508
16182
  createOneDriveProviderFactory,
15509
16183
  createOutboxRoute,
@@ -15537,6 +16211,7 @@ var defaultRunner = ({ client, route, signal }) => {
15537
16211
  inboxFailedPath,
15538
16212
  inboxProcessedPath,
15539
16213
  isClassicProviderId,
16214
+ isMainModule,
15540
16215
  isSensitiveKey,
15541
16216
  joinRemotePath,
15542
16217
  matchKnownHosts,
@@ -15569,6 +16244,7 @@ var defaultRunner = ({ client, route, signal }) => {
15569
16244
  resolveSecret,
15570
16245
  runConnectionDiagnostics,
15571
16246
  runRoute,
16247
+ runSshCommand,
15572
16248
  serializeRemoteManifest,
15573
16249
  signWebhookPayload,
15574
16250
  sortRemoteEntries,