paratix 0.0.1 → 0.2.0

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.
@@ -0,0 +1,1706 @@
1
+ // src/sshHelpers.ts
2
+ function validateMode(mode) {
3
+ if (!new RegExp("^[0-7]{3,4}$", "v").test(mode)) {
4
+ throw new Error(
5
+ `Invalid file mode '${mode}': expected a 3- or 4-digit octal string (e.g. "644", "0755")`
6
+ );
7
+ }
8
+ }
9
+ function shellQuote(s) {
10
+ return `'${s.replaceAll("'", "'\\''")}'`;
11
+ }
12
+ var CONNECTION_TIMEOUT = 1e4;
13
+ var MAX_OUTPUT_LENGTH = 500;
14
+ function truncateOutput(text) {
15
+ let count = 0;
16
+ let sliceEnd = 0;
17
+ for (const char of text) {
18
+ if (count >= MAX_OUTPUT_LENGTH) return `${text.slice(0, sliceEnd)}\u2026(truncated)`;
19
+ sliceEnd += char.length;
20
+ count++;
21
+ }
22
+ return text;
23
+ }
24
+ function codepointLengthExceeds(text, limit) {
25
+ let count = 0;
26
+ for (const _char of text) {
27
+ count++;
28
+ if (count > limit) return true;
29
+ }
30
+ return false;
31
+ }
32
+ var CommandError = class _CommandError extends Error {
33
+ /** Full, untruncated standard error of the failed command. */
34
+ fullStderr;
35
+ /** Full, untruncated standard output of the failed command. */
36
+ fullStdout;
37
+ constructor(message, fullStdout, fullStderr) {
38
+ super(message);
39
+ this.name = "CommandError";
40
+ this.fullStdout = fullStdout;
41
+ this.fullStderr = fullStderr;
42
+ Error.captureStackTrace(this, _CommandError);
43
+ }
44
+ };
45
+ var REDACTED = "[REDACTED]";
46
+ function resolveSecret(secret) {
47
+ return typeof secret === "function" ? secret() : secret;
48
+ }
49
+ function getSecretVariants(secrets) {
50
+ const variants = /* @__PURE__ */ new Set();
51
+ for (const source of secrets) {
52
+ const secret = resolveSecret(source);
53
+ if (secret.length === 0) continue;
54
+ if (secret.includes(REDACTED)) {
55
+ throw new Error(`Secret must not contain the redaction placeholder "${REDACTED}"`);
56
+ }
57
+ variants.add(secret);
58
+ const encoded = encodeURIComponent(secret);
59
+ if (encoded !== secret) variants.add(encoded);
60
+ const quoted = shellQuote(secret);
61
+ if (quoted !== secret) variants.add(quoted);
62
+ }
63
+ return [...variants].sort((a, b) => b.length - a.length);
64
+ }
65
+ function createLazyVariantResolver(secrets) {
66
+ let variants = null;
67
+ const getVariants = () => {
68
+ variants ??= getSecretVariants(secrets);
69
+ return variants;
70
+ };
71
+ return {
72
+ getMaxLength() {
73
+ return Math.max(0, ...getVariants().map((variant) => variant.length));
74
+ },
75
+ mask(text) {
76
+ let masked = text;
77
+ for (const variant of getVariants()) {
78
+ masked = masked.replaceAll(variant, REDACTED);
79
+ }
80
+ return masked;
81
+ }
82
+ };
83
+ }
84
+ function maskSecrets(text, secrets) {
85
+ let masked = text;
86
+ const variants = getSecretVariants(secrets);
87
+ for (const variant of variants) {
88
+ masked = masked.replaceAll(variant, REDACTED);
89
+ }
90
+ return masked;
91
+ }
92
+ function createStreamMasker(write, secrets) {
93
+ const resolver = createLazyVariantResolver(secrets);
94
+ let overlap = null;
95
+ const getOverlap = () => {
96
+ overlap ??= Math.max(0, resolver.getMaxLength() - 1);
97
+ return overlap;
98
+ };
99
+ let pending = "";
100
+ return {
101
+ flush() {
102
+ if (pending.length > 0) {
103
+ write(resolver.mask(pending));
104
+ pending = "";
105
+ }
106
+ },
107
+ push(chunk) {
108
+ const currentOverlap = getOverlap();
109
+ if (currentOverlap === 0) {
110
+ write(resolver.mask(chunk));
111
+ return;
112
+ }
113
+ pending += chunk;
114
+ if (pending.length <= currentOverlap) return;
115
+ const masked = resolver.mask(pending);
116
+ if (masked.length <= currentOverlap) {
117
+ pending = masked;
118
+ return;
119
+ }
120
+ write(masked.slice(0, -currentOverlap));
121
+ pending = masked.slice(-currentOverlap);
122
+ }
123
+ };
124
+ }
125
+ function writeStdout(t) {
126
+ process.stdout.write(t);
127
+ }
128
+ function writeStderr(t) {
129
+ process.stderr.write(t);
130
+ }
131
+ function normalizeSshCloseCode(code) {
132
+ return code ?? 0;
133
+ }
134
+ function collectStreamOutput(parameters) {
135
+ const { command, options, reject, resolve, stream, timer } = parameters;
136
+ let stdout = "";
137
+ let stderr = "";
138
+ const secrets = parameters.secrets ?? [];
139
+ const stdoutMasker = createStreamMasker((text) => {
140
+ stdout += text;
141
+ if (!options.silent) writeStdout(text);
142
+ }, secrets);
143
+ const stderrMasker = createStreamMasker((text) => {
144
+ stderr += text;
145
+ if (!options.silent) writeStderr(text);
146
+ }, secrets);
147
+ stream.on("data", (data) => {
148
+ stdoutMasker.push(data.toString());
149
+ });
150
+ stream.stderr.on("data", (data) => {
151
+ stderrMasker.push(data.toString());
152
+ });
153
+ stream.on("error", (error) => {
154
+ clearTimeout(timer);
155
+ stdoutMasker.flush();
156
+ stderrMasker.flush();
157
+ reject(error);
158
+ });
159
+ stream.on("close", (code) => {
160
+ clearTimeout(timer);
161
+ stdoutMasker.flush();
162
+ stderrMasker.flush();
163
+ const exitCode = normalizeSshCloseCode(code);
164
+ if (exitCode !== 0 && options.ignoreExitCode !== true) {
165
+ const wasTruncated = codepointLengthExceeds(stdout, MAX_OUTPUT_LENGTH) || codepointLengthExceeds(stderr, MAX_OUTPUT_LENGTH);
166
+ const hint = wasTruncated ? "\n(use --verbose for full output)" : "";
167
+ reject(
168
+ new CommandError(
169
+ `Command failed with exit code ${exitCode}: ${maskSecrets(command, secrets)}
170
+ stdout: ${truncateOutput(stdout)}
171
+ stderr: ${truncateOutput(stderr)}${hint}`,
172
+ stdout,
173
+ stderr
174
+ )
175
+ );
176
+ return;
177
+ }
178
+ resolve({ code: exitCode, stderr, stdout });
179
+ });
180
+ }
181
+ function buildConnectConfig(parameters) {
182
+ const { agent, agentForward, host, hostVerifier, password, port, privateKey, username } = parameters;
183
+ const connectConfig = {
184
+ host,
185
+ port,
186
+ readyTimeout: CONNECTION_TIMEOUT,
187
+ username
188
+ };
189
+ if (privateKey != null) {
190
+ connectConfig.privateKey = privateKey;
191
+ }
192
+ if (agent != null) {
193
+ connectConfig.agent = agent;
194
+ }
195
+ if (agentForward === true) {
196
+ connectConfig.agentForward = true;
197
+ }
198
+ if (typeof password === "string") {
199
+ connectConfig.password = password;
200
+ connectConfig.tryKeyboard = true;
201
+ }
202
+ if (hostVerifier != null) {
203
+ connectConfig.hostVerifier = hostVerifier;
204
+ }
205
+ return connectConfig;
206
+ }
207
+ async function tryConnectOnPort(parameters) {
208
+ const { client, port } = parameters;
209
+ const connectConfig = buildConnectConfig(parameters);
210
+ return new Promise((resolve, reject) => {
211
+ const timeout = setTimeout(() => {
212
+ client.end();
213
+ reject(new Error(`Connection timeout on port ${port}`));
214
+ }, CONNECTION_TIMEOUT);
215
+ client.on("ready", () => {
216
+ clearTimeout(timeout);
217
+ resolve();
218
+ });
219
+ client.on("error", (error) => {
220
+ clearTimeout(timeout);
221
+ reject(error);
222
+ });
223
+ client.connect(connectConfig);
224
+ });
225
+ }
226
+
227
+ // src/serverDefinitionValidation.ts
228
+ var STRICT_HOST_KEY_ERROR = `Invalid property 'ssh.strictHostKeyChecking' (expected "accept-new", "no", or "yes")`;
229
+ var MAX_TCP_PORT = 65535;
230
+ function describeType(value) {
231
+ return value === null ? "null" : typeof value;
232
+ }
233
+ function isHostKeyMode(value) {
234
+ return value === "accept-new" || value === "no" || value === "yes";
235
+ }
236
+ function isRecord(value) {
237
+ return Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null;
238
+ }
239
+ function collectOptionalBooleanErrors(object, key, errors) {
240
+ if (!(key in object) || object[key] == null) return;
241
+ if (typeof object[key] !== "boolean") {
242
+ errors.push(
243
+ `Invalid property 'ssh.${key}' (expected boolean, got ${describeType(object[key])})`
244
+ );
245
+ }
246
+ }
247
+ function collectOptionalNumberErrors(parameters) {
248
+ const { errors, key, object, options } = parameters;
249
+ if (!(key in object) || object[key] == null) return;
250
+ const value = object[key];
251
+ const validatedNumber = validateNumberValue(value);
252
+ if (validatedNumber == null) {
253
+ errors.push(`Invalid property 'ssh.${key}' (expected number, got ${describeType(value)})`);
254
+ return;
255
+ }
256
+ if (options?.integer === true && !Number.isInteger(validatedNumber)) {
257
+ errors.push(`Property 'ssh.${key}' must be an integer`);
258
+ return;
259
+ }
260
+ if (options?.positive === true && validatedNumber <= 0) {
261
+ errors.push(`Property 'ssh.${key}' must be greater than 0`);
262
+ }
263
+ }
264
+ function validateNumberValue(value) {
265
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
266
+ }
267
+ function isValidTcpPort(value) {
268
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_TCP_PORT;
269
+ }
270
+ function collectOptionalStringErrors(object, key, errors) {
271
+ if (!(key in object) || object[key] == null) return;
272
+ if (typeof object[key] !== "string") {
273
+ errors.push(`Invalid property 'ssh.${key}' (expected string, got ${describeType(object[key])})`);
274
+ return;
275
+ }
276
+ if (object[key].length === 0) {
277
+ errors.push(`Property 'ssh.${key}' must not be an empty string`);
278
+ }
279
+ }
280
+ function collectPortsErrors(ssh, errors) {
281
+ if (!("ports" in ssh)) {
282
+ errors.push("Missing property 'ssh.ports' (expected array)");
283
+ return;
284
+ }
285
+ if (!Array.isArray(ssh.ports)) {
286
+ errors.push(`Invalid property 'ssh.ports' (expected array, got ${describeType(ssh.ports)})`);
287
+ return;
288
+ }
289
+ if (ssh.ports.length === 0) {
290
+ errors.push("Property 'ssh.ports' must not be empty");
291
+ return;
292
+ }
293
+ for (const [index, port] of ssh.ports.entries()) {
294
+ if (!isValidTcpPort(port)) {
295
+ errors.push(`Property 'ssh.ports[${index}]' must be an integer between 1 and 65535`);
296
+ }
297
+ }
298
+ }
299
+ function collectRequiredUserErrors(ssh, errors) {
300
+ if (!("user" in ssh)) {
301
+ errors.push("Missing property 'ssh.user' (expected string)");
302
+ return;
303
+ }
304
+ if (typeof ssh.user !== "string") {
305
+ errors.push(`Invalid property 'ssh.user' (expected string, got ${describeType(ssh.user)})`);
306
+ return;
307
+ }
308
+ if (ssh.user.length === 0) {
309
+ errors.push("Property 'ssh.user' must not be an empty string");
310
+ }
311
+ }
312
+ function collectStrictHostKeyCheckingErrors(ssh, errors) {
313
+ if (!("strictHostKeyChecking" in ssh) || ssh.strictHostKeyChecking == null) return;
314
+ if (typeof ssh.strictHostKeyChecking !== "string" || !isHostKeyMode(ssh.strictHostKeyChecking)) {
315
+ errors.push(STRICT_HOST_KEY_ERROR);
316
+ }
317
+ }
318
+ function collectOptionalSshFieldErrors(ssh, errors) {
319
+ collectOptionalStringErrors(ssh, "privateKey", errors);
320
+ collectOptionalStringErrors(ssh, "expectedHostFingerprint", errors);
321
+ collectOptionalStringErrors(ssh, "expectedHostPublicKey", errors);
322
+ collectOptionalBooleanErrors(ssh, "agentForward", errors);
323
+ collectOptionalBooleanErrors(ssh, "passwordFallback", errors);
324
+ collectOptionalNumberErrors({
325
+ errors,
326
+ key: "reconnectTimeout",
327
+ object: ssh,
328
+ options: { positive: true }
329
+ });
330
+ collectOptionalNumberErrors({
331
+ errors,
332
+ key: "maxReconnectAttempts",
333
+ object: ssh,
334
+ options: { integer: true, positive: true }
335
+ });
336
+ collectStrictHostKeyCheckingErrors(ssh, errors);
337
+ }
338
+ function collectSshConfigErrors(value) {
339
+ const errors = [];
340
+ if (value === null) {
341
+ errors.push("Invalid property 'ssh' (expected object, got null)");
342
+ return errors;
343
+ }
344
+ if (typeof value !== "object") {
345
+ errors.push(`Invalid property 'ssh' (expected object, got ${describeType(value)})`);
346
+ return errors;
347
+ }
348
+ if (!isRecord(value)) {
349
+ errors.push("Invalid property 'ssh' (expected object, got object)");
350
+ return errors;
351
+ }
352
+ const ssh = value;
353
+ collectPortsErrors(ssh, errors);
354
+ collectRequiredUserErrors(ssh, errors);
355
+ collectOptionalSshFieldErrors(ssh, errors);
356
+ return errors;
357
+ }
358
+ function normalizeServerDefinitionSshError(error) {
359
+ const mappedErrors = {
360
+ "Missing property 'ssh.user' (expected string)": "ssh.user is required",
361
+ "Property 'ssh.expectedHostFingerprint' must not be an empty string": "ssh.expectedHostFingerprint must not be an empty string",
362
+ "Property 'ssh.expectedHostPublicKey' must not be an empty string": "ssh.expectedHostPublicKey must not be an empty string",
363
+ "Property 'ssh.ports' must not be empty": "ssh.ports must not be empty",
364
+ "Property 'ssh.privateKey' must not be an empty string": "ssh.privateKey must not be an empty string",
365
+ "Property 'ssh.user' must not be an empty string": "ssh.user is required",
366
+ [STRICT_HOST_KEY_ERROR]: "ssh.strictHostKeyChecking must be one of accept-new, no, yes"
367
+ };
368
+ return mappedErrors[error] ?? error;
369
+ }
370
+ function validateSshConfig(ssh) {
371
+ const errors = collectSshConfigErrors(ssh);
372
+ if (errors.length === 0) return;
373
+ throw new Error(`ServerDefinition: ${normalizeServerDefinitionSshError(errors[0])}`);
374
+ }
375
+
376
+ // src/meta.ts
377
+ var SYSTEM_HOST_KIND = "system.host";
378
+ var SYSTEM_REBOOT_KIND = "system.reboot";
379
+ function hasValidMetaName(name) {
380
+ return typeof name === "string" && name.length > 0;
381
+ }
382
+ function inferMetaValueType(value, explicitValueType) {
383
+ if (explicitValueType != null) return explicitValueType;
384
+ if (typeof value === "boolean") return "boolean";
385
+ if (typeof value === "number") return "number";
386
+ return "string";
387
+ }
388
+ function normalizeMetaValueResolver(value) {
389
+ if (typeof value === "function") {
390
+ return async () => {
391
+ await Promise.resolve();
392
+ return value();
393
+ };
394
+ }
395
+ return async () => {
396
+ await Promise.resolve();
397
+ return value;
398
+ };
399
+ }
400
+ function isRecord2(value) {
401
+ return typeof value === "object" && value !== null;
402
+ }
403
+ function assertValidEnvironmentMetaEntry(candidate) {
404
+ if (!hasValidMetaName(candidate.name)) {
405
+ throw new TypeError("Invalid env meta entry: name must be a non-empty string");
406
+ }
407
+ if (typeof candidate.resolve !== "function") {
408
+ throw new TypeError("Invalid env meta entry: resolve must be a function returning a Promise");
409
+ }
410
+ if (candidate.valueType !== "boolean" && candidate.valueType !== "number" && candidate.valueType !== "string") {
411
+ throw new TypeError("Invalid env meta entry: valueType must be boolean, number, or string");
412
+ }
413
+ }
414
+ function assertValidSshdPortMetaEntry(candidate) {
415
+ if (!isValidTcpPort(candidate.port)) {
416
+ throw new TypeError("Invalid sshd.port meta entry: port must be an integer between 1 and 65535");
417
+ }
418
+ }
419
+ function assertValidSystemHostMetaEntry(candidate) {
420
+ if (typeof candidate.host !== "string" || candidate.host.length === 0) {
421
+ throw new TypeError("Invalid system.host meta entry: host must be a non-empty string");
422
+ }
423
+ }
424
+ function environmentMeta(name, value, valueType) {
425
+ if (!hasValidMetaName(name)) {
426
+ throw new TypeError("Meta env entry name must be a non-empty string");
427
+ }
428
+ return {
429
+ kind: "env",
430
+ name,
431
+ resolve: normalizeMetaValueResolver(value),
432
+ valueType: inferMetaValueType(value, valueType)
433
+ };
434
+ }
435
+ function sshdPortMeta(port) {
436
+ if (!isValidTcpPort(port)) {
437
+ throw new TypeError("Meta entry sshd.port requires an integer port between 1 and 65535");
438
+ }
439
+ return { kind: "sshd.port", port };
440
+ }
441
+ function systemHostMeta(host) {
442
+ if (host.length === 0) {
443
+ throw new TypeError("Meta entry system.host requires a non-empty host");
444
+ }
445
+ return { host, kind: SYSTEM_HOST_KIND };
446
+ }
447
+ function systemRebootMeta() {
448
+ return { kind: SYSTEM_REBOOT_KIND };
449
+ }
450
+ var meta = {
451
+ env: environmentMeta,
452
+ sshdPort: sshdPortMeta,
453
+ systemHost: systemHostMeta,
454
+ systemReboot: systemRebootMeta
455
+ };
456
+ function isEnvironmentMetaEntry(entry) {
457
+ return entry.kind === "env";
458
+ }
459
+ function isStringEnvironmentMetaEntry(entry) {
460
+ return isEnvironmentMetaEntry(entry) && entry.valueType === "string";
461
+ }
462
+ function isNumberEnvironmentMetaEntry(entry) {
463
+ return isEnvironmentMetaEntry(entry) && entry.valueType === "number";
464
+ }
465
+ function isBooleanEnvironmentMetaEntry(entry) {
466
+ return isEnvironmentMetaEntry(entry) && entry.valueType === "boolean";
467
+ }
468
+ function isLazyEnvironmentMetaEntry(entry) {
469
+ return isEnvironmentMetaEntry(entry);
470
+ }
471
+ function isSshdPortMetaEntry(entry) {
472
+ return entry.kind === "sshd.port";
473
+ }
474
+ function isSystemHostMetaEntry(entry) {
475
+ return entry.kind === SYSTEM_HOST_KIND;
476
+ }
477
+ function isSystemRebootMetaEntry(entry) {
478
+ return entry.kind === SYSTEM_REBOOT_KIND;
479
+ }
480
+ function assertValidModuleMetaEntry(entry) {
481
+ if (!isRecord2(entry)) {
482
+ throw new TypeError(`Invalid meta entry: expected object, got ${typeof entry}`);
483
+ }
484
+ switch (entry.kind) {
485
+ case "env": {
486
+ assertValidEnvironmentMetaEntry(entry);
487
+ return;
488
+ }
489
+ case "sshd.port": {
490
+ assertValidSshdPortMetaEntry(entry);
491
+ return;
492
+ }
493
+ case SYSTEM_HOST_KIND: {
494
+ assertValidSystemHostMetaEntry(entry);
495
+ return;
496
+ }
497
+ case SYSTEM_REBOOT_KIND: {
498
+ return;
499
+ }
500
+ default: {
501
+ throw new TypeError(`Invalid meta entry kind: ${String(entry.kind)}`);
502
+ }
503
+ }
504
+ }
505
+ function assertValidModuleMetaEntries(entries) {
506
+ if (entries == null) return;
507
+ for (const entry of entries) {
508
+ assertValidModuleMetaEntry(entry);
509
+ }
510
+ }
511
+ async function mergeEnvironmentFromMeta(environment, entries) {
512
+ if (entries == null || entries.length === 0) {
513
+ await Promise.resolve();
514
+ return environment;
515
+ }
516
+ const nextEnvironment = { ...environment };
517
+ for (const entry of entries) {
518
+ if (!isEnvironmentMetaEntry(entry)) continue;
519
+ nextEnvironment[entry.name] = async () => entry.resolve();
520
+ }
521
+ await Promise.resolve();
522
+ return nextEnvironment;
523
+ }
524
+ function environmentToMetaEntries(environment) {
525
+ return Object.entries(environment).map(([name, value]) => environmentMeta(name, value));
526
+ }
527
+ function diffEnvironmentToMetaEntries(original, current) {
528
+ const entries = [];
529
+ for (const key of Object.keys(current)) {
530
+ if (!(key in original) || current[key] !== original[key]) {
531
+ entries.push(environmentMeta(key, current[key]));
532
+ }
533
+ }
534
+ return entries.length === 0 ? void 0 : entries;
535
+ }
536
+
537
+ // src/ssh.ts
538
+ import { randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
539
+ import { unlinkSync as unlinkSync2, writeFileSync } from "fs";
540
+ import { readFile, stat } from "fs/promises";
541
+ import { homedir as homedir2, tmpdir } from "os";
542
+ import { join as join3, posix } from "path";
543
+ import { Client } from "ssh2";
544
+
545
+ // src/knownHosts.ts
546
+ import { createHash, createHmac, timingSafeEqual } from "crypto";
547
+ import { readFileSync } from "fs";
548
+ import { appendFile, mkdir } from "fs/promises";
549
+ import { homedir } from "os";
550
+ import { join } from "path";
551
+ var HostKeyVerificationError = class _HostKeyVerificationError extends Error {
552
+ constructor(message) {
553
+ super(message);
554
+ this.name = "HostKeyVerificationError";
555
+ Error.captureStackTrace(this, _HostKeyVerificationError);
556
+ }
557
+ };
558
+ var MIN_KNOWN_HOSTS_FIELDS = 3;
559
+ var DEFAULT_SSH_PORT = 22;
560
+ var UINT32_SIZE = 4;
561
+ var HASHED_HOST_PARTS = 4;
562
+ var inMemoryHostKeys = /* @__PURE__ */ new Map();
563
+ function parseKnownHostsLine(line) {
564
+ const parts = line.split(new RegExp("\\s+", "v"));
565
+ const offset = parts[0]?.startsWith("@") ? 1 : 0;
566
+ if (parts.length < MIN_KNOWN_HOSTS_FIELDS + offset) return [];
567
+ const marker = offset === 1 ? parts[0] : void 0;
568
+ const hostsPart = parts[offset];
569
+ const algo = parts[offset + 1];
570
+ const base64Key = parts[offset + 2];
571
+ const key = Buffer.from(base64Key, "base64");
572
+ return hostsPart.split(",").map((host) => ({ algo, host, key, marker }));
573
+ }
574
+ function parseKnownHosts(content) {
575
+ const entries = [];
576
+ for (const raw of content.split("\n")) {
577
+ const line = raw.trim();
578
+ if (line.length === 0 || line.startsWith("#")) continue;
579
+ entries.push(...parseKnownHostsLine(line));
580
+ }
581
+ return entries;
582
+ }
583
+ function formatHostNeedle(host, port) {
584
+ return port === DEFAULT_SSH_PORT ? host : `[${host}]:${port}`;
585
+ }
586
+ function matchesHashedHost(pattern, needle) {
587
+ if (!pattern.startsWith("|1|")) return false;
588
+ const parts = pattern.split("|");
589
+ if (parts.length !== HASHED_HOST_PARTS || parts[1] !== "1") return false;
590
+ const salt = Buffer.from(parts[2] ?? "", "base64");
591
+ const expectedHash = Buffer.from(parts[3] ?? "", "base64");
592
+ if (salt.length === 0 || expectedHash.length === 0) return false;
593
+ const actualHash = createHmac("sha1", salt).update(needle).digest();
594
+ return actualHash.length === expectedHash.length && timingSafeEqual(actualHash, expectedHash);
595
+ }
596
+ function findMatchingEntries(entries, host, port) {
597
+ const needle = formatHostNeedle(host, port);
598
+ return entries.filter((entry) => entry.host === needle || matchesHashedHost(entry.host, needle));
599
+ }
600
+ function findRevokedEntry(entries, key) {
601
+ return entries.find(
602
+ (entry) => entry.marker === "@revoked" && entry.key.length === key.length && timingSafeEqual(entry.key, key)
603
+ );
604
+ }
605
+ function throwHostKeyMismatch(host, presentedKey, existingKey) {
606
+ const presentedAlgo = extractAlgoFromKey(presentedKey);
607
+ const knownHostsDetails = existingKey == null ? "remote host key does not match the key in known_hosts. " : `remote host key (${presentedAlgo}) does not match the key in known_hosts (${extractAlgoFromKey(existingKey)}). `;
608
+ throw new HostKeyVerificationError(
609
+ `HOST KEY VERIFICATION FAILED for ${host}: ${knownHostsDetails}This could indicate a man-in-the-middle attack.`
610
+ );
611
+ }
612
+ function verifyHostKeyAgainstKnownEntries(parameters) {
613
+ const { cachedKey, fileEntries, host, key } = parameters;
614
+ const revokedKey = findRevokedEntry(fileEntries, key);
615
+ if (revokedKey != null) {
616
+ throw new HostKeyVerificationError(
617
+ `HOST KEY VERIFICATION FAILED for ${host}: remote host key (${extractAlgoFromKey(revokedKey.key)}) is marked as revoked in known_hosts.`
618
+ );
619
+ }
620
+ const nonRevokedEntries = fileEntries.filter((entry) => entry.marker !== "@revoked");
621
+ const matchingEntry = nonRevokedEntries.find(
622
+ (entry) => entry.key.length === key.length && timingSafeEqual(entry.key, key)
623
+ );
624
+ if (matchingEntry != null) return true;
625
+ if (cachedKey?.length === key.length && timingSafeEqual(cachedKey, key)) {
626
+ return true;
627
+ }
628
+ const firstNonRevokedKey = nonRevokedEntries.at(0)?.key;
629
+ if (firstNonRevokedKey != null) throwHostKeyMismatch(host, key, firstNonRevokedKey);
630
+ if (cachedKey != null) throwHostKeyMismatch(host, key, cachedKey);
631
+ return false;
632
+ }
633
+ function extractAlgoFromKey(keyBuffer) {
634
+ if (keyBuffer.length < UINT32_SIZE) {
635
+ throw new Error("Invalid SSH key buffer: too short to contain algorithm length");
636
+ }
637
+ const algoLength = keyBuffer.readUInt32BE(0);
638
+ if (algoLength === 0 || UINT32_SIZE + algoLength > keyBuffer.length) {
639
+ throw new Error("Invalid SSH key buffer: algorithm length exceeds buffer size");
640
+ }
641
+ return keyBuffer.subarray(UINT32_SIZE, UINT32_SIZE + algoLength).toString("ascii");
642
+ }
643
+ function computeFingerprint(key) {
644
+ const hash = createHash("sha256").update(key).digest("base64");
645
+ return `SHA256:${hash.replaceAll("=", "")}`;
646
+ }
647
+ async function appendHostKey(host, port, keyBuffer) {
648
+ const hostLabel = formatHostNeedle(host, port);
649
+ const algo = extractAlgoFromKey(keyBuffer);
650
+ const base64Key = keyBuffer.toString("base64");
651
+ const line = `${hostLabel} ${algo} ${base64Key}
652
+ `;
653
+ const sshDirectory = join(homedir(), ".ssh");
654
+ const filePath = join(sshDirectory, "known_hosts");
655
+ await mkdir(sshDirectory, { mode: 448, recursive: true });
656
+ await appendFile(filePath, line, { mode: 420 });
657
+ }
658
+ function loadKnownHostEntries() {
659
+ const filePath = join(homedir(), ".ssh", "known_hosts");
660
+ let content = "";
661
+ try {
662
+ content = readFileSync(filePath, "utf8");
663
+ } catch {
664
+ }
665
+ return parseKnownHosts(content);
666
+ }
667
+ async function acceptAndPersistHostKey(host, port, key) {
668
+ try {
669
+ const algo = extractAlgoFromKey(key);
670
+ const fingerprint = computeFingerprint(key);
671
+ process.stderr.write(
672
+ `WARNING: Permanently added '${host}' (${algo}) to the list of known hosts. Fingerprint: ${fingerprint}
673
+ `
674
+ );
675
+ } catch {
676
+ process.stderr.write(`WARNING: Permanently added '${host}' to the list of known hosts.
677
+ `);
678
+ }
679
+ inMemoryHostKeys.set(formatHostNeedle(host, port), key);
680
+ try {
681
+ await appendHostKey(host, port, key);
682
+ } catch (error) {
683
+ const keyscanArguments = port === DEFAULT_SSH_PORT ? shellQuote(host) : `-p ${port} ${shellQuote(host)}`;
684
+ process.stderr.write(
685
+ `WARNING: Could not persist host key for ${host} \u2014 the key is cached in memory for this session. To persist it, ensure ~/.ssh/ is writable or run: ssh-keyscan ${keyscanArguments} >> ~/.ssh/known_hosts. ${String(error)}
686
+ `
687
+ );
688
+ }
689
+ }
690
+ function normalizePinnedPublicKey(publicKey) {
691
+ const parts = publicKey.trim().split(new RegExp("\\s+", "v"));
692
+ if (parts.length < 2) {
693
+ throw new Error("Expected host public key must use the format '<algorithm> <base64>'");
694
+ }
695
+ const [algorithm, key] = parts;
696
+ return `${algorithm} ${key}`;
697
+ }
698
+ function formatPresentedPublicKey(key) {
699
+ return `${extractAlgoFromKey(key)} ${key.toString("base64")}`;
700
+ }
701
+ function hasPinnedHostTrustAnchor(options) {
702
+ return options?.expectedHostFingerprint != null || options?.expectedHostPublicKey != null;
703
+ }
704
+ function verifyPinnedHostKey(host, key, options) {
705
+ const normalizedExpectedPublicKey = options.expectedHostPublicKey == null ? null : normalizePinnedPublicKey(options.expectedHostPublicKey);
706
+ const expectedFingerprint = options.expectedHostFingerprint ?? null;
707
+ const presentedPublicKey = formatPresentedPublicKey(key);
708
+ const presentedFingerprint = computeFingerprint(key);
709
+ if (normalizedExpectedPublicKey === presentedPublicKey || expectedFingerprint === presentedFingerprint) {
710
+ return;
711
+ }
712
+ throw new HostKeyVerificationError(
713
+ `HOST KEY VERIFICATION FAILED for ${host}: the remote host key does not match the configured trust anchor.`
714
+ );
715
+ }
716
+ function buildHostVerifier(mode, location, options = {}) {
717
+ const { host, port } = location;
718
+ if (mode === "no" && !hasPinnedHostTrustAnchor(options)) return {};
719
+ const entries = loadKnownHostEntries();
720
+ const fileEntries = findMatchingEntries(entries, host, port);
721
+ const cachedKey = inMemoryHostKeys.get(formatHostNeedle(host, port)) ?? null;
722
+ const result = {
723
+ hostVerifier(key) {
724
+ if (mode !== "no" && verifyHostKeyAgainstKnownEntries({ cachedKey, fileEntries, host, key })) {
725
+ if (hasPinnedHostTrustAnchor(options)) verifyPinnedHostKey(host, key, options);
726
+ return true;
727
+ }
728
+ if (hasPinnedHostTrustAnchor(options)) {
729
+ verifyPinnedHostKey(host, key, options);
730
+ return true;
731
+ }
732
+ if (mode === "yes") {
733
+ throw new HostKeyVerificationError(
734
+ `Host key for ${host} not found in known_hosts. Set strictHostKeyChecking to "accept-new" for explicit TOFU or configure ssh.expectedHostFingerprint / ssh.expectedHostPublicKey.`
735
+ );
736
+ }
737
+ if (mode === "no") return true;
738
+ result.pendingPersist = acceptAndPersistHostKey(host, port, key);
739
+ return true;
740
+ }
741
+ };
742
+ return result;
743
+ }
744
+
745
+ // src/sftp.ts
746
+ import { randomUUID } from "crypto";
747
+ import { createReadStream, createWriteStream, renameSync, unlinkSync } from "fs";
748
+ import { dirname, join as join2 } from "path";
749
+ var SFTP_TIMEOUT = 12e4;
750
+ function wireStreams(options) {
751
+ const { readStream, reject, resolve, sftp, timeout, timeoutMessage, writeStream } = options;
752
+ let settled = false;
753
+ const timer = setTimeout(() => {
754
+ if (settled) return;
755
+ settled = true;
756
+ readStream.destroy();
757
+ writeStream.destroy();
758
+ sftp.end();
759
+ reject(new Error(timeoutMessage));
760
+ }, timeout);
761
+ writeStream.on("close", () => {
762
+ clearTimeout(timer);
763
+ if (settled) return;
764
+ settled = true;
765
+ sftp.end();
766
+ resolve();
767
+ });
768
+ writeStream.on("error", (writeError) => {
769
+ clearTimeout(timer);
770
+ if (settled) return;
771
+ settled = true;
772
+ readStream.destroy();
773
+ if (typeof writeStream.destroy === "function") writeStream.destroy();
774
+ sftp.end();
775
+ reject(writeError);
776
+ });
777
+ readStream.on("error", (readError) => {
778
+ clearTimeout(timer);
779
+ if (settled) return;
780
+ settled = true;
781
+ if (typeof readStream.destroy === "function") readStream.destroy();
782
+ if (typeof writeStream.destroy === "function") writeStream.destroy();
783
+ sftp.end();
784
+ reject(readError);
785
+ });
786
+ readStream.pipe(writeStream);
787
+ }
788
+ async function sftpDownload(client, remotePath, localPath, timeout = SFTP_TIMEOUT) {
789
+ return new Promise((resolve, reject) => {
790
+ const temporaryPath = join2(dirname(localPath), `.paratix-download-${randomUUID()}.tmp`);
791
+ let shouldCleanupTemporaryFile = false;
792
+ const rejectWithCleanup = (reason) => {
793
+ if (shouldCleanupTemporaryFile) {
794
+ try {
795
+ unlinkSync(temporaryPath);
796
+ } catch {
797
+ }
798
+ }
799
+ reject(reason);
800
+ };
801
+ client.sftp((error, sftp) => {
802
+ if (error) {
803
+ rejectWithCleanup(error);
804
+ return;
805
+ }
806
+ const readStream = sftp.createReadStream(remotePath);
807
+ const writeStream = createWriteStream(temporaryPath, { mode: 384 });
808
+ shouldCleanupTemporaryFile = true;
809
+ wireStreams({
810
+ readStream,
811
+ reject: rejectWithCleanup,
812
+ resolve: () => {
813
+ try {
814
+ renameSync(temporaryPath, localPath);
815
+ resolve();
816
+ } catch (finalizeError) {
817
+ rejectWithCleanup(
818
+ finalizeError instanceof Error ? finalizeError : new Error(`Failed to finalize SFTP download: ${String(finalizeError)}`)
819
+ );
820
+ }
821
+ },
822
+ sftp,
823
+ timeout,
824
+ timeoutMessage: `SFTP download timed out after ${timeout}ms: ${remotePath}`,
825
+ writeStream
826
+ });
827
+ });
828
+ });
829
+ }
830
+ async function sftpUpload(client, localPath, remotePath, timeout = SFTP_TIMEOUT) {
831
+ return new Promise((resolve, reject) => {
832
+ client.sftp((error, sftp) => {
833
+ if (error) {
834
+ reject(error);
835
+ return;
836
+ }
837
+ const readStream = createReadStream(localPath);
838
+ const writeStream = sftp.createWriteStream(remotePath, { mode: 384 });
839
+ wireStreams({
840
+ readStream,
841
+ reject,
842
+ resolve,
843
+ sftp,
844
+ timeout,
845
+ timeoutMessage: `SFTP upload timed out after ${timeout}ms: ${remotePath}`,
846
+ writeStream
847
+ });
848
+ });
849
+ });
850
+ }
851
+
852
+ // src/terminal.ts
853
+ import { createInterface } from "readline";
854
+ function normalizePromptAbortReason(reason) {
855
+ return reason instanceof Error ? reason : new Error(String(reason));
856
+ }
857
+ function installHiddenPromptOutput(rl, question) {
858
+ const rlInternal = rl;
859
+ rlInternal._writeToOutput = (text) => {
860
+ if (text.includes(question)) {
861
+ rlInternal.output.write(text);
862
+ }
863
+ };
864
+ }
865
+ function createPromptAbortHandler(parameters) {
866
+ return () => {
867
+ if (parameters.settled()) return;
868
+ parameters.setSettled();
869
+ parameters.cleanup();
870
+ parameters.rl.close();
871
+ if (parameters.hidden) process.stderr.write("\n");
872
+ parameters.reject(
873
+ normalizePromptAbortReason(
874
+ parameters.abortSignal?.reason ?? new Error("Terminal prompt aborted")
875
+ )
876
+ );
877
+ };
878
+ }
879
+ async function promptTerminal(question, hidden = false, options) {
880
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
881
+ const abortSignal = options?.abortSignal;
882
+ if (hidden) installHiddenPromptOutput(rl, question);
883
+ return new Promise((resolve, reject) => {
884
+ let settled = false;
885
+ const cleanup = () => {
886
+ abortSignal?.removeEventListener("abort", rejectPrompt);
887
+ };
888
+ const resolvePrompt = (answer) => {
889
+ if (settled) return;
890
+ settled = true;
891
+ cleanup();
892
+ rl.close();
893
+ if (hidden) process.stderr.write("\n");
894
+ resolve(answer);
895
+ };
896
+ const rejectPrompt = createPromptAbortHandler({
897
+ abortSignal,
898
+ cleanup,
899
+ hidden,
900
+ reject,
901
+ rl,
902
+ setSettled: () => {
903
+ settled = true;
904
+ },
905
+ settled: () => settled
906
+ });
907
+ if (abortSignal?.aborted === true) {
908
+ rejectPrompt();
909
+ return;
910
+ }
911
+ abortSignal?.addEventListener("abort", rejectPrompt, { once: true });
912
+ rl.question(question, (answer) => {
913
+ resolvePrompt(answer);
914
+ });
915
+ });
916
+ }
917
+
918
+ // src/ssh.ts
919
+ function validateMktempPath(directory, path, prefix) {
920
+ const normalizedDirectory = directory === "/" ? "" : directory;
921
+ const expectedPrefix = `${normalizedDirectory}/${prefix}.`;
922
+ if (!path.startsWith(expectedPrefix) || path.includes("\n") || path.endsWith("/")) {
923
+ throw new Error(`Unexpected mktemp output: ${path}`);
924
+ }
925
+ return path;
926
+ }
927
+ function expandHomePath(path) {
928
+ if (path === "~") return homedir2();
929
+ if (path.startsWith("~/")) return join3(homedir2(), path.slice(2));
930
+ return path;
931
+ }
932
+ function resolveWriteFileMode(remotePath, options) {
933
+ if (options?.mode == null) {
934
+ throw new Error(
935
+ `[ssh.writeFile: ${remotePath}] missing options.mode; pass { mode: "0644" } or another explicit file mode`
936
+ );
937
+ }
938
+ try {
939
+ validateMode(options.mode);
940
+ } catch (error) {
941
+ const reason = error instanceof Error ? error.message : String(error);
942
+ throw new Error(
943
+ `[ssh.writeFile: ${remotePath}] invalid options.mode "${options.mode}": ${reason}`,
944
+ { cause: error }
945
+ );
946
+ }
947
+ return options.mode;
948
+ }
949
+ var COMMAND_TIMEOUT = 12e4;
950
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
951
+ var DEFAULT_RECONNECT_TIMEOUT = 12e4;
952
+ var JITTER_BASE = 0.75;
953
+ var JITTER_RANGE = 0.5;
954
+ var RECONNECT_BASE_DELAY = 1e3;
955
+ var RECONNECT_MAX_DELAY = 3e4;
956
+ var SshConnectionImpl = class {
957
+ agentSocket = null;
958
+ authMethod = null;
959
+ /**
960
+ * Cached sudo password stored as a Buffer so it can be actively zeroed
961
+ * after use via `buffer.fill(0)`.
962
+ *
963
+ * **Limitations:** Buffer zeroing in JavaScript/V8 only reduces the window
964
+ * for potential memory leaks — it cannot eliminate them entirely. The GC may
965
+ * create internal copies. The masking pipeline only materializes a string
966
+ * from this buffer on actual output/error paths. This is a best-effort
967
+ * mitigation, not a guarantee.
968
+ */
969
+ cachedSudoPassword = null;
970
+ client = null;
971
+ config;
972
+ connectedPort = 0;
973
+ pendingRejects = /* @__PURE__ */ new Set();
974
+ pinnedHostKey = null;
975
+ promptAbortSignal;
976
+ runtime;
977
+ sudoProbePromise = null;
978
+ sudoReady = false;
979
+ verifiedHostKey = null;
980
+ constructor(host, config) {
981
+ this.runtime = {
982
+ host,
983
+ ports: [...config.ports]
984
+ };
985
+ this.config = {
986
+ ...config,
987
+ ports: [...config.ports]
988
+ };
989
+ if (config.sudoPassword != null && (config.sudoPassword.includes("\n") || config.sudoPassword.includes("\r"))) {
990
+ throw new Error("Sudo password must not contain newline characters");
991
+ }
992
+ this.cachedSudoPassword = config.sudoPassword == null ? null : Buffer.from(config.sudoPassword);
993
+ }
994
+ addPort(port) {
995
+ if (!this.runtime.ports.includes(port)) this.runtime.ports.push(port);
996
+ }
997
+ /**
998
+ * Establish the SSH connection using the configured credentials.
999
+ *
1000
+ * Authentication strategy (in order):
1001
+ * 1. If `privateKey` is set: connect with the key, optionally falling back to
1002
+ * password authentication when `passwordFallback` is enabled.
1003
+ * 2. If `privateKey` is omitted: connect via the SSH agent identified by
1004
+ * `SSH_AUTH_SOCK`, optionally falling back to password authentication
1005
+ * when `passwordFallback` is enabled. Throws if the environment variable
1006
+ * is not set.
1007
+ *
1008
+ * @param options - Optional prompt behavior for interactive password fallback.
1009
+ * @throws {Error} When no port in `config.ports` accepts the connection.
1010
+ */
1011
+ async connect(options) {
1012
+ this.promptAbortSignal = options?.abortSignal;
1013
+ if (this.config.privateKey == null) {
1014
+ await this.connectViaAgent(options);
1015
+ return;
1016
+ }
1017
+ await this.connectViaPrivateKey(options);
1018
+ }
1019
+ disconnect() {
1020
+ this.clearCachedPassword();
1021
+ this.disconnectTransport();
1022
+ }
1023
+ async downloadFile(remotePath, localPath) {
1024
+ const client = this.ensureClient();
1025
+ let sourcePath = remotePath;
1026
+ try {
1027
+ if (this.config.user !== "root") {
1028
+ sourcePath = await this.createRemoteTempPath(
1029
+ "mktemp /tmp/paratix-download.XXXXXX",
1030
+ "paratix-download"
1031
+ );
1032
+ await this.exec(`cat ${shellQuote(remotePath)} > ${shellQuote(sourcePath)}`, {
1033
+ silent: true
1034
+ });
1035
+ }
1036
+ await sftpDownload(client, sourcePath, localPath);
1037
+ } finally {
1038
+ if (sourcePath !== remotePath) {
1039
+ try {
1040
+ await this.cleanupRemoteTempFile(sourcePath);
1041
+ } catch (cleanupError) {
1042
+ process.stderr.write(
1043
+ `Warning: failed to remove temp file ${sourcePath}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
1044
+ `
1045
+ );
1046
+ }
1047
+ }
1048
+ }
1049
+ }
1050
+ async exec(command, options = {}) {
1051
+ await this.ensureSudoReady();
1052
+ return this.execPrepared(command, options);
1053
+ }
1054
+ async exists(remotePath) {
1055
+ return this.test(`[ -e ${shellQuote(remotePath)} ]`);
1056
+ }
1057
+ getConnectionInfo() {
1058
+ return {
1059
+ agentSocket: this.authMethod === "agent" ? this.agentSocket ?? void 0 : void 0,
1060
+ authMethod: this.authMethod ?? void 0,
1061
+ host: this.runtime.host,
1062
+ port: this.connectedPort,
1063
+ privateKeyPath: this.authMethod === "privateKey" && this.config.privateKey != null ? expandHomePath(this.config.privateKey) : void 0,
1064
+ user: this.config.user,
1065
+ verifiedHostPublicKey: this.verifiedHostKey == null ? void 0 : `${extractAlgoFromKey(this.verifiedHostKey)} ${this.verifiedHostKey.toString("base64")}`
1066
+ };
1067
+ }
1068
+ async lines(command) {
1069
+ const out = await this.output(command);
1070
+ return out === "" ? [] : out.split("\n");
1071
+ }
1072
+ async output(command) {
1073
+ const result = await this.exec(command, { silent: true });
1074
+ return result.stdout.trim();
1075
+ }
1076
+ /**
1077
+ * Probe whether passwordless sudo is available. If not, prompt the user
1078
+ * for a password and cache it for the remainder of the run.
1079
+ *
1080
+ * @param options - Optional prompt behavior for the interactive sudo password prompt.
1081
+ */
1082
+ async probeSudo(options) {
1083
+ this.promptAbortSignal = options?.abortSignal ?? this.promptAbortSignal;
1084
+ if (this.isSudoReadyWithoutProbe()) {
1085
+ this.sudoReady = true;
1086
+ return;
1087
+ }
1088
+ await this.ensureSudoInstalled();
1089
+ if (await this.hasPasswordlessSudo()) {
1090
+ this.sudoReady = true;
1091
+ return;
1092
+ }
1093
+ await this.promptAndCacheSudoPassword();
1094
+ }
1095
+ async readFile(remotePath) {
1096
+ return this.output(`cat ${shellQuote(remotePath)}`);
1097
+ }
1098
+ async reconnect() {
1099
+ const timeout = this.config.reconnectTimeout ?? DEFAULT_RECONNECT_TIMEOUT;
1100
+ const maxAttempts = this.config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
1101
+ const deadline = Date.now() + timeout;
1102
+ let attempt = 0;
1103
+ while (Date.now() < deadline && attempt < maxAttempts) {
1104
+ try {
1105
+ this.disconnectTransport();
1106
+ await this.connect();
1107
+ return;
1108
+ } catch (error) {
1109
+ if (error instanceof HostKeyVerificationError || error instanceof Error && error.message === "SSH connection closed")
1110
+ throw error;
1111
+ const jitter = Math.min(RECONNECT_BASE_DELAY * 2 ** attempt, RECONNECT_MAX_DELAY) * (JITTER_BASE + Math.random() * JITTER_RANGE);
1112
+ const delay = Math.min(jitter, Math.max(0, deadline - Date.now()));
1113
+ await new Promise((resolve) => {
1114
+ setTimeout(resolve, delay);
1115
+ });
1116
+ attempt++;
1117
+ }
1118
+ }
1119
+ throw new Error(
1120
+ `Failed to reconnect to ${this.runtime.host} after ${attempt} attempts (timeout: ${timeout}ms)`
1121
+ );
1122
+ }
1123
+ removePort(port) {
1124
+ this.runtime.ports = this.runtime.ports.filter((candidate) => candidate !== port);
1125
+ }
1126
+ async sha256(remotePath) {
1127
+ const exists = await this.test(`[ -f ${shellQuote(remotePath)} ]`);
1128
+ if (!exists) return null;
1129
+ const out = await this.output(`sha256sum ${shellQuote(remotePath)}`);
1130
+ return out.split(new RegExp("\\s+", "v"))[0] ?? null;
1131
+ }
1132
+ async test(command) {
1133
+ try {
1134
+ const result = await this.exec(command, { ignoreExitCode: true, silent: true });
1135
+ return result.code === 0;
1136
+ } catch {
1137
+ return false;
1138
+ }
1139
+ }
1140
+ updateHost(host) {
1141
+ this.runtime.host = host;
1142
+ }
1143
+ async uploadFile(localPath, remotePath, options) {
1144
+ const client = this.ensureClient();
1145
+ const temporaryPath = await this.createRemoteWritableTempPath(remotePath, "paratix-upload");
1146
+ const temporaryMode = options?.mode ?? "0600";
1147
+ try {
1148
+ await sftpUpload(client, localPath, temporaryPath);
1149
+ await this.setRemoteTempMode(temporaryPath, temporaryMode);
1150
+ await this.finalizeRemoteTempFile(temporaryPath, remotePath, temporaryMode);
1151
+ } finally {
1152
+ try {
1153
+ await this.cleanupRemoteTempFile(temporaryPath);
1154
+ } catch (cleanupError) {
1155
+ process.stderr.write(
1156
+ `Warning: failed to remove temp file ${temporaryPath}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
1157
+ `
1158
+ );
1159
+ }
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Write a string to a remote file atomically via write-to-temp + mv.
1164
+ *
1165
+ * The content is first written to a local temporary file, uploaded via SFTP
1166
+ * to a remote temporary file, then moved to the final destination with `mv`.
1167
+ * This ensures the target file is never left in a half-written state.
1168
+ *
1169
+ * @param remotePath - Destination path on the remote host.
1170
+ * @param content - The string content to write.
1171
+ * @param options - Settings for the remote write.
1172
+ * @param options.mode - File mode to set via `chmod` on the temp file before moving (e.g. `"0644"`).
1173
+ */
1174
+ async writeFile(remotePath, content, options) {
1175
+ const client = this.ensureClient();
1176
+ const localTemporary = join3(tmpdir(), `paratix-write-${randomUUID2()}`);
1177
+ const remoteTemporary = await this.createRemoteWritableTempPath(remotePath, "paratix-write");
1178
+ const temporaryMode = resolveWriteFileMode(
1179
+ remotePath,
1180
+ options
1181
+ );
1182
+ try {
1183
+ writeFileSync(localTemporary, content, { mode: 384 });
1184
+ await sftpUpload(client, localTemporary, remoteTemporary);
1185
+ await this.setRemoteTempMode(remoteTemporary, temporaryMode);
1186
+ await this.finalizeRemoteTempFile(remoteTemporary, remotePath, temporaryMode);
1187
+ } finally {
1188
+ try {
1189
+ unlinkSync2(localTemporary);
1190
+ } catch {
1191
+ }
1192
+ try {
1193
+ await this.cleanupRemoteTempFile(remoteTemporary);
1194
+ } catch (cleanupError) {
1195
+ process.stderr.write(
1196
+ `Warning: failed to remove temp file ${remoteTemporary}: ${maskSecrets(String(cleanupError), this.buildSecrets())}
1197
+ `
1198
+ );
1199
+ }
1200
+ }
1201
+ }
1202
+ buildEnvPrefix(environment) {
1203
+ if (environment == null) return "";
1204
+ for (const key of Object.keys(environment)) {
1205
+ if (!new RegExp("^[A-Za-z_]\\w*$", "v").test(key)) {
1206
+ throw new Error(`Invalid environment variable name: ${key}`);
1207
+ }
1208
+ }
1209
+ const pairs = Object.entries(environment).map(([k, v]) => `${k}=${shellQuote(v)}`);
1210
+ return `${pairs.join(" ")} `;
1211
+ }
1212
+ buildSecrets(extra) {
1213
+ const cachedPasswordSecret = this.cachedSudoPassword == null ? [] : [() => this.cachedSudoPassword?.toString("utf8") ?? ""];
1214
+ return [...cachedPasswordSecret, ...extra ?? []];
1215
+ }
1216
+ async cacheAndValidateSudoPassword(password) {
1217
+ if (password.includes("\n") || password.includes("\r")) {
1218
+ throw new Error("Sudo password must not contain newline characters");
1219
+ }
1220
+ this.cachedSudoPassword = Buffer.from(password);
1221
+ try {
1222
+ await this.execPrepared("true", { silent: true, timeout: 1e4 });
1223
+ this.sudoReady = true;
1224
+ } catch (error) {
1225
+ const masked = maskSecrets(String(error), [password]);
1226
+ this.clearCachedPassword();
1227
+ throw new Error(`Sudo authentication failed: ${masked}`, { cause: error });
1228
+ }
1229
+ }
1230
+ async cleanupRemoteTempFile(remotePath) {
1231
+ const cleanup = this.config.user === "root" ? this.exec(`rm -f ${shellQuote(remotePath)}`, { silent: true }) : this.execWithoutSudo(`rm -f ${shellQuote(remotePath)}`);
1232
+ await cleanup;
1233
+ }
1234
+ clearCachedPassword() {
1235
+ if (this.cachedSudoPassword != null) {
1236
+ this.cachedSudoPassword.fill(0);
1237
+ this.cachedSudoPassword = null;
1238
+ }
1239
+ }
1240
+ async connectViaAgent(options) {
1241
+ const agent = process.env.SSH_AUTH_SOCK;
1242
+ if (agent == null || agent.length === 0) {
1243
+ if (await this.tryPasswordFallback(options)) return;
1244
+ if (this.config.passwordFallback) {
1245
+ throw new Error(
1246
+ `Failed to connect to ${this.runtime.host} on ports: ${this.runtime.ports.join(", ")}`
1247
+ );
1248
+ }
1249
+ throw new Error("No privateKey configured and SSH_AUTH_SOCK is not set");
1250
+ }
1251
+ try {
1252
+ await stat(agent);
1253
+ } catch {
1254
+ throw new Error(`SSH_AUTH_SOCK points to non-existent path: ${agent}`);
1255
+ }
1256
+ if (await this.tryConnectOnPorts(void 0, void 0, agent)) {
1257
+ this.agentSocket = agent;
1258
+ this.authMethod = "agent";
1259
+ return;
1260
+ }
1261
+ if (await this.tryPasswordFallback(options, agent)) return;
1262
+ throw new Error(
1263
+ `Could not connect to ${this.runtime.host} via SSH agent on ports ${this.runtime.ports.join(", ")}`
1264
+ );
1265
+ }
1266
+ async connectViaPrivateKey(options) {
1267
+ const privateKeyPath = this.config.privateKey;
1268
+ if (privateKeyPath == null) {
1269
+ throw new Error("connectViaPrivateKey requires config.privateKey");
1270
+ }
1271
+ const privateKey = await readFile(expandHomePath(privateKeyPath));
1272
+ try {
1273
+ if (await this.tryConnectOnPorts(privateKey)) {
1274
+ this.authMethod = "privateKey";
1275
+ return;
1276
+ }
1277
+ if (this.config.passwordFallback) {
1278
+ const password = await promptTerminal(
1279
+ `Password for ${this.config.user}@${this.runtime.host}: `,
1280
+ true,
1281
+ options
1282
+ );
1283
+ if (await this.tryConnectOnPorts(privateKey, password)) {
1284
+ this.authMethod = "password";
1285
+ return;
1286
+ }
1287
+ }
1288
+ throw new Error(
1289
+ `Failed to connect to ${this.runtime.host} on ports: ${this.runtime.ports.join(", ")}`
1290
+ );
1291
+ } finally {
1292
+ privateKey.fill(0);
1293
+ }
1294
+ }
1295
+ async createRemoteTempPath(command, prefix) {
1296
+ const path = this.config.user === "root" ? await this.output(command) : await this.outputWithoutSudo(command);
1297
+ return validateMktempPath("/tmp", path, prefix);
1298
+ }
1299
+ async createRemoteTempPathInDestination(remotePath, prefix) {
1300
+ const directory = posix.dirname(remotePath);
1301
+ const template = `${directory}/${prefix}.XXXXXX`;
1302
+ const command = `mktemp ${shellQuote(template)}`;
1303
+ const path = this.config.user === "root" ? await this.output(command) : await this.outputWithoutSudo(command);
1304
+ return validateMktempPath(directory, path, prefix);
1305
+ }
1306
+ async createRemoteWritableTempPath(remotePath, prefix) {
1307
+ if (this.config.user === "root") {
1308
+ return this.createRemoteTempPathInDestination(remotePath, prefix);
1309
+ }
1310
+ return this.createRemoteTempPath(`mktemp '/tmp/${prefix}.XXXXXX'`, prefix);
1311
+ }
1312
+ createSettledCallbacks(resolve, reject) {
1313
+ let settled = false;
1314
+ const wrappedReject = (reason) => {
1315
+ if (settled) return;
1316
+ settled = true;
1317
+ this.pendingRejects.delete(wrappedReject);
1318
+ reject(reason);
1319
+ };
1320
+ const wrappedResolve = (value) => {
1321
+ if (settled) return;
1322
+ settled = true;
1323
+ this.pendingRejects.delete(wrappedReject);
1324
+ resolve(value);
1325
+ };
1326
+ this.pendingRejects.add(wrappedReject);
1327
+ return { wrappedReject, wrappedResolve };
1328
+ }
1329
+ /** Tear down the SSH transport without touching the cached sudo password. */
1330
+ disconnectTransport() {
1331
+ if (this.client) {
1332
+ this.client.end();
1333
+ this.client = null;
1334
+ }
1335
+ const error = new Error("SSH connection closed");
1336
+ for (const rejectFunction of this.pendingRejects) {
1337
+ rejectFunction(error);
1338
+ }
1339
+ this.pendingRejects.clear();
1340
+ }
1341
+ ensureClient() {
1342
+ if (!this.client) throw new Error("SSH not connected");
1343
+ return this.client;
1344
+ }
1345
+ /** Verify that `sudo` is available on the remote host. */
1346
+ async ensureSudoInstalled() {
1347
+ const result = await this.execRaw("command -v sudo");
1348
+ if (result.exitCode !== 0) {
1349
+ throw new Error("sudo is not installed on the remote host");
1350
+ }
1351
+ }
1352
+ async ensureSudoReady() {
1353
+ if (this.config.user === "root" || this.sudoReady) return;
1354
+ if (this.cachedSudoPassword != null) {
1355
+ this.sudoReady = true;
1356
+ return;
1357
+ }
1358
+ if (this.sudoProbePromise != null) {
1359
+ await this.sudoProbePromise;
1360
+ return;
1361
+ }
1362
+ this.sudoProbePromise = this.probeSudo({ abortSignal: this.promptAbortSignal }).finally(() => {
1363
+ this.sudoProbePromise = null;
1364
+ });
1365
+ await this.sudoProbePromise;
1366
+ }
1367
+ async execPrepared(command, options = {}) {
1368
+ const client = this.ensureClient();
1369
+ const environmentPrefix = this.buildEnvPrefix(options.env);
1370
+ const { command: cmd, needsPassword } = this.sudoCommand(command, environmentPrefix);
1371
+ return new Promise((resolve, reject) => {
1372
+ const { wrappedReject, wrappedResolve } = this.createSettledCallbacks(
1373
+ resolve,
1374
+ reject
1375
+ );
1376
+ const timeout = options.timeout ?? COMMAND_TIMEOUT;
1377
+ const secrets = this.buildSecrets(options.secrets);
1378
+ let activeStream = null;
1379
+ const timer = setTimeout(() => {
1380
+ activeStream?.close();
1381
+ wrappedReject(
1382
+ new Error(`Command timed out after ${timeout}ms: ${maskSecrets(command, secrets)}`)
1383
+ );
1384
+ }, timeout);
1385
+ client.exec(cmd, (error, stream) => {
1386
+ if (error) {
1387
+ clearTimeout(timer);
1388
+ wrappedReject(error);
1389
+ return;
1390
+ }
1391
+ activeStream = stream;
1392
+ collectStreamOutput({
1393
+ command,
1394
+ options,
1395
+ reject: wrappedReject,
1396
+ resolve: wrappedResolve,
1397
+ secrets,
1398
+ stream,
1399
+ timer
1400
+ });
1401
+ if (needsPassword && this.cachedSudoPassword != null) {
1402
+ this.writeSudoPassword(stream);
1403
+ }
1404
+ });
1405
+ });
1406
+ }
1407
+ /**
1408
+ * Execute a command directly over the SSH transport without sudo wrapping.
1409
+ * Used only by {@link probeSudo} to check whether `sudo` is installed.
1410
+ *
1411
+ * @param command - The raw shell command to run.
1412
+ * @returns The exit code and captured stdout.
1413
+ */
1414
+ async execRaw(command) {
1415
+ const client = this.ensureClient();
1416
+ return new Promise((resolve, reject) => {
1417
+ const { wrappedReject, wrappedResolve } = this.createSettledCallbacks(resolve, reject);
1418
+ let activeStream = null;
1419
+ const timer = setTimeout(() => {
1420
+ activeStream?.close();
1421
+ wrappedReject(new Error(`Command timed out after ${COMMAND_TIMEOUT}ms: ${command}`));
1422
+ }, COMMAND_TIMEOUT);
1423
+ client.exec(command, (error, stream) => {
1424
+ if (error) {
1425
+ clearTimeout(timer);
1426
+ wrappedReject(error);
1427
+ return;
1428
+ }
1429
+ activeStream = stream;
1430
+ const chunks = [];
1431
+ stream.on("data", (chunk) => {
1432
+ chunks.push(chunk);
1433
+ });
1434
+ stream.on("close", (code) => {
1435
+ clearTimeout(timer);
1436
+ wrappedResolve({
1437
+ exitCode: normalizeSshCloseCode(code),
1438
+ stdout: Buffer.concat(chunks).toString("utf8")
1439
+ });
1440
+ });
1441
+ stream.stderr.on("data", () => {
1442
+ });
1443
+ });
1444
+ });
1445
+ }
1446
+ async execWithoutSudo(command) {
1447
+ const result = await this.execRaw(command);
1448
+ if (result.exitCode !== 0) {
1449
+ throw new Error(`Command failed (exit code ${result.exitCode}): ${command}`);
1450
+ }
1451
+ }
1452
+ async finalizeRemoteTempFile(temporaryPath, remotePath, mode) {
1453
+ validateMode(mode);
1454
+ if (this.config.user === "root") {
1455
+ await this.exec(`mv ${shellQuote(temporaryPath)} ${shellQuote(remotePath)}`, {
1456
+ silent: true
1457
+ });
1458
+ return;
1459
+ }
1460
+ const directory = posix.dirname(remotePath);
1461
+ const basename = posix.basename(remotePath);
1462
+ const finalTemplate = `${directory}/.${basename}.paratix.XXXXXX`;
1463
+ const finalizeScript = `
1464
+ target_owner=$(stat -c '%u:%g' ${shellQuote(remotePath)} 2>/dev/null || printf '0:0')
1465
+ target_temp=''
1466
+ cleanup() {
1467
+ if [ -n "$target_temp" ]; then
1468
+ rm -f "$target_temp"
1469
+ fi
1470
+ }
1471
+ trap cleanup EXIT
1472
+ target_temp=$(mktemp ${shellQuote(finalTemplate)})
1473
+ mv ${shellQuote(temporaryPath)} "$target_temp"
1474
+ chmod ${shellQuote(mode)} "$target_temp"
1475
+ chown "$target_owner" "$target_temp"
1476
+ mv "$target_temp" ${shellQuote(remotePath)}
1477
+ trap - EXIT
1478
+ `;
1479
+ await this.exec(finalizeScript, { silent: true });
1480
+ }
1481
+ async hasPasswordlessSudo() {
1482
+ try {
1483
+ await this.execPrepared("true", { silent: true, timeout: 1e4 });
1484
+ return true;
1485
+ } catch {
1486
+ return false;
1487
+ }
1488
+ }
1489
+ isSudoReadyWithoutProbe() {
1490
+ return this.config.user === "root" || this.cachedSudoPassword != null;
1491
+ }
1492
+ async outputWithoutSudo(command) {
1493
+ const result = await this.execRaw(command);
1494
+ if (result.exitCode !== 0) {
1495
+ throw new Error(`Command failed (exit code ${result.exitCode}): ${command}`);
1496
+ }
1497
+ return result.stdout.trim();
1498
+ }
1499
+ async promptAndCacheSudoPassword() {
1500
+ const password = await promptTerminal(
1501
+ `[sudo] password for ${this.config.user}@${this.runtime.host}: `,
1502
+ true,
1503
+ { abortSignal: this.promptAbortSignal }
1504
+ );
1505
+ await this.cacheAndValidateSudoPassword(password);
1506
+ }
1507
+ async setRemoteTempMode(remotePath, mode) {
1508
+ validateMode(mode);
1509
+ const command = `chmod ${shellQuote(mode)} ${shellQuote(remotePath)}`;
1510
+ if (this.config.user === "root") {
1511
+ await this.exec(command, { silent: true });
1512
+ return;
1513
+ }
1514
+ await this.execWithoutSudo(command);
1515
+ }
1516
+ /**
1517
+ * Build the sudo-wrapped command string for execution.
1518
+ *
1519
+ * @param command - The raw command to execute.
1520
+ * @param environmentPrefix - Optional env var prefix string.
1521
+ * @returns An object with the final command and whether a password must be written to stdin.
1522
+ */
1523
+ sudoCommand(command, environmentPrefix = "") {
1524
+ if (this.config.user === "root") {
1525
+ return { command: `${environmentPrefix}${command}`, needsPassword: false };
1526
+ }
1527
+ const quoted = shellQuote(`${environmentPrefix}${command}`);
1528
+ if (this.cachedSudoPassword != null) {
1529
+ return { command: `SUDO_PROMPT='' sudo -S bash -c ${quoted}`, needsPassword: true };
1530
+ }
1531
+ return { command: `sudo bash -c ${quoted}`, needsPassword: false };
1532
+ }
1533
+ /**
1534
+ * Iterate over `config.ports` and attempt a connection on each one.
1535
+ *
1536
+ * @param privateKey - PEM-encoded private key content, or `undefined` when using agent auth.
1537
+ * @param password - Optional password for keyboard-interactive fallback.
1538
+ * @param agent - SSH agent socket path (e.g. `SSH_AUTH_SOCK`). Used when `privateKey` is absent.
1539
+ * @returns `true` if a port connected successfully, `false` if all ports failed.
1540
+ */
1541
+ async tryConnectOnPorts(privateKey, password, agent) {
1542
+ const mode = this.config.strictHostKeyChecking ?? "yes";
1543
+ for (const port of this.runtime.ports) {
1544
+ try {
1545
+ const client = new Client();
1546
+ const verifier = buildHostVerifier(
1547
+ mode,
1548
+ { host: this.runtime.host, port },
1549
+ {
1550
+ expectedHostFingerprint: this.config.expectedHostFingerprint,
1551
+ expectedHostPublicKey: this.config.expectedHostPublicKey
1552
+ }
1553
+ );
1554
+ const wrappedVerifier = this.wrapHostVerifier(verifier.hostVerifier);
1555
+ await tryConnectOnPort({
1556
+ agent,
1557
+ agentForward: this.config.agentForward,
1558
+ client,
1559
+ host: this.runtime.host,
1560
+ hostVerifier: wrappedVerifier,
1561
+ password,
1562
+ port,
1563
+ privateKey,
1564
+ username: this.config.user
1565
+ });
1566
+ if (verifier.pendingPersist != null) await verifier.pendingPersist;
1567
+ client.on("close", () => {
1568
+ const error = new Error("SSH connection closed unexpectedly");
1569
+ for (const rejectFunction of this.pendingRejects) {
1570
+ rejectFunction(error);
1571
+ }
1572
+ this.pendingRejects.clear();
1573
+ });
1574
+ this.client = client;
1575
+ this.connectedPort = port;
1576
+ return true;
1577
+ } catch (error) {
1578
+ if (error instanceof HostKeyVerificationError) throw error;
1579
+ }
1580
+ }
1581
+ return false;
1582
+ }
1583
+ async tryPasswordFallback(options, agent) {
1584
+ if (!this.config.passwordFallback) return false;
1585
+ const password = await promptTerminal(
1586
+ `Password for ${this.config.user}@${this.runtime.host}: `,
1587
+ true,
1588
+ options
1589
+ );
1590
+ if (!await this.tryConnectOnPorts(void 0, password, agent)) return false;
1591
+ this.authMethod = "password";
1592
+ return true;
1593
+ }
1594
+ /**
1595
+ * Wrap a host-key verifier to pin the accepted key on first connection and
1596
+ * reject key changes on subsequent connections (reconnects).
1597
+ *
1598
+ * @param original - The original verifier from `buildHostVerifier`, if any.
1599
+ * @returns A verifier that enforces host-key pinning.
1600
+ */
1601
+ wrapHostVerifier(original) {
1602
+ return (key) => {
1603
+ if (this.pinnedHostKey != null && (this.pinnedHostKey.length !== key.length || !timingSafeEqual2(this.pinnedHostKey, key))) {
1604
+ this.clearCachedPassword();
1605
+ throw new HostKeyVerificationError(
1606
+ `HOST KEY CHANGED on reconnect to ${this.runtime.host}: the remote host key does not match the key from the initial connection. This could indicate a man-in-the-middle attack.`
1607
+ );
1608
+ }
1609
+ if (original != null) {
1610
+ const accepted = original(key);
1611
+ if (!accepted) return false;
1612
+ this.verifiedHostKey ??= Buffer.from(key);
1613
+ }
1614
+ this.pinnedHostKey ??= Buffer.from(key);
1615
+ return true;
1616
+ };
1617
+ }
1618
+ /**
1619
+ * Write the cached sudo password followed by a newline to the given stream.
1620
+ *
1621
+ * @param stream - The SSH channel to write the password to.
1622
+ */
1623
+ writeSudoPassword(stream) {
1624
+ stream.write(this.cachedSudoPassword);
1625
+ stream.write("\n");
1626
+ }
1627
+ };
1628
+
1629
+ // src/environment.ts
1630
+ import { readFile as readFile2 } from "fs/promises";
1631
+ async function resolveEnvironment(environment, key) {
1632
+ const value = environment[key];
1633
+ if (value === void 0) {
1634
+ throw new Error(`Env key "${key}" is not defined`);
1635
+ }
1636
+ if (typeof value === "function") {
1637
+ return value();
1638
+ }
1639
+ return value;
1640
+ }
1641
+ async function loadDotEnvironment(filePath) {
1642
+ const content = await readFile2(filePath, "utf8");
1643
+ const environment = {};
1644
+ for (const line of content.split("\n")) {
1645
+ const trimmed = line.trim();
1646
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
1647
+ const eqIndex = trimmed.indexOf("=");
1648
+ if (eqIndex === -1) continue;
1649
+ const key = trimmed.slice(0, eqIndex).trim();
1650
+ environment[key] = processValue(trimmed.slice(eqIndex + 1).trim());
1651
+ }
1652
+ return environment;
1653
+ }
1654
+ function processValue(raw) {
1655
+ if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
1656
+ return raw.slice(1, -1).replaceAll("\\\\", "\0").replaceAll("\\n", "\n").replaceAll('\\"', '"').replaceAll("\0", "\\");
1657
+ }
1658
+ if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) {
1659
+ return raw.slice(1, -1);
1660
+ }
1661
+ const commentIndex = raw.search(new RegExp("\\s#", "v"));
1662
+ return commentIndex === -1 ? raw : raw.slice(0, commentIndex).trimEnd();
1663
+ }
1664
+ function mergeEnvironment(...environments) {
1665
+ const result = {};
1666
+ for (const environment of environments) {
1667
+ if (environment != null) {
1668
+ Object.assign(result, environment);
1669
+ }
1670
+ }
1671
+ return result;
1672
+ }
1673
+
1674
+ export {
1675
+ validateMode,
1676
+ shellQuote,
1677
+ CommandError,
1678
+ maskSecrets,
1679
+ isValidTcpPort,
1680
+ collectSshConfigErrors,
1681
+ validateSshConfig,
1682
+ environmentMeta,
1683
+ sshdPortMeta,
1684
+ systemHostMeta,
1685
+ systemRebootMeta,
1686
+ meta,
1687
+ isEnvironmentMetaEntry,
1688
+ isStringEnvironmentMetaEntry,
1689
+ isNumberEnvironmentMetaEntry,
1690
+ isBooleanEnvironmentMetaEntry,
1691
+ isLazyEnvironmentMetaEntry,
1692
+ isSshdPortMetaEntry,
1693
+ isSystemHostMetaEntry,
1694
+ isSystemRebootMetaEntry,
1695
+ assertValidModuleMetaEntry,
1696
+ assertValidModuleMetaEntries,
1697
+ mergeEnvironmentFromMeta,
1698
+ environmentToMetaEntries,
1699
+ diffEnvironmentToMetaEntries,
1700
+ resolveEnvironment,
1701
+ loadDotEnvironment,
1702
+ mergeEnvironment,
1703
+ computeFingerprint,
1704
+ SshConnectionImpl
1705
+ };
1706
+ //# sourceMappingURL=chunk-G3BMCQKU.js.map