@zero-transfer/ftp 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 +0 -2
- package/dist/index.cjs +348 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +228 -24
- package/dist/index.d.ts +228 -24
- package/dist/index.mjs +345 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,7 @@ __export(ftp_exports, {
|
|
|
61
61
|
copyBetween: () => copyBetween,
|
|
62
62
|
createAtomicDeployPlan: () => createAtomicDeployPlan,
|
|
63
63
|
createBandwidthThrottle: () => createBandwidthThrottle,
|
|
64
|
+
createDefaultRetryPolicy: () => createDefaultRetryPolicy,
|
|
64
65
|
createFtpProviderFactory: () => createFtpProviderFactory,
|
|
65
66
|
createLocalProviderFactory: () => createLocalProviderFactory,
|
|
66
67
|
createMemoryProviderFactory: () => createMemoryProviderFactory,
|
|
@@ -104,8 +105,10 @@ __export(ftp_exports, {
|
|
|
104
105
|
parseUnixListLine: () => parseUnixListLine,
|
|
105
106
|
redactCommand: () => redactCommand,
|
|
106
107
|
redactConnectionProfile: () => redactConnectionProfile,
|
|
108
|
+
redactErrorForLogging: () => redactErrorForLogging,
|
|
107
109
|
redactObject: () => redactObject,
|
|
108
110
|
redactSecretSource: () => redactSecretSource,
|
|
111
|
+
redactUrlForLogging: () => redactUrlForLogging,
|
|
109
112
|
redactValue: () => redactValue,
|
|
110
113
|
resolveConnectionProfileSecrets: () => resolveConnectionProfileSecrets,
|
|
111
114
|
resolveOpenSshHost: () => resolveOpenSshHost,
|
|
@@ -126,6 +129,68 @@ module.exports = __toCommonJS(ftp_exports);
|
|
|
126
129
|
// src/client/ZeroTransfer.ts
|
|
127
130
|
var import_node_events = require("events");
|
|
128
131
|
|
|
132
|
+
// src/logging/redaction.ts
|
|
133
|
+
var REDACTED = "[REDACTED]";
|
|
134
|
+
var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
|
|
135
|
+
var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
|
|
136
|
+
var URL_KEY_PATTERN = /(?:url|uri|href)$/i;
|
|
137
|
+
function isSensitiveKey(key) {
|
|
138
|
+
return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
|
|
139
|
+
}
|
|
140
|
+
function redactCommand(command) {
|
|
141
|
+
return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
|
|
142
|
+
return `${commandName.toUpperCase()} ${REDACTED}`;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function redactValue(value) {
|
|
146
|
+
if (typeof value === "string") {
|
|
147
|
+
return redactCommand(value);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
return value.map((item) => redactValue(item));
|
|
151
|
+
}
|
|
152
|
+
if (value !== null && typeof value === "object") {
|
|
153
|
+
return redactObject(value);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
function redactObject(input) {
|
|
158
|
+
return Object.fromEntries(
|
|
159
|
+
Object.entries(input).map(([key, value]) => {
|
|
160
|
+
if (isSensitiveKey(key)) {
|
|
161
|
+
return [key, REDACTED];
|
|
162
|
+
}
|
|
163
|
+
if (URL_KEY_PATTERN.test(key) && typeof value === "string") {
|
|
164
|
+
return [key, redactUrlForLogging(value)];
|
|
165
|
+
}
|
|
166
|
+
return [key, redactValue(value)];
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
function redactUrlForLogging(url) {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = typeof url === "string" ? new URL(url) : url;
|
|
174
|
+
} catch {
|
|
175
|
+
return REDACTED;
|
|
176
|
+
}
|
|
177
|
+
const origin = parsed.host.length > 0 ? `${parsed.protocol}//${parsed.host}` : parsed.protocol;
|
|
178
|
+
const query = parsed.search.length > 0 ? `?${REDACTED}` : "";
|
|
179
|
+
return `${origin}${parsed.pathname}${query}`;
|
|
180
|
+
}
|
|
181
|
+
function redactErrorForLogging(error) {
|
|
182
|
+
if (error !== null && typeof error === "object") {
|
|
183
|
+
const candidate = error;
|
|
184
|
+
if (typeof candidate.toJSON === "function") {
|
|
185
|
+
return redactObject(candidate.toJSON());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (error instanceof Error) {
|
|
189
|
+
return redactObject({ message: error.message, name: error.name });
|
|
190
|
+
}
|
|
191
|
+
return { message: redactValue(typeof error === "string" ? error : String(error)) };
|
|
192
|
+
}
|
|
193
|
+
|
|
129
194
|
// src/errors/ZeroTransferError.ts
|
|
130
195
|
var ZeroTransferError = class extends Error {
|
|
131
196
|
/** Stable machine-readable error code. */
|
|
@@ -167,6 +232,11 @@ var ZeroTransferError = class extends Error {
|
|
|
167
232
|
/**
|
|
168
233
|
* Serializes the error into a plain object suitable for logs or API responses.
|
|
169
234
|
*
|
|
235
|
+
* `details` and `command` are passed through secret redaction so serialized
|
|
236
|
+
* errors never leak credentials, signed URLs, or raw protocol commands. The
|
|
237
|
+
* live {@link ZeroTransferError.details | details} property stays unredacted
|
|
238
|
+
* for programmatic consumers.
|
|
239
|
+
*
|
|
170
240
|
* @returns A JSON-safe object containing public structured error fields.
|
|
171
241
|
*/
|
|
172
242
|
toJSON() {
|
|
@@ -176,12 +246,12 @@ var ZeroTransferError = class extends Error {
|
|
|
176
246
|
message: this.message,
|
|
177
247
|
protocol: this.protocol,
|
|
178
248
|
host: this.host,
|
|
179
|
-
command: this.command,
|
|
249
|
+
command: this.command === void 0 ? void 0 : redactCommand(this.command),
|
|
180
250
|
ftpCode: this.ftpCode,
|
|
181
251
|
sftpCode: this.sftpCode,
|
|
182
252
|
path: this.path,
|
|
183
253
|
retryable: this.retryable,
|
|
184
|
-
details: this.details
|
|
254
|
+
details: this.details === void 0 ? void 0 : redactObject(this.details)
|
|
185
255
|
};
|
|
186
256
|
}
|
|
187
257
|
};
|
|
@@ -704,15 +774,20 @@ var ProviderRegistry = class {
|
|
|
704
774
|
var TransferClient = class {
|
|
705
775
|
/** Provider registry used by this client. */
|
|
706
776
|
registry;
|
|
777
|
+
/** Execution defaults applied when call sites omit their own values. */
|
|
778
|
+
defaults;
|
|
707
779
|
logger;
|
|
708
780
|
/**
|
|
709
781
|
* Creates a transfer client without opening any provider connections.
|
|
710
782
|
*
|
|
711
|
-
* @param options - Optional registry, provider factories, and
|
|
783
|
+
* @param options - Optional registry, provider factories, logger, and execution defaults.
|
|
712
784
|
*/
|
|
713
785
|
constructor(options = {}) {
|
|
714
786
|
this.registry = options.registry ?? new ProviderRegistry();
|
|
715
787
|
this.logger = options.logger ?? noopLogger;
|
|
788
|
+
if (options.defaults !== void 0) {
|
|
789
|
+
this.defaults = { ...options.defaults };
|
|
790
|
+
}
|
|
716
791
|
for (const provider of options.providers ?? []) {
|
|
717
792
|
this.registry.register(provider);
|
|
718
793
|
}
|
|
@@ -1285,18 +1360,25 @@ var TransferEngine = class {
|
|
|
1285
1360
|
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
1286
1361
|
this.throwIfAborted(abortScope.signal, job);
|
|
1287
1362
|
const attemptStartedAt = this.now();
|
|
1363
|
+
const attemptScope = createAttemptScope(
|
|
1364
|
+
abortScope.signal,
|
|
1365
|
+
options.timeout,
|
|
1366
|
+
job,
|
|
1367
|
+
attemptNumber
|
|
1368
|
+
);
|
|
1288
1369
|
const context = this.createExecutionContext(
|
|
1289
1370
|
job,
|
|
1290
1371
|
attemptNumber,
|
|
1291
1372
|
attemptStartedAt,
|
|
1292
1373
|
options,
|
|
1293
|
-
|
|
1374
|
+
attemptScope.signal,
|
|
1294
1375
|
(bytesTransferred) => {
|
|
1295
1376
|
latestBytesTransferred = bytesTransferred;
|
|
1296
|
-
}
|
|
1377
|
+
},
|
|
1378
|
+
attemptScope.notifyProgress
|
|
1297
1379
|
);
|
|
1298
1380
|
try {
|
|
1299
|
-
const result = await runExecutor(executor, context,
|
|
1381
|
+
const result = await runExecutor(executor, context, attemptScope.signal, job);
|
|
1300
1382
|
context.throwIfAborted();
|
|
1301
1383
|
latestBytesTransferred = result.bytesTransferred;
|
|
1302
1384
|
const completedAt = this.now();
|
|
@@ -1314,16 +1396,27 @@ var TransferEngine = class {
|
|
|
1314
1396
|
summarizeError(error)
|
|
1315
1397
|
);
|
|
1316
1398
|
attempts.push(attempt);
|
|
1317
|
-
if (error instanceof AbortError ||
|
|
1399
|
+
if (error instanceof AbortError || abortScope.signal?.aborted === true) {
|
|
1318
1400
|
throw error;
|
|
1319
1401
|
}
|
|
1320
|
-
const retryInput = {
|
|
1402
|
+
const retryInput = {
|
|
1403
|
+
attempt: attemptNumber,
|
|
1404
|
+
elapsedMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
|
|
1405
|
+
error,
|
|
1406
|
+
job
|
|
1407
|
+
};
|
|
1321
1408
|
const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
|
|
1322
1409
|
if (shouldRetry) {
|
|
1323
1410
|
options.retry?.onRetry?.(retryInput);
|
|
1411
|
+
const delayMs = normalizeDelayMs(options.retry?.getDelayMs?.(retryInput));
|
|
1412
|
+
if (delayMs > 0) {
|
|
1413
|
+
await sleepWithAbort(delayMs, abortScope.signal, job);
|
|
1414
|
+
}
|
|
1324
1415
|
continue;
|
|
1325
1416
|
}
|
|
1326
1417
|
throw createTransferFailure(job, error, attempts);
|
|
1418
|
+
} finally {
|
|
1419
|
+
attemptScope.dispose();
|
|
1327
1420
|
}
|
|
1328
1421
|
}
|
|
1329
1422
|
throw createTransferFailure(job, void 0, attempts);
|
|
@@ -1331,12 +1424,13 @@ var TransferEngine = class {
|
|
|
1331
1424
|
abortScope.dispose();
|
|
1332
1425
|
}
|
|
1333
1426
|
}
|
|
1334
|
-
createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
|
|
1427
|
+
createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred, notifyProgress) {
|
|
1335
1428
|
const context = {
|
|
1336
1429
|
attempt,
|
|
1337
1430
|
job,
|
|
1338
1431
|
reportProgress: (bytesTransferred, totalBytes) => {
|
|
1339
1432
|
this.throwIfAborted(signal, job);
|
|
1433
|
+
notifyProgress();
|
|
1340
1434
|
updateBytesTransferred(bytesTransferred);
|
|
1341
1435
|
const progressInput = {
|
|
1342
1436
|
bytesTransferred,
|
|
@@ -1405,6 +1499,96 @@ function createAbortScope(parentSignal, timeout, job) {
|
|
|
1405
1499
|
signal: controller.signal
|
|
1406
1500
|
};
|
|
1407
1501
|
}
|
|
1502
|
+
function createAttemptScope(parentSignal, timeout, job, attempt) {
|
|
1503
|
+
const attemptTimeoutMs = normalizeTimeoutMs(timeout?.attemptTimeoutMs);
|
|
1504
|
+
const stallTimeoutMs = normalizeTimeoutMs(timeout?.stallTimeoutMs);
|
|
1505
|
+
if (attemptTimeoutMs === void 0 && stallTimeoutMs === void 0) {
|
|
1506
|
+
const scope = {
|
|
1507
|
+
dispose: () => void 0,
|
|
1508
|
+
notifyProgress: () => void 0
|
|
1509
|
+
};
|
|
1510
|
+
if (parentSignal !== void 0) scope.signal = parentSignal;
|
|
1511
|
+
return scope;
|
|
1512
|
+
}
|
|
1513
|
+
const controller = new AbortController();
|
|
1514
|
+
const retryable = timeout?.retryable ?? true;
|
|
1515
|
+
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
|
1516
|
+
if (parentSignal?.aborted === true) {
|
|
1517
|
+
abortFromParent();
|
|
1518
|
+
} else {
|
|
1519
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
1520
|
+
}
|
|
1521
|
+
const attemptTimer = attemptTimeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
1522
|
+
controller.abort(
|
|
1523
|
+
new TimeoutError({
|
|
1524
|
+
details: { attempt, attemptTimeoutMs, jobId: job.id, operation: job.operation },
|
|
1525
|
+
message: `Transfer attempt ${String(attempt)} timed out after ${String(attemptTimeoutMs)}ms: ${job.id}`,
|
|
1526
|
+
retryable
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
}, attemptTimeoutMs);
|
|
1530
|
+
let stallTimer;
|
|
1531
|
+
const armStallWatchdog = () => {
|
|
1532
|
+
if (stallTimeoutMs === void 0 || controller.signal.aborted) return;
|
|
1533
|
+
if (stallTimer !== void 0) clearTimeout(stallTimer);
|
|
1534
|
+
stallTimer = setTimeout(() => {
|
|
1535
|
+
controller.abort(
|
|
1536
|
+
new TimeoutError({
|
|
1537
|
+
details: { attempt, jobId: job.id, operation: job.operation, stallTimeoutMs },
|
|
1538
|
+
message: `Transfer attempt ${String(attempt)} stalled (no progress for ${String(stallTimeoutMs)}ms): ${job.id}`,
|
|
1539
|
+
retryable
|
|
1540
|
+
})
|
|
1541
|
+
);
|
|
1542
|
+
}, stallTimeoutMs);
|
|
1543
|
+
};
|
|
1544
|
+
armStallWatchdog();
|
|
1545
|
+
return {
|
|
1546
|
+
dispose: () => {
|
|
1547
|
+
if (attemptTimer !== void 0) clearTimeout(attemptTimer);
|
|
1548
|
+
if (stallTimer !== void 0) clearTimeout(stallTimer);
|
|
1549
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
1550
|
+
},
|
|
1551
|
+
notifyProgress: armStallWatchdog,
|
|
1552
|
+
signal: controller.signal
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function sleepWithAbort(delayMs, signal, job) {
|
|
1556
|
+
return new Promise((resolve, reject) => {
|
|
1557
|
+
if (signal === void 0) {
|
|
1558
|
+
setTimeout(resolve, delayMs);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (signal.aborted) {
|
|
1562
|
+
reject(toAbortFailure(signal, job));
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const rejectAbort = () => {
|
|
1566
|
+
clearTimeout(timer);
|
|
1567
|
+
reject(toAbortFailure(signal, job));
|
|
1568
|
+
};
|
|
1569
|
+
const timer = setTimeout(() => {
|
|
1570
|
+
signal.removeEventListener("abort", rejectAbort);
|
|
1571
|
+
resolve();
|
|
1572
|
+
}, delayMs);
|
|
1573
|
+
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
function toAbortFailure(signal, job) {
|
|
1577
|
+
if (signal.reason instanceof ZeroTransferError) {
|
|
1578
|
+
return signal.reason;
|
|
1579
|
+
}
|
|
1580
|
+
return new AbortError({
|
|
1581
|
+
details: { jobId: job.id, operation: job.operation },
|
|
1582
|
+
message: `Transfer job aborted: ${job.id}`,
|
|
1583
|
+
retryable: false
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
function normalizeDelayMs(value) {
|
|
1587
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) {
|
|
1588
|
+
return 0;
|
|
1589
|
+
}
|
|
1590
|
+
return Math.floor(value);
|
|
1591
|
+
}
|
|
1408
1592
|
function normalizeTimeoutMs(value) {
|
|
1409
1593
|
if (value === void 0 || !Number.isFinite(value) || value <= 0) {
|
|
1410
1594
|
return void 0;
|
|
@@ -1573,7 +1757,7 @@ async function runRoute(options) {
|
|
|
1573
1757
|
const executor = createProviderTransferExecutor({
|
|
1574
1758
|
resolveSession: ({ role }) => sessions.get(role)
|
|
1575
1759
|
});
|
|
1576
|
-
return await engine.execute(job, executor, buildExecuteOptions(options));
|
|
1760
|
+
return await engine.execute(job, executor, buildExecuteOptions(options, client));
|
|
1577
1761
|
} finally {
|
|
1578
1762
|
if (destinationSession !== void 0) {
|
|
1579
1763
|
await destinationSession.disconnect();
|
|
@@ -1610,12 +1794,14 @@ function defaultJobId(route, now) {
|
|
|
1610
1794
|
const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1611
1795
|
return `route:${route.id}:${timestamp.toString(36)}`;
|
|
1612
1796
|
}
|
|
1613
|
-
function buildExecuteOptions(options) {
|
|
1797
|
+
function buildExecuteOptions(options, client) {
|
|
1614
1798
|
const execute = {};
|
|
1799
|
+
const retry = options.retry ?? client.defaults?.retry;
|
|
1800
|
+
const timeout = options.timeout ?? client.defaults?.timeout;
|
|
1615
1801
|
if (options.signal !== void 0) execute.signal = options.signal;
|
|
1616
|
-
if (
|
|
1802
|
+
if (retry !== void 0) execute.retry = retry;
|
|
1617
1803
|
if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
|
|
1618
|
-
if (
|
|
1804
|
+
if (timeout !== void 0) execute.timeout = timeout;
|
|
1619
1805
|
if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
|
|
1620
1806
|
return execute;
|
|
1621
1807
|
}
|
|
@@ -1672,41 +1858,6 @@ function defaultRouteSuffix(source, destination) {
|
|
|
1672
1858
|
return `${source}->${destination}`;
|
|
1673
1859
|
}
|
|
1674
1860
|
|
|
1675
|
-
// src/logging/redaction.ts
|
|
1676
|
-
var REDACTED = "[REDACTED]";
|
|
1677
|
-
var SENSITIVE_KEY_PATTERN = /(?:password|passphrase|privatekey|token|secret|username|user)$/i;
|
|
1678
|
-
var SECRET_COMMAND_PATTERN = /^(PASS|USER|ACCT)\s+(.+)$/i;
|
|
1679
|
-
function isSensitiveKey(key) {
|
|
1680
|
-
return SENSITIVE_KEY_PATTERN.test(key.replace(/[_-]/g, ""));
|
|
1681
|
-
}
|
|
1682
|
-
function redactCommand(command) {
|
|
1683
|
-
return command.replace(SECRET_COMMAND_PATTERN, (_fullMatch, commandName) => {
|
|
1684
|
-
return `${commandName.toUpperCase()} ${REDACTED}`;
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
function redactValue(value) {
|
|
1688
|
-
if (typeof value === "string") {
|
|
1689
|
-
return redactCommand(value);
|
|
1690
|
-
}
|
|
1691
|
-
if (Array.isArray(value)) {
|
|
1692
|
-
return value.map((item) => redactValue(item));
|
|
1693
|
-
}
|
|
1694
|
-
if (value !== null && typeof value === "object") {
|
|
1695
|
-
return redactObject(value);
|
|
1696
|
-
}
|
|
1697
|
-
return value;
|
|
1698
|
-
}
|
|
1699
|
-
function redactObject(input) {
|
|
1700
|
-
return Object.fromEntries(
|
|
1701
|
-
Object.entries(input).map(([key, value]) => {
|
|
1702
|
-
if (isSensitiveKey(key)) {
|
|
1703
|
-
return [key, REDACTED];
|
|
1704
|
-
}
|
|
1705
|
-
return [key, redactValue(value)];
|
|
1706
|
-
})
|
|
1707
|
-
);
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
1861
|
// src/profiles/SecretSource.ts
|
|
1711
1862
|
var import_node_buffer2 = require("buffer");
|
|
1712
1863
|
var import_promises = require("fs/promises");
|
|
@@ -2078,11 +2229,11 @@ var import_promises2 = require("fs/promises");
|
|
|
2078
2229
|
var import_node_path2 = __toESM(require("path"));
|
|
2079
2230
|
|
|
2080
2231
|
// src/utils/path.ts
|
|
2081
|
-
var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
|
|
2232
|
+
var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n\0]/;
|
|
2082
2233
|
function assertSafeFtpArgument(value, label = "path") {
|
|
2083
2234
|
if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
|
|
2084
2235
|
throw new ConfigurationError({
|
|
2085
|
-
message: `Unsafe FTP ${label}: CR and
|
|
2236
|
+
message: `Unsafe FTP ${label}: CR, LF, and NUL characters are not allowed`,
|
|
2086
2237
|
retryable: false,
|
|
2087
2238
|
details: {
|
|
2088
2239
|
label
|
|
@@ -3384,7 +3535,6 @@ function expandAlgorithms(values) {
|
|
|
3384
3535
|
}
|
|
3385
3536
|
|
|
3386
3537
|
// src/profiles/importers/FileZillaImporter.ts
|
|
3387
|
-
var import_node_buffer5 = require("buffer");
|
|
3388
3538
|
function importFileZillaSites(xml) {
|
|
3389
3539
|
const events = tokenizeXml(xml);
|
|
3390
3540
|
if (events.length === 0) {
|
|
@@ -3400,7 +3550,6 @@ function importFileZillaSites(xml) {
|
|
|
3400
3550
|
const folderNamePending = [];
|
|
3401
3551
|
let inServer = false;
|
|
3402
3552
|
let serverFields = {};
|
|
3403
|
-
let serverPasswordEncoding;
|
|
3404
3553
|
let activeTag;
|
|
3405
3554
|
let captureFolderName = false;
|
|
3406
3555
|
for (const event of events) {
|
|
@@ -3413,13 +3562,9 @@ function importFileZillaSites(xml) {
|
|
|
3413
3562
|
if (event.name === "Server") {
|
|
3414
3563
|
inServer = true;
|
|
3415
3564
|
serverFields = {};
|
|
3416
|
-
serverPasswordEncoding = void 0;
|
|
3417
3565
|
continue;
|
|
3418
3566
|
}
|
|
3419
3567
|
activeTag = event.name;
|
|
3420
|
-
if (event.name === "Pass" && inServer) {
|
|
3421
|
-
serverPasswordEncoding = event.attributes["encoding"];
|
|
3422
|
-
}
|
|
3423
3568
|
if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
|
|
3424
3569
|
captureFolderName = true;
|
|
3425
3570
|
}
|
|
@@ -3445,7 +3590,7 @@ function importFileZillaSites(xml) {
|
|
|
3445
3590
|
}
|
|
3446
3591
|
if (event.name === "Server") {
|
|
3447
3592
|
const folder = folderStack.filter((segment) => segment !== "");
|
|
3448
|
-
const result = buildSiteFromFields(serverFields
|
|
3593
|
+
const result = buildSiteFromFields(serverFields);
|
|
3449
3594
|
if (result.kind === "site") {
|
|
3450
3595
|
sites.push({ ...result.site, folder });
|
|
3451
3596
|
} else {
|
|
@@ -3457,7 +3602,6 @@ function importFileZillaSites(xml) {
|
|
|
3457
3602
|
}
|
|
3458
3603
|
inServer = false;
|
|
3459
3604
|
serverFields = {};
|
|
3460
|
-
serverPasswordEncoding = void 0;
|
|
3461
3605
|
activeTag = void 0;
|
|
3462
3606
|
continue;
|
|
3463
3607
|
}
|
|
@@ -3466,7 +3610,7 @@ function importFileZillaSites(xml) {
|
|
|
3466
3610
|
}
|
|
3467
3611
|
return { sites, skipped };
|
|
3468
3612
|
}
|
|
3469
|
-
function buildSiteFromFields(fields
|
|
3613
|
+
function buildSiteFromFields(fields) {
|
|
3470
3614
|
const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
|
|
3471
3615
|
const host = (fields["Host"] ?? "").trim();
|
|
3472
3616
|
if (host === "") return { kind: "skipped", name };
|
|
@@ -3485,18 +3629,9 @@ function buildSiteFromFields(fields, passwordEncoding) {
|
|
|
3485
3629
|
}
|
|
3486
3630
|
const user = fields["User"]?.trim();
|
|
3487
3631
|
if (user !== void 0 && user !== "") profile.username = { value: user };
|
|
3488
|
-
let password;
|
|
3489
3632
|
const rawPass = fields["Pass"];
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
password = import_node_buffer5.Buffer.from(rawPass, "base64").toString("utf8");
|
|
3493
|
-
} else {
|
|
3494
|
-
password = rawPass;
|
|
3495
|
-
}
|
|
3496
|
-
if (password !== void 0 && password !== "") profile.password = { value: password };
|
|
3497
|
-
}
|
|
3498
|
-
const site = { name, profile };
|
|
3499
|
-
if (password !== void 0) site.password = password;
|
|
3633
|
+
const hasStoredPassword = rawPass !== void 0 && rawPass !== "";
|
|
3634
|
+
const site = { hasStoredPassword, name, profile };
|
|
3500
3635
|
const logonText = fields["Logontype"];
|
|
3501
3636
|
if (logonText !== void 0) {
|
|
3502
3637
|
const logonType = Number.parseInt(logonText.trim(), 10);
|
|
@@ -3739,6 +3874,62 @@ function mapFtp550(details) {
|
|
|
3739
3874
|
return new PermissionDeniedError(details);
|
|
3740
3875
|
}
|
|
3741
3876
|
|
|
3877
|
+
// src/transfers/createDefaultRetryPolicy.ts
|
|
3878
|
+
var DEFAULT_MAX_ATTEMPTS = 4;
|
|
3879
|
+
var DEFAULT_BASE_DELAY_MS = 250;
|
|
3880
|
+
var DEFAULT_MAX_DELAY_MS = 3e4;
|
|
3881
|
+
var DEFAULT_MAX_ELAPSED_MS = 3e5;
|
|
3882
|
+
function createDefaultRetryPolicy(options = {}) {
|
|
3883
|
+
const maxAttempts = normalizePositiveInteger(options.maxAttempts, DEFAULT_MAX_ATTEMPTS);
|
|
3884
|
+
const baseDelayMs = normalizeNonNegative(options.baseDelayMs, DEFAULT_BASE_DELAY_MS);
|
|
3885
|
+
const maxDelayMs = normalizeNonNegative(options.maxDelayMs, DEFAULT_MAX_DELAY_MS);
|
|
3886
|
+
const maxElapsedMs = normalizeNonNegative(options.maxElapsedMs, DEFAULT_MAX_ELAPSED_MS);
|
|
3887
|
+
const random = options.random ?? Math.random;
|
|
3888
|
+
return {
|
|
3889
|
+
getDelayMs(input) {
|
|
3890
|
+
const retryAfterMs = readRetryAfterMs(input.error);
|
|
3891
|
+
if (retryAfterMs !== void 0) {
|
|
3892
|
+
return retryAfterMs;
|
|
3893
|
+
}
|
|
3894
|
+
const exponentialMs = baseDelayMs * 2 ** (input.attempt - 1);
|
|
3895
|
+
const cappedMs = Math.min(maxDelayMs, exponentialMs);
|
|
3896
|
+
return Math.floor(random() * cappedMs);
|
|
3897
|
+
},
|
|
3898
|
+
maxAttempts,
|
|
3899
|
+
shouldRetry(input) {
|
|
3900
|
+
if (!(input.error instanceof ZeroTransferError) || !input.error.retryable) {
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
if (input.elapsedMs >= maxElapsedMs) {
|
|
3904
|
+
return false;
|
|
3905
|
+
}
|
|
3906
|
+
const retryAfterMs = readRetryAfterMs(input.error);
|
|
3907
|
+
if (retryAfterMs !== void 0 && input.elapsedMs + retryAfterMs > maxElapsedMs) {
|
|
3908
|
+
return false;
|
|
3909
|
+
}
|
|
3910
|
+
return true;
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
function readRetryAfterMs(error) {
|
|
3915
|
+
if (!(error instanceof ZeroTransferError)) return void 0;
|
|
3916
|
+
const value = error.details?.["retryAfterMs"];
|
|
3917
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
3918
|
+
return Math.floor(value);
|
|
3919
|
+
}
|
|
3920
|
+
function normalizePositiveInteger(value, fallback) {
|
|
3921
|
+
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
3922
|
+
return fallback;
|
|
3923
|
+
}
|
|
3924
|
+
return Math.floor(value);
|
|
3925
|
+
}
|
|
3926
|
+
function normalizeNonNegative(value, fallback) {
|
|
3927
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) {
|
|
3928
|
+
return fallback;
|
|
3929
|
+
}
|
|
3930
|
+
return Math.floor(value);
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3742
3933
|
// src/transfers/TransferPlan.ts
|
|
3743
3934
|
function createTransferPlan(input) {
|
|
3744
3935
|
const plan = {
|
|
@@ -3836,8 +4027,8 @@ var TransferQueue = class {
|
|
|
3836
4027
|
this.concurrency = normalizeConcurrency(options.concurrency);
|
|
3837
4028
|
this.defaultExecutor = options.executor;
|
|
3838
4029
|
this.resolveExecutor = options.resolveExecutor;
|
|
3839
|
-
this.retry = options.retry;
|
|
3840
|
-
this.timeout = options.timeout;
|
|
4030
|
+
this.retry = options.retry ?? options.client?.defaults?.retry;
|
|
4031
|
+
this.timeout = options.timeout ?? options.client?.defaults?.timeout;
|
|
3841
4032
|
this.bandwidthLimit = options.bandwidthLimit;
|
|
3842
4033
|
this.onProgress = options.onProgress;
|
|
3843
4034
|
this.onReceipt = options.onReceipt;
|
|
@@ -4914,7 +5105,7 @@ function isMainModule(importMetaUrl) {
|
|
|
4914
5105
|
}
|
|
4915
5106
|
|
|
4916
5107
|
// src/providers/classic/ftp/FtpProvider.ts
|
|
4917
|
-
var
|
|
5108
|
+
var import_node_buffer5 = require("buffer");
|
|
4918
5109
|
var import_node_net = require("net");
|
|
4919
5110
|
var import_node_tls = require("tls");
|
|
4920
5111
|
|
|
@@ -5813,38 +6004,53 @@ async function expectCompletion(control, command, path2) {
|
|
|
5813
6004
|
const response = await control.sendCommand(command);
|
|
5814
6005
|
assertPathCommandSucceeded(response, command, path2, control.providerId);
|
|
5815
6006
|
}
|
|
5816
|
-
async function
|
|
5817
|
-
const dataConnection = await openPassiveDataCommand(control, command, path2
|
|
6007
|
+
async function readPassiveLinesCommand(control, command, path2, onLine) {
|
|
6008
|
+
const dataConnection = await openPassiveDataCommand(control, command, path2);
|
|
5818
6009
|
try {
|
|
5819
|
-
const
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
path2,
|
|
5823
|
-
control.providerId
|
|
5824
|
-
);
|
|
6010
|
+
const failure = await consumePassiveLines(dataConnection, control.operationTimeoutMs, {
|
|
6011
|
+
command,
|
|
6012
|
+
onLine,
|
|
6013
|
+
path: path2,
|
|
6014
|
+
providerId: control.providerId
|
|
6015
|
+
});
|
|
5825
6016
|
const finalResponse = await control.readFinalResponse({
|
|
5826
6017
|
command,
|
|
5827
6018
|
operation: "data command completion",
|
|
5828
6019
|
path: path2
|
|
5829
6020
|
});
|
|
5830
6021
|
assertPathCommandSucceeded(finalResponse, command, path2, control.providerId);
|
|
5831
|
-
|
|
6022
|
+
if (failure !== void 0) throw failure;
|
|
5832
6023
|
} catch (error) {
|
|
5833
6024
|
dataConnection.close();
|
|
5834
6025
|
throw error;
|
|
5835
6026
|
}
|
|
5836
6027
|
}
|
|
5837
6028
|
async function readDirectoryEntries(control, path2) {
|
|
6029
|
+
const entries = [];
|
|
6030
|
+
const collectEntry = (entry) => {
|
|
6031
|
+
if (entry.name === "." || entry.name === "..") return;
|
|
6032
|
+
entries.push(entry);
|
|
6033
|
+
};
|
|
5838
6034
|
try {
|
|
5839
|
-
|
|
5840
|
-
|
|
6035
|
+
await readPassiveLinesCommand(control, `MLSD ${path2}`, path2, (rawLine) => {
|
|
6036
|
+
const line = rawLine.trimEnd();
|
|
6037
|
+
if (line.length === 0) return;
|
|
6038
|
+
collectEntry(parseMlsdLine(line, path2));
|
|
6039
|
+
});
|
|
6040
|
+
return entries;
|
|
5841
6041
|
} catch (error) {
|
|
5842
6042
|
if (!isUnsupportedFtpCommandError(error, "MLSD")) {
|
|
5843
6043
|
throw error;
|
|
5844
6044
|
}
|
|
5845
6045
|
}
|
|
5846
|
-
|
|
5847
|
-
|
|
6046
|
+
entries.length = 0;
|
|
6047
|
+
const now = /* @__PURE__ */ new Date();
|
|
6048
|
+
await readPassiveLinesCommand(control, `LIST ${path2}`, path2, (rawLine) => {
|
|
6049
|
+
const line = rawLine.trimEnd();
|
|
6050
|
+
if (line.length === 0 || line.toLowerCase().startsWith("total ")) return;
|
|
6051
|
+
collectEntry(parseUnixListLine(line, path2, now));
|
|
6052
|
+
});
|
|
6053
|
+
return entries;
|
|
5848
6054
|
}
|
|
5849
6055
|
async function openPassiveDataCommand(control, command, path2, options = {}) {
|
|
5850
6056
|
const offset = normalizeOptionalByteCount3(options.offset, "offset", path2);
|
|
@@ -6017,22 +6223,58 @@ function openPassiveDataConnection(endpoint, timeoutMs, path2, control) {
|
|
|
6017
6223
|
}
|
|
6018
6224
|
};
|
|
6019
6225
|
}
|
|
6020
|
-
|
|
6021
|
-
|
|
6226
|
+
var MAX_LIST_LINE_BYTES = 64 * 1024;
|
|
6227
|
+
async function consumePassiveLines(dataConnection, timeoutMs, input) {
|
|
6228
|
+
let carry = import_node_buffer5.Buffer.alloc(0);
|
|
6229
|
+
let failure;
|
|
6022
6230
|
const clearIdleTimeout = setSocketTimeout(dataConnection.socket, timeoutMs, {
|
|
6023
6231
|
host: dataConnection.endpoint.host,
|
|
6024
6232
|
operation: "passive data transfer",
|
|
6025
|
-
path:
|
|
6026
|
-
providerId
|
|
6233
|
+
path: input.path,
|
|
6234
|
+
providerId: input.providerId
|
|
6235
|
+
});
|
|
6236
|
+
const overlongLineFailure = () => new ParseError({
|
|
6237
|
+
details: { command: input.command, limitBytes: MAX_LIST_LINE_BYTES, path: input.path },
|
|
6238
|
+
message: `FTP listing line exceeded ${String(MAX_LIST_LINE_BYTES)} bytes for ${input.command}`,
|
|
6239
|
+
retryable: false
|
|
6027
6240
|
});
|
|
6241
|
+
const emit = (lineBytes) => {
|
|
6242
|
+
if (failure !== void 0) return;
|
|
6243
|
+
let end = lineBytes.length;
|
|
6244
|
+
if (end > 0 && lineBytes[end - 1] === 13) end -= 1;
|
|
6245
|
+
if (end === 0) return;
|
|
6246
|
+
if (end > MAX_LIST_LINE_BYTES) {
|
|
6247
|
+
failure = overlongLineFailure();
|
|
6248
|
+
return;
|
|
6249
|
+
}
|
|
6250
|
+
try {
|
|
6251
|
+
input.onLine(lineBytes.toString("utf8", 0, end));
|
|
6252
|
+
} catch (error) {
|
|
6253
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
6254
|
+
}
|
|
6255
|
+
};
|
|
6028
6256
|
try {
|
|
6029
6257
|
for await (const chunk of dataConnection.socket) {
|
|
6030
|
-
|
|
6258
|
+
if (failure !== void 0) continue;
|
|
6259
|
+
const data = carry.length > 0 ? import_node_buffer5.Buffer.concat([carry, chunk]) : chunk;
|
|
6260
|
+
let start = 0;
|
|
6261
|
+
let newline = data.indexOf(10, start);
|
|
6262
|
+
while (newline !== -1) {
|
|
6263
|
+
emit(data.subarray(start, newline));
|
|
6264
|
+
start = newline + 1;
|
|
6265
|
+
newline = data.indexOf(10, start);
|
|
6266
|
+
}
|
|
6267
|
+
carry = import_node_buffer5.Buffer.from(data.subarray(start));
|
|
6268
|
+
if (carry.length > MAX_LIST_LINE_BYTES && failure === void 0) {
|
|
6269
|
+
failure = overlongLineFailure();
|
|
6270
|
+
}
|
|
6271
|
+
if (failure !== void 0) carry = import_node_buffer5.Buffer.alloc(0);
|
|
6031
6272
|
}
|
|
6273
|
+
if (carry.length > 0) emit(carry);
|
|
6032
6274
|
} finally {
|
|
6033
6275
|
clearIdleTimeout();
|
|
6034
6276
|
}
|
|
6035
|
-
return
|
|
6277
|
+
return failure;
|
|
6036
6278
|
}
|
|
6037
6279
|
async function* createPassiveReadSource(control, dataConnection, command, path2, range, request) {
|
|
6038
6280
|
let bytesEmitted = 0;
|
|
@@ -6049,7 +6291,7 @@ async function* createPassiveReadSource(control, dataConnection, command, path2,
|
|
|
6049
6291
|
});
|
|
6050
6292
|
for await (const chunk of dataConnection.socket) {
|
|
6051
6293
|
request.throwIfAborted();
|
|
6052
|
-
const buffer =
|
|
6294
|
+
const buffer = import_node_buffer5.Buffer.from(chunk);
|
|
6053
6295
|
if (range.length === void 0) {
|
|
6054
6296
|
bytesEmitted += buffer.byteLength;
|
|
6055
6297
|
yield new Uint8Array(buffer);
|
|
@@ -6281,6 +6523,13 @@ function createTlsPinnedFingerprints(profile) {
|
|
|
6281
6523
|
if (pinnedFingerprint256 === void 0) {
|
|
6282
6524
|
return void 0;
|
|
6283
6525
|
}
|
|
6526
|
+
if (profile.tls?.rejectUnauthorized === false) {
|
|
6527
|
+
throw new ConfigurationError({
|
|
6528
|
+
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.",
|
|
6529
|
+
protocol: FTPS_PROVIDER_ID,
|
|
6530
|
+
retryable: false
|
|
6531
|
+
});
|
|
6532
|
+
}
|
|
6284
6533
|
const fingerprints = Array.isArray(pinnedFingerprint256) ? pinnedFingerprint256 : [pinnedFingerprint256];
|
|
6285
6534
|
if (fingerprints.length === 0) {
|
|
6286
6535
|
throw new ConfigurationError({
|
|
@@ -6335,9 +6584,9 @@ function normalizeCertificateFingerprint256(certificate) {
|
|
|
6335
6584
|
}
|
|
6336
6585
|
function normalizeTlsSecretValue(value) {
|
|
6337
6586
|
if (Array.isArray(value)) {
|
|
6338
|
-
return value.map((item) =>
|
|
6587
|
+
return value.map((item) => import_node_buffer5.Buffer.isBuffer(item) ? import_node_buffer5.Buffer.from(item) : item);
|
|
6339
6588
|
}
|
|
6340
|
-
return
|
|
6589
|
+
return import_node_buffer5.Buffer.isBuffer(value) ? import_node_buffer5.Buffer.from(value) : value;
|
|
6341
6590
|
}
|
|
6342
6591
|
async function authenticateFtpSession(control, username, password, host) {
|
|
6343
6592
|
const safeUsername = assertSafeFtpArgument(username, "username");
|
|
@@ -6516,7 +6765,7 @@ function compareEntries5(left, right) {
|
|
|
6516
6765
|
return left.path.localeCompare(right.path);
|
|
6517
6766
|
}
|
|
6518
6767
|
function secretToString(value) {
|
|
6519
|
-
return
|
|
6768
|
+
return import_node_buffer5.Buffer.isBuffer(value) ? value.toString("utf8") : value;
|
|
6520
6769
|
}
|
|
6521
6770
|
|
|
6522
6771
|
// src/providers/classic/ftp/FtpFeatureParser.ts
|
|
@@ -6592,6 +6841,7 @@ function normalizeFeatureLines(input) {
|
|
|
6592
6841
|
copyBetween,
|
|
6593
6842
|
createAtomicDeployPlan,
|
|
6594
6843
|
createBandwidthThrottle,
|
|
6844
|
+
createDefaultRetryPolicy,
|
|
6595
6845
|
createFtpProviderFactory,
|
|
6596
6846
|
createLocalProviderFactory,
|
|
6597
6847
|
createMemoryProviderFactory,
|
|
@@ -6635,8 +6885,10 @@ function normalizeFeatureLines(input) {
|
|
|
6635
6885
|
parseUnixListLine,
|
|
6636
6886
|
redactCommand,
|
|
6637
6887
|
redactConnectionProfile,
|
|
6888
|
+
redactErrorForLogging,
|
|
6638
6889
|
redactObject,
|
|
6639
6890
|
redactSecretSource,
|
|
6891
|
+
redactUrlForLogging,
|
|
6640
6892
|
redactValue,
|
|
6641
6893
|
resolveConnectionProfileSecrets,
|
|
6642
6894
|
resolveOpenSshHost,
|