@zero-transfer/ftp 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
@@ -61,6 +61,7 @@ __export(ftp_exports, {
61
61
  copyBetween: () => copyBetween,
62
62
  createAtomicDeployPlan: () => createAtomicDeployPlan,
63
63
  createBandwidthThrottle: () => createBandwidthThrottle,
64
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
64
65
  createFtpProviderFactory: () => createFtpProviderFactory,
65
66
  createLocalProviderFactory: () => createLocalProviderFactory,
66
67
  createMemoryProviderFactory: () => createMemoryProviderFactory,
@@ -104,8 +105,10 @@ __export(ftp_exports, {
104
105
  parseUnixListLine: () => parseUnixListLine,
105
106
  redactCommand: () => redactCommand,
106
107
  redactConnectionProfile: () => redactConnectionProfile,
108
+ redactErrorForLogging: () => redactErrorForLogging,
107
109
  redactObject: () => redactObject,
108
110
  redactSecretSource: () => redactSecretSource,
111
+ redactUrlForLogging: () => redactUrlForLogging,
109
112
  redactValue: () => redactValue,
110
113
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
111
114
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -126,6 +129,68 @@ module.exports = __toCommonJS(ftp_exports);
126
129
  // src/client/ZeroTransfer.ts
127
130
  var import_node_events = require("events");
128
131
 
132
+ // src/logging/redaction.ts
133
+ var REDACTED = "[REDACTED]";
134
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
135
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
136
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
137
+ function isSensitiveKey(key) {
138
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
139
+ }
140
+ function redactCommand(command) {
141
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
142
+ return `${commandName.toUpperCase()} ${REDACTED}`;
143
+ });
144
+ }
145
+ function redactValue(value) {
146
+ if (typeof value === "string") {
147
+ return redactCommand(value);
148
+ }
149
+ if (Array.isArray(value)) {
150
+ return value.map((item) => redactValue(item));
151
+ }
152
+ if (value !== null && typeof value === "object") {
153
+ return redactObject(value);
154
+ }
155
+ return value;
156
+ }
157
+ function redactObject(input) {
158
+ return Object.fromEntries(
159
+ Object.entries(input).map(([key, value]) => {
160
+ if (isSensitiveKey(key)) {
161
+ return [key, REDACTED];
162
+ }
163
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
164
+ return [key, redactUrlForLogging(value)];
165
+ }
166
+ return [key, redactValue(value)];
167
+ })
168
+ );
169
+ }
170
+ function redactUrlForLogging(url) {
171
+ let parsed;
172
+ try {
173
+ parsed = typeof url === "string" ? new URL(url) : url;
174
+ } catch {
175
+ return REDACTED;
176
+ }
177
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
178
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
179
+ return `${origin}${parsed.pathname}${query}`;
180
+ }
181
+ function redactErrorForLogging(error) {
182
+ if (error !== null && typeof error === "object") {
183
+ const candidate = error;
184
+ if (typeof candidate.toJSON === "function") {
185
+ return redactObject(candidate.toJSON());
186
+ }
187
+ }
188
+ if (error instanceof Error) {
189
+ return redactObject({ message: error.message, name: error.name });
190
+ }
191
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
192
+ }
193
+
129
194
  // src/errors/ZeroTransferError.ts
130
195
  var ZeroTransferError = class extends Error {
131
196
  /** Stable machine-readable error code. */
@@ -167,6 +232,11 @@ var ZeroTransferError = class extends Error {
167
232
  /**
168
233
  * Serializes the error into a plain object suitable for logs or API responses.
169
234
  *
235
+ * `details` and `command` are passed through secret redaction so serialized
236
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
237
+ * live {@link ZeroTransferError.details | details} property stays unredacted
238
+ * for programmatic consumers.
239
+ *
170
240
  * @returns A JSON-safe object containing public structured error fields.
171
241
  */
172
242
  toJSON() {
@@ -176,12 +246,12 @@ var ZeroTransferError = class extends Error {
176
246
  message: this.message,
177
247
  protocol: this.protocol,
178
248
  host: this.host,
179
- command: this.command,
249
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
180
250
  ftpCode: this.ftpCode,
181
251
  sftpCode: this.sftpCode,
182
252
  path: this.path,
183
253
  retryable: this.retryable,
184
- details: this.details
254
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
185
255
  };
186
256
  }
187
257
  };
@@ -704,15 +774,20 @@ var ProviderRegistry = class {
704
774
  var TransferClient = class {
705
775
  /** Provider registry used by this client. */
706
776
  registry;
777
+ /** Execution defaults applied when call sites omit their own values. */
778
+ defaults;
707
779
  logger;
708
780
  /**
709
781
  * Creates a transfer client without opening any provider connections.
710
782
  *
711
- * @param options - Optional registry, provider factories, and logger.
783
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
712
784
  */
713
785
  constructor(options = {}) {
714
786
  this.registry = options.registry ?? new ProviderRegistry();
715
787
  this.logger = options.logger ?? noopLogger;
788
+ if (options.defaults !== void 0) {
789
+ this.defaults = { ...options.defaults };
790
+ }
716
791
  for (const provider of options.providers ?? []) {
717
792
  this.registry.register(provider);
718
793
  }
@@ -1285,18 +1360,25 @@ var TransferEngine = class {
1285
1360
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1286
1361
  this.throwIfAborted(abortScope.signal, job);
1287
1362
  const attemptStartedAt = this.now();
1363
+ const attemptScope = createAttemptScope(
1364
+ abortScope.signal,
1365
+ options.timeout,
1366
+ job,
1367
+ attemptNumber
1368
+ );
1288
1369
  const context = this.createExecutionContext(
1289
1370
  job,
1290
1371
  attemptNumber,
1291
1372
  attemptStartedAt,
1292
1373
  options,
1293
- abortScope.signal,
1374
+ attemptScope.signal,
1294
1375
  (bytesTransferred) => {
1295
1376
  latestBytesTransferred = bytesTransferred;
1296
- }
1377
+ },
1378
+ attemptScope.notifyProgress
1297
1379
  );
1298
1380
  try {
1299
- const result = await runExecutor(executor, context, abortScope.signal, job);
1381
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1300
1382
  context.throwIfAborted();
1301
1383
  latestBytesTransferred = result.bytesTransferred;
1302
1384
  const completedAt = this.now();
@@ -1314,16 +1396,27 @@ var TransferEngine = class {
1314
1396
  summarizeError(error)
1315
1397
  );
1316
1398
  attempts.push(attempt);
1317
- if (error instanceof AbortError || error instanceof TimeoutError) {
1399
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1318
1400
  throw error;
1319
1401
  }
1320
- const retryInput = { attempt: attemptNumber, error, job };
1402
+ const retryInput = {
1403
+ attempt: attemptNumber,
1404
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1405
+ error,
1406
+ job
1407
+ };
1321
1408
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1322
1409
  if (shouldRetry) {
1323
1410
  options.retry?.onRetry?.(retryInput);
1411
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1412
+ if (delayMs > 0) {
1413
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1414
+ }
1324
1415
  continue;
1325
1416
  }
1326
1417
  throw createTransferFailure(job, error, attempts);
1418
+ } finally {
1419
+ attemptScope.dispose();
1327
1420
  }
1328
1421
  }
1329
1422
  throw createTransferFailure(job, void 0, attempts);
@@ -1331,12 +1424,13 @@ var TransferEngine = class {
1331
1424
  abortScope.dispose();
1332
1425
  }
1333
1426
  }
1334
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1427
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1335
1428
  const context = {
1336
1429
  attempt,
1337
1430
  job,
1338
1431
  reportProgress: (bytesTransferred, totalBytes) => {
1339
1432
  this.throwIfAborted(signal, job);
1433
+ notifyProgress();
1340
1434
  updateBytesTransferred(bytesTransferred);
1341
1435
  const progressInput = {
1342
1436
  bytesTransferred,
@@ -1405,6 +1499,96 @@ function createAbortScope(parentSignal, timeout, job) {
1405
1499
  signal: controller.signal
1406
1500
  };
1407
1501
  }
1502
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1503
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1504
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1505
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1506
+ const scope = {
1507
+ dispose: () => void 0,
1508
+ notifyProgress: () => void 0
1509
+ };
1510
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1511
+ return scope;
1512
+ }
1513
+ const controller = new AbortController();
1514
+ const retryable = timeout?.retryable ?? true;
1515
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1516
+ if (parentSignal?.aborted === true) {
1517
+ abortFromParent();
1518
+ } else {
1519
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1520
+ }
1521
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1522
+ controller.abort(
1523
+ new TimeoutError({
1524
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1525
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1526
+ retryable
1527
+ })
1528
+ );
1529
+ }, attemptTimeoutMs);
1530
+ let stallTimer;
1531
+ const armStallWatchdog = () => {
1532
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1533
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1534
+ stallTimer = setTimeout(() => {
1535
+ controller.abort(
1536
+ new TimeoutError({
1537
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1538
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1539
+ retryable
1540
+ })
1541
+ );
1542
+ }, stallTimeoutMs);
1543
+ };
1544
+ armStallWatchdog();
1545
+ return {
1546
+ dispose: () => {
1547
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1548
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1549
+ parentSignal?.removeEventListener("abort", abortFromParent);
1550
+ },
1551
+ notifyProgress: armStallWatchdog,
1552
+ signal: controller.signal
1553
+ };
1554
+ }
1555
+ function sleepWithAbort(delayMs, signal, job) {
1556
+ return new Promise((resolve, reject) => {
1557
+ if (signal === void 0) {
1558
+ setTimeout(resolve, delayMs);
1559
+ return;
1560
+ }
1561
+ if (signal.aborted) {
1562
+ reject(toAbortFailure(signal, job));
1563
+ return;
1564
+ }
1565
+ const rejectAbort = () => {
1566
+ clearTimeout(timer);
1567
+ reject(toAbortFailure(signal, job));
1568
+ };
1569
+ const timer = setTimeout(() => {
1570
+ signal.removeEventListener("abort", rejectAbort);
1571
+ resolve();
1572
+ }, delayMs);
1573
+ signal.addEventListener("abort", rejectAbort, { once: true });
1574
+ });
1575
+ }
1576
+ function toAbortFailure(signal, job) {
1577
+ if (signal.reason instanceof ZeroTransferError) {
1578
+ return signal.reason;
1579
+ }
1580
+ return new AbortError({
1581
+ details: { jobId: job.id, operation: job.operation },
1582
+ message: `Transfer job aborted: ${job.id}`,
1583
+ retryable: false
1584
+ });
1585
+ }
1586
+ function normalizeDelayMs(value) {
1587
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1588
+ return 0;
1589
+ }
1590
+ return Math.floor(value);
1591
+ }
1408
1592
  function normalizeTimeoutMs(value) {
1409
1593
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1410
1594
  return void 0;
@@ -1573,7 +1757,7 @@ async function runRoute(options) {
1573
1757
  const executor = createProviderTransferExecutor({
1574
1758
  resolveSession: ({ role }) => sessions.get(role)
1575
1759
  });
1576
- return await engine.execute(job, executor, buildExecuteOptions(options));
1760
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1577
1761
  } finally {
1578
1762
  if (destinationSession !== void 0) {
1579
1763
  await destinationSession.disconnect();
@@ -1610,12 +1794,14 @@ function defaultJobId(route, now) {
1610
1794
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1611
1795
  return `route:${route.id}:${timestamp.toString(36)}`;
1612
1796
  }
1613
- function buildExecuteOptions(options) {
1797
+ function buildExecuteOptions(options, client) {
1614
1798
  const execute = {};
1799
+ const retry = options.retry ?? client.defaults?.retry;
1800
+ const timeout = options.timeout ?? client.defaults?.timeout;
1615
1801
  if (options.signal !== void 0) execute.signal = options.signal;
1616
- if (options.retry !== void 0) execute.retry = options.retry;
1802
+ if (retry !== void 0) execute.retry = retry;
1617
1803
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1618
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1804
+ if (timeout !== void 0) execute.timeout = timeout;
1619
1805
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1620
1806
  return execute;
1621
1807
  }
@@ -1672,41 +1858,6 @@ function defaultRouteSuffix(source, destination) {
1672
1858
  return `${source}->${destination}`;
1673
1859
  }
1674
1860
 
1675
- // src/logging/redaction.ts
1676
- var REDACTED = "[REDACTED]";
1677
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1678
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1679
- function isSensitiveKey(key) {
1680
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1681
- }
1682
- function redactCommand(command) {
1683
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1684
- return `${commandName.toUpperCase()} ${REDACTED}`;
1685
- });
1686
- }
1687
- function redactValue(value) {
1688
- if (typeof value === "string") {
1689
- return redactCommand(value);
1690
- }
1691
- if (Array.isArray(value)) {
1692
- return value.map((item) => redactValue(item));
1693
- }
1694
- if (value !== null && typeof value === "object") {
1695
- return redactObject(value);
1696
- }
1697
- return value;
1698
- }
1699
- function redactObject(input) {
1700
- return Object.fromEntries(
1701
- Object.entries(input).map(([key, value]) => {
1702
- if (isSensitiveKey(key)) {
1703
- return [key, REDACTED];
1704
- }
1705
- return [key, redactValue(value)];
1706
- })
1707
- );
1708
- }
1709
-
1710
1861
  // src/profiles/SecretSource.ts
1711
1862
  var import_node_buffer2 = require("buffer");
1712
1863
  var import_promises = require("fs/promises");
@@ -2078,11 +2229,11 @@ var import_promises2 = require("fs/promises");
2078
2229
  var import_node_path2 = __toESM(require("path"));
2079
2230
 
2080
2231
  // src/utils/path.ts
2081
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2232
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2082
2233
  function assertSafeFtpArgument(value, label = "path") {
2083
2234
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2084
2235
  throw new ConfigurationError({
2085
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2236
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2086
2237
  retryable: false,
2087
2238
  details: {
2088
2239
  label
@@ -3384,7 +3535,6 @@ function expandAlgorithms(values) {
3384
3535
  }
3385
3536
 
3386
3537
  // src/profiles/importers/FileZillaImporter.ts
3387
- var import_node_buffer5 = require("buffer");
3388
3538
  function importFileZillaSites(xml) {
3389
3539
  const events = tokenizeXml(xml);
3390
3540
  if (events.length === 0) {
@@ -3400,7 +3550,6 @@ function importFileZillaSites(xml) {
3400
3550
  const folderNamePending = [];
3401
3551
  let inServer = false;
3402
3552
  let serverFields = {};
3403
- let serverPasswordEncoding;
3404
3553
  let activeTag;
3405
3554
  let captureFolderName = false;
3406
3555
  for (const event of events) {
@@ -3413,13 +3562,9 @@ function importFileZillaSites(xml) {
3413
3562
  if (event.name === "Server") {
3414
3563
  inServer = true;
3415
3564
  serverFields = {};
3416
- serverPasswordEncoding = void 0;
3417
3565
  continue;
3418
3566
  }
3419
3567
  activeTag = event.name;
3420
- if (event.name === "Pass" && inServer) {
3421
- serverPasswordEncoding = event.attributes["encoding"];
3422
- }
3423
3568
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
3424
3569
  captureFolderName = true;
3425
3570
  }
@@ -3445,7 +3590,7 @@ function importFileZillaSites(xml) {
3445
3590
  }
3446
3591
  if (event.name === "Server") {
3447
3592
  const folder = folderStack.filter((segment) => segment !== "");
3448
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
3593
+ const result = buildSiteFromFields(serverFields);
3449
3594
  if (result.kind === "site") {
3450
3595
  sites.push({ ...result.site, folder });
3451
3596
  } else {
@@ -3457,7 +3602,6 @@ function importFileZillaSites(xml) {
3457
3602
  }
3458
3603
  inServer = false;
3459
3604
  serverFields = {};
3460
- serverPasswordEncoding = void 0;
3461
3605
  activeTag = void 0;
3462
3606
  continue;
3463
3607
  }
@@ -3466,7 +3610,7 @@ function importFileZillaSites(xml) {
3466
3610
  }
3467
3611
  return { sites, skipped };
3468
3612
  }
3469
- function buildSiteFromFields(fields, passwordEncoding) {
3613
+ function buildSiteFromFields(fields) {
3470
3614
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
3471
3615
  const host = (fields["Host"] ?? "").trim();
3472
3616
  if (host === "") return { kind: "skipped", name };
@@ -3485,18 +3629,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
3485
3629
  }
3486
3630
  const user = fields["User"]?.trim();
3487
3631
  if (user !== void 0 && user !== "") profile.username = { value: user };
3488
- let password;
3489
3632
  const rawPass = fields["Pass"];
3490
- if (rawPass !== void 0 && rawPass !== "") {
3491
- if (passwordEncoding === "base64") {
3492
- password = import_node_buffer5.Buffer.from(rawPass, "base64").toString("utf8");
3493
- } else {
3494
- password = rawPass;
3495
- }
3496
- if (password !== void 0 && password !== "") profile.password = { value: password };
3497
- }
3498
- const site = { name, profile };
3499
- if (password !== void 0) site.password = password;
3633
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
3634
+ const site = { hasStoredPassword, name, profile };
3500
3635
  const logonText = fields["Logontype"];
3501
3636
  if (logonText !== void 0) {
3502
3637
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -3739,6 +3874,62 @@ function mapFtp550(details) {
3739
3874
  return new PermissionDeniedError(details);
3740
3875
  }
3741
3876
 
3877
+ // src/transfers/createDefaultRetryPolicy.ts
3878
+ var DEFAULT_MAX_ATTEMPTS = 4;
3879
+ var DEFAULT_BASE_DELAY_MS = 250;
3880
+ var DEFAULT_MAX_DELAY_MS = 3e4;
3881
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
3882
+ function createDefaultRetryPolicy(options = {}) {
3883
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
3884
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
3885
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
3886
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
3887
+ const random = options.random ?? Math.random;
3888
+ return {
3889
+ getDelayMs(input) {
3890
+ const retryAfterMs = readRetryAfterMs(input.error);
3891
+ if (retryAfterMs !== void 0) {
3892
+ return retryAfterMs;
3893
+ }
3894
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
3895
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
3896
+ return Math.floor(random() * cappedMs);
3897
+ },
3898
+ maxAttempts,
3899
+ shouldRetry(input) {
3900
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
3901
+ return false;
3902
+ }
3903
+ if (input.elapsedMs >= maxElapsedMs) {
3904
+ return false;
3905
+ }
3906
+ const retryAfterMs = readRetryAfterMs(input.error);
3907
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
3908
+ return false;
3909
+ }
3910
+ return true;
3911
+ }
3912
+ };
3913
+ }
3914
+ function readRetryAfterMs(error) {
3915
+ if (!(error instanceof ZeroTransferError)) return void 0;
3916
+ const value = error.details?.["retryAfterMs"];
3917
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
3918
+ return Math.floor(value);
3919
+ }
3920
+ function normalizePositiveInteger(value, fallback) {
3921
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
3922
+ return fallback;
3923
+ }
3924
+ return Math.floor(value);
3925
+ }
3926
+ function normalizeNonNegative(value, fallback) {
3927
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
3928
+ return fallback;
3929
+ }
3930
+ return Math.floor(value);
3931
+ }
3932
+
3742
3933
  // src/transfers/TransferPlan.ts
3743
3934
  function createTransferPlan(input) {
3744
3935
  const plan = {
@@ -3836,8 +4027,8 @@ var TransferQueue = class {
3836
4027
  this.concurrency = normalizeConcurrency(options.concurrency);
3837
4028
  this.defaultExecutor = options.executor;
3838
4029
  this.resolveExecutor = options.resolveExecutor;
3839
- this.retry = options.retry;
3840
- this.timeout = options.timeout;
4030
+ this.retry = options.retry ?? options.client?.defaults?.retry;
4031
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
3841
4032
  this.bandwidthLimit = options.bandwidthLimit;
3842
4033
  this.onProgress = options.onProgress;
3843
4034
  this.onReceipt = options.onReceipt;
@@ -4914,7 +5105,7 @@ function isMainModule(importMetaUrl) {
4914
5105
  }
4915
5106
 
4916
5107
  // src/providers/classic/ftp/FtpProvider.ts
4917
- var import_node_buffer6 = require("buffer");
5108
+ var import_node_buffer5 = require("buffer");
4918
5109
  var import_node_net = require("net");
4919
5110
  var import_node_tls = require("tls");
4920
5111
 
@@ -5813,38 +6004,53 @@ async function expectCompletion(control, command, path2) {
5813
6004
  const response = await control.sendCommand(command);
5814
6005
  assertPathCommandSucceeded(response, command, path2, control.providerId);
5815
6006
  }
5816
- async function readPassiveDataCommand(control, command, path2, options = {}) {
5817
- const dataConnection = await openPassiveDataCommand(control, command, path2, options);
6007
+ async function readPassiveLinesCommand(control, command, path2, onLine) {
6008
+ const dataConnection = await openPassiveDataCommand(control, command, path2);
5818
6009
  try {
5819
- const payload = await collectPassiveData(
5820
- dataConnection,
5821
- control.operationTimeoutMs,
5822
- path2,
5823
- control.providerId
5824
- );
6010
+ const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
6011
+ command,
6012
+ onLine,
6013
+ path: path2,
6014
+ providerId: control.providerId
6015
+ });
5825
6016
  const finalResponse = await control.readFinalResponse({
5826
6017
  command,
5827
6018
  operation: "data command completion",
5828
6019
  path: path2
5829
6020
  });
5830
6021
  assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
5831
- return payload;
6022
+ if (failure !== void 0) throw failure;
5832
6023
  } catch (error) {
5833
6024
  dataConnection.close();
5834
6025
  throw error;
5835
6026
  }
5836
6027
  }
5837
6028
  async function readDirectoryEntries(control, path2) {
6029
+ const entries = [];
6030
+ const collectEntry = (entry) => {
6031
+ if (entry.name === "." || entry.name === "..") return;
6032
+ entries.push(entry);
6033
+ };
5838
6034
  try {
5839
- const payload2 = await readPassiveDataCommand(control, `MLSD ${path2}`, path2);
5840
- return parseMlsdList(payload2.toString("utf8"), path2);
6035
+ await readPassiveLinesCommand(control, `MLSD ${path2}`, path2, (rawLine) => {
6036
+ const line = rawLine.trimEnd();
6037
+ if (line.length === 0) return;
6038
+ collectEntry(parseMlsdLine(line, path2));
6039
+ });
6040
+ return entries;
5841
6041
  } catch (error) {
5842
6042
  if (!isUnsupportedFtpCommandError(error, "MLSD")) {
5843
6043
  throw error;
5844
6044
  }
5845
6045
  }
5846
- const payload = await readPassiveDataCommand(control, `LIST ${path2}`, path2);
5847
- return parseUnixList(payload.toString("utf8"), path2);
6046
+ entries.length = 0;
6047
+ const now = /* @__PURE__ */ new Date();
6048
+ await readPassiveLinesCommand(control, `LIST ${path2}`, path2, (rawLine) => {
6049
+ const line = rawLine.trimEnd();
6050
+ if (line.length === 0 || line.toLowerCase().startsWith("total ")) return;
6051
+ collectEntry(parseUnixListLine(line, path2, now));
6052
+ });
6053
+ return entries;
5848
6054
  }
5849
6055
  async function openPassiveDataCommand(control, command, path2, options = {}) {
5850
6056
  const offset = normalizeOptionalByteCount3(options.offset, "offset", path2);
@@ -6017,22 +6223,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
6017
6223
  }
6018
6224
  };
6019
6225
  }
6020
- async function collectPassiveData(dataConnection, timeoutMs, path2, providerId) {
6021
- const chunks = [];
6226
+ var MAX_LIST_LINE_BYTES = 64 * 1024;
6227
+ async function consumePassiveLines(dataConnection, timeoutMs, input) {
6228
+ let carry = import_node_buffer5.Buffer.alloc(0);
6229
+ let failure;
6022
6230
  const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
6023
6231
  host: dataConnection.endpoint.host,
6024
6232
  operation: "passive data transfer",
6025
- path: path2,
6026
- providerId
6233
+ path: input.path,
6234
+ providerId: input.providerId
6235
+ });
6236
+ const overlongLineFailure = () => new ParseError({
6237
+ details: { command: input.command, limitBytes: MAX_LIST_LINE_BYTES, path: input.path },
6238
+ message: `FTP listing line exceeded ${String(MAX_LIST_LINE_BYTES)} bytes for ${input.command}`,
6239
+ retryable: false
6027
6240
  });
6241
+ const emit = (lineBytes) => {
6242
+ if (failure !== void 0) return;
6243
+ let end = lineBytes.length;
6244
+ if (end > 0 && lineBytes[end - 1] === 13) end -= 1;
6245
+ if (end === 0) return;
6246
+ if (end > MAX_LIST_LINE_BYTES) {
6247
+ failure = overlongLineFailure();
6248
+ return;
6249
+ }
6250
+ try {
6251
+ input.onLine(lineBytes.toString("utf8", 0, end));
6252
+ } catch (error) {
6253
+ failure = error instanceof Error ? error : new Error(String(error));
6254
+ }
6255
+ };
6028
6256
  try {
6029
6257
  for await (const chunk of dataConnection.socket) {
6030
- chunks.push(import_node_buffer6.Buffer.from(chunk));
6258
+ if (failure !== void 0) continue;
6259
+ const data = carry.length > 0 ? import_node_buffer5.Buffer.concat([carry, chunk]) : chunk;
6260
+ let start = 0;
6261
+ let newline = data.indexOf(10, start);
6262
+ while (newline !== -1) {
6263
+ emit(data.subarray(start, newline));
6264
+ start = newline + 1;
6265
+ newline = data.indexOf(10, start);
6266
+ }
6267
+ carry = import_node_buffer5.Buffer.from(data.subarray(start));
6268
+ if (carry.length > MAX_LIST_LINE_BYTES && failure === void 0) {
6269
+ failure = overlongLineFailure();
6270
+ }
6271
+ if (failure !== void 0) carry = import_node_buffer5.Buffer.alloc(0);
6031
6272
  }
6273
+ if (carry.length > 0) emit(carry);
6032
6274
  } finally {
6033
6275
  clearIdleTimeout();
6034
6276
  }
6035
- return import_node_buffer6.Buffer.concat(chunks);
6277
+ return failure;
6036
6278
  }
6037
6279
  async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
6038
6280
  let bytesEmitted = 0;
@@ -6049,7 +6291,7 @@ async function* createPassiveReadSource(control, dataConnection, command, path2,
6049
6291
  });
6050
6292
  for await (const chunk of dataConnection.socket) {
6051
6293
  request.throwIfAborted();
6052
- const buffer = import_node_buffer6.Buffer.from(chunk);
6294
+ const buffer = import_node_buffer5.Buffer.from(chunk);
6053
6295
  if (range.length === void 0) {
6054
6296
  bytesEmitted += buffer.byteLength;
6055
6297
  yield new Uint8Array(buffer);
@@ -6281,6 +6523,13 @@ function createTlsPinnedFingerprints(profile) {
6281
6523
  if (pinnedFingerprint256 === void 0) {
6282
6524
  return void 0;
6283
6525
  }
6526
+ if (profile.tls?.rejectUnauthorized === false) {
6527
+ throw new ConfigurationError({
6528
+ 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.",
6529
+ protocol: FTPS_PROVIDER_ID,
6530
+ retryable: false
6531
+ });
6532
+ }
6284
6533
  const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
6285
6534
  if (fingerprints.length === 0) {
6286
6535
  throw new ConfigurationError({
@@ -6335,9 +6584,9 @@ function normalizeCertificateFingerprint256(certificate) {
6335
6584
  }
6336
6585
  function normalizeTlsSecretValue(value) {
6337
6586
  if (Array.isArray(value)) {
6338
- return value.map((item) => import_node_buffer6.Buffer.isBuffer(item) ? import_node_buffer6.Buffer.from(item) : item);
6587
+ return value.map((item) => import_node_buffer5.Buffer.isBuffer(item) ? import_node_buffer5.Buffer.from(item) : item);
6339
6588
  }
6340
- return import_node_buffer6.Buffer.isBuffer(value) ? import_node_buffer6.Buffer.from(value) : value;
6589
+ return import_node_buffer5.Buffer.isBuffer(value) ? import_node_buffer5.Buffer.from(value) : value;
6341
6590
  }
6342
6591
  async function authenticateFtpSession(control, username, password, host) {
6343
6592
  const safeUsername = assertSafeFtpArgument(username, "username");
@@ -6516,7 +6765,7 @@ function compareEntries5(left, right) {
6516
6765
  return left.path.localeCompare(right.path);
6517
6766
  }
6518
6767
  function secretToString(value) {
6519
- return import_node_buffer6.Buffer.isBuffer(value) ? value.toString("utf8") : value;
6768
+ return import_node_buffer5.Buffer.isBuffer(value) ? value.toString("utf8") : value;
6520
6769
  }
6521
6770
 
6522
6771
  // src/providers/classic/ftp/FtpFeatureParser.ts
@@ -6592,6 +6841,7 @@ function normalizeFeatureLines(input) {
6592
6841
  copyBetween,
6593
6842
  createAtomicDeployPlan,
6594
6843
  createBandwidthThrottle,
6844
+ createDefaultRetryPolicy,
6595
6845
  createFtpProviderFactory,
6596
6846
  createLocalProviderFactory,
6597
6847
  createMemoryProviderFactory,
@@ -6635,8 +6885,10 @@ function normalizeFeatureLines(input) {
6635
6885
  parseUnixListLine,
6636
6886
  redactCommand,
6637
6887
  redactConnectionProfile,
6888
+ redactErrorForLogging,
6638
6889
  redactObject,
6639
6890
  redactSecretSource,
6891
+ redactUrlForLogging,
6640
6892
  redactValue,
6641
6893
  resolveConnectionProfileSecrets,
6642
6894
  resolveOpenSshHost,