@zero-transfer/sdk 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.mjs CHANGED
@@ -1,6 +1,68 @@
1
1
  // src/client/ZeroTransfer.ts
2
2
  import { EventEmitter } from "events";
3
3
 
4
+ // src/logging/redaction.ts
5
+ var REDACTED = "[REDACTED]";
6
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
7
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
8
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
9
+ function isSensitiveKey(key) {
10
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
11
+ }
12
+ function redactCommand(command) {
13
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
14
+ return `${commandName.toUpperCase()} ${REDACTED}`;
15
+ });
16
+ }
17
+ function redactValue(value) {
18
+ if (typeof value === "string") {
19
+ return redactCommand(value);
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return value.map((item) => redactValue(item));
23
+ }
24
+ if (value !== null && typeof value === "object") {
25
+ return redactObject(value);
26
+ }
27
+ return value;
28
+ }
29
+ function redactObject(input) {
30
+ return Object.fromEntries(
31
+ Object.entries(input).map(([key, value]) => {
32
+ if (isSensitiveKey(key)) {
33
+ return [key, REDACTED];
34
+ }
35
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
36
+ return [key, redactUrlForLogging(value)];
37
+ }
38
+ return [key, redactValue(value)];
39
+ })
40
+ );
41
+ }
42
+ function redactUrlForLogging(url) {
43
+ let parsed;
44
+ try {
45
+ parsed = typeof url === "string" ? new URL(url) : url;
46
+ } catch {
47
+ return REDACTED;
48
+ }
49
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
50
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
51
+ return `${origin}${parsed.pathname}${query}`;
52
+ }
53
+ function redactErrorForLogging(error) {
54
+ if (error !== null && typeof error === "object") {
55
+ const candidate = error;
56
+ if (typeof candidate.toJSON === "function") {
57
+ return redactObject(candidate.toJSON());
58
+ }
59
+ }
60
+ if (error instanceof Error) {
61
+ return redactObject({ message: error.message, name: error.name });
62
+ }
63
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
64
+ }
65
+
4
66
  // src/errors/ZeroTransferError.ts
