@zero-transfer/s3 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
@@ -60,6 +60,7 @@ __export(s3_exports, {
60
60
  copyBetween: () => copyBetween,
61
61
  createAtomicDeployPlan: () => createAtomicDeployPlan,
62
62
  createBandwidthThrottle: () => createBandwidthThrottle,
63
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
63
64
  createFileSystemS3MultipartResumeStore: () => createFileSystemS3MultipartResumeStore,
64
65
  createLocalProviderFactory: () => createLocalProviderFactory,
65
66
  createMemoryProviderFactory: () => createMemoryProviderFactory,
@@ -98,8 +99,10 @@ __export(s3_exports, {
98
99
  parseRemoteManifest: () => parseRemoteManifest,
99
100
  redactCommand: () => redactCommand,
100
101
  redactConnectionProfile: () => redactConnectionProfile,
102
+ redactErrorForLogging: () => redactErrorForLogging,
101
103
  redactObject: () => redactObject,
102
104
  redactSecretSource: () => redactSecretSource,
105
+ redactUrlForLogging: () => redactUrlForLogging,
103
106
  redactValue: () => redactValue,
104
107
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
105
108
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -120,6 +123,68 @@ module.exports = __toCommonJS(s3_exports);
120
123
  // src/client/ZeroTransfer.ts
121
124
  var import_node_events = require("events");
122
125
 
126
+ // src/logging/redaction.ts
127
+ var REDACTED = "[REDACTED]";
128
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
129
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
130
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
131
+ function isSensitiveKey(key) {
132
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
133
+ }
134
+ function redactCommand(command) {
135
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
136
+ return `${commandName.toUpperCase()} ${REDACTED}`;
137
+ });
138
+ }
139
+ function redactValue(value) {
140
+ if (typeof value === "string") {
141
+ return redactCommand(value);
142
+ }
143
+ if (Array.isArray(value)) {
144
+ return value.map((item) => redactValue(item));
145
+ }
146
+ if (value !== null && typeof value === "object") {
147
+ return redactObject(value);
148
+ }
149
+ return value;
150
+ }
151
+ function redactObject(input) {
152
+ return Object.fromEntries(
153
+ Object.entries(input).map(([key, value]) => {
154
+ if (isSensitiveKey(key)) {
155
+ return [key, REDACTED];
156
+ }
157
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
158
+ return [key, redactUrlForLogging(value)];
159
+ }
160
+ return [key, redactValue(value)];
161
+ })
162
+ );
163
+ }
164
+ function redactUrlForLogging(url) {
165
+ let parsed;
166
+ try {
167
+ parsed = typeof url === "string" ? new URL(url) : url;
168
+ } catch {
169
+ return REDACTED;
170
+ }
171
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
172
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
173
+ return `${origin}${parsed.pathname}${query}`;
174
+ }
175
+ function redactErrorForLogging(error) {
176
+ if (error !== null && typeof error === "object") {
177
+ const candidate = error;
178
+ if (typeof candidate.toJSON === "function") {
179
+ return redactObject(candidate.toJSON());
180
+ }
181
+ }
182
+ if (error instanceof Error) {
183
+ return redactObject({ message: error.message, name: error.name });
184
+ }
185
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
186
+ }
187
+
123
188
  // src/errors/ZeroTransferError.ts
124
189
  var ZeroTransferError = class extends Error {
125
190
  /** Stable machine-readable error code. */
@@ -161,6 +226,11 @@ var ZeroTransferError = class extends Error {
161
226
  /**
162
227
  * Serializes the error into a plain object suitable for logs or API responses.
163
228
  *
229
+ * `details` and `command` are passed through secret redaction so serialized
230
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
231
+ * live {@link ZeroTransferError.details | details} property stays unredacted
232
+ * for programmatic consumers.
233
+ *
164
234
  * @returns A JSON-safe object containing public structured error fields.
165
235
  */
166
236
  toJSON() {
@@ -170,12 +240,12 @@ var ZeroTransferError = class extends Error {
170
240
  message: this.message,
171
241
  protocol: this.protocol,
172
242
  host: this.host,
173
- command: this.command,
243
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
174
244
  ftpCode: this.ftpCode,
175
245
  sftpCode: this.sftpCode,
176
246
  path: this.path,
177
247
  retryable: this.retryable,
178
- details: this.details
248
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
179
249
  };
180
250
  }
181
251
  };
@@ -698,15 +768,20 @@ var ProviderRegistry = class {
698
768
  var TransferClient = class {
699
769
  /** Provider registry used by this client. */
700
770
  registry;
771
+ /** Execution defaults applied when call sites omit their own values. */
772
+ defaults;
701
773
  logger;
702
774
  /**
703
775
  * Creates a transfer client without opening any provider connections.
704
776
  *
705
- * @param options - Optional registry, provider factories, and logger.
777
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
706
778
  */
707
779
  constructor(options = {}) {
708
780
  this.registry = options.registry ?? new ProviderRegistry();
709
781
  this.logger = options.logger ?? noopLogger;
782
+ if (options.defaults !== void 0) {
783
+ this.defaults = { ...options.defaults };
784
+ }
710
785
  for (const provider of options.providers ?? []) {
711
786
  this.registry.register(provider);
712
787
  }
@@ -1279,18 +1354,25 @@ var TransferEngine = class {
1279
1354
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1280
1355
  this.throwIfAborted(abortScope.signal, job);
1281
1356
  const attemptStartedAt = this.now();
1357
+ const attemptScope = createAttemptScope(
1358
+ abortScope.signal,
1359
+ options.timeout,
1360
+ job,
1361
+ attemptNumber
1362
+ );
1282
1363
  const context = this.createExecutionContext(
1283
1364
  job,
1284
1365
  attemptNumber,
1285
1366
  attemptStartedAt,
1286
1367
  options,
1287
- abortScope.signal,
1368
+ attemptScope.signal,
1288
1369
  (bytesTransferred) => {
1289
1370
  latestBytesTransferred = bytesTransferred;
1290
- }
1371
+ },
1372
+ attemptScope.notifyProgress
1291
1373
  );
1292
1374
  try {
1293
- const result = await runExecutor(executor, context, abortScope.signal, job);
1375
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1294
1376
  context.throwIfAborted();
1295
1377
  latestBytesTransferred = result.bytesTransferred;
1296
1378
  const completedAt = this.now();
@@ -1308,16 +1390,27 @@ var TransferEngine = class {
1308
1390
  summarizeError(error)
1309
1391
  );
1310
1392
  attempts.push(attempt);
1311
- if (error instanceof AbortError || error instanceof TimeoutError) {
1393
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1312
1394
  throw error;
1313
1395
  }
1314
- const retryInput = { attempt: attemptNumber, error, job };
1396
+ const retryInput = {
1397
+ attempt: attemptNumber,
1398
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1399
+ error,
1400
+ job
1401
+ };
1315
1402
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1316
1403
  if (shouldRetry) {
1317
1404
  options.retry?.onRetry?.(retryInput);
1405
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1406
+ if (delayMs > 0) {
1407
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1408
+ }
1318
1409
  continue;
1319
1410
  }
1320
1411
  throw createTransferFailure(job, error, attempts);
1412
+ } finally {
1413
+ attemptScope.dispose();
1321
1414
  }
1322
1415
  }
1323
1416
  throw createTransferFailure(job, void 0, attempts);
@@ -1325,12 +1418,13 @@ var TransferEngine = class {
1325
1418
  abortScope.dispose();
1326
1419
  }
1327
1420
  }
1328
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1421
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1329
1422
  const context = {
1330
1423
  attempt,
1331
1424
  job,
1332
1425
  reportProgress: (bytesTransferred, totalBytes) => {
1333
1426
  this.throwIfAborted(signal, job);
1427
+ notifyProgress();
1334
1428
  updateBytesTransferred(bytesTransferred);
1335
1429
  const progressInput = {
1336
1430
  bytesTransferred,
@@ -1399,6 +1493,96 @@ function createAbortScope(parentSignal, timeout, job) {
1399
1493
  signal: controller.signal
1400
1494
  };
1401
1495
  }
1496
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1497
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1498
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1499
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1500
+ const scope = {
1501
+ dispose: () => void 0,
1502
+ notifyProgress: () => void 0
1503
+ };
1504
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1505
+ return scope;
1506
+ }
1507
+ const controller = new AbortController();
1508
+ const retryable = timeout?.retryable ?? true;
1509
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1510
+ if (parentSignal?.aborted === true) {
1511
+ abortFromParent();
1512
+ } else {
1513
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1514
+ }
1515
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1516
+ controller.abort(
1517
+ new TimeoutError({
1518
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1519
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1520
+ retryable
1521
+ })
1522
+ );
1523
+ }, attemptTimeoutMs);
1524
+ let stallTimer;
1525
+ const armStallWatchdog = () => {
1526
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1527
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1528
+ stallTimer = setTimeout(() => {
1529
+ controller.abort(
1530
+ new TimeoutError({
1531
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1532
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1533
+ retryable
1534
+ })
1535
+ );
1536
+ }, stallTimeoutMs);
1537
+ };
1538
+ armStallWatchdog();
1539
+ return {
1540
+ dispose: () => {
1541
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1542
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1543
+ parentSignal?.removeEventListener("abort", abortFromParent);
1544
+ },
1545
+ notifyProgress: armStallWatchdog,
1546
+ signal: controller.signal
1547
+ };
1548
+ }
1549
+ function sleepWithAbort(delayMs, signal, job) {
1550
+ return new Promise((resolve, reject) => {
1551
+ if (signal === void 0) {
1552
+ setTimeout(resolve, delayMs);
1553
+ return;
1554
+ }
1555
+ if (signal.aborted) {
1556
+ reject(toAbortFailure(signal, job));
1557
+ return;
1558
+ }
1559
+ const rejectAbort = () => {
1560
+ clearTimeout(timer);
1561
+ reject(toAbortFailure(signal, job));
1562
+ };
1563
+ const timer = setTimeout(() => {
1564
+ signal.removeEventListener("abort", rejectAbort);
1565
+ resolve();
1566
+ }, delayMs);
1567
+ signal.addEventListener("abort", rejectAbort, { once: true });
1568
+ });
1569
+ }
1570
+ function toAbortFailure(signal, job) {
1571
+ if (signal.reason instanceof ZeroTransferError) {
1572
+ return signal.reason;
1573
+ }
1574
+ return new AbortError({
1575
+ details: { jobId: job.id, operation: job.operation },
1576
+ message: `Transfer job aborted: ${job.id}`,
1577
+ retryable: false
1578
+ });
1579
+ }
1580
+ function normalizeDelayMs(value) {
1581
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1582
+ return 0;
1583
+ }
1584
+ return Math.floor(value);
1585
+ }
1402
1586
  function normalizeTimeoutMs(value) {
1403
1587
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1404
1588
  return void 0;
@@ -1567,7 +1751,7 @@ async function runRoute(options) {
1567
1751
  const executor = createProviderTransferExecutor({
1568
1752
  resolveSession: ({ role }) => sessions.get(role)
1569
1753
  });
1570
- return await engine.execute(job, executor, buildExecuteOptions(options));
1754
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1571
1755
  } finally {
1572
1756
  if (destinationSession !== void 0) {
1573
1757
  await destinationSession.disconnect();
@@ -1604,12 +1788,14 @@ function defaultJobId(route, now) {
1604
1788
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1605
1789
  return `route:${route.id}:${timestamp.toString(36)}`;
1606
1790
  }
1607
- function buildExecuteOptions(options) {
1791
+ function buildExecuteOptions(options, client) {
1608
1792
  const execute = {};
1793
+ const retry = options.retry ?? client.defaults?.retry;
1794
+ const timeout = options.timeout ?? client.defaults?.timeout;
1609
1795
  if (options.signal !== void 0) execute.signal = options.signal;
1610
- if (options.retry !== void 0) execute.retry = options.retry;
1796
+ if (retry !== void 0) execute.retry = retry;
1611
1797
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1612
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1798
+ if (timeout !== void 0) execute.timeout = timeout;
1613
1799
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1614
1800
  return execute;
1615
1801
  }
@@ -1666,41 +1852,6 @@ function defaultRouteSuffix(source, destination) {
1666
1852
  return `${source}->${destination}`;
1667
1853
  }
1668
1854
 
1669
- // src/logging/redaction.ts
1670
- var REDACTED = "[REDACTED]";
1671
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1672
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1673
- function isSensitiveKey(key) {
1674
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1675
- }
1676
- function redactCommand(command) {
1677
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1678
- return `${commandName.toUpperCase()} ${REDACTED}`;
1679
- });
1680
- }
1681
- function redactValue(value) {
1682
- if (typeof value === "string") {
1683
- return redactCommand(value);
1684
- }
1685
- if (Array.isArray(value)) {
1686
- return value.map((item) => redactValue(item));
1687
- }
1688
- if (value !== null && typeof value === "object") {
1689
- return redactObject(value);
1690
- }
1691
- return value;
1692
- }
1693
- function redactObject(input) {
1694
- return Object.fromEntries(
1695
- Object.entries(input).map(([key, value]) => {
1696
- if (isSensitiveKey(key)) {
1697
- return [key, REDACTED];
1698
- }
1699
- return [key, redactValue(value)];
1700
- })
1701
- );
1702
- }
1703
-
1704
1855
  // src/profiles/SecretSource.ts
1705
1856
  var import_node_buffer2 = require("buffer");
1706
1857
  var import_promises = require("fs/promises");
@@ -2072,11 +2223,11 @@ var import_promises2 = require("fs/promises");
2072
2223
  var import_node_path2 = __toESM(require("path"));
2073
2224
 
2074
2225
  // src/utils/path.ts
2075
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2226
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2076
2227
  function assertSafeFtpArgument(value, label = "path") {
2077
2228
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2078
2229
  throw new ConfigurationError({
2079
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2230
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2080
2231
  retryable: false,
2081
2232
  details: {
2082
2233
  label
@@ -3378,7 +3529,6 @@ function expandAlgorithms(values) {
3378
3529
  }
3379
3530
 
3380
3531
  // src/profiles/importers/FileZillaImporter.ts
3381
- var import_node_buffer5 = require("buffer");
3382
3532
  function importFileZillaSites(xml) {
3383
3533
  const events = tokenizeXml(xml);
3384
3534
  if (events.length === 0) {
@@ -3394,7 +3544,6 @@ function importFileZillaSites(xml) {
3394
3544
  const folderNamePending = [];
3395
3545
  let inServer = false;
3396
3546
  let serverFields = {};
3397
- let serverPasswordEncoding;
3398
3547
  let activeTag;
3399
3548
  let captureFolderName = false;
3400
3549
  for (const event of events) {
@@ -3407,13 +3556,9 @@ function importFileZillaSites(xml) {
3407
3556
  if (event.name === "Server") {
3408
3557
  inServer = true;
3409
3558
  serverFields = {};
3410
- serverPasswordEncoding = void 0;
3411
3559
  continue;
3412
3560
  }
3413
3561
  activeTag = event.name;
3414
- if (event.name === "Pass" && inServer) {
3415
- serverPasswordEncoding = event.attributes["encoding"];
3416
- }
3417
3562
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
3418
3563
  captureFolderName = true;
3419
3564
  }
@@ -3439,7 +3584,7 @@ function importFileZillaSites(xml) {
3439
3584
  }
3440
3585
  if (event.name === "Server") {
3441
3586
  const folder = folderStack.filter((segment) => segment !== "");
3442
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
3587
+ const result = buildSiteFromFields(serverFields);
3443
3588
  if (result.kind === "site") {
3444
3589
  sites.push({ ...result.site, folder });
3445
3590
  } else {
@@ -3451,7 +3596,6 @@ function importFileZillaSites(xml) {
3451
3596
  }
3452
3597
  inServer = false;
3453
3598
  serverFields = {};
3454
- serverPasswordEncoding = void 0;
3455
3599
  activeTag = void 0;
3456
3600
  continue;
3457
3601
  }
@@ -3460,7 +3604,7 @@ function importFileZillaSites(xml) {
3460
3604
  }
3461
3605
  return { sites, skipped };
3462
3606
  }
3463
- function buildSiteFromFields(fields, passwordEncoding) {
3607
+ function buildSiteFromFields(fields) {
3464
3608
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
3465
3609
  const host = (fields["Host"] ?? "").trim();
3466
3610
  if (host === "") return { kind: "skipped", name };
@@ -3479,18 +3623,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
3479
3623
  }
3480
3624
  const user = fields["User"]?.trim();
3481
3625
  if (user !== void 0 && user !== "") profile.username = { value: user };
3482
- let password;
3483
3626
  const rawPass = fields["Pass"];
3484
- if (rawPass !== void 0 && rawPass !== "") {
3485
- if (passwordEncoding === "base64") {
3486
- password = import_node_buffer5.Buffer.from(rawPass, "base64").toString("utf8");
3487
- } else {
3488
- password = rawPass;
3489
- }
3490
- if (password !== void 0 && password !== "") profile.password = { value: password };
3491
- }
3492
- const site = { name, profile };
3493
- if (password !== void 0) site.password = password;
3627
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
3628
+ const site = { hasStoredPassword, name, profile };
3494
3629
  const logonText = fields["Logontype"];
3495
3630
  if (logonText !== void 0) {
3496
3631
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -3733,6 +3868,62 @@ function mapFtp550(details) {
3733
3868
  return new PermissionDeniedError(details);
3734
3869
  }
3735
3870
 
3871
+ // src/transfers/createDefaultRetryPolicy.ts
3872
+ var DEFAULT_MAX_ATTEMPTS = 4;
3873
+ var DEFAULT_BASE_DELAY_MS = 250;
3874
+ var DEFAULT_MAX_DELAY_MS = 3e4;
3875
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
3876
+ function createDefaultRetryPolicy(options = {}) {
3877
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
3878
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
3879
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
3880
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
3881
+ const random = options.random ?? Math.random;
3882
+ return {
3883
+ getDelayMs(input) {
3884
+ const retryAfterMs = readRetryAfterMs(input.error);
3885
+ if (retryAfterMs !== void 0) {
3886
+ return retryAfterMs;
3887
+ }
3888
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
3889
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
3890
+ return Math.floor(random() * cappedMs);
3891
+ },
3892
+ maxAttempts,
3893
+ shouldRetry(input) {
3894
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
3895
+ return false;
3896
+ }
3897
+ if (input.elapsedMs >= maxElapsedMs) {
3898
+ return false;
3899
+ }
3900
+ const retryAfterMs = readRetryAfterMs(input.error);
3901
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
3902
+ return false;
3903
+ }
3904
+ return true;
3905
+ }
3906
+ };
3907
+ }
3908
+ function readRetryAfterMs(error) {
3909
+ if (!(error instanceof ZeroTransferError)) return void 0;
3910
+ const value = error.details?.["retryAfterMs"];
3911
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
3912
+ return Math.floor(value);
3913
+ }
3914
+ function normalizePositiveInteger(value, fallback) {
3915
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
3916
+ return fallback;
3917
+ }
3918
+ return Math.floor(value);
3919
+ }
3920
+ function normalizeNonNegative(value, fallback) {
3921
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
3922
+ return fallback;
3923
+ }
3924
+ return Math.floor(value);
3925
+ }
3926
+
3736
3927
  // src/transfers/TransferPlan.ts
3737
3928
  function createTransferPlan(input) {
3738
3929
  const plan = {
@@ -3830,8 +4021,8 @@ var TransferQueue = class {
3830
4021
  this.concurrency = normalizeConcurrency(options.concurrency);
3831
4022
  this.defaultExecutor = options.executor;
3832
4023
  this.resolveExecutor = options.resolveExecutor;
3833
- this.retry = options.retry;
3834
- this.timeout = options.timeout;
4024
+ this.retry = options.retry ?? options.client?.defaults?.retry;
4025
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
3835
4026
  this.bandwidthLimit = options.bandwidthLimit;
3836
4027
  this.onProgress = options.onProgress;
3837
4028
  this.onReceipt = options.onReceipt;
@@ -4914,11 +5105,12 @@ var import_node_path3 = require("path");
4914
5105
 
4915
5106
  // src/providers/web/awsSigv4.ts
4916
5107
  var import_node_crypto2 = require("crypto");
5108
+ var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
4917
5109
  function signSigV4(input) {
4918
5110
  const now = input.now ?? /* @__PURE__ */ new Date();
4919
5111
  const amzDate = formatAmzDate(now);
4920
5112
  const dateStamp = amzDate.slice(0, 8);
4921
- const payloadHash = input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
5113
+ const payloadHash = input.unsignedPayload === true ? UNSIGNED_PAYLOAD : input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
4922
5114
  input.headers["host"] = input.url.host;
4923
5115
  input.headers["x-amz-date"] = amzDate;
4924
5116
  input.headers["x-amz-content-sha256"] = payloadHash;
@@ -4993,7 +5185,7 @@ function hmacHex(key, data) {
4993
5185
  }
4994
5186
 
4995
5187
  // src/providers/web/httpInternals.ts
4996
- var import_node_buffer6 = require("buffer");
5188
+ var import_node_buffer5 = require("buffer");
4997
5189
  function parseContentRangeTotal(value) {
4998
5190
  const match = /\/(\d+)\s*$/.exec(value);
4999
5191
  if (match === null) return void 0;
@@ -5019,8 +5211,48 @@ function formatRangeHeader(offset, length) {
5019
5211
  const end = offset + length - 1;
5020
5212
  return `bytes=${String(offset)}-${String(end)}`;
5021
5213
  }
5022
- function mapResponseError(response, path2) {
5023
- const details = { path: path2, status: response.status, statusText: response.statusText };
5214
+ var ERROR_BODY_EXCERPT_LIMIT = 2048;
5215
+ async function readErrorBodyExcerpt(response) {
5216
+ try {
5217
+ const text = await response.text();
5218
+ if (text.length === 0) return void 0;
5219
+ return text.length > ERROR_BODY_EXCERPT_LIMIT ? `${text.slice(0, ERROR_BODY_EXCERPT_LIMIT)}... [truncated]` : text;
5220
+ } catch {
5221
+ return void 0;
5222
+ }
5223
+ }
5224
+ function parseRetryAfterMs(value, now = Date.now) {
5225
+ if (value === null) return void 0;
5226
+ const trimmed = value.trim();
5227
+ if (trimmed.length === 0) return void 0;
5228
+ if (/^\d+$/.test(trimmed)) {
5229
+ const seconds = Number.parseInt(trimmed, 10);
5230
+ return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
5231
+ }
5232
+ if (!/[A-Za-z]/.test(trimmed)) return void 0;
5233
+ const retryAt = Date.parse(trimmed);
5234
+ if (Number.isNaN(retryAt)) return void 0;
5235
+ return Math.max(0, retryAt - now());
5236
+ }
5237
+ async function mapResponseErrorWithBody(response, path2) {
5238
+ return mapResponseError(response, path2, await readErrorBodyExcerpt(response));
5239
+ }
5240
+ function mapResponseError(response, path2, bodyExcerpt) {
5241
+ const details = {
5242
+ path: path2,
5243
+ status: response.status,
5244
+ statusText: response.statusText
5245
+ };
5246
+ if (bodyExcerpt !== void 0) details["body"] = bodyExcerpt;
5247
+ if (response.status === 429 || response.status === 503) {
5248
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
5249
+ if (retryAfterMs !== void 0) details["retryAfterMs"] = retryAfterMs;
5250
+ return new ConnectionError({
5251
+ details,
5252
+ message: response.status === 429 ? `HTTP request for ${path2} was rate limited (429)` : `HTTP service unavailable for ${path2} (503)`,
5253
+ retryable: true
5254
+ });
5255
+ }
5024
5256
  if (response.status === 401) {
5025
5257
  return new AuthenticationError({
5026
5258
  details,
@@ -5052,18 +5284,58 @@ async function* webStreamToAsyncIterable(body) {
5052
5284
  const reader = body.getReader();
5053
5285
  try {
5054
5286
  while (true) {
5055
- const { done, value } = await reader.read();
5056
- if (done) break;
5057
- if (value !== void 0) yield value;
5287
+ let result;
5288
+ try {
5289
+ result = await reader.read();
5290
+ } catch (error) {
5291
+ if (error instanceof ZeroTransferError) throw error;
5292
+ throw new ConnectionError({
5293
+ cause: error,
5294
+ message: "HTTP response stream was interrupted before completion",
5295
+ retryable: true
5296
+ });
5297
+ }
5298
+ if (result.done) break;
5299
+ if (result.value !== void 0) yield result.value;
5058
5300
  }
5059
5301
  } finally {
5060
5302
  reader.releaseLock();
5061
5303
  }
5062
5304
  }
5305
+ function asyncIterableToReadableStream(source, onChunk) {
5306
+ const iterator = source[Symbol.asyncIterator]();
5307
+ return new ReadableStream({
5308
+ async pull(controller) {
5309
+ try {
5310
+ const next = await iterator.next();
5311
+ if (next.done === true) {
5312
+ controller.close();
5313
+ return;
5314
+ }
5315
+ const chunk = next.value;
5316
+ if (chunk.byteLength === 0) {
5317
+ return;
5318
+ }
5319
+ controller.enqueue(chunk);
5320
+ onChunk(chunk);
5321
+ } catch (error) {
5322
+ controller.error(error);
5323
+ }
5324
+ },
5325
+ async cancel(reason) {
5326
+ if (typeof iterator.return === "function") {
5327
+ try {
5328
+ await iterator.return(reason);
5329
+ } catch {
5330
+ }
5331
+ }
5332
+ }
5333
+ });
5334
+ }
5063
5335
  function secretToString(value) {
5064
5336
  if (typeof value === "string") return value;
5065
- if (value instanceof Uint8Array || import_node_buffer6.Buffer.isBuffer(value)) {
5066
- return import_node_buffer6.Buffer.from(value).toString("utf8");
5337
+ if (value instanceof Uint8Array || import_node_buffer5.Buffer.isBuffer(value)) {
5338
+ return import_node_buffer5.Buffer.from(value).toString("utf8");
5067
5339
  }
5068
5340
  return String(value);
5069
5341
  }
@@ -5275,7 +5547,7 @@ var S3FileSystem = class {
5275
5547
  url.searchParams.set("delimiter", "/");
5276
5548
  if (prefix.length > 0) url.searchParams.set("prefix", prefix);
5277
5549
  const response = await s3Fetch(this.options, "GET", url);
5278
- if (!response.ok) throw mapResponseError(response, normalized);
5550
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
5279
5551
  const body = await response.text();
5280
5552
  return parseListObjectsV2(body, prefix);
5281
5553
  }
@@ -5283,7 +5555,7 @@ var S3FileSystem = class {
5283
5555
  const normalized = normalizeRemotePath(path2);
5284
5556
  const url = buildObjectUrl(this.options, normalized);
5285
5557
  const response = await s3Fetch(this.options, "HEAD", url);
5286
- if (!response.ok) throw mapResponseError(response, normalized);
5558
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
5287
5559
  const stat = {
5288
5560
  exists: true,
5289
5561
  name: basenameRemotePath(normalized),
@@ -5323,12 +5595,12 @@ var S3TransferOperations = class {
5323
5595
  extraHeaders: headers
5324
5596
  });
5325
5597
  if (!response.ok && response.status !== 206) {
5326
- throw mapResponseError(response, normalized);
5598
+ throw await mapResponseErrorWithBody(response, normalized);
5327
5599
  }
5328
5600
  const body = response.body;
5329
5601
  if (body === null) {
5330
5602
  throw new ConnectionError({
5331
- message: `S3 response had no body for ${url.toString()}`,
5603
+ message: `S3 response had no body for ${redactUrlForLogging(url)}`,
5332
5604
  retryable: true
5333
5605
  });
5334
5606
  }
@@ -5364,19 +5636,33 @@ var S3TransferOperations = class {
5364
5636
  }
5365
5637
  return this.writeSingleShot(request, normalized);
5366
5638
  }
5639
+ /**
5640
+ * Single PUT upload used when multipart is disabled. Streams the body with
5641
+ * a declared `Content-Length` (signed as `UNSIGNED-PAYLOAD`) when the
5642
+ * caller knows the total size; S3 requires a length up front, so only
5643
+ * unknown-size payloads fall back to buffering the content in memory.
5644
+ */
5367
5645
  async writeSingleShot(request, normalized) {
5368
5646
  const url = buildObjectUrl(this.options, normalized);
5369
- const buffered = await collectChunks(request.content);
5647
+ const totalBytes = request.totalBytes;
5648
+ if (typeof totalBytes !== "number" || totalBytes < 0) {
5649
+ const buffered = await collectChunks(request.content);
5650
+ return this.singleShotFromBuffer(request, normalized, buffered);
5651
+ }
5652
+ let bytesTransferred = 0;
5653
+ const stream = asyncIterableToReadableStream(request.content, (chunk) => {
5654
+ bytesTransferred += chunk.byteLength;
5655
+ request.reportProgress(bytesTransferred, totalBytes);
5656
+ });
5370
5657
  const response = await s3Fetch(this.options, "PUT", url, {
5371
5658
  ...request.signal !== void 0 ? { signal: request.signal } : {},
5372
- body: buffered,
5373
- extraHeaders: { "content-type": "application/octet-stream" }
5659
+ extraHeaders: { "content-type": "application/octet-stream" },
5660
+ streamBody: { content: stream, contentLength: totalBytes }
5374
5661
  });
5375
- if (!response.ok) throw mapResponseError(response, normalized);
5376
- request.reportProgress(buffered.byteLength, buffered.byteLength);
5662
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
5377
5663
  const result = {
5378
- bytesTransferred: buffered.byteLength,
5379
- totalBytes: buffered.byteLength
5664
+ bytesTransferred,
5665
+ totalBytes
5380
5666
  };
5381
5667
  const etag = response.headers.get("etag");
5382
5668
  if (etag !== null) result.checksum = etag;
@@ -5440,7 +5726,7 @@ var S3TransferOperations = class {
5440
5726
  ...request.signal !== void 0 ? { signal: request.signal } : {},
5441
5727
  extraHeaders: { "content-type": "application/octet-stream" }
5442
5728
  });
5443
- if (!initiateResponse.ok) throw mapResponseError(initiateResponse, normalized);
5729
+ if (!initiateResponse.ok) throw await mapResponseErrorWithBody(initiateResponse, normalized);
5444
5730
  const initiateBody = await initiateResponse.text();
5445
5731
  const initiated = innerText(initiateBody, "UploadId");
5446
5732
  if (initiated === void 0 || initiated === "") {
@@ -5479,7 +5765,7 @@ var S3TransferOperations = class {
5479
5765
  body: partBytes.bytes
5480
5766
  });
5481
5767
  if (!partResponse.ok) {
5482
- throw mapResponseError(partResponse, normalized);
5768
+ throw await mapResponseErrorWithBody(partResponse, normalized);
5483
5769
  }
5484
5770
  const partEtag = partResponse.headers.get("etag");
5485
5771
  if (partEtag === null) {
@@ -5535,7 +5821,7 @@ var S3TransferOperations = class {
5535
5821
  if (resumeStore === void 0) {
5536
5822
  await abortMultipart(this.options, objectUrl, uploadId).catch(() => void 0);
5537
5823
  }
5538
- throw mapResponseError(completeResponse, normalized);
5824
+ throw await mapResponseErrorWithBody(completeResponse, normalized);
5539
5825
  }
5540
5826
  if (resumeStore !== void 0) await resumeStore.clear(resumeKey);
5541
5827
  const completeBody = await completeResponse.text();
@@ -5554,7 +5840,7 @@ var S3TransferOperations = class {
5554
5840
  body: buffered,
5555
5841
  extraHeaders: { "content-type": "application/octet-stream" }
5556
5842
  });
5557
- if (!response.ok) throw mapResponseError(response, normalized);
5843
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
5558
5844
  request.reportProgress(buffered.byteLength, buffered.byteLength);
5559
5845
  const result = {
5560
5846
  bytesTransferred: buffered.byteLength,
@@ -5572,6 +5858,8 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
5572
5858
  };
5573
5859
  if (fetchOptions.body !== void 0) {
5574
5860
  headers["content-length"] = String(fetchOptions.body.byteLength);
5861
+ } else if (fetchOptions.streamBody !== void 0) {
5862
+ headers["content-length"] = String(fetchOptions.streamBody.contentLength);
5575
5863
  }
5576
5864
  signSigV4({
5577
5865
  accessKeyId: options.accessKeyId,
@@ -5582,10 +5870,16 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
5582
5870
  service: options.service,
5583
5871
  url,
5584
5872
  ...fetchOptions.body !== void 0 ? { body: fetchOptions.body } : {},
5873
+ ...fetchOptions.streamBody !== void 0 ? { unsignedPayload: true } : {},
5585
5874
  ...options.sessionToken !== void 0 ? { sessionToken: options.sessionToken } : {}
5586
5875
  });
5587
5876
  const init = { headers, method };
5588
- if (fetchOptions.body !== void 0) init.body = fetchOptions.body;
5877
+ if (fetchOptions.body !== void 0) {
5878
+ init.body = fetchOptions.body;
5879
+ } else if (fetchOptions.streamBody !== void 0) {
5880
+ init.body = fetchOptions.streamBody.content;
5881
+ init.duplex = "half";
5882
+ }
5589
5883
  if (fetchOptions.signal !== void 0) init.signal = fetchOptions.signal;
5590
5884
  const controller = new AbortController();
5591
5885
  const upstreamSignal = init.signal ?? null;
@@ -5603,10 +5897,11 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
5603
5897
  try {
5604
5898
  return await options.fetch(url.toString(), { ...init, signal: controller.signal });
5605
5899
  } catch (error) {
5900
+ const safeUrl = redactUrlForLogging(url);
5606
5901
  throw new ConnectionError({
5607
5902
  cause: error,
5608
- details: { url: url.toString() },
5609
- message: `S3 request to ${url.toString()} failed`,
5903
+ details: { url: safeUrl },
5904
+ message: `S3 request to ${safeUrl} failed`,
5610
5905
  retryable: true
5611
5906
  });
5612
5907
  } finally {
@@ -5779,6 +6074,7 @@ function innerText(xml, tag) {
5779
6074
  copyBetween,
5780
6075
  createAtomicDeployPlan,
5781
6076
  createBandwidthThrottle,
6077
+ createDefaultRetryPolicy,
5782
6078
  createFileSystemS3MultipartResumeStore,
5783
6079
  createLocalProviderFactory,
5784
6080
  createMemoryProviderFactory,
@@ -5817,8 +6113,10 @@ function innerText(xml, tag) {
5817
6113
  parseRemoteManifest,
5818
6114
  redactCommand,
5819
6115
  redactConnectionProfile,
6116
+ redactErrorForLogging,
5820
6117
  redactObject,
5821
6118
  redactSecretSource,
6119
+ redactUrlForLogging,
5822
6120
  redactValue,
5823
6121
  resolveConnectionProfileSecrets,
5824
6122
  resolveOpenSshHost,