@zero-transfer/webdav 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -60,6 +60,7 @@ __export(webdav_exports, {
60
60
  copyBetween: () => copyBetween,
61
61
  createAtomicDeployPlan: () => createAtomicDeployPlan,
62
62
  createBandwidthThrottle: () => createBandwidthThrottle,
63
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
63
64
  createLocalProviderFactory: () => createLocalProviderFactory,
64
65
  createMemoryProviderFactory: () => createMemoryProviderFactory,
65
66
  createOAuthTokenSecretSource: () => createOAuthTokenSecretSource,
@@ -96,8 +97,10 @@ __export(webdav_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(webdav_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,10 +5097,30 @@ function isMainModule(importMetaUrl) {
4906
5097
  }
4907
5098
 
4908
5099
  // src/providers/web/WebDavProvider.ts
4909
- var import_node_buffer7 = require("buffer");
5100
+ var import_node_buffer6 = require("buffer");
4910
5101
 
4911
5102
  // src/providers/web/httpInternals.ts
4912
- var import_node_buffer6 = require("buffer");
5103
+ var import_node_buffer5 = require("buffer");
5104
+ function assertHttpsEnforced(input) {
5105
+ if (input.enforceHttps && !input.secure) {
5106
+ throw new ConfigurationError({
5107
+ details: { provider: input.providerId },
5108
+ message: `Provider "${input.providerId}" is configured with enforceHttps but its transport is cleartext http; set secure: true (or drop enforceHttps to explicitly accept cleartext)`,
5109
+ retryable: false
5110
+ });
5111
+ }
5112
+ }
5113
+ var cleartextWarnedKeys = /* @__PURE__ */ new Set();
5114
+ function warnCleartextCredentials(input) {
5115
+ if (!input.hasCredentials) return;
5116
+ const key = `${input.providerId}:${input.host}`;
5117
+ if (cleartextWarnedKeys.has(key)) return;
5118
+ cleartextWarnedKeys.add(key);
5119
+ process.emitWarning(
5120
+ `Provider "${input.providerId}" is sending credentials to ${input.host} over cleartext http; use https or set enforceHttps to block this`,
5121
+ { code: "ZERO_TRANSFER_CLEARTEXT_CREDENTIALS", type: "SecurityWarning" }
5122
+ );
5123
+ }
4913
5124
  function buildBaseUrl(profile, options) {
4914
5125
  const protocol = options.secure ? "https:" : "http:";
4915
5126
  const portSegment = profile.port !== void 0 ? `:${profile.port}` : "";
@@ -4957,18 +5168,19 @@ async function dispatchRequest(options, url, init) {
4957
5168
  signal: controller.signal
4958
5169
  });
4959
5170
  } catch (error) {
5171
+ const safeUrl = redactUrlForLogging(url);
4960
5172
  if (controller.signal.aborted && upstreamSignal?.aborted !== true) {
4961
5173
  throw new TimeoutError({
4962
5174
  cause: error,
4963
- details: { timeoutMs: options.timeoutMs, url: url.toString() },
4964
- message: `HTTP request to ${url.toString()} timed out after ${String(options.timeoutMs)}ms`,
5175
+ details: { timeoutMs: options.timeoutMs, url: safeUrl },
5176
+ message: `HTTP request to ${safeUrl} timed out after ${String(options.timeoutMs)}ms`,
4965
5177
  retryable: true
4966
5178
  });
4967
5179
  }
4968
5180
  throw new ConnectionError({
4969
5181
  cause: error,
4970
- details: { url: url.toString() },
4971
- message: `HTTP request to ${url.toString()} failed`,
5182
+ details: { url: safeUrl },
5183
+ message: `HTTP request to ${safeUrl} failed`,
4972
5184
  retryable: true
4973
5185
  });
4974
5186
  } finally {
@@ -5000,8 +5212,48 @@ function formatRangeHeader(offset, length) {
5000
5212
  const end = offset + length - 1;
5001
5213
  return `bytes=${String(offset)}-${String(end)}`;
5002
5214
  }
5003
- function mapResponseError(response, path2) {
5004
- const details = { path: path2, status: response.status, statusText: response.statusText };
5215
+ var ERROR_BODY_EXCERPT_LIMIT = 2048;
5216
+ async function readErrorBodyExcerpt(response) {
5217
+ try {
5218
+ const text = await response.text();
5219
+ if (text.length === 0) return void 0;
5220
+ return text.length > ERROR_BODY_EXCERPT_LIMIT ? `${text.slice(0, ERROR_BODY_EXCERPT_LIMIT)}... [truncated]` : text;
5221
+ } catch {
5222
+ return void 0;
5223
+ }
5224
+ }
5225
+ function parseRetryAfterMs(value, now = Date.now) {
5226
+ if (value === null) return void 0;
5227
+ const trimmed = value.trim();
5228
+ if (trimmed.length === 0) return void 0;
5229
+ if (/^\d+$/.test(trimmed)) {
5230
+ const seconds = Number.parseInt(trimmed, 10);
5231
+ return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
5232
+ }
5233
+ if (!/[A-Za-z]/.test(trimmed)) return void 0;
5234
+ const retryAt = Date.parse(trimmed);
5235
+ if (Number.isNaN(retryAt)) return void 0;
5236
+ return Math.max(0, retryAt - now());
5237
+ }
5238
+ async function mapResponseErrorWithBody(response, path2) {
5239
+ return mapResponseError(response, path2, await readErrorBodyExcerpt(response));
5240
+ }
5241
+ function mapResponseError(response, path2, bodyExcerpt) {
5242
+ const details = {
5243
+ path: path2,
5244
+ status: response.status,
5245
+ statusText: response.statusText
5246
+ };
5247
+ if (bodyExcerpt !== void 0) details["body"] = bodyExcerpt;
5248
+ if (response.status === 429 || response.status === 503) {
5249
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
5250
+ if (retryAfterMs !== void 0) details["retryAfterMs"] = retryAfterMs;
5251
+ return new ConnectionError({
5252
+ details,
5253
+ message: response.status === 429 ? `HTTP request for ${path2} was rate limited (429)` : `HTTP service unavailable for ${path2} (503)`,
5254
+ retryable: true
5255
+ });
5256
+ }
5005
5257
  if (response.status === 401) {
5006
5258
  return new AuthenticationError({
5007
5259
  details,
@@ -5033,18 +5285,58 @@ async function* webStreamToAsyncIterable(body) {
5033
5285
  const reader = body.getReader();
5034
5286
  try {
5035
5287
  while (true) {
5036
- const { done, value } = await reader.read();
5037
- if (done) break;
5038
- if (value !== void 0) yield value;
5288
+ let result;
5289
+ try {
5290
+ result = await reader.read();
5291
+ } catch (error) {
5292
+ if (error instanceof ZeroTransferError) throw error;
5293
+ throw new ConnectionError({
5294
+ cause: error,
5295
+ message: "HTTP response stream was interrupted before completion",
5296
+ retryable: true
5297
+ });
5298
+ }
5299
+ if (result.done) break;
5300
+ if (result.value !== void 0) yield result.value;
5039
5301
  }
5040
5302
  } finally {
5041
5303
  reader.releaseLock();
5042
5304
  }
5043
5305
  }
5306
+ function asyncIterableToReadableStream(source, onChunk) {
5307
+ const iterator = source[Symbol.asyncIterator]();
5308
+ return new ReadableStream({
5309
+ async pull(controller) {
5310
+ try {
5311
+ const next = await iterator.next();
5312
+ if (next.done === true) {
5313
+ controller.close();
5314
+ return;
5315
+ }
5316
+ const chunk = next.value;
5317
+ if (chunk.byteLength === 0) {
5318
+ return;
5319
+ }
5320
+ controller.enqueue(chunk);
5321
+ onChunk(chunk);
5322
+ } catch (error) {
5323
+ controller.error(error);
5324
+ }
5325
+ },
5326
+ async cancel(reason) {
5327
+ if (typeof iterator.return === "function") {
5328
+ try {
5329
+ await iterator.return(reason);
5330
+ } catch {
5331
+ }
5332
+ }
5333
+ }
5334
+ });
5335
+ }
5044
5336
  function secretToString(value) {
5045
5337
  if (typeof value === "string") return value;
5046
- if (value instanceof Uint8Array || import_node_buffer6.Buffer.isBuffer(value)) {
5047
- return import_node_buffer6.Buffer.from(value).toString("utf8");
5338
+ if (value instanceof Uint8Array || import_node_buffer5.Buffer.isBuffer(value)) {
5339
+ return import_node_buffer5.Buffer.from(value).toString("utf8");
5048
5340
  }
5049
5341
  return String(value);
5050
5342
  }
@@ -5056,7 +5348,8 @@ function createWebDavProviderFactory(options = {}) {
5056
5348
  const secure = options.secure ?? false;
5057
5349
  const basePath = options.basePath ?? "";
5058
5350
  const fetchImpl = options.fetch ?? globalThis.fetch;
5059
- const uploadStreaming = options.uploadStreaming ?? "when-known-size";
5351
+ const uploadStreaming = options.uploadStreaming ?? "always";
5352
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
5060
5353
  if (typeof fetchImpl !== "function") {
5061
5354
  throw new ConfigurationError({
5062
5355
  message: "Global fetch is unavailable; supply WebDavProviderOptions.fetch explicitly",
@@ -5108,13 +5401,20 @@ var WebDavProvider = class {
5108
5401
  id;
5109
5402
  capabilities;
5110
5403
  async connect(profile) {
5404
+ if (!this.internals.secure) {
5405
+ warnCleartextCredentials({
5406
+ hasCredentials: profile.username !== void 0 || profile.password !== void 0,
5407
+ host: profile.host,
5408
+ providerId: this.internals.id
5409
+ });
5410
+ }
5111
5411
  const headers = { ...this.internals.defaultHeaders };
5112
5412
  if (profile.username !== void 0) {
5113
5413
  const username = await resolveSecret(profile.username);
5114
5414
  const password = profile.password !== void 0 ? await resolveSecret(profile.password) : "";
5115
5415
  const usernameText = secretToString(username);
5116
5416
  const passwordText = secretToString(password);
5117
- headers["Authorization"] = `Basic ${import_node_buffer7.Buffer.from(`${usernameText}:${passwordText}`).toString(
5417
+ headers["Authorization"] = `Basic ${import_node_buffer6.Buffer.from(`${usernameText}:${passwordText}`).toString(
5118
5418
  "base64"
5119
5419
  )}`;
5120
5420
  }
@@ -5162,7 +5462,7 @@ var WebDavFileSystem = class {
5162
5462
  method: "PROPFIND"
5163
5463
  });
5164
5464
  if (!response.ok && response.status !== 207) {
5165
- throw mapResponseError(response, normalized);
5465
+ throw await mapResponseErrorWithBody(response, normalized);
5166
5466
  }
5167
5467
  const body = await response.text();
5168
5468
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -5176,7 +5476,7 @@ var WebDavFileSystem = class {
5176
5476
  method: "PROPFIND"
5177
5477
  });
5178
5478
  if (!response.ok && response.status !== 207) {
5179
- throw mapResponseError(response, normalized);
5479
+ throw await mapResponseErrorWithBody(response, normalized);
5180
5480
  }
5181
5481
  const body = await response.text();
5182
5482
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -5221,12 +5521,12 @@ var WebDavTransferOperations = class {
5221
5521
  if (request.signal !== void 0) init.signal = request.signal;
5222
5522
  const response = await dispatchRequest(this.options, url, init);
5223
5523
  if (!response.ok && response.status !== 206) {
5224
- throw mapResponseError(response, normalized);
5524
+ throw await mapResponseErrorWithBody(response, normalized);
5225
5525
  }
5226
5526
  const body = response.body;
5227
5527
  if (body === null) {
5228
5528
  throw new ConnectionError({
5229
- message: `WebDAV response had no body for ${url.toString()}`,
5529
+ message: `WebDAV response had no body for ${redactUrlForLogging(url)}`,
5230
5530
  retryable: true
5231
5531
  });
5232
5532
  }
@@ -5270,7 +5570,7 @@ var WebDavTransferOperations = class {
5270
5570
  if (request.signal !== void 0) init2.signal = request.signal;
5271
5571
  const response2 = await dispatchRequest(this.options, url, init2);
5272
5572
  if (!response2.ok) {
5273
- throw mapResponseError(response2, normalized);
5573
+ throw await mapResponseErrorWithBody(response2, normalized);
5274
5574
  }
5275
5575
  request.reportProgress(buffered.byteLength, buffered.byteLength);
5276
5576
  const result2 = {
@@ -5306,7 +5606,7 @@ var WebDavTransferOperations = class {
5306
5606
  if (request.signal !== void 0) init.signal = request.signal;
5307
5607
  const response = await dispatchRequest(this.options, url, init);
5308
5608
  if (!response.ok) {
5309
- throw mapResponseError(response, normalized);
5609
+ throw await mapResponseErrorWithBody(response, normalized);
5310
5610
  }
5311
5611
  const result = {
5312
5612
  bytesTransferred,
@@ -5332,36 +5632,6 @@ async function collectChunks(source) {
5332
5632
  }
5333
5633
  return out;
5334
5634
  }
5335
- function asyncIterableToReadableStream(source, onChunk) {
5336
- const iterator = source[Symbol.asyncIterator]();
5337
- return new ReadableStream({
5338
- async pull(controller) {
5339
- try {
5340
- const next = await iterator.next();
5341
- if (next.done === true) {
5342
- controller.close();
5343
- return;
5344
- }
5345
- const chunk = next.value;
5346
- if (chunk.byteLength === 0) {
5347
- return;
5348
- }
5349
- controller.enqueue(chunk);
5350
- onChunk(chunk);
5351
- } catch (error) {
5352
- controller.error(error);
5353
- }
5354
- },
5355
- async cancel(reason) {
5356
- if (typeof iterator.return === "function") {
5357
- try {
5358
- await iterator.return(reason);
5359
- } catch {
5360
- }
5361
- }
5362
- }
5363
- });
5364
- }
5365
5635
  function parsePropfindResponses(xml, baseUrl) {
5366
5636
  const entries = [];
5367
5637
  const responseRegex = /<(?:[a-zA-Z0-9-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9-]+:)?response>/gi;
@@ -5472,6 +5742,7 @@ function parentOf(path2) {
5472
5742
  copyBetween,
5473
5743
  createAtomicDeployPlan,
5474
5744
  createBandwidthThrottle,
5745
+ createDefaultRetryPolicy,
5475
5746
  createLocalProviderFactory,
5476
5747
  createMemoryProviderFactory,
5477
5748
  createOAuthTokenSecretSource,
@@ -5508,8 +5779,10 @@ function parentOf(path2) {
5508
5779
  parseRemoteManifest,
5509
5780
  redactCommand,
5510
5781
  redactConnectionProfile,
5782
+ redactErrorForLogging,
5511
5783
  redactObject,
5512
5784
  redactSecretSource,
5785
+ redactUrlForLogging,
5513
5786
  redactValue,
5514
5787
  resolveConnectionProfileSecrets,
5515
5788
  resolveOpenSshHost,