@zero-transfer/mft 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,8 +8,6 @@
8
8
  npm install @zero-transfer/mft
9
9
  ```
10
10
 
11
- Installing this package automatically pulls in [`@zero-transfer/core`](https://www.npmjs.com/package/@zero-transfer/core) as a transitive dependency. The full core surface (`createTransferClient`, `uploadFile`, `downloadFile`, profiles, errors, sync planner, …) is re-exported from this package, so a single `import { … } from "@zero-transfer/mft"` is all you need. If your app uses multiple protocols, install the umbrella [`@zero-transfer/sdk`](https://www.npmjs.com/package/@zero-transfer/sdk) instead of multiple scoped packages.
12
-
13
11
  ## Overview
14
12
 
15
13
  Managed File Transfer workflow primitives: routes, schedules (interval + cron), inbox/outbox conventions, retention policies, audit logs (in-memory, JSONL, fan-out, webhook-backed), HMAC-signed webhook delivery, and approval gates that require human sign-off before a scheduled run executes.
@@ -22,7 +20,7 @@ import { MftRoute, RouteRegistry, runRoute } from "@zero-transfer/mft";
22
20
 
23
21
  ## Public surface
24
22
 
25
- This package publishes a narrowed surface of **30** exports. These symbols are also available from [`@zero-transfer/sdk`](https://www.npmjs.com/package/@zero-transfer/sdk); the table below links into the full API reference:
23
+ This package publishes a narrowed surface of **31** exports. These symbols are also available from [`@zero-transfer/sdk`](https://www.npmjs.com/package/@zero-transfer/sdk); the table below links into the full API reference:
26
24
 
27
25
  | Symbol | Kind | Notes |
28
26
  | ------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------ |
@@ -56,6 +54,7 @@ This package publishes a narrowed surface of **30** exports. These symbols are a
56
54
  | [`ApprovalRegistry`](https://github.com/tonywied17/zero-transfer/blob/main/docs/api-md/classes/ApprovalRegistry.md) | Class | See API reference. |
57
55
  | [`createApprovalGate`](https://github.com/tonywied17/zero-transfer/blob/main/docs/api-md/functions/createApprovalGate.md) | Function | See API reference. |
58
56
  | [`ApprovalRejectedError`](https://github.com/tonywied17/zero-transfer/blob/main/docs/api-md/classes/ApprovalRejectedError.md) | Class | See API reference. |
57
+ | [`ApprovalTimeoutError`](https://github.com/tonywied17/zero-transfer/blob/main/docs/api-md/classes/ApprovalTimeoutError.md) | Class | See API reference. |
59
58
 
60
59
  ## Examples
61
60
 
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(mft_exports, {
33
33
  AbortError: () => AbortError,
34
34
  ApprovalRegistry: () => ApprovalRegistry,
35
35
  ApprovalRejectedError: () => ApprovalRejectedError,
36
+ ApprovalTimeoutError: () => ApprovalTimeoutError,
36
37
  AuthenticationError: () => AuthenticationError,
37
38
  AuthorizationError: () => AuthorizationError,
38
39
  CLASSIC_PROVIDER_IDS: () => CLASSIC_PROVIDER_IDS,
@@ -70,6 +71,7 @@ __export(mft_exports, {
70
71
  createApprovalGate: () => createApprovalGate,
71
72
  createAtomicDeployPlan: () => createAtomicDeployPlan,
72
73
  createBandwidthThrottle: () => createBandwidthThrottle,
74
+ createDefaultRetryPolicy: () => createDefaultRetryPolicy,
73
75
  createInboxRoute: () => createInboxRoute,
74
76
  createJsonlAuditLog: () => createJsonlAuditLog,
75
77
  createLocalProviderFactory: () => createLocalProviderFactory,
@@ -117,8 +119,10 @@ __export(mft_exports, {
117
119
  parseRemoteManifest: () => parseRemoteManifest,
118
120
  redactCommand: () => redactCommand,
119
121
  redactConnectionProfile: () => redactConnectionProfile,
122
+ redactErrorForLogging: () => redactErrorForLogging,
120
123
  redactObject: () => redactObject,
121
124
  redactSecretSource: () => redactSecretSource,
125
+ redactUrlForLogging: () => redactUrlForLogging,
122
126
  redactValue: () => redactValue,
123
127
  resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
124
128
  resolveOpenSshHost: () => resolveOpenSshHost,
@@ -143,6 +147,68 @@ module.exports = __toCommonJS(mft_exports);
143
147
  // src/client/ZeroTransfer.ts
144
148
  var import_node_events = require("events");
145
149
 
150
+ // src/logging/redaction.ts
151
+ var REDACTED = "[REDACTED]";
152
+ var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
153
+ var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
154
+ var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
155
+ function isSensitiveKey(key) {
156
+ return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
157
+ }
158
+ function redactCommand(command) {
159
+ return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
160
+ return `${commandName.toUpperCase()} ${REDACTED}`;
161
+ });
162
+ }
163
+ function redactValue(value) {
164
+ if (typeof value === "string") {
165
+ return redactCommand(value);
166
+ }
167
+ if (Array.isArray(value)) {
168
+ return value.map((item) => redactValue(item));
169
+ }
170
+ if (value !== null && typeof value === "object") {
171
+ return redactObject(value);
172
+ }
173
+ return value;
174
+ }
175
+ function redactObject(input) {
176
+ return Object.fromEntries(
177
+ Object.entries(input).map(([key, value]) => {
178
+ if (isSensitiveKey(key)) {
179
+ return [key, REDACTED];
180
+ }
181
+ if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
182
+ return [key, redactUrlForLogging(value)];
183
+ }
184
+ return [key, redactValue(value)];
185
+ })
186
+ );
187
+ }
188
+ function redactUrlForLogging(url) {
189
+ let parsed;
190
+ try {
191
+ parsed = typeof url === "string" ? new URL(url) : url;
192
+ } catch {
193
+ return REDACTED;
194
+ }
195
+ const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
196
+ const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
197
+ return `${origin}${parsed.pathname}${query}`;
198
+ }
199
+ function redactErrorForLogging(error) {
200
+ if (error !== null && typeof error === "object") {
201
+ const candidate = error;
202
+ if (typeof candidate.toJSON === "function") {
203
+ return redactObject(candidate.toJSON());
204
+ }
205
+ }
206
+ if (error instanceof Error) {
207
+ return redactObject({ message: error.message, name: error.name });
208
+ }
209
+ return { message: redactValue(typeof error === "string" ? error : String(error)) };
210
+ }
211
+
146
212
  // src/errors/ZeroTransferError.ts
147
213
  var ZeroTransferError = class extends Error {
148
214
  /** Stable machine-readable error code. */
@@ -184,6 +250,11 @@ var ZeroTransferError = class extends Error {
184
250
  /**
185
251
  * Serializes the error into a plain object suitable for logs or API responses.
186
252
  *
253
+ * `details` and `command` are passed through secret redaction so serialized
254
+ * errors never leak credentials, signed URLs, or raw protocol commands. The
255
+ * live {@link ZeroTransferError.details | details} property stays unredacted
256
+ * for programmatic consumers.
257
+ *
187
258
  * @returns A JSON-safe object containing public structured error fields.
188
259
  */
189
260
  toJSON() {
@@ -193,12 +264,12 @@ var ZeroTransferError = class extends Error {
193
264
  message: this.message,
194
265
  protocol: this.protocol,
195
266
  host: this.host,
196
- command: this.command,
267
+ command: this.command === void 0 ? void 0 : redactCommand(this.command),
197
268
  ftpCode: this.ftpCode,
198
269
  sftpCode: this.sftpCode,
199
270
  path: this.path,
200
271
  retryable: this.retryable,
201
- details: this.details
272
+ details: this.details === void 0 ? void 0 : redactObject(this.details)
202
273
  };
203
274
  }
204
275
  };
@@ -721,15 +792,20 @@ var ProviderRegistry = class {
721
792
  var TransferClient = class {
722
793
  /** Provider registry used by this client. */
723
794
  registry;
795
+ /** Execution defaults applied when call sites omit their own values. */
796
+ defaults;
724
797
  logger;
725
798
  /**
726
799
  * Creates a transfer client without opening any provider connections.
727
800
  *
728
- * @param options - Optional registry, provider factories, and logger.
801
+ * @param options - Optional registry, provider factories, logger, and execution defaults.
729
802
  */
730
803
  constructor(options = {}) {
731
804
  this.registry = options.registry ?? new ProviderRegistry();
732
805
  this.logger = options.logger ?? noopLogger;
806
+ if (options.defaults !== void 0) {
807
+ this.defaults = { ...options.defaults };
808
+ }
733
809
  for (const provider of options.providers ?? []) {
734
810
  this.registry.register(provider);
735
811
  }
@@ -1302,18 +1378,25 @@ var TransferEngine = class {
1302
1378
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1303
1379
  this.throwIfAborted(abortScope.signal, job);
1304
1380
  const attemptStartedAt = this.now();
1381
+ const attemptScope = createAttemptScope(
1382
+ abortScope.signal,
1383
+ options.timeout,
1384
+ job,
1385
+ attemptNumber
1386
+ );
1305
1387
  const context = this.createExecutionContext(
1306
1388
  job,
1307
1389
  attemptNumber,
1308
1390
  attemptStartedAt,
1309
1391
  options,
1310
- abortScope.signal,
1392
+ attemptScope.signal,
1311
1393
  (bytesTransferred) => {
1312
1394
  latestBytesTransferred = bytesTransferred;
1313
- }
1395
+ },
1396
+ attemptScope.notifyProgress
1314
1397
  );
1315
1398
  try {
1316
- const result = await runExecutor(executor, context, abortScope.signal, job);
1399
+ const result = await runExecutor(executor, context, attemptScope.signal, job);
1317
1400
  context.throwIfAborted();
1318
1401
  latestBytesTransferred = result.bytesTransferred;
1319
1402
  const completedAt = this.now();
@@ -1331,16 +1414,27 @@ var TransferEngine = class {
1331
1414
  summarizeError(error)
1332
1415
  );
1333
1416
  attempts.push(attempt);
1334
- if (error instanceof AbortError || error instanceof TimeoutError) {
1417
+ if (error instanceof AbortError || abortScope.signal?.aborted === true) {
1335
1418
  throw error;
1336
1419
  }
1337
- const retryInput = { attempt: attemptNumber, error, job };
1420
+ const retryInput = {
1421
+ attempt: attemptNumber,
1422
+ elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1423
+ error,
1424
+ job
1425
+ };
1338
1426
  const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1339
1427
  if (shouldRetry) {
1340
1428
  options.retry?.onRetry?.(retryInput);
1429
+ const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
1430
+ if (delayMs > 0) {
1431
+ await sleepWithAbort(delayMs, abortScope.signal, job);
1432
+ }
1341
1433
  continue;
1342
1434
  }
1343
1435
  throw createTransferFailure(job, error, attempts);
1436
+ } finally {
1437
+ attemptScope.dispose();
1344
1438
  }
1345
1439
  }
1346
1440
  throw createTransferFailure(job, void 0, attempts);
@@ -1348,12 +1442,13 @@ var TransferEngine = class {
1348
1442
  abortScope.dispose();
1349
1443
  }
1350
1444
  }
1351
- createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1445
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
1352
1446
  const context = {
1353
1447
  attempt,
1354
1448
  job,
1355
1449
  reportProgress: (bytesTransferred, totalBytes) => {
1356
1450
  this.throwIfAborted(signal, job);
1451
+ notifyProgress();
1357
1452
  updateBytesTransferred(bytesTransferred);
1358
1453
  const progressInput = {
1359
1454
  bytesTransferred,
@@ -1422,6 +1517,96 @@ function createAbortScope(parentSignal, timeout, job) {
1422
1517
  signal: controller.signal
1423
1518
  };
1424
1519
  }
1520
+ function createAttemptScope(parentSignal, timeout, job, attempt) {
1521
+ const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
1522
+ const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
1523
+ if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
1524
+ const scope = {
1525
+ dispose: () => void 0,
1526
+ notifyProgress: () => void 0
1527
+ };
1528
+ if (parentSignal !== void 0) scope.signal = parentSignal;
1529
+ return scope;
1530
+ }
1531
+ const controller = new AbortController();
1532
+ const retryable = timeout?.retryable ?? true;
1533
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1534
+ if (parentSignal?.aborted === true) {
1535
+ abortFromParent();
1536
+ } else {
1537
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1538
+ }
1539
+ const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
1540
+ controller.abort(
1541
+ new TimeoutError({
1542
+ details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
1543
+ message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
1544
+ retryable
1545
+ })
1546
+ );
1547
+ }, attemptTimeoutMs);
1548
+ let stallTimer;
1549
+ const armStallWatchdog = () => {
1550
+ if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
1551
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1552
+ stallTimer = setTimeout(() => {
1553
+ controller.abort(
1554
+ new TimeoutError({
1555
+ details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
1556
+ message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
1557
+ retryable
1558
+ })
1559
+ );
1560
+ }, stallTimeoutMs);
1561
+ };
1562
+ armStallWatchdog();
1563
+ return {
1564
+ dispose: () => {
1565
+ if (attemptTimer !== void 0) clearTimeout(attemptTimer);
1566
+ if (stallTimer !== void 0) clearTimeout(stallTimer);
1567
+ parentSignal?.removeEventListener("abort", abortFromParent);
1568
+ },
1569
+ notifyProgress: armStallWatchdog,
1570
+ signal: controller.signal
1571
+ };
1572
+ }
1573
+ function sleepWithAbort(delayMs, signal, job) {
1574
+ return new Promise((resolve, reject) => {
1575
+ if (signal === void 0) {
1576
+ setTimeout(resolve, delayMs);
1577
+ return;
1578
+ }
1579
+ if (signal.aborted) {
1580
+ reject(toAbortFailure(signal, job));
1581
+ return;
1582
+ }
1583
+ const rejectAbort = () => {
1584
+ clearTimeout(timer);
1585
+ reject(toAbortFailure(signal, job));
1586
+ };
1587
+ const timer = setTimeout(() => {
1588
+ signal.removeEventListener("abort", rejectAbort);
1589
+ resolve();
1590
+ }, delayMs);
1591
+ signal.addEventListener("abort", rejectAbort, { once: true });
1592
+ });
1593
+ }
1594
+ function toAbortFailure(signal, job) {
1595
+ if (signal.reason instanceof ZeroTransferError) {
1596
+ return signal.reason;
1597
+ }
1598
+ return new AbortError({
1599
+ details: { jobId: job.id, operation: job.operation },
1600
+ message: `Transfer job aborted: ${job.id}`,
1601
+ retryable: false
1602
+ });
1603
+ }
1604
+ function normalizeDelayMs(value) {
1605
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1606
+ return 0;
1607
+ }
1608
+ return Math.floor(value);
1609
+ }
1425
1610
  function normalizeTimeoutMs(value) {
1426
1611
  if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1427
1612
  return void 0;
@@ -1590,7 +1775,7 @@ async function runRoute(options) {
1590
1775
  const executor = createProviderTransferExecutor({
1591
1776
  resolveSession: ({ role }) => sessions.get(role)
1592
1777
  });
1593
- return await engine.execute(job, executor, buildExecuteOptions(options));
1778
+ return await engine.execute(job, executor, buildExecuteOptions(options, client));
1594
1779
  } finally {
1595
1780
  if (destinationSession !== void 0) {
1596
1781
  await destinationSession.disconnect();
@@ -1627,12 +1812,14 @@ function defaultJobId(route, now) {
1627
1812
  const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1628
1813
  return `route:${route.id}:${timestamp.toString(36)}`;
1629
1814
  }
1630
- function buildExecuteOptions(options) {
1815
+ function buildExecuteOptions(options, client) {
1631
1816
  const execute = {};
1817
+ const retry = options.retry ?? client.defaults?.retry;
1818
+ const timeout = options.timeout ?? client.defaults?.timeout;
1632
1819
  if (options.signal !== void 0) execute.signal = options.signal;
1633
- if (options.retry !== void 0) execute.retry = options.retry;
1820
+ if (retry !== void 0) execute.retry = retry;
1634
1821
  if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1635
- if (options.timeout !== void 0) execute.timeout = options.timeout;
1822
+ if (timeout !== void 0) execute.timeout = timeout;
1636
1823
  if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1637
1824
  return execute;
1638
1825
  }
@@ -1689,41 +1876,6 @@ function defaultRouteSuffix(source, destination) {
1689
1876
  return `${source}->${destination}`;
1690
1877
  }
1691
1878
 
1692
- // src/logging/redaction.ts
1693
- var REDACTED = "[REDACTED]";
1694
- var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
1695
- var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
1696
- function isSensitiveKey(key) {
1697
- return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
1698
- }
1699
- function redactCommand(command) {
1700
- return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
1701
- return `${commandName.toUpperCase()} ${REDACTED}`;
1702
- });
1703
- }
1704
- function redactValue(value) {
1705
- if (typeof value === "string") {
1706
- return redactCommand(value);
1707
- }
1708
- if (Array.isArray(value)) {
1709
- return value.map((item) => redactValue(item));
1710
- }
1711
- if (value !== null && typeof value === "object") {
1712
- return redactObject(value);
1713
- }
1714
- return value;
1715
- }
1716
- function redactObject(input) {
1717
- return Object.fromEntries(
1718
- Object.entries(input).map(([key, value]) => {
1719
- if (isSensitiveKey(key)) {
1720
- return [key, REDACTED];
1721
- }
1722
- return [key, redactValue(value)];
1723
- })
1724
- );
1725
- }
1726
-
1727
1879
  // src/profiles/SecretSource.ts
1728
1880
  var import_node_buffer2 = require("buffer");
1729
1881
  var import_promises = require("fs/promises");
@@ -2095,11 +2247,11 @@ var import_promises2 = require("fs/promises");
2095
2247
  var import_node_path2 = __toESM(require("path"));
2096
2248
 
2097
2249
  // src/utils/path.ts
2098
- var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
2250
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
2099
2251
  function assertSafeFtpArgument(value, label = "path") {
2100
2252
  if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
2101
2253
  throw new ConfigurationError({
2102
- message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
2254
+ message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
2103
2255
  retryable: false,
2104
2256
  details: {
2105
2257
  label
@@ -3401,7 +3553,6 @@ function expandAlgorithms(values) {
3401
3553
  }
3402
3554
 
3403
3555
  // src/profiles/importers/FileZillaImporter.ts
3404
- var import_node_buffer5 = require("buffer");
3405
3556
  function importFileZillaSites(xml) {
3406
3557
  const events = tokenizeXml(xml);
3407
3558
  if (events.length === 0) {
@@ -3417,7 +3568,6 @@ function importFileZillaSites(xml) {
3417
3568
  const folderNamePending = [];
3418
3569
  let inServer = false;
3419
3570
  let serverFields = {};
3420
- let serverPasswordEncoding;
3421
3571
  let activeTag;
3422
3572
  let captureFolderName = false;
3423
3573
  for (const event of events) {
@@ -3430,13 +3580,9 @@ function importFileZillaSites(xml) {
3430
3580
  if (event.name === "Server") {
3431
3581
  inServer = true;
3432
3582
  serverFields = {};
3433
- serverPasswordEncoding = void 0;
3434
3583
  continue;
3435
3584
  }
3436
3585
  activeTag = event.name;
3437
- if (event.name === "Pass" && inServer) {
3438
- serverPasswordEncoding = event.attributes["encoding"];
3439
- }
3440
3586
  if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
3441
3587
  captureFolderName = true;
3442
3588
  }
@@ -3462,7 +3608,7 @@ function importFileZillaSites(xml) {
3462
3608
  }
3463
3609
  if (event.name === "Server") {
3464
3610
  const folder = folderStack.filter((segment) => segment !== "");
3465
- const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
3611
+ const result = buildSiteFromFields(serverFields);
3466
3612
  if (result.kind === "site") {
3467
3613
  sites.push({ ...result.site, folder });
3468
3614
  } else {
@@ -3474,7 +3620,6 @@ function importFileZillaSites(xml) {
3474
3620
  }
3475
3621
  inServer = false;
3476
3622
  serverFields = {};
3477
- serverPasswordEncoding = void 0;
3478
3623
  activeTag = void 0;
3479
3624
  continue;
3480
3625
  }
@@ -3483,7 +3628,7 @@ function importFileZillaSites(xml) {
3483
3628
  }
3484
3629
  return { sites, skipped };
3485
3630
  }
3486
- function buildSiteFromFields(fields, passwordEncoding) {
3631
+ function buildSiteFromFields(fields) {
3487
3632
  const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
3488
3633
  const host = (fields["Host"] ?? "").trim();
3489
3634
  if (host === "") return { kind: "skipped", name };
@@ -3502,18 +3647,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
3502
3647
  }
3503
3648
  const user = fields["User"]?.trim();
3504
3649
  if (user !== void 0 && user !== "") profile.username = { value: user };
3505
- let password;
3506
3650
  const rawPass = fields["Pass"];
3507
- if (rawPass !== void 0 && rawPass !== "") {
3508
- if (passwordEncoding === "base64") {
3509
- password = import_node_buffer5.Buffer.from(rawPass, "base64").toString("utf8");
3510
- } else {
3511
- password = rawPass;
3512
- }
3513
- if (password !== void 0 && password !== "") profile.password = { value: password };
3514
- }
3515
- const site = { name, profile };
3516
- if (password !== void 0) site.password = password;
3651
+ const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
3652
+ const site = { hasStoredPassword, name, profile };
3517
3653
  const logonText = fields["Logontype"];
3518
3654
  if (logonText !== void 0) {
3519
3655
  const logonType = Number.parseInt(logonText.trim(), 10);
@@ -3756,6 +3892,62 @@ function mapFtp550(details) {
3756
3892
  return new PermissionDeniedError(details);
3757
3893
  }
3758
3894
 
3895
+ // src/transfers/createDefaultRetryPolicy.ts
3896
+ var DEFAULT_MAX_ATTEMPTS = 4;
3897
+ var DEFAULT_BASE_DELAY_MS = 250;
3898
+ var DEFAULT_MAX_DELAY_MS = 3e4;
3899
+ var DEFAULT_MAX_ELAPSED_MS = 3e5;
3900
+ function createDefaultRetryPolicy(options = {}) {
3901
+ const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
3902
+ const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
3903
+ const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
3904
+ const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
3905
+ const random = options.random ?? Math.random;
3906
+ return {
3907
+ getDelayMs(input) {
3908
+ const retryAfterMs = readRetryAfterMs(input.error);
3909
+ if (retryAfterMs !== void 0) {
3910
+ return retryAfterMs;
3911
+ }
3912
+ const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
3913
+ const cappedMs = Math.min(maxDelayMs, exponentialMs);
3914
+ return Math.floor(random() * cappedMs);
3915
+ },
3916
+ maxAttempts,
3917
+ shouldRetry(input) {
3918
+ if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
3919
+ return false;
3920
+ }
3921
+ if (input.elapsedMs >= maxElapsedMs) {
3922
+ return false;
3923
+ }
3924
+ const retryAfterMs = readRetryAfterMs(input.error);
3925
+ if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
3926
+ return false;
3927
+ }
3928
+ return true;
3929
+ }
3930
+ };
3931
+ }
3932
+ function readRetryAfterMs(error) {
3933
+ if (!(error instanceof ZeroTransferError)) return void 0;
3934
+ const value = error.details?.["retryAfterMs"];
3935
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
3936
+ return Math.floor(value);
3937
+ }
3938
+ function normalizePositiveInteger(value, fallback) {
3939
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
3940
+ return fallback;
3941
+ }
3942
+ return Math.floor(value);
3943
+ }
3944
+ function normalizeNonNegative(value, fallback) {
3945
+ if (value === void 0 || !Number.isFinite(value) || value < 0) {
3946
+ return fallback;
3947
+ }
3948
+ return Math.floor(value);
3949
+ }
3950
+
3759
3951
  // src/transfers/TransferPlan.ts
3760
3952
  function createTransferPlan(input) {
3761
3953
  const plan = {
@@ -3853,8 +4045,8 @@ var TransferQueue = class {
3853
4045
  this.concurrency = normalizeConcurrency(options.concurrency);
3854
4046
  this.defaultExecutor = options.executor;
3855
4047
  this.resolveExecutor = options.resolveExecutor;
3856
- this.retry = options.retry;
3857
- this.timeout = options.timeout;
4048
+ this.retry = options.retry ?? options.client?.defaults?.retry;
4049
+ this.timeout = options.timeout ?? options.client?.defaults?.timeout;
3858
4050
  this.bandwidthLimit = options.bandwidthLimit;
3859
4051
  this.onProgress = options.onProgress;
3860
4052
  this.onReceipt = options.onReceipt;
@@ -5267,6 +5459,7 @@ async function dispatchWebhook(options) {
5267
5459
  return { attempts: attempt, delivered: false, status: lastStatus };
5268
5460
  }
5269
5461
  function createWebhookAuditLog(options) {
5462
+ validateTarget(options.target);
5270
5463
  return {
5271
5464
  list: () => Promise.resolve([]),
5272
5465
  record: async (entry) => {
@@ -5289,6 +5482,24 @@ function validateTarget(target) {
5289
5482
  retryable: false
5290
5483
  });
5291
5484
  }
5485
+ let parsed;
5486
+ try {
5487
+ parsed = new URL(target.url);
5488
+ } catch (error) {
5489
+ throw new ConfigurationError({
5490
+ cause: error,
5491
+ details: { url: target.url },
5492
+ message: "Webhook target url must be an absolute URL",
5493
+ retryable: false
5494
+ });
5495
+ }
5496
+ if (parsed.protocol === "https:") return;
5497
+ if (parsed.protocol === "http:" && target.allowInsecureUrl === true) return;
5498
+ throw new ConfigurationError({
5499
+ details: { protocol: parsed.protocol, url: target.url },
5500
+ 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}"`,
5501
+ retryable: false
5502
+ });
5292
5503
  }