5
67
  var ZeroTransferError = class extends Error {
6
68
  /** Stable machine-readable error code. */
@@ -42,6 +104,11 @@ var ZeroTransferError = class extends Error {
42
104
  /**
43
105
  * Serializes the error into a plain object suitable for logs or API responses.
44
106
  *
107
+ * `details` and `command` are passed through secret redaction so serialized
108
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
109
+ * live {@link ZeroTransferError.details | details} property stays unredacted
110
+ * for programmatic consumers.
111
+ *
45
112
  * @returns A JSON-safe object containing public structured error fields.
46
113
  */
47
114
  toJSON() {
@@ -51,12 +118,12 @@ var ZeroTransferError = class extends Error {
51
118
  message: this.message,
52
119
  protocol: this.protocol,
53
120
  host: this.host,
54
- command: this.command,
121
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
55
122
  ftpCode: this.ftpCode,
56
123
  sftpCode: this.sftpCode,
57
124
  path: this.path,
58
125
  retryable: this.retryable,
59
- details: this.details
126
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
60
127
  };
61
128
  }
62
129
  };
@@ -579,15 +646,20 @@ var ProviderRegistry = class {
579
646
  var TransferClient = class {
580
647
  /** Provider registry used by this client. */
581
648
  registry;
649
+ /** Execution defaults applied when call sites omit their own values. */
650
+ defaults;
582
651
  logger;
583
652
  /**
584
653
  * Creates a transfer client without opening any provider connections.
585
654
  *
586
- * @param options - Optional registry, provider factories, and logger.
655
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
587
656
  */
588
657
  constructor(options = {}) {
589
658
  this.registry = options.registry ?? new ProviderRegistry();
590
659
  this.logger = options.logger ?? noopLogger;
660
+ if (options.defaults !== void 0) {
661
+ this.defaults = { ...options.defaults };
662
+ }
591
663
  for (const provider of options.providers ?? []) {
592
664
  this.registry.register(provider);
593
665
  }
@@ -1160,18 +1232,25 @@ var TransferEngine = class {
1160
1232
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1161
1233
  this.throwIfAborted(abortScope.signal, job);
1162
1234
  const attemptStartedAt = this.now();
1235
+ const attemptScope = createAttemptScope(
1236
+ abortScope.signal,
1237
+ options.timeout,
1238
+ job,
1239
+ attemptNumber
1240
+ );
1163
1241
  const context = this.createExecutionContext(
1164
1242
  job,
1165
1243
  attemptNumber,
1166
1244
  attemptStartedAt,
1167
1245
  options,
1168
- abortScope.signal,
1246
+ attemptScope.signal,
1169
1247
  (bytesTransferred) => {
1170
1248
  latestBytesTransferred = bytesTransferred;
1171
- }
1249
+ },
1250
+ attemptScope.notifyProgress
1172
1251
  );
1173
1252
  try {
1174
- const result = await runExecutor(executor, context, abortScope.signal, job);
1253
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1175
1254
  context.throwIfAborted();
1176
1255
  latestBytesTransferred = result.bytesTransferred;
1177
1256
  const completedAt = this.now();
@@ -1189,16 +1268,27 @@ var TransferEngine = class {
1189
1268
  summarizeError(error)
1190
1269
  );
1191
1270
  attempts.push(attempt);
1192
- if (error instanceof AbortError || error instanceof TimeoutError) {
1271
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1193
1272
  throw error;
1194
1273
  }
1195
- const retryInput = { attempt: attemptNumber, error, job };
1274
+ const retryInput = {
1275
+ attempt: attemptNumber,
1276
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1277
+ error,
1278
+ job
1279
+ };
1196
1280
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1197
1281
  if (shouldRetry) {
1198
1282
  options.retry?.onRetry?.(retryInput);
1283
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1284
+ if (delayMs > 0) {
1285
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1286
+ }
1199
1287
  continue;
1200
1288
  }
1201
1289
  throw createTransferFailure(job, error, attempts);
1290
+ } finally {
1291
+ attemptScope.dispose();
1202
1292
  }
1203
1293
  }
1204
1294
  throw createTransferFailure(job, void 0, attempts);
@@ -1206,12 +1296,13 @@ var TransferEngine = class {
1206
1296
  abortScope.dispose();
1207
1297
  }
1208
1298
  }
1209
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1299
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1210
1300
  const context = {
1211
1301
  attempt,
1212
1302
  job,
1213
1303
  reportProgress: (bytesTransferred, totalBytes) => {
1214
1304
  this.throwIfAborted(signal, job);
1305
+ notifyProgress();
1215
1306
  updateBytesTransferred(bytesTransferred);
1216
1307
  const progressInput = {
1217
1308
  bytesTransferred,
@@ -1280,6 +1371,96 @@ function createAbortScope(parentSignal, timeout, job) {
1280
1371
  signal: controller.signal
1281
1372
  };
1282
1373
  }
1374
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1375
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1376
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1377
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1378
+ const scope = {
1379
+ dispose: () => void 0,
1380
+ notifyProgress: () => void 0
1381
+ };
1382
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1383
+ return scope;
1384
+ }
1385
+ const controller = new AbortController();
1386
+ const retryable = timeout?.retryable ?? true;
1387
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1388
+ if (parentSignal?.aborted === true) {
1389
+ abortFromParent();
1390
+ } else {
1391
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1392
+ }
1393
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1394
+ controller.abort(
1395
+ new TimeoutError({
1396
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1397
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1398
+ retryable
1399
+ })
1400
+ );
1401
+ }, attemptTimeoutMs);
1402
+ let stallTimer;
1403
+ const armStallWatchdog = () => {
1404
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1405
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1406
+ stallTimer = setTimeout(() => {
1407
+ controller.abort(
1408
+ new TimeoutError({
1409
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1410
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1411
+ retryable
1412
+ })
1413
+ );
1414
+ }, stallTimeoutMs);
1415
+ };
1416
+ armStallWatchdog();
1417
+ return {
1418
+ dispose: () => {
1419
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1420
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1421
+ parentSignal?.removeEventListener("abort", abortFromParent);
1422
+ },
1423
+ notifyProgress: armStallWatchdog,
1424
+ signal: controller.signal
1425
+ };
1426
+ }
1427
+ function sleepWithAbort(delayMs, signal, job) {
1428
+ return new Promise((resolve, reject) => {
1429
+ if (signal === void 0) {
1430
+ setTimeout(resolve, delayMs);
1431
+ return;
1432
+ }
1433
+ if (signal.aborted) {
1434
+ reject(toAbortFailure(signal, job));
1435
+ return;
1436
+ }
1437
+ const rejectAbort = () => {
1438
+ clearTimeout(timer);
1439
+ reject(toAbortFailure(signal, job));
1440
+ };
1441
+ const timer = setTimeout(() => {
1442
+ signal.removeEventListener("abort", rejectAbort);
1443
+ resolve();
1444
+ }, delayMs);
1445
+ signal.addEventListener("abort", rejectAbort, { once: true });
1446
+ });
1447
+ }
1448
+ function toAbortFailure(signal, job) {
1449
+ if (signal.reason instanceof ZeroTransferError) {
1450
+ return signal.reason;
1451
+ }
1452
+ return new AbortError({
1453
+ details: { jobId: job.id, operation: job.operation },
1454
+ message: `Transfer job aborted: ${job.id}`,
1455
+ retryable: false
1456
+ });
1457
+ }
1458
+ function normalizeDelayMs(value) {
1459
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1460
+ return 0;
1461
+ }
1462
+ return Math.floor(value);
1463
+ }
1283
1464
  function normalizeTimeoutMs(value) {
1284
1465
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1285
1466
  return void 0;
@@ -1448,7 +1629,7 @@ async function runRoute(options) {
1448
1629
  const executor = createProviderTransferExecutor({
1449
1630
  resolveSession: ({ role }) => sessions.get(role)
1450
1631
  });
1451
- return await engine.execute(job, executor, buildExecuteOptions(options));
1632
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1452
1633
  } finally {
1453
1634
  if (destinationSession !== void 0) {
1454
1635
  await destinationSession.disconnect();
@@ -1485,12 +1666,14 @@ function defaultJobId(route, now) {
1485
1666
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1486
1667
  return `route:${route.id}:${timestamp.toString(36)}`;
1487
1668
  }
1488
- function buildExecuteOptions(options) {
1669
+ function buildExecuteOptions(options, client) {
1489
1670
  const execute = {};
1671
+ const retry = options.retry ?? client.defaults?.retry;
1672
+ const timeout = options.timeout ?? client.defaults?.timeout;
1490
1673
  if (options.signal !== void 0) execute.signal = options.signal;
1491
- if (options.retry !== void 0) execute.retry = options.retry;
1674
+ if (retry !== void 0) execute.retry = retry;
1492
1675
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1493
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1676
+ if (timeout !== void 0) execute.timeout = timeout;
1494
1677
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1495
1678
  return execute;
1496
1679
  }
@@ -1547,41 +1730,6 @@ function defaultRouteSuffix(source, destination) {
1547
1730
  return `${source}->${destination}`;
1548
1731
  }
1549
1732
 
1550
- // src/logging/redaction.ts
1551
- var REDACTED = "[REDACTED]";
1552
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1553
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1554
- function isSensitiveKey(key) {
1555
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1556
- }
1557
- function redactCommand(command) {
1558
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1559
- return `${commandName.toUpperCase()} ${REDACTED}`;
1560
- });
1561
- }
1562
- function redactValue(value) {
1563
- if (typeof value === "string") {
1564
- return redactCommand(value);
1565
- }
1566
- if (Array.isArray(value)) {
1567
- return value.map((item) => redactValue(item));
1568
- }
1569
- if (value !== null && typeof value === "object") {
1570
- return redactObject(value);
1571
- }
1572
- return value;
1573
- }
1574
- function redactObject(input) {
1575
- return Object.fromEntries(
1576
- Object.entries(input).map(([key, value]) => {
1577
- if (isSensitiveKey(key)) {
1578
- return [key, REDACTED];
1579
- }
1580
- return [key, redactValue(value)];
1581
- })
1582
- );
1583
- }
1584
-
1585
1733
  // src/profiles/SecretSource.ts
1586
1734
  import { Buffer as Buffer3 } from "buffer";
1587
1735
  import { readFile } from "fs/promises";
@@ -2005,11 +2153,11 @@ async function resolveTlsSecretSource(source, options) {
2005
2153
  }
2006
2154
 
2007
2155
  // src/utils/path.ts
2008
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2156
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2009
2157
  function assertSafeFtpArgument(value, label = "path") {
2010
2158
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2011
2159
  throw new ConfigurationError({
2012
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2160
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2013
2161
  retryable: false,
2014
2162
  details: {
2015
2163
  label
@@ -2971,38 +3119,53 @@ async function expectCompletion(control, command, path2) {
2971
3119
  const response = await control.sendCommand(command);
2972
3120
  assertPathCommandSucceeded(response, command, path2, control.providerId);
2973
3121
  }
2974
- async function readPassiveDataCommand(control, command, path2, options = {}) {
2975
- const dataConnection = await openPassiveDataCommand(control, command, path2, options);
3122
+ async function readPassiveLinesCommand(control, command, path2, onLine) {
3123
+ const dataConnection = await openPassiveDataCommand(control, command, path2);
2976
3124
  try {
2977
- const payload = await collectPassiveData(
2978
- dataConnection,
2979
- control.operationTimeoutMs,
2980
- path2,
2981
- control.providerId
2982
- );
3125
+ const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
3126
+ command,
3127
+ onLine,
3128
+ path: path2,
3129
+ providerId: control.providerId
3130
+ });
2983
3131
  const finalResponse = await control.readFinalResponse({
2984
3132
  command,
2985
3133
  operation: "data command completion",
2986
3134
  path: path2
2987
3135
  });
2988
3136
  assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
2989
- return payload;
3137
+ if (failure !== void 0) throw failure;
2990
3138
  } catch (error) {
2991
3139
  dataConnection.close();
2992
3140
  throw error;
2993
3141
  }
2994
3142
  }
2995
3143
  async function readDirectoryEntries(control, path2) {
3144
+ const entries = [];
3145
+ const collectEntry = (entry) => {
3146
+ if (entry.name === "." || entry.name === "..") return;
3147
+ entries.push(entry);
3148
+ };
2996
3149
  try {
2997
- const payload2 = await readPassiveDataCommand(control, `MLSD ${path2}`, path2);
2998
- return parseMlsdList(payload2.toString("utf8"), path2);
3150
+ await readPassiveLinesCommand(control, `MLSD ${path2}`, path2, (rawLine) => {
3151
+ const line = rawLine.trimEnd();
3152
+ if (line.length === 0) return;
3153
+ collectEntry(parseMlsdLine(line, path2));
3154
+ });
3155
+ return entries;
2999
3156
  } catch (error) {
3000
3157
  if (!isUnsupportedFtpCommandError(error, "MLSD")) {
3001
3158
  throw error;
3002
3159
  }
3003
3160
  }
3004
- const payload = await readPassiveDataCommand(control, `LIST ${path2}`, path2);
3005
- return parseUnixList(payload.toString("utf8"), path2);
3161
+ entries.length = 0;
3162
+ const now = /* @__PURE__ */ new Date();
3163
+ await readPassiveLinesCommand(control, `LIST ${path2}`, path2, (rawLine) => {
3164
+ const line = rawLine.trimEnd();
3165
+ if (line.length === 0 || line.toLowerCase().startsWith("total ")) return;
3166
+ collectEntry(parseUnixListLine(line, path2, now));
3167
+ });
3168
+ return entries;
3006
3169
  }
3007
3170
  async function openPassiveDataCommand(control, command, path2, options = {}) {
3008
3171
  const offset = normalizeOptionalByteCount(options.offset, "offset", path2);
@@ -3175,22 +3338,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
3175
3338
  }
3176
3339
  };
3177
3340
  }
3178
- async function collectPassiveData(dataConnection, timeoutMs, path2, providerId) {
3179
- const chunks = [];
3341
+ var MAX_LIST_LINE_BYTES = 64 * 1024;
3342
+ async function consumePassiveLines(dataConnection, timeoutMs, input) {
3343
+ let carry = Buffer4.alloc(0);
3344
+ let failure;
3180
3345
  const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
3181
3346
  host: dataConnection.endpoint.host,
3182
3347
  operation: "passive data transfer",
3183
- path: path2,
3184
- providerId
3348
+ path: input.path,
3349
+ providerId: input.providerId
3185
3350
  });
3351
+ const overlongLineFailure = () => new ParseError({
3352
+ details: { command: input.command, limitBytes: MAX_LIST_LINE_BYTES, path: input.path },
3353
+ message: `FTP listing line exceeded ${String(MAX_LIST_LINE_BYTES)} bytes for ${input.command}`,
3354
+ retryable: false
3355
+ });
3356
+ const emit = (lineBytes) => {
3357
+ if (failure !== void 0) return;
3358
+ let end = lineBytes.length;
3359
+ if (end > 0 && lineBytes[end - 1] === 13) end -= 1;
3360
+ if (end === 0) return;
3361
+ if (end > MAX_LIST_LINE_BYTES) {
3362
+ failure = overlongLineFailure();
3363
+ return;
3364
+ }
3365
+ try {
3366
+ input.onLine(lineBytes.toString("utf8", 0, end));
3367
+ } catch (error) {
3368
+ failure = error instanceof Error ? error : new Error(String(error));
3369
+ }
3370
+ };
3186
3371
  try {
3187
3372
  for await (const chunk of dataConnection.socket) {
3188
- chunks.push(Buffer4.from(chunk));
3373
+ if (failure !== void 0) continue;
3374
+ const data = carry.length > 0 ? Buffer4.concat([carry, chunk]) : chunk;
3375
+ let start = 0;
3376
+ let newline = data.indexOf(10, start);
3377
+ while (newline !== -1) {
3378
+ emit(data.subarray(start, newline));
3379
+ start = newline + 1;
3380
+ newline = data.indexOf(10, start);
3381
+ }
3382
+ carry = Buffer4.from(data.subarray(start));
3383
+ if (carry.length > MAX_LIST_LINE_BYTES && failure === void 0) {
3384
+ failure = overlongLineFailure();
3385
+ }
3386
+ if (failure !== void 0) carry = Buffer4.alloc(0);
3189
3387
  }
3388
+ if (carry.length > 0) emit(carry);
3190
3389
  } finally {
3191
3390
  clearIdleTimeout();
3192
3391
  }
3193
- return Buffer4.concat(chunks);
3392
+ return failure;
3194
3393
  }
3195
3394
  async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
3196
3395
  let bytesEmitted = 0;
@@ -3439,6 +3638,13 @@ function createTlsPinnedFingerprints(profile) {
3439
3638
  if (pinnedFingerprint256 === void 0) {
3440
3639
  return void 0;
3441
3640
  }
3641
+ if (profile.tls?.rejectUnauthorized === false) {
3642
+ throw new ConfigurationError({
3643
+ message: "FTPS tls.pinnedFingerprint256 cannot be combined with rejectUnauthorized: false; pin verification runs after the TLS handshake, so chain validation must stay enabled. For self-signed certificates supply tls.ca instead of disabling validation.",
3644
+ protocol: FTPS_PROVIDER_ID,
3645
+ retryable: false
3646
+ });
3647
+ }
3442
3648
  const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
3443
3649
  if (fingerprints.length === 0) {
3444
3650
  throw new ConfigurationError({
@@ -5410,6 +5616,7 @@ import { Buffer as Buffer13 } from "buffer";
5410
5616
  import { randomBytes as randomBytes2 } from "crypto";
5411
5617
  var MIN_PADDING_LENGTH = 4;
5412
5618
  var MIN_PACKET_LENGTH = 1 + MIN_PADDING_LENGTH;
5619
+ var MAX_SSH_PACKET_LENGTH = 256 * 1024;
5413
5620
  function encodeSshTransportPacket(payload, options = {}) {
5414
5621
  const body = Buffer13.from(payload);
5415
5622
  const blockSize = normalizeBlockSize(options.blockSize ?? 8);
@@ -5487,6 +5694,14 @@ var SshTransportPacketFramer = class {
5487
5694
  const packets = [];
5488
5695
  while (this.pending.length >= 4) {
5489
5696
  const packetLength = this.pending.readUInt32BE(0);
5697
+ if (packetLength > MAX_SSH_PACKET_LENGTH) {
5698
+ throw new ParseError({
5699
+ details: { maxPacketLength: MAX_SSH_PACKET_LENGTH, packetLength },
5700
+ message: "SSH transport packet length exceeds the maximum accepted size",
5701
+ protocol: "sftp",
5702
+ retryable: false
5703
+ });
5704
+ }
5490
5705
  const frameLength = 4 + packetLength;
5491
5706
  if (this.pending.length < frameLength) {
5492
5707
  break;
@@ -5964,14 +6179,35 @@ var SshTransportHandshake = class {
5964
6179
  return { outbound };
5965
6180
  }
5966
6181
  };
6182
+ var MAX_IDENTIFICATION_LINE_BYTES = 8192;
6183
+ var MAX_PRE_IDENTIFICATION_LINES = 1024;
5967
6184
  var SshIdentificationAccumulator = class {
5968
6185
  pending = Buffer15.alloc(0);
6186
+ bannerLineCount = 0;
5969
6187
  push(chunk) {
5970
6188
  this.pending = Buffer15.concat([this.pending, Buffer15.from(chunk)]);
5971
6189
  const bannerLines = [];
5972
6190
  while (true) {
5973
6191
  const lfIndex = this.pending.indexOf(10);
5974
- if (lfIndex < 0) break;
6192
+ if (lfIndex < 0) {
6193
+ if (this.pending.length > MAX_IDENTIFICATION_LINE_BYTES) {
6194
+ throw new ProtocolError({
6195
+ details: { limitBytes: MAX_IDENTIFICATION_LINE_BYTES },
6196
+ message: "SSH identification line exceeds the maximum accepted length",
6197
+ protocol: "sftp",
6198
+ retryable: false
6199
+ });
6200
+ }
6201
+ break;
6202
+ }
6203
+ if (lfIndex > MAX_IDENTIFICATION_LINE_BYTES) {
6204
+ throw new ProtocolError({
6205
+ details: { limitBytes: MAX_IDENTIFICATION_LINE_BYTES },
6206
+ message: "SSH identification line exceeds the maximum accepted length",
6207
+ protocol: "sftp",
6208
+ retryable: false
6209
+ });
6210
+ }
5975
6211
  const lineText = trimLineEndings(this.pending.subarray(0, lfIndex + 1).toString("ascii"));
5976
6212
  const remainder = Buffer15.from(this.pending.subarray(lfIndex + 1));
5977
6213
  this.pending = remainder;
@@ -5979,6 +6215,15 @@ var SshIdentificationAccumulator = class {
5979
6215
  this.pending = Buffer15.alloc(0);
5980
6216
  return { bannerLines, identLine: lineText, remainder };
5981
6217
  }
6218
+ this.bannerLineCount += 1;
6219
+ if (this.bannerLineCount > MAX_PRE_IDENTIFICATION_LINES) {
6220
+ throw new ProtocolError({
6221
+ details: { limitLines: MAX_PRE_IDENTIFICATION_LINES },
6222
+ message: "SSH server sent too many pre-identification banner lines",
6223
+ protocol: "sftp",
6224
+ retryable: false
6225
+ });
6226
+ }
5982
6227
  bannerLines.push(lineText);
5983
6228
  }
5984
6229
  return { bannerLines, remainder: Buffer15.alloc(0) };
@@ -6116,6 +6361,14 @@ var SshTransportPacketUnprotector = class {
6116
6361
  this.framePendingRaw = Buffer16.from(this.framePendingRaw.subarray(this.blockLength));
6117
6362
  this.framePartialDecrypted = this.decipher ? Buffer16.from(this.decipher.update(firstBlock)) : Buffer16.from(firstBlock);
6118
6363
  const packetLength = this.framePartialDecrypted.readUInt32BE(0);
6364
+ if (packetLength > MAX_SSH_PACKET_LENGTH) {
6365
+ throw new ProtocolError({
6366
+ details: { maxPacketLength: MAX_SSH_PACKET_LENGTH, packetLength },
6367
+ message: "SSH encrypted packet length exceeds the maximum accepted size",
6368
+ protocol: "sftp",
6369
+ retryable: false
6370
+ });
6371
+ }
6119
6372
  const remaining = 4 + packetLength - this.blockLength + this.macLength;
6120
6373
  if (remaining < 0) {
6121
6374
  throw new ProtocolError({
@@ -6739,6 +6992,7 @@ function decodeSftpAttributesFromReader(reader) {
6739
6992
 
6740
6993
  // src/protocols/sftp/v3/SftpPacket.ts
6741
6994
  import { Buffer as Buffer18 } from "buffer";
6995
+ var MAX_SFTP_PACKET_LENGTH = 256 * 1024;
6742
6996
  var SFTP_PACKET_TYPE = {
6743
6997
  ATTRS: 105,
6744
6998
  CLOSE: 4,
@@ -6798,6 +7052,13 @@ var SftpPacketFramer = class {
6798
7052
  const packets = [];
6799
7053
  while (this.pending.length >= 4) {
6800
7054
  const bodyLength = this.pending.readUInt32BE(0);
7055
+ if (bodyLength > MAX_SFTP_PACKET_LENGTH) {
7056
+ throw new ParseError({
7057
+ details: { bodyLength, maxPacketLength: MAX_SFTP_PACKET_LENGTH },
7058
+ message: "SFTP packet length exceeds the maximum accepted size",
7059
+ retryable: false
7060
+ });
7061
+ }
6801
7062
  const frameLength = 4 + bodyLength;
6802
7063
  if (this.pending.length < frameLength) {
6803
7064
  break;
@@ -7399,7 +7660,7 @@ function buildNativeSftpCapabilities(maxConcurrency) {
7399
7660
  var NATIVE_SFTP_PROVIDER_CAPABILITIES = buildNativeSftpCapabilities(
7400
7661
  NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
7401
7662
  );
7402
- function createNativeSftpProviderFactory(options = {}) {
7663
+ function createSftpProviderFactory(options = {}) {
7403
7664
  validateNativeSftpOptions(options);
7404
7665
  const capabilities = buildNativeSftpCapabilities(
7405
7666
  options.maxConcurrency ?? NATIVE_SFTP_DEFAULT_MAX_CONCURRENCY
@@ -8058,6 +8319,26 @@ function validateNativeSftpOptions(options) {
8058
8319
 
8059
8320
  // src/providers/web/httpInternals.ts
8060
8321
  import { Buffer as Buffer21 } from "buffer";
8322
+ function assertHttpsEnforced(input) {
8323
+ if (input.enforceHttps && !input.secure) {
8324
+ throw new ConfigurationError({
8325
+ details: { provider: input.providerId },
8326
+ message: `Provider "${input.providerId}" is configured with enforceHttps but its transport is cleartext http; set secure: true (or drop enforceHttps to explicitly accept cleartext)`,
8327
+ retryable: false
8328
+ });
8329
+ }
8330
+ }
8331
+ var cleartextWarnedKeys = /* @__PURE__ */ new Set();
8332
+ function warnCleartextCredentials(input) {
8333
+ if (!input.hasCredentials) return;
8334
+ const key = `${input.providerId}:${input.host}`;
8335
+ if (cleartextWarnedKeys.has(key)) return;
8336
+ cleartextWarnedKeys.add(key);
8337
+ process.emitWarning(
8338
+ `Provider "${input.providerId}" is sending credentials to ${input.host} over cleartext http; use https or set enforceHttps to block this`,
8339
+ { code: "ZERO_TRANSFER_CLEARTEXT_CREDENTIALS", type: "SecurityWarning" }
8340
+ );
8341
+ }
8061
8342
  function buildBaseUrl(profile, options) {
8062
8343
  const protocol = options.secure ? "https:" : "http:";
8063
8344
  const portSegment = profile.port !== void 0 ? `:${profile.port}` : "";
@@ -8105,18 +8386,19 @@ async function dispatchRequest(options, url, init) {
8105
8386
  signal: controller.signal
8106
8387
  });
8107
8388
  } catch (error) {
8389
+ const safeUrl = redactUrlForLogging(url);
8108
8390
  if (controller.signal.aborted && upstreamSignal?.aborted !== true) {
8109
8391
  throw new TimeoutError({
8110
8392
  cause: error,
8111
- details: { timeoutMs: options.timeoutMs, url: url.toString() },
8112
- message: `HTTP request to ${url.toString()} timed out after ${String(options.timeoutMs)}ms`,
8393
+ details: { timeoutMs: options.timeoutMs, url: safeUrl },
8394
+ message: `HTTP request to ${safeUrl} timed out after ${String(options.timeoutMs)}ms`,
8113
8395
  retryable: true
8114
8396
  });
8115
8397
  }
8116
8398
  throw new ConnectionError({
8117
8399
  cause: error,
8118
- details: { url: url.toString() },
8119
- message: `HTTP request to ${url.toString()} failed`,
8400
+ details: { url: safeUrl },
8401
+ message: `HTTP request to ${safeUrl} failed`,
8120
8402
  retryable: true
8121
8403
  });
8122
8404
  } finally {
@@ -8148,8 +8430,48 @@ function formatRangeHeader(offset, length) {
8148
8430
  const end = offset + length - 1;
8149
8431
  return `bytes=${String(offset)}-${String(end)}`;
8150
8432
  }
8151
- function mapResponseError(response, path2) {
8152
- const details = { path: path2, status: response.status, statusText: response.statusText };
8433
+ var ERROR_BODY_EXCERPT_LIMIT = 2048;
8434
+ async function readErrorBodyExcerpt(response) {
8435
+ try {
8436
+ const text = await response.text();
8437
+ if (text.length === 0) return void 0;
8438
+ return text.length > ERROR_BODY_EXCERPT_LIMIT ? `${text.slice(0, ERROR_BODY_EXCERPT_LIMIT)}... [truncated]` : text;
8439
+ } catch {
8440
+ return void 0;
8441
+ }
8442
+ }
8443
+ function parseRetryAfterMs(value, now = Date.now) {
8444
+ if (value === null) return void 0;
8445
+ const trimmed = value.trim();
8446
+ if (trimmed.length === 0) return void 0;
8447
+ if (/^\d+$/.test(trimmed)) {
8448
+ const seconds = Number.parseInt(trimmed, 10);
8449
+ return Number.isFinite(seconds) ? seconds * 1e3 : void 0;
8450
+ }
8451
+ if (!/[A-Za-z]/.test(trimmed)) return void 0;
8452
+ const retryAt = Date.parse(trimmed);
8453
+ if (Number.isNaN(retryAt)) return void 0;
8454
+ return Math.max(0, retryAt - now());
8455
+ }
8456
+ async function mapResponseErrorWithBody(response, path2) {
8457
+ return mapResponseError(response, path2, await readErrorBodyExcerpt(response));
8458
+ }
8459
+ function mapResponseError(response, path2, bodyExcerpt) {
8460
+ const details = {
8461
+ path: path2,
8462
+ status: response.status,
8463
+ statusText: response.statusText
8464
+ };
8465
+ if (bodyExcerpt !== void 0) details["body"] = bodyExcerpt;
8466
+ if (response.status === 429 || response.status === 503) {
8467
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
8468
+ if (retryAfterMs !== void 0) details["retryAfterMs"] = retryAfterMs;
8469
+ return new ConnectionError({
8470
+ details,
8471
+ message: response.status === 429 ? `HTTP request for ${path2} was rate limited (429)` : `HTTP service unavailable for ${path2} (503)`,
8472
+ retryable: true
8473
+ });
8474
+ }
8153
8475
  if (response.status === 401) {
8154
8476
  return new AuthenticationError({
8155
8477
  details,
@@ -8181,14 +8503,54 @@ async function* webStreamToAsyncIterable(body) {
8181
8503
  const reader = body.getReader();
8182
8504
  try {
8183
8505
  while (true) {
8184
- const { done, value } = await reader.read();
8185
- if (done) break;
8186
- if (value !== void 0) yield value;
8506
+ let result;
8507
+ try {
8508
+ result = await reader.read();
8509
+ } catch (error) {
8510
+ if (error instanceof ZeroTransferError) throw error;
8511
+ throw new ConnectionError({
8512
+ cause: error,
8513
+ message: "HTTP response stream was interrupted before completion",
8514
+ retryable: true
8515
+ });
8516
+ }
8517
+ if (result.done) break;
8518
+ if (result.value !== void 0) yield result.value;
8187
8519
  }
8188
8520
  } finally {
8189
8521
  reader.releaseLock();
8190
8522
  }
8191
8523
  }
8524
+ function asyncIterableToReadableStream(source, onChunk) {
8525
+ const iterator = source[Symbol.asyncIterator]();
8526
+ return new ReadableStream({
8527
+ async pull(controller) {
8528
+ try {
8529
+ const next = await iterator.next();
8530
+ if (next.done === true) {
8531
+ controller.close();
8532
+ return;
8533
+ }
8534
+ const chunk = next.value;
8535
+ if (chunk.byteLength === 0) {
8536
+ return;
8537
+ }
8538
+ controller.enqueue(chunk);
8539
+ onChunk(chunk);
8540
+ } catch (error) {
8541
+ controller.error(error);
8542
+ }
8543
+ },
8544
+ async cancel(reason) {
8545
+ if (typeof iterator.return === "function") {
8546
+ try {
8547
+ await iterator.return(reason);
8548
+ } catch {
8549
+ }
8550
+ }
8551
+ }
8552
+ });
8553
+ }
8192
8554
  function secretToString2(value) {
8193
8555
  if (typeof value === "string") return value;
8194
8556
  if (value instanceof Uint8Array || Buffer21.isBuffer(value)) {
@@ -11797,6 +12159,7 @@ function createHttpProviderFactory(options = {}) {
11797
12159
  const secure = options.secure ?? id === "https";
11798
12160
  const basePath = options.basePath ?? "";
11799
12161
  const fetchImpl = options.fetch ?? globalThis.fetch;
12162
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
11800
12163
  if (typeof fetchImpl !== "function") {
11801
12164
  throw new ConfigurationError({
11802
12165
  message: "Global fetch is unavailable; supply HttpProviderOptions.fetch explicitly",
@@ -11846,6 +12209,13 @@ var HttpProvider = class {
11846
12209
  id;
11847
12210
  capabilities;
11848
12211
  async connect(profile) {
12212
+ if (!this.internals.secure) {
12213
+ warnCleartextCredentials({
12214
+ hasCredentials: profile.username !== void 0 || profile.password !== void 0,
12215
+ host: profile.host,
12216
+ providerId: this.internals.id
12217
+ });
12218
+ }
11849
12219
  const headers = { ...this.internals.defaultHeaders };
11850
12220
  if (profile.username !== void 0) {
11851
12221
  const username = await resolveSecret(profile.username);
@@ -11904,7 +12274,7 @@ var HttpFileSystem = class {
11904
12274
  method: "HEAD"
11905
12275
  });
11906
12276
  if (!response.ok) {
11907
- throw mapResponseError(response, normalized);
12277
+ throw await mapResponseErrorWithBody(response, normalized);
11908
12278
  }
11909
12279
  return responseToStat(response, normalized);
11910
12280
  }
@@ -11929,12 +12299,12 @@ var HttpTransferOperations = class {
11929
12299
  if (request.signal !== void 0) requestInit.signal = request.signal;
11930
12300
  const response = await dispatchRequest(this.options, url, requestInit);
11931
12301
  if (!response.ok && response.status !== 206) {
11932
- throw mapResponseError(response, normalized);
12302
+ throw await mapResponseErrorWithBody(response, normalized);
11933
12303
  }
11934
12304
  const body = response.body;
11935
12305
  if (body === null) {
11936
12306
  throw new ConnectionError({
11937
- message: `HTTP response had no body for ${url.toString()}`,
12307
+ message: `HTTP response had no body for ${redactUrlForLogging(url)}`,
11938
12308
  retryable: true
11939
12309
  });
11940
12310
  }
@@ -11992,7 +12362,8 @@ function createWebDavProviderFactory(options = {}) {
11992
12362
  const secure = options.secure ?? false;
11993
12363
  const basePath = options.basePath ?? "";
11994
12364
  const fetchImpl = options.fetch ?? globalThis.fetch;
11995
- const uploadStreaming = options.uploadStreaming ?? "when-known-size";
12365
+ const uploadStreaming = options.uploadStreaming ?? "always";
12366
+ assertHttpsEnforced({ enforceHttps: options.enforceHttps ?? false, providerId: id, secure });
11996
12367
  if (typeof fetchImpl !== "function") {
11997
12368
  throw new ConfigurationError({
11998
12369
  message: "Global fetch is unavailable; supply WebDavProviderOptions.fetch explicitly",
@@ -12044,6 +12415,13 @@ var WebDavProvider = class {
12044
12415
  id;
12045
12416
  capabilities;
12046
12417
  async connect(profile) {
12418
+ if (!this.internals.secure) {
12419
+ warnCleartextCredentials({
12420
+ hasCredentials: profile.username !== void 0 || profile.password !== void 0,
12421
+ host: profile.host,
12422
+ providerId: this.internals.id
12423
+ });
12424
+ }
12047
12425
  const headers = { ...this.internals.defaultHeaders };
12048
12426
  if (profile.username !== void 0) {
12049
12427
  const username = await resolveSecret(profile.username);
@@ -12098,7 +12476,7 @@ var WebDavFileSystem = class {
12098
12476
  method: "PROPFIND"
12099
12477
  });
12100
12478
  if (!response.ok && response.status !== 207) {
12101
- throw mapResponseError(response, normalized);
12479
+ throw await mapResponseErrorWithBody(response, normalized);
12102
12480
  }
12103
12481
  const body = await response.text();
12104
12482
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12112,7 +12490,7 @@ var WebDavFileSystem = class {
12112
12490
  method: "PROPFIND"
12113
12491
  });
12114
12492
  if (!response.ok && response.status !== 207) {
12115
- throw mapResponseError(response, normalized);
12493
+ throw await mapResponseErrorWithBody(response, normalized);
12116
12494
  }
12117
12495
  const body = await response.text();
12118
12496
  const entries = parsePropfindResponses(body, this.options.baseUrl);
@@ -12157,12 +12535,12 @@ var WebDavTransferOperations = class {
12157
12535
  if (request.signal !== void 0) init.signal = request.signal;
12158
12536
  const response = await dispatchRequest(this.options, url, init);
12159
12537
  if (!response.ok && response.status !== 206) {
12160
- throw mapResponseError(response, normalized);
12538
+ throw await mapResponseErrorWithBody(response, normalized);
12161
12539
  }
12162
12540
  const body = response.body;
12163
12541
  if (body === null) {
12164
12542
  throw new ConnectionError({
12165
- message: `WebDAV response had no body for ${url.toString()}`,
12543
+ message: `WebDAV response had no body for ${redactUrlForLogging(url)}`,
12166
12544
  retryable: true
12167
12545
  });
12168
12546
  }
@@ -12206,7 +12584,7 @@ var WebDavTransferOperations = class {
12206
12584
  if (request.signal !== void 0) init2.signal = request.signal;
12207
12585
  const response2 = await dispatchRequest(this.options, url, init2);
12208
12586
  if (!response2.ok) {
12209
- throw mapResponseError(response2, normalized);
12587
+ throw await mapResponseErrorWithBody(response2, normalized);
12210
12588
  }
12211
12589
  request.reportProgress(buffered.byteLength, buffered.byteLength);
12212
12590
  const result2 = {
@@ -12242,7 +12620,7 @@ var WebDavTransferOperations = class {
12242
12620
  if (request.signal !== void 0) init.signal = request.signal;
12243
12621
  const response = await dispatchRequest(this.options, url, init);
12244
12622
  if (!response.ok) {
12245
- throw mapResponseError(response, normalized);
12623
+ throw await mapResponseErrorWithBody(response, normalized);
12246
12624
  }
12247
12625
  const result = {
12248
12626
  bytesTransferred,
@@ -12268,36 +12646,6 @@ async function collectChunks6(source) {
12268
12646
  }
12269
12647
  return out;
12270
12648
  }
12271
- function asyncIterableToReadableStream(source, onChunk) {
12272
- const iterator = source[Symbol.asyncIterator]();
12273
- return new ReadableStream({
12274
- async pull(controller) {
12275
- try {
12276
- const next = await iterator.next();
12277
- if (next.done === true) {
12278
- controller.close();
12279
- return;
12280
- }
12281
- const chunk = next.value;
12282
- if (chunk.byteLength === 0) {
12283
- return;
12284
- }
12285
- controller.enqueue(chunk);
12286
- onChunk(chunk);
12287
- } catch (error) {
12288
- controller.error(error);
12289
- }
12290
- },
12291
- async cancel(reason) {
12292
- if (typeof iterator.return === "function") {
12293
- try {
12294
- await iterator.return(reason);
12295
- } catch {
12296
- }
12297
- }
12298
- }
12299
- });
12300
- }
12301
12649
  function parsePropfindResponses(xml, baseUrl) {
12302
12650
  const entries = [];
12303
12651
  const responseRegex = /<(?:[a-zA-Z0-9-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9-]+:)?response>/gi;
@@ -12390,11 +12738,12 @@ import { join as joinPath4 } from "path";
12390
12738
 
12391
12739
  // src/providers/web/awsSigv4.ts
12392
12740
  import { createHash as createHash4, createHmac as createHmac3 } from "crypto";
12741
+ var UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
12393
12742
  function signSigV4(input) {
12394
12743
  const now = input.now ?? /* @__PURE__ */ new Date();
12395
12744
  const amzDate = formatAmzDate(now);
12396
12745
  const dateStamp = amzDate.slice(0, 8);
12397
- const payloadHash = input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
12746
+ const payloadHash = input.unsignedPayload === true ? UNSIGNED_PAYLOAD : input.body !== void 0 ? sha256Hex(input.body) : sha256Hex(new Uint8Array());
12398
12747
  input.headers["host"] = input.url.host;
12399
12748
  input.headers["x-amz-date"] = amzDate;
12400
12749
  input.headers["x-amz-content-sha256"] = payloadHash;
@@ -12675,7 +13024,7 @@ var S3FileSystem = class {
12675
13024
  url.searchParams.set("delimiter", "/");
12676
13025
  if (prefix.length > 0) url.searchParams.set("prefix", prefix);
12677
13026
  const response = await s3Fetch(this.options, "GET", url);
12678
- if (!response.ok) throw mapResponseError(response, normalized);
13027
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12679
13028
  const body = await response.text();
12680
13029
  return parseListObjectsV2(body, prefix);
12681
13030
  }
@@ -12683,7 +13032,7 @@ var S3FileSystem = class {
12683
13032
  const normalized = normalizeRemotePath(path2);
12684
13033
  const url = buildObjectUrl(this.options, normalized);
12685
13034
  const response = await s3Fetch(this.options, "HEAD", url);
12686
- if (!response.ok) throw mapResponseError(response, normalized);
13035
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12687
13036
  const stat = {
12688
13037
  exists: true,
12689
13038
  name: basenameRemotePath(normalized),
@@ -12723,12 +13072,12 @@ var S3TransferOperations = class {
12723
13072
  extraHeaders: headers
12724
13073
  });
12725
13074
  if (!response.ok && response.status !== 206) {
12726
- throw mapResponseError(response, normalized);
13075
+ throw await mapResponseErrorWithBody(response, normalized);
12727
13076
  }
12728
13077
  const body = response.body;
12729
13078
  if (body === null) {
12730
13079
  throw new ConnectionError({
12731
- message: `S3 response had no body for ${url.toString()}`,
13080
+ message: `S3 response had no body for ${redactUrlForLogging(url)}`,
12732
13081
  retryable: true
12733
13082
  });
12734
13083
  }
@@ -12764,19 +13113,33 @@ var S3TransferOperations = class {
12764
13113
  }
12765
13114
  return this.writeSingleShot(request, normalized);
12766
13115
  }
13116
+ /**
13117
+ * Single PUT upload used when multipart is disabled. Streams the body with
13118
+ * a declared `Content-Length` (signed as `UNSIGNED-PAYLOAD`) when the
13119
+ * caller knows the total size; S3 requires a length up front, so only
13120
+ * unknown-size payloads fall back to buffering the content in memory.
13121
+ */
12767
13122
  async writeSingleShot(request, normalized) {
12768
13123
  const url = buildObjectUrl(this.options, normalized);
12769
- const buffered = await collectChunks7(request.content);
13124
+ const totalBytes = request.totalBytes;
13125
+ if (typeof totalBytes !== "number" || totalBytes < 0) {
13126
+ const buffered = await collectChunks7(request.content);
13127
+ return this.singleShotFromBuffer(request, normalized, buffered);
13128
+ }
13129
+ let bytesTransferred = 0;
13130
+ const stream = asyncIterableToReadableStream(request.content, (chunk) => {
13131
+ bytesTransferred += chunk.byteLength;
13132
+ request.reportProgress(bytesTransferred, totalBytes);
13133
+ });
12770
13134
  const response = await s3Fetch(this.options, "PUT", url, {
12771
13135
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12772
- body: buffered,
12773
- extraHeaders: { "content-type": "application/octet-stream" }
13136
+ extraHeaders: { "content-type": "application/octet-stream" },
13137
+ streamBody: { content: stream, contentLength: totalBytes }
12774
13138
  });
12775
- if (!response.ok) throw mapResponseError(response, normalized);
12776
- request.reportProgress(buffered.byteLength, buffered.byteLength);
13139
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12777
13140
  const result = {
12778
- bytesTransferred: buffered.byteLength,
12779
- totalBytes: buffered.byteLength
13141
+ bytesTransferred,
13142
+ totalBytes
12780
13143
  };
12781
13144
  const etag = response.headers.get("etag");
12782
13145
  if (etag !== null) result.checksum = etag;
@@ -12840,7 +13203,7 @@ var S3TransferOperations = class {
12840
13203
  ...request.signal !== void 0 ? { signal: request.signal } : {},
12841
13204
  extraHeaders: { "content-type": "application/octet-stream" }
12842
13205
  });
12843
- if (!initiateResponse.ok) throw mapResponseError(initiateResponse, normalized);
13206
+ if (!initiateResponse.ok) throw await mapResponseErrorWithBody(initiateResponse, normalized);
12844
13207
  const initiateBody = await initiateResponse.text();
12845
13208
  const initiated = innerText(initiateBody, "UploadId");
12846
13209
  if (initiated === void 0 || initiated === "") {
@@ -12879,7 +13242,7 @@ var S3TransferOperations = class {
12879
13242
  body: partBytes.bytes
12880
13243
  });
12881
13244
  if (!partResponse.ok) {
12882
- throw mapResponseError(partResponse, normalized);
13245
+ throw await mapResponseErrorWithBody(partResponse, normalized);
12883
13246
  }
12884
13247
  const partEtag = partResponse.headers.get("etag");
12885
13248
  if (partEtag === null) {
@@ -12935,7 +13298,7 @@ var S3TransferOperations = class {
12935
13298
  if (resumeStore === void 0) {
12936
13299
  await abortMultipart(this.options, objectUrl, uploadId).catch(() => void 0);
12937
13300
  }
12938
- throw mapResponseError(completeResponse, normalized);
13301
+ throw await mapResponseErrorWithBody(completeResponse, normalized);
12939
13302
  }
12940
13303
  if (resumeStore !== void 0) await resumeStore.clear(resumeKey);
12941
13304
  const completeBody = await completeResponse.text();
@@ -12954,7 +13317,7 @@ var S3TransferOperations = class {
12954
13317
  body: buffered,
12955
13318
  extraHeaders: { "content-type": "application/octet-stream" }
12956
13319
  });
12957
- if (!response.ok) throw mapResponseError(response, normalized);
13320
+ if (!response.ok) throw await mapResponseErrorWithBody(response, normalized);
12958
13321
  request.reportProgress(buffered.byteLength, buffered.byteLength);
12959
13322
  const result = {
12960
13323
  bytesTransferred: buffered.byteLength,
@@ -12972,6 +13335,8 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
12972
13335
  };
12973
13336
  if (fetchOptions.body !== void 0) {
12974
13337
  headers["content-length"] = String(fetchOptions.body.byteLength);
13338
+ } else if (fetchOptions.streamBody !== void 0) {
13339
+ headers["content-length"] = String(fetchOptions.streamBody.contentLength);
12975
13340
  }
12976
13341
  signSigV4({
12977
13342
  accessKeyId: options.accessKeyId,
@@ -12982,10 +13347,16 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
12982
13347
  service: options.service,
12983
13348
  url,
12984
13349
  ...fetchOptions.body !== void 0 ? { body: fetchOptions.body } : {},
13350
+ ...fetchOptions.streamBody !== void 0 ? { unsignedPayload: true } : {},
12985
13351
  ...options.sessionToken !== void 0 ? { sessionToken: options.sessionToken } : {}
12986
13352
  });
12987
13353
  const init = { headers, method };
12988
- if (fetchOptions.body !== void 0) init.body = fetchOptions.body;
13354
+ if (fetchOptions.body !== void 0) {
13355
+ init.body = fetchOptions.body;
13356
+ } else if (fetchOptions.streamBody !== void 0) {
13357
+ init.body = fetchOptions.streamBody.content;
13358
+ init.duplex = "half";
13359
+ }
12989
13360
  if (fetchOptions.signal !== void 0) init.signal = fetchOptions.signal;
12990
13361
  const controller = new AbortController();
12991
13362
  const upstreamSignal = init.signal ?? null;
@@ -13003,10 +13374,11 @@ async function s3Fetch(options, method, url, fetchOptions = {}) {
13003
13374
  try {
13004
13375
  return await options.fetch(url.toString(), { ...init, signal: controller.signal });
13005
13376
  } catch (error) {
13377
+ const safeUrl = redactUrlForLogging(url);
13006
13378
  throw new ConnectionError({
13007
13379
  cause: error,
13008
- details: { url: url.toString() },
13009
- message: `S3 request to ${url.toString()} failed`,
13380
+ details: { url: safeUrl },
13381
+ message: `S3 request to ${safeUrl} failed`,
13010
13382
  retryable: true
13011
13383
  });
13012
13384
  } finally {
@@ -13173,7 +13545,7 @@ function getBuiltinCapabilityMatrix() {
13173
13545
  label: "FTPS"
13174
13546
  },
13175
13547
  {
13176
- capabilities: createNativeSftpProviderFactory().capabilities,
13548
+ capabilities: createSftpProviderFactory().capabilities,
13177
13549
  id: "sftp",
13178
13550
  label: "SFTP"
13179
13551
  },
@@ -13474,7 +13846,6 @@ function expandAlgorithms(values) {
13474
13846
  }
13475
13847
 
13476
13848
  // src/profiles/importers/FileZillaImporter.ts
13477
- import { Buffer as Buffer26 } from "buffer";
13478
13849
  function importFileZillaSites(xml) {
13479
13850
  const events = tokenizeXml(xml);
13480
13851
  if (events.length === 0) {
@@ -13490,7 +13861,6 @@ function importFileZillaSites(xml) {
13490
13861
  const folderNamePending = [];
13491
13862
  let inServer = false;
13492
13863
  let serverFields = {};
13493
- let serverPasswordEncoding;
13494
13864
  let activeTag;
13495
13865
  let captureFolderName = false;
13496
13866
  for (const event of events) {
@@ -13503,13 +13873,9 @@ function importFileZillaSites(xml) {
13503
13873
  if (event.name === "Server") {
13504
13874
  inServer = true;
13505
13875
  serverFields = {};
13506
- serverPasswordEncoding = void 0;
13507
13876
  continue;
13508
13877
  }
13509
13878
  activeTag = event.name;
13510
- if (event.name === "Pass" && inServer) {
13511
- serverPasswordEncoding = event.attributes["encoding"];
13512
- }
13513
13879
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
13514
13880
  captureFolderName = true;
13515
13881
  }
@@ -13535,7 +13901,7 @@ function importFileZillaSites(xml) {
13535
13901
  }
13536
13902
  if (event.name === "Server") {
13537
13903
  const folder = folderStack.filter((segment) => segment !== "");
13538
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
13904
+ const result = buildSiteFromFields(serverFields);
13539
13905
  if (result.kind === "site") {
13540
13906
  sites.push({ ...result.site, folder });
13541
13907
  } else {
@@ -13547,7 +13913,6 @@ function importFileZillaSites(xml) {
13547
13913
  }
13548
13914
  inServer = false;
13549
13915
  serverFields = {};
13550
- serverPasswordEncoding = void 0;
13551
13916
  activeTag = void 0;
13552
13917
  continue;
13553
13918
  }
@@ -13556,7 +13921,7 @@ function importFileZillaSites(xml) {
13556
13921
  }
13557
13922
  return { sites, skipped };
13558
13923
  }
13559
- function buildSiteFromFields(fields, passwordEncoding) {
13924
+ function buildSiteFromFields(fields) {
13560
13925
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
13561
13926
  const host = (fields["Host"] ?? "").trim();
13562
13927
  if (host === "") return { kind: "skipped", name };
@@ -13575,18 +13940,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
13575
13940
  }
13576
13941
  const user = fields["User"]?.trim();
13577
13942
  if (user !== void 0 && user !== "") profile.username = { value: user };
13578
- let password;
13579
13943
  const rawPass = fields["Pass"];
13580
- if (rawPass !== void 0 && rawPass !== "") {
13581
- if (passwordEncoding === "base64") {
13582
- password = Buffer26.from(rawPass, "base64").toString("utf8");
13583
- } else {
13584
- password = rawPass;
13585
- }
13586
- if (password !== void 0 && password !== "") profile.password = { value: password };
13587
- }
13588
- const site = { name, profile };
13589
- if (password !== void 0) site.password = password;
13944
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
13945
+ const site = { hasStoredPassword, name, profile };
13590
13946
  const logonText = fields["Logontype"];
13591
13947
  if (logonText !== void 0) {
13592
13948
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -13907,6 +14263,62 @@ function openTcpSocket(host, port, timeoutMs) {
13907
14263
  });
13908
14264
  }
13909
14265
 
14266
+ // src/transfers/createDefaultRetryPolicy.ts
14267
+ var DEFAULT_MAX_ATTEMPTS = 4;
14268
+ var DEFAULT_BASE_DELAY_MS = 250;
14269
+ var DEFAULT_MAX_DELAY_MS = 3e4;
14270
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
14271
+ function createDefaultRetryPolicy(options = {}) {
14272
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
14273
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
14274
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
14275
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
14276
+ const random = options.random ?? Math.random;
14277
+ return {
14278
+ getDelayMs(input) {
14279
+ const retryAfterMs = readRetryAfterMs(input.error);
14280
+ if (retryAfterMs !== void 0) {
14281
+ return retryAfterMs;
14282
+ }
14283
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
14284
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
14285
+ return Math.floor(random() * cappedMs);
14286
+ },
14287
+ maxAttempts,
14288
+ shouldRetry(input) {
14289
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
14290
+ return false;
14291
+ }
14292
+ if (input.elapsedMs >= maxElapsedMs) {
14293
+ return false;
14294
+ }
14295
+ const retryAfterMs = readRetryAfterMs(input.error);
14296
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
14297
+ return false;
14298
+ }
14299
+ return true;
14300
+ }
14301
+ };
14302
+ }
14303
+ function readRetryAfterMs(error) {
14304
+ if (!(error instanceof ZeroTransferError)) return void 0;
14305
+ const value = error.details?.["retryAfterMs"];
14306
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
14307
+ return Math.floor(value);
14308
+ }
14309
+ function normalizePositiveInteger(value, fallback) {
14310
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
14311
+ return fallback;
14312
+ }
14313
+ return Math.floor(value);
14314
+ }
14315
+ function normalizeNonNegative(value, fallback) {
14316
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
14317
+ return fallback;
14318
+ }
14319
+ return Math.floor(value);
14320
+ }
14321
+
13910
14322
  // src/transfers/TransferPlan.ts
13911
14323
  function createTransferPlan(input) {
13912
14324
  const plan = {
@@ -14004,8 +14416,8 @@ var TransferQueue = class {
14004
14416
  this.concurrency = normalizeConcurrency(options.concurrency);
14005
14417
  this.defaultExecutor = options.executor;
14006
14418
  this.resolveExecutor = options.resolveExecutor;
14007
- this.retry = options.retry;
14008
- this.timeout = options.timeout;
14419
+ this.retry = options.retry ?? options.client?.defaults?.retry;
14420
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
14009
14421
  this.bandwidthLimit = options.bandwidthLimit;
14010
14422
  this.onProgress = options.onProgress;
14011
14423
  this.onReceipt = options.onReceipt;
@@ -15405,6 +15817,7 @@ async function dispatchWebhook(options) {
15405
15817
  return { attempts: attempt, delivered: false, status: lastStatus };
15406
15818
  }
15407
15819
  function createWebhookAuditLog(options) {
15820
+ validateTarget(options.target);
15408
15821
  return {
15409
15822
  list: () => Promise.resolve([]),
15410
15823
  record: async (entry) => {
@@ -15427,6 +15840,24 @@ function validateTarget(target) {
15427
15840
  retryable: false
15428
15841
  });
15429
15842
  }
15843
+ let parsed;
15844
+ try {
15845
+ parsed = new URL(target.url);
15846
+ } catch (error) {
15847
+ throw new ConfigurationError({
15848
+ cause: error,
15849
+ details: { url: target.url },
15850
+ message: "Webhook target url must be an absolute URL",
15851
+ retryable: false
15852
+ });
15853
+ }
15854
+ if (parsed.protocol === "https:") return;
15855
+ if (parsed.protocol === "http:" && target.allowInsecureUrl === true) return;
15856
+ throw new ConfigurationError({
15857
+ details: { protocol: parsed.protocol, url: target.url },
15858
+ message: parsed.protocol === "http:" ? "Webhook target url must use https; set allowInsecureUrl: true to permit cleartext http delivery" : `Webhook target url must use https, got "${parsed.protocol}"`,
15859
+ retryable: false
15860
+ });
15430
15861
  }
15431
15862
  function normalizeRetry(retry) {
15432
15863
  return {
@@ -15463,6 +15894,26 @@ var ApprovalRejectedError = class _ApprovalRejectedError extends ZeroTransferErr
15463
15894
  }
15464
15895
  request;
15465
15896
  };
15897
+ var ApprovalTimeoutError = class _ApprovalTimeoutError extends ZeroTransferError {
15898
+ /**
15899
+ * Creates an approval timeout error.
15900
+ *
15901
+ * @param request - The approval request that timed out while pending.
15902
+ * @param timeoutMs - Configured timeout window in milliseconds.
15903
+ */
15904
+ constructor(request, timeoutMs) {
15905
+ super({
15906
+ code: "approval_timeout",
15907
+ details: { approvalId: request.id, routeId: request.routeId, timeoutMs },
15908
+ message: `Approval "${request.id}" for route "${request.routeId}" timed out after ${String(timeoutMs)}ms`,
15909
+ retryable: false
15910
+ });
15911
+ this.request = request;
15912
+ Object.setPrototypeOf(this, _ApprovalTimeoutError.prototype);
15913
+ this.name = "ApprovalTimeoutError";
15914
+ }
15915
+ request;
15916
+ };
15466
15917
  var ApprovalRegistry = class {
15467
15918
  requests = /* @__PURE__ */ new Map();
15468
15919
  pending = /* @__PURE__ */ new Map();
@@ -15588,9 +16039,27 @@ function createApprovalGate(options) {
15588
16039
  };
15589
16040
  if (input.signal.aborted) onAbort();
15590
16041
  input.signal.addEventListener("abort", onAbort);
16042
+ let timeoutTimer;
16043
+ const timeoutMs = options.timeoutMs;
16044
+ const pendingPromises = [settled];
16045
+ if (timeoutMs !== void 0) {
16046
+ pendingPromises.push(
16047
+ new Promise((_resolve, reject) => {
16048
+ timeoutTimer = setTimeout(() => {
16049
+ const current = options.registry.get(approvalId) ?? request;
16050
+ reject(new ApprovalTimeoutError(current, timeoutMs));
16051
+ if (current.status === "pending") {
16052
+ settled.catch(() => void 0);
16053
+ options.registry.reject(approvalId, { reason: "timeout" }, now());
16054
+ }
16055
+ }, timeoutMs);
16056
+ })
16057
+ );
16058
+ }
15591
16059
  try {
15592
- await settled;
16060
+ await Promise.race(pendingPromises);
15593
16061
  } finally {
16062
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
15594
16063
  input.signal.removeEventListener("abort", onAbort);
15595
16064
  }
15596
16065
  return options.runner(input);
@@ -15962,6 +16431,7 @@ export {
15962
16431
  AbortError,
15963
16432
  ApprovalRegistry,
15964
16433
  ApprovalRejectedError,
16434
+ ApprovalTimeoutError,
15965
16435
  AuthenticationError,
15966
16436
  AuthorizationError,
15967
16437
  CLASSIC_PROVIDER_IDS,
@@ -16011,6 +16481,7 @@ export {
16011
16481
  createAtomicDeployPlan,
16012
16482
  createAzureBlobProviderFactory,
16013
16483
  createBandwidthThrottle,
16484
+ createDefaultRetryPolicy,
16014
16485
  createDropboxProviderFactory,
16015
16486
  createFileSystemS3MultipartResumeStore,
16016
16487
  createFtpProviderFactory,
@@ -16023,7 +16494,6 @@ export {
16023
16494
  createLocalProviderFactory,
16024
16495
  createMemoryProviderFactory,
16025
16496
  createMemoryS3MultipartResumeStore,
16026
- createNativeSftpProviderFactory,
16027
16497
  createOAuthTokenSecretSource,
16028
16498
  createOneDriveProviderFactory,
16029
16499
  createOutboxRoute,
@@ -16033,7 +16503,7 @@ export {
16033
16503
  createRemoteBrowser,
16034
16504
  createRemoteManifest,
16035
16505
  createS3ProviderFactory,
16036
- createNativeSftpProviderFactory as createSftpProviderFactory,
16506
+ createSftpProviderFactory,
16037
16507
  createSyncPlan,
16038
16508
  createTransferClient,
16039
16509
  createTransferJobsFromPlan,
@@ -16081,8 +16551,10 @@ export {
16081
16551
  parseUnixListLine,
16082
16552
  redactCommand,
16083
16553
  redactConnectionProfile,
16554
+ redactErrorForLogging,
16084
16555
  redactObject,
16085
16556
  redactSecretSource,
16557
+ redactUrlForLogging,
16086
16558
  redactValue,
16087
16559
  resolveConnectionProfileSecrets,
16088
16560
  resolveOpenSshHost,