@zero-transfer/sdk 0.4.2 → 0.4.6

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