@zero-transfer/dropbox 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/README.md CHANGED
@@ -8,8 +8,6 @@
8
8
  npm install @zero-transfer/dropbox
9
9
  ```
10
10
 
11
- Installing this package automatically pulls in [`@zero-transfer/core`](https://www.npmjs.com/package/@zero-transfer/core) as a transitive dependency. The full core surface (`createTransferClient`, `uploadFile`, `downloadFile`, profiles, errors, sync planner, …) is re-exported from this package, so a single `import { … } from "@zero-transfer/dropbox"` is all you need. If your app uses multiple protocols, install the umbrella [`@zero-transfer/sdk`](https://www.npmjs.com/package/@zero-transfer/sdk) instead of multiple scoped packages.
12
-
13
11
  ## Overview
14
12
 
15
13
  Dropbox provider - RPC + content-host APIs, list-folder cursor pagination, ranged downloads, single-shot uploads in `overwrite` mode, and `content_hash` exposed as both `uniqueId` and `checksum`.
package/dist/index.cjs CHANGED
@@ -60,6 +60,7 @@ __export(dropbox_exports, {
60
60
  copyBetween: () => copyBetween,
61
61
  createAtomicDeployPlan: () => createAtomicDeployPlan,
62
62
  createBandwidthThrottle: () => createBandwidthThrottle,
63
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
63
64
  createDropboxProviderFactory: () => createDropboxProviderFactory,
64
65
  createLocalProviderFactory: () => createLocalProviderFactory,
65
66
  createMemoryProviderFactory: () => createMemoryProviderFactory,
@@ -96,8 +97,10 @@ __export(dropbox_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(dropbox_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/web/httpInternals.ts
4909
- var import_node_buffer6 = require("buffer");
5100
+ var import_node_buffer5 = require("buffer");
4910
5101
  function parseContentRangeTotal(value) {
4911
5102
  const match = /\/(\d+)\s*$/.exec(value);
4912
5103
  if (match === null) return void 0;
@@ -4936,9 +5127,19 @@ async function* webStreamToAsyncIterable(body) {
4936
5127
  const reader = body.getReader();
4937
5128
  try {
4938
5129
  while (true) {
4939
- const { done, value } = await reader.read();
4940
- if (done) break;
4941
- if (value !== void 0) yield value;
5130
+ let result;
5131
+ try {
5132
+ result = await reader.read();
5133
+ } catch (error) {
5134
+ if (error instanceof ZeroTransferError) throw error;
5135
+ throw new ConnectionError({
5136
+ cause: error,
5137
+ message: "HTTP response stream was interrupted before completion",
5138
+ retryable: true
5139
+ });
5140
+ }
5141
+ if (result.done) break;
5142
+ if (result.value !== void 0) yield result.value;
4942
5143
  }
4943
5144
  } finally {
4944
5145
  reader.releaseLock();
@@ -4946,8 +5147,8 @@ async function* webStreamToAsyncIterable(body) {
4946
5147
  }
4947
5148
  function secretToString(value) {
4948
5149
  if (typeof value === "string") return value;
4949
- if (value instanceof Uint8Array || import_node_buffer6.Buffer.isBuffer(value)) {
4950
- return import_node_buffer6.Buffer.from(value).toString("utf8");
5150
+ if (value instanceof Uint8Array || import_node_buffer5.Buffer.isBuffer(value)) {
5151
+ return import_node_buffer5.Buffer.from(value).toString("utf8");
4951
5152
  }
4952
5153
  return String(value);
4953
5154
  }
@@ -5375,6 +5576,7 @@ async function collectChunks(source) {
5375
5576
  copyBetween,
5376
5577
  createAtomicDeployPlan,
5377
5578
  createBandwidthThrottle,
5579
+ createDefaultRetryPolicy,
5378
5580
  createDropboxProviderFactory,
5379
5581
  createLocalProviderFactory,
5380
5582
  createMemoryProviderFactory,
@@ -5411,8 +5613,10 @@ async function collectChunks(source) {
5411
5613
  parseRemoteManifest,
5412
5614
  redactCommand,
5413
5615
  redactConnectionProfile,
5616
+ redactErrorForLogging,
5414
5617
  redactObject,
5415
5618
  redactSecretSource,
5619
+ redactUrlForLogging,
5416
5620
  redactValue,
5417
5621
  resolveConnectionProfileSecrets,
5418
5622
  resolveOpenSshHost,