@zero-transfer/ftps 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
@@ -60,6 +60,7 @@ __export(ftps_exports, {
60
60
  copyBetween: () => copyBetween,
61
61
  createAtomicDeployPlan: () => createAtomicDeployPlan,
62
62
  createBandwidthThrottle: () => createBandwidthThrottle,
63
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
63
64
  createFtpsProviderFactory: () => createFtpsProviderFactory,
64
65
  createLocalProviderFactory: () => createLocalProviderFactory,
65
66
  createMemoryProviderFactory: () => createMemoryProviderFactory,
@@ -96,8 +97,10 @@ __export(ftps_exports, {
96
97
  parseRemoteManifest: () => parseRemoteManifest,
97
98
  redactCommand: () => redactCommand,
98
99
  redactConnectionProfile: () => redactConnectionProfile,
100
+ redactErrorForLogging: () => redactErrorForLogging,
99
101
  redactObject: () => redactObject,
100
102
  redactSecretSource: () => redactSecretSource,
103
+ redactUrlForLogging: () => redactUrlForLogging,
101
104
  redactValue: () => redactValue,
102
105
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
103
106
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -118,6 +121,68 @@ module.exports = __toCommonJS(ftps_exports);
118
121
  // src/client/ZeroTransfer.ts
119
122
  var import_node_events = require("events");
120
123
 
124
+ // src/logging/redaction.ts
125
+ var REDACTED = "[REDACTED]";
126
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
127
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
128
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
129
+ function isSensitiveKey(key) {
130
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
131
+ }
132
+ function redactCommand(command) {
133
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
134
+ return `${commandName.toUpperCase()} ${REDACTED}`;
135
+ });
136
+ }
137
+ function redactValue(value) {
138
+ if (typeof value === "string") {
139
+ return redactCommand(value);
140
+ }
141
+ if (Array.isArray(value)) {
142
+ return value.map((item) => redactValue(item));
143
+ }
144
+ if (value !== null && typeof value === "object") {
145
+ return redactObject(value);
146
+ }
147
+ return value;
148
+ }
149
+ function redactObject(input) {
150
+ return Object.fromEntries(
151
+ Object.entries(input).map(([key, value]) => {
152
+ if (isSensitiveKey(key)) {
153
+ return [key, REDACTED];
154
+ }
155
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
156
+ return [key, redactUrlForLogging(value)];
157
+ }
158
+ return [key, redactValue(value)];
159
+ })
160
+ );
161
+ }
162
+ function redactUrlForLogging(url) {
163
+ let parsed;
164
+ try {
165
+ parsed = typeof url === "string" ? new URL(url) : url;
166
+ } catch {
167
+ return REDACTED;
168
+ }
169
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
170
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
171
+ return `${origin}${parsed.pathname}${query}`;
172
+ }
173
+ function redactErrorForLogging(error) {
174
+ if (error !== null && typeof error === "object") {
175
+ const candidate = error;
176
+ if (typeof candidate.toJSON === "function") {
177
+ return redactObject(candidate.toJSON());
178
+ }
179
+ }
180
+ if (error instanceof Error) {
181
+ return redactObject({ message: error.message, name: error.name });
182
+ }
183
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
184
+ }
185
+
121
186
  // src/errors/ZeroTransferError.ts
122
187
  var ZeroTransferError = class extends Error {
123
188
  /** Stable machine-readable error code. */
@@ -159,6 +224,11 @@ var ZeroTransferError = class extends Error {
159
224
  /**
160
225
  * Serializes the error into a plain object suitable for logs or API responses.
161
226
  *
227
+ * `details` and `command` are passed through secret redaction so serialized
228
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
229
+ * live {@link ZeroTransferError.details | details} property stays unredacted
230
+ * for programmatic consumers.
231
+ *
162
232
  * @returns A JSON-safe object containing public structured error fields.
163
233
  */
164
234
  toJSON() {
@@ -168,12 +238,12 @@ var ZeroTransferError = class extends Error {
168
238
  message: this.message,
169
239
  protocol: this.protocol,
170
240
  host: this.host,
171
- command: this.command,
241
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
172
242
  ftpCode: this.ftpCode,
173
243
  sftpCode: this.sftpCode,
174
244
  path: this.path,
175
245
  retryable: this.retryable,
176
- details: this.details
246
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
177
247
  };
178
248
  }
179
249
  };
@@ -696,15 +766,20 @@ var ProviderRegistry = class {
696
766
  var TransferClient = class {
697
767
  /** Provider registry used by this client. */
698
768
  registry;
769
+ /** Execution defaults applied when call sites omit their own values. */
770
+ defaults;
699
771
  logger;
700
772
  /**
701
773
  * Creates a transfer client without opening any provider connections.
702
774
  *
703
- * @param options - Optional registry, provider factories, and logger.
775
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
704
776
  */
705
777
  constructor(options = {}) {
706
778
  this.registry = options.registry ?? new ProviderRegistry();
707
779
  this.logger = options.logger ?? noopLogger;
780
+ if (options.defaults !== void 0) {
781
+ this.defaults = { ...options.defaults };
782
+ }
708
783
  for (const provider of options.providers ?? []) {
709
784
  this.registry.register(provider);
710
785
  }
@@ -1277,18 +1352,25 @@ var TransferEngine = class {
1277
1352
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1278
1353
  this.throwIfAborted(abortScope.signal, job);
1279
1354
  const attemptStartedAt = this.now();
1355
+ const attemptScope = createAttemptScope(
1356
+ abortScope.signal,
1357
+ options.timeout,
1358
+ job,
1359
+ attemptNumber
1360
+ );
1280
1361
  const context = this.createExecutionContext(
1281
1362
  job,
1282
1363
  attemptNumber,
1283
1364
  attemptStartedAt,
1284
1365
  options,
1285
- abortScope.signal,
1366
+ attemptScope.signal,
1286
1367
  (bytesTransferred) => {
1287
1368
  latestBytesTransferred = bytesTransferred;
1288
- }
1369
+ },
1370
+ attemptScope.notifyProgress
1289
1371
  );
1290
1372
  try {
1291
- const result = await runExecutor(executor, context, abortScope.signal, job);
1373
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1292
1374
  context.throwIfAborted();
1293
1375
  latestBytesTransferred = result.bytesTransferred;
1294
1376
  const completedAt = this.now();
@@ -1306,16 +1388,27 @@ var TransferEngine = class {
1306
1388
  summarizeError(error)
1307
1389
  );
1308
1390
  attempts.push(attempt);
1309
- if (error instanceof AbortError || error instanceof TimeoutError) {
1391
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1310
1392
  throw error;
1311
1393
  }
1312
- const retryInput = { attempt: attemptNumber, error, job };
1394
+ const retryInput = {
1395
+ attempt: attemptNumber,
1396
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1397
+ error,
1398
+ job
1399
+ };
1313
1400
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1314
1401
  if (shouldRetry) {
1315
1402
  options.retry?.onRetry?.(retryInput);
1403
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1404
+ if (delayMs > 0) {
1405
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1406
+ }
1316
1407
  continue;
1317
1408
  }
1318
1409
  throw createTransferFailure(job, error, attempts);
1410
+ } finally {
1411
+ attemptScope.dispose();
1319
1412
  }
1320
1413
  }
1321
1414
  throw createTransferFailure(job, void 0, attempts);
@@ -1323,12 +1416,13 @@ var TransferEngine = class {
1323
1416
  abortScope.dispose();
1324
1417
  }
1325
1418
  }
1326
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1419
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1327
1420
  const context = {
1328
1421
  attempt,
1329
1422
  job,
1330
1423
  reportProgress: (bytesTransferred, totalBytes) => {
1331
1424
  this.throwIfAborted(signal, job);
1425
+ notifyProgress();
1332
1426
  updateBytesTransferred(bytesTransferred);
1333
1427
  const progressInput = {
1334
1428
  bytesTransferred,
@@ -1397,6 +1491,96 @@ function createAbortScope(parentSignal, timeout, job) {
1397
1491
  signal: controller.signal
1398
1492
  };
1399
1493
  }
1494
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1495
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1496
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1497
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1498
+ const scope = {
1499
+ dispose: () => void 0,
1500
+ notifyProgress: () => void 0
1501
+ };
1502
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1503
+ return scope;
1504
+ }
1505
+ const controller = new AbortController();
1506
+ const retryable = timeout?.retryable ?? true;
1507
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1508
+ if (parentSignal?.aborted === true) {
1509
+ abortFromParent();
1510
+ } else {
1511
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1512
+ }
1513
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1514
+ controller.abort(
1515
+ new TimeoutError({
1516
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1517
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1518
+ retryable
1519
+ })
1520
+ );
1521
+ }, attemptTimeoutMs);
1522
+ let stallTimer;
1523
+ const armStallWatchdog = () => {
1524
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1525
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1526
+ stallTimer = setTimeout(() => {
1527
+ controller.abort(
1528
+ new TimeoutError({
1529
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1530
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1531
+ retryable
1532
+ })
1533
+ );
1534
+ }, stallTimeoutMs);
1535
+ };
1536
+ armStallWatchdog();
1537
+ return {
1538
+ dispose: () => {
1539
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1540
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1541
+ parentSignal?.removeEventListener("abort", abortFromParent);
1542
+ },
1543
+ notifyProgress: armStallWatchdog,
1544
+ signal: controller.signal
1545
+ };
1546
+ }
1547
+ function sleepWithAbort(delayMs, signal, job) {
1548
+ return new Promise((resolve, reject) => {
1549
+ if (signal === void 0) {
1550
+ setTimeout(resolve, delayMs);
1551
+ return;
1552
+ }
1553
+ if (signal.aborted) {
1554
+ reject(toAbortFailure(signal, job));
1555
+ return;
1556
+ }
1557
+ const rejectAbort = () => {
1558
+ clearTimeout(timer);
1559
+ reject(toAbortFailure(signal, job));
1560
+ };
1561
+ const timer = setTimeout(() => {
1562
+ signal.removeEventListener("abort", rejectAbort);
1563
+ resolve();
1564
+ }, delayMs);
1565
+ signal.addEventListener("abort", rejectAbort, { once: true });
1566
+ });
1567
+ }
1568
+ function toAbortFailure(signal, job) {
1569
+ if (signal.reason instanceof ZeroTransferError) {
1570
+ return signal.reason;
1571
+ }
1572
+ return new AbortError({
1573
+ details: { jobId: job.id, operation: job.operation },
1574
+ message: `Transfer job aborted: ${job.id}`,
1575
+ retryable: false
1576
+ });
1577
+ }
1578
+ function normalizeDelayMs(value) {
1579
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1580
+ return 0;
1581
+ }
1582
+ return Math.floor(value);
1583
+ }
1400
1584
  function normalizeTimeoutMs(value) {
1401
1585
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1402
1586
  return void 0;
@@ -1565,7 +1749,7 @@ async function runRoute(options) {
1565
1749
  const executor = createProviderTransferExecutor({
1566
1750
  resolveSession: ({ role }) => sessions.get(role)
1567
1751
  });
1568
- return await engine.execute(job, executor, buildExecuteOptions(options));
1752
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1569
1753
  } finally {
1570
1754
  if (destinationSession !== void 0) {
1571
1755
  await destinationSession.disconnect();
@@ -1602,12 +1786,14 @@ function defaultJobId(route, now) {
1602
1786
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1603
1787
  return `route:${route.id}:${timestamp.toString(36)}`;
1604
1788
  }
1605
- function buildExecuteOptions(options) {
1789
+ function buildExecuteOptions(options, client) {
1606
1790
  const execute = {};
1791
+ const retry = options.retry ?? client.defaults?.retry;
1792
+ const timeout = options.timeout ?? client.defaults?.timeout;
1607
1793
  if (options.signal !== void 0) execute.signal = options.signal;
1608
- if (options.retry !== void 0) execute.retry = options.retry;
1794
+ if (retry !== void 0) execute.retry = retry;
1609
1795
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1610
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1796
+ if (timeout !== void 0) execute.timeout = timeout;
1611
1797
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1612
1798
  return execute;
1613
1799
  }
@@ -1664,41 +1850,6 @@ function defaultRouteSuffix(source, destination) {
1664
1850
  return `${source}->${destination}`;
1665
1851
  }
1666
1852
 
1667
- // src/logging/redaction.ts
1668
- var REDACTED = "[REDACTED]";
1669
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1670
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1671
- function isSensitiveKey(key) {
1672
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1673
- }
1674
- function redactCommand(command) {
1675
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1676
- return `${commandName.toUpperCase()} ${REDACTED}`;
1677
- });
1678
- }
1679
- function redactValue(value) {
1680
- if (typeof value === "string") {
1681
- return redactCommand(value);
1682
- }
1683
- if (Array.isArray(value)) {
1684
- return value.map((item) => redactValue(item));
1685
- }
1686
- if (value !== null && typeof value === "object") {
1687
- return redactObject(value);
1688
- }
1689
- return value;
1690
- }
1691
- function redactObject(input) {
1692
- return Object.fromEntries(
1693
- Object.entries(input).map(([key, value]) => {
1694
- if (isSensitiveKey(key)) {
1695
- return [key, REDACTED];
1696
- }
1697
- return [key, redactValue(value)];
1698
- })
1699
- );
1700
- }
1701
-
1702
1853
  // src/profiles/SecretSource.ts
1703
1854
  var import_node_buffer2 = require("buffer");
1704
1855
  var import_promises = require("fs/promises");
@@ -2070,11 +2221,11 @@ var import_promises2 = require("fs/promises");
2070
2221
  var import_node_path2 = __toESM(require("path"));
2071
2222
 
2072
2223
  // src/utils/path.ts
2073
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2224
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2074
2225
  function assertSafeFtpArgument(value, label = "path") {
2075
2226
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2076
2227
  throw new ConfigurationError({
2077
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2228
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2078
2229
  retryable: false,
2079
2230
  details: {
2080
2231
  label
@@ -3376,7 +3527,6 @@ function expandAlgorithms(values) {
3376
3527
  }
3377
3528
 
3378
3529
  // src/profiles/importers/FileZillaImporter.ts
3379
- var import_node_buffer5 = require("buffer");
3380
3530
  function importFileZillaSites(xml) {
3381
3531
  const events = tokenizeXml(xml);
3382
3532
  if (events.length === 0) {
@@ -3392,7 +3542,6 @@ function importFileZillaSites(xml) {
3392
3542
  const folderNamePending = [];
3393
3543
  let inServer = false;
3394
3544
  let serverFields = {};
3395
- let serverPasswordEncoding;
3396
3545
  let activeTag;
3397
3546
  let captureFolderName = false;
3398
3547
  for (const event of events) {
@@ -3405,13 +3554,9 @@ function importFileZillaSites(xml) {
3405
3554
  if (event.name === "Server") {
3406
3555
  inServer = true;
3407
3556
  serverFields = {};
3408
- serverPasswordEncoding = void 0;
3409
3557
  continue;
3410
3558
  }
3411
3559
  activeTag = event.name;
3412
- if (event.name === "Pass" && inServer) {
3413
- serverPasswordEncoding = event.attributes["encoding"];
3414
- }
3415
3560
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
3416
3561
  captureFolderName = true;
3417
3562
  }
@@ -3437,7 +3582,7 @@ function importFileZillaSites(xml) {
3437
3582
  }
3438
3583
  if (event.name === "Server") {
3439
3584
  const folder = folderStack.filter((segment) => segment !== "");
3440
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
3585
+ const result = buildSiteFromFields(serverFields);
3441
3586
  if (result.kind === "site") {
3442
3587
  sites.push({ ...result.site, folder });
3443
3588
  } else {
@@ -3449,7 +3594,6 @@ function importFileZillaSites(xml) {
3449
3594
  }
3450
3595
  inServer = false;
3451
3596
  serverFields = {};
3452
- serverPasswordEncoding = void 0;
3453
3597
  activeTag = void 0;
3454
3598
  continue;
3455
3599
  }
@@ -3458,7 +3602,7 @@ function importFileZillaSites(xml) {
3458
3602
  }
3459
3603
  return { sites, skipped };
3460
3604
  }
3461
- function buildSiteFromFields(fields, passwordEncoding) {
3605
+ function buildSiteFromFields(fields) {
3462
3606
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
3463
3607
  const host = (fields["Host"] ?? "").trim();
3464
3608
  if (host === "") return { kind: "skipped", name };
@@ -3477,18 +3621,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
3477
3621
  }
3478
3622
  const user = fields["User"]?.trim();
3479
3623
  if (user !== void 0 && user !== "") profile.username = { value: user };
3480
- let password;
3481
3624
  const rawPass = fields["Pass"];
3482
- if (rawPass !== void 0 && rawPass !== "") {
3483
- if (passwordEncoding === "base64") {
3484
- password = import_node_buffer5.Buffer.from(rawPass, "base64").toString("utf8");
3485
- } else {
3486
- password = rawPass;
3487
- }
3488
- if (password !== void 0 && password !== "") profile.password = { value: password };
3489
- }
3490
- const site = { name, profile };
3491
- if (password !== void 0) site.password = password;
3625
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
3626
+ const site = { hasStoredPassword, name, profile };
3492
3627
  const logonText = fields["Logontype"];
3493
3628
  if (logonText !== void 0) {
3494
3629
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -3731,6 +3866,62 @@ function mapFtp550(details) {
3731
3866
  return new PermissionDeniedError(details);
3732
3867
  }
3733
3868
 
3869
+ // src/transfers/createDefaultRetryPolicy.ts
3870
+ var DEFAULT_MAX_ATTEMPTS = 4;
3871
+ var DEFAULT_BASE_DELAY_MS = 250;
3872
+ var DEFAULT_MAX_DELAY_MS = 3e4;
3873
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
3874
+ function createDefaultRetryPolicy(options = {}) {
3875
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
3876
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
3877
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
3878
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
3879
+ const random = options.random ?? Math.random;
3880
+ return {
3881
+ getDelayMs(input) {
3882
+ const retryAfterMs = readRetryAfterMs(input.error);
3883
+ if (retryAfterMs !== void 0) {
3884
+ return retryAfterMs;
3885
+ }
3886
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
3887
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
3888
+ return Math.floor(random() * cappedMs);
3889
+ },
3890
+ maxAttempts,
3891
+ shouldRetry(input) {
3892
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
3893
+ return false;
3894
+ }
3895
+ if (input.elapsedMs >= maxElapsedMs) {
3896
+ return false;
3897
+ }
3898
+ const retryAfterMs = readRetryAfterMs(input.error);
3899
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
3900
+ return false;
3901
+ }
3902
+ return true;
3903
+ }
3904
+ };
3905
+ }
3906
+ function readRetryAfterMs(error) {
3907
+ if (!(error instanceof ZeroTransferError)) return void 0;
3908
+ const value = error.details?.["retryAfterMs"];
3909
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
3910
+ return Math.floor(value);
3911
+ }
3912
+ function normalizePositiveInteger(value, fallback) {
3913
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
3914
+ return fallback;
3915
+ }
3916
+ return Math.floor(value);
3917
+ }
3918
+ function normalizeNonNegative(value, fallback) {
3919
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
3920
+ return fallback;
3921
+ }
3922
+ return Math.floor(value);
3923
+ }
3924
+
3734
3925
  // src/transfers/TransferPlan.ts
3735
3926
  function createTransferPlan(input) {
3736
3927
  const plan = {
@@ -3828,8 +4019,8 @@ var TransferQueue = class {
3828
4019
  this.concurrency = normalizeConcurrency(options.concurrency);
3829
4020
  this.defaultExecutor = options.executor;
3830
4021
  this.resolveExecutor = options.resolveExecutor;
3831
- this.retry = options.retry;
3832
- this.timeout = options.timeout;
4022
+ this.retry = options.retry ?? options.client?.defaults?.retry;
4023
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
3833
4024
  this.bandwidthLimit = options.bandwidthLimit;
3834
4025
  this.onProgress = options.onProgress;
3835
4026
  this.onReceipt = options.onReceipt;
@@ -4906,7 +5097,7 @@ function isMainModule(importMetaUrl) {
4906
5097
  }
4907
5098
 
4908
5099
  // src/providers/classic/ftp/FtpProvider.ts
4909
- var import_node_buffer6 = require("buffer");
5100
+ var import_node_buffer5 = require("buffer");
4910
5101
  var import_node_net = require("net");
4911
5102
  var import_node_tls = require("tls");
4912
5103
 
@@ -4916,12 +5107,6 @@ var UNIX_LIST_MONTHS = new Map(
4916
5107
  (month, index) => [month, index]
4917
5108
  )
4918
5109
  );
4919
- function parseMlsdList(input, directory = ".") {
4920
- return input.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0).map((line) => parseMlsdLine(line, directory)).filter((entry) => entry.name !== "." && entry.name !== "..");
4921
- }
4922
- function parseUnixList(input, directory = ".", now = /* @__PURE__ */ new Date()) {
4923
- return input.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0 && !line.toLowerCase().startsWith("total ")).map((line) => parseUnixListLine(line, directory, now)).filter((entry) => entry.name !== "." && entry.name !== "..");
4924
- }
4925
5110
  function parseUnixListLine(line, directory = ".", now = /* @__PURE__ */ new Date()) {
4926
5111
  const match = /^(\S{10})\s+\d+\s+(\S+)\s+(\S+)\s+(\d+)\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4}|\d{1,2}:\d{2})\s+(.+)$/.exec(
4927
5112
  line
@@ -5797,38 +5982,53 @@ async function expectCompletion(control, command, path2) {
5797
5982
  const response = await control.sendCommand(command);
5798
5983
  assertPathCommandSucceeded(response, command, path2, control.providerId);
5799
5984
  }
5800
- async function readPassiveDataCommand(control, command, path2, options = {}) {
5801
- const dataConnection = await openPassiveDataCommand(control, command, path2, options);
5985
+ async function readPassiveLinesCommand(control, command, path2, onLine) {
5986
+ const dataConnection = await openPassiveDataCommand(control, command, path2);
5802
5987
  try {
5803
- const payload = await collectPassiveData(
5804
- dataConnection,
5805
- control.operationTimeoutMs,
5806
- path2,
5807
- control.providerId
5808
- );
5988
+ const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
5989
+ command,
5990
+ onLine,
5991
+ path: path2,
5992
+ providerId: control.providerId
5993
+ });
5809
5994
  const finalResponse = await control.readFinalResponse({
5810
5995
  command,
5811
5996
  operation: "data command completion",
5812
5997
  path: path2
5813
5998
  });
5814
5999
  assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
5815
- return payload;
6000
+ if (failure !== void 0) throw failure;
5816
6001
  } catch (error) {
5817
6002
  dataConnection.close();
5818
6003
  throw error;
5819
6004
  }
5820
6005
  }
5821
6006
  async function readDirectoryEntries(control, path2) {
6007
+ const entries = [];
6008
+ const collectEntry = (entry) => {
6009
+ if (entry.name === "." || entry.name === "..") return;
6010
+ entries.push(entry);
6011
+ };
5822
6012
  try {
5823
- const payload2 = await readPassiveDataCommand(control, `MLSD ${path2}`, path2);
5824
- return parseMlsdList(payload2.toString("utf8"), path2);
6013
+ await readPassiveLinesCommand(control, `MLSD ${path2}`, path2, (rawLine) => {
6014
+ const line = rawLine.trimEnd();
6015
+ if (line.length === 0) return;
6016
+ collectEntry(parseMlsdLine(line, path2));
6017
+ });
6018
+ return entries;
5825
6019
  } catch (error) {
5826
6020
  if (!isUnsupportedFtpCommandError(error, "MLSD")) {
5827
6021
  throw error;
5828
6022
  }
5829
6023
  }
5830
- const payload = await readPassiveDataCommand(control, `LIST ${path2}`, path2);
5831
- return parseUnixList(payload.toString("utf8"), path2);
6024
+ entries.length = 0;
6025
+ const now = /* @__PURE__ */ new Date();
6026
+ await readPassiveLinesCommand(control, `LIST ${path2}`, path2, (rawLine) => {
6027
+ const line = rawLine.trimEnd();
6028
+ if (line.length === 0 || line.toLowerCase().startsWith("total ")) return;
6029
+ collectEntry(parseUnixListLine(line, path2, now));
6030
+ });
6031
+ return entries;
5832
6032
  }
5833
6033
  async function openPassiveDataCommand(control, command, path2, options = {}) {
5834
6034
  const offset = normalizeOptionalByteCount3(options.offset, "offset", path2);
@@ -6001,22 +6201,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
6001
6201
  }
6002
6202
  };
6003
6203
  }
6004
- async function collectPassiveData(dataConnection, timeoutMs, path2, providerId) {
6005
- const chunks = [];
6204
+ var MAX_LIST_LINE_BYTES = 64 * 1024;
6205
+ async function consumePassiveLines(dataConnection, timeoutMs, input) {
6206
+ let carry = import_node_buffer5.Buffer.alloc(0);
6207
+ let failure;
6006
6208
  const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
6007
6209
  host: dataConnection.endpoint.host,
6008
6210
  operation: "passive data transfer",
6009
- path: path2,
6010
- providerId
6211
+ path: input.path,
6212
+ providerId: input.providerId
6011
6213
  });
6214
+ const overlongLineFailure = () => new ParseError({
6215
+ details: { command: input.command, limitBytes: MAX_LIST_LINE_BYTES, path: input.path },
6216
+ message: `FTP listing line exceeded ${String(MAX_LIST_LINE_BYTES)} bytes for ${input.command}`,
6217
+ retryable: false
6218
+ });
6219
+ const emit = (lineBytes) => {
6220
+ if (failure !== void 0) return;
6221
+ let end = lineBytes.length;
6222
+ if (end > 0 && lineBytes[end - 1] === 13) end -= 1;
6223
+ if (end === 0) return;
6224
+ if (end > MAX_LIST_LINE_BYTES) {
6225
+ failure = overlongLineFailure();
6226
+ return;
6227
+ }
6228
+ try {
6229
+ input.onLine(lineBytes.toString("utf8", 0, end));
6230
+ } catch (error) {
6231
+ failure = error instanceof Error ? error : new Error(String(error));
6232
+ }
6233
+ };
6012
6234
  try {
6013
6235
  for await (const chunk of dataConnection.socket) {
6014
- chunks.push(import_node_buffer6.Buffer.from(chunk));
6236
+ if (failure !== void 0) continue;
6237
+ const data = carry.length > 0 ? import_node_buffer5.Buffer.concat([carry, chunk]) : chunk;
6238
+ let start = 0;
6239
+ let newline = data.indexOf(10, start);
6240
+ while (newline !== -1) {
6241
+ emit(data.subarray(start, newline));
6242
+ start = newline + 1;
6243
+ newline = data.indexOf(10, start);
6244
+ }
6245
+ carry = import_node_buffer5.Buffer.from(data.subarray(start));
6246
+ if (carry.length > MAX_LIST_LINE_BYTES && failure === void 0) {
6247
+ failure = overlongLineFailure();
6248
+ }
6249
+ if (failure !== void 0) carry = import_node_buffer5.Buffer.alloc(0);
6015
6250
  }
6251
+ if (carry.length > 0) emit(carry);
6016
6252
  } finally {
6017
6253
  clearIdleTimeout();
6018
6254
  }
6019
- return import_node_buffer6.Buffer.concat(chunks);
6255
+ return failure;
6020
6256
  }
6021
6257
  async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
6022
6258
  let bytesEmitted = 0;
@@ -6033,7 +6269,7 @@ async function* createPassiveReadSource(control, dataConnection, command, path2,
6033
6269
  });
6034
6270
  for await (const chunk of dataConnection.socket) {
6035
6271
  request.throwIfAborted();
6036
- const buffer = import_node_buffer6.Buffer.from(chunk);
6272
+ const buffer = import_node_buffer5.Buffer.from(chunk);
6037
6273
  if (range.length === void 0) {
6038
6274
  bytesEmitted += buffer.byteLength;
6039
6275
  yield new Uint8Array(buffer);
@@ -6265,6 +6501,13 @@ function createTlsPinnedFingerprints(profile) {
6265
6501
  if (pinnedFingerprint256 === void 0) {
6266
6502
  return void 0;
6267
6503
  }
6504
+ if (profile.tls?.rejectUnauthorized === false) {
6505
+ throw new ConfigurationError({
6506
+ 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.",
6507
+ protocol: FTPS_PROVIDER_ID,
6508
+ retryable: false
6509
+ });
6510
+ }
6268
6511
  const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
6269
6512
  if (fingerprints.length === 0) {
6270
6513
  throw new ConfigurationError({
@@ -6319,9 +6562,9 @@ function normalizeCertificateFingerprint256(certificate) {
6319
6562
  }
6320
6563
  function normalizeTlsSecretValue(value) {
6321
6564
  if (Array.isArray(value)) {
6322
- return value.map((item) => import_node_buffer6.Buffer.isBuffer(item) ? import_node_buffer6.Buffer.from(item) : item);
6565
+ return value.map((item) => import_node_buffer5.Buffer.isBuffer(item) ? import_node_buffer5.Buffer.from(item) : item);
6323
6566
  }
6324
- return import_node_buffer6.Buffer.isBuffer(value) ? import_node_buffer6.Buffer.from(value) : value;
6567
+ return import_node_buffer5.Buffer.isBuffer(value) ? import_node_buffer5.Buffer.from(value) : value;
6325
6568
  }
6326
6569
  async function authenticateFtpSession(control, username, password, host) {
6327
6570
  const safeUsername = assertSafeFtpArgument(username, "username");
@@ -6500,7 +6743,7 @@ function compareEntries5(left, right) {
6500
6743
  return left.path.localeCompare(right.path);
6501
6744
  }
6502
6745
  function secretToString(value) {
6503
- return import_node_buffer6.Buffer.isBuffer(value) ? value.toString("utf8") : value;
6746
+ return import_node_buffer5.Buffer.isBuffer(value) ? value.toString("utf8") : value;
6504
6747
  }
6505
6748
  // Annotate the CommonJS export names for ESM import in node:
6506
6749
  0 && (module.exports = {
@@ -6534,6 +6777,7 @@ function secretToString(value) {
6534
6777
  copyBetween,
6535
6778
  createAtomicDeployPlan,
6536
6779
  createBandwidthThrottle,
6780
+ createDefaultRetryPolicy,
6537
6781
  createFtpsProviderFactory,
6538
6782
  createLocalProviderFactory,
6539
6783
  createMemoryProviderFactory,
@@ -6570,8 +6814,10 @@ function secretToString(value) {
6570
6814
  parseRemoteManifest,
6571
6815
  redactCommand,
6572
6816
  redactConnectionProfile,
6817
+ redactErrorForLogging,
6573
6818
  redactObject,
6574
6819
  redactSecretSource,
6820
+ redactUrlForLogging,
6575
6821
  redactValue,
6576
6822
  resolveConnectionProfileSecrets,
6577
6823
  resolveOpenSshHost,