@zero-transfer/sdk 0.4.6 → 0.4.8

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
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  AbortError: () => AbortError,
34
34
  ApprovalRegistry: () => ApprovalRegistry,
35
35
  ApprovalRejectedError: () => ApprovalRejectedError,
36
+ ApprovalTimeoutError: () => ApprovalTimeoutError,
36
37
  AuthenticationError: () => AuthenticationError,
37
38
  AuthorizationError: () => AuthorizationError,
38
39
  CLASSIC_PROVIDER_IDS: () => CLASSIC_PROVIDER_IDS,
@@ -82,6 +83,7 @@ __export(index_exports, {
82
83
  createAtomicDeployPlan: () => createAtomicDeployPlan,
83
84
  createAzureBlobProviderFactory: () => createAzureBlobProviderFactory,
84
85
  createBandwidthThrottle: () => createBandwidthThrottle,
86
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
85
87
  createDropboxProviderFactory: () => createDropboxProviderFactory,
86
88
  createFileSystemS3MultipartResumeStore: () => createFileSystemS3MultipartResumeStore,
87
89
  createFtpProviderFactory: () => createFtpProviderFactory,
@@ -94,7 +96,6 @@ __export(index_exports, {
94
96
  createLocalProviderFactory: () => createLocalProviderFactory,
95
97
  createMemoryProviderFactory: () => createMemoryProviderFactory,
96
98
  createMemoryS3MultipartResumeStore: () => createMemoryS3MultipartResumeStore,
97
- createNativeSftpProviderFactory: () => createNativeSftpProviderFactory,
98
99
  createOAuthTokenSecretSource: () => createOAuthTokenSecretSource,
99
100
  createOneDriveProviderFactory: () => createOneDriveProviderFactory,
100
101
  createOutboxRoute: () => createOutboxRoute,
@@ -104,7 +105,7 @@ __export(index_exports, {
104
105
  createRemoteBrowser: () => createRemoteBrowser,
105
106
  createRemoteManifest: () => createRemoteManifest,
106
107
  createS3ProviderFactory: () => createS3ProviderFactory,
107
- createSftpProviderFactory: () => createNativeSftpProviderFactory,
108
+ createSftpProviderFactory: () => createSftpProviderFactory,
108
109
  createSyncPlan: () => createSyncPlan,
109
110
  createTransferClient: () => createTransferClient,
110
111
  createTransferJobsFromPlan: () => createTransferJobsFromPlan,
@@ -152,8 +153,10 @@ __export(index_exports, {
152
153
  parseUnixListLine: () => parseUnixListLine,
153
154
  redactCommand: () => redactCommand,
154
155
  redactConnectionProfile: () => redactConnectionProfile,
156
+ redactErrorForLogging: () => redactErrorForLogging,
155
157
  redactObject: () => redactObject,
156
158
  redactSecretSource: () => redactSecretSource,
159
+ redactUrlForLogging: () => redactUrlForLogging,
157
160
  redactValue: () => redactValue,
158
161
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
159
162
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -179,6 +182,68 @@ module.exports = __toCommonJS(index_exports);
179
182
  // src/client/ZeroTransfer.ts
180
183
  var import_node_events = require("events");
181
184
 
185
+ // src/logging/redaction.ts
186
+ var REDACTED = "[REDACTED]";
187
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
188
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
189
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
190
+ function isSensitiveKey(key) {
191
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
192
+ }
193
+ function redactCommand(command) {
194
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
195
+ return `${commandName.toUpperCase()} ${REDACTED}`;
196
+ });
197
+ }
198
+ function redactValue(value) {
199
+ if (typeof value === "string") {
200
+ return redactCommand(value);
201
+ }
202
+ if (Array.isArray(value)) {
203
+ return value.map((item) => redactValue(item));
204
+ }
205
+ if (value !== null && typeof value === "object") {
206
+ return redactObject(value);
207
+ }
208
+ return value;
209
+ }
210
+ function redactObject(input) {
211
+ return Object.fromEntries(
212
+ Object.entries(input).map(([key, value]) => {
213
+ if (isSensitiveKey(key)) {
214
+ return [key, REDACTED];
215
+ }
216
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
217
+ return [key, redactUrlForLogging(value)];
218
+ }
219
+ return [key, redactValue(value)];
220
+ })
221
+ );
222
+ }
223
+ function redactUrlForLogging(url) {
224
+ let parsed;
225
+ try {
226
+ parsed = typeof url === "string" ? new URL(url) : url;
227
+ } catch {
228
+ return REDACTED;
229
+ }
230
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
231
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
232
+ return `${origin}${parsed.pathname}${query}`;
233
+ }
234
+ function redactErrorForLogging(error) {
235
+ if (error !== null && typeof error === "object") {
236
+ const candidate = error;
237
+ if (typeof candidate.toJSON === "function") {
238
+ return redactObject(candidate.toJSON());
239
+ }
240
+ }
241
+ if (error instanceof Error) {
242
+ return redactObject({ message: error.message, name: error.name });
243
+ }
244
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
245
+ }
246
+
182
247
  // src/errors/ZeroTransferError.ts
183
248
  var ZeroTransferError = class extends Error {
184
249
  /** Stable machine-readable error code. */
@@ -220,6 +285,11 @@ var ZeroTransferError = class extends Error {
220
285
  /**
221
286
  * Serializes the error into a plain object suitable for logs or API responses.
222
287
  *
288
+ * `details` and `command` are passed through secret redaction so serialized
289
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
290
+ * live {@link ZeroTransferError.details | details} property stays unredacted
291
+ * for programmatic consumers.
292
+ *
223
293
  * @returns A JSON-safe object containing public structured error fields.
224
294
  */
225
295
  toJSON() {
@@ -229,12 +299,12 @@ var ZeroTransferError = class extends Error {
229
299
  message: this.message,
230
300
  protocol: this.protocol,
231
301
  host: this.host,
232
- command: this.command,
302
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
233
303
  ftpCode: this.ftpCode,
234
304
  sftpCode: this.sftpCode,
235
305
  path: this.path,
236
306
  retryable: this.retryable,
237
- details: this.details
307
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
238
308
  };
239
309
  }
240
310
  };
@@ -757,15 +827,20 @@ var ProviderRegistry = class {
757
827
  var TransferClient = class {
758
828
  /** Provider registry used by this client. */
759
829
  registry;
830
+ /** Execution defaults applied when call sites omit their own values. */
831
+ defaults;
760
832
  logger;
761
833
  /**
762
834
  * Creates a transfer client without opening any provider connections.
763
835
  *
764
- * @param options - Optional registry, provider factories, and logger.
836
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
765
837
  */
766
838
  constructor(options = {}) {
767
839
  this.registry = options.registry ?? new ProviderRegistry();
768
840
  this.logger = options.logger ?? noopLogger;
841
+ if (options.defaults !== void 0) {
842
+ this.defaults = { ...options.defaults };
843
+ }
769
844
  for (const provider of options.providers ?? []) {
770
845
  this.registry.register(provider);
771
846
  }
@@ -1338,18 +1413,25 @@ var TransferEngine = class {
1338
1413
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1339
1414
  this.throwIfAborted(abortScope.signal, job);
1340
1415
  const attemptStartedAt = this.now();
1416
+ const attemptScope = createAttemptScope(
1417
+ abortScope.signal,
1418
+ options.timeout,
1419
+ job,
1420
+ attemptNumber
1421
+ );
1341
1422
  const context = this.createExecutionContext(
1342
1423
  job,
1343
1424
  attemptNumber,
1344
1425
  attemptStartedAt,
1345
1426
  options,
1346
- abortScope.signal,
1427
+ attemptScope.signal,
1347
1428
  (bytesTransferred) => {
1348
1429
  latestBytesTransferred = bytesTransferred;
1349
- }
1430
+ },
1431
+ attemptScope.notifyProgress
1350
1432
  );
1351
1433
  try {
1352
- const result = await runExecutor(executor, context, abortScope.signal, job);
1434
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1353
1435
  context.throwIfAborted();
1354
1436
  latestBytesTransferred = result.bytesTransferred;
1355
1437
  const completedAt = this.now();
@@ -1367,16 +1449,27 @@ var TransferEngine = class {
1367
1449
  summarizeError(error)
1368
1450
  );
1369
1451
  attempts.push(attempt);
1370
- if (error instanceof AbortError || error instanceof TimeoutError) {
1452
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1371
1453
  throw error;
1372
1454
  }
1373
- const retryInput = { attempt: attemptNumber, error, job };
1455
+ const retryInput = {
1456
+ attempt: attemptNumber,
1457
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1458
+ error,
1459
+ job
1460
+ };
1374
1461
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1375
1462
  if (shouldRetry) {
1376
1463
  options.retry?.onRetry?.(retryInput);
1464
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1465
+ if (delayMs > 0) {
1466
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1467
+ }
1377
1468
  continue;
1378
1469
  }
1379
1470
  throw createTransferFailure(job, error, attempts);
1471
+ } finally {
1472
+ attemptScope.dispose();
1380
1473
  }
1381
1474
  }
1382
1475
  throw createTransferFailure(job, void 0, attempts);
@@ -1384,12 +1477,13 @@ var TransferEngine = class {
1384
1477
  abortScope.dispose();
1385
1478
  }
1386
1479
  }
1387
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1480
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1388
1481
  const context = {
1389
1482
  attempt,
1390
1483
  job,
1391
1484
  reportProgress: (bytesTransferred, totalBytes) => {
1392
1485
  this.throwIfAborted(signal, job);
1486
+ notifyProgress();
1393
1487
  updateBytesTransferred(bytesTransferred);
1394
1488
  const progressInput = {
1395
1489
  bytesTransferred,
@@ -1458,6 +1552,96 @@ function createAbortScope(parentSignal, timeout, job) {
1458
1552
  signal: controller.signal
1459
1553
  };
1460
1554
  }
1555
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1556
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1557
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1558
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1559
+ const scope = {
1560
+ dispose: () => void 0,
1561
+ notifyProgress: () => void 0
1562
+ };
1563
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1564
+ return scope;
1565
+ }
1566
+ const controller = new AbortController();
1567
+ const retryable = timeout?.retryable ?? true;
1568
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1569
+ if (parentSignal?.aborted === true) {
1570
+ abortFromParent();
1571
+ } else {
1572
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1573
+ }
1574
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1575
+ controller.abort(
1576
+ new TimeoutError({
1577
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1578
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1579
+ retryable
1580
+ })
1581
+ );
1582
+ }, attemptTimeoutMs);
1583
+ let stallTimer;
1584
+ const armStallWatchdog = () => {
1585
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1586
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1587
+ stallTimer = setTimeout(() => {
1588
+ controller.abort(
1589
+ new TimeoutError({
1590
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1591
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1592
+ retryable
1593
+ })
1594
+ );
1595
+ }, stallTimeoutMs);
1596
+ };
1597
+ armStallWatchdog();
1598
+ return {
1599
+ dispose: () => {
1600
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1601
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1602
+ parentSignal?.removeEventListener("abort", abortFromParent);
1603
+ },
1604
+ notifyProgress: armStallWatchdog,
1605
+ signal: controller.signal
1606
+ };
1607
+ }
1608
+ function sleepWithAbort(delayMs, signal, job) {
1609
+ return new Promise((resolve, reject) => {
1610
+ if (signal === void 0) {
1611
+ setTimeout(resolve, delayMs);
1612
+ return;
1613
+ }
1614
+ if (signal.aborted) {
1615
+ reject(toAbortFailure(signal, job));
1616
+ return;
1617
+ }
1618
+ const rejectAbort = () => {
1619
+ clearTimeout(timer);
1620
+ reject(toAbortFailure(signal, job));
1621
+ };
1622
+ const timer = setTimeout(() => {
1623
+ signal.removeEventListener("abort", rejectAbort);
1624
+ resolve();
1625
+ }, delayMs);
1626
+ signal.addEventListener("abort", rejectAbort, { once: true });
1627
+ });
1628
+ }
1629
+ function toAbortFailure(signal, job) {
1630
+ if (signal.reason instanceof ZeroTransferError) {
1631
+ return signal.reason;
1632
+ }
1633
+ return new AbortError({
1634
+ details: { jobId: job.id, operation: job.operation },
1635
+ message: `Transfer job aborted: ${job.id}`,
1636
+ retryable: false
1637
+ });
1638
+ }
1639
+ function normalizeDelayMs(value) {
1640
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1641
+ return 0;
1642
+ }
1643
+ return Math.floor(value);
1644
+ }
1461
1645
  function normalizeTimeoutMs(value) {
1462
1646
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1463
1647
  return void 0;
@@ -1626,7 +1810,7 @@ async function runRoute(options) {
1626
1810
  const executor = createProviderTransferExecutor({
1627
1811
  resolveSession: ({ role }) => sessions.get(role)
1628
1812
  });
1629
- return await engine.execute(job, executor, buildExecuteOptions(options));
1813
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1630
1814
  } finally {
1631
1815
  if (destinationSession !== void 0) {
1632
1816
  await destinationSession.disconnect();
@@ -1663,12 +1847,14 @@ function defaultJobId(route, now) {
1663
1847
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1664
1848
  return `route:${route.id}:${timestamp.toString(36)}`;
1665
1849
  }
1666
- function buildExecuteOptions(options) {
1850
+ function buildExecuteOptions(options, client) {
1667
1851
  const execute = {};
1852
+ const retry = options.retry ?? client.defaults?.retry;
1853
+ const timeout = options.timeout ?? client.defaults?.timeout;
1668
1854
  if (options.signal !== void 0) execute.signal = options.signal;
1669
- if (options.retry !== void 0) execute.retry = options.retry;
1855
+ if (retry !== void 0) execute.retry = retry;
1670
1856
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1671
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1857
+ if (timeout !== void 0) execute.timeout = timeout;
1672
1858
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1673
1859
  return execute;
1674
1860
  }
@@ -1725,41 +1911,6 @@ function defaultRouteSuffix(source, destination) {
1725
1911
  return `${source}->${destination}`;
1726
1912
  }
1727
1913
 
1728
- // src/logging/redaction.ts
1729
- var REDACTED = "[REDACTED]";
1730
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1731
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1732
- function isSensitiveKey(key) {
1733
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1734
- }
1735
- function redactCommand(command) {
1736
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1737
- return `${commandName.toUpperCase()} ${REDACTED}`;
1738
- });
1739
- }
1740
- function redactValue(value) {
1741
- if (typeof value === "string") {
1742
- return redactCommand(value);
1743
- }
1744
- if (Array.isArray(value)) {
1745
- return value.map((item) => redactValue(item));
1746
- }
1747
- if (value !== null && typeof value === "object") {
1748
- return redactObject(value);
1749
- }
1750
- return value;
1751
- }
1752
- function redactObject(input) {
1753
- return Object.fromEntries(
1754
- Object.entries(input).map(([key, value]) => {
1755
- if (isSensitiveKey(key)) {
1756
- return [key, REDACTED];
1757
- }
1758
- return [key, redactValue(value)];
1759
- })
1760
- );
1761
- }
1762
-
1763
1914
  // src/profiles/SecretSource.ts
1764
1915
  var import_node_buffer2 = require("buffer");
1765
1916
  var import_promises = require("fs/promises");
@@ -2181,11 +2332,11 @@ async function resolveTlsSecretSource(source, options) {
2181
2332
  }
2182
2333
 
2183
2334
  // src/utils/path.ts
2184
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2335
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2185
2336
  function assertSafeFtpArgument(value, label = "path") {
2186
2337
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2187
2338
  throw new ConfigurationError({
2188
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2339
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2189
2340
  retryable: false,
2190
2341
  details: {
2191
2342
  label
@@ -3147,38 +3298,53 @@ async function expectCompletion(control, command, path2) {
3147
3298
  const response = await control.sendCommand(command);
3148
3299
  assertPathCommandSucceeded(response, command, path2, control.providerId);
3149
3300
  }
3150
- async function readPassiveDataCommand(control, command, path2, options = {}) {
3151
- const dataConnection = await openPassiveDataCommand(control, command, path2, options);
3301
+ async function readPassiveLinesCommand(control, command, path2, onLine) {
3302
+ const dataConnection = await openPassiveDataCommand(control, command, path2);
3152
3303
  try {
3153
- const payload = await collectPassiveData(
3154
- dataConnection,
3155
- control.operationTimeoutMs,
3156
- path2,
3157
- control.providerId
3158
- );
3304
+ const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
3305
+ command,
3306
+ onLine,
3307
+ path: path2,
3308
+ providerId: control.providerId
3309
+ });
3159
3310
  const finalResponse = await control.readFinalResponse({
3160
3311
  command,
3161
3312
  operation: "data command completion",
3162
3313
  path: path2
3163
3314
  });
3164
3315
  assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
3165
- return payload;
3316
+ if (failure !== void 0) throw failure;
3166
3317
  } catch (error) {
3167
3318
  dataConnection.close();
3168
3319
  throw error;
3169
3320
  }
3170
3321
  }
3171
3322
  async function readDirectoryEntries(control, path2) {
3323
+ const entries = [];
3324
+ const collectEntry = (entry) => {
3325
+ if (entry.name === "." || entry.name === "..") return;
3326
+ entries.push(entry);
3327
+ };
3172
3328
  try {
3173
- const payload2 = await readPassiveDataCommand(control, `MLSD ${path2}`, path2);
3174
- return parseMlsdList(payload2.toString("utf8"), path2);
3329
+ await readPassiveLinesCommand(control, `MLSD ${path2}`, path2, (rawLine) => {
3330
+ const line = rawLine.trimEnd();
3331
+ if (line.length === 0) return;
3332
+ collectEntry(parseMlsdLine(line, path2));
3333
+ });
3334
+ return entries;
3175
3335
  } catch (error) {
3176
3336
  if (!isUnsupportedFtpCommandError(error, "MLSD")) {
3177
3337
  throw error;
3178
3338
  }
3179
3339
  }
3180
- const payload = await readPassiveDataCommand(control, `LIST ${path2}`, path2);
3181
- return parseUnixList(payload.toString("utf8"), path2);
3340
+ entries.length = 0;
3341
+ const now = /* @__PURE__ */ new Date();
3342
+ await readPassiveLinesCommand(control, `LIST ${path2}`, path2, (rawLine) => {
3343
+ const line = rawLine.trimEnd();
3344
+ if (line.length === 0 || line.toLowerCase().startsWith("total ")) return;
3345
+ collectEntry(parseUnixListLine(line, path2, now));
3346
+ });
3347
+ return entries;
3182
3348
  }
3183
3349
  async function openPassiveDataCommand(control, command, path2, options = {}) {
3184
3350
  const offset = normalizeOptionalByteCount(options.offset, "offset", path2);
@@ -3351,22 +3517,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
3351
3517
  }
3352
3518
  };
3353
3519
  }
3354
- async function collectPassiveData(dataConnection, timeoutMs, path2, providerId) {
3355
- const chunks = [];
3520
+ var MAX_LIST_LINE_BYTES = 64 * 1024;
3521
+ async function consumePassiveLines(dataConnection, timeoutMs, input) {
3522
+ let carry = import_node_buffer3.Buffer.alloc(0);
3523
+ let failure;
3356
3524
  const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
3357
3525
  host: dataConnection.endpoint.host,
3358
3526
  operation: "passive data transfer",
3359
- path: path2,
3360
- providerId
3527
+ path: input.path,
3528
+ providerId: input.providerId
3361
3529
  });
3530
+ const overlongLineFailure = () => new ParseError({
3531
+ details: { command: input.command, limitBytes: MAX_LIST_LINE_BYTES, path: input.path },
3532
+ message: `FTP listing line exceeded ${String(MAX_LIST_LINE_BYTES)} bytes for ${input.command}`,
3533
+ retryable: false
3534
+ });
3535
+ const emit = (lineBytes) => {
3536
+ if (failure !== void 0) return;
3537
+ let end = lineBytes.length;
3538
+ if (end > 0 && lineBytes[end - 1] === 13) end -= 1;
3539
+ if (end === 0) return;
3540
+ if (end > MAX_LIST_LINE_BYTES) {
3541
+ failure = overlongLineFailure();
3542
+ return;
3543
+ }
3544
+ try {
3545
+ input.onLine(lineBytes.toString("utf8", 0, end));
3546
+ } catch (error) {
3547
+ failure = error instanceof Error ? error : new Error(String(error));
3548
+ }
3549
+ };
3362
3550
  try {
3363
3551
  for await (const chunk of dataConnection.socket) {
3364
- chunks.push(import_node_buffer3.Buffer.from(chunk));
3552
+ if (failure !== void 0) continue;
3553
+ const data = carry.length > 0 ? import_node_buffer3.Buffer.concat([carry, chunk]) : chunk;
3554
+ let start = 0;
3555
+ let newline = data.indexOf(10, start);
3556
+ while (newline !== -1) {
3557
+ emit(data.subarray(start, newline));
3558
+ start = newline + 1;
3559
+ newline = data.indexOf(10, start);
3560
+ }
3561
+ carry = import_node_buffer3.Buffer.from(data.subarray(start));
3562
+ if (carry.length > MAX_LIST_LINE_BYTES && failure === void 0) {
3563
+ failure = overlongLineFailure();
3564
+ }
3565
+ if (failure !== void 0) carry = import_node_buffer3.Buffer.alloc(0);
3365
3566
  }
3567
+ if (carry.length > 0) emit(carry);
3366
3568
  } finally {
3367
3569
  clearIdleTimeout();
3368
3570
  }
3369
- return import_node_buffer3.Buffer.concat(chunks);
3571
+ return failure;
3370
3572
  }
3371
3573
  async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
3372
3574
  let bytesEmitted = 0;
@@ -3615,6 +3817,13 @@ function createTlsPinnedFingerprints(profile) {
3615
3817
  if (pinnedFingerprint256 === void 0) {
3616
3818
  return void 0;
3617
3819
  }
3820
+ if (profile.tls?.rejectUnauthorized === false) {
3821
+ throw new ConfigurationError({
3822
+ message: "FTPS tls.pinnedFingerprint256 cannot be combined with rejectUnauthorized: false; pin verification runs after the TLS handshake, so chain validation must stay enabled. For self-signed certificates supply tls.ca instead of disabling validation.",
3823
+ protocol: FTPS_PROVIDER_ID,
3824
+ retryable: false
3825
+ });
3826
+ }
3618
3827
  const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
3619
3828
  if (fingerprints.length === 0) {
3620
3829
  throw new ConfigurationError({
@@ -5586,6 +5795,7 @@ var import_node_buffer12 = require("buffer");
5586
5795
  var import_node_crypto6 = require("crypto");
5587
5796
  var MIN_PADDING_LENGTH = 4;
5588
5797
  var MIN_PACKET_LENGTH = 1 + MIN_PADDING_LENGTH;
5798
+ var MAX_SSH_PACKET_LENGTH = 256 * 1024;
5589
5799
  function encodeSshTransportPacket(payload, options = {}) {
5590
5800
  const body = import_node_buffer12.Buffer.from(payload);
5591
5801
  const blockSize = normalizeBlockSize(options.blockSize ?? 8);
@@ -5663,6 +5873,14 @@ var SshTransportPacketFramer = class {
5663
5873
  const packets = [];
5664
5874
  while (this.pending.length >= 4) {
5665
5875
  const packetLength = this.pending.readUInt32BE(0);
5876
+ if (packetLength > MAX_SSH_PACKET_LENGTH) {
5877
+ throw new ParseError({
5878
+ details: { maxPacketLength: MAX_SSH_PACKET_LENGTH, packetLength },
5879
+ message: "SSH transport packet length exceeds the maximum accepted size",
5880
+ protocol: "sftp",
5881
+ retryable: false
5882
+ });
5883
+ }
5666
5884
  const frameLength = 4 + packetLength;
5667
5885
  if (this.pending.length < frameLength) {
5668
5886
  break;
@@ -6140,14 +6358,35 @@ var SshTransportHandshake = class {
6140
6358
  return { outbound };
6141
6359
  }
6142
6360
  };
6361
+ var MAX_IDENTIFICATION_LINE_BYTES = 8192;
6362
+ var MAX_PRE_IDENTIFICATION_LINES = 1024;
6143
6363
  var SshIdentificationAccumulator = class {
6144
6364
  pending = import_node_buffer14.Buffer.alloc(0);
6365
+ bannerLineCount = 0;
6145
6366
  push(chunk) {
6146
6367
  this.pending = import_node_buffer14.Buffer.concat([this.pending, import_node_buffer14.Buffer.from(chunk)]);
6147
6368
  const bannerLines = [];
6148
6369
  while (true) {
6149
6370
  const lfIndex = this.pending.indexOf(10);
6150
- if (lfIndex < 0) break;
6371
+ if (lfIndex < 0) {
6372
+ if (this.pending.length > MAX_IDENTIFICATION_LINE_BYTES) {
6373
+ throw new ProtocolError({
6374
+ details: { limitBytes: MAX_IDENTIFICATION_LINE_BYTES },
6375
+ message: "SSH identification line exceeds the maximum accepted length",
6376
+ protocol: "sftp",
6377
+ retryable: false
6378
+ });
6379
+ }
6380
+ break;
6381
+ }
6382
+ if (lfIndex > MAX_IDENTIFICATION_LINE_BYTES) {
6383
+ throw new ProtocolError({
6384
+ details: { limitBytes: MAX_IDENTIFICATION_LINE_BYTES },
6385
+ message: "SSH identification line exceeds the maximum accepted length",
6386
+ protocol: "sftp",
6387
+ retryable: false
6388
+ });
6389
+ }
6151
6390
  const lineText = trimLineEndings(this.pending.subarray(0, lfIndex + 1).toString("ascii"));
6152
6391
  const remainder = import_node_buffer14.Buffer.from(this.pending.subarray(lfIndex + 1));
6153
6392
  this.pending = remainder;
@@ -6155,6 +6394,15 @@ var SshIdentificationAccumulator = class {
6155
6394
  this.pending = import_node_buffer14.Buffer.alloc(0);
6156
6395
  return { bannerLines, identLine: lineText, remainder };
6157
6396
  }
6397
+ this.bannerLineCount += 1;
6398
+ if (this.bannerLineCount > MAX_PRE_IDENTIFICATION_LINES) {
6399
+ throw new ProtocolError({
6400
+ details: { limitLines: MAX_PRE_IDENTIFICATION_LINES },
6401
+ message: "SSH server sent too many pre-identification banner lines",
6402
+ protocol: "sftp",
6403
+ retryable: false
6404
+ });
6405
+ }
6158
6406
  bannerLines.push(lineText);
6159
6407
  }
6160
6408
  return { bannerLines, remainder: import_node_buffer14.Buffer.alloc(0) };
@@ -6287,6 +6535,14 @@ var SshTransportPacketUnprotector = class {
6287
6535
  this.framePendingRaw = import_node_buffer15.Buffer.from(this.framePendingRaw.subarray(this.blockLength));
6288
6536
  this.framePartialDecrypted = this.decipher ? import_node_buffer15.Buffer.from(this.decipher.update(firstBlock)) : import_node_buffer15.Buffer.from(firstBlock);
6289
6537
  const packetLength = this.framePartialDecrypted.readUInt32BE(0);
6538
+ if (packetLength > MAX_SSH_PACKET_LENGTH) {
6539
+ throw new ProtocolError({
6540
+ details: { maxPacketLength: MAX_SSH_PACKET_LENGTH, packetLength },
6541
+ message: "SSH encrypted packet length exceeds the maximum accepted size",
6542
+ protocol: "sftp",
6543
+ retryable: false
6544
+ });
6545
+ }
6290
6546
  const remaining = 4 + packetLength - this.blockLength + this.macLength;
6291
6547
  if (remaining < 0) {
6292
6548
  throw new ProtocolError({
@@ -6910,6 +7166,7 @@ function decodeSftpAttributesFromReader(reader) {
6910
7166
 
6911
7167
  // src/protocols/sftp/v3/SftpPacket.ts
6912
7168
  var import_node_buffer17 = require("buffer");
7169
+ var MAX_SFTP_PACKET_LENGTH = 256 * 1024;
6913
7170
  var SFTP_PACKET_TYPE = {
6914
7171
  ATTRS: 105,
6915
7172
  CLOSE: 4,
@@ -6969,6 +7226,13 @@ var SftpPacketFramer = class {
6969
7226
  const packets = [];
6970
7227
  while (this.pending.length >= 4) {
6971
7228
  const bodyLength = this.pending.readUInt32BE(0);
7229
+ if (bodyLength > MAX_SFTP_PACKET_LENGTH) {
7230
+ throw new ParseError({
7231
+ details: { bodyLength, maxPacketLength: MAX_SFTP_PACKET_LENGTH },
7232
+ message: "SFTP packet length exceeds the maximum accepted size",
7233
+ retryable: false
7234
+ });
7235
+ }
6972
7236
  const frameLength = 4 + bodyLength;
6973
7237
  if (this.pending.length < frameLength) {
6974
7238
  break;
@@ -7570,7 +7834,7 @@ function buildNativeSftpCapabilities(maxConcurrency) {
7570
7834
  var NATIVE_SFTP_PROVIDER_CAPABILITIES = buildNativeSftpCapabilities(
7571
7835
  NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
7572
7836
  );
7573
- function createNativeSftpProviderFactory(options = {}) {
7837
+ function createSftpProviderFactory(options = {}) {
7574
7838
  validateNativeSftpOptions(options);
7575
7839
  const capabilities = buildNativeSftpCapabilities(
7576
7840
  options.maxConcurrency ?? NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
@@ -8229,6 +8493,26 @@ function validateNativeSftpOptions(options) {
8229
8493
 
8230
8494
  // src/providers/web/httpInternals.ts
8231
8495
  var import_node_buffer20 = require("buffer");
8496
+ function assertHttpsEnforced(input) {
8497
+ if (input.enforceHttps && !input.secure) {
8498
+ throw new ConfigurationError({
8499
+ details: { provider: input.providerId },
8500
+ message: `Provider "${input.providerId}" is configured with enforceHttps but its transport is cleartext http; set secure: true (or drop enforceHttps to explicitly accept cleartext)`,
8501
+ retryable: false
8502
+ });
8503
+ }
8504
+ }
8505
+ var cleartextWarnedKeys = /* @__PURE__ */ new Set();
8506
+ function warnCleartextCredentials(input) {
8507
+ if (!input.hasCredentials) return;
8508
+ const key = `${input.providerId}:${input.host}`;
8509
+ if (cleartextWarnedKeys.has(key)) return;
8510
+ cleartextWarnedKeys.add(key);
8511
+ process.emitWarning(
8512
+ `Provider "${input.providerId}" is sending credentials to ${input.host} over cleartext http; use https or set enforceHttps to block this`,
8513
+ { code: "ZERO_TRANSFER_CLEARTEXT_CREDENTIALS", type: "SecurityWarning" }
8514
+ );
8515
+ }
8232
8516
  function buildBaseUrl(profile, options) {
8233
8517
  const protocol = options.secure ? "https:" : "http:";
8234
8518
  const portSegment = profile.port !== void 0 ? `:${profile.port}` : "";
@@ -8276,18 +8560,19 @@ async function dispatchRequest(options, url, init) {
8276
8560
  signal: controller.signal
8277
8561
  });
8278
8562
  } catch (error) {
8563
+ const safeUrl = redactUrlForLogging(url);
8279
8564
  if (controller.signal.aborted && upstreamSignal?.aborted !== true) {
8280
8565
  throw new TimeoutError({
8281
8566
  cause: error,
8282
- details: { timeoutMs: options.timeoutMs, url: url.toString() },
8283
- message: `HTTP request to ${url.toString()} timed out after ${String(options.timeoutMs)}ms`,
8567
+ details: { timeoutMs: options.timeoutMs, url: safeUrl },
8568
+ message: `HTTP request to ${safeUrl} timed out after ${String(options.timeoutMs)}ms`,
8284
8569
  retryable: true
8285
8570
  });
8286
8571
  }
8287
8572
  throw new ConnectionError({
8288
8573
  cause: error,
8289
- details: { url: url.toString() },
8290
- message: `HTTP request to ${url.toString()} failed`,
8574
+ details: { url: safeUrl },
8575
+ message: `HTTP request to ${safeUrl} failed`,
8291
8576
  retryable: true
8292
8577
  });
8293
8578
  } finally {
@@ -8319,8 +8604,48 @@ function formatRangeHeader(offset, length) {
8319
8604
  const end = offset + length - 1;
8320
8605
  return `bytes=${String(offset)}-${String(end)}`;
8321
8606
  }
8322
- function mapResponseError(response, path2) {
8323
- const details = { path: path2, status: response.status, statusText: response.statusText };
8607
+ var ERROR_BODY_EXCERPT_LIMIT = 2048;
8608
+ async function readErrorBodyExcerpt(response) {
8609
+ try {
8610
+ const text = await response.text();
8611
+ if (text.length === 0) return void 0;
8612
+ return text.length > ERROR_BODY_EXCERPT_LIMIT ? `${text.slice(0, ERROR_BODY_EXCERPT_LIMIT)}... [truncated]` : text;
8613
+ } catch {
8614
+ return void 0;
8615
+ }
8616
+ }
8617
+ function parseRetryAfterMs(value, now = Date.now) {
8618
+ if (value === null) return void 0;
8619
+ const trimmed = value.trim();
8620
+ if (trimmed.length === 0) return void 0;
8621
+ if (/^\d+$/.test(trimmed)) {
8622
+ const seconds = Number.parseInt(trimmed, 10);
8623
+ return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
8624
+ }
8625
+ if (!/[A-Za-z]/.test(trimmed)) return void 0;
8626
+ const retryAt = Date.parse(trimmed);
8627
+ if (Number.isNaN(retryAt)) return void 0;
8628
+ return Math.max(0, retryAt - now());
8629
+ }
8630
+ async function mapResponseErrorWithBody(response, path2) {
8631
+ return mapResponseError(response, path2, await readErrorBodyExcerpt(response));
8632
+ }
8633
+ function mapResponseError(response, path2, bodyExcerpt) {
8634
+ const details = {
8635
+ path: path2,
8636
+ status: response.status,
8637
+ statusText: response.statusText
8638
+ };
8639
+ if (bodyExcerpt !== void 0) details["body"] = bodyExcerpt;
8640
+ if (response.status === 429 || response.status === 503) {
8641
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
8642
+ if (retryAfterMs !== void 0) details["retryAfterMs"] = retryAfterMs;
8643
+ return new ConnectionError({
8644
+ details,
8645
+ message: response.status === 429 ? `HTTP request for ${path2} was rate limited (429)` : `HTTP service unavailable for ${path2} (503)`,
8646
+ retryable: true
8647
+ });
8648
+ }
8324
8649
  if (response.status === 401) {
8325
8650
  return new AuthenticationError({
8326
8651
  details,
@@ -8352,14 +8677,54 @@ async function* webStreamToAsyncIterable(body) {
8352
8677
  const reader = body.getReader();
8353
8678
  try {
8354
8679
  while (true) {
8355
- const { done, value } = await reader.read();
8356
- if (done) break;
8357
- if (value !== void 0) yield value;
8680
+ let result;
8681
+ try {
8682
+ result = await reader.read();
8683
+ } catch (error) {
8684
+ if (error instanceof ZeroTransferError) throw error;
8685
+ throw new ConnectionError({
8686
+ cause: error,
8687
+ message: "HTTP response stream was interrupted before completion",
8688
+ retryable: true
8689
+ });
8690
+ }
8691
+ if (result.done) break;
8692
+ if (result.value !== void 0) yield result.value;
8358
8693
  }
8359
8694
  } finally {
8360
8695
  reader.releaseLock();
8361
8696
  }
8362
8697
  }
8698
+ function asyncIterableToReadableStream(source, onChunk) {
8699
+ const iterator = source[Symbol.asyncIterator]();
8700
+ return new ReadableStream({
8701
+ async pull(controller) {
8702
+ try {
8703
+ const next = await iterator.next();
8704
+ if (next.done === true) {
8705
+ controller.close();
8706
+ return;
8707
+ }
8708
+ const chunk = next.value;
8709
+ if (chunk.byteLength === 0) {
8710
+ return;
8711
+ }
8712
+ controller.enqueue(chunk);
8713
+ onChunk(chunk);
8714
+ } catch (error) {
8715
+ controller.error(error);
8716
+ }
8717
+ },
8718
+ async cancel(reason) {
8719
+ if (typeof iterator.return === "function") {
8720
+ try {
8721
+ await iterator.return(reason);
8722
+ } catch {
8723
+ }
8724
+ }
8725
+ }
8726
+ });
8727
+ }
8363
8728
  function secretToString2(value) {
8364
8729
  if (typeof value === "string") return value;
8365
8730
  if (value instanceof Uint8Array || import_node_buffer20.Buffer.isBuffer(value)) {
@@ -11958,6 +12323,7 @@ function createHttpProviderFactory(options = {}) {
11958
12323
  const secure = options.secure ?? id === "https";
11959
12324
  const basePath = options.basePath ?? "";
11960
12325
  const fetchImpl = options.fetch ?? globalThis.fetch;
12326
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
11961
12327
  if (typeof fetchImpl !== "function") {
11962
12328
  throw new ConfigurationError({
11963
12329
  message: "Global fetch is unavailable; supply HttpProviderOptions.fetch explicitly",
@@ -12007,6 +12373,13 @@ var HttpProvider = class {
12007
12373
  id;
12008
12374
  capabilities;
12009
12375
  async connect(profile) {
12376
+ if (!this.internals.secure) {
12377
+ warnCleartextCredentials({
12378
+ hasCredentials: profile.username !== void 0 || profile.password !== void 0,
12379
+ host: profile.host,
12380
+ providerId: this.internals.id
12381
+ });
12382
+ }
12010
12383
  const headers = { ...this.internals.defaultHeaders };
12011
12384
  if (profile.username !== void 0) {
12012
12385
  const username = await resolveSecret(profile.username);
@@ -12065,7 +12438,7 @@ var HttpFileSystem = class {
12065
12438
  method: "HEAD"
12066
12439
  });
12067
12440
  if (!response.ok) {
12068
- throw mapResponseError(response, normalized);
12441
+ throw await mapResponseErrorWithBody(response, normalized);
12069
12442
  }
12070
12443
  return responseToStat(response, normalized);
12071
12444
  }
@@ -12090,12 +12463,12 @@ var HttpTransferOperations = class {
12090
12463
  if (request.signal !== void 0) requestInit.signal = request.signal;
12091
12464
  const response = await dispatchRequest(this.options, url, requestInit);
12092
12465
  if (!response.ok && response.status !== 206) {
12093
- throw mapResponseError(response, normalized);
12466
+ throw await mapResponseErrorWithBody(response, normalized);
12094
12467
  }
12095
12468
  const body = response.body;
12096
12469
  if (body === null) {
12097
12470
  throw new ConnectionError({
12098
- message: `HTTP response had no body for ${url.toString()}`,
12471
+ message: `HTTP response had no body for ${redactUrlForLogging(url)}`,
12099
12472
  retryable: true
12100
12473
  });
12101
12474
  }
@@ -12153,7 +12526,8 @@ function createWebDavProviderFactory(options = {}) {
12153
12526
  const secure = options.secure ?? false;
12154
12527
  const basePath = options.basePath ?? "";
12155
12528
  const fetchImpl = options.fetch ?? globalThis.fetch;
12156
- const uploadStreaming = options.uploadStreaming ?? "when-known-size";
12529
+ const uploadStreaming = options.uploadStreaming ?? "always";
12530
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
12157
12531
  if (typeof fetchImpl !== "function") {
12158
12532
  throw new ConfigurationError({
12159
12533
  message: "Global fetch is unavailable; supply WebDavProviderOptions.fetch explicitly",
@@ -12205,6 +12579,13 @@ var WebDavProvider = class {
12205
12579
  id;
12206
12580
  capabilities;
12207
12581
  async connect(profile) {
12582
+ if (!this.internals.secure) {
12583
+ warnCleartextCredentials({
12584
+ hasCredentials: profile.username !== void 0 || profile.password !== void 0,
12585
+ host: profile.host,
12586
+ providerId: this.internals.id
12587
+ });
12588
+ }
12208
12589
  const headers = { ...this.internals.defaultHeaders };
12209
12590
  if (profile.username !== void 0) {
12210
12591
  const username = await resolveSecret(profile.username);
@@ -12259,7 +12640,7 @@ var WebDavFileSystem = class {
12259
12640
  method: "PROPFIND"
12260
12641
  });
12261
12642
  if (!response.ok && response.status !== 207) {
12262
- throw mapResponseError(response, normalized);
12643
+ throw await mapResponseErrorWithBody(response, normalized);
12263
12644
  }
12264
12645
  const body = await response.text();
12265
12646
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12273,7 +12654,7 @@ var WebDavFileSystem = class {
12273
12654
  method: "PROPFIND"
12274
12655
  });
12275
12656
  if (!response.ok && response.status !== 207) {
12276
- throw mapResponseError(response, normalized);
12657
+ throw await mapResponseErrorWithBody(response, normalized);
12277
12658
  }
12278
12659
  const body = await response.text();
12279
12660
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12318,12 +12699,12 @@ var WebDavTransferOperations = class {
12318
12699
  if (request.signal !== void 0) init.signal = request.signal;
12319
12700
  const response = await dispatchRequest(this.options, url, init);
12320
12701
  if (!response.ok && response.status !== 206) {
12321
- throw mapResponseError(response, normalized);
12702
+ throw await mapResponseErrorWithBody(response, normalized);
12322
12703
  }
12323
12704
  const body = response.body;
12324
12705
  if (body === null) {
12325
12706
  throw new ConnectionError({
12326
- message: `WebDAV response had no body for ${url.toString()}`,
12707
+ message: `WebDAV response had no body for ${redactUrlForLogging(url)}`,
12327
12708
  retryable: true
12328
12709
  });
12329
12710
  }
@@ -12367,7 +12748,7 @@ var WebDavTransferOperations = class {
12367
12748
  if (request.signal !== void 0) init2.signal = request.signal;
12368
12749
  const response2 = await dispatchRequest(this.options, url, init2);
12369
12750
  if (!response2.ok) {
12370
- throw mapResponseError(response2, normalized);
12751
+ throw await mapResponseErrorWithBody(response2, normalized);
12371
12752
  }
12372
12753
  request.reportProgress(buffered.byteLength, buffered.byteLength);
12373
12754
  const result2 = {
@@ -12403,7 +12784,7 @@ var WebDavTransferOperations = class {
12403
12784
  if (request.signal !== void 0) init.signal = request.signal;
12404
12785
  const response = await dispatchRequest(this.options, url, init);
12405
12786
  if (!response.ok) {
12406
- throw mapResponseError(response, normalized);
12787
+ throw await mapResponseErrorWithBody(response, normalized);
12407
12788
  }
12408
12789
  const result = {
12409
12790
  bytesTransferred,
@@ -12429,36 +12810,6 @@ async function collectChunks6(source) {
12429
12810
  }
12430
12811
  return out;
12431
12812
  }
12432
- function asyncIterableToReadableStream(source, onChunk) {
12433
- const iterator = source[Symbol.asyncIterator]();
12434
- return new ReadableStream({
12435
- async pull(controller) {
12436
- try {
12437
- const next = await iterator.next();
12438
- if (next.done === true) {
12439
- controller.close();
12440
- return;
12441
- }
12442
- const chunk = next.value;
12443
- if (chunk.byteLength === 0) {
12444
- return;
12445
- }
12446
- controller.enqueue(chunk);
12447
- onChunk(chunk);
12448
- } catch (error) {
12449
- controller.error(error);
12450
- }
12451
- },
12452
- async cancel(reason) {
12453
- if (typeof iterator.return === "function") {
12454
- try {
12455
- await iterator.return(reason);
12456
- } catch {
12457
- }
12458
- }
12459
- }
12460
- });
12461
- }
12462
12813
  function parsePropfindResponses(xml, baseUrl) {
12463
12814
  const entries = [];
12464
12815
  const responseRegex = /<(?:[a-zA-Z0-9-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9-]+:)?response>/gi;
@@ -12545,11 +12896,12 @@ var import_node_path3 = require("path");
12545
12896
 
12546
12897
  // src/providers/web/awsSigv4.ts
12547
12898
  var import_node_crypto12 = require("crypto");
12899
+ var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
12548
12900
  function signSigV4(input) {
12549
12901
  const now = input.now ?? /* @__PURE__ */ new Date();
12550
12902
  const amzDate = formatAmzDate(now);
12551
12903
  const dateStamp = amzDate.slice(0, 8);
12552
- const payloadHash = input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
12904
+ const payloadHash = input.unsignedPayload === true ? UNSIGNED_PAYLOAD : input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
12553
12905
  input.headers["host"] = input.url.host;
12554
12906
  input.headers["x-amz-date"] = amzDate;
12555
12907
  input.headers["x-amz-content-sha256"] = payloadHash;
@@ -12830,7 +13182,7 @@ var S3FileSystem = class {
12830
13182
  url.searchParams.set("delimiter", "/");
12831
13183
  if (prefix.length > 0) url.searchParams.set("prefix", prefix);
12832
13184
  const response = await s3Fetch(this.options, "GET", url);
12833
- if (!response.ok) throw mapResponseError(response, normalized);
13185
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12834
13186
  const body = await response.text();
12835
13187
  return parseListObjectsV2(body, prefix);
12836
13188
  }
@@ -12838,7 +13190,7 @@ var S3FileSystem = class {
12838
13190
  const normalized = normalizeRemotePath(path2);
12839
13191
  const url = buildObjectUrl(this.options, normalized);
12840
13192
  const response = await s3Fetch(this.options, "HEAD", url);
12841
- if (!response.ok) throw mapResponseError(response, normalized);
13193
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12842
13194
  const stat = {
12843
13195
  exists: true,
12844
13196
  name: basenameRemotePath(normalized),
@@ -12878,12 +13230,12 @@ var S3TransferOperations = class {
12878
13230
  extraHeaders: headers
12879
13231
  });
12880
13232
  if (!response.ok && response.status !== 206) {
12881
- throw mapResponseError(response, normalized);
13233
+ throw await mapResponseErrorWithBody(response, normalized);
12882
13234
  }
12883
13235
  const body = response.body;
12884
13236
  if (body === null) {
12885
13237
  throw new ConnectionError({
12886
- message: `S3 response had no body for ${url.toString()}`,
13238
+ message: `S3 response had no body for ${redactUrlForLogging(url)}`,
12887
13239
  retryable: true
12888
13240
  });
12889
13241
  }
@@ -12919,19 +13271,33 @@ var S3TransferOperations = class {
12919
13271
  }
12920
13272
  return this.writeSingleShot(request, normalized);
12921
13273
  }
13274
+ /**
13275
+ * Single PUT upload used when multipart is disabled. Streams the body with
13276
+ * a declared `Content-Length` (signed as `UNSIGNED-PAYLOAD`) when the
13277
+ * caller knows the total size; S3 requires a length up front, so only
13278
+ * unknown-size payloads fall back to buffering the content in memory.
13279
+ */
12922
13280
  async writeSingleShot(request, normalized) {
12923
13281
  const url = buildObjectUrl(this.options, normalized);
12924
- const buffered = await collectChunks7(request.content);
13282
+ const totalBytes = request.totalBytes;
13283
+ if (typeof totalBytes !== "number" || totalBytes < 0) {
13284
+ const buffered = await collectChunks7(request.content);
13285
+ return this.singleShotFromBuffer(request, normalized, buffered);
13286
+ }
13287
+ let bytesTransferred = 0;
13288
+ const stream = asyncIterableToReadableStream(request.content, (chunk) => {
13289
+ bytesTransferred += chunk.byteLength;
13290
+ request.reportProgress(bytesTransferred, totalBytes);
13291
+ });
12925
13292
  const response = await s3Fetch(this.options, "PUT", url, {
12926
13293
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12927
- body: buffered,
12928
- extraHeaders: { "content-type": "application/octet-stream" }
13294
+ extraHeaders: { "content-type": "application/octet-stream" },
13295
+ streamBody: { content: stream, contentLength: totalBytes }
12929
13296
  });
12930
- if (!response.ok) throw mapResponseError(response, normalized);
12931
- request.reportProgress(buffered.byteLength, buffered.byteLength);
13297
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12932
13298
  const result = {
12933
- bytesTransferred: buffered.byteLength,
12934
- totalBytes: buffered.byteLength
13299
+ bytesTransferred,
13300
+ totalBytes
12935
13301
  };
12936
13302
  const etag = response.headers.get("etag");
12937
13303
  if (etag !== null) result.checksum = etag;
@@ -12995,7 +13361,7 @@ var S3TransferOperations = class {
12995
13361
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12996
13362
  extraHeaders: { "content-type": "application/octet-stream" }
12997
13363
  });
12998
- if (!initiateResponse.ok) throw mapResponseError(initiateResponse, normalized);
13364
+ if (!initiateResponse.ok) throw await mapResponseErrorWithBody(initiateResponse, normalized);
12999
13365
  const initiateBody = await initiateResponse.text();
13000
13366
  const initiated = innerText(initiateBody, "UploadId");
13001
13367
  if (initiated === void 0 || initiated === "") {
@@ -13034,7 +13400,7 @@ var S3TransferOperations = class {
13034
13400
  body: partBytes.bytes
13035
13401
  });
13036
13402
  if (!partResponse.ok) {
13037
- throw mapResponseError(partResponse, normalized);
13403
+ throw await mapResponseErrorWithBody(partResponse, normalized);
13038
13404
  }
13039
13405
  const partEtag = partResponse.headers.get("etag");
13040
13406
  if (partEtag === null) {
@@ -13090,7 +13456,7 @@ var S3TransferOperations = class {
13090
13456
  if (resumeStore === void 0) {
13091
13457
  await abortMultipart(this.options, objectUrl, uploadId).catch(() => void 0);
13092
13458
  }
13093
- throw mapResponseError(completeResponse, normalized);
13459
+ throw await mapResponseErrorWithBody(completeResponse, normalized);
13094
13460
  }
13095
13461
  if (resumeStore !== void 0) await resumeStore.clear(resumeKey);
13096
13462
  const completeBody = await completeResponse.text();
@@ -13109,7 +13475,7 @@ var S3TransferOperations = class {
13109
13475
  body: buffered,
13110
13476
  extraHeaders: { "content-type": "application/octet-stream" }
13111
13477
  });
13112
- if (!response.ok) throw mapResponseError(response, normalized);
13478
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
13113
13479
  request.reportProgress(buffered.byteLength, buffered.byteLength);
13114
13480
  const result = {
13115
13481
  bytesTransferred: buffered.byteLength,
@@ -13127,6 +13493,8 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13127
13493
  };
13128
13494
  if (fetchOptions.body !== void 0) {
13129
13495
  headers["content-length"] = String(fetchOptions.body.byteLength);
13496
+ } else if (fetchOptions.streamBody !== void 0) {
13497
+ headers["content-length"] = String(fetchOptions.streamBody.contentLength);
13130
13498
  }
13131
13499
  signSigV4({
13132
13500
  accessKeyId: options.accessKeyId,
@@ -13137,10 +13505,16 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13137
13505
  service: options.service,
13138
13506
  url,
13139
13507
  ...fetchOptions.body !== void 0 ? { body: fetchOptions.body } : {},
13508
+ ...fetchOptions.streamBody !== void 0 ? { unsignedPayload: true } : {},
13140
13509
  ...options.sessionToken !== void 0 ? { sessionToken: options.sessionToken } : {}
13141
13510
  });
13142
13511
  const init = { headers, method };
13143
- if (fetchOptions.body !== void 0) init.body = fetchOptions.body;
13512
+ if (fetchOptions.body !== void 0) {
13513
+ init.body = fetchOptions.body;
13514
+ } else if (fetchOptions.streamBody !== void 0) {
13515
+ init.body = fetchOptions.streamBody.content;
13516
+ init.duplex = "half";
13517
+ }
13144
13518
  if (fetchOptions.signal !== void 0) init.signal = fetchOptions.signal;
13145
13519
  const controller = new AbortController();
13146
13520
  const upstreamSignal = init.signal ?? null;
@@ -13158,10 +13532,11 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13158
13532
  try {
13159
13533
  return await options.fetch(url.toString(), { ...init, signal: controller.signal });
13160
13534
  } catch (error) {
13535
+ const safeUrl = redactUrlForLogging(url);
13161
13536
  throw new ConnectionError({
13162
13537
  cause: error,
13163
- details: { url: url.toString() },
13164
- message: `S3 request to ${url.toString()} failed`,
13538
+ details: { url: safeUrl },
13539
+ message: `S3 request to ${safeUrl} failed`,
13165
13540
  retryable: true
13166
13541
  });
13167
13542
  } finally {
@@ -13328,7 +13703,7 @@ function getBuiltinCapabilityMatrix() {
13328
13703
  label: "FTPS"
13329
13704
  },
13330
13705
  {
13331
- capabilities: createNativeSftpProviderFactory().capabilities,
13706
+ capabilities: createSftpProviderFactory().capabilities,
13332
13707
  id: "sftp",
13333
13708
  label: "SFTP"
13334
13709
  },
@@ -13629,7 +14004,6 @@ function expandAlgorithms(values) {
13629
14004
  }
13630
14005
 
13631
14006
  // src/profiles/importers/FileZillaImporter.ts
13632
- var import_node_buffer25 = require("buffer");
13633
14007
  function importFileZillaSites(xml) {
13634
14008
  const events = tokenizeXml(xml);
13635
14009
  if (events.length === 0) {
@@ -13645,7 +14019,6 @@ function importFileZillaSites(xml) {
13645
14019
  const folderNamePending = [];
13646
14020
  let inServer = false;
13647
14021
  let serverFields = {};
13648
- let serverPasswordEncoding;
13649
14022
  let activeTag;
13650
14023
  let captureFolderName = false;
13651
14024
  for (const event of events) {
@@ -13658,13 +14031,9 @@ function importFileZillaSites(xml) {
13658
14031
  if (event.name === "Server") {
13659
14032
  inServer = true;
13660
14033
  serverFields = {};
13661
- serverPasswordEncoding = void 0;
13662
14034
  continue;
13663
14035
  }
13664
14036
  activeTag = event.name;
13665
- if (event.name === "Pass" && inServer) {
13666
- serverPasswordEncoding = event.attributes["encoding"];
13667
- }
13668
14037
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
13669
14038
  captureFolderName = true;
13670
14039
  }
@@ -13690,7 +14059,7 @@ function importFileZillaSites(xml) {
13690
14059
  }
13691
14060
  if (event.name === "Server") {
13692
14061
  const folder = folderStack.filter((segment) => segment !== "");
13693
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
14062
+ const result = buildSiteFromFields(serverFields);
13694
14063
  if (result.kind === "site") {
13695
14064
  sites.push({ ...result.site, folder });
13696
14065
  } else {
@@ -13702,7 +14071,6 @@ function importFileZillaSites(xml) {
13702
14071
  }
13703
14072
  inServer = false;
13704
14073
  serverFields = {};
13705
- serverPasswordEncoding = void 0;
13706
14074
  activeTag = void 0;
13707
14075
  continue;
13708
14076
  }
@@ -13711,7 +14079,7 @@ function importFileZillaSites(xml) {
13711
14079
  }
13712
14080
  return { sites, skipped };
13713
14081
  }
13714
- function buildSiteFromFields(fields, passwordEncoding) {
14082
+ function buildSiteFromFields(fields) {
13715
14083
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
13716
14084
  const host = (fields["Host"] ?? "").trim();
13717
14085
  if (host === "") return { kind: "skipped", name };
@@ -13730,18 +14098,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
13730
14098
  }
13731
14099
  const user = fields["User"]?.trim();
13732
14100
  if (user !== void 0 && user !== "") profile.username = { value: user };
13733
- let password;
13734
14101
  const rawPass = fields["Pass"];
13735
- if (rawPass !== void 0 && rawPass !== "") {
13736
- if (passwordEncoding === "base64") {
13737
- password = import_node_buffer25.Buffer.from(rawPass, "base64").toString("utf8");
13738
- } else {
13739
- password = rawPass;
13740
- }
13741
- if (password !== void 0 && password !== "") profile.password = { value: password };
13742
- }
13743
- const site = { name, profile };
13744
- if (password !== void 0) site.password = password;
14102
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
14103
+ const site = { hasStoredPassword, name, profile };
13745
14104
  const logonText = fields["Logontype"];
13746
14105
  if (logonText !== void 0) {
13747
14106
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -14062,6 +14421,62 @@ function openTcpSocket(host, port, timeoutMs) {
14062
14421
  });
14063
14422
  }
14064
14423
 
14424
+ // src/transfers/createDefaultRetryPolicy.ts
14425
+ var DEFAULT_MAX_ATTEMPTS = 4;
14426
+ var DEFAULT_BASE_DELAY_MS = 250;
14427
+ var DEFAULT_MAX_DELAY_MS = 3e4;
14428
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
14429
+ function createDefaultRetryPolicy(options = {}) {
14430
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
14431
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
14432
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
14433
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
14434
+ const random = options.random ?? Math.random;
14435
+ return {
14436
+ getDelayMs(input) {
14437
+ const retryAfterMs = readRetryAfterMs(input.error);
14438
+ if (retryAfterMs !== void 0) {
14439
+ return retryAfterMs;
14440
+ }
14441
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
14442
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
14443
+ return Math.floor(random() * cappedMs);
14444
+ },
14445
+ maxAttempts,
14446
+ shouldRetry(input) {
14447
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
14448
+ return false;
14449
+ }
14450
+ if (input.elapsedMs >= maxElapsedMs) {
14451
+ return false;
14452
+ }
14453
+ const retryAfterMs = readRetryAfterMs(input.error);
14454
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
14455
+ return false;
14456
+ }
14457
+ return true;
14458
+ }
14459
+ };
14460
+ }
14461
+ function readRetryAfterMs(error) {
14462
+ if (!(error instanceof ZeroTransferError)) return void 0;
14463
+ const value = error.details?.["retryAfterMs"];
14464
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
14465
+ return Math.floor(value);
14466
+ }
14467
+ function normalizePositiveInteger(value, fallback) {
14468
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
14469
+ return fallback;
14470
+ }
14471
+ return Math.floor(value);
14472
+ }
14473
+ function normalizeNonNegative(value, fallback) {
14474
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
14475
+ return fallback;
14476
+ }
14477
+ return Math.floor(value);
14478
+ }
14479
+
14065
14480
  // src/transfers/TransferPlan.ts
14066
14481
  function createTransferPlan(input) {
14067
14482
  const plan = {
@@ -14159,8 +14574,8 @@ var TransferQueue = class {
14159
14574
  this.concurrency = normalizeConcurrency(options.concurrency);
14160
14575
  this.defaultExecutor = options.executor;
14161
14576
  this.resolveExecutor = options.resolveExecutor;
14162
- this.retry = options.retry;
14163
- this.timeout = options.timeout;
14577
+ this.retry = options.retry ?? options.client?.defaults?.retry;
14578
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
14164
14579
  this.bandwidthLimit = options.bandwidthLimit;
14165
14580
  this.onProgress = options.onProgress;
14166
14581
  this.onReceipt = options.onReceipt;
@@ -15560,6 +15975,7 @@ async function dispatchWebhook(options) {
15560
15975
  return { attempts: attempt, delivered: false, status: lastStatus };
15561
15976
  }
15562
15977
  function createWebhookAuditLog(options) {
15978
+ validateTarget(options.target);
15563
15979
  return {
15564
15980
  list: () => Promise.resolve([]),
15565
15981
  record: async (entry) => {
@@ -15582,6 +15998,24 @@ function validateTarget(target) {
15582
15998
  retryable: false
15583
15999
  });
15584
16000
  }
16001
+ let parsed;
16002
+ try {
16003
+ parsed = new URL(target.url);
16004
+ } catch (error) {
16005
+ throw new ConfigurationError({
16006
+ cause: error,
16007
+ details: { url: target.url },
16008
+ message: "Webhook target url must be an absolute URL",
16009
+ retryable: false
16010
+ });
16011
+ }
16012
+ if (parsed.protocol === "https:") return;
16013
+ if (parsed.protocol === "http:" && target.allowInsecureUrl === true) return;
16014
+ throw new ConfigurationError({
16015
+ details: { protocol: parsed.protocol, url: target.url },
16016
+ message: parsed.protocol === "http:" ? "Webhook target url must use https; set allowInsecureUrl: true to permit cleartext http delivery" : `Webhook target url must use https, got "${parsed.protocol}"`,
16017
+ retryable: false
16018
+ });
15585
16019
  }
15586
16020
  function normalizeRetry(retry) {
15587
16021
  return {
@@ -15618,6 +16052,26 @@ var ApprovalRejectedError = class _ApprovalRejectedError extends ZeroTransferErr
15618
16052
  }
15619
16053
  request;
15620
16054
  };
16055
+ var ApprovalTimeoutError = class _ApprovalTimeoutError extends ZeroTransferError {
16056
+ /**
16057
+ * Creates an approval timeout error.
16058
+ *
16059
+ * @param request - The approval request that timed out while pending.
16060
+ * @param timeoutMs - Configured timeout window in milliseconds.
16061
+ */
16062
+ constructor(request, timeoutMs) {
16063
+ super({
16064
+ code: "approval_timeout",
16065
+ details: { approvalId: request.id, routeId: request.routeId, timeoutMs },
16066
+ message: `Approval "${request.id}" for route "${request.routeId}" timed out after ${String(timeoutMs)}ms`,
16067
+ retryable: false
16068
+ });
16069
+ this.request = request;
16070
+ Object.setPrototypeOf(this, _ApprovalTimeoutError.prototype);
16071
+ this.name = "ApprovalTimeoutError";
16072
+ }
16073
+ request;
16074
+ };
15621
16075
  var ApprovalRegistry = class {
15622
16076
  requests = /* @__PURE__ */ new Map();
15623
16077
  pending = /* @__PURE__ */ new Map();
@@ -15743,9 +16197,27 @@ function createApprovalGate(options) {
15743
16197
  };
15744
16198
  if (input.signal.aborted) onAbort();
15745
16199
  input.signal.addEventListener("abort", onAbort);
16200
+ let timeoutTimer;
16201
+ const timeoutMs = options.timeoutMs;
16202
+ const pendingPromises = [settled];
16203
+ if (timeoutMs !== void 0) {
16204
+ pendingPromises.push(
16205
+ new Promise((_resolve, reject) => {
16206
+ timeoutTimer = setTimeout(() => {
16207
+ const current = options.registry.get(approvalId) ?? request;
16208
+ reject(new ApprovalTimeoutError(current, timeoutMs));
16209
+ if (current.status === "pending") {
16210
+ settled.catch(() => void 0);
16211
+ options.registry.reject(approvalId, { reason: "timeout" }, now());
16212
+ }
16213
+ }, timeoutMs);
16214
+ })
16215
+ );
16216
+ }
15746
16217
  try {
15747
- await settled;
16218
+ await Promise.race(pendingPromises);
15748
16219
  } finally {
16220
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
15749
16221
  input.signal.removeEventListener("abort", onAbort);
15750
16222
  }
15751
16223
  return options.runner(input);
@@ -16118,6 +16590,7 @@ function isMainModule(importMetaUrl) {
16118
16590
  AbortError,
16119
16591
  ApprovalRegistry,
16120
16592
  ApprovalRejectedError,
16593
+ ApprovalTimeoutError,
16121
16594
  AuthenticationError,
16122
16595
  AuthorizationError,
16123
16596
  CLASSIC_PROVIDER_IDS,
@@ -16167,6 +16640,7 @@ function isMainModule(importMetaUrl) {
16167
16640
  createAtomicDeployPlan,
16168
16641
  createAzureBlobProviderFactory,
16169
16642
  createBandwidthThrottle,
16643
+ createDefaultRetryPolicy,
16170
16644
  createDropboxProviderFactory,
16171
16645
  createFileSystemS3MultipartResumeStore,
16172
16646
  createFtpProviderFactory,
@@ -16179,7 +16653,6 @@ function isMainModule(importMetaUrl) {
16179
16653
  createLocalProviderFactory,
16180
16654
  createMemoryProviderFactory,
16181
16655
  createMemoryS3MultipartResumeStore,
16182
- createNativeSftpProviderFactory,
16183
16656
  createOAuthTokenSecretSource,
16184
16657
  createOneDriveProviderFactory,
16185
16658
  createOutboxRoute,
@@ -16237,8 +16710,10 @@ function isMainModule(importMetaUrl) {
16237
16710
  parseUnixListLine,
16238
16711
  redactCommand,
16239
16712
  redactConnectionProfile,
16713
+ redactErrorForLogging,
16240
16714
  redactObject,
16241
16715
  redactSecretSource,
16716
+ redactUrlForLogging,
16242
16717
  redactValue,
16243
16718
  resolveConnectionProfileSecrets,
16244
16719
  resolveOpenSshHost,