@zero-transfer/core 0.1.4 → 0.1.6

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,2 +1,4688 @@
1
- // AUTO-GENERATED. Edit scripts/scope-manifest.mjs and re-run packages:generate.
2
- export { TransferClient, createTransferClient, ProviderRegistry, TransferEngine, TransferQueue, createBandwidthThrottle, createProviderTransferExecutor, createTransferJobsFromPlan, createTransferPlan, summarizeTransferPlan, throttleByteIterable, copyBetween, uploadFile, downloadFile, runConnectionDiagnostics, summarizeClientDiagnostics, createLocalProviderFactory, createMemoryProviderFactory, createSyncPlan, diffRemoteTrees, createAtomicDeployPlan, walkRemoteTree, createRemoteBrowser, validateConnectionProfile, resolveConnectionProfileSecrets, redactConnectionProfile, redactSecretSource, resolveSecret, createOAuthTokenSecretSource, ZeroTransferError, noopLogger, emitLog } from "@zero-transfer/sdk";
1
+ // src/client/ZeroTransfer.ts
2
+ import { EventEmitter } from "events";
3
+
4
+ // src/errors/ZeroTransferError.ts
5
+ var ZeroTransferError = class extends Error {
6
+ /** Stable machine-readable error code. */
7
+ code;
8
+ /** Protocol active when the error occurred. */
9
+ protocol;
10
+ /** Remote host associated with the failing operation. */
11
+ host;
12
+ /** Protocol command associated with the failure, if any. */
13
+ command;
14
+ /** FTP response code associated with the failure. */
15
+ ftpCode;
16
+ /** SFTP status code associated with the failure. */
17
+ sftpCode;
18
+ /** Remote path associated with the failure. */
19
+ path;
20
+ /** Whether retry policy may safely retry this failure. */
21
+ retryable;
22
+ /** Additional structured details for diagnostics. */
23
+ details;
24
+ /**
25
+ * Creates a structured SDK error.
26
+ *
27
+ * @param details - Code, message, retryability, and optional protocol context.
28
+ */
29
+ constructor(details) {
30
+ super(details.message, details.cause === void 0 ? void 0 : { cause: details.cause });
31
+ this.name = new.target.name;
32
+ this.code = details.code;
33
+ this.retryable = details.retryable;
34
+ if (details.protocol !== void 0) this.protocol = details.protocol;
35
+ if (details.host !== void 0) this.host = details.host;
36
+ if (details.command !== void 0) this.command = details.command;
37
+ if (details.ftpCode !== void 0) this.ftpCode = details.ftpCode;
38
+ if (details.sftpCode !== void 0) this.sftpCode = details.sftpCode;
39
+ if (details.path !== void 0) this.path = details.path;
40
+ if (details.details !== void 0) this.details = details.details;
41
+ }
42
+ /**
43
+ * Serializes the error into a plain object suitable for logs or API responses.
44
+ *
45
+ * @returns A JSON-safe object containing public structured error fields.
46
+ */
47
+ toJSON() {
48
+ return {
49
+ name: this.name,
50
+ code: this.code,
51
+ message: this.message,
52
+ protocol: this.protocol,
53
+ host: this.host,
54
+ command: this.command,
55
+ ftpCode: this.ftpCode,
56
+ sftpCode: this.sftpCode,
57
+ path: this.path,
58
+ retryable: this.retryable,
59
+ details: this.details
60
+ };
61
+ }
62
+ };
63
+ function withDefaultCode(details, code) {
64
+ return {
65
+ ...details,
66
+ code: details.code ?? code
67
+ };
68
+ }
69
+ var ConnectionError = class extends ZeroTransferError {
70
+ /**
71
+ * Creates a connection failure.
72
+ *
73
+ * @param details - Error context with optional host, protocol, and retryability details.
74
+ */
75
+ constructor(details) {
76
+ super(withDefaultCode(details, "ZERO_TRANSFER_CONNECTION_ERROR"));
77
+ }
78
+ };
79
+ var AuthenticationError = class extends ZeroTransferError {
80
+ /**
81
+ * Creates an authentication failure.
82
+ *
83
+ * @param details - Error context with optional host, protocol, and command details.
84
+ */
85
+ constructor(details) {
86
+ super(withDefaultCode(details, "ZERO_TRANSFER_AUTHENTICATION_ERROR"));
87
+ }
88
+ };
89
+ var AuthorizationError = class extends ZeroTransferError {
90
+ /**
91
+ * Creates an authorization failure.
92
+ *
93
+ * @param details - Error context with optional path and protocol details.
94
+ */
95
+ constructor(details) {
96
+ super(withDefaultCode(details, "ZERO_TRANSFER_AUTHORIZATION_ERROR"));
97
+ }
98
+ };
99
+ var PathNotFoundError = class extends ZeroTransferError {
100
+ /**
101
+ * Creates a missing-path failure.
102
+ *
103
+ * @param details - Error context with optional path and protocol details.
104
+ */
105
+ constructor(details) {
106
+ super(withDefaultCode(details, "ZERO_TRANSFER_PATH_NOT_FOUND"));
107
+ }
108
+ };
109
+ var PathAlreadyExistsError = class extends ZeroTransferError {
110
+ /**
111
+ * Creates an already-exists failure.
112
+ *
113
+ * @param details - Error context with optional path and command details.
114
+ */
115
+ constructor(details) {
116
+ super(withDefaultCode(details, "ZERO_TRANSFER_PATH_ALREADY_EXISTS"));
117
+ }
118
+ };
119
+ var PermissionDeniedError = class extends ZeroTransferError {
120
+ /**
121
+ * Creates a permission failure.
122
+ *
123
+ * @param details - Error context with optional path, command, and protocol details.
124
+ */
125
+ constructor(details) {
126
+ super(withDefaultCode(details, "ZERO_TRANSFER_PERMISSION_DENIED"));
127
+ }
128
+ };
129
+ var TimeoutError = class extends ZeroTransferError {
130
+ /**
131
+ * Creates a timeout failure.
132
+ *
133
+ * @param details - Error context with optional duration and retryability details.
134
+ */
135
+ constructor(details) {
136
+ super(withDefaultCode(details, "ZERO_TRANSFER_TIMEOUT"));
137
+ }
138
+ };
139
+ var AbortError = class extends ZeroTransferError {
140
+ /**
141
+ * Creates an aborted-operation failure.
142
+ *
143
+ * @param details - Error context with optional operation and path details.
144
+ */
145
+ constructor(details) {
146
+ super(withDefaultCode(details, "ZERO_TRANSFER_ABORTED"));
147
+ }
148
+ };
149
+ var ProtocolError = class extends ZeroTransferError {
150
+ /**
151
+ * Creates a protocol failure.
152
+ *
153
+ * @param details - Error context with optional response code and command details.
154
+ */
155
+ constructor(details) {
156
+ super(withDefaultCode(details, "ZERO_TRANSFER_PROTOCOL_ERROR"));
157
+ }
158
+ };
159
+ var ParseError = class extends ZeroTransferError {
160
+ /**
161
+ * Creates a parser failure.
162
+ *
163
+ * @param details - Error context with malformed input details.
164
+ */
165
+ constructor(details) {
166
+ super(withDefaultCode(details, "ZERO_TRANSFER_PARSE_ERROR"));
167
+ }
168
+ };
169
+ var TransferError = class extends ZeroTransferError {
170
+ /**
171
+ * Creates a transfer failure.
172
+ *
173
+ * @param details - Error context with optional path, bytes, and retryability details.
174
+ */
175
+ constructor(details) {
176
+ super(withDefaultCode(details, "ZERO_TRANSFER_TRANSFER_ERROR"));
177
+ }
178
+ };
179
+ var VerificationError = class extends ZeroTransferError {
180
+ /**
181
+ * Creates a verification failure.
182
+ *
183
+ * @param details - Error context with checksum, size, or timestamp mismatch details.
184
+ */
185
+ constructor(details) {
186
+ super(withDefaultCode(details, "ZERO_TRANSFER_VERIFICATION_ERROR"));
187
+ }
188
+ };
189
+ var UnsupportedFeatureError = class extends ZeroTransferError {
190
+ /**
191
+ * Creates an unsupported-feature failure.
192
+ *
193
+ * @param details - Error context describing the missing feature or adapter.
194
+ */
195
+ constructor(details) {
196
+ super(withDefaultCode(details, "ZERO_TRANSFER_UNSUPPORTED_FEATURE"));
197
+ }
198
+ };
199
+ var ConfigurationError = class extends ZeroTransferError {
200
+ /**
201
+ * Creates a configuration failure.
202
+ *
203
+ * @param details - Error context describing the invalid option or argument.
204
+ */
205
+ constructor(details) {
206
+ super(withDefaultCode(details, "ZERO_TRANSFER_CONFIGURATION_ERROR"));
207
+ }
208
+ };
209
+
210
+ // src/logging/Logger.ts
211
+ var noopLogger = {
212
+ trace() {
213
+ },
214
+ debug() {
215
+ },
216
+ info() {
217
+ },
218
+ warn() {
219
+ },
220
+ error() {
221
+ }
222
+ };
223
+ function emitLog(logger, level, record) {
224
+ const method = logger[level];
225
+ if (method === void 0) {
226
+ return;
227
+ }
228
+ const logRecord = {
229
+ ...record,
230
+ level
231
+ };
232
+ method(logRecord, logRecord.message);
233
+ }
234
+
235
+ // src/profiles/ProfileValidator.ts
236
+ import { Buffer } from "buffer";
237
+
238
+ // src/core/ProviderId.ts
239
+ var CLASSIC_PROVIDER_IDS = ["ftp", "ftps", "sftp"];
240
+ function isClassicProviderId(providerId) {
241
+ return typeof providerId === "string" && CLASSIC_PROVIDER_IDS.includes(providerId);
242
+ }
243
+ function resolveProviderId(selection) {
244
+ return selection.provider ?? selection.protocol;
245
+ }
246
+
247
+ // src/profiles/ProfileValidator.ts
248
+ var TLS_VERSIONS = /* @__PURE__ */ new Set(["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"]);
249
+ var SHA256_FINGERPRINT_HEX_LENGTH = 64;
250
+ var SHA256_DIGEST_BYTE_LENGTH = 32;
251
+ function validateConnectionProfile(profile) {
252
+ if (resolveProviderId(profile) === void 0) {
253
+ throw new ConfigurationError({
254
+ message: "Connection profiles must include a provider or protocol",
255
+ retryable: false
256
+ });
257
+ }
258
+ if (profile.host.trim().length === 0) {
259
+ throw new ConfigurationError({
260
+ message: "Connection profiles must include a non-empty host",
261
+ retryable: false
262
+ });
263
+ }
264
+ if (profile.port !== void 0 && !isValidPort(profile.port)) {
265
+ throw new ConfigurationError({
266
+ details: { port: profile.port },
267
+ message: "Connection profile port must be an integer between 1 and 65535",
268
+ retryable: false
269
+ });
270
+ }
271
+ if (profile.timeoutMs !== void 0 && !isPositiveFiniteNumber(profile.timeoutMs)) {
272
+ throw new ConfigurationError({
273
+ details: { timeoutMs: profile.timeoutMs },
274
+ message: "Connection profile timeoutMs must be a positive finite number",
275
+ retryable: false
276
+ });
277
+ }
278
+ if (profile.tls !== void 0) {
279
+ validateTlsProfile(profile.tls);
280
+ }
281
+ if (profile.ssh !== void 0) {
282
+ validateSshProfile(profile.ssh);
283
+ }
284
+ return profile;
285
+ }
286
+ function validateSshProfile(profile) {
287
+ validatePinnedHostKeySha256(profile.pinnedHostKeySha256);
288
+ if (profile.algorithms !== void 0) {
289
+ validateSshAlgorithms(profile.algorithms);
290
+ }
291
+ if (profile.agent !== void 0) {
292
+ validateSshAgentSource(profile.agent);
293
+ }
294
+ if (profile.keyboardInteractive !== void 0 && typeof profile.keyboardInteractive !== "function") {
295
+ throw new ConfigurationError({
296
+ details: { keyboardInteractive: typeof profile.keyboardInteractive },
297
+ message: "Connection profile ssh.keyboardInteractive must be a function when provided",
298
+ retryable: false
299
+ });
300
+ }
301
+ if (profile.socketFactory !== void 0 && typeof profile.socketFactory !== "function") {
302
+ throw new ConfigurationError({
303
+ details: { socketFactory: typeof profile.socketFactory },
304
+ message: "Connection profile ssh.socketFactory must be a function when provided",
305
+ retryable: false
306
+ });
307
+ }
308
+ }
309
+ function validateSshAlgorithms(value) {
310
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
311
+ throw createSshAlgorithmsError(value);
312
+ }
313
+ const algorithms = value;
314
+ for (const [name, list] of Object.entries(algorithms)) {
315
+ if (list === void 0) {
316
+ continue;
317
+ }
318
+ if (Array.isArray(list)) {
319
+ if (!isNonEmptyStringArray(list)) {
320
+ throw createSshAlgorithmsError({ [name]: list });
321
+ }
322
+ continue;
323
+ }
324
+ if (typeof list !== "object" || list === null || Array.isArray(list)) {
325
+ throw createSshAlgorithmsError({ [name]: list });
326
+ }
327
+ const operationLists = list;
328
+ for (const [operation, operationList] of Object.entries(operationLists)) {
329
+ if (!["append", "prepend", "remove"].includes(operation)) {
330
+ throw createSshAlgorithmsError({ [name]: list });
331
+ }
332
+ if (typeof operationList !== "string" && (!Array.isArray(operationList) || !isNonEmptyStringArray(operationList))) {
333
+ throw createSshAlgorithmsError({ [name]: list });
334
+ }
335
+ }
336
+ }
337
+ }
338
+ function isNonEmptyStringArray(value) {
339
+ return value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
340
+ }
341
+ function validateSshAgentSource(value) {
342
+ if (typeof value === "string") {
343
+ if (value.trim().length > 0) {
344
+ return;
345
+ }
346
+ } else if (typeof value === "object" && value !== null && typeof value.getIdentities === "function" && typeof value.sign === "function") {
347
+ return;
348
+ }
349
+ throw new ConfigurationError({
350
+ details: { agent: typeof value },
351
+ message: "Connection profile ssh.agent must be a non-empty socket path or agent object",
352
+ retryable: false
353
+ });
354
+ }
355
+ function validateTlsProfile(profile) {
356
+ if (profile.servername !== void 0 && profile.servername.trim().length === 0) {
357
+ throw new ConfigurationError({
358
+ message: "Connection profile tls.servername must be non-empty when provided",
359
+ retryable: false
360
+ });
361
+ }
362
+ if (profile.rejectUnauthorized !== void 0 && typeof profile.rejectUnauthorized !== "boolean") {
363
+ throw new ConfigurationError({
364
+ details: { rejectUnauthorized: profile.rejectUnauthorized },
365
+ message: "Connection profile tls.rejectUnauthorized must be a boolean",
366
+ retryable: false
367
+ });
368
+ }
369
+ validateTlsVersion(profile.minVersion, "minVersion");
370
+ validateTlsVersion(profile.maxVersion, "maxVersion");
371
+ validatePinnedFingerprint256(profile.pinnedFingerprint256);
372
+ }
373
+ function validatePinnedFingerprint256(value) {
374
+ if (value === void 0) {
375
+ return;
376
+ }
377
+ const fingerprints = Array.isArray(value) ? value : [value];
378
+ if (fingerprints.length === 0) {
379
+ throw createPinnedFingerprintError(value);
380
+ }
381
+ for (const fingerprint of fingerprints) {
382
+ if (typeof fingerprint !== "string" || !isSha256Fingerprint(fingerprint)) {
383
+ throw createPinnedFingerprintError(value);
384
+ }
385
+ }
386
+ }
387
+ function validatePinnedHostKeySha256(value) {
388
+ if (value === void 0) {
389
+ return;
390
+ }
391
+ const fingerprints = Array.isArray(value) ? value : [value];
392
+ if (fingerprints.length === 0) {
393
+ throw createPinnedHostKeyError(value);
394
+ }
395
+ for (const fingerprint of fingerprints) {
396
+ if (typeof fingerprint !== "string" || !isSshHostKeySha256Fingerprint(fingerprint)) {
397
+ throw createPinnedHostKeyError(value);
398
+ }
399
+ }
400
+ }
401
+ function isSha256Fingerprint(value) {
402
+ const normalized = value.trim().replace(/:/g, "");
403
+ return normalized.length === SHA256_FINGERPRINT_HEX_LENGTH && /^[a-f0-9]+$/i.test(normalized);
404
+ }
405
+ function isSshHostKeySha256Fingerprint(value) {
406
+ const trimmed = value.trim();
407
+ if (isSha256Fingerprint(trimmed)) {
408
+ return true;
409
+ }
410
+ const bare = trimmed.startsWith("SHA256:") ? trimmed.slice("SHA256:".length) : trimmed;
411
+ const padded = padBase64(bare);
412
+ if (!/^[a-z0-9+/]+={0,2}$/i.test(padded)) {
413
+ return false;
414
+ }
415
+ try {
416
+ return Buffer.from(padded, "base64").byteLength === SHA256_DIGEST_BYTE_LENGTH;
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
421
+ function padBase64(value) {
422
+ const remainder = value.length % 4;
423
+ return remainder === 0 ? value : `${value}${"=".repeat(4 - remainder)}`;
424
+ }
425
+ function createPinnedFingerprintError(value) {
426
+ return new ConfigurationError({
427
+ details: { pinnedFingerprint256: value },
428
+ message: "Connection profile tls.pinnedFingerprint256 must be a SHA-256 hex fingerprint or non-empty array of fingerprints",
429
+ retryable: false
430
+ });
431
+ }
432
+ function createPinnedHostKeyError(value) {
433
+ return new ConfigurationError({
434
+ details: { pinnedHostKeySha256: value },
435
+ message: "Connection profile ssh.pinnedHostKeySha256 must be an OpenSSH SHA256, base64, or hex fingerprint or non-empty array of fingerprints",
436
+ retryable: false
437
+ });
438
+ }
439
+ function createSshAlgorithmsError(value) {
440
+ return new ConfigurationError({
441
+ details: { algorithms: value },
442
+ message: "Connection profile ssh.algorithms must use ssh2-compatible non-empty algorithm lists",
443
+ retryable: false
444
+ });
445
+ }
446
+ function validateTlsVersion(value, field) {
447
+ if (value === void 0) {
448
+ return;
449
+ }
450
+ if (!TLS_VERSIONS.has(value)) {
451
+ throw new ConfigurationError({
452
+ details: { [field]: value },
453
+ message: `Connection profile tls.${field} must be a supported TLS version`,
454
+ retryable: false
455
+ });
456
+ }
457
+ }
458
+ function isValidPort(value) {
459
+ return Number.isInteger(value) && value >= 1 && value <= 65535;
460
+ }
461
+ function isPositiveFiniteNumber(value) {
462
+ return Number.isFinite(value) && value > 0;
463
+ }
464
+
465
+ // src/core/ProviderRegistry.ts
466
+ var ProviderRegistry = class {
467
+ factories = /* @__PURE__ */ new Map();
468
+ /**
469
+ * Creates a registry and optionally seeds it with provider factories.
470
+ *
471
+ * @param providers - Provider factories to register immediately.
472
+ */
473
+ constructor(providers = []) {
474
+ for (const provider of providers) {
475
+ this.register(provider);
476
+ }
477
+ }
478
+ /**
479
+ * Registers a provider factory.
480
+ *
481
+ * @param provider - Provider factory to add.
482
+ * @returns This registry for fluent setup.
483
+ * @throws {@link ConfigurationError} When a provider id is registered twice.
484
+ */
485
+ register(provider) {
486
+ if (this.factories.has(provider.id)) {
487
+ throw new ConfigurationError({
488
+ details: { provider: provider.id },
489
+ message: `Provider "${provider.id}" is already registered`,
490
+ retryable: false
491
+ });
492
+ }
493
+ this.factories.set(provider.id, provider);
494
+ return this;
495
+ }
496
+ /**
497
+ * Removes a provider factory from the registry.
498
+ *
499
+ * @param providerId - Provider id to remove.
500
+ * @returns `true` when a provider was removed.
501
+ */
502
+ unregister(providerId) {
503
+ return this.factories.delete(providerId);
504
+ }
505
+ /**
506
+ * Checks whether a provider id is registered.
507
+ *
508
+ * @param providerId - Provider id to inspect.
509
+ * @returns `true` when a provider factory exists.
510
+ */
511
+ has(providerId) {
512
+ return this.factories.has(providerId);
513
+ }
514
+ /**
515
+ * Gets a provider factory when registered.
516
+ *
517
+ * @param providerId - Provider id to retrieve.
518
+ * @returns The provider factory, or `undefined` when missing.
519
+ */
520
+ get(providerId) {
521
+ return this.factories.get(providerId);
522
+ }
523
+ /**
524
+ * Gets a registered provider factory or throws a typed SDK error.
525
+ *
526
+ * @param providerId - Provider id to retrieve.
527
+ * @returns The registered provider factory.
528
+ * @throws {@link UnsupportedFeatureError} When no provider has been registered.
529
+ */
530
+ require(providerId) {
531
+ const provider = this.get(providerId);
532
+ if (provider === void 0) {
533
+ throw new UnsupportedFeatureError({
534
+ details: { provider: providerId },
535
+ message: `Provider "${providerId}" is not registered`,
536
+ retryable: false
537
+ });
538
+ }
539
+ return provider;
540
+ }
541
+ /**
542
+ * Gets a provider capability snapshot when registered.
543
+ *
544
+ * @param providerId - Provider id to inspect.
545
+ * @returns Capability snapshot, or `undefined` when missing.
546
+ */
547
+ getCapabilities(providerId) {
548
+ return this.get(providerId)?.capabilities;
549
+ }
550
+ /**
551
+ * Gets a provider capability snapshot or throws a typed SDK error.
552
+ *
553
+ * @param providerId - Provider id to inspect.
554
+ * @returns Capability snapshot for the registered provider.
555
+ * @throws {@link UnsupportedFeatureError} When no provider has been registered.
556
+ */
557
+ requireCapabilities(providerId) {
558
+ return this.require(providerId).capabilities;
559
+ }
560
+ /**
561
+ * Lists registered provider factories in insertion order.
562
+ *
563
+ * @returns Registered provider factories.
564
+ */
565
+ list() {
566
+ return [...this.factories.values()];
567
+ }
568
+ /**
569
+ * Lists registered provider capabilities in insertion order.
570
+ *
571
+ * @returns Capability snapshots for every registered provider.
572
+ */
573
+ listCapabilities() {
574
+ return this.list().map((provider) => provider.capabilities);
575
+ }
576
+ };
577
+
578
+ // src/core/TransferClient.ts
579
+ var TransferClient = class {
580
+ /** Provider registry used by this client. */
581
+ registry;
582
+ logger;
583
+ /**
584
+ * Creates a transfer client without opening any provider connections.
585
+ *
586
+ * @param options - Optional registry, provider factories, and logger.
587
+ */
588
+ constructor(options = {}) {
589
+ this.registry = options.registry ?? new ProviderRegistry();
590
+ this.logger = options.logger ?? noopLogger;
591
+ for (const provider of options.providers ?? []) {
592
+ this.registry.register(provider);
593
+ }
594
+ }
595
+ /**
596
+ * Registers a provider factory with this client's registry.
597
+ *
598
+ * @param provider - Provider factory to register.
599
+ * @returns This client for fluent setup.
600
+ */
601
+ registerProvider(provider) {
602
+ this.registry.register(provider);
603
+ return this;
604
+ }
605
+ /**
606
+ * Checks whether this client can create sessions for a provider id.
607
+ *
608
+ * @param providerId - Provider id to inspect.
609
+ * @returns `true` when a provider factory is registered.
610
+ */
611
+ hasProvider(providerId) {
612
+ return this.registry.has(providerId);
613
+ }
614
+ getCapabilities(providerId) {
615
+ if (providerId === void 0) {
616
+ return this.registry.listCapabilities();
617
+ }
618
+ return this.registry.requireCapabilities(providerId);
619
+ }
620
+ /**
621
+ * Opens a provider session using `profile.provider`, with `profile.protocol` as compatibility fallback.
622
+ *
623
+ * @param profile - Connection profile containing a provider or legacy protocol field.
624
+ * @returns A connected provider session.
625
+ * @throws {@link ConfigurationError} When neither provider nor protocol is present.
626
+ */
627
+ async connect(profile) {
628
+ const validProfile = validateConnectionProfile(profile);
629
+ const providerId = resolveProviderId(validProfile);
630
+ if (providerId === void 0) {
631
+ throw new ConfigurationError({
632
+ message: "Connection profiles must include a provider or protocol",
633
+ retryable: false
634
+ });
635
+ }
636
+ const providerFactory = this.registry.require(providerId);
637
+ const provider = providerFactory.create();
638
+ const normalizedProfile = {
639
+ ...validProfile,
640
+ provider: providerId
641
+ };
642
+ if (normalizedProfile.protocol === void 0 && isClassicProviderId(providerId)) {
643
+ normalizedProfile.protocol = providerId;
644
+ }
645
+ emitLog(this.logger, "info", createConnectLogRecord(normalizedProfile, providerId));
646
+ return provider.connect(normalizedProfile);
647
+ }
648
+ };
649
+ function createConnectLogRecord(profile, providerId) {
650
+ const record = {
651
+ component: "core",
652
+ host: profile.host,
653
+ message: "Connecting through provider",
654
+ provider: providerId
655
+ };
656
+ if (isClassicProviderId(providerId)) {
657
+ record.protocol = providerId;
658
+ }
659
+ return record;
660
+ }
661
+
662
+ // src/core/createTransferClient.ts
663
+ function createTransferClient(options = {}) {
664
+ return new TransferClient(options);
665
+ }
666
+
667
+ // src/client/ZeroTransfer.ts
668
+ var ZeroTransfer = class _ZeroTransfer extends EventEmitter {
669
+ /** Creates a provider-neutral transfer client with the built-in provider registry. */
670
+ static createTransferClient = createTransferClient;
671
+ /** Protocol selected for this client instance. */
672
+ protocol;
673
+ logger;
674
+ adapter;
675
+ connected = false;
676
+ /**
677
+ * Creates a client facade without opening a network connection.
678
+ *
679
+ * @param options - Optional facade configuration, logger, and protocol adapter.
680
+ */
681
+ constructor(options = {}) {
682
+ super();
683
+ this.protocol = options.protocol ?? "ftp";
684
+ this.logger = options.logger ?? noopLogger;
685
+ this.adapter = options.adapter;
686
+ }
687
+ /**
688
+ * Creates a new client facade using the provided options.
689
+ *
690
+ * @param options - Optional facade configuration, logger, and adapter.
691
+ * @returns A disconnected {@link ZeroTransfer} instance.
692
+ */
693
+ static create(options = {}) {
694
+ return new _ZeroTransfer(options);
695
+ }
696
+ /**
697
+ * Creates a client and connects it in one step.
698
+ *
699
+ * @param profile - Remote host, authentication, and protocol connection settings.
700
+ * @param options - Optional facade settings that can be overridden by the profile.
701
+ * @returns A connected {@link ZeroTransfer} instance.
702
+ * @throws {@link UnsupportedFeatureError} When no adapter is available for the protocol.
703
+ */
704
+ static async connect(profile, options = {}) {
705
+ const clientOptions = { ...options };
706
+ if (profile.logger !== void 0) {
707
+ clientOptions.logger = profile.logger;
708
+ }
709
+ if (profile.protocol !== void 0) {
710
+ clientOptions.protocol = profile.protocol;
711
+ } else if (isClassicProviderId(profile.provider)) {
712
+ clientOptions.protocol = profile.provider;
713
+ }
714
+ const client = new _ZeroTransfer(clientOptions);
715
+ await client.connect(profile);
716
+ return client;
717
+ }
718
+ /**
719
+ * Opens a remote connection through the configured protocol adapter.
720
+ *
721
+ * @param profile - Remote host, authentication, timeout, logger, and protocol settings.
722
+ * @returns A promise that resolves after the adapter reports a successful connection.
723
+ * @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
724
+ */
725
+ async connect(profile) {
726
+ const adapter = this.requireAdapter();
727
+ const protocol = profile.protocol ?? (isClassicProviderId(profile.provider) ? profile.provider : this.protocol);
728
+ emitLog(this.logger, "info", {
729
+ component: "client",
730
+ host: profile.host,
731
+ message: "Connecting",
732
+ protocol
733
+ });
734
+ await adapter.connect({
735
+ ...profile,
736
+ protocol
737
+ });
738
+ this.connected = true;
739
+ this.emit("connect", {
740
+ host: profile.host,
741
+ protocol
742
+ });
743
+ }
744
+ /**
745
+ * Closes the active remote connection if one exists.
746
+ *
747
+ * @returns A promise that resolves after the adapter disconnects or immediately when idle.
748
+ */
749
+ async disconnect() {
750
+ if (this.adapter !== void 0 && this.connected) {
751
+ await this.adapter.disconnect();
752
+ }
753
+ this.connected = false;
754
+ this.emit("disconnect");
755
+ }
756
+ /**
757
+ * Checks whether the facade currently considers the adapter connected.
758
+ *
759
+ * @returns `true` after a successful connection and before disconnection.
760
+ */
761
+ isConnected() {
762
+ return this.connected;
763
+ }
764
+ /**
765
+ * Describes protocol and adapter readiness for feature discovery.
766
+ *
767
+ * @returns A capability snapshot for diagnostics and UI state.
768
+ */
769
+ getCapabilities() {
770
+ return {
771
+ adapterReady: this.adapter !== void 0,
772
+ protocol: this.protocol
773
+ };
774
+ }
775
+ /**
776
+ * Lists remote entries for a path using the configured adapter.
777
+ *
778
+ * @param path - Remote directory path to inspect.
779
+ * @param options - Optional listing controls such as recursion and abort signal.
780
+ * @returns Normalized remote entries for the requested directory.
781
+ * @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
782
+ */
783
+ async list(path2, options) {
784
+ return this.requireAdapter().list(path2, options);
785
+ }
786
+ /**
787
+ * Reads metadata for a remote path using the configured adapter.
788
+ *
789
+ * @param path - Remote file, directory, or symbolic-link path to inspect.
790
+ * @param options - Optional stat controls such as abort signal.
791
+ * @returns Normalized metadata for an existing remote entry.
792
+ * @throws {@link UnsupportedFeatureError} When the client does not have an adapter.
793
+ */
794
+ async stat(path2, options) {
795
+ return this.requireAdapter().stat(path2, options);
796
+ }
797
+ /**
798
+ * Returns the configured adapter or raises the alpha unsupported-feature error.
799
+ *
800
+ * @returns A concrete remote file adapter ready to execute operations.
801
+ * @throws {@link UnsupportedFeatureError} When no adapter has been provided.
802
+ */
803
+ requireAdapter() {
804
+ if (this.adapter === void 0) {
805
+ throw new UnsupportedFeatureError({
806
+ message: `The ${this.protocol.toUpperCase()} adapter is not implemented in this alpha foundation yet`,
807
+ protocol: this.protocol,
808
+ retryable: false
809
+ });
810
+ }
811
+ return this.adapter;
812
+ }
813
+ };
814
+
815
+ // src/client/operations.ts
816
+ import { isAbsolute, resolve as resolvePath } from "path";
817
+
818
+ // src/transfers/BandwidthThrottle.ts
819
+ function createBandwidthThrottle(limit, options = {}) {
820
+ if (limit === void 0) return void 0;
821
+ const bytesPerSecond = normalizeRate(limit.bytesPerSecond);
822
+ const burstBytes = normalizeBurst(limit.burstBytes, bytesPerSecond);
823
+ const now = options.now ?? Date.now;
824
+ const sleep = options.sleep ?? defaultSleep;
825
+ let tokens = burstBytes;
826
+ let lastRefillAt = now();
827
+ function refill() {
828
+ const current = now();
829
+ const elapsedMs = Math.max(0, current - lastRefillAt);
830
+ if (elapsedMs > 0) {
831
+ tokens = Math.min(burstBytes, tokens + elapsedMs / 1e3 * bytesPerSecond);
832
+ lastRefillAt = current;
833
+ }
834
+ }
835
+ async function consume(bytes, signal) {
836
+ if (!Number.isFinite(bytes) || bytes < 0) {
837
+ throw new ConfigurationError({
838
+ details: { bytes },
839
+ message: "Bandwidth throttle byte count must be a non-negative number",
840
+ retryable: false
841
+ });
842
+ }
843
+ if (bytes === 0) return;
844
+ let remaining = bytes;
845
+ while (remaining > 0) {
846
+ throwIfAborted(signal);
847
+ refill();
848
+ if (tokens >= remaining) {
849
+ tokens -= remaining;
850
+ return;
851
+ }
852
+ if (tokens >= burstBytes) {
853
+ const drained = tokens;
854
+ tokens = 0;
855
+ remaining -= drained;
856
+ const waitMs2 = Math.ceil(Math.min(remaining, burstBytes) / bytesPerSecond * 1e3);
857
+ await sleep(waitMs2, signal);
858
+ continue;
859
+ }
860
+ const deficit = Math.min(remaining, burstBytes) - tokens;
861
+ const waitMs = Math.max(1, Math.ceil(deficit / bytesPerSecond * 1e3));
862
+ await sleep(waitMs, signal);
863
+ }
864
+ }
865
+ return { burstBytes, bytesPerSecond, consume };
866
+ }
867
+ function throttleByteIterable(source, throttle, signal) {
868
+ if (throttle === void 0) return source;
869
+ return {
870
+ [Symbol.asyncIterator]: async function* () {
871
+ for await (const chunk of source) {
872
+ throwIfAborted(signal);
873
+ if (chunk.byteLength > 0) {
874
+ await throttle.consume(chunk.byteLength, signal);
875
+ }
876
+ yield chunk;
877
+ }
878
+ }
879
+ };
880
+ }
881
+ function normalizeRate(value) {
882
+ if (!Number.isFinite(value) || value <= 0) {
883
+ throw new ConfigurationError({
884
+ details: { bytesPerSecond: value },
885
+ message: "Bandwidth limit bytesPerSecond must be a positive number",
886
+ retryable: false
887
+ });
888
+ }
889
+ return value;
890
+ }
891
+ function normalizeBurst(value, bytesPerSecond) {
892
+ if (value === void 0) return bytesPerSecond;
893
+ if (!Number.isFinite(value) || value <= 0) {
894
+ throw new ConfigurationError({
895
+ details: { burstBytes: value },
896
+ message: "Bandwidth limit burstBytes must be a positive number when provided",
897
+ retryable: false
898
+ });
899
+ }
900
+ return value;
901
+ }
902
+ function throwIfAborted(signal) {
903
+ if (signal?.aborted === true) {
904
+ throw new AbortError({
905
+ message: "Bandwidth throttle wait aborted",
906
+ retryable: false
907
+ });
908
+ }
909
+ }
910
+ function defaultSleep(delayMs, signal) {
911
+ if (delayMs <= 0) return Promise.resolve();
912
+ return new Promise((resolve, reject) => {
913
+ const timer = setTimeout(() => {
914
+ cleanup();
915
+ resolve();
916
+ }, delayMs);
917
+ const onAbort = () => {
918
+ cleanup();
919
+ reject(
920
+ new AbortError({
921
+ message: "Bandwidth throttle wait aborted",
922
+ retryable: false
923
+ })
924
+ );
925
+ };
926
+ function cleanup() {
927
+ clearTimeout(timer);
928
+ signal?.removeEventListener("abort", onAbort);
929
+ }
930
+ if (signal !== void 0) {
931
+ if (signal.aborted) {
932
+ onAbort();
933
+ return;
934
+ }
935
+ signal.addEventListener("abort", onAbort, { once: true });
936
+ }
937
+ });
938
+ }
939
+
940
+ // src/transfers/createProviderTransferExecutor.ts
941
+ function createProviderTransferExecutor(options) {
942
+ return async (context) => {
943
+ const { job } = context;
944
+ if (!isReadWriteOperation(job.operation)) {
945
+ throw new UnsupportedFeatureError({
946
+ details: { jobId: job.id, operation: job.operation },
947
+ message: `Provider read/write executor does not support transfer operation: ${job.operation}`,
948
+ retryable: false
949
+ });
950
+ }
951
+ const source = requireEndpoint(job, "source");
952
+ const destination = requireEndpoint(job, "destination");
953
+ const sourceSession = options.resolveSession({ endpoint: source, job, role: "source" });
954
+ const destinationSession = options.resolveSession({
955
+ endpoint: destination,
956
+ job,
957
+ role: "destination"
958
+ });
959
+ const sourceTransfers = requireTransferOperations(sourceSession, source, "source", job);
960
+ const destinationTransfers = requireTransferOperations(
961
+ destinationSession,
962
+ destination,
963
+ "destination",
964
+ job
965
+ );
966
+ context.throwIfAborted();
967
+ const readResult = await sourceTransfers.read(createReadRequest(context, source));
968
+ context.throwIfAborted();
969
+ const throttledReadResult = applyBandwidthThrottle(readResult, context, options.throttle);
970
+ const writeResult = await destinationTransfers.write(
971
+ createWriteRequest(context, destination, throttledReadResult)
972
+ );
973
+ return mergeProviderTransferResult(readResult, writeResult, job);
974
+ };
975
+ }
976
+ function applyBandwidthThrottle(readResult, context, options) {
977
+ const throttle = createBandwidthThrottle(context.bandwidthLimit, options);
978
+ if (throttle === void 0) return readResult;
979
+ return {
980
+ ...readResult,
981
+ content: throttleByteIterable(readResult.content, throttle, context.signal)
982
+ };
983
+ }
984
+ function isReadWriteOperation(operation) {
985
+ return operation === "copy" || operation === "download" || operation === "upload";
986
+ }
987
+ function requireEndpoint(job, role) {
988
+ const endpoint = role === "source" ? job.source : job.destination;
989
+ if (endpoint === void 0) {
990
+ throw new ConfigurationError({
991
+ details: { jobId: job.id, operation: job.operation, role },
992
+ message: `Transfer job requires a ${role} endpoint: ${job.id}`,
993
+ retryable: false
994
+ });
995
+ }
996
+ return endpoint;
997
+ }
998
+ function requireTransferOperations(session, endpoint, role, job) {
999
+ if (session === void 0) {
1000
+ throw new UnsupportedFeatureError({
1001
+ details: { endpoint: cloneEndpoint(endpoint), jobId: job.id, operation: job.operation, role },
1002
+ message: `No provider session resolved for ${role} endpoint: ${endpoint.path}`,
1003
+ retryable: false
1004
+ });
1005
+ }
1006
+ if (session.transfers === void 0) {
1007
+ throw new UnsupportedFeatureError({
1008
+ details: {
1009
+ endpoint: cloneEndpoint(endpoint),
1010
+ jobId: job.id,
1011
+ operation: job.operation,
1012
+ provider: session.provider,
1013
+ role
1014
+ },
1015
+ message: `Provider session does not expose transfer operations: ${session.provider}`,
1016
+ retryable: false
1017
+ });
1018
+ }
1019
+ return session.transfers;
1020
+ }
1021
+ function createReadRequest(context, endpoint) {
1022
+ const request = {
1023
+ attempt: context.attempt,
1024
+ endpoint: cloneEndpoint(endpoint),
1025
+ job: context.job,
1026
+ reportProgress: (bytesTransferred, totalBytes) => context.reportProgress(bytesTransferred, totalBytes),
1027
+ throwIfAborted: () => context.throwIfAborted()
1028
+ };
1029
+ if (context.signal !== void 0) request.signal = context.signal;
1030
+ if (context.bandwidthLimit !== void 0) {
1031
+ request.bandwidthLimit = { ...context.bandwidthLimit };
1032
+ }
1033
+ return request;
1034
+ }
1035
+ function createWriteRequest(context, endpoint, readResult) {
1036
+ const request = {
1037
+ attempt: context.attempt,
1038
+ content: readResult.content,
1039
+ endpoint: cloneEndpoint(endpoint),
1040
+ job: context.job,
1041
+ reportProgress: (bytesTransferred, totalBytes2) => context.reportProgress(bytesTransferred, totalBytes2),
1042
+ throwIfAborted: () => context.throwIfAborted()
1043
+ };
1044
+ const totalBytes = readResult.totalBytes ?? context.job.totalBytes;
1045
+ if (context.signal !== void 0) request.signal = context.signal;
1046
+ if (context.bandwidthLimit !== void 0) {
1047
+ request.bandwidthLimit = { ...context.bandwidthLimit };
1048
+ }
1049
+ if (totalBytes !== void 0) request.totalBytes = totalBytes;
1050
+ if (context.job.resumed === true) request.offset = readResult.bytesRead ?? 0;
1051
+ if (readResult.verification !== void 0) {
1052
+ request.verification = cloneVerification(readResult.verification);
1053
+ }
1054
+ return request;
1055
+ }
1056
+ function mergeProviderTransferResult(readResult, writeResult, job) {
1057
+ const result = {
1058
+ bytesTransferred: writeResult.bytesTransferred
1059
+ };
1060
+ const totalBytes = writeResult.totalBytes ?? readResult.totalBytes ?? job.totalBytes;
1061
+ const warnings = [...readResult.warnings ?? [], ...writeResult.warnings ?? []];
1062
+ if (totalBytes !== void 0) result.totalBytes = totalBytes;
1063
+ if (writeResult.resumed !== void 0) result.resumed = writeResult.resumed;
1064
+ if (writeResult.verified !== void 0) result.verified = writeResult.verified;
1065
+ if (writeResult.checksum !== void 0) result.checksum = writeResult.checksum;
1066
+ else if (readResult.checksum !== void 0) result.checksum = readResult.checksum;
1067
+ if (writeResult.verification !== void 0) {
1068
+ result.verification = cloneVerification(writeResult.verification);
1069
+ } else if (readResult.verification !== void 0) {
1070
+ result.verification = cloneVerification(readResult.verification);
1071
+ }
1072
+ if (warnings.length > 0) result.warnings = warnings;
1073
+ return result;
1074
+ }
1075
+ function cloneEndpoint(endpoint) {
1076
+ const clone = { path: endpoint.path };
1077
+ if (endpoint.provider !== void 0) clone.provider = endpoint.provider;
1078
+ return clone;
1079
+ }
1080
+ function cloneVerification(verification) {
1081
+ const clone = { verified: verification.verified };
1082
+ if (verification.method !== void 0) clone.method = verification.method;
1083
+ if (verification.checksum !== void 0) clone.checksum = verification.checksum;
1084
+ if (verification.expectedChecksum !== void 0) {
1085
+ clone.expectedChecksum = verification.expectedChecksum;
1086
+ }
1087
+ if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
1088
+ if (verification.details !== void 0) clone.details = { ...verification.details };
1089
+ return clone;
1090
+ }
1091
+
1092
+ // src/services/TransferService.ts
1093
+ function createTransferResult(input) {
1094
+ const durationMs = Math.max(0, input.completedAt.getTime() - input.startedAt.getTime());
1095
+ const result = {
1096
+ destinationPath: input.destinationPath,
1097
+ bytesTransferred: input.bytesTransferred,
1098
+ startedAt: input.startedAt,
1099
+ completedAt: input.completedAt,
1100
+ durationMs,
1101
+ averageBytesPerSecond: calculateBytesPerSecond(input.bytesTransferred, durationMs),
1102
+ resumed: input.resumed ?? false,
1103
+ verified: input.verified ?? false
1104
+ };
1105
+ if (input.sourcePath !== void 0) result.sourcePath = input.sourcePath;
1106
+ if (input.checksum !== void 0) result.checksum = input.checksum;
1107
+ return result;
1108
+ }
1109
+ function createProgressEvent(input) {
1110
+ const now = input.now ?? /* @__PURE__ */ new Date();
1111
+ const elapsedMs = Math.max(0, now.getTime() - input.startedAt.getTime());
1112
+ const event = {
1113
+ transferId: input.transferId,
1114
+ bytesTransferred: input.bytesTransferred,
1115
+ startedAt: input.startedAt,
1116
+ elapsedMs,
1117
+ bytesPerSecond: calculateBytesPerSecond(input.bytesTransferred, elapsedMs)
1118
+ };
1119
+ if (input.totalBytes !== void 0) {
1120
+ event.totalBytes = input.totalBytes;
1121
+ event.percent = input.totalBytes > 0 ? input.bytesTransferred / input.totalBytes * 100 : 0;
1122
+ }
1123
+ return event;
1124
+ }
1125
+ function calculateBytesPerSecond(bytes, durationMs) {
1126
+ if (durationMs <= 0) {
1127
+ return bytes;
1128
+ }
1129
+ return bytes / (durationMs / 1e3);
1130
+ }
1131
+
1132
+ // src/transfers/TransferEngine.ts
1133
+ var TransferEngine = class {
1134
+ now;
1135
+ /**
1136
+ * Creates a transfer engine.
1137
+ *
1138
+ * @param options - Optional clock override for deterministic tests.
1139
+ */
1140
+ constructor(options = {}) {
1141
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1142
+ }
1143
+ /**
1144
+ * Executes a transfer job through a caller-supplied operation.
1145
+ *
1146
+ * @param job - Job metadata used for correlation and receipts.
1147
+ * @param executor - Concrete transfer operation implementation.
1148
+ * @param options - Optional abort, retry, and progress hooks.
1149
+ * @returns Receipt for the completed transfer.
1150
+ * @throws {@link AbortError} When execution is cancelled.
1151
+ * @throws {@link TransferError} When all attempts fail.
1152
+ */
1153
+ async execute(job, executor, options = {}) {
1154
+ const maxAttempts = normalizeMaxAttempts(options.retry?.maxAttempts);
1155
+ const attempts = [];
1156
+ const startedAt = this.now();
1157
+ const abortScope = createAbortScope(options.signal, options.timeout, job);
1158
+ let latestBytesTransferred = 0;
1159
+ try {
1160
+ for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
1161
+ this.throwIfAborted(abortScope.signal, job);
1162
+ const attemptStartedAt = this.now();
1163
+ const context = this.createExecutionContext(
1164
+ job,
1165
+ attemptNumber,
1166
+ attemptStartedAt,
1167
+ options,
1168
+ abortScope.signal,
1169
+ (bytesTransferred) => {
1170
+ latestBytesTransferred = bytesTransferred;
1171
+ }
1172
+ );
1173
+ try {
1174
+ const result = await runExecutor(executor, context, abortScope.signal, job);
1175
+ context.throwIfAborted();
1176
+ latestBytesTransferred = result.bytesTransferred;
1177
+ const completedAt = this.now();
1178
+ attempts.push(
1179
+ createAttempt(attemptNumber, attemptStartedAt, completedAt, result.bytesTransferred)
1180
+ );
1181
+ return createReceipt(job, result, attempts, startedAt, completedAt);
1182
+ } catch (error) {
1183
+ const completedAt = this.now();
1184
+ const attempt = createAttempt(
1185
+ attemptNumber,
1186
+ attemptStartedAt,
1187
+ completedAt,
1188
+ latestBytesTransferred,
1189
+ summarizeError(error)
1190
+ );
1191
+ attempts.push(attempt);
1192
+ if (error instanceof AbortError || error instanceof TimeoutError) {
1193
+ throw error;
1194
+ }
1195
+ const retryInput = { attempt: attemptNumber, error, job };
1196
+ const shouldRetry = attemptNumber < maxAttempts && (options.retry?.shouldRetry?.(retryInput) ?? isRetryable(error));
1197
+ if (shouldRetry) {
1198
+ options.retry?.onRetry?.(retryInput);
1199
+ continue;
1200
+ }
1201
+ throw createTransferFailure(job, error, attempts);
1202
+ }
1203
+ }
1204
+ throw createTransferFailure(job, void 0, attempts);
1205
+ } finally {
1206
+ abortScope.dispose();
1207
+ }
1208
+ }
1209
+ createExecutionContext(job, attempt, startedAt, options, signal, updateBytesTransferred) {
1210
+ const context = {
1211
+ attempt,
1212
+ job,
1213
+ reportProgress: (bytesTransferred, totalBytes) => {
1214
+ this.throwIfAborted(signal, job);
1215
+ updateBytesTransferred(bytesTransferred);
1216
+ const progressInput = {
1217
+ bytesTransferred,
1218
+ now: this.now(),
1219
+ startedAt,
1220
+ transferId: job.id
1221
+ };
1222
+ const resolvedTotalBytes = totalBytes ?? job.totalBytes;
1223
+ const event = createProgressEvent(
1224
+ resolvedTotalBytes === void 0 ? progressInput : { ...progressInput, totalBytes: resolvedTotalBytes }
1225
+ );
1226
+ options.onProgress?.(event);
1227
+ return event;
1228
+ },
1229
+ throwIfAborted: () => this.throwIfAborted(signal, job)
1230
+ };
1231
+ if (signal !== void 0) {
1232
+ context.signal = signal;
1233
+ }
1234
+ if (options.bandwidthLimit !== void 0) {
1235
+ context.bandwidthLimit = { ...options.bandwidthLimit };
1236
+ }
1237
+ return context;
1238
+ }
1239
+ throwIfAborted(signal, job) {
1240
+ if (signal?.aborted === true) {
1241
+ if (signal.reason instanceof ZeroTransferError) {
1242
+ throw signal.reason;
1243
+ }
1244
+ throw new AbortError({
1245
+ details: { jobId: job.id, operation: job.operation },
1246
+ message: `Transfer job aborted: ${job.id}`,
1247
+ retryable: false
1248
+ });
1249
+ }
1250
+ }
1251
+ };
1252
+ function createAbortScope(parentSignal, timeout, job) {
1253
+ const timeoutMs = normalizeTimeoutMs(timeout?.timeoutMs);
1254
+ if (parentSignal === void 0 && timeoutMs === void 0) {
1255
+ return { dispose: () => void 0 };
1256
+ }
1257
+ const controller = new AbortController();
1258
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
1259
+ const timeoutHandle = timeoutMs === void 0 ? void 0 : setTimeout(() => {
1260
+ controller.abort(
1261
+ new TimeoutError({
1262
+ details: { jobId: job.id, operation: job.operation, timeoutMs },
1263
+ message: `Transfer job timed out after ${timeoutMs}ms: ${job.id}`,
1264
+ retryable: timeout?.retryable ?? true
1265
+ })
1266
+ );
1267
+ }, timeoutMs);
1268
+ if (parentSignal?.aborted === true) {
1269
+ abortFromParent();
1270
+ } else {
1271
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
1272
+ }
1273
+ return {
1274
+ dispose: () => {
1275
+ if (timeoutHandle !== void 0) {
1276
+ clearTimeout(timeoutHandle);
1277
+ }
1278
+ parentSignal?.removeEventListener("abort", abortFromParent);
1279
+ },
1280
+ signal: controller.signal
1281
+ };
1282
+ }
1283
+ function normalizeTimeoutMs(value) {
1284
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
1285
+ return void 0;
1286
+ }
1287
+ return Math.floor(value);
1288
+ }
1289
+ async function runExecutor(executor, context, signal, job) {
1290
+ if (signal === void 0) {
1291
+ return executor(context);
1292
+ }
1293
+ return Promise.race([executor(context), rejectWhenAborted(signal, job)]);
1294
+ }
1295
+ function rejectWhenAborted(signal, job) {
1296
+ return new Promise((_, reject) => {
1297
+ const rejectAbort = () => {
1298
+ if (signal.reason instanceof ZeroTransferError) {
1299
+ reject(signal.reason);
1300
+ return;
1301
+ }
1302
+ reject(
1303
+ new AbortError({
1304
+ details: { jobId: job.id, operation: job.operation },
1305
+ message: `Transfer job aborted: ${job.id}`,
1306
+ retryable: false
1307
+ })
1308
+ );
1309
+ };
1310
+ if (signal.aborted) {
1311
+ rejectAbort();
1312
+ return;
1313
+ }
1314
+ signal.addEventListener("abort", rejectAbort, { once: true });
1315
+ });
1316
+ }
1317
+ function normalizeMaxAttempts(value) {
1318
+ if (value === void 0) {
1319
+ return 1;
1320
+ }
1321
+ return Math.max(1, Math.floor(value));
1322
+ }
1323
+ function createAttempt(attempt, startedAt, completedAt, bytesTransferred, error) {
1324
+ const result = {
1325
+ attempt,
1326
+ bytesTransferred,
1327
+ completedAt,
1328
+ durationMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
1329
+ startedAt
1330
+ };
1331
+ if (error !== void 0) {
1332
+ result.error = error;
1333
+ }
1334
+ return result;
1335
+ }
1336
+ function createReceipt(job, result, attempts, startedAt, completedAt) {
1337
+ const durationMs = Math.max(0, completedAt.getTime() - startedAt.getTime());
1338
+ const verification = normalizeVerificationResult(result);
1339
+ const receipt = {
1340
+ attempts,
1341
+ averageBytesPerSecond: calculateBytesPerSecond2(result.bytesTransferred, durationMs),
1342
+ bytesTransferred: result.bytesTransferred,
1343
+ completedAt,
1344
+ durationMs,
1345
+ jobId: job.id,
1346
+ operation: job.operation,
1347
+ resumed: result.resumed ?? job.resumed ?? false,
1348
+ startedAt,
1349
+ transferId: job.id,
1350
+ verified: verification?.verified ?? result.verified ?? false,
1351
+ warnings: [...result.warnings ?? []]
1352
+ };
1353
+ if (job.source !== void 0) receipt.source = { ...job.source };
1354
+ if (job.destination !== void 0) receipt.destination = { ...job.destination };
1355
+ if (result.totalBytes !== void 0) receipt.totalBytes = result.totalBytes;
1356
+ else if (job.totalBytes !== void 0) receipt.totalBytes = job.totalBytes;
1357
+ if (result.checksum !== void 0) receipt.checksum = result.checksum;
1358
+ else if (verification?.checksum !== void 0) receipt.checksum = verification.checksum;
1359
+ if (verification !== void 0) receipt.verification = verification;
1360
+ if (job.metadata !== void 0) receipt.metadata = { ...job.metadata };
1361
+ return receipt;
1362
+ }
1363
+ function normalizeVerificationResult(result) {
1364
+ const verification = result.verification;
1365
+ if (verification !== void 0) {
1366
+ const normalized2 = { verified: verification.verified };
1367
+ if (verification.method !== void 0) normalized2.method = verification.method;
1368
+ if (verification.checksum !== void 0) normalized2.checksum = verification.checksum;
1369
+ if (verification.expectedChecksum !== void 0) {
1370
+ normalized2.expectedChecksum = verification.expectedChecksum;
1371
+ }
1372
+ if (verification.actualChecksum !== void 0)
1373
+ normalized2.actualChecksum = verification.actualChecksum;
1374
+ if (verification.details !== void 0) normalized2.details = { ...verification.details };
1375
+ return normalized2;
1376
+ }
1377
+ if (result.verified === void 0 && result.checksum === void 0) {
1378
+ return void 0;
1379
+ }
1380
+ const normalized = { verified: result.verified ?? false };
1381
+ if (result.checksum !== void 0) {
1382
+ normalized.checksum = result.checksum;
1383
+ }
1384
+ return normalized;
1385
+ }
1386
+ function createTransferFailure(job, error, attempts) {
1387
+ return new TransferError({
1388
+ cause: error,
1389
+ details: {
1390
+ attempts,
1391
+ jobId: job.id,
1392
+ operation: job.operation
1393
+ },
1394
+ message: `Transfer job failed: ${job.id}`,
1395
+ retryable: isRetryable(error)
1396
+ });
1397
+ }
1398
+ function summarizeError(error) {
1399
+ if (error instanceof ZeroTransferError) {
1400
+ return {
1401
+ code: error.code,
1402
+ message: error.message,
1403
+ name: error.name,
1404
+ retryable: error.retryable
1405
+ };
1406
+ }
1407
+ if (error instanceof Error) {
1408
+ return {
1409
+ message: error.message,
1410
+ name: error.name
1411
+ };
1412
+ }
1413
+ return {
1414
+ message: String(error),
1415
+ name: "Error"
1416
+ };
1417
+ }
1418
+ function isRetryable(error) {
1419
+ return error instanceof ZeroTransferError && error.retryable;
1420
+ }
1421
+ function calculateBytesPerSecond2(bytes, durationMs) {
1422
+ if (durationMs <= 0) {
1423
+ return bytes;
1424
+ }
1425
+ return bytes / (durationMs / 1e3);
1426
+ }
1427
+
1428
+ // src/mft/runRoute.ts
1429
+ async function runRoute(options) {
1430
+ const { client, route } = options;
1431
+ if (route.enabled === false) {
1432
+ throw new ConfigurationError({
1433
+ details: { routeId: route.id },
1434
+ message: `MFT route "${route.id}" is disabled`,
1435
+ retryable: false
1436
+ });
1437
+ }
1438
+ const sourceSession = await client.connect(route.source.profile);
1439
+ let destinationSession;
1440
+ try {
1441
+ destinationSession = await client.connect(route.destination.profile);
1442
+ const engine = options.engine ?? new TransferEngine();
1443
+ const job = createRouteJob(route, sourceSession, destinationSession, options);
1444
+ const sessions = /* @__PURE__ */ new Map([
1445
+ ["source", sourceSession],
1446
+ ["destination", destinationSession]
1447
+ ]);
1448
+ const executor = createProviderTransferExecutor({
1449
+ resolveSession: ({ role }) => sessions.get(role)
1450
+ });
1451
+ return await engine.execute(job, executor, buildExecuteOptions(options));
1452
+ } finally {
1453
+ if (destinationSession !== void 0) {
1454
+ await destinationSession.disconnect();
1455
+ }
1456
+ await sourceSession.disconnect();
1457
+ }
1458
+ }
1459
+ function createRouteJob(route, sourceSession, destinationSession, options) {
1460
+ const operation = route.operation ?? "copy";
1461
+ const source = {
1462
+ path: route.source.path,
1463
+ provider: sourceSession.provider
1464
+ };
1465
+ const destination = {
1466
+ path: route.destination.path,
1467
+ provider: destinationSession.provider
1468
+ };
1469
+ const baseMetadata = { routeId: route.id };
1470
+ if (route.name !== void 0) baseMetadata["routeName"] = route.name;
1471
+ if (route.metadata !== void 0) Object.assign(baseMetadata, route.metadata);
1472
+ if (options.metadata !== void 0) Object.assign(baseMetadata, options.metadata);
1473
+ const job = {
1474
+ destination,
1475
+ id: options.jobId ?? defaultJobId(route, options.now),
1476
+ operation,
1477
+ source
1478
+ };
1479
+ if (Object.keys(baseMetadata).length > 0) {
1480
+ job.metadata = baseMetadata;
1481
+ }
1482
+ return job;
1483
+ }
1484
+ function defaultJobId(route, now) {
1485
+ const timestamp = (now?.() ?? /* @__PURE__ */ new Date()).getTime();
1486
+ return `route:${route.id}:${timestamp.toString(36)}`;
1487
+ }
1488
+ function buildExecuteOptions(options) {
1489
+ const execute = {};
1490
+ if (options.signal !== void 0) execute.signal = options.signal;
1491
+ if (options.retry !== void 0) execute.retry = options.retry;
1492
+ if (options.onProgress !== void 0) execute.onProgress = options.onProgress;
1493
+ if (options.timeout !== void 0) execute.timeout = options.timeout;
1494
+ if (options.bandwidthLimit !== void 0) execute.bandwidthLimit = options.bandwidthLimit;
1495
+ return execute;
1496
+ }
1497
+
1498
+ // src/client/operations.ts
1499
+ var LOCAL_PROFILE = { host: "local", provider: "local" };
1500
+ function uploadFile(options) {
1501
+ const { client, destination, localPath, routeId, routeName, ...rest } = options;
1502
+ const route = buildRoute({
1503
+ destination: { path: destination.path, profile: destination.profile },
1504
+ id: routeId ?? `upload:${defaultRouteSuffix(localPath, destination.path)}`,
1505
+ name: routeName,
1506
+ operation: "upload",
1507
+ source: { path: absolutePath(localPath), profile: LOCAL_PROFILE }
1508
+ });
1509
+ return runRoute({ client, route, ...rest });
1510
+ }
1511
+ function downloadFile(options) {
1512
+ const { client, localPath, routeId, routeName, source, ...rest } = options;
1513
+ const route = buildRoute({
1514
+ destination: { path: absolutePath(localPath), profile: LOCAL_PROFILE },
1515
+ id: routeId ?? `download:${defaultRouteSuffix(source.path, localPath)}`,
1516
+ name: routeName,
1517
+ operation: "download",
1518
+ source: { path: source.path, profile: source.profile }
1519
+ });
1520
+ return runRoute({ client, route, ...rest });
1521
+ }
1522
+ function copyBetween(options) {
1523
+ const { client, destination, routeId, routeName, source, ...rest } = options;
1524
+ const route = buildRoute({
1525
+ destination: { path: destination.path, profile: destination.profile },
1526
+ id: routeId ?? `copy:${defaultRouteSuffix(source.path, destination.path)}`,
1527
+ name: routeName,
1528
+ operation: "copy",
1529
+ source: { path: source.path, profile: source.profile }
1530
+ });
1531
+ return runRoute({ client, route, ...rest });
1532
+ }
1533
+ function buildRoute(input) {
1534
+ const route = {
1535
+ destination: input.destination,
1536
+ id: input.id,
1537
+ operation: input.operation,
1538
+ source: input.source
1539
+ };
1540
+ if (input.name !== void 0) route.name = input.name;
1541
+ return route;
1542
+ }
1543
+ function absolutePath(localPath) {
1544
+ return isAbsolute(localPath) ? localPath : resolvePath(localPath);
1545
+ }
1546
+ function defaultRouteSuffix(source, destination) {
1547
+ return `${source}->${destination}`;
1548
+ }
1549
+
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
+ // src/profiles/SecretSource.ts
1586
+ import { Buffer as Buffer2 } from "buffer";
1587
+ import { readFile } from "fs/promises";
1588
+ async function resolveSecret(source, options = {}) {
1589
+ if (isSecretValue(source)) {
1590
+ return cloneSecretValue(source);
1591
+ }
1592
+ if (typeof source === "function") {
1593
+ return cloneSecretValue(await source());
1594
+ }
1595
+ if (isValueSecretSource(source)) {
1596
+ return cloneSecretValue(source.value);
1597
+ }
1598
+ if (isEnvSecretSource(source)) {
1599
+ const value = (options.env ?? process.env)[source.env];
1600
+ if (value === void 0) {
1601
+ throw createSecretConfigurationError(
1602
+ "Secret environment variable is not set",
1603
+ "env",
1604
+ source.env
1605
+ );
1606
+ }
1607
+ return value;
1608
+ }
1609
+ if (isBase64EnvSecretSource(source)) {
1610
+ const value = (options.env ?? process.env)[source.base64Env];
1611
+ if (value === void 0) {
1612
+ throw createSecretConfigurationError(
1613
+ "Secret environment variable is not set",
1614
+ "base64Env",
1615
+ source.base64Env
1616
+ );
1617
+ }
1618
+ return Buffer2.from(value, "base64");
1619
+ }
1620
+ if (isFileSecretSource(source)) {
1621
+ const fileReader = options.readFile ?? readFile;
1622
+ const value = await fileReader(source.path);
1623
+ if (source.encoding === "buffer") {
1624
+ return Buffer2.from(value);
1625
+ }
1626
+ return value.toString(source.encoding ?? "utf8");
1627
+ }
1628
+ throw createSecretConfigurationError("Unsupported secret source", "source", "unknown");
1629
+ }
1630
+ function redactSecretSource(source) {
1631
+ if (isSecretValue(source) || typeof source === "function") {
1632
+ return REDACTED;
1633
+ }
1634
+ if (isValueSecretSource(source)) return { value: REDACTED };
1635
+ if (isEnvSecretSource(source)) return { env: REDACTED };
1636
+ if (isBase64EnvSecretSource(source)) return { base64Env: REDACTED };
1637
+ if (isFileSecretSource(source)) return { encoding: source.encoding, path: REDACTED };
1638
+ return REDACTED;
1639
+ }
1640
+ function isSecretValue(value) {
1641
+ return typeof value === "string" || Buffer2.isBuffer(value);
1642
+ }
1643
+ function isValueSecretSource(value) {
1644
+ return isRecord(value) && "value" in value && isSecretValue(value.value);
1645
+ }
1646
+ function isEnvSecretSource(value) {
1647
+ return isRecord(value) && typeof value.env === "string";
1648
+ }
1649
+ function isBase64EnvSecretSource(value) {
1650
+ return isRecord(value) && typeof value.base64Env === "string";
1651
+ }
1652
+ function isFileSecretSource(value) {
1653
+ return isRecord(value) && typeof value.path === "string";
1654
+ }
1655
+ function isRecord(value) {
1656
+ return typeof value === "object" && value !== null;
1657
+ }
1658
+ function cloneSecretValue(value) {
1659
+ return Buffer2.isBuffer(value) ? Buffer2.from(value) : value;
1660
+ }
1661
+ function createSecretConfigurationError(message, sourceType, sourceName) {
1662
+ return new ConfigurationError({
1663
+ details: { sourceName, sourceType },
1664
+ message,
1665
+ retryable: false
1666
+ });
1667
+ }
1668
+
1669
+ // src/profiles/ProfileRedactor.ts
1670
+ function redactConnectionProfile(profile) {
1671
+ const { logger, password, signal, ssh, tls, username, ...rest } = profile;
1672
+ const redacted = redactObject(rest);
1673
+ if (username !== void 0) redacted.username = redactSecretSource(username);
1674
+ if (password !== void 0) redacted.password = redactSecretSource(password);
1675
+ if (ssh !== void 0) redacted.ssh = redactSshProfile(ssh);
1676
+ if (tls !== void 0) redacted.tls = redactTlsProfile(tls);
1677
+ if (signal !== void 0) redacted.signal = "[AbortSignal]";
1678
+ if (logger !== void 0) redacted.logger = REDACTED;
1679
+ return redacted;
1680
+ }
1681
+ function redactSshProfile(profile) {
1682
+ const { agent, keyboardInteractive, knownHosts, passphrase, privateKey, socketFactory, ...rest } = profile;
1683
+ const redacted = redactObject(rest);
1684
+ if (agent !== void 0) redacted.agent = REDACTED;
1685
+ if (privateKey !== void 0) redacted.privateKey = redactSecretSource(privateKey);
1686
+ if (passphrase !== void 0) redacted.passphrase = redactSecretSource(passphrase);
1687
+ if (knownHosts !== void 0) redacted.knownHosts = redactSshKnownHostsSource(knownHosts);
1688
+ if (keyboardInteractive !== void 0) redacted.keyboardInteractive = REDACTED;
1689
+ if (socketFactory !== void 0) redacted.socketFactory = REDACTED;
1690
+ return redacted;
1691
+ }
1692
+ function redactSshKnownHostsSource(source) {
1693
+ if (Array.isArray(source)) {
1694
+ return source.map((item) => redactSecretSource(item));
1695
+ }
1696
+ return redactSecretSource(source);
1697
+ }
1698
+ function redactTlsProfile(profile) {
1699
+ const { ca, cert, checkServerIdentity, key, passphrase, pfx, ...rest } = profile;
1700
+ const redacted = redactObject(rest);
1701
+ if (ca !== void 0) redacted.ca = redactTlsSecretSource(ca);
1702
+ if (cert !== void 0) redacted.cert = redactSecretSource(cert);
1703
+ if (key !== void 0) redacted.key = redactSecretSource(key);
1704
+ if (passphrase !== void 0) redacted.passphrase = redactSecretSource(passphrase);
1705
+ if (pfx !== void 0) redacted.pfx = redactSecretSource(pfx);
1706
+ if (checkServerIdentity !== void 0) redacted.checkServerIdentity = REDACTED;
1707
+ return redacted;
1708
+ }
1709
+ function redactTlsSecretSource(source) {
1710
+ if (Array.isArray(source)) {
1711
+ return source.map((item) => redactSecretSource(item));
1712
+ }
1713
+ return redactSecretSource(source);
1714
+ }
1715
+
1716
+ // src/diagnostics/index.ts
1717
+ function summarizeClientDiagnostics(client) {
1718
+ const capabilities = client.getCapabilities();
1719
+ return {
1720
+ providers: capabilities.map((entry) => ({ capabilities: entry, id: entry.provider }))
1721
+ };
1722
+ }
1723
+ async function runConnectionDiagnostics(options) {
1724
+ const now = options.now ?? (() => performance.now());
1725
+ const probeList = options.probeList !== false;
1726
+ const listPath = options.listPath ?? "/";
1727
+ const sampleSize = Math.max(0, options.sampleSize ?? 5);
1728
+ const redactedProfile = redactConnectionProfile(options.profile);
1729
+ const result = {
1730
+ host: options.profile.host,
1731
+ ok: false,
1732
+ redactedProfile,
1733
+ timings: {}
1734
+ };
1735
+ const connectStart = now();
1736
+ try {
1737
+ const session = await options.client.connect(options.profile);
1738
+ result.timings.connectMs = now() - connectStart;
1739
+ result.provider = session.provider;
1740
+ result.capabilities = session.capabilities;
1741
+ try {
1742
+ if (probeList) {
1743
+ const listStart = now();
1744
+ const entries = await session.fs.list(listPath);
1745
+ result.timings.listMs = now() - listStart;
1746
+ result.sample = entries.slice(0, sampleSize);
1747
+ }
1748
+ result.ok = true;
1749
+ } finally {
1750
+ const disconnectStart = now();
1751
+ await session.disconnect();
1752
+ result.timings.disconnectMs = now() - disconnectStart;
1753
+ }
1754
+ } catch (error) {
1755
+ result.error = summarizeDiagnosticError(error);
1756
+ }
1757
+ return result;
1758
+ }
1759
+ function summarizeDiagnosticError(error) {
1760
+ if (error instanceof Error) {
1761
+ const summary = { message: error.message };
1762
+ if (error.name !== "Error") summary.name = error.name;
1763
+ const code = error.code;
1764
+ if (typeof code === "string") summary.code = code;
1765
+ return summary;
1766
+ }
1767
+ return { message: String(error) };
1768
+ }
1769
+
1770
+ // src/providers/local/LocalProvider.ts
1771
+ import { createReadStream } from "fs";
1772
+ import {
1773
+ lstat,
1774
+ mkdir,
1775
+ open,
1776
+ readdir,
1777
+ readlink,
1778
+ rename,
1779
+ rm,
1780
+ unlink,
1781
+ writeFile
1782
+ } from "fs/promises";
1783
+ import path from "path";
1784
+
1785
+ // src/utils/path.ts
1786
+ var UNSAFE_FTP_ARGUMENT_PATTERN = /[\r\n]/;
1787
+ function assertSafeFtpArgument(value, label = "path") {
1788
+ if (UNSAFE_FTP_ARGUMENT_PATTERN.test(value)) {
1789
+ throw new ConfigurationError({
1790
+ message: `Unsafe FTP ${label}: CR and LF characters are not allowed`,
1791
+ retryable: false,
1792
+ details: {
1793
+ label
1794
+ }
1795
+ });
1796
+ }
1797
+ return value;
1798
+ }
1799
+ function normalizeRemotePath(input) {
1800
+ assertSafeFtpArgument(input);
1801
+ if (input.length === 0) {
1802
+ return ".";
1803
+ }
1804
+ const isAbsolute2 = input.startsWith("/");
1805
+ const segments = [];
1806
+ for (const segment of input.split(/[\\/]+/)) {
1807
+ if (segment.length === 0 || segment === ".") {
1808
+ continue;
1809
+ }
1810
+ if (segment === "..") {
1811
+ if (segments.length > 0 && segments[segments.length - 1] !== "..") {
1812
+ segments.pop();
1813
+ } else if (!isAbsolute2) {
1814
+ segments.push(segment);
1815
+ }
1816
+ continue;
1817
+ }
1818
+ segments.push(segment);
1819
+ }
1820
+ const normalized = segments.join("/");
1821
+ if (isAbsolute2) {
1822
+ return normalized.length > 0 ? `/${normalized}` : "/";
1823
+ }
1824
+ return normalized.length > 0 ? normalized : ".";
1825
+ }
1826
+ function joinRemotePath(...segments) {
1827
+ if (segments.length === 0) {
1828
+ return ".";
1829
+ }
1830
+ return normalizeRemotePath(segments.join("/"));
1831
+ }
1832
+ function basenameRemotePath(input) {
1833
+ const normalized = normalizeRemotePath(input);
1834
+ const parts = normalized.split("/").filter(Boolean);
1835
+ return parts[parts.length - 1] ?? normalized;
1836
+ }
1837
+
1838
+ // src/providers/local/LocalProvider.ts
1839
+ var LOCAL_PROVIDER_ID = "local";
1840
+ var LOCAL_PROVIDER_CAPABILITIES = {
1841
+ provider: LOCAL_PROVIDER_ID,
1842
+ authentication: ["anonymous"],
1843
+ list: true,
1844
+ stat: true,
1845
+ readStream: true,
1846
+ writeStream: true,
1847
+ serverSideCopy: false,
1848
+ serverSideMove: false,
1849
+ resumeDownload: true,
1850
+ resumeUpload: true,
1851
+ checksum: [],
1852
+ atomicRename: false,
1853
+ chmod: false,
1854
+ chown: false,
1855
+ symlink: true,
1856
+ metadata: ["accessedAt", "createdAt", "modifiedAt", "permissions", "symlinkTarget", "uniqueId"],
1857
+ maxConcurrency: 16,
1858
+ notes: ["Local filesystem provider for tests and local-only workflows"]
1859
+ };
1860
+ function createLocalProviderFactory(options = {}) {
1861
+ return {
1862
+ id: LOCAL_PROVIDER_ID,
1863
+ capabilities: LOCAL_PROVIDER_CAPABILITIES,
1864
+ create: () => new LocalProvider(options.rootPath)
1865
+ };
1866
+ }
1867
+ var LocalProvider = class {
1868
+ constructor(configuredRootPath) {
1869
+ this.configuredRootPath = configuredRootPath;
1870
+ }
1871
+ configuredRootPath;
1872
+ id = LOCAL_PROVIDER_ID;
1873
+ capabilities = LOCAL_PROVIDER_CAPABILITIES;
1874
+ connect(profile) {
1875
+ return Promise.resolve().then(() => {
1876
+ const rootPath = path.resolve(this.configuredRootPath ?? profile.host);
1877
+ return new LocalTransferSession(rootPath);
1878
+ });
1879
+ }
1880
+ };
1881
+ var LocalTransferSession = class {
1882
+ provider = LOCAL_PROVIDER_ID;
1883
+ capabilities = LOCAL_PROVIDER_CAPABILITIES;
1884
+ fs;
1885
+ transfers;
1886
+ constructor(rootPath) {
1887
+ this.fs = new LocalFileSystem(rootPath);
1888
+ this.transfers = new LocalTransferOperations(rootPath);
1889
+ }
1890
+ disconnect() {
1891
+ return Promise.resolve();
1892
+ }
1893
+ };
1894
+ var LocalTransferOperations = class {
1895
+ constructor(rootPath) {
1896
+ this.rootPath = rootPath;
1897
+ }
1898
+ rootPath;
1899
+ async read(request) {
1900
+ request.throwIfAborted();
1901
+ const remotePath = normalizeLocalProviderPath(request.endpoint.path);
1902
+ const entry = await readLocalEntry(this.rootPath, remotePath);
1903
+ if (entry.type !== "file") {
1904
+ throw createPathNotFoundError(remotePath, `Local provider path is not a file: ${remotePath}`);
1905
+ }
1906
+ const range = resolveReadRange(entry.size ?? 0, request.range);
1907
+ const result = {
1908
+ content: createLocalReadSource(resolveLocalPath(this.rootPath, remotePath), range),
1909
+ totalBytes: range.length
1910
+ };
1911
+ if (range.offset > 0) {
1912
+ result.bytesRead = range.offset;
1913
+ }
1914
+ return result;
1915
+ }
1916
+ async write(request) {
1917
+ request.throwIfAborted();
1918
+ const remotePath = normalizeLocalProviderPath(request.endpoint.path);
1919
+ const localPath = resolveLocalPath(this.rootPath, remotePath);
1920
+ const content = await collectTransferContent(request);
1921
+ const offset = normalizeOptionalByteCount(request.offset, "offset", remotePath);
1922
+ await ensureLocalParentDirectory(localPath, remotePath);
1923
+ await writeLocalContent(localPath, remotePath, content, offset);
1924
+ const stat = await readLocalEntry(this.rootPath, remotePath);
1925
+ const result = {
1926
+ bytesTransferred: content.byteLength,
1927
+ resumed: offset !== void 0 && offset > 0,
1928
+ verified: request.verification?.verified ?? false
1929
+ };
1930
+ if (stat.size !== void 0) {
1931
+ result.totalBytes = stat.size;
1932
+ }
1933
+ if (request.verification !== void 0) {
1934
+ result.verification = cloneVerification2(request.verification);
1935
+ }
1936
+ return result;
1937
+ }
1938
+ };
1939
+ var LocalFileSystem = class {
1940
+ constructor(rootPath) {
1941
+ this.rootPath = rootPath;
1942
+ }
1943
+ rootPath;
1944
+ async list(path2) {
1945
+ const remotePath = normalizeLocalProviderPath(path2);
1946
+ const directory = await this.stat(remotePath);
1947
+ if (directory.type !== "directory") {
1948
+ throw createPathNotFoundError(
1949
+ remotePath,
1950
+ `Local provider path is not a directory: ${remotePath}`
1951
+ );
1952
+ }
1953
+ const localPath = resolveLocalPath(this.rootPath, remotePath);
1954
+ const names = await readLocalDirectory(localPath, remotePath);
1955
+ const entries = await Promise.all(
1956
+ names.map((name) => readLocalEntry(this.rootPath, joinRemotePath(remotePath, name)))
1957
+ );
1958
+ return entries.sort(compareEntries);
1959
+ }
1960
+ async stat(path2) {
1961
+ return readLocalEntry(this.rootPath, normalizeLocalProviderPath(path2));
1962
+ }
1963
+ async remove(remote, options = {}) {
1964
+ const remotePath = normalizeLocalProviderPath(remote);
1965
+ const localPath = resolveLocalPath(this.rootPath, remotePath);
1966
+ try {
1967
+ await unlink(localPath);
1968
+ } catch (error) {
1969
+ if (options.ignoreMissing && isNodeErrno(error, "ENOENT")) return;
1970
+ if (isNodeErrno(error, "ENOENT")) {
1971
+ throw createPathNotFoundError(remotePath, `Local path not found: ${remotePath}`);
1972
+ }
1973
+ throw error;
1974
+ }
1975
+ }
1976
+ async rename(from, to) {
1977
+ const fromRemote = normalizeLocalProviderPath(from);
1978
+ const toRemote = normalizeLocalProviderPath(to);
1979
+ const fromLocal = resolveLocalPath(this.rootPath, fromRemote);
1980
+ const toLocal = resolveLocalPath(this.rootPath, toRemote);
1981
+ try {
1982
+ await rename(fromLocal, toLocal);
1983
+ } catch (error) {
1984
+ if (isNodeErrno(error, "ENOENT")) {
1985
+ throw createPathNotFoundError(fromRemote, `Local path not found: ${fromRemote}`);
1986
+ }
1987
+ throw error;
1988
+ }
1989
+ }
1990
+ async mkdir(remote, options = {}) {
1991
+ const remotePath = normalizeLocalProviderPath(remote);
1992
+ const localPath = resolveLocalPath(this.rootPath, remotePath);
1993
+ await mkdir(localPath, { recursive: options.recursive === true });
1994
+ }
1995
+ async rmdir(remote, options = {}) {
1996
+ const remotePath = normalizeLocalProviderPath(remote);
1997
+ const localPath = resolveLocalPath(this.rootPath, remotePath);
1998
+ try {
1999
+ await rm(localPath, { recursive: options.recursive === true, force: false });
2000
+ } catch (error) {
2001
+ if (isNodeErrno(error, "ENOENT")) {
2002
+ if (options.ignoreMissing) return;
2003
+ throw createPathNotFoundError(remotePath, `Local path not found: ${remotePath}`);
2004
+ }
2005
+ throw error;
2006
+ }
2007
+ }
2008
+ };
2009
+ function isNodeErrno(error, code) {
2010
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
2011
+ }
2012
+ function resolveReadRange(size, range) {
2013
+ if (range === void 0) {
2014
+ return { length: size, offset: 0 };
2015
+ }
2016
+ const requestedOffset = normalizeByteCount(range.offset, "offset", "/");
2017
+ const requestedLength = range.length === void 0 ? size - Math.min(requestedOffset, size) : normalizeByteCount(range.length, "length", "/");
2018
+ const offset = Math.min(requestedOffset, size);
2019
+ const length = Math.max(0, Math.min(requestedLength, size - offset));
2020
+ return { length, offset };
2021
+ }
2022
+ async function* createLocalReadSource(localPath, range) {
2023
+ if (range.length <= 0) {
2024
+ return;
2025
+ }
2026
+ const stream = createReadStream(localPath, {
2027
+ end: range.offset + range.length - 1,
2028
+ start: range.offset
2029
+ });
2030
+ for await (const chunk of stream) {
2031
+ yield new Uint8Array(chunk);
2032
+ }
2033
+ }
2034
+ async function collectTransferContent(request) {
2035
+ const chunks = [];
2036
+ let byteLength = 0;
2037
+ for await (const chunk of request.content) {
2038
+ request.throwIfAborted();
2039
+ const clonedChunk = new Uint8Array(chunk);
2040
+ chunks.push(clonedChunk);
2041
+ byteLength += clonedChunk.byteLength;
2042
+ request.reportProgress(byteLength, request.totalBytes);
2043
+ }
2044
+ return concatChunks(chunks, byteLength);
2045
+ }
2046
+ function concatChunks(chunks, byteLength) {
2047
+ const content = new Uint8Array(byteLength);
2048
+ let offset = 0;
2049
+ for (const chunk of chunks) {
2050
+ content.set(chunk, offset);
2051
+ offset += chunk.byteLength;
2052
+ }
2053
+ return content;
2054
+ }
2055
+ async function ensureLocalParentDirectory(localPath, remotePath) {
2056
+ try {
2057
+ await mkdir(path.dirname(localPath), { recursive: true });
2058
+ } catch (error) {
2059
+ throw mapLocalFileSystemError(error, remotePath);
2060
+ }
2061
+ }
2062
+ async function writeLocalContent(localPath, remotePath, content, offset) {
2063
+ try {
2064
+ if (offset === void 0) {
2065
+ await writeFile(localPath, content);
2066
+ return;
2067
+ }
2068
+ const handle = await openLocalFileForOffsetWrite(localPath);
2069
+ try {
2070
+ await handle.write(content, 0, content.byteLength, offset);
2071
+ } finally {
2072
+ await handle.close();
2073
+ }
2074
+ } catch (error) {
2075
+ throw mapLocalFileSystemError(error, remotePath);
2076
+ }
2077
+ }
2078
+ async function openLocalFileForOffsetWrite(localPath) {
2079
+ try {
2080
+ return await open(localPath, "r+");
2081
+ } catch (error) {
2082
+ if (getErrorCode(error) === "ENOENT") {
2083
+ return open(localPath, "w+");
2084
+ }
2085
+ throw error;
2086
+ }
2087
+ }
2088
+ function normalizeOptionalByteCount(value, field, remotePath) {
2089
+ return value === void 0 ? void 0 : normalizeByteCount(value, field, remotePath);
2090
+ }
2091
+ function normalizeByteCount(value, field, remotePath) {
2092
+ if (!Number.isFinite(value) || value < 0) {
2093
+ throw new ConfigurationError({
2094
+ details: { field, provider: LOCAL_PROVIDER_ID },
2095
+ message: `Local provider ${field} must be a non-negative number`,
2096
+ path: remotePath,
2097
+ retryable: false
2098
+ });
2099
+ }
2100
+ return Math.floor(value);
2101
+ }
2102
+ function cloneVerification2(verification) {
2103
+ const clone = { verified: verification.verified };
2104
+ if (verification.method !== void 0) clone.method = verification.method;
2105
+ if (verification.checksum !== void 0) clone.checksum = verification.checksum;
2106
+ if (verification.expectedChecksum !== void 0) {
2107
+ clone.expectedChecksum = verification.expectedChecksum;
2108
+ }
2109
+ if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
2110
+ if (verification.details !== void 0) clone.details = { ...verification.details };
2111
+ return clone;
2112
+ }
2113
+ async function readLocalDirectory(localPath, remotePath) {
2114
+ try {
2115
+ return await readdir(localPath);
2116
+ } catch (error) {
2117
+ throw mapLocalFileSystemError(error, remotePath);
2118
+ }
2119
+ }
2120
+ async function readLocalEntry(rootPath, remotePath) {
2121
+ const localPath = resolveLocalPath(rootPath, remotePath);
2122
+ let stats;
2123
+ try {
2124
+ stats = await lstat(localPath);
2125
+ } catch (error) {
2126
+ throw mapLocalFileSystemError(error, remotePath);
2127
+ }
2128
+ const entry = {
2129
+ accessedAt: cloneDate(stats.atime),
2130
+ createdAt: cloneDate(stats.birthtime),
2131
+ modifiedAt: cloneDate(stats.mtime),
2132
+ name: basenameRemotePath(remotePath),
2133
+ path: remotePath,
2134
+ permissions: { raw: formatMode(stats.mode) },
2135
+ size: stats.size,
2136
+ type: getLocalEntryType(stats),
2137
+ uniqueId: `${stats.dev}:${stats.ino}`
2138
+ };
2139
+ if (entry.type === "symlink") {
2140
+ const symlinkTarget = await readSymlinkTarget(localPath);
2141
+ if (symlinkTarget !== void 0) {
2142
+ entry.symlinkTarget = symlinkTarget;
2143
+ }
2144
+ }
2145
+ return {
2146
+ ...entry,
2147
+ exists: true
2148
+ };
2149
+ }
2150
+ async function readSymlinkTarget(localPath) {
2151
+ try {
2152
+ return await readlink(localPath);
2153
+ } catch {
2154
+ return void 0;
2155
+ }
2156
+ }
2157
+ function normalizeLocalProviderPath(input) {
2158
+ const normalized = normalizeRemotePath(input);
2159
+ if (normalized === ".." || normalized.startsWith("../")) {
2160
+ throw new ConfigurationError({
2161
+ details: { provider: LOCAL_PROVIDER_ID },
2162
+ message: `Local provider path escapes the configured root: ${normalized}`,
2163
+ path: normalized,
2164
+ retryable: false
2165
+ });
2166
+ }
2167
+ if (normalized === "." || normalized === "/") {
2168
+ return "/";
2169
+ }
2170
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
2171
+ }
2172
+ function resolveLocalPath(rootPath, remotePath) {
2173
+ const normalizedRemotePath = normalizeLocalProviderPath(remotePath);
2174
+ const resolvedRootPath = path.resolve(rootPath);
2175
+ const candidateAbsolute = path.resolve(normalizedRemotePath.split("/").join(path.sep));
2176
+ if (candidateAbsolute === resolvedRootPath || candidateAbsolute.startsWith(resolvedRootPath + path.sep)) {
2177
+ return candidateAbsolute;
2178
+ }
2179
+ const relativePath = normalizedRemotePath === "/" ? "." : normalizedRemotePath.slice(1);
2180
+ const resolvedPath = path.resolve(rootPath, relativePath.split("/").join(path.sep));
2181
+ const relativeToRoot = path.relative(rootPath, resolvedPath);
2182
+ if (relativeToRoot === "" || !relativeToRoot.startsWith("..") && !path.isAbsolute(relativeToRoot)) {
2183
+ return resolvedPath;
2184
+ }
2185
+ throw new ConfigurationError({
2186
+ details: { provider: LOCAL_PROVIDER_ID, rootPath },
2187
+ message: `Local provider path escapes the configured root: ${normalizedRemotePath}`,
2188
+ path: normalizedRemotePath,
2189
+ retryable: false
2190
+ });
2191
+ }
2192
+ function getLocalEntryType(stats) {
2193
+ if (stats.isFile()) return "file";
2194
+ if (stats.isDirectory()) return "directory";
2195
+ if (stats.isSymbolicLink()) return "symlink";
2196
+ return "unknown";
2197
+ }
2198
+ function formatMode(mode) {
2199
+ return (mode & 511).toString(8).padStart(3, "0");
2200
+ }
2201
+ function cloneDate(value) {
2202
+ return new Date(value.getTime());
2203
+ }
2204
+ function compareEntries(left, right) {
2205
+ return left.path.localeCompare(right.path);
2206
+ }
2207
+ function mapLocalFileSystemError(error, remotePath) {
2208
+ const code = getErrorCode(error);
2209
+ if (code === "ENOENT" || code === "ENOTDIR") {
2210
+ return createPathNotFoundError(remotePath, `Local provider path not found: ${remotePath}`);
2211
+ }
2212
+ if (code === "EACCES" || code === "EPERM") {
2213
+ return new PermissionDeniedError({
2214
+ cause: error,
2215
+ details: { provider: LOCAL_PROVIDER_ID },
2216
+ message: `Local provider permission denied: ${remotePath}`,
2217
+ path: remotePath,
2218
+ retryable: false
2219
+ });
2220
+ }
2221
+ return new ConfigurationError({
2222
+ cause: error,
2223
+ details: { code, provider: LOCAL_PROVIDER_ID },
2224
+ message: `Local provider filesystem operation failed: ${remotePath}`,
2225
+ path: remotePath,
2226
+ retryable: false
2227
+ });
2228
+ }
2229
+ function createPathNotFoundError(path2, message) {
2230
+ return new PathNotFoundError({
2231
+ details: { provider: LOCAL_PROVIDER_ID },
2232
+ message,
2233
+ path: path2,
2234
+ retryable: false
2235
+ });
2236
+ }
2237
+ function getErrorCode(error) {
2238
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
2239
+ }
2240
+
2241
+ // src/providers/memory/MemoryProvider.ts
2242
+ import { Buffer as Buffer3 } from "buffer";
2243
+ var MEMORY_PROVIDER_ID = "memory";
2244
+ var MEMORY_PROVIDER_CAPABILITIES = {
2245
+ provider: MEMORY_PROVIDER_ID,
2246
+ authentication: ["anonymous"],
2247
+ list: true,
2248
+ stat: true,
2249
+ readStream: true,
2250
+ writeStream: true,
2251
+ serverSideCopy: false,
2252
+ serverSideMove: false,
2253
+ resumeDownload: true,
2254
+ resumeUpload: true,
2255
+ checksum: [],
2256
+ atomicRename: false,
2257
+ chmod: false,
2258
+ chown: false,
2259
+ symlink: true,
2260
+ metadata: [
2261
+ "accessedAt",
2262
+ "createdAt",
2263
+ "group",
2264
+ "modifiedAt",
2265
+ "owner",
2266
+ "permissions",
2267
+ "symlinkTarget",
2268
+ "uniqueId"
2269
+ ],
2270
+ maxConcurrency: 32,
2271
+ notes: ["Deterministic in-memory provider for tests and provider contract validation"]
2272
+ };
2273
+ function createMemoryProviderFactory(options = {}) {
2274
+ const state = createMemoryState(options.entries ?? []);
2275
+ return {
2276
+ id: MEMORY_PROVIDER_ID,
2277
+ capabilities: MEMORY_PROVIDER_CAPABILITIES,
2278
+ create: () => new MemoryProvider(state)
2279
+ };
2280
+ }
2281
+ var MemoryProvider = class {
2282
+ constructor(state) {
2283
+ this.state = state;
2284
+ }
2285
+ state;
2286
+ id = MEMORY_PROVIDER_ID;
2287
+ capabilities = MEMORY_PROVIDER_CAPABILITIES;
2288
+ connect() {
2289
+ return Promise.resolve(new MemoryTransferSession(this.state));
2290
+ }
2291
+ };
2292
+ var MemoryTransferSession = class {
2293
+ provider = MEMORY_PROVIDER_ID;
2294
+ capabilities = MEMORY_PROVIDER_CAPABILITIES;
2295
+ fs;
2296
+ transfers;
2297
+ constructor(state) {
2298
+ this.fs = new MemoryFileSystem(state);
2299
+ this.transfers = new MemoryTransferOperations(state);
2300
+ }
2301
+ disconnect() {
2302
+ return Promise.resolve();
2303
+ }
2304
+ };
2305
+ var MemoryFileSystem = class {
2306
+ constructor(state) {
2307
+ this.state = state;
2308
+ }
2309
+ state;
2310
+ list(path2) {
2311
+ return Promise.resolve().then(() => {
2312
+ const normalizedPath = normalizeMemoryPath(path2);
2313
+ const directory = this.requireEntry(normalizedPath);
2314
+ if (directory.type !== "directory") {
2315
+ throw createPathNotFoundError2(
2316
+ normalizedPath,
2317
+ `Memory path is not a directory: ${normalizedPath}`
2318
+ );
2319
+ }
2320
+ return [...this.state.entries.values()].filter(
2321
+ (entry) => entry.path !== normalizedPath && getParentPath(entry.path) === normalizedPath
2322
+ ).map(cloneRemoteEntry).sort(compareEntries2);
2323
+ });
2324
+ }
2325
+ stat(path2) {
2326
+ return Promise.resolve().then(
2327
+ () => cloneRemoteStat(this.requireEntry(normalizeMemoryPath(path2)))
2328
+ );
2329
+ }
2330
+ remove(path2, options = {}) {
2331
+ return Promise.resolve().then(() => {
2332
+ const normalized = normalizeMemoryPath(path2);
2333
+ const entry = this.state.entries.get(normalized);
2334
+ if (entry === void 0) {
2335
+ if (options.ignoreMissing) return;
2336
+ throw createPathNotFoundError2(normalized, `Memory path not found: ${normalized}`);
2337
+ }
2338
+ if (entry.type === "directory") {
2339
+ throw createPathNotFoundError2(
2340
+ normalized,
2341
+ `Memory path is a directory; use rmdir: ${normalized}`
2342
+ );
2343
+ }
2344
+ this.state.entries.delete(normalized);
2345
+ this.state.content.delete(normalized);
2346
+ });
2347
+ }
2348
+ rename(from, to) {
2349
+ return Promise.resolve().then(() => {
2350
+ const fromPath = normalizeMemoryPath(from);
2351
+ const toPath = normalizeMemoryPath(to);
2352
+ const entry = this.state.entries.get(fromPath);
2353
+ if (entry === void 0) {
2354
+ throw createPathNotFoundError2(fromPath, `Memory path not found: ${fromPath}`);
2355
+ }
2356
+ ensureParentDirectories(this.state.entries, toPath);
2357
+ const moved = { ...entry, path: toPath, name: basenameRemotePath(toPath) };
2358
+ this.state.entries.delete(fromPath);
2359
+ this.state.entries.set(toPath, moved);
2360
+ const content = this.state.content.get(fromPath);
2361
+ if (content !== void 0) {
2362
+ this.state.content.delete(fromPath);
2363
+ this.state.content.set(toPath, content);
2364
+ }
2365
+ });
2366
+ }
2367
+ mkdir(path2, options = {}) {
2368
+ return Promise.resolve().then(() => {
2369
+ const normalized = normalizeMemoryPath(path2);
2370
+ const existing = this.state.entries.get(normalized);
2371
+ if (existing !== void 0) {
2372
+ if (existing.type === "directory" && options.recursive) return;
2373
+ throw createInvalidFixtureError(normalized, `Memory path already exists: ${normalized}`);
2374
+ }
2375
+ if (options.recursive) {
2376
+ ensureParentDirectories(this.state.entries, normalized);
2377
+ } else {
2378
+ const parent = getParentPath(normalized);
2379
+ if (parent !== void 0 && !this.state.entries.has(parent)) {
2380
+ throw createPathNotFoundError2(parent, `Memory parent not found: ${parent}`);
2381
+ }
2382
+ }
2383
+ this.state.entries.set(normalized, createDirectoryEntry(normalized));
2384
+ });
2385
+ }
2386
+ rmdir(path2, options = {}) {
2387
+ return Promise.resolve().then(() => {
2388
+ const normalized = normalizeMemoryPath(path2);
2389
+ const entry = this.state.entries.get(normalized);
2390
+ if (entry === void 0) {
2391
+ if (options.ignoreMissing) return;
2392
+ throw createPathNotFoundError2(normalized, `Memory path not found: ${normalized}`);
2393
+ }
2394
+ if (entry.type !== "directory") {
2395
+ throw createPathNotFoundError2(normalized, `Memory path is not a directory: ${normalized}`);
2396
+ }
2397
+ const children = [...this.state.entries.values()].filter(
2398
+ (child) => child.path !== normalized && getParentPath(child.path) === normalized
2399
+ );
2400
+ if (children.length > 0 && !options.recursive) {
2401
+ throw createInvalidFixtureError(normalized, `Memory directory not empty: ${normalized}`);
2402
+ }
2403
+ const stack = [...children];
2404
+ while (stack.length > 0) {
2405
+ const next = stack.pop();
2406
+ if (!next) continue;
2407
+ if (next.type === "directory") {
2408
+ for (const grand of this.state.entries.values()) {
2409
+ if (grand.path !== next.path && getParentPath(grand.path) === next.path) {
2410
+ stack.push(grand);
2411
+ }
2412
+ }
2413
+ }
2414
+ this.state.entries.delete(next.path);
2415
+ this.state.content.delete(next.path);
2416
+ }
2417
+ this.state.entries.delete(normalized);
2418
+ });
2419
+ }
2420
+ requireEntry(path2) {
2421
+ const entry = this.state.entries.get(path2);
2422
+ if (entry === void 0) {
2423
+ throw createPathNotFoundError2(path2, `Memory path not found: ${path2}`);
2424
+ }
2425
+ return entry;
2426
+ }
2427
+ };
2428
+ var MemoryTransferOperations = class {
2429
+ constructor(state) {
2430
+ this.state = state;
2431
+ }
2432
+ state;
2433
+ read(request) {
2434
+ return Promise.resolve().then(() => {
2435
+ request.throwIfAborted();
2436
+ const path2 = normalizeMemoryPath(request.endpoint.path);
2437
+ const entry = requireFileEntry(this.state, path2);
2438
+ const content = this.state.content.get(path2) ?? new Uint8Array(entry.size ?? 0);
2439
+ const range = resolveByteRange(content.byteLength, request.range);
2440
+ const chunk = content.slice(range.offset, range.offset + range.length);
2441
+ const result = {
2442
+ content: createMemoryContentSource(chunk),
2443
+ totalBytes: chunk.byteLength
2444
+ };
2445
+ if (range.offset > 0) {
2446
+ result.bytesRead = range.offset;
2447
+ }
2448
+ return result;
2449
+ });
2450
+ }
2451
+ async write(request) {
2452
+ request.throwIfAborted();
2453
+ const path2 = normalizeMemoryPath(request.endpoint.path);
2454
+ const existing = this.state.entries.get(path2);
2455
+ if (existing?.type === "directory") {
2456
+ throw createInvalidFixtureError(path2, `Memory path is a directory: ${path2}`);
2457
+ }
2458
+ const writtenContent = await collectTransferContent2(request);
2459
+ const offset = normalizeOptionalByteCount2(request.offset, "offset");
2460
+ const previousContent = this.state.content.get(path2) ?? new Uint8Array(0);
2461
+ const content = offset === void 0 ? writtenContent : mergeContentAtOffset(previousContent, writtenContent, offset);
2462
+ ensureParentDirectories(this.state.entries, path2);
2463
+ this.state.entries.set(path2, createWrittenFileEntry(path2, content.byteLength));
2464
+ this.state.content.set(path2, content);
2465
+ const result = {
2466
+ bytesTransferred: writtenContent.byteLength,
2467
+ resumed: offset !== void 0 && offset > 0,
2468
+ totalBytes: content.byteLength,
2469
+ verified: request.verification?.verified ?? false
2470
+ };
2471
+ if (request.verification !== void 0) {
2472
+ result.verification = cloneVerification3(request.verification);
2473
+ }
2474
+ return result;
2475
+ }
2476
+ };
2477
+ function createMemoryState(entries) {
2478
+ const state = {
2479
+ content: /* @__PURE__ */ new Map(),
2480
+ entries: /* @__PURE__ */ new Map([["/", createDirectoryEntry("/")]])
2481
+ };
2482
+ for (const input of entries) {
2483
+ const entry = createMemoryEntry(input);
2484
+ const content = createMemoryContent(input, entry);
2485
+ if (entry.path === "/" && entry.type !== "directory") {
2486
+ throw createInvalidFixtureError(entry.path, "Memory provider root must be a directory");
2487
+ }
2488
+ ensureParentDirectories(state.entries, entry.path);
2489
+ state.entries.set(entry.path, entry);
2490
+ if (content !== void 0) {
2491
+ state.content.set(entry.path, content);
2492
+ }
2493
+ }
2494
+ return state;
2495
+ }
2496
+ function createMemoryEntry(input) {
2497
+ const path2 = normalizeMemoryPath(input.path);
2498
+ const entry = {
2499
+ name: input.name ?? basenameRemotePath(path2),
2500
+ path: path2,
2501
+ type: input.type
2502
+ };
2503
+ copyOptionalEntryFields(entry, input);
2504
+ const content = normalizeMemoryContent(input.content);
2505
+ if (content !== void 0) {
2506
+ entry.size = content.byteLength;
2507
+ }
2508
+ return {
2509
+ ...entry,
2510
+ exists: true
2511
+ };
2512
+ }
2513
+ function createMemoryContent(input, entry) {
2514
+ const content = normalizeMemoryContent(input.content);
2515
+ if (content !== void 0) {
2516
+ if (entry.type !== "file") {
2517
+ throw createInvalidFixtureError(
2518
+ entry.path,
2519
+ `Memory fixture content requires a file: ${entry.path}`
2520
+ );
2521
+ }
2522
+ return content;
2523
+ }
2524
+ if (entry.type === "file") {
2525
+ return new Uint8Array(entry.size ?? 0);
2526
+ }
2527
+ return void 0;
2528
+ }
2529
+ function normalizeMemoryContent(content) {
2530
+ if (content === void 0) {
2531
+ return void 0;
2532
+ }
2533
+ return typeof content === "string" ? Buffer3.from(content) : new Uint8Array(content);
2534
+ }
2535
+ function createWrittenFileEntry(path2, size) {
2536
+ return {
2537
+ exists: true,
2538
+ modifiedAt: /* @__PURE__ */ new Date(),
2539
+ name: basenameRemotePath(path2),
2540
+ path: path2,
2541
+ size,
2542
+ type: "file"
2543
+ };
2544
+ }
2545
+ function createDirectoryEntry(path2) {
2546
+ return {
2547
+ exists: true,
2548
+ name: basenameRemotePath(path2),
2549
+ path: path2,
2550
+ type: "directory"
2551
+ };
2552
+ }
2553
+ function ensureParentDirectories(state, path2) {
2554
+ for (const parentPath of getAncestorPaths(path2)) {
2555
+ const parent = state.get(parentPath);
2556
+ if (parent !== void 0 && parent.type !== "directory") {
2557
+ throw createInvalidFixtureError(
2558
+ parentPath,
2559
+ `Memory fixture parent is not a directory: ${parentPath}`
2560
+ );
2561
+ }
2562
+ if (parent === void 0) {
2563
+ state.set(parentPath, createDirectoryEntry(parentPath));
2564
+ }
2565
+ }
2566
+ }
2567
+ function normalizeMemoryPath(path2) {
2568
+ const normalized = normalizeRemotePath(path2);
2569
+ if (normalized === "." || normalized === "/") {
2570
+ return "/";
2571
+ }
2572
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
2573
+ }
2574
+ function getAncestorPaths(path2) {
2575
+ const ancestors = [];
2576
+ let parentPath = getParentPath(path2);
2577
+ while (parentPath !== void 0 && parentPath !== "/") {
2578
+ ancestors.unshift(parentPath);
2579
+ parentPath = getParentPath(parentPath);
2580
+ }
2581
+ return ancestors;
2582
+ }
2583
+ function getParentPath(path2) {
2584
+ if (path2 === "/") {
2585
+ return void 0;
2586
+ }
2587
+ const parentEnd = path2.lastIndexOf("/");
2588
+ return parentEnd <= 0 ? "/" : path2.slice(0, parentEnd);
2589
+ }
2590
+ function requireFileEntry(state, path2) {
2591
+ const entry = state.entries.get(path2);
2592
+ if (entry === void 0) {
2593
+ throw createPathNotFoundError2(path2, `Memory path not found: ${path2}`);
2594
+ }
2595
+ if (entry.type !== "file") {
2596
+ throw createPathNotFoundError2(path2, `Memory path is not a file: ${path2}`);
2597
+ }
2598
+ return entry;
2599
+ }
2600
+ function resolveByteRange(size, range) {
2601
+ if (range === void 0) {
2602
+ return { length: size, offset: 0 };
2603
+ }
2604
+ const requestedOffset = normalizeByteCount2(range.offset, "offset");
2605
+ const requestedLength = range.length === void 0 ? size - Math.min(requestedOffset, size) : normalizeByteCount2(range.length, "length");
2606
+ const offset = Math.min(requestedOffset, size);
2607
+ const length = Math.max(0, Math.min(requestedLength, size - offset));
2608
+ return { length, offset };
2609
+ }
2610
+ async function collectTransferContent2(request) {
2611
+ const chunks = [];
2612
+ let byteLength = 0;
2613
+ for await (const chunk of request.content) {
2614
+ request.throwIfAborted();
2615
+ const clonedChunk = new Uint8Array(chunk);
2616
+ chunks.push(clonedChunk);
2617
+ byteLength += clonedChunk.byteLength;
2618
+ request.reportProgress(byteLength, request.totalBytes);
2619
+ }
2620
+ return concatChunks2(chunks, byteLength);
2621
+ }
2622
+ function concatChunks2(chunks, byteLength) {
2623
+ const content = new Uint8Array(byteLength);
2624
+ let offset = 0;
2625
+ for (const chunk of chunks) {
2626
+ content.set(chunk, offset);
2627
+ offset += chunk.byteLength;
2628
+ }
2629
+ return content;
2630
+ }
2631
+ function mergeContentAtOffset(previousContent, writtenContent, offset) {
2632
+ const content = new Uint8Array(
2633
+ Math.max(previousContent.byteLength, offset + writtenContent.byteLength)
2634
+ );
2635
+ content.set(previousContent);
2636
+ content.set(writtenContent, offset);
2637
+ return content;
2638
+ }
2639
+ async function* createMemoryContentSource(content) {
2640
+ await Promise.resolve();
2641
+ yield new Uint8Array(content);
2642
+ }
2643
+ function normalizeOptionalByteCount2(value, field) {
2644
+ return value === void 0 ? void 0 : normalizeByteCount2(value, field);
2645
+ }
2646
+ function normalizeByteCount2(value, field) {
2647
+ if (!Number.isFinite(value) || value < 0) {
2648
+ throw createInvalidFixtureError("/", `Memory provider ${field} must be a non-negative number`);
2649
+ }
2650
+ return Math.floor(value);
2651
+ }
2652
+ function cloneVerification3(verification) {
2653
+ const clone = { verified: verification.verified };
2654
+ if (verification.method !== void 0) clone.method = verification.method;
2655
+ if (verification.checksum !== void 0) clone.checksum = verification.checksum;
2656
+ if (verification.expectedChecksum !== void 0) {
2657
+ clone.expectedChecksum = verification.expectedChecksum;
2658
+ }
2659
+ if (verification.actualChecksum !== void 0) clone.actualChecksum = verification.actualChecksum;
2660
+ if (verification.details !== void 0) clone.details = { ...verification.details };
2661
+ return clone;
2662
+ }
2663
+ function cloneRemoteEntry(entry) {
2664
+ const clone = {
2665
+ name: entry.name,
2666
+ path: entry.path,
2667
+ type: entry.type
2668
+ };
2669
+ copyOptionalEntryFields(clone, entry);
2670
+ return clone;
2671
+ }
2672
+ function cloneRemoteStat(entry) {
2673
+ return {
2674
+ ...cloneRemoteEntry(entry),
2675
+ exists: true
2676
+ };
2677
+ }
2678
+ function copyOptionalEntryFields(target, source) {
2679
+ if (source.size !== void 0) target.size = source.size;
2680
+ if (source.modifiedAt !== void 0) target.modifiedAt = cloneDate2(source.modifiedAt);
2681
+ if (source.createdAt !== void 0) target.createdAt = cloneDate2(source.createdAt);
2682
+ if (source.accessedAt !== void 0) target.accessedAt = cloneDate2(source.accessedAt);
2683
+ if (source.permissions !== void 0) target.permissions = clonePermissions(source.permissions);
2684
+ if (source.owner !== void 0) target.owner = source.owner;
2685
+ if (source.group !== void 0) target.group = source.group;
2686
+ if (source.symlinkTarget !== void 0) target.symlinkTarget = source.symlinkTarget;
2687
+ if (source.uniqueId !== void 0) target.uniqueId = source.uniqueId;
2688
+ if (source.raw !== void 0) target.raw = source.raw;
2689
+ }
2690
+ function cloneDate2(value) {
2691
+ return new Date(value.getTime());
2692
+ }
2693
+ function clonePermissions(permissions) {
2694
+ return { ...permissions };
2695
+ }
2696
+ function compareEntries2(left, right) {
2697
+ return left.path.localeCompare(right.path);
2698
+ }
2699
+ function createPathNotFoundError2(path2, message) {
2700
+ return new PathNotFoundError({
2701
+ details: { provider: MEMORY_PROVIDER_ID },
2702
+ message,
2703
+ path: path2,
2704
+ retryable: false
2705
+ });
2706
+ }
2707
+ function createInvalidFixtureError(path2, message) {
2708
+ return new ConfigurationError({
2709
+ details: { provider: MEMORY_PROVIDER_ID },
2710
+ message,
2711
+ path: path2,
2712
+ retryable: false
2713
+ });
2714
+ }
2715
+
2716
+ // src/profiles/resolveConnectionProfileSecrets.ts
2717
+ async function resolveConnectionProfileSecrets(profile, options = {}) {
2718
+ const { password, ssh, tls, username, ...rest } = profile;
2719
+ const resolved = { ...rest };
2720
+ if (username !== void 0) {
2721
+ resolved.username = await resolveSecret(username, options);
2722
+ }
2723
+ if (password !== void 0) {
2724
+ resolved.password = await resolveSecret(password, options);
2725
+ }
2726
+ if (tls !== void 0) {
2727
+ resolved.tls = await resolveTlsProfile(tls, options);
2728
+ }
2729
+ if (ssh !== void 0) {
2730
+ resolved.ssh = await resolveSshProfile(ssh, options);
2731
+ }
2732
+ return resolved;
2733
+ }
2734
+ async function resolveSshProfile(profile, options) {
2735
+ const { knownHosts, passphrase, privateKey, ...rest } = profile;
2736
+ const resolved = { ...rest };
2737
+ if (privateKey !== void 0) resolved.privateKey = await resolveSecret(privateKey, options);
2738
+ if (passphrase !== void 0) resolved.passphrase = await resolveSecret(passphrase, options);
2739
+ if (knownHosts !== void 0)
2740
+ resolved.knownHosts = await resolveKnownHostsSource(knownHosts, options);
2741
+ return resolved;
2742
+ }
2743
+ async function resolveKnownHostsSource(source, options) {
2744
+ if (Array.isArray(source)) {
2745
+ return Promise.all(source.map((item) => resolveSecret(item, options)));
2746
+ }
2747
+ return resolveSecret(source, options);
2748
+ }
2749
+ async function resolveTlsProfile(profile, options) {
2750
+ const { ca, cert, key, passphrase, pfx, ...rest } = profile;
2751
+ const resolved = { ...rest };
2752
+ if (ca !== void 0) resolved.ca = await resolveTlsSecretSource(ca, options);
2753
+ if (cert !== void 0) resolved.cert = await resolveSecret(cert, options);
2754
+ if (key !== void 0) resolved.key = await resolveSecret(key, options);
2755
+ if (passphrase !== void 0) resolved.passphrase = await resolveSecret(passphrase, options);
2756
+ if (pfx !== void 0) resolved.pfx = await resolveSecret(pfx, options);
2757
+ return resolved;
2758
+ }
2759
+ async function resolveTlsSecretSource(source, options) {
2760
+ if (Array.isArray(source)) {
2761
+ return Promise.all(source.map((item) => resolveSecret(item, options)));
2762
+ }
2763
+ return resolveSecret(source, options);
2764
+ }
2765
+
2766
+ // src/profiles/OAuthTokenSource.ts
2767
+ function createOAuthTokenSecretSource(refresh, options = {}) {
2768
+ if (typeof refresh !== "function") {
2769
+ throw new ConfigurationError({
2770
+ message: "createOAuthTokenSecretSource requires a refresh callback",
2771
+ retryable: false
2772
+ });
2773
+ }
2774
+ const skewMs = options.skewMs ?? 6e4;
2775
+ const now = options.now ?? (() => Date.now());
2776
+ if (skewMs < 0) {
2777
+ throw new ConfigurationError({
2778
+ message: "OAuthTokenSecretSourceOptions.skewMs must be non-negative",
2779
+ retryable: false
2780
+ });
2781
+ }
2782
+ let cache;
2783
+ let pending;
2784
+ const renew = async () => {
2785
+ const result = await refresh();
2786
+ if (typeof result.accessToken !== "string" || result.accessToken === "") {
2787
+ throw new ConfigurationError({
2788
+ message: "OAuth refresh callback returned an empty access token",
2789
+ retryable: false
2790
+ });
2791
+ }
2792
+ let expiresAtMs;
2793
+ if (result.expiresAt !== void 0) {
2794
+ const ts = result.expiresAt.getTime();
2795
+ if (Number.isFinite(ts)) expiresAtMs = ts;
2796
+ } else if (typeof result.expiresInSeconds === "number") {
2797
+ if (!Number.isFinite(result.expiresInSeconds) || result.expiresInSeconds <= 0) {
2798
+ throw new ConfigurationError({
2799
+ message: "OAuth refresh callback returned a non-positive expiresInSeconds",
2800
+ retryable: false
2801
+ });
2802
+ }
2803
+ expiresAtMs = now() + result.expiresInSeconds * 1e3;
2804
+ }
2805
+ const cached = { accessToken: result.accessToken, expiresAtMs };
2806
+ cache = cached;
2807
+ return cached;
2808
+ };
2809
+ return async () => {
2810
+ const current = cache;
2811
+ if (current !== void 0 && isFresh(current, skewMs, now)) {
2812
+ return current.accessToken;
2813
+ }
2814
+ if (pending === void 0) {
2815
+ pending = renew().finally(() => {
2816
+ pending = void 0;
2817
+ });
2818
+ }
2819
+ const refreshed = await pending;
2820
+ return refreshed.accessToken;
2821
+ };
2822
+ }
2823
+ function isFresh(token, skewMs, now) {
2824
+ if (token.expiresAtMs === void 0) return true;
2825
+ return token.expiresAtMs - skewMs > now();
2826
+ }
2827
+
2828
+ // src/profiles/importers/KnownHostsParser.ts
2829
+ import { Buffer as Buffer4 } from "buffer";
2830
+ import { createHmac } from "crypto";
2831
+ function parseKnownHosts(text) {
2832
+ const entries = [];
2833
+ const lines = text.split(/\r?\n/);
2834
+ for (const line of lines) {
2835
+ const trimmed = line.trim();
2836
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2837
+ const entry = parseKnownHostsLine(line);
2838
+ if (entry !== void 0) entries.push(entry);
2839
+ }
2840
+ return entries;
2841
+ }
2842
+ function parseKnownHostsLine(line) {
2843
+ const tokens = line.trim().split(/\s+/);
2844
+ if (tokens.length < 3) return void 0;
2845
+ let index = 0;
2846
+ let marker;
2847
+ const first2 = tokens[index];
2848
+ if (first2 === "@cert-authority" || first2 === "@revoked") {
2849
+ marker = first2 === "@cert-authority" ? "cert-authority" : "revoked";
2850
+ index += 1;
2851
+ }
2852
+ const hostField = tokens[index];
2853
+ const keyType = tokens[index + 1];
2854
+ const keyBase64 = tokens[index + 2];
2855
+ if (hostField === void 0 || keyType === void 0 || keyBase64 === void 0) return void 0;
2856
+ const commentTokens = tokens.slice(index + 3);
2857
+ const comment = commentTokens.length > 0 ? commentTokens.join(" ") : void 0;
2858
+ let hostPatterns = [];
2859
+ let hashedSalt;
2860
+ let hashedHash;
2861
+ if (hostField.startsWith("|1|")) {
2862
+ const parts = hostField.split("|");
2863
+ if (parts.length < 4) return void 0;
2864
+ hashedSalt = parts[2];
2865
+ hashedHash = parts[3];
2866
+ } else {
2867
+ hostPatterns = hostField.split(",").filter((token) => token !== "");
2868
+ }
2869
+ const entry = {
2870
+ hostPatterns,
2871
+ keyBase64,
2872
+ keyType,
2873
+ raw: line
2874
+ };
2875
+ if (marker !== void 0) entry.marker = marker;
2876
+ if (comment !== void 0) entry.comment = comment;
2877
+ if (hashedSalt !== void 0) entry.hashedSalt = hashedSalt;
2878
+ if (hashedHash !== void 0) entry.hashedHash = hashedHash;
2879
+ return entry;
2880
+ }
2881
+ var DEFAULT_SSH_PORT = 22;
2882
+ function matchKnownHostsEntry(entry, host, port = DEFAULT_SSH_PORT) {
2883
+ if (entry.hashedSalt !== void 0 && entry.hashedHash !== void 0) {
2884
+ return matchesHashedEntry(entry.hashedSalt, entry.hashedHash, host, port);
2885
+ }
2886
+ let matched = false;
2887
+ for (const pattern of entry.hostPatterns) {
2888
+ if (pattern.startsWith("!")) {
2889
+ const negated = pattern.slice(1);
2890
+ if (matchesPlainPattern(negated, host, port)) return false;
2891
+ continue;
2892
+ }
2893
+ if (matchesPlainPattern(pattern, host, port)) matched = true;
2894
+ }
2895
+ return matched;
2896
+ }
2897
+ function matchKnownHosts(entries, host, port = DEFAULT_SSH_PORT) {
2898
+ return entries.filter((entry) => matchKnownHostsEntry(entry, host, port));
2899
+ }
2900
+ function matchesPlainPattern(pattern, host, port) {
2901
+ const portMatch = pattern.match(/^\[(.+)\]:(\d+)$/);
2902
+ if (portMatch) {
2903
+ const [, hostPattern, portText] = portMatch;
2904
+ if (hostPattern === void 0 || portText === void 0) return false;
2905
+ const expectedPort = Number.parseInt(portText, 10);
2906
+ if (Number.isNaN(expectedPort) || expectedPort !== port) return false;
2907
+ return globMatch(hostPattern, host);
2908
+ }
2909
+ return port === DEFAULT_SSH_PORT && globMatch(pattern, host);
2910
+ }
2911
+ function globMatch(pattern, value) {
2912
+ const regex = new RegExp(
2913
+ `^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
2914
+ "i"
2915
+ );
2916
+ return regex.test(value);
2917
+ }
2918
+ function matchesHashedEntry(salt, hash, host, port) {
2919
+ const saltBuffer = Buffer4.from(salt, "base64");
2920
+ if (saltBuffer.length === 0) return false;
2921
+ const candidates = port === DEFAULT_SSH_PORT ? [host] : [`[${host}]:${String(port)}`, host];
2922
+ for (const candidate of candidates) {
2923
+ const expected = createHmac("sha1", saltBuffer).update(candidate).digest("base64");
2924
+ if (expected === hash) return true;
2925
+ }
2926
+ return false;
2927
+ }
2928
+
2929
+ // src/profiles/importers/OpenSshConfigImporter.ts
2930
+ function parseOpenSshConfig(text) {
2931
+ const entries = [];
2932
+ let current;
2933
+ let skipping = false;
2934
+ const lines = text.split(/\r?\n/);
2935
+ for (const rawLine of lines) {
2936
+ const line = rawLine.replace(/#.*$/, "").trim();
2937
+ if (line === "") continue;
2938
+ const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=?\s*(.*)$/);
2939
+ if (!match) continue;
2940
+ const [, keywordRaw, valueRaw] = match;
2941
+ if (keywordRaw === void 0 || valueRaw === void 0) continue;
2942
+ const keyword = keywordRaw.toLowerCase();
2943
+ const value = valueRaw.trim();
2944
+ if (keyword === "host") {
2945
+ if (current !== void 0) entries.push(current);
2946
+ current = { options: {}, patterns: tokenizeValues(value) };
2947
+ skipping = false;
2948
+ continue;
2949
+ }
2950
+ if (keyword === "match") {
2951
+ if (current !== void 0) entries.push(current);
2952
+ current = void 0;
2953
+ skipping = true;
2954
+ continue;
2955
+ }
2956
+ if (skipping || current === void 0) continue;
2957
+ const values = tokenizeValues(value);
2958
+ const existing = current.options[keyword];
2959
+ if (existing === void 0) {
2960
+ current.options[keyword] = [...values];
2961
+ } else {
2962
+ existing.push(...values);
2963
+ }
2964
+ }
2965
+ if (current !== void 0) entries.push(current);
2966
+ return entries;
2967
+ }
2968
+ function tokenizeValues(value) {
2969
+ if (value === "") return [];
2970
+ const tokens = [];
2971
+ const regex = /"([^"]*)"|(\S+)/g;
2972
+ let match;
2973
+ while ((match = regex.exec(value)) !== null) {
2974
+ tokens.push(match[1] ?? match[2] ?? "");
2975
+ }
2976
+ return tokens;
2977
+ }
2978
+ function resolveOpenSshHost(entries, alias) {
2979
+ const merged = {};
2980
+ const matched = [];
2981
+ for (const entry of entries) {
2982
+ if (!entryMatchesAlias(entry, alias)) continue;
2983
+ matched.push(entry);
2984
+ for (const [key, values] of Object.entries(entry.options)) {
2985
+ if (merged[key] === void 0) merged[key] = [...values];
2986
+ }
2987
+ }
2988
+ return { alias, matched, options: merged };
2989
+ }
2990
+ function entryMatchesAlias(entry, alias) {
2991
+ let matched = false;
2992
+ for (const pattern of entry.patterns) {
2993
+ if (pattern.startsWith("!")) {
2994
+ if (globMatch2(pattern.slice(1), alias)) return false;
2995
+ continue;
2996
+ }
2997
+ if (globMatch2(pattern, alias)) matched = true;
2998
+ }
2999
+ return matched;
3000
+ }
3001
+ function globMatch2(pattern, value) {
3002
+ const regex = new RegExp(
3003
+ `^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
3004
+ "i"
3005
+ );
3006
+ return regex.test(value);
3007
+ }
3008
+ function importOpenSshConfig(options) {
3009
+ const { alias } = options;
3010
+ const entries = options.entries ?? (options.text !== void 0 ? parseOpenSshConfig(options.text) : void 0);
3011
+ if (entries === void 0) {
3012
+ throw new ConfigurationError({
3013
+ code: "openssh_config_input_missing",
3014
+ message: "importOpenSshConfig requires either text or pre-parsed entries.",
3015
+ retryable: false
3016
+ });
3017
+ }
3018
+ const resolved = resolveOpenSshHost(entries, alias);
3019
+ const optionsMap = resolved.options;
3020
+ const host = first(optionsMap, "hostname") ?? alias;
3021
+ const portText = first(optionsMap, "port");
3022
+ const port = portText !== void 0 ? safeInt(portText) : void 0;
3023
+ const user = first(optionsMap, "user");
3024
+ const identityFiles = optionsMap["identityfile"] ?? [];
3025
+ const knownHostsFiles = optionsMap["userknownhostsfile"] ?? [];
3026
+ const connectTimeoutText = first(optionsMap, "connecttimeout");
3027
+ const proxyJump = first(optionsMap, "proxyjump");
3028
+ const kex = optionsMap["kexalgorithms"] ?? [];
3029
+ const ciphers = optionsMap["ciphers"] ?? [];
3030
+ const macs = optionsMap["macs"] ?? [];
3031
+ const serverHostKey = optionsMap["hostkeyalgorithms"] ?? [];
3032
+ const profile = { host, provider: "sftp" };
3033
+ if (port !== void 0) profile.port = port;
3034
+ if (user !== void 0) profile.username = { value: user };
3035
+ if (connectTimeoutText !== void 0) {
3036
+ const seconds = safeInt(connectTimeoutText);
3037
+ if (seconds !== void 0) profile.timeoutMs = seconds * 1e3;
3038
+ }
3039
+ const ssh = {};
3040
+ if (identityFiles.length > 0) {
3041
+ const firstKey = identityFiles[0];
3042
+ if (firstKey !== void 0) ssh.privateKey = { path: expandHome(firstKey) };
3043
+ }
3044
+ if (knownHostsFiles.length > 0) {
3045
+ ssh.knownHosts = knownHostsFiles.map((path2) => ({ path: expandHome(path2) }));
3046
+ }
3047
+ const algorithms = {};
3048
+ if (kex.length > 0) algorithms["kex"] = expandAlgorithms(kex);
3049
+ if (ciphers.length > 0) algorithms["cipher"] = expandAlgorithms(ciphers);
3050
+ if (macs.length > 0) algorithms["hmac"] = expandAlgorithms(macs);
3051
+ if (serverHostKey.length > 0) algorithms["serverHostKey"] = expandAlgorithms(serverHostKey);
3052
+ if (Object.keys(algorithms).length > 0) {
3053
+ ssh.algorithms = algorithms;
3054
+ }
3055
+ if (Object.keys(ssh).length > 0) profile.ssh = ssh;
3056
+ const result = {
3057
+ identityFiles: identityFiles.map(expandHome),
3058
+ profile,
3059
+ resolved
3060
+ };
3061
+ if (proxyJump !== void 0) result.proxyJump = proxyJump;
3062
+ return result;
3063
+ }
3064
+ function first(options, key) {
3065
+ const values = options[key];
3066
+ return values !== void 0 && values.length > 0 ? values[0] : void 0;
3067
+ }
3068
+ function safeInt(text) {
3069
+ const value = Number.parseInt(text, 10);
3070
+ return Number.isFinite(value) ? value : void 0;
3071
+ }
3072
+ function expandHome(path2) {
3073
+ if (!path2.startsWith("~")) return path2;
3074
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"];
3075
+ if (home === void 0) return path2;
3076
+ if (path2 === "~") return home;
3077
+ if (path2.startsWith("~/") || path2.startsWith("~\\")) return `${home}${path2.slice(1)}`;
3078
+ return path2;
3079
+ }
3080
+ function expandAlgorithms(values) {
3081
+ const out = [];
3082
+ for (const value of values) {
3083
+ for (const part of value.split(",")) {
3084
+ const trimmed = part.trim();
3085
+ if (trimmed !== "") out.push(trimmed);
3086
+ }
3087
+ }
3088
+ return out;
3089
+ }
3090
+
3091
+ // src/profiles/importers/FileZillaImporter.ts
3092
+ import { Buffer as Buffer5 } from "buffer";
3093
+ function importFileZillaSites(xml) {
3094
+ const events = tokenizeXml(xml);
3095
+ if (events.length === 0) {
3096
+ throw new ConfigurationError({
3097
+ code: "filezilla_xml_empty",
3098
+ message: "FileZilla sitemanager XML is empty.",
3099
+ retryable: false
3100
+ });
3101
+ }
3102
+ const sites = [];
3103
+ const skipped = [];
3104
+ const folderStack = [];
3105
+ const folderNamePending = [];
3106
+ let inServer = false;
3107
+ let serverFields = {};
3108
+ let serverPasswordEncoding;
3109
+ let activeTag;
3110
+ let captureFolderName = false;
3111
+ for (const event of events) {
3112
+ if (event.kind === "open") {
3113
+ if (event.name === "Folder") {
3114
+ folderStack.push("");
3115
+ folderNamePending.push(true);
3116
+ continue;
3117
+ }
3118
+ if (event.name === "Server") {
3119
+ inServer = true;
3120
+ serverFields = {};
3121
+ serverPasswordEncoding = void 0;
3122
+ continue;
3123
+ }
3124
+ activeTag = event.name;
3125
+ if (event.name === "Pass" && inServer) {
3126
+ serverPasswordEncoding = event.attributes["encoding"];
3127
+ }
3128
+ if (event.name === "Name" && !inServer && folderNamePending.length > 0) {
3129
+ captureFolderName = true;
3130
+ }
3131
+ continue;
3132
+ }
3133
+ if (event.kind === "text") {
3134
+ if (captureFolderName) {
3135
+ const top = folderStack.length - 1;
3136
+ if (top >= 0) folderStack[top] = event.text.trim();
3137
+ captureFolderName = false;
3138
+ continue;
3139
+ }
3140
+ if (inServer && activeTag !== void 0) {
3141
+ serverFields[activeTag] = (serverFields[activeTag] ?? "") + event.text;
3142
+ }
3143
+ continue;
3144
+ }
3145
+ if (event.kind === "close") {
3146
+ if (event.name === "Folder") {
3147
+ folderStack.pop();
3148
+ folderNamePending.pop();
3149
+ continue;
3150
+ }
3151
+ if (event.name === "Server") {
3152
+ const folder = folderStack.filter((segment) => segment !== "");
3153
+ const result = buildSiteFromFields(serverFields, serverPasswordEncoding);
3154
+ if (result.kind === "site") {
3155
+ sites.push({ ...result.site, folder });
3156
+ } else {
3157
+ skipped.push({
3158
+ folder,
3159
+ name: result.name,
3160
+ ...result.protocol !== void 0 ? { protocol: result.protocol } : {}
3161
+ });
3162
+ }
3163
+ inServer = false;
3164
+ serverFields = {};
3165
+ serverPasswordEncoding = void 0;
3166
+ activeTag = void 0;
3167
+ continue;
3168
+ }
3169
+ if (activeTag === event.name) activeTag = void 0;
3170
+ }
3171
+ }
3172
+ return { sites, skipped };
3173
+ }
3174
+ function buildSiteFromFields(fields, passwordEncoding) {
3175
+ const name = (fields["Name"] ?? fields["Host"] ?? "Untitled").trim();
3176
+ const host = (fields["Host"] ?? "").trim();
3177
+ if (host === "") return { kind: "skipped", name };
3178
+ const protocolText = fields["Protocol"];
3179
+ const protocol = protocolText !== void 0 ? Number.parseInt(protocolText.trim(), 10) : 0;
3180
+ const mapped = mapFileZillaProtocol(protocol);
3181
+ if (mapped === void 0) {
3182
+ return Number.isFinite(protocol) ? { kind: "skipped", name, protocol } : { kind: "skipped", name };
3183
+ }
3184
+ const profile = { host, provider: mapped.provider };
3185
+ if (mapped.secure !== void 0) profile.secure = mapped.secure;
3186
+ const portText = fields["Port"];
3187
+ if (portText !== void 0) {
3188
+ const port = Number.parseInt(portText.trim(), 10);
3189
+ if (Number.isFinite(port)) profile.port = port;
3190
+ }
3191
+ const user = fields["User"]?.trim();
3192
+ if (user !== void 0 && user !== "") profile.username = { value: user };
3193
+ let password;
3194
+ const rawPass = fields["Pass"];
3195
+ if (rawPass !== void 0 && rawPass !== "") {
3196
+ if (passwordEncoding === "base64") {
3197
+ password = Buffer5.from(rawPass, "base64").toString("utf8");
3198
+ } else {
3199
+ password = rawPass;
3200
+ }
3201
+ if (password !== void 0 && password !== "") profile.password = { value: password };
3202
+ }
3203
+ const site = { name, profile };
3204
+ if (password !== void 0) site.password = password;
3205
+ const logonText = fields["Logontype"];
3206
+ if (logonText !== void 0) {
3207
+ const logonType = Number.parseInt(logonText.trim(), 10);
3208
+ if (Number.isFinite(logonType)) site.logonType = logonType;
3209
+ }
3210
+ return { kind: "site", site };
3211
+ }
3212
+ function mapFileZillaProtocol(code) {
3213
+ switch (code) {
3214
+ case 0:
3215
+ return { provider: "ftp" };
3216
+ case 1:
3217
+ return { provider: "sftp" };
3218
+ case 4:
3219
+ return { provider: "ftps", secure: true };
3220
+ case 5:
3221
+ return { provider: "ftps", secure: true };
3222
+ case 6:
3223
+ return { provider: "ftp", secure: false };
3224
+ default:
3225
+ return void 0;
3226
+ }
3227
+ }
3228
+ function tokenizeXml(xml) {
3229
+ const events = [];
3230
+ let index = 0;
3231
+ const length = xml.length;
3232
+ while (index < length) {
3233
+ const lt = xml.indexOf("<", index);
3234
+ if (lt === -1) {
3235
+ const text = xml.slice(index);
3236
+ if (text.trim() !== "") events.push({ kind: "text", text: decodeEntities(text) });
3237
+ break;
3238
+ }
3239
+ if (lt > index) {
3240
+ const text = xml.slice(index, lt);
3241
+ if (text.trim() !== "") events.push({ kind: "text", text: decodeEntities(text) });
3242
+ }
3243
+ if (xml.startsWith("<!--", lt)) {
3244
+ const end = xml.indexOf("-->", lt + 4);
3245
+ index = end === -1 ? length : end + 3;
3246
+ continue;
3247
+ }
3248
+ if (xml.startsWith("<![CDATA[", lt)) {
3249
+ const end = xml.indexOf("]]>", lt + 9);
3250
+ const cdataEnd = end === -1 ? length : end;
3251
+ events.push({ kind: "text", text: xml.slice(lt + 9, cdataEnd) });
3252
+ index = end === -1 ? length : end + 3;
3253
+ continue;
3254
+ }
3255
+ if (xml[lt + 1] === "?" || xml[lt + 1] === "!") {
3256
+ const gt2 = xml.indexOf(">", lt + 1);
3257
+ index = gt2 === -1 ? length : gt2 + 1;
3258
+ continue;
3259
+ }
3260
+ const gt = xml.indexOf(">", lt + 1);
3261
+ if (gt === -1) break;
3262
+ const tagBody = xml.slice(lt + 1, gt);
3263
+ if (tagBody.startsWith("/")) {
3264
+ events.push({ kind: "close", name: tagBody.slice(1).trim() });
3265
+ } else {
3266
+ const selfClosing = tagBody.endsWith("/");
3267
+ const body = selfClosing ? tagBody.slice(0, -1) : tagBody;
3268
+ const { name, attributes } = parseTagBody(body.trim());
3269
+ events.push({ attributes, kind: "open", name, selfClosing });
3270
+ if (selfClosing) events.push({ kind: "close", name });
3271
+ }
3272
+ index = gt + 1;
3273
+ }
3274
+ return events;
3275
+ }
3276
+ function parseTagBody(body) {
3277
+ const match = body.match(/^([A-Za-z_:][\w:.-]*)\s*(.*)$/);
3278
+ if (!match) return { attributes: {}, name: body };
3279
+ const name = match[1] ?? "";
3280
+ const rest = match[2] ?? "";
3281
+ const attributes = {};
3282
+ const attrRegex = /([A-Za-z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
3283
+ let attrMatch;
3284
+ while ((attrMatch = attrRegex.exec(rest)) !== null) {
3285
+ const key = attrMatch[1];
3286
+ const value = attrMatch[3] ?? attrMatch[4] ?? "";
3287
+ if (key !== void 0) attributes[key] = decodeEntities(value);
3288
+ }
3289
+ return { attributes, name };
3290
+ }
3291
+ function decodeEntities(text) {
3292
+ return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
3293
+ }
3294
+
3295
+ // src/profiles/importers/WinScpImporter.ts
3296
+ function importWinScpSessions(ini) {
3297
+ const sections = parseIni(ini);
3298
+ const sessionSections = sections.filter((section) => section.name.startsWith("Sessions\\"));
3299
+ if (sessionSections.length === 0) {
3300
+ throw new ConfigurationError({
3301
+ code: "winscp_ini_no_sessions",
3302
+ message: "WinSCP INI does not contain any [Sessions\\...] sections.",
3303
+ retryable: false
3304
+ });
3305
+ }
3306
+ const sessions = [];
3307
+ const skipped = [];
3308
+ for (const section of sessionSections) {
3309
+ const decodedPath = decodeSessionPath(section.name.slice("Sessions\\".length));
3310
+ const segments = decodedPath.split("/").filter((segment) => segment !== "");
3311
+ const name = segments[segments.length - 1] ?? decodedPath;
3312
+ const folder = segments.slice(0, -1);
3313
+ const built = buildSessionProfile(name, section.values);
3314
+ if (built.kind === "session") {
3315
+ sessions.push({ ...built.session, folder });
3316
+ } else {
3317
+ skipped.push({
3318
+ folder,
3319
+ name,
3320
+ ...built.fsProtocol !== void 0 ? { fsProtocol: built.fsProtocol } : {}
3321
+ });
3322
+ }
3323
+ }
3324
+ return { sessions, skipped };
3325
+ }
3326
+ function buildSessionProfile(name, values) {
3327
+ const host = values["HostName"]?.trim();
3328
+ if (host === void 0 || host === "") return { kind: "skipped" };
3329
+ const fsProtocolText = values["FSProtocol"];
3330
+ const fsProtocol = fsProtocolText !== void 0 ? Number.parseInt(fsProtocolText, 10) : 1;
3331
+ const ftpsText = values["Ftps"];
3332
+ const ftps = ftpsText !== void 0 ? Number.parseInt(ftpsText, 10) : 0;
3333
+ const mapped = mapWinScpProtocol(fsProtocol, ftps);
3334
+ if (mapped === void 0) {
3335
+ return Number.isFinite(fsProtocol) ? { fsProtocol, kind: "skipped" } : { kind: "skipped" };
3336
+ }
3337
+ const profile = { host, provider: mapped.provider };
3338
+ if (mapped.secure !== void 0) profile.secure = mapped.secure;
3339
+ const portText = values["PortNumber"];
3340
+ if (portText !== void 0) {
3341
+ const port = Number.parseInt(portText, 10);
3342
+ if (Number.isFinite(port)) profile.port = port;
3343
+ }
3344
+ const user = values["UserName"]?.trim();
3345
+ if (user !== void 0 && user !== "") profile.username = { value: user };
3346
+ if (mapped.provider === "sftp") {
3347
+ const ssh = {};
3348
+ const keyPath = values["PublicKeyFile"]?.trim();
3349
+ if (keyPath !== void 0 && keyPath !== "") ssh.privateKey = { path: keyPath };
3350
+ if (Object.keys(ssh).length > 0) profile.ssh = ssh;
3351
+ }
3352
+ const session = { name, profile };
3353
+ if (Number.isFinite(fsProtocol)) session.fsProtocol = fsProtocol;
3354
+ if (Number.isFinite(ftps) && ftps !== 0) session.ftps = ftps;
3355
+ return { kind: "session", session };
3356
+ }
3357
+ function mapWinScpProtocol(fsProtocol, ftps) {
3358
+ switch (fsProtocol) {
3359
+ case 0:
3360
+ case 1:
3361
+ case 2:
3362
+ return { provider: "sftp" };
3363
+ case 5:
3364
+ return ftps === 0 ? { provider: "ftp" } : { provider: "ftps", secure: ftps === 1 };
3365
+ default:
3366
+ return void 0;
3367
+ }
3368
+ }
3369
+ function parseIni(text) {
3370
+ const sections = [];
3371
+ let current;
3372
+ const lines = text.split(/\r?\n/);
3373
+ for (const rawLine of lines) {
3374
+ const line = rawLine.replace(/^\s*[#;].*$/, "").trim();
3375
+ if (line === "") continue;
3376
+ const sectionMatch = line.match(/^\[(.+)\]$/);
3377
+ if (sectionMatch && sectionMatch[1] !== void 0) {
3378
+ current = { name: sectionMatch[1], values: {} };
3379
+ sections.push(current);
3380
+ continue;
3381
+ }
3382
+ if (current === void 0) continue;
3383
+ const eq = line.indexOf("=");
3384
+ if (eq === -1) continue;
3385
+ const key = line.slice(0, eq).trim();
3386
+ const value = line.slice(eq + 1).trim();
3387
+ if (key !== "") current.values[key] = value;
3388
+ }
3389
+ return sections;
3390
+ }
3391
+ function decodeSessionPath(name) {
3392
+ try {
3393
+ return decodeURIComponent(name);
3394
+ } catch {
3395
+ return name;
3396
+ }
3397
+ }
3398
+
3399
+ // src/errors/errorFactory.ts
3400
+ function errorFromFtpReply(input) {
3401
+ const details = {
3402
+ ftpCode: input.ftpCode,
3403
+ message: input.message,
3404
+ protocol: input.protocol ?? "ftp",
3405
+ retryable: false
3406
+ };
3407
+ if (input.command !== void 0) details.command = input.command;
3408
+ if (input.path !== void 0) details.path = input.path;
3409
+ if (input.cause !== void 0) details.cause = input.cause;
3410
+ if (input.ftpCode === 530) {
3411
+ return new AuthenticationError(details);
3412
+ }
3413
+ if (input.ftpCode === 421) {
3414
+ return new ConnectionError({
3415
+ ...details,
3416
+ retryable: true
3417
+ });
3418
+ }
3419
+ if (input.ftpCode === 550) {
3420
+ return mapFtp550(details);
3421
+ }
3422
+ if ([450, 451, 452].includes(input.ftpCode)) {
3423
+ return new TransferError({
3424
+ ...details,
3425
+ retryable: true
3426
+ });
3427
+ }
3428
+ if (input.ftpCode >= 400 && input.ftpCode < 500) {
3429
+ return new ConnectionError({
3430
+ ...details,
3431
+ retryable: true
3432
+ });
3433
+ }
3434
+ return new ProtocolError(details);
3435
+ }
3436
+ function mapFtp550(details) {
3437
+ const lowerMessage = details.message.toLowerCase();
3438
+ if (lowerMessage.includes("already") || lowerMessage.includes("exists")) {
3439
+ return new PathAlreadyExistsError(details);
3440
+ }
3441
+ if (lowerMessage.includes("not found") || lowerMessage.includes("no such") || lowerMessage.includes("unavailable")) {
3442
+ return new PathNotFoundError(details);
3443
+ }
3444
+ return new PermissionDeniedError(details);
3445
+ }
3446
+
3447
+ // src/transfers/TransferPlan.ts
3448
+ function createTransferPlan(input) {
3449
+ const plan = {
3450
+ createdAt: input.now?.() ?? /* @__PURE__ */ new Date(),
3451
+ dryRun: input.dryRun ?? true,
3452
+ id: input.id,
3453
+ steps: input.steps.map(clonePlanStep),
3454
+ warnings: [...input.warnings ?? []]
3455
+ };
3456
+ if (input.metadata !== void 0) {
3457
+ plan.metadata = { ...input.metadata };
3458
+ }
3459
+ return plan;
3460
+ }
3461
+ function summarizeTransferPlan(plan) {
3462
+ const actions = {};
3463
+ let destructiveSteps = 0;
3464
+ let executableSteps = 0;
3465
+ let skippedSteps = 0;
3466
+ let totalExpectedBytes = 0;
3467
+ for (const step of plan.steps) {
3468
+ actions[step.action] = (actions[step.action] ?? 0) + 1;
3469
+ destructiveSteps += step.destructive === true ? 1 : 0;
3470
+ skippedSteps += step.action === "skip" ? 1 : 0;
3471
+ executableSteps += step.action === "skip" ? 0 : 1;
3472
+ totalExpectedBytes += step.expectedBytes ?? 0;
3473
+ }
3474
+ return {
3475
+ actions,
3476
+ destructiveSteps,
3477
+ executableSteps,
3478
+ skippedSteps,
3479
+ totalExpectedBytes,
3480
+ totalSteps: plan.steps.length
3481
+ };
3482
+ }
3483
+ function createTransferJobsFromPlan(plan) {
3484
+ return plan.steps.flatMap((step) => {
3485
+ if (step.action === "skip") {
3486
+ return [];
3487
+ }
3488
+ const job = {
3489
+ id: `${plan.id}:${step.id}`,
3490
+ operation: step.action
3491
+ };
3492
+ if (step.source !== void 0) job.source = cloneEndpoint2(step.source);
3493
+ if (step.destination !== void 0) job.destination = cloneEndpoint2(step.destination);
3494
+ if (step.expectedBytes !== void 0) job.totalBytes = step.expectedBytes;
3495
+ if (step.metadata !== void 0) job.metadata = { ...step.metadata };
3496
+ return [job];
3497
+ });
3498
+ }
3499
+ function clonePlanStep(step) {
3500
+ const clone = {
3501
+ action: step.action,
3502
+ id: step.id
3503
+ };
3504
+ if (step.source !== void 0) clone.source = cloneEndpoint2(step.source);
3505
+ if (step.destination !== void 0) clone.destination = cloneEndpoint2(step.destination);
3506
+ if (step.expectedBytes !== void 0) clone.expectedBytes = step.expectedBytes;
3507
+ if (step.destructive !== void 0) clone.destructive = step.destructive;
3508
+ if (step.reason !== void 0) clone.reason = step.reason;
3509
+ if (step.metadata !== void 0) clone.metadata = { ...step.metadata };
3510
+ return clone;
3511
+ }
3512
+ function cloneEndpoint2(endpoint) {
3513
+ const clone = { path: endpoint.path };
3514
+ if (endpoint.provider !== void 0) {
3515
+ clone.provider = endpoint.provider;
3516
+ }
3517
+ return clone;
3518
+ }
3519
+
3520
+ // src/transfers/TransferQueue.ts
3521
+ var TransferQueue = class {
3522
+ engine;
3523
+ items = [];
3524
+ defaultExecutor;
3525
+ resolveExecutor;
3526
+ retry;
3527
+ timeout;
3528
+ bandwidthLimit;
3529
+ onProgress;
3530
+ onReceipt;
3531
+ onError;
3532
+ concurrency;
3533
+ paused = false;
3534
+ /**
3535
+ * Creates a transfer queue.
3536
+ *
3537
+ * @param options - Queue engine, concurrency, executor, and observer options.
3538
+ */
3539
+ constructor(options = {}) {
3540
+ this.engine = options.engine ?? new TransferEngine();
3541
+ this.concurrency = normalizeConcurrency(options.concurrency);
3542
+ this.defaultExecutor = options.executor;
3543
+ this.resolveExecutor = options.resolveExecutor;
3544
+ this.retry = options.retry;
3545
+ this.timeout = options.timeout;
3546
+ this.bandwidthLimit = options.bandwidthLimit;
3547
+ this.onProgress = options.onProgress;
3548
+ this.onReceipt = options.onReceipt;
3549
+ this.onError = options.onError;
3550
+ }
3551
+ /** Adds a transfer job to the queue. */
3552
+ add(job, executor) {
3553
+ if (this.items.some((item2) => item2.id === job.id)) {
3554
+ throw new ConfigurationError({
3555
+ details: { jobId: job.id },
3556
+ message: `Transfer queue already contains job: ${job.id}`,
3557
+ retryable: false
3558
+ });
3559
+ }
3560
+ const item = {
3561
+ controller: new AbortController(),
3562
+ id: job.id,
3563
+ job: cloneTransferJob(job),
3564
+ status: "queued"
3565
+ };
3566
+ if (executor !== void 0) {
3567
+ item.executor = executor;
3568
+ }
3569
+ this.items.push(item);
3570
+ return toPublicItem(item);
3571
+ }
3572
+ /** Pauses dispatch of new queued jobs. Running jobs are allowed to finish. */
3573
+ pause() {
3574
+ this.paused = true;
3575
+ }
3576
+ /** Resumes dispatch of queued jobs on the next `run()` call. */
3577
+ resume() {
3578
+ this.paused = false;
3579
+ }
3580
+ /** Updates queue concurrency for subsequent drains. */
3581
+ setConcurrency(concurrency) {
3582
+ this.concurrency = normalizeConcurrency(concurrency);
3583
+ }
3584
+ /** Cancels a queued or running job. */
3585
+ cancel(jobId) {
3586
+ const item = this.items.find((candidate) => candidate.id === jobId);
3587
+ if (item === void 0 || item.status === "completed" || item.status === "failed" || item.status === "canceled") {
3588
+ return false;
3589
+ }
3590
+ item.controller.abort();
3591
+ if (item.status === "queued") {
3592
+ item.status = "canceled";
3593
+ }
3594
+ return true;
3595
+ }
3596
+ /** Returns a queued item snapshot by id. */
3597
+ get(jobId) {
3598
+ const item = this.items.find((candidate) => candidate.id === jobId);
3599
+ return item === void 0 ? void 0 : toPublicItem(item);
3600
+ }
3601
+ /** Lists queue item snapshots in insertion order. */
3602
+ list() {
3603
+ return this.items.map(toPublicItem);
3604
+ }
3605
+ /** Drains currently queued jobs until complete, failed, canceled, or paused. */
3606
+ async run(options = {}) {
3607
+ const workerCount = Math.max(1, Math.min(this.concurrency, this.countDispatchableItems()));
3608
+ const workers = Array.from({ length: workerCount }, () => this.runWorker(options));
3609
+ await Promise.all(workers);
3610
+ return this.summarize();
3611
+ }
3612
+ /** Returns a queue summary without executing more work. */
3613
+ summarize() {
3614
+ const publicItems = this.items.map(toPublicItem);
3615
+ return {
3616
+ canceled: publicItems.filter((item) => item.status === "canceled").length,
3617
+ completed: publicItems.filter((item) => item.status === "completed").length,
3618
+ failed: publicItems.filter((item) => item.status === "failed").length,
3619
+ failures: publicItems.filter((item) => item.status === "failed"),
3620
+ queued: publicItems.filter((item) => item.status === "queued").length,
3621
+ receipts: publicItems.filter(
3622
+ (item) => item.receipt !== void 0
3623
+ ).map((item) => item.receipt),
3624
+ running: publicItems.filter((item) => item.status === "running").length,
3625
+ total: publicItems.length
3626
+ };
3627
+ }
3628
+ async runWorker(options) {
3629
+ for (; ; ) {
3630
+ const item = this.nextQueuedItem();
3631
+ if (item === void 0) {
3632
+ return;
3633
+ }
3634
+ await this.runItem(item, options);
3635
+ }
3636
+ }
3637
+ nextQueuedItem() {
3638
+ if (this.paused) {
3639
+ return void 0;
3640
+ }
3641
+ const item = this.items.find((candidate) => candidate.status === "queued");
3642
+ if (item !== void 0) {
3643
+ item.status = item.controller.signal.aborted ? "canceled" : "running";
3644
+ }
3645
+ return item?.status === "running" ? item : void 0;
3646
+ }
3647
+ async runItem(item, options) {
3648
+ const abortListener = createAbortForwarder(options.signal, item.controller);
3649
+ try {
3650
+ const executeOptions = {
3651
+ signal: item.controller.signal
3652
+ };
3653
+ const onProgress = options.onProgress ?? this.onProgress;
3654
+ const retry = options.retry ?? this.retry;
3655
+ const timeout = options.timeout ?? this.timeout;
3656
+ const bandwidthLimit = options.bandwidthLimit ?? this.bandwidthLimit;
3657
+ if (onProgress !== void 0) {
3658
+ executeOptions.onProgress = onProgress;
3659
+ }
3660
+ if (retry !== void 0) {
3661
+ executeOptions.retry = retry;
3662
+ }
3663
+ if (timeout !== void 0) {
3664
+ executeOptions.timeout = timeout;
3665
+ }
3666
+ if (bandwidthLimit !== void 0) {
3667
+ executeOptions.bandwidthLimit = bandwidthLimit;
3668
+ }
3669
+ const receipt = await this.engine.execute(
3670
+ item.job,
3671
+ this.requireExecutor(item),
3672
+ executeOptions
3673
+ );
3674
+ item.receipt = receipt;
3675
+ item.status = "completed";
3676
+ this.onReceipt?.(receipt);
3677
+ } catch (error) {
3678
+ item.error = error;
3679
+ item.status = item.controller.signal.aborted ? "canceled" : "failed";
3680
+ if (item.status === "failed") {
3681
+ this.onError?.(toPublicItem(item), error);
3682
+ }
3683
+ } finally {
3684
+ abortListener.dispose();
3685
+ }
3686
+ }
3687
+ requireExecutor(item) {
3688
+ const executor = item.executor ?? this.defaultExecutor ?? this.resolveExecutor?.(item.job);
3689
+ if (executor === void 0) {
3690
+ throw new ConfigurationError({
3691
+ details: { jobId: item.job.id },
3692
+ message: `Transfer queue job has no executor: ${item.job.id}`,
3693
+ retryable: false
3694
+ });
3695
+ }
3696
+ return executor;
3697
+ }
3698
+ countDispatchableItems() {
3699
+ return this.items.filter((item) => item.status === "queued" && !item.controller.signal.aborted).length;
3700
+ }
3701
+ };
3702
+ function normalizeConcurrency(value) {
3703
+ if (value === void 0 || !Number.isFinite(value)) {
3704
+ return 1;
3705
+ }
3706
+ return Math.max(1, Math.floor(value));
3707
+ }
3708
+ function createAbortForwarder(source, target) {
3709
+ if (source === void 0) {
3710
+ return { dispose: () => void 0 };
3711
+ }
3712
+ const abort = () => target.abort();
3713
+ if (source.aborted) {
3714
+ abort();
3715
+ return { dispose: () => void 0 };
3716
+ }
3717
+ source.addEventListener("abort", abort, { once: true });
3718
+ return {
3719
+ dispose: () => source.removeEventListener("abort", abort)
3720
+ };
3721
+ }
3722
+ function toPublicItem(item) {
3723
+ const snapshot = {
3724
+ id: item.id,
3725
+ job: cloneTransferJob(item.job),
3726
+ status: item.status
3727
+ };
3728
+ if (item.receipt !== void 0) snapshot.receipt = item.receipt;
3729
+ if (item.error !== void 0) snapshot.error = item.error;
3730
+ return snapshot;
3731
+ }
3732
+ function cloneTransferJob(job) {
3733
+ const clone = {
3734
+ id: job.id,
3735
+ operation: job.operation
3736
+ };
3737
+ if (job.source !== void 0) clone.source = { ...job.source };
3738
+ if (job.destination !== void 0) clone.destination = { ...job.destination };
3739
+ if (job.totalBytes !== void 0) clone.totalBytes = job.totalBytes;
3740
+ if (job.resumed !== void 0) clone.resumed = job.resumed;
3741
+ if (job.metadata !== void 0) clone.metadata = { ...job.metadata };
3742
+ return clone;
3743
+ }
3744
+
3745
+ // src/sync/createRemoteBrowser.ts
3746
+ function parentRemotePath(input) {
3747
+ const normalized = normalizeRemotePath(input);
3748
+ if (normalized === "/") return "/";
3749
+ const parts = normalized.split("/").filter(Boolean);
3750
+ parts.pop();
3751
+ if (parts.length === 0) return "/";
3752
+ return `/${parts.join("/")}`;
3753
+ }
3754
+ function buildRemoteBreadcrumbs(input) {
3755
+ const normalized = normalizeRemotePath(input);
3756
+ const crumbs = [{ name: "/", path: "/" }];
3757
+ if (normalized === "/") return crumbs;
3758
+ const parts = normalized.split("/").filter(Boolean);
3759
+ let cursor = "";
3760
+ for (const part of parts) {
3761
+ cursor += `/${part}`;
3762
+ crumbs.push({ name: part, path: cursor });
3763
+ }
3764
+ return crumbs;
3765
+ }
3766
+ function sortRemoteEntries(entries, key = "name", order = "asc") {
3767
+ const direction = order === "asc" ? 1 : -1;
3768
+ return [...entries].sort((left, right) => {
3769
+ if (key !== "type") {
3770
+ const leftIsDir = left.type === "directory";
3771
+ const rightIsDir = right.type === "directory";
3772
+ if (leftIsDir !== rightIsDir) return leftIsDir ? -1 : 1;
3773
+ }
3774
+ const compared = compareEntriesByKey(left, right, key);
3775
+ if (compared !== 0) return compared * direction;
3776
+ return compareNames(left, right);
3777
+ });
3778
+ }
3779
+ function filterRemoteEntries(entries, options = {}) {
3780
+ const showHidden = options.showHidden ?? true;
3781
+ const filter = options.filter;
3782
+ return entries.filter((entry) => {
3783
+ if (!showHidden && entry.name.startsWith(".")) return false;
3784
+ if (filter !== void 0 && !filter(entry)) return false;
3785
+ return true;
3786
+ });
3787
+ }
3788
+ function createRemoteBrowser(options) {
3789
+ const { fs } = options;
3790
+ let currentPath = normalizeRemotePath(options.initialPath ?? "/");
3791
+ let cachedEntries = [];
3792
+ let sortKey = options.sortKey ?? "name";
3793
+ let sortOrder = options.sortOrder ?? "asc";
3794
+ let showHidden = options.showHidden ?? true;
3795
+ const filter = options.filter;
3796
+ async function loadCurrent() {
3797
+ const raw = await fs.list(currentPath);
3798
+ const projected = projectEntries(raw);
3799
+ cachedEntries = projected;
3800
+ return snapshot();
3801
+ }
3802
+ function projectEntries(raw) {
3803
+ const filterOptions = { showHidden };
3804
+ if (filter !== void 0) filterOptions.filter = filter;
3805
+ const filtered = filterRemoteEntries(raw, filterOptions);
3806
+ return sortRemoteEntries(filtered, sortKey, sortOrder);
3807
+ }
3808
+ function snapshot() {
3809
+ return {
3810
+ breadcrumbs: buildRemoteBreadcrumbs(currentPath),
3811
+ entries: [...cachedEntries],
3812
+ path: currentPath
3813
+ };
3814
+ }
3815
+ async function navigate(target) {
3816
+ currentPath = resolveTarget(currentPath, target);
3817
+ return loadCurrent();
3818
+ }
3819
+ async function open2(entry) {
3820
+ if (entry.type !== "directory") {
3821
+ throw new TypeError(`Cannot open non-directory entry "${entry.path}" (type: ${entry.type})`);
3822
+ }
3823
+ return navigate(entry.path);
3824
+ }
3825
+ return {
3826
+ breadcrumbs: () => buildRemoteBreadcrumbs(currentPath),
3827
+ get entries() {
3828
+ return cachedEntries;
3829
+ },
3830
+ navigate,
3831
+ open: open2,
3832
+ get path() {
3833
+ return currentPath;
3834
+ },
3835
+ refresh: loadCurrent,
3836
+ setShowHidden(value) {
3837
+ showHidden = value;
3838
+ },
3839
+ setSort(key, order = sortOrder) {
3840
+ sortKey = key;
3841
+ sortOrder = order;
3842
+ },
3843
+ up: () => navigate(parentRemotePath(currentPath))
3844
+ };
3845
+ }
3846
+ function resolveTarget(currentPath, target) {
3847
+ if (target.startsWith("/")) return normalizeRemotePath(target);
3848
+ if (target === "" || target === ".") return currentPath;
3849
+ if (target === "..") return parentRemotePath(currentPath);
3850
+ const base = currentPath === "/" ? "" : currentPath;
3851
+ return normalizeRemotePath(`${base}/${target}`);
3852
+ }
3853
+ function compareEntriesByKey(left, right, key) {
3854
+ switch (key) {
3855
+ case "size":
3856
+ return (left.size ?? 0) - (right.size ?? 0);
3857
+ case "modifiedAt": {
3858
+ const leftTime = left.modifiedAt?.getTime() ?? 0;
3859
+ const rightTime = right.modifiedAt?.getTime() ?? 0;
3860
+ return leftTime - rightTime;
3861
+ }
3862
+ case "type":
3863
+ return left.type.localeCompare(right.type);
3864
+ case "name":
3865
+ default:
3866
+ return compareNames(left, right);
3867
+ }
3868
+ }
3869
+ function compareNames(left, right) {
3870
+ return left.name.localeCompare(right.name, void 0, { numeric: true, sensitivity: "base" });
3871
+ }
3872
+
3873
+ // src/sync/createSyncPlan.ts
3874
+ function createSyncPlan(options) {
3875
+ const direction = options.direction ?? "source-to-destination";
3876
+ const deletePolicy = options.deletePolicy ?? "never";
3877
+ const conflictPolicy = options.conflictPolicy ?? "overwrite";
3878
+ const includeDirectoryActions = options.includeDirectoryActions ?? false;
3879
+ const sourceRoot = normalizeRemotePath(options.source.rootPath);
3880
+ const destinationRoot = normalizeRemotePath(options.destination.rootPath);
3881
+ const warnings = [];
3882
+ const steps = [];
3883
+ for (const entry of options.diff.entries) {
3884
+ const context = {
3885
+ conflictPolicy,
3886
+ deletePolicy,
3887
+ destinationRoot,
3888
+ direction,
3889
+ entry,
3890
+ includeDirectoryActions,
3891
+ sourceRoot,
3892
+ warnings
3893
+ };
3894
+ if (options.source.provider !== void 0) context.sourceProvider = options.source.provider;
3895
+ if (options.destination.provider !== void 0) {
3896
+ context.destinationProvider = options.destination.provider;
3897
+ }
3898
+ const step = planEntry(context);
3899
+ if (step !== void 0) steps.push(step);
3900
+ }
3901
+ const planInput = {
3902
+ id: options.id,
3903
+ steps,
3904
+ warnings
3905
+ };
3906
+ if (options.dryRun !== void 0) planInput.dryRun = options.dryRun;
3907
+ if (options.now !== void 0) planInput.now = options.now;
3908
+ if (options.metadata !== void 0) planInput.metadata = options.metadata;
3909
+ return createTransferPlan(planInput);
3910
+ }
3911
+ function planEntry(context) {
3912
+ const { entry } = context;
3913
+ const isDirectory = isDirectoryEntry(entry);
3914
+ if (isDirectory && !context.includeDirectoryActions) {
3915
+ return void 0;
3916
+ }
3917
+ switch (entry.status) {
3918
+ case "added":
3919
+ return planAdded(context);
3920
+ case "removed":
3921
+ return planRemoved(context);
3922
+ case "modified":
3923
+ return planModified(context);
3924
+ case "unchanged":
3925
+ return planUnchanged(context);
3926
+ default:
3927
+ return void 0;
3928
+ }
3929
+ }
3930
+ function planAdded(context) {
3931
+ if (context.direction === "source-to-destination") {
3932
+ return createCopyStep(context, "source", "destination", expectedBytesFor(context.entry));
3933
+ }
3934
+ if (context.deletePolicy === "never") {
3935
+ return createSkipStep(context, "Source-only entry preserved by delete policy");
3936
+ }
3937
+ return createDeleteStep(context, "source");
3938
+ }
3939
+ function planRemoved(context) {
3940
+ if (context.direction === "destination-to-source") {
3941
+ return createCopyStep(context, "destination", "source", expectedBytesFor(context.entry));
3942
+ }
3943
+ if (context.deletePolicy === "never") {
3944
+ return createSkipStep(context, "Destination-only entry preserved by delete policy");
3945
+ }
3946
+ if (context.deletePolicy === "replace-only") {
3947
+ return createSkipStep(
3948
+ context,
3949
+ "Destination-only entry preserved (no source replacement available)"
3950
+ );
3951
+ }
3952
+ return createDeleteStep(context, "destination");
3953
+ }
3954
+ function planModified(context) {
3955
+ switch (context.conflictPolicy) {
3956
+ case "overwrite":
3957
+ return createCopyStep(context, "source", "destination", expectedBytesFor(context.entry), {
3958
+ destructive: true
3959
+ });
3960
+ case "prefer-destination":
3961
+ return createCopyStep(context, "destination", "source", expectedBytesFor(context.entry), {
3962
+ destructive: true
3963
+ });
3964
+ case "skip":
3965
+ return createSkipStep(context, `Conflict skipped: ${context.entry.reasons.join(",")}`);
3966
+ case "error":
3967
+ throw new ConfigurationError({
3968
+ details: {
3969
+ path: context.entry.path,
3970
+ reasons: context.entry.reasons
3971
+ },
3972
+ message: `Sync plan conflict at ${context.entry.path} with reasons: ${context.entry.reasons.join(", ")}`,
3973
+ retryable: false
3974
+ });
3975
+ default:
3976
+ return createSkipStep(context, "Conflict skipped");
3977
+ }
3978
+ }
3979
+ function planUnchanged(context) {
3980
+ return createSkipStep(context, "Entry already in sync");
3981
+ }
3982
+ function createCopyStep(context, fromSide, toSide, expectedBytes, overrides = {}) {
3983
+ const step = {
3984
+ action: "copy",
3985
+ id: makeStepId(context.entry, `copy-${fromSide}-to-${toSide}`),
3986
+ reason: describeReasons(context.entry, `Copy ${fromSide} to ${toSide}`)
3987
+ };
3988
+ step.source = endpointFor(context, fromSide);
3989
+ step.destination = endpointFor(context, toSide);
3990
+ if (expectedBytes !== void 0) step.expectedBytes = expectedBytes;
3991
+ if (overrides.destructive === true) step.destructive = true;
3992
+ if (overrides.metadata !== void 0) step.metadata = { ...overrides.metadata };
3993
+ return step;
3994
+ }
3995
+ function createDeleteStep(context, side) {
3996
+ return {
3997
+ action: "delete",
3998
+ destination: endpointFor(context, side),
3999
+ destructive: true,
4000
+ id: makeStepId(context.entry, `delete-${side}`),
4001
+ reason: `Delete ${side} entry not present on the other side`
4002
+ };
4003
+ }
4004
+ function createSkipStep(context, reason) {
4005
+ return {
4006
+ action: "skip",
4007
+ id: makeStepId(context.entry, "skip"),
4008
+ reason,
4009
+ source: endpointFor(context, "source"),
4010
+ destination: endpointFor(context, "destination")
4011
+ };
4012
+ }
4013
+ function endpointFor(context, side) {
4014
+ const root = side === "source" ? context.sourceRoot : context.destinationRoot;
4015
+ const provider = side === "source" ? context.sourceProvider : context.destinationProvider;
4016
+ const endpoint = {
4017
+ path: joinRootAndRelative(root, context.entry.path)
4018
+ };
4019
+ if (provider !== void 0) endpoint.provider = provider;
4020
+ return endpoint;
4021
+ }
4022
+ function joinRootAndRelative(rootPath, relativePath) {
4023
+ if (rootPath === "/") return relativePath;
4024
+ if (relativePath === "/") return rootPath;
4025
+ return joinRemotePath(rootPath, relativePath);
4026
+ }
4027
+ function makeStepId(entry, suffix) {
4028
+ return `${entry.path}#${suffix}`;
4029
+ }
4030
+ function describeReasons(entry, prefix) {
4031
+ if (entry.reasons.length === 0) return prefix;
4032
+ return `${prefix} (${entry.reasons.join(",")})`;
4033
+ }
4034
+ function expectedBytesFor(entry) {
4035
+ return entry.source?.size ?? entry.destination?.size;
4036
+ }
4037
+ function isDirectoryEntry(entry) {
4038
+ return entry.source?.type === "directory" || entry.destination?.type === "directory";
4039
+ }
4040
+
4041
+ // src/sync/createAtomicDeployPlan.ts
4042
+ var DEFAULT_RELEASES_DIRECTORY = ".releases";
4043
+ var DEFAULT_RETAIN = 3;
4044
+ function createAtomicDeployPlan(options) {
4045
+ const retain = options.retain ?? DEFAULT_RETAIN;
4046
+ if (retain < 1) {
4047
+ throw new ConfigurationError({
4048
+ details: { retain },
4049
+ message: "Atomic deploy retain count must be at least 1",
4050
+ retryable: false
4051
+ });
4052
+ }
4053
+ const livePath = normalizeRemotePath(options.destination.rootPath);
4054
+ if (livePath === "/") {
4055
+ throw new ConfigurationError({
4056
+ message: "Atomic deploy destination rootPath must not be the filesystem root",
4057
+ retryable: false
4058
+ });
4059
+ }
4060
+ const strategy = options.strategy ?? "rename";
4061
+ const now = options.now?.() ?? /* @__PURE__ */ new Date();
4062
+ const releaseId = options.releaseId ?? defaultReleaseId(now);
4063
+ const releasesRoot = joinRemotePath(
4064
+ livePath,
4065
+ options.releasesDirectory ?? DEFAULT_RELEASES_DIRECTORY
4066
+ );
4067
+ const stagingPath = joinRemotePath(releasesRoot, releaseId);
4068
+ const backupPath = strategy === "rename" ? joinRemotePath(releasesRoot, `${releaseId}.previous`) : void 0;
4069
+ const provider = options.destination.provider ?? options.source.provider;
4070
+ const warnings = [];
4071
+ const uploadPlan = createSyncPlan({
4072
+ conflictPolicy: "overwrite",
4073
+ deletePolicy: "never",
4074
+ destination: {
4075
+ ...options.destination.provider !== void 0 ? { provider: options.destination.provider } : {},
4076
+ rootPath: stagingPath
4077
+ },
4078
+ diff: options.diff,
4079
+ direction: "source-to-destination",
4080
+ dryRun: options.dryRun ?? true,
4081
+ id: `${options.id}/upload`,
4082
+ includeDirectoryActions: false,
4083
+ ...options.now !== void 0 ? { now: options.now } : {},
4084
+ source: options.source
4085
+ });
4086
+ const activate = buildActivateSteps({
4087
+ backupPath,
4088
+ livePath,
4089
+ planId: options.id,
4090
+ provider,
4091
+ stagingPath,
4092
+ strategy
4093
+ });
4094
+ const prune = buildPruneSteps({
4095
+ existingReleases: options.existingReleases ?? [],
4096
+ planId: options.id,
4097
+ provider,
4098
+ releaseId,
4099
+ releasesRoot,
4100
+ retain
4101
+ });
4102
+ const plan = {
4103
+ activate,
4104
+ createdAt: now,
4105
+ id: options.id,
4106
+ livePath,
4107
+ prune,
4108
+ releaseId,
4109
+ releasesRoot,
4110
+ retain,
4111
+ stagingPath,
4112
+ strategy,
4113
+ uploadPlan,
4114
+ warnings
4115
+ };
4116
+ if (provider !== void 0) plan.provider = provider;
4117
+ if (backupPath !== void 0) plan.backupPath = backupPath;
4118
+ if (options.metadata !== void 0) plan.metadata = { ...options.metadata };
4119
+ return plan;
4120
+ }
4121
+ function buildActivateSteps(context) {
4122
+ if (context.strategy === "symlink") {
4123
+ const step = {
4124
+ destructive: true,
4125
+ fromPath: context.stagingPath,
4126
+ id: `${context.planId}/activate/symlink`,
4127
+ operation: "symlink",
4128
+ reason: "Update live symlink to point at the new release",
4129
+ toPath: context.livePath
4130
+ };
4131
+ if (context.provider !== void 0) step.provider = context.provider;
4132
+ return [step];
4133
+ }
4134
+ const steps = [];
4135
+ if (context.backupPath !== void 0) {
4136
+ const backup = {
4137
+ destructive: true,
4138
+ fromPath: context.livePath,
4139
+ id: `${context.planId}/activate/backup`,
4140
+ operation: "rename",
4141
+ reason: "Rename current live path aside as a release backup",
4142
+ toPath: context.backupPath
4143
+ };
4144
+ if (context.provider !== void 0) backup.provider = context.provider;
4145
+ steps.push(backup);
4146
+ }
4147
+ const promote = {
4148
+ destructive: true,
4149
+ fromPath: context.stagingPath,
4150
+ id: `${context.planId}/activate/promote`,
4151
+ operation: "rename",
4152
+ reason: "Promote the staged release to the live path",
4153
+ toPath: context.livePath
4154
+ };
4155
+ if (context.provider !== void 0) promote.provider = context.provider;
4156
+ steps.push(promote);
4157
+ return steps;
4158
+ }
4159
+ function buildPruneSteps(context) {
4160
+ if (context.existingReleases.length === 0) return [];
4161
+ const normalizedRoot = normalizeRemotePath(context.releasesRoot);
4162
+ const newReleasePath = joinRemotePath(normalizedRoot, context.releaseId);
4163
+ const candidates = [...new Set(context.existingReleases.map((path2) => normalizeRemotePath(path2)))].filter((path2) => path2 !== newReleasePath).sort();
4164
+ const releasesToRetain = Math.max(0, context.retain - 1);
4165
+ if (candidates.length <= releasesToRetain) return [];
4166
+ const toPrune = candidates.slice(0, candidates.length - releasesToRetain);
4167
+ return toPrune.map((path2, index) => {
4168
+ const step = {
4169
+ id: `${context.planId}/prune/${index}`,
4170
+ path: path2,
4171
+ reason: "Older release exceeds retain window"
4172
+ };
4173
+ if (context.provider !== void 0) step.provider = context.provider;
4174
+ return step;
4175
+ });
4176
+ }
4177
+ function defaultReleaseId(now) {
4178
+ return now.toISOString().replace(/[:.]/g, "-");
4179
+ }
4180
+
4181
+ // src/sync/walkRemoteTree.ts
4182
+ async function* walkRemoteTree(fs, rootPath, options = {}) {
4183
+ const recursive = options.recursive ?? true;
4184
+ const includeDirectories = options.includeDirectories ?? true;
4185
+ const includeFiles = options.includeFiles ?? true;
4186
+ const followSymlinks = options.followSymlinks ?? false;
4187
+ const root = normalizeRemotePath(rootPath);
4188
+ const normalized = {
4189
+ followSymlinks,
4190
+ includeDirectories,
4191
+ includeFiles,
4192
+ recursive
4193
+ };
4194
+ if (options.maxDepth !== void 0) normalized.maxDepth = options.maxDepth;
4195
+ if (options.filter !== void 0) normalized.filter = options.filter;
4196
+ if (options.signal !== void 0) normalized.signal = options.signal;
4197
+ yield* walkDirectory(fs, root, 0, normalized);
4198
+ }
4199
+ async function* walkDirectory(fs, path2, depth, options) {
4200
+ throwIfAborted2(options.signal);
4201
+ const entries = await fs.list(path2);
4202
+ const sorted = [...entries].sort(compareEntries3);
4203
+ for (const entry of sorted) {
4204
+ if (options.filter !== void 0 && !options.filter(entry)) continue;
4205
+ if (matchesEntryKind(entry, options.includeDirectories, options.includeFiles)) {
4206
+ yield { depth, entry, parentPath: path2 };
4207
+ }
4208
+ if (options.recursive && canDescendInto(entry, options.followSymlinks) && (options.maxDepth === void 0 || depth < options.maxDepth)) {
4209
+ yield* walkDirectory(fs, ensureDescendPath(entry, path2), depth + 1, options);
4210
+ }
4211
+ }
4212
+ }
4213
+ function matchesEntryKind(entry, includeDirectories, includeFiles) {
4214
+ if (entry.type === "directory") return includeDirectories;
4215
+ if (entry.type === "file") return includeFiles;
4216
+ return true;
4217
+ }
4218
+ function canDescendInto(entry, followSymlinks) {
4219
+ if (entry.type === "directory") return true;
4220
+ return followSymlinks && entry.type === "symlink";
4221
+ }
4222
+ function ensureDescendPath(entry, parentPath) {
4223
+ if (entry.path !== "" && entry.path !== entry.name) {
4224
+ return normalizeRemotePath(entry.path);
4225
+ }
4226
+ return joinRemotePath(parentPath, entry.name);
4227
+ }
4228
+ function compareEntries3(left, right) {
4229
+ if (left.path < right.path) return -1;
4230
+ if (left.path > right.path) return 1;
4231
+ return 0;
4232
+ }
4233
+ function throwIfAborted2(signal) {
4234
+ if (signal?.aborted === true) {
4235
+ throw new AbortError({
4236
+ message: "Remote tree walk aborted",
4237
+ retryable: false
4238
+ });
4239
+ }
4240
+ }
4241
+
4242
+ // src/sync/diffRemoteTrees.ts
4243
+ async function diffRemoteTrees(source, sourcePath, destination, destinationPath, options = {}) {
4244
+ const includeUnchanged = options.includeUnchanged ?? false;
4245
+ const sourceRoot = normalizeRemotePath(sourcePath);
4246
+ const destinationRoot = normalizeRemotePath(destinationPath);
4247
+ const sourceWalk = createWalkOptions(options, options.sourceFilter);
4248
+ const destinationWalk = createWalkOptions(options, options.destinationFilter);
4249
+ const [sourceEntries, destinationEntries] = await Promise.all([
4250
+ collectEntries(source, sourceRoot, sourceWalk),
4251
+ collectEntries(destination, destinationRoot, destinationWalk)
4252
+ ]);
4253
+ const aligned = alignEntries(sourceEntries, destinationEntries);
4254
+ const entries = [];
4255
+ const summary = {
4256
+ added: 0,
4257
+ modified: 0,
4258
+ removed: 0,
4259
+ total: 0,
4260
+ unchanged: 0
4261
+ };
4262
+ for (const { path: path2, source: sourceEntry, destination: destinationEntry } of aligned) {
4263
+ summary.total += 1;
4264
+ const reasons = [];
4265
+ let status;
4266
+ if (sourceEntry !== void 0 && destinationEntry === void 0) {
4267
+ status = "added";
4268
+ summary.added += 1;
4269
+ } else if (sourceEntry === void 0 && destinationEntry !== void 0) {
4270
+ status = "removed";
4271
+ summary.removed += 1;
4272
+ } else if (sourceEntry !== void 0 && destinationEntry !== void 0) {
4273
+ const computedReasons = compareEntries4(sourceEntry, destinationEntry, options);
4274
+ if (computedReasons.length === 0) {
4275
+ status = "unchanged";
4276
+ summary.unchanged += 1;
4277
+ } else {
4278
+ status = "modified";
4279
+ reasons.push(...computedReasons);
4280
+ summary.modified += 1;
4281
+ }
4282
+ } else {
4283
+ continue;
4284
+ }
4285
+ if (status === "unchanged" && !includeUnchanged) continue;
4286
+ const record = { path: path2, reasons, status };
4287
+ if (sourceEntry !== void 0) record.source = sourceEntry;
4288
+ if (destinationEntry !== void 0) record.destination = destinationEntry;
4289
+ entries.push(record);
4290
+ }
4291
+ entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
4292
+ return { entries, summary };
4293
+ }
4294
+ function createWalkOptions(options, filter) {
4295
+ const walk = options.walk ?? {};
4296
+ const merged = {};
4297
+ if (walk.recursive !== void 0) merged.recursive = walk.recursive;
4298
+ if (walk.maxDepth !== void 0) merged.maxDepth = walk.maxDepth;
4299
+ if (walk.includeDirectories !== void 0) merged.includeDirectories = walk.includeDirectories;
4300
+ if (walk.includeFiles !== void 0) merged.includeFiles = walk.includeFiles;
4301
+ if (walk.followSymlinks !== void 0) merged.followSymlinks = walk.followSymlinks;
4302
+ const resolvedFilter = filter ?? walk.filter;
4303
+ if (resolvedFilter !== void 0) merged.filter = resolvedFilter;
4304
+ if (options.signal !== void 0) merged.signal = options.signal;
4305
+ return merged;
4306
+ }
4307
+ async function collectEntries(fs, rootPath, walkOptions) {
4308
+ const map = /* @__PURE__ */ new Map();
4309
+ for await (const record of walkRemoteTree(fs, rootPath, walkOptions)) {
4310
+ const collected = toCollectedEntry(record.entry, rootPath);
4311
+ if (collected !== void 0) map.set(collected.relativePath, collected.entry);
4312
+ }
4313
+ return map;
4314
+ }
4315
+ function toCollectedEntry(entry, rootPath) {
4316
+ const root = normalizeRemotePath(rootPath);
4317
+ const path2 = normalizeRemotePath(entry.path);
4318
+ if (path2 === root) return void 0;
4319
+ if (root === "/") return { entry, relativePath: path2 };
4320
+ if (path2.startsWith(`${root}/`)) {
4321
+ return { entry, relativePath: path2.slice(root.length) };
4322
+ }
4323
+ return void 0;
4324
+ }
4325
+ function alignEntries(sourceEntries, destinationEntries) {
4326
+ const paths = /* @__PURE__ */ new Set([...sourceEntries.keys(), ...destinationEntries.keys()]);
4327
+ const aligned = [];
4328
+ for (const path2 of paths) {
4329
+ const pair = { path: path2 };
4330
+ const source = sourceEntries.get(path2);
4331
+ const destination = destinationEntries.get(path2);
4332
+ if (source !== void 0) pair.source = source;
4333
+ if (destination !== void 0) pair.destination = destination;
4334
+ aligned.push(pair);
4335
+ }
4336
+ return aligned;
4337
+ }
4338
+ function compareEntries4(source, destination, options) {
4339
+ const reasons = [];
4340
+ const compareSize = options.compareSize ?? true;
4341
+ const compareModifiedAt = options.compareModifiedAt ?? true;
4342
+ const compareUniqueId = options.compareUniqueId ?? false;
4343
+ const tolerance = options.modifiedAtToleranceMs ?? 1e3;
4344
+ if (source.type !== destination.type) {
4345
+ reasons.push("type");
4346
+ }
4347
+ if (compareSize && isSizeRelevant(source, destination) && source.size !== destination.size) {
4348
+ reasons.push("size");
4349
+ }
4350
+ if (compareModifiedAt && isModifiedAtDifferent(source, destination, tolerance)) {
4351
+ reasons.push("modifiedAt");
4352
+ }
4353
+ if (compareUniqueId && source.uniqueId !== void 0 && destination.uniqueId !== void 0 && source.uniqueId !== destination.uniqueId) {
4354
+ reasons.push("checksum");
4355
+ }
4356
+ return reasons;
4357
+ }
4358
+ function isSizeRelevant(source, destination) {
4359
+ if (source.type !== "file" || destination.type !== "file") return false;
4360
+ return source.size !== void 0 && destination.size !== void 0;
4361
+ }
4362
+ function isModifiedAtDifferent(source, destination, toleranceMs) {
4363
+ if (source.modifiedAt === void 0 || destination.modifiedAt === void 0) return false;
4364
+ const delta = Math.abs(source.modifiedAt.getTime() - destination.modifiedAt.getTime());
4365
+ return delta > toleranceMs;
4366
+ }
4367
+
4368
+ // src/sync/manifest.ts
4369
+ var REMOTE_MANIFEST_FORMAT_VERSION = 1;
4370
+ async function createRemoteManifest(fs, rootPath, options = {}) {
4371
+ const root = normalizeRemotePath(rootPath);
4372
+ const walkOptions = { ...options.walk ?? {} };
4373
+ const resolvedFilter = options.filter ?? options.walk?.filter;
4374
+ if (resolvedFilter !== void 0) walkOptions.filter = resolvedFilter;
4375
+ if (options.signal !== void 0) walkOptions.signal = options.signal;
4376
+ const entries = [];
4377
+ for await (const record of walkRemoteTree(fs, root, walkOptions)) {
4378
+ const relativePath = relativeFromRoot(record.entry.path, root);
4379
+ if (relativePath === void 0) continue;
4380
+ entries.push(toManifestEntry(record.entry, relativePath));
4381
+ }
4382
+ entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
4383
+ const generatedAt = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
4384
+ const manifest = {
4385
+ entries,
4386
+ formatVersion: REMOTE_MANIFEST_FORMAT_VERSION,
4387
+ generatedAt,
4388
+ root
4389
+ };
4390
+ if (options.provider !== void 0) manifest.provider = options.provider;
4391
+ return manifest;
4392
+ }
4393
+ function serializeRemoteManifest(manifest, indent = 2) {
4394
+ return JSON.stringify(manifest, void 0, indent);
4395
+ }
4396
+ function parseRemoteManifest(text) {
4397
+ let parsed;
4398
+ try {
4399
+ parsed = JSON.parse(text);
4400
+ } catch (error) {
4401
+ throw new ConfigurationError({
4402
+ cause: error,
4403
+ message: "Failed to parse remote manifest payload as JSON",
4404
+ retryable: false
4405
+ });
4406
+ }
4407
+ if (parsed === null || typeof parsed !== "object") {
4408
+ throw new ConfigurationError({
4409
+ message: "Remote manifest payload must be a JSON object",
4410
+ retryable: false
4411
+ });
4412
+ }
4413
+ const candidate = parsed;
4414
+ if (candidate.formatVersion !== REMOTE_MANIFEST_FORMAT_VERSION) {
4415
+ throw new ConfigurationError({
4416
+ details: {
4417
+ expected: REMOTE_MANIFEST_FORMAT_VERSION,
4418
+ received: candidate.formatVersion
4419
+ },
4420
+ message: `Unsupported remote manifest formatVersion: ${String(candidate.formatVersion)}`,
4421
+ retryable: false
4422
+ });
4423
+ }
4424
+ if (typeof candidate.root !== "string" || candidate.root.length === 0) {
4425
+ throw new ConfigurationError({
4426
+ message: "Remote manifest root must be a non-empty string",
4427
+ retryable: false
4428
+ });
4429
+ }
4430
+ if (typeof candidate.generatedAt !== "string") {
4431
+ throw new ConfigurationError({
4432
+ message: "Remote manifest generatedAt must be an ISO timestamp string",
4433
+ retryable: false
4434
+ });
4435
+ }
4436
+ if (!Array.isArray(candidate.entries)) {
4437
+ throw new ConfigurationError({
4438
+ message: "Remote manifest entries must be an array",
4439
+ retryable: false
4440
+ });
4441
+ }
4442
+ const entries = candidate.entries.map((entry, index) => normalizeManifestEntry(entry, index));
4443
+ const manifest = {
4444
+ entries,
4445
+ formatVersion: REMOTE_MANIFEST_FORMAT_VERSION,
4446
+ generatedAt: candidate.generatedAt,
4447
+ root: normalizeRemotePath(candidate.root)
4448
+ };
4449
+ if (typeof candidate.provider === "string") manifest.provider = candidate.provider;
4450
+ return manifest;
4451
+ }
4452
+ function compareRemoteManifests(source, destination, options = {}) {
4453
+ const includeUnchanged = options.includeUnchanged ?? false;
4454
+ const sourceMap = indexEntries(source);
4455
+ const destinationMap = indexEntries(destination);
4456
+ const paths = /* @__PURE__ */ new Set([...sourceMap.keys(), ...destinationMap.keys()]);
4457
+ const entries = [];
4458
+ const summary = {
4459
+ added: 0,
4460
+ modified: 0,
4461
+ removed: 0,
4462
+ total: 0,
4463
+ unchanged: 0
4464
+ };
4465
+ for (const path2 of paths) {
4466
+ summary.total += 1;
4467
+ const sourceEntry = sourceMap.get(path2);
4468
+ const destinationEntry = destinationMap.get(path2);
4469
+ const reasons = [];
4470
+ let status;
4471
+ if (sourceEntry !== void 0 && destinationEntry === void 0) {
4472
+ status = "added";
4473
+ summary.added += 1;
4474
+ } else if (sourceEntry === void 0 && destinationEntry !== void 0) {
4475
+ status = "removed";
4476
+ summary.removed += 1;
4477
+ } else if (sourceEntry !== void 0 && destinationEntry !== void 0) {
4478
+ const computed = compareManifestEntries(sourceEntry, destinationEntry, options);
4479
+ if (computed.length === 0) {
4480
+ status = "unchanged";
4481
+ summary.unchanged += 1;
4482
+ } else {
4483
+ status = "modified";
4484
+ reasons.push(...computed);
4485
+ summary.modified += 1;
4486
+ }
4487
+ } else {
4488
+ continue;
4489
+ }
4490
+ if (status === "unchanged" && !includeUnchanged) continue;
4491
+ const record = { path: path2, reasons, status };
4492
+ if (sourceEntry !== void 0) {
4493
+ record.source = manifestEntryToRemote(sourceEntry, source.root);
4494
+ }
4495
+ if (destinationEntry !== void 0) {
4496
+ record.destination = manifestEntryToRemote(destinationEntry, destination.root);
4497
+ }
4498
+ entries.push(record);
4499
+ }
4500
+ entries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
4501
+ return { entries, summary };
4502
+ }
4503
+ function relativeFromRoot(entryPath, root) {
4504
+ const path2 = normalizeRemotePath(entryPath);
4505
+ if (path2 === root) return void 0;
4506
+ if (root === "/") return path2;
4507
+ if (path2.startsWith(`${root}/`)) return path2.slice(root.length);
4508
+ return void 0;
4509
+ }
4510
+ function toManifestEntry(entry, relativePath) {
4511
+ const manifestEntry = {
4512
+ path: relativePath,
4513
+ type: entry.type
4514
+ };
4515
+ if (entry.size !== void 0) manifestEntry.size = entry.size;
4516
+ if (entry.modifiedAt !== void 0) manifestEntry.modifiedAt = entry.modifiedAt.toISOString();
4517
+ if (entry.uniqueId !== void 0) manifestEntry.uniqueId = entry.uniqueId;
4518
+ if (entry.symlinkTarget !== void 0) manifestEntry.symlinkTarget = entry.symlinkTarget;
4519
+ return manifestEntry;
4520
+ }
4521
+ function normalizeManifestEntry(value, index) {
4522
+ if (value === null || typeof value !== "object") {
4523
+ throw new ConfigurationError({
4524
+ details: { index },
4525
+ message: `Remote manifest entry at index ${index} must be an object`,
4526
+ retryable: false
4527
+ });
4528
+ }
4529
+ const candidate = value;
4530
+ if (typeof candidate.path !== "string" || candidate.path.length === 0) {
4531
+ throw new ConfigurationError({
4532
+ details: { index },
4533
+ message: `Remote manifest entry at index ${index} must have a non-empty path`,
4534
+ retryable: false
4535
+ });
4536
+ }
4537
+ if (!isRemoteEntryType(candidate.type)) {
4538
+ throw new ConfigurationError({
4539
+ details: { index, received: candidate.type },
4540
+ message: `Remote manifest entry at index ${index} has an invalid type`,
4541
+ retryable: false
4542
+ });
4543
+ }
4544
+ const entry = {
4545
+ path: candidate.path,
4546
+ type: candidate.type
4547
+ };
4548
+ if (typeof candidate.size === "number") entry.size = candidate.size;
4549
+ if (typeof candidate.modifiedAt === "string") entry.modifiedAt = candidate.modifiedAt;
4550
+ if (typeof candidate.uniqueId === "string") entry.uniqueId = candidate.uniqueId;
4551
+ if (typeof candidate.symlinkTarget === "string") entry.symlinkTarget = candidate.symlinkTarget;
4552
+ return entry;
4553
+ }
4554
+ function isRemoteEntryType(value) {
4555
+ return value === "file" || value === "directory" || value === "symlink" || value === "unknown";
4556
+ }
4557
+ function indexEntries(manifest) {
4558
+ const map = /* @__PURE__ */ new Map();
4559
+ for (const entry of manifest.entries) map.set(entry.path, entry);
4560
+ return map;
4561
+ }
4562
+ function manifestEntryToRemote(entry, root) {
4563
+ const absolutePath2 = root === "/" ? entry.path : `${root}${entry.path}`;
4564
+ const remote = {
4565
+ name: deriveName(entry.path),
4566
+ path: absolutePath2,
4567
+ type: entry.type
4568
+ };
4569
+ if (entry.size !== void 0) remote.size = entry.size;
4570
+ if (entry.modifiedAt !== void 0) {
4571
+ const parsed = new Date(entry.modifiedAt);
4572
+ if (!Number.isNaN(parsed.getTime())) remote.modifiedAt = parsed;
4573
+ }
4574
+ if (entry.uniqueId !== void 0) remote.uniqueId = entry.uniqueId;
4575
+ if (entry.symlinkTarget !== void 0) remote.symlinkTarget = entry.symlinkTarget;
4576
+ return remote;
4577
+ }
4578
+ function deriveName(path2) {
4579
+ const segments = path2.split("/").filter(Boolean);
4580
+ return segments.length === 0 ? "/" : segments[segments.length - 1] ?? "/";
4581
+ }
4582
+ function compareManifestEntries(source, destination, options) {
4583
+ const reasons = [];
4584
+ const compareSize = options.compareSize ?? true;
4585
+ const compareModifiedAt = options.compareModifiedAt ?? true;
4586
+ const compareUniqueId = options.compareUniqueId ?? false;
4587
+ const tolerance = options.modifiedAtToleranceMs ?? 1e3;
4588
+ if (source.type !== destination.type) reasons.push("type");
4589
+ if (compareSize && source.type === "file" && destination.type === "file" && source.size !== void 0 && destination.size !== void 0 && source.size !== destination.size) {
4590
+ reasons.push("size");
4591
+ }
4592
+ if (compareModifiedAt && isModifiedAtDifferent2(source, destination, tolerance)) {
4593
+ reasons.push("modifiedAt");
4594
+ }
4595
+ if (compareUniqueId && source.uniqueId !== void 0 && destination.uniqueId !== void 0 && source.uniqueId !== destination.uniqueId) {
4596
+ reasons.push("checksum");
4597
+ }
4598
+ return reasons;
4599
+ }
4600
+ function isModifiedAtDifferent2(source, destination, toleranceMs) {
4601
+ if (source.modifiedAt === void 0 || destination.modifiedAt === void 0) return false;
4602
+ const sourceTime = Date.parse(source.modifiedAt);
4603
+ const destinationTime = Date.parse(destination.modifiedAt);
4604
+ if (Number.isNaN(sourceTime) || Number.isNaN(destinationTime)) return false;
4605
+ return Math.abs(sourceTime - destinationTime) > toleranceMs;
4606
+ }
4607
+ export {
4608
+ AbortError,
4609
+ AuthenticationError,
4610
+ AuthorizationError,
4611
+ CLASSIC_PROVIDER_IDS,
4612
+ ConfigurationError,
4613
+ ConnectionError,
4614
+ ParseError,
4615
+ PathAlreadyExistsError,
4616
+ PathNotFoundError,
4617
+ PermissionDeniedError,
4618
+ ProtocolError,
4619
+ ProviderRegistry,
4620
+ REDACTED,
4621
+ REMOTE_MANIFEST_FORMAT_VERSION,
4622
+ TimeoutError,
4623
+ TransferClient,
4624
+ TransferEngine,
4625
+ TransferError,
4626
+ TransferQueue,
4627
+ UnsupportedFeatureError,
4628
+ VerificationError,
4629
+ ZeroTransfer,
4630
+ ZeroTransferError,
4631
+ assertSafeFtpArgument,
4632
+ basenameRemotePath,
4633
+ buildRemoteBreadcrumbs,
4634
+ compareRemoteManifests,
4635
+ copyBetween,
4636
+ createAtomicDeployPlan,
4637
+ createBandwidthThrottle,
4638
+ createLocalProviderFactory,
4639
+ createMemoryProviderFactory,
4640
+ createOAuthTokenSecretSource,
4641
+ createProgressEvent,
4642
+ createProviderTransferExecutor,
4643
+ createRemoteBrowser,
4644
+ createRemoteManifest,
4645
+ createSyncPlan,
4646
+ createTransferClient,
4647
+ createTransferJobsFromPlan,
4648
+ createTransferPlan,
4649
+ createTransferResult,
4650
+ diffRemoteTrees,
4651
+ downloadFile,
4652
+ emitLog,
4653
+ errorFromFtpReply,
4654
+ filterRemoteEntries,
4655
+ importFileZillaSites,
4656
+ importOpenSshConfig,
4657
+ importWinScpSessions,
4658
+ isClassicProviderId,
4659
+ isSensitiveKey,
4660
+ joinRemotePath,
4661
+ matchKnownHosts,
4662
+ matchKnownHostsEntry,
4663
+ noopLogger,
4664
+ normalizeRemotePath,
4665
+ parentRemotePath,
4666
+ parseKnownHosts,
4667
+ parseOpenSshConfig,
4668
+ parseRemoteManifest,
4669
+ redactCommand,
4670
+ redactConnectionProfile,
4671
+ redactObject,
4672
+ redactSecretSource,
4673
+ redactValue,
4674
+ resolveConnectionProfileSecrets,
4675
+ resolveOpenSshHost,
4676
+ resolveProviderId,
4677
+ resolveSecret,
4678
+ runConnectionDiagnostics,
4679
+ serializeRemoteManifest,
4680
+ sortRemoteEntries,
4681
+ summarizeClientDiagnostics,
4682
+ summarizeTransferPlan,
4683
+ throttleByteIterable,
4684
+ uploadFile,
4685
+ validateConnectionProfile,
4686
+ walkRemoteTree
4687
+ };
4688
+ //# sourceMappingURL=index.mjs.map