@zero-transfer/sdk 0.4.7 → 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,
@@ -151,8 +153,10 @@ __export(index_exports, {
151
153
  parseUnixListLine: () => parseUnixListLine,
152
154
  redactCommand: () => redactCommand,
153
155
  redactConnectionProfile: () => redactConnectionProfile,
156
+ redactErrorForLogging: () => redactErrorForLogging,
154
157
  redactObject: () => redactObject,
155
158
  redactSecretSource: () => redactSecretSource,
159
+ redactUrlForLogging: () => redactUrlForLogging,
156
160
  redactValue: () => redactValue,
157
161
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
158
162
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -178,6 +182,68 @@ module.exports = __toCommonJS(index_exports);
178
182
  // src/client/ZeroTransfer.ts
179
183
  var import_node_events = require("events");
180
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
+
181
247
  // src/errors/ZeroTransferError.ts
182
248
  var ZeroTransferError = class extends Error {
183
249
  /** Stable machine-readable error code. */
@@ -219,6 +285,11 @@ var ZeroTransferError = class extends Error {
219
285
  /**
220
286
  * Serializes the error into a plain object suitable for logs or API responses.
221
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
+ *
222
293
  * @returns A JSON-safe object containing public structured error fields.
223
294
  */
224
295
  toJSON() {
@@ -228,12 +299,12 @@ var ZeroTransferError = class extends Error {
228
299
  message: this.message,
229
300
  protocol: this.protocol,
230
301
  host: this.host,
231
- command: this.command,
302
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
232
303
  ftpCode: this.ftpCode,
233
304
  sftpCode: this.sftpCode,
234
305
  path: this.path,
235
306
  retryable: this.retryable,
236
- details: this.details
307
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
237
308
  };
238
309
  }
239
310
  };
@@ -756,15 +827,20 @@ var ProviderRegistry = class {
756
827
  var TransferClient = class {
757
828
  /** Provider registry used by this client. */
758
829
  registry;
830
+ /** Execution defaults applied when call sites omit their own values. */
831
+ defaults;
759
832
  logger;
760
833
  /**
761
834
  * Creates a transfer client without opening any provider connections.
762
835
  *
763
- * @param options - Optional registry, provider factories, and logger.
836
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
764
837
  */
765
838
  constructor(options = {}) {
766
839
  this.registry = options.registry ?? new ProviderRegistry();
767
840
  this.logger = options.logger ?? noopLogger;
841
+ if (options.defaults !== void 0) {
842
+ this.defaults = { ...options.defaults };
843
+ }
768
844
  for (const provider of options.providers ?? []) {
769
845
  this.registry.register(provider);
770
846
  }
@@ -1337,18 +1413,25 @@ var TransferEngine = class {
1337
1413
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1338
1414
  this.throwIfAborted(abortScope.signal, job);
1339
1415
  const attemptStartedAt = this.now();
1416
+ const attemptScope = createAttemptScope(
1417
+ abortScope.signal,
1418
+ options.timeout,
1419
+ job,
1420
+ attemptNumber
1421
+ );
1340
1422
  const context = this.createExecutionContext(
1341
1423
  job,
1342
1424
  attemptNumber,
1343
1425
  attemptStartedAt,
1344
1426
  options,
1345
- abortScope.signal,
1427
+ attemptScope.signal,
1346
1428
  (bytesTransferred) => {
1347
1429
  latestBytesTransferred = bytesTransferred;
1348
- }
1430
+ },
1431
+ attemptScope.notifyProgress
1349
1432
  );
1350
1433
  try {
1351
- const result = await runExecutor(executor, context, abortScope.signal, job);
1434
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1352
1435
  context.throwIfAborted();
1353
1436
  latestBytesTransferred = result.bytesTransferred;
1354
1437
  const completedAt = this.now();
@@ -1366,16 +1449,27 @@ var TransferEngine = class {
1366
1449
  summarizeError(error)
1367
1450
  );
1368
1451
  attempts.push(attempt);
1369
- if (error instanceof AbortError || error instanceof TimeoutError) {
1452
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1370
1453
  throw error;
1371
1454
  }
1372
- 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
+ };
1373
1461
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1374
1462
  if (shouldRetry) {
1375
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
+ }
1376
1468
  continue;
1377
1469
  }
1378
1470
  throw createTransferFailure(job, error, attempts);
1471
+ } finally {
1472
+ attemptScope.dispose();
1379
1473
  }
1380
1474
  }
1381
1475
  throw createTransferFailure(job, void 0, attempts);
@@ -1383,12 +1477,13 @@ var TransferEngine = class {
1383
1477
  abortScope.dispose();
1384
1478
  }
1385
1479
  }
1386
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1480
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1387
1481
  const context = {
1388
1482
  attempt,
1389
1483
  job,
1390
1484
  reportProgress: (bytesTransferred, totalBytes) => {
1391
1485
  this.throwIfAborted(signal, job);
1486
+ notifyProgress();
1392
1487
  updateBytesTransferred(bytesTransferred);
1393
1488
  const progressInput = {
1394
1489
  bytesTransferred,
@@ -1457,6 +1552,96 @@ function createAbortScope(parentSignal, timeout, job) {
1457
1552
  signal: controller.signal
1458
1553
  };
1459
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
+ }
1460
1645
  function normalizeTimeoutMs(value) {
1461
1646
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1462
1647
  return void 0;
@@ -1625,7 +1810,7 @@ async function runRoute(options) {
1625
1810
  const executor = createProviderTransferExecutor({
1626
1811
  resolveSession: ({ role }) => sessions.get(role)
1627
1812
  });
1628
- return await engine.execute(job, executor, buildExecuteOptions(options));
1813
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1629
1814
  } finally {
1630
1815
  if (destinationSession !== void 0) {
1631
1816
  await destinationSession.disconnect();
@@ -1662,12 +1847,14 @@ function defaultJobId(route, now) {
1662
1847
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1663
1848
  return `route:${route.id}:${timestamp.toString(36)}`;
1664
1849
  }
1665
- function buildExecuteOptions(options) {
1850
+ function buildExecuteOptions(options, client) {
1666
1851
  const execute = {};
1852
+ const retry = options.retry ?? client.defaults?.retry;
1853
+ const timeout = options.timeout ?? client.defaults?.timeout;
1667
1854
  if (options.signal !== void 0) execute.signal = options.signal;
1668
- if (options.retry !== void 0) execute.retry = options.retry;
1855
+ if (retry !== void 0) execute.retry = retry;
1669
1856
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1670
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1857
+ if (timeout !== void 0) execute.timeout = timeout;
1671
1858
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1672
1859
  return execute;
1673
1860
  }
@@ -1724,41 +1911,6 @@ function defaultRouteSuffix(source, destination) {
1724
1911
  return `${source}->${destination}`;
1725
1912
  }
1726
1913
 
1727
- // src/logging/redaction.ts
1728
- var REDACTED = "[REDACTED]";
1729
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1730
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1731
- function isSensitiveKey(key) {
1732
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1733
- }
1734
- function redactCommand(command) {
1735
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1736
- return `${commandName.toUpperCase()} ${REDACTED}`;
1737
- });
1738
- }
1739
- function redactValue(value) {
1740
- if (typeof value === "string") {
1741
- return redactCommand(value);
1742
- }
1743
- if (Array.isArray(value)) {
1744
- return value.map((item) => redactValue(item));
1745
- }
1746
- if (value !== null && typeof value === "object") {
1747
- return redactObject(value);
1748
- }
1749
- return value;
1750
- }
1751
- function redactObject(input) {
1752
- return Object.fromEntries(
1753
- Object.entries(input).map(([key, value]) => {
1754
- if (isSensitiveKey(key)) {
1755
- return [key, REDACTED];
1756
- }
1757
- return [key, redactValue(value)];
1758
- })
1759
- );
1760
- }
1761
-
1762
1914
  // src/profiles/SecretSource.ts
1763
1915
  var import_node_buffer2 = require("buffer");
1764
1916
  var import_promises = require("fs/promises");
@@ -2180,11 +2332,11 @@ async function resolveTlsSecretSource(source, options) {
2180
2332
  }
2181
2333
 
2182
2334
  // src/utils/path.ts
2183
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2335
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2184
2336
  function assertSafeFtpArgument(value, label = "path") {
2185
2337
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2186
2338
  throw new ConfigurationError({
2187
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2339
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2188
2340
  retryable: false,
2189
2341
  details: {
2190
2342
  label
@@ -3146,38 +3298,53 @@ async function expectCompletion(control, command, path2) {
3146
3298
  const response = await control.sendCommand(command);
3147
3299
  assertPathCommandSucceeded(response, command, path2, control.providerId);
3148
3300
  }
3149
- async function readPassiveDataCommand(control, command, path2, options = {}) {
3150
- const dataConnection = await openPassiveDataCommand(control, command, path2, options);
3301
+ async function readPassiveLinesCommand(control, command, path2, onLine) {
3302
+ const dataConnection = await openPassiveDataCommand(control, command, path2);
3151
3303
  try {
3152
- const payload = await collectPassiveData(
3153
- dataConnection,
3154
- control.operationTimeoutMs,
3155
- path2,
3156
- control.providerId
3157
- );
3304
+ const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
3305
+ command,
3306
+ onLine,
3307
+ path: path2,
3308
+ providerId: control.providerId
3309
+ });
3158
3310
  const finalResponse = await control.readFinalResponse({
3159
3311
  command,
3160
3312
  operation: "data command completion",
3161
3313
  path: path2
3162
3314
  });
3163
3315
  assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
3164
- return payload;
3316
+ if (failure !== void 0) throw failure;
3165
3317
  } catch (error) {
3166
3318
  dataConnection.close();
3167
3319
  throw error;
3168
3320
  }
3169
3321
  }
3170
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
+ };
3171
3328
  try {
3172
- const payload2 = await readPassiveDataCommand(control, `MLSD ${path2}`, path2);
3173
- 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;
3174
3335
  } catch (error) {
3175
3336
  if (!isUnsupportedFtpCommandError(error, "MLSD")) {
3176
3337
  throw error;
3177
3338
  }
3178
3339
  }
3179
- const payload = await readPassiveDataCommand(control, `LIST ${path2}`, path2);
3180
- 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;
3181
3348
  }
3182
3349
  async function openPassiveDataCommand(control, command, path2, options = {}) {
3183
3350
  const offset = normalizeOptionalByteCount(options.offset, "offset", path2);
@@ -3350,22 +3517,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
3350
3517
  }
3351
3518
  };
3352
3519
  }
3353
- async function collectPassiveData(dataConnection, timeoutMs, path2, providerId) {
3354
- 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;
3355
3524
  const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
3356
3525
  host: dataConnection.endpoint.host,
3357
3526
  operation: "passive data transfer",
3358
- path: path2,
3359
- providerId
3527
+ path: input.path,
3528
+ providerId: input.providerId
3360
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
+ };
3361
3550
  try {
3362
3551
  for await (const chunk of dataConnection.socket) {
3363
- 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);
3364
3566
  }
3567
+ if (carry.length > 0) emit(carry);
3365
3568
  } finally {
3366
3569
  clearIdleTimeout();
3367
3570
  }
3368
- return import_node_buffer3.Buffer.concat(chunks);
3571
+ return failure;
3369
3572
  }
3370
3573
  async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
3371
3574
  let bytesEmitted = 0;
@@ -3614,6 +3817,13 @@ function createTlsPinnedFingerprints(profile) {
3614
3817
  if (pinnedFingerprint256 === void 0) {
3615
3818
  return void 0;
3616
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
+ }
3617
3827
  const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
3618
3828
  if (fingerprints.length === 0) {
3619
3829
  throw new ConfigurationError({
@@ -5585,6 +5795,7 @@ var import_node_buffer12 = require("buffer");
5585
5795
  var import_node_crypto6 = require("crypto");
5586
5796
  var MIN_PADDING_LENGTH = 4;
5587
5797
  var MIN_PACKET_LENGTH = 1 + MIN_PADDING_LENGTH;
5798
+ var MAX_SSH_PACKET_LENGTH = 256 * 1024;
5588
5799
  function encodeSshTransportPacket(payload, options = {}) {
5589
5800
  const body = import_node_buffer12.Buffer.from(payload);
5590
5801
  const blockSize = normalizeBlockSize(options.blockSize ?? 8);
@@ -5662,6 +5873,14 @@ var SshTransportPacketFramer = class {
5662
5873
  const packets = [];
5663
5874
  while (this.pending.length >= 4) {
5664
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
+ }
5665
5884
  const frameLength = 4 + packetLength;
5666
5885
  if (this.pending.length < frameLength) {
5667
5886
  break;
@@ -6139,14 +6358,35 @@ var SshTransportHandshake = class {
6139
6358
  return { outbound };
6140
6359
  }
6141
6360
  };
6361
+ var MAX_IDENTIFICATION_LINE_BYTES = 8192;
6362
+ var MAX_PRE_IDENTIFICATION_LINES = 1024;
6142
6363
  var SshIdentificationAccumulator = class {
6143
6364
  pending = import_node_buffer14.Buffer.alloc(0);
6365
+ bannerLineCount = 0;
6144
6366
  push(chunk) {
6145
6367
  this.pending = import_node_buffer14.Buffer.concat([this.pending, import_node_buffer14.Buffer.from(chunk)]);
6146
6368
  const bannerLines = [];
6147
6369
  while (true) {
6148
6370
  const lfIndex = this.pending.indexOf(10);
6149
- 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
+ }
6150
6390
  const lineText = trimLineEndings(this.pending.subarray(0, lfIndex + 1).toString("ascii"));
6151
6391
  const remainder = import_node_buffer14.Buffer.from(this.pending.subarray(lfIndex + 1));
6152
6392
  this.pending = remainder;
@@ -6154,6 +6394,15 @@ var SshIdentificationAccumulator = class {
6154
6394
  this.pending = import_node_buffer14.Buffer.alloc(0);
6155
6395
  return { bannerLines, identLine: lineText, remainder };
6156
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
+ }
6157
6406
  bannerLines.push(lineText);
6158
6407
  }
6159
6408
  return { bannerLines, remainder: import_node_buffer14.Buffer.alloc(0) };
@@ -6286,6 +6535,14 @@ var SshTransportPacketUnprotector = class {
6286
6535
  this.framePendingRaw = import_node_buffer15.Buffer.from(this.framePendingRaw.subarray(this.blockLength));
6287
6536
  this.framePartialDecrypted = this.decipher ? import_node_buffer15.Buffer.from(this.decipher.update(firstBlock)) : import_node_buffer15.Buffer.from(firstBlock);
6288
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
+ }
6289
6546
  const remaining = 4 + packetLength - this.blockLength + this.macLength;
6290
6547
  if (remaining < 0) {
6291
6548
  throw new ProtocolError({
@@ -6909,6 +7166,7 @@ function decodeSftpAttributesFromReader(reader) {
6909
7166
 
6910
7167
  // src/protocols/sftp/v3/SftpPacket.ts
6911
7168
  var import_node_buffer17 = require("buffer");
7169
+ var MAX_SFTP_PACKET_LENGTH = 256 * 1024;
6912
7170
  var SFTP_PACKET_TYPE = {
6913
7171
  ATTRS: 105,
6914
7172
  CLOSE: 4,
@@ -6968,6 +7226,13 @@ var SftpPacketFramer = class {
6968
7226
  const packets = [];
6969
7227
  while (this.pending.length >= 4) {
6970
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
+ }
6971
7236
  const frameLength = 4 + bodyLength;
6972
7237
  if (this.pending.length < frameLength) {
6973
7238
  break;
@@ -8228,6 +8493,26 @@ function validateNativeSftpOptions(options) {
8228
8493
 
8229
8494
  // src/providers/web/httpInternals.ts
8230
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
+ }
8231
8516
  function buildBaseUrl(profile, options) {
8232
8517
  const protocol = options.secure ? "https:" : "http:";
8233
8518
  const portSegment = profile.port !== void 0 ? `:${profile.port}` : "";
@@ -8275,18 +8560,19 @@ async function dispatchRequest(options, url, init) {
8275
8560
  signal: controller.signal
8276
8561
  });
8277
8562
  } catch (error) {
8563
+ const safeUrl = redactUrlForLogging(url);
8278
8564
  if (controller.signal.aborted && upstreamSignal?.aborted !== true) {
8279
8565
  throw new TimeoutError({
8280
8566
  cause: error,
8281
- details: { timeoutMs: options.timeoutMs, url: url.toString() },
8282
- 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`,
8283
8569
  retryable: true
8284
8570
  });
8285
8571
  }
8286
8572
  throw new ConnectionError({
8287
8573
  cause: error,
8288
- details: { url: url.toString() },
8289
- message: `HTTP request to ${url.toString()} failed`,
8574
+ details: { url: safeUrl },
8575
+ message: `HTTP request to ${safeUrl} failed`,
8290
8576
  retryable: true
8291
8577
  });
8292
8578
  } finally {
@@ -8318,8 +8604,48 @@ function formatRangeHeader(offset, length) {
8318
8604
  const end = offset + length - 1;
8319
8605
  return `bytes=${String(offset)}-${String(end)}`;
8320
8606
  }
8321
- function mapResponseError(response, path2) {
8322
- 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
+ }
8323
8649
  if (response.status === 401) {
8324
8650
  return new AuthenticationError({
8325
8651
  details,
@@ -8351,14 +8677,54 @@ async function* webStreamToAsyncIterable(body) {
8351
8677
  const reader = body.getReader();
8352
8678
  try {
8353
8679
  while (true) {
8354
- const { done, value } = await reader.read();
8355
- if (done) break;
8356
- 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;
8357
8693
  }
8358
8694
  } finally {
8359
8695
  reader.releaseLock();
8360
8696
  }
8361
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
+ }
8362
8728
  function secretToString2(value) {
8363
8729
  if (typeof value === "string") return value;
8364
8730
  if (value instanceof Uint8Array || import_node_buffer20.Buffer.isBuffer(value)) {
@@ -11957,6 +12323,7 @@ function createHttpProviderFactory(options = {}) {
11957
12323
  const secure = options.secure ?? id === "https";
11958
12324
  const basePath = options.basePath ?? "";
11959
12325
  const fetchImpl = options.fetch ?? globalThis.fetch;
12326
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
11960
12327
  if (typeof fetchImpl !== "function") {
11961
12328
  throw new ConfigurationError({
11962
12329
  message: "Global fetch is unavailable; supply HttpProviderOptions.fetch explicitly",
@@ -12006,6 +12373,13 @@ var HttpProvider = class {
12006
12373
  id;
12007
12374
  capabilities;
12008
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
+ }
12009
12383
  const headers = { ...this.internals.defaultHeaders };
12010
12384
  if (profile.username !== void 0) {
12011
12385
  const username = await resolveSecret(profile.username);
@@ -12064,7 +12438,7 @@ var HttpFileSystem = class {
12064
12438
  method: "HEAD"
12065
12439
  });
12066
12440
  if (!response.ok) {
12067
- throw mapResponseError(response, normalized);
12441
+ throw await mapResponseErrorWithBody(response, normalized);
12068
12442
  }
12069
12443
  return responseToStat(response, normalized);
12070
12444
  }
@@ -12089,12 +12463,12 @@ var HttpTransferOperations = class {
12089
12463
  if (request.signal !== void 0) requestInit.signal = request.signal;
12090
12464
  const response = await dispatchRequest(this.options, url, requestInit);
12091
12465
  if (!response.ok && response.status !== 206) {
12092
- throw mapResponseError(response, normalized);
12466
+ throw await mapResponseErrorWithBody(response, normalized);
12093
12467
  }
12094
12468
  const body = response.body;
12095
12469
  if (body === null) {
12096
12470
  throw new ConnectionError({
12097
- message: `HTTP response had no body for ${url.toString()}`,
12471
+ message: `HTTP response had no body for ${redactUrlForLogging(url)}`,
12098
12472
  retryable: true
12099
12473
  });
12100
12474
  }
@@ -12152,7 +12526,8 @@ function createWebDavProviderFactory(options = {}) {
12152
12526
  const secure = options.secure ?? false;
12153
12527
  const basePath = options.basePath ?? "";
12154
12528
  const fetchImpl = options.fetch ?? globalThis.fetch;
12155
- const uploadStreaming = options.uploadStreaming ?? "when-known-size";
12529
+ const uploadStreaming = options.uploadStreaming ?? "always";
12530
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
12156
12531
  if (typeof fetchImpl !== "function") {
12157
12532
  throw new ConfigurationError({
12158
12533
  message: "Global fetch is unavailable; supply WebDavProviderOptions.fetch explicitly",
@@ -12204,6 +12579,13 @@ var WebDavProvider = class {
12204
12579
  id;
12205
12580
  capabilities;
12206
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
+ }
12207
12589
  const headers = { ...this.internals.defaultHeaders };
12208
12590
  if (profile.username !== void 0) {
12209
12591
  const username = await resolveSecret(profile.username);
@@ -12258,7 +12640,7 @@ var WebDavFileSystem = class {
12258
12640
  method: "PROPFIND"
12259
12641
  });
12260
12642
  if (!response.ok && response.status !== 207) {
12261
- throw mapResponseError(response, normalized);
12643
+ throw await mapResponseErrorWithBody(response, normalized);
12262
12644
  }
12263
12645
  const body = await response.text();
12264
12646
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12272,7 +12654,7 @@ var WebDavFileSystem = class {
12272
12654
  method: "PROPFIND"
12273
12655
  });
12274
12656
  if (!response.ok && response.status !== 207) {
12275
- throw mapResponseError(response, normalized);
12657
+ throw await mapResponseErrorWithBody(response, normalized);
12276
12658
  }
12277
12659
  const body = await response.text();
12278
12660
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12317,12 +12699,12 @@ var WebDavTransferOperations = class {
12317
12699
  if (request.signal !== void 0) init.signal = request.signal;
12318
12700
  const response = await dispatchRequest(this.options, url, init);
12319
12701
  if (!response.ok && response.status !== 206) {
12320
- throw mapResponseError(response, normalized);
12702
+ throw await mapResponseErrorWithBody(response, normalized);
12321
12703
  }
12322
12704
  const body = response.body;
12323
12705
  if (body === null) {
12324
12706
  throw new ConnectionError({
12325
- message: `WebDAV response had no body for ${url.toString()}`,
12707
+ message: `WebDAV response had no body for ${redactUrlForLogging(url)}`,
12326
12708
  retryable: true
12327
12709
  });
12328
12710
  }
@@ -12366,7 +12748,7 @@ var WebDavTransferOperations = class {
12366
12748
  if (request.signal !== void 0) init2.signal = request.signal;
12367
12749
  const response2 = await dispatchRequest(this.options, url, init2);
12368
12750
  if (!response2.ok) {
12369
- throw mapResponseError(response2, normalized);
12751
+ throw await mapResponseErrorWithBody(response2, normalized);
12370
12752
  }
12371
12753
  request.reportProgress(buffered.byteLength, buffered.byteLength);
12372
12754
  const result2 = {
@@ -12402,7 +12784,7 @@ var WebDavTransferOperations = class {
12402
12784
  if (request.signal !== void 0) init.signal = request.signal;
12403
12785
  const response = await dispatchRequest(this.options, url, init);
12404
12786
  if (!response.ok) {
12405
- throw mapResponseError(response, normalized);
12787
+ throw await mapResponseErrorWithBody(response, normalized);
12406
12788
  }
12407
12789
  const result = {
12408
12790
  bytesTransferred,
@@ -12428,36 +12810,6 @@ async function collectChunks6(source) {
12428
12810
  }
12429
12811
  return out;
12430
12812
  }
12431
- function asyncIterableToReadableStream(source, onChunk) {
12432
- const iterator = source[Symbol.asyncIterator]();
12433
- return new ReadableStream({
12434
- async pull(controller) {
12435
- try {
12436
- const next = await iterator.next();
12437
- if (next.done === true) {
12438
- controller.close();
12439
- return;
12440
- }
12441
- const chunk = next.value;
12442
- if (chunk.byteLength === 0) {
12443
- return;
12444
- }
12445
- controller.enqueue(chunk);
12446
- onChunk(chunk);
12447
- } catch (error) {
12448
- controller.error(error);
12449
- }
12450
- },
12451
- async cancel(reason) {
12452
- if (typeof iterator.return === "function") {
12453
- try {
12454
- await iterator.return(reason);
12455
- } catch {
12456
- }
12457
- }
12458
- }
12459
- });
12460
- }
12461
12813
  function parsePropfindResponses(xml, baseUrl) {
12462
12814
  const entries = [];
12463
12815
  const responseRegex = /<(?:[a-zA-Z0-9-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9-]+:)?response>/gi;
@@ -12544,11 +12896,12 @@ var import_node_path3 = require("path");
12544
12896
 
12545
12897
  // src/providers/web/awsSigv4.ts
12546
12898
  var import_node_crypto12 = require("crypto");
12899
+ var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
12547
12900
  function signSigV4(input) {
12548
12901
  const now = input.now ?? /* @__PURE__ */ new Date();
12549
12902
  const amzDate = formatAmzDate(now);
12550
12903
  const dateStamp = amzDate.slice(0, 8);
12551
- 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());
12552
12905
  input.headers["host"] = input.url.host;
12553
12906
  input.headers["x-amz-date"] = amzDate;
12554
12907
  input.headers["x-amz-content-sha256"] = payloadHash;
@@ -12829,7 +13182,7 @@ var S3FileSystem = class {
12829
13182
  url.searchParams.set("delimiter", "/");
12830
13183
  if (prefix.length > 0) url.searchParams.set("prefix", prefix);
12831
13184
  const response = await s3Fetch(this.options, "GET", url);
12832
- if (!response.ok) throw mapResponseError(response, normalized);
13185
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12833
13186
  const body = await response.text();
12834
13187
  return parseListObjectsV2(body, prefix);
12835
13188
  }
@@ -12837,7 +13190,7 @@ var S3FileSystem = class {
12837
13190
  const normalized = normalizeRemotePath(path2);
12838
13191
  const url = buildObjectUrl(this.options, normalized);
12839
13192
  const response = await s3Fetch(this.options, "HEAD", url);
12840
- if (!response.ok) throw mapResponseError(response, normalized);
13193
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12841
13194
  const stat = {
12842
13195
  exists: true,
12843
13196
  name: basenameRemotePath(normalized),
@@ -12877,12 +13230,12 @@ var S3TransferOperations = class {
12877
13230
  extraHeaders: headers
12878
13231
  });
12879
13232
  if (!response.ok && response.status !== 206) {
12880
- throw mapResponseError(response, normalized);
13233
+ throw await mapResponseErrorWithBody(response, normalized);
12881
13234
  }
12882
13235
  const body = response.body;
12883
13236
  if (body === null) {
12884
13237
  throw new ConnectionError({
12885
- message: `S3 response had no body for ${url.toString()}`,
13238
+ message: `S3 response had no body for ${redactUrlForLogging(url)}`,
12886
13239
  retryable: true
12887
13240
  });
12888
13241
  }
@@ -12918,19 +13271,33 @@ var S3TransferOperations = class {
12918
13271
  }
12919
13272
  return this.writeSingleShot(request, normalized);
12920
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
+ */
12921
13280
  async writeSingleShot(request, normalized) {
12922
13281
  const url = buildObjectUrl(this.options, normalized);
12923
- 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
+ });
12924
13292
  const response = await s3Fetch(this.options, "PUT", url, {
12925
13293
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12926
- body: buffered,
12927
- extraHeaders: { "content-type": "application/octet-stream" }
13294
+ extraHeaders: { "content-type": "application/octet-stream" },
13295
+ streamBody: { content: stream, contentLength: totalBytes }
12928
13296
  });
12929
- if (!response.ok) throw mapResponseError(response, normalized);
12930
- request.reportProgress(buffered.byteLength, buffered.byteLength);
13297
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12931
13298
  const result = {
12932
- bytesTransferred: buffered.byteLength,
12933
- totalBytes: buffered.byteLength
13299
+ bytesTransferred,
13300
+ totalBytes
12934
13301
  };
12935
13302
  const etag = response.headers.get("etag");
12936
13303
  if (etag !== null) result.checksum = etag;
@@ -12994,7 +13361,7 @@ var S3TransferOperations = class {
12994
13361
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12995
13362
  extraHeaders: { "content-type": "application/octet-stream" }
12996
13363
  });
12997
- if (!initiateResponse.ok) throw mapResponseError(initiateResponse, normalized);
13364
+ if (!initiateResponse.ok) throw await mapResponseErrorWithBody(initiateResponse, normalized);
12998
13365
  const initiateBody = await initiateResponse.text();
12999
13366
  const initiated = innerText(initiateBody, "UploadId");
13000
13367
  if (initiated === void 0 || initiated === "") {
@@ -13033,7 +13400,7 @@ var S3TransferOperations = class {
13033
13400
  body: partBytes.bytes
13034
13401
  });
13035
13402
  if (!partResponse.ok) {
13036
- throw mapResponseError(partResponse, normalized);
13403
+ throw await mapResponseErrorWithBody(partResponse, normalized);
13037
13404
  }
13038
13405
  const partEtag = partResponse.headers.get("etag");
13039
13406
  if (partEtag === null) {
@@ -13089,7 +13456,7 @@ var S3TransferOperations = class {
13089
13456
  if (resumeStore === void 0) {
13090
13457
  await abortMultipart(this.options, objectUrl, uploadId).catch(() => void 0);
13091
13458
  }
13092
- throw mapResponseError(completeResponse, normalized);
13459
+ throw await mapResponseErrorWithBody(completeResponse, normalized);
13093
13460
  }
13094
13461
  if (resumeStore !== void 0) await resumeStore.clear(resumeKey);
13095
13462
  const completeBody = await completeResponse.text();
@@ -13108,7 +13475,7 @@ var S3TransferOperations = class {
13108
13475
  body: buffered,
13109
13476
  extraHeaders: { "content-type": "application/octet-stream" }
13110
13477
  });
13111
- if (!response.ok) throw mapResponseError(response, normalized);
13478
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
13112
13479
  request.reportProgress(buffered.byteLength, buffered.byteLength);
13113
13480
  const result = {
13114
13481
  bytesTransferred: buffered.byteLength,
@@ -13126,6 +13493,8 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13126
13493
  };
13127
13494
  if (fetchOptions.body !== void 0) {
13128
13495
  headers["content-length"] = String(fetchOptions.body.byteLength);
13496
+ } else if (fetchOptions.streamBody !== void 0) {
13497
+ headers["content-length"] = String(fetchOptions.streamBody.contentLength);
13129
13498
  }
13130
13499
  signSigV4({
13131
13500
  accessKeyId: options.accessKeyId,
@@ -13136,10 +13505,16 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13136
13505
  service: options.service,
13137
13506
  url,
13138
13507
  ...fetchOptions.body !== void 0 ? { body: fetchOptions.body } : {},
13508
+ ...fetchOptions.streamBody !== void 0 ? { unsignedPayload: true } : {},
13139
13509
  ...options.sessionToken !== void 0 ? { sessionToken: options.sessionToken } : {}
13140
13510
  });
13141
13511
  const init = { headers, method };
13142
- 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
+ }
13143
13518
  if (fetchOptions.signal !== void 0) init.signal = fetchOptions.signal;
13144
13519
  const controller = new AbortController();
13145
13520
  const upstreamSignal = init.signal ?? null;
@@ -13157,10 +13532,11 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13157
13532
  try {
13158
13533
  return await options.fetch(url.toString(), { ...init, signal: controller.signal });
13159
13534
  } catch (error) {
13535
+ const safeUrl = redactUrlForLogging(url);
13160
13536
  throw new ConnectionError({
13161
13537
  cause: error,
13162
- details: { url: url.toString() },
13163
- message: `S3 request to ${url.toString()} failed`,
13538
+ details: { url: safeUrl },
13539
+ message: `S3 request to ${safeUrl} failed`,
13164
13540
  retryable: true
13165
13541
  });
13166
13542
  } finally {
@@ -13628,7 +14004,6 @@ function expandAlgorithms(values) {
13628
14004
  }
13629
14005
 
13630
14006
  // src/profiles/importers/FileZillaImporter.ts
13631
- var import_node_buffer25 = require("buffer");
13632
14007
  function importFileZillaSites(xml) {
13633
14008
  const events = tokenizeXml(xml);
13634
14009
  if (events.length === 0) {
@@ -13644,7 +14019,6 @@ function importFileZillaSites(xml) {
13644
14019
  const folderNamePending = [];
13645
14020
  let inServer = false;
13646
14021
  let serverFields = {};
13647
- let serverPasswordEncoding;
13648
14022
  let activeTag;
13649
14023
  let captureFolderName = false;
13650
14024
  for (const event of events) {
@@ -13657,13 +14031,9 @@ function importFileZillaSites(xml) {
13657
14031
  if (event.name === "Server") {
13658
14032
  inServer = true;
13659
14033
  serverFields = {};
13660
- serverPasswordEncoding = void 0;
13661
14034
  continue;
13662
14035
  }
13663
14036
  activeTag = event.name;
13664
- if (event.name === "Pass" && inServer) {
13665
- serverPasswordEncoding = event.attributes["encoding"];
13666
- }
13667
14037
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
13668
14038
  captureFolderName = true;
13669
14039
  }
@@ -13689,7 +14059,7 @@ function importFileZillaSites(xml) {
13689
14059
  }
13690
14060
  if (event.name === "Server") {
13691
14061
  const folder = folderStack.filter((segment) => segment !== "");
13692
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
14062
+ const result = buildSiteFromFields(serverFields);
13693
14063
  if (result.kind === "site") {
13694
14064
  sites.push({ ...result.site, folder });
13695
14065
  } else {
@@ -13701,7 +14071,6 @@ function importFileZillaSites(xml) {
13701
14071
  }
13702
14072
  inServer = false;
13703
14073
  serverFields = {};
13704
- serverPasswordEncoding = void 0;
13705
14074
  activeTag = void 0;
13706
14075
  continue;
13707
14076
  }
@@ -13710,7 +14079,7 @@ function importFileZillaSites(xml) {
13710
14079
  }
13711
14080
  return { sites, skipped };
13712
14081
  }
13713
- function buildSiteFromFields(fields, passwordEncoding) {
14082
+ function buildSiteFromFields(fields) {
13714
14083
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
13715
14084
  const host = (fields["Host"] ?? "").trim();
13716
14085
  if (host === "") return { kind: "skipped", name };
@@ -13729,18 +14098,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
13729
14098
  }
13730
14099
  const user = fields["User"]?.trim();
13731
14100
  if (user !== void 0 && user !== "") profile.username = { value: user };
13732
- let password;
13733
14101
  const rawPass = fields["Pass"];
13734
- if (rawPass !== void 0 && rawPass !== "") {
13735
- if (passwordEncoding === "base64") {
13736
- password = import_node_buffer25.Buffer.from(rawPass, "base64").toString("utf8");
13737
- } else {
13738
- password = rawPass;
13739
- }
13740
- if (password !== void 0 && password !== "") profile.password = { value: password };
13741
- }
13742
- const site = { name, profile };
13743
- if (password !== void 0) site.password = password;
14102
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
14103
+ const site = { hasStoredPassword, name, profile };
13744
14104
  const logonText = fields["Logontype"];
13745
14105
  if (logonText !== void 0) {
13746
14106
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -14061,6 +14421,62 @@ function openTcpSocket(host, port, timeoutMs) {
14061
14421
  });
14062
14422
  }
14063
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
+
14064
14480
  // src/transfers/TransferPlan.ts
14065
14481
  function createTransferPlan(input) {
14066
14482
  const plan = {
@@ -14158,8 +14574,8 @@ var TransferQueue = class {
14158
14574
  this.concurrency = normalizeConcurrency(options.concurrency);
14159
14575
  this.defaultExecutor = options.executor;
14160
14576
  this.resolveExecutor = options.resolveExecutor;
14161
- this.retry = options.retry;
14162
- this.timeout = options.timeout;
14577
+ this.retry = options.retry ?? options.client?.defaults?.retry;
14578
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
14163
14579
  this.bandwidthLimit = options.bandwidthLimit;
14164
14580
  this.onProgress = options.onProgress;
14165
14581
  this.onReceipt = options.onReceipt;
@@ -15559,6 +15975,7 @@ async function dispatchWebhook(options) {
15559
15975
  return { attempts: attempt, delivered: false, status: lastStatus };
15560
15976
  }
15561
15977
  function createWebhookAuditLog(options) {
15978
+ validateTarget(options.target);
15562
15979
  return {
15563
15980
  list: () => Promise.resolve([]),
15564
15981
  record: async (entry) => {
@@ -15581,6 +15998,24 @@ function validateTarget(target) {
15581
15998
  retryable: false
15582
15999
  });
15583
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
+ });
15584
16019
  }
15585
16020
  function normalizeRetry(retry) {
15586
16021
  return {
@@ -15617,6 +16052,26 @@ var ApprovalRejectedError = class _ApprovalRejectedError extends ZeroTransferErr
15617
16052
  }
15618
16053
  request;
15619
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
+ };
15620
16075
  var ApprovalRegistry = class {
15621
16076
  requests = /* @__PURE__ */ new Map();
15622
16077
  pending = /* @__PURE__ */ new Map();
@@ -15742,9 +16197,27 @@ function createApprovalGate(options) {
15742
16197
  };
15743
16198
  if (input.signal.aborted) onAbort();
15744
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
+ }
15745
16217
  try {
15746
- await settled;
16218
+ await Promise.race(pendingPromises);
15747
16219
  } finally {
16220
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
15748
16221
  input.signal.removeEventListener("abort", onAbort);
15749
16222
  }
15750
16223
  return options.runner(input);
@@ -16117,6 +16590,7 @@ function isMainModule(importMetaUrl) {
16117
16590
  AbortError,
16118
16591
  ApprovalRegistry,
16119
16592
  ApprovalRejectedError,
16593
+ ApprovalTimeoutError,
16120
16594
  AuthenticationError,
16121
16595
  AuthorizationError,
16122
16596
  CLASSIC_PROVIDER_IDS,
@@ -16166,6 +16640,7 @@ function isMainModule(importMetaUrl) {
16166
16640
  createAtomicDeployPlan,
16167
16641
  createAzureBlobProviderFactory,
16168
16642
  createBandwidthThrottle,
16643
+ createDefaultRetryPolicy,
16169
16644
  createDropboxProviderFactory,
16170
16645
  createFileSystemS3MultipartResumeStore,
16171
16646
  createFtpProviderFactory,
@@ -16235,8 +16710,10 @@ function isMainModule(importMetaUrl) {
16235
16710
  parseUnixListLine,
16236
16711
  redactCommand,
16237
16712
  redactConnectionProfile,
16713
+ redactErrorForLogging,
16238
16714
  redactObject,
16239
16715
  redactSecretSource,
16716
+ redactUrlForLogging,
16240
16717
  redactValue,
16241
16718
  resolveConnectionProfileSecrets,
16242
16719
  resolveOpenSshHost,