5293
5504
  function normalizeRetry(retry) {
5294
5505
  return {
@@ -5325,6 +5536,26 @@ var ApprovalRejectedError = class _ApprovalRejectedError extends ZeroTransferErr
5325
5536
  }
5326
5537
  request;
5327
5538
  };
5539
+ var ApprovalTimeoutError = class _ApprovalTimeoutError extends ZeroTransferError {
5540
+ /**
5541
+ * Creates an approval timeout error.
5542
+ *
5543
+ * @param request - The approval request that timed out while pending.
5544
+ * @param timeoutMs - Configured timeout window in milliseconds.
5545
+ */
5546
+ constructor(request, timeoutMs) {
5547
+ super({
5548
+ code: "approval_timeout",
5549
+ details: { approvalId: request.id, routeId: request.routeId, timeoutMs },
5550
+ message: `Approval "${request.id}" for route "${request.routeId}" timed out after ${String(timeoutMs)}ms`,
5551
+ retryable: false
5552
+ });
5553
+ this.request = request;
5554
+ Object.setPrototypeOf(this, _ApprovalTimeoutError.prototype);
5555
+ this.name = "ApprovalTimeoutError";
5556
+ }
5557
+ request;
5558
+ };
5328
5559
  var ApprovalRegistry = class {
5329
5560
  requests = /* @__PURE__ */ new Map();
5330
5561
  pending = /* @__PURE__ */ new Map();
@@ -5450,9 +5681,27 @@ function createApprovalGate(options) {
5450
5681
  };
5451
5682
  if (input.signal.aborted) onAbort();
5452
5683
  input.signal.addEventListener("abort", onAbort);
5684
+ let timeoutTimer;
5685
+ const timeoutMs = options.timeoutMs;
5686
+ const pendingPromises = [settled];
5687
+ if (timeoutMs !== void 0) {
5688
+ pendingPromises.push(
5689
+ new Promise((_resolve, reject) => {
5690
+ timeoutTimer = setTimeout(() => {
5691
+ const current = options.registry.get(approvalId) ?? request;
5692
+ reject(new ApprovalTimeoutError(current, timeoutMs));
5693
+ if (current.status === "pending") {
5694
+ settled.catch(() => void 0);
5695
+ options.registry.reject(approvalId, { reason: "timeout" }, now());
5696
+ }
5697
+ }, timeoutMs);
5698
+ })
5699
+ );
5700
+ }
5453
5701
  try {
5454
- await settled;
5702
+ await Promise.race(pendingPromises);
5455
5703
  } finally {
5704
+ if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
5456
5705
  input.signal.removeEventListener("abort", onAbort);
5457
5706
  }
5458
5707
  return options.runner(input);
@@ -5812,6 +6061,7 @@ var defaultRunner = ({ client, route, signal }) => {
5812
6061
  AbortError,
5813
6062
  ApprovalRegistry,
5814
6063
  ApprovalRejectedError,
6064
+ ApprovalTimeoutError,
5815
6065
  AuthenticationError,
5816
6066
  AuthorizationError,
5817
6067
  CLASSIC_PROVIDER_IDS,
@@ -5849,6 +6099,7 @@ var defaultRunner = ({ client, route, signal }) => {
5849
6099
  createApprovalGate,
5850
6100
  createAtomicDeployPlan,
5851
6101
  createBandwidthThrottle,
6102
+ createDefaultRetryPolicy,
5852
6103
  createInboxRoute,
5853
6104
  createJsonlAuditLog,
5854
6105
  createLocalProviderFactory,
@@ -5896,8 +6147,10 @@ var defaultRunner = ({ client, route, signal }) => {
5896
6147
  parseRemoteManifest,
5897
6148
  redactCommand,
5898
6149
  redactConnectionProfile,
6150
+ redactErrorForLogging,
5899
6151
  redactObject,
5900
6152
  redactSecretSource,
6153
+ redactUrlForLogging,
5901
6154
  redactValue,
5902
6155
  resolveConnectionProfileSecrets,
5903
6156
  resolveOpenSshHost,