openship 0.6.0 → 0.6.5

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.
Files changed (63) hide show
  1. package/README.md +7 -2
  2. package/dist/{bare-FSUAM5V5.js → bare-XQIPDO4R.js} +10 -9
  3. package/dist/{chunk-PSZSKEPA.js → chunk-3ZAGC5NT.js} +64 -17
  4. package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
  5. package/dist/chunk-6HOA5JT7.js +124 -0
  6. package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
  7. package/dist/{chunk-GUQ3QCJ7.js → chunk-7334NSJL.js} +399 -369
  8. package/dist/chunk-7S4USQYF.js +1753 -0
  9. package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
  10. package/dist/chunk-ELGRMZOW.js +25 -0
  11. package/dist/chunk-EZPPTWJQ.js +292 -0
  12. package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
  13. package/dist/chunk-HPXP6LXZ.js +428 -0
  14. package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
  15. package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
  16. package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
  17. package/dist/{chunk-OGY63DGD.js → chunk-PZRRL5PC.js} +71 -28
  18. package/dist/{chunk-VE2YOK4I.js → chunk-QLX7NBEA.js} +1218 -588
  19. package/dist/chunk-RFWJE7ZN.js +68 -0
  20. package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
  21. package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
  22. package/dist/{chunk-B3CE2OMR.js → chunk-VHMOQL5Y.js} +276 -50
  23. package/dist/{chunk-QEH4QUBX.js → chunk-VLQ3UIVO.js} +393 -53
  24. package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
  25. package/dist/chunk-WFRXYWK2.js +444 -0
  26. package/dist/chunk-XRNRT37W.js +316 -0
  27. package/dist/{chunk-EP2TNALB.js → chunk-XVOBL7BC.js} +31 -8
  28. package/dist/cloud-3DODOWTR.js +36 -0
  29. package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
  30. package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
  31. package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
  32. package/dist/{edge-import-CQWHN2Q3.js → edge-import-MCJNVTZ2.js} +26 -23
  33. package/dist/edge-preflight-ES6FH4JK.js +42 -0
  34. package/dist/{ensure-container-edge-MJVXJQG5.js → ensure-container-edge-YPITNH45.js} +9 -8
  35. package/dist/{executor-JELARVKC.js → executor-DZGX55K2.js} +12 -5
  36. package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
  37. package/dist/index.js +3668 -4063
  38. package/dist/{nginx-AXGNWRKZ.js → nginx-NSAHWGJV.js} +11 -5
  39. package/dist/node-bootstrap.js +241 -0
  40. package/dist/node-entry.js +52 -1
  41. package/dist/{openresty-lua-IE4P5YQX.js → openresty-lua-DR4D3EZ6.js} +6 -2
  42. package/dist/privilege-YTVI4SKF.js +19 -0
  43. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
  44. package/dist/server/index.js +21501 -13137
  45. package/dist/server/lua/geo_country.lua +4 -3
  46. package/dist/server/lua/pipe_stream.lua +52 -39
  47. package/dist/server/lua/site_logger.lua +22 -24
  48. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  49. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  50. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  51. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  52. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  53. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  54. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  55. package/dist/server/migrations/meta/_journal.json +49 -0
  56. package/dist/{setup-FA4PW4NU.js → setup-WUUZURHS.js} +10 -9
  57. package/dist/up-XEO3SR6Z.js +58 -0
  58. package/package.json +4 -2
  59. package/dist/chunk-IW2UKBVF.js +0 -144
  60. package/dist/chunk-ZCFVHA25.js +0 -89
  61. package/dist/cloud-E5M6VMCG.js +0 -31
  62. package/dist/server/lua/pipe_log.lua +0 -77
  63. /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
@@ -5,14 +5,12 @@ const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  connectSshClient,
7
7
  getTarCreateEnv,
8
+ openDockerDialStdioChannel,
8
9
  openSftp,
9
10
  openSshUnixSocket,
10
11
  prepareSourceTarArgs,
11
12
  reconcileKnownHosts
12
- } from "./chunk-6ODUBKUB.js";
13
- import {
14
- SshDisconnectedError
15
- } from "./chunk-ZCFVHA25.js";
13
+ } from "./chunk-5OUWIPOB.js";
16
14
  import {
17
15
  LocalExecutor
18
16
  } from "./chunk-6FH4AXVY.js";
@@ -24,13 +22,77 @@ import {
24
22
  sq
25
23
  } from "./chunk-ZFJXPCQP.js";
26
24
  import {
25
+ HOST_CHANNEL_NOT_PROVISIONED,
26
+ HOST_CHANNEL_UNPROVISIONED,
27
+ HostChannelUnavailableError,
28
+ SshDisconnectedError,
27
29
  TRANSFER_EXCLUDES,
30
+ commandForError,
31
+ explainHostChannelCause,
28
32
  formatBytes,
33
+ hostFirewallRule,
29
34
  safeErrorMessage
30
- } from "./chunk-OTLSJYAC.js";
35
+ } from "./chunk-LEYSL46B.js";
31
36
 
32
37
  // ../../packages/adapters/src/system/executor.ts
33
38
  import { existsSync, readFileSync } from "fs";
39
+ import { networkInterfaces } from "os";
40
+
41
+ // ../../packages/adapters/src/system/reachability.ts
42
+ import { connect } from "net";
43
+ import { request } from "http";
44
+ function classifyDialError(code) {
45
+ switch (code) {
46
+ case "ECONNREFUSED":
47
+ case "ECONNRESET":
48
+ return "refused";
49
+ case "ENOTFOUND":
50
+ case "EAI_AGAIN":
51
+ return "unresolved";
52
+ case "EHOSTUNREACH":
53
+ case "ENETUNREACH":
54
+ case "ENETDOWN":
55
+ return "no_route";
56
+ // The kernel's own connect timeout, i.e. our `timeout` reported for us.
57
+ case "ETIMEDOUT":
58
+ return "timeout";
59
+ default:
60
+ return "error";
61
+ }
62
+ }
63
+ function probeTcpDetailed(host, port, timeoutMs = 2500) {
64
+ return new Promise((resolve) => {
65
+ let settled = false;
66
+ const done = (result, socket2) => {
67
+ if (settled) return;
68
+ settled = true;
69
+ try {
70
+ socket2?.destroy();
71
+ } catch {
72
+ }
73
+ resolve(result);
74
+ };
75
+ const socket = connect({ host, port });
76
+ socket.setTimeout(timeoutMs);
77
+ socket.once("connect", () => done({ ok: true }, socket));
78
+ socket.once(
79
+ "timeout",
80
+ () => done({ ok: false, reason: "timeout", message: `no response within ${timeoutMs}ms` }, socket)
81
+ );
82
+ socket.once("error", (err) => {
83
+ const errno = err;
84
+ done(
85
+ {
86
+ ok: false,
87
+ reason: classifyDialError(errno.code),
88
+ ...errno.code ? { code: errno.code } : {},
89
+ ...errno.message ? { message: errno.message } : {}
90
+ },
91
+ socket
92
+ );
93
+ });
94
+ });
95
+ }
34
96
 
35
97
  // ../../packages/adapters/src/system/ssh-executor.ts
36
98
  import { createReadStream } from "fs";
@@ -240,7 +302,7 @@ var SshExecutor = class _SshExecutor {
240
302
  async connect() {
241
303
  if (this.client) return this.client;
242
304
  if (this.connecting) return this.connecting;
243
- this.connecting = (async () => {
305
+ const attempt = (async () => {
244
306
  const client = await connectSshClient(this.config);
245
307
  const onTransportDown = (cause) => {
246
308
  if (this.client !== client) return;
@@ -252,10 +314,14 @@ var SshExecutor = class _SshExecutor {
252
314
  client.on("end", () => onTransportDown());
253
315
  client.on("error", (err) => onTransportDown(err));
254
316
  this.client = client;
255
- this.connecting = null;
256
317
  return client;
257
318
  })();
258
- return this.connecting;
319
+ this.connecting = attempt;
320
+ const clearMemo = () => {
321
+ if (this.connecting === attempt) this.connecting = null;
322
+ };
323
+ attempt.then(clearMemo, clearMemo);
324
+ return attempt;
259
325
  }
260
326
  /**
261
327
  * Subscribe to transport-level disconnects. Returns an unsubscribe fn.
@@ -402,7 +468,9 @@ var SshExecutor = class _SshExecutor {
402
468
  };
403
469
  this.inflight.add(abort);
404
470
  timer = setTimeout(
405
- () => finish(() => reject(new Error(`Command timed out after ${timeout}ms: ${command}`))),
471
+ () => finish(
472
+ () => reject(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`))
473
+ ),
406
474
  timeout
407
475
  );
408
476
  client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
@@ -426,30 +494,47 @@ var SshExecutor = class _SshExecutor {
426
494
  });
427
495
  });
428
496
  }
429
- streamExec(command, onLog) {
430
- return this._streamExec(command, onLog).catch((err) => {
497
+ streamExec(command, onLog, opts) {
498
+ return this._streamExec(command, onLog, opts).catch((err) => {
431
499
  if (_SshExecutor.isChannelError(err)) {
432
500
  this.recoverFromChannelError();
433
- return this._streamExec(command, onLog);
501
+ return this._streamExec(command, onLog, opts);
434
502
  }
435
503
  throw err;
436
504
  });
437
505
  }
438
- async _streamExec(command, onLog) {
506
+ async _streamExec(command, onLog, opts) {
439
507
  const client = await this.connect();
508
+ const signal = opts?.signal;
440
509
  return new Promise((resolve, reject) => {
441
510
  let settled = false;
511
+ const chunks = [];
442
512
  const abort = (err) => finish(() => reject(err));
443
513
  const finish = (act) => {
444
514
  if (settled) return;
445
515
  settled = true;
446
516
  this.inflight.delete(abort);
517
+ signal?.removeEventListener("abort", onSignalAbort);
447
518
  act();
448
519
  };
449
520
  this.inflight.add(abort);
521
+ let channel = null;
522
+ const onSignalAbort = () => {
523
+ finish(() => resolve({ code: 0, output: chunks.join("") }));
524
+ try {
525
+ channel?.close();
526
+ } catch {
527
+ }
528
+ };
529
+ if (signal?.aborted) {
530
+ finish(() => resolve({ code: 0, output: "" }));
531
+ return;
532
+ }
450
533
  client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
451
534
  if (err) return finish(() => reject(err));
452
- const chunks = [];
535
+ channel = stream;
536
+ if (signal?.aborted) return onSignalAbort();
537
+ signal?.addEventListener("abort", onSignalAbort, { once: true });
453
538
  const onChunk = (data, level) => {
454
539
  const text = data.toString();
455
540
  if (!text) return;
@@ -689,24 +774,23 @@ var SshExecutor = class _SshExecutor {
689
774
  * Carry the Docker Engine API over a `docker system dial-stdio` exec channel
690
775
  * on the pooled connection — the streamlocal-free transport used when the
691
776
  * SSH server (or the Bun-compiled desktop runtime) can't do socket
692
- * forwarding. Uses the SAME ENV_PREFIX as streamExec so `docker` resolves on
693
- * PATH exactly as it does for the remote build (which is proven to work).
777
+ * forwarding.
778
+ *
779
+ * Deliberately WITHOUT `ENV_PREFIX`: that prefix only exports apt's
780
+ * non-interactive variables (it sets no PATH, despite what the comment here
781
+ * used to claim), and the bridge's own ephemeral dial-stdio client opens the
782
+ * bare command. Two dial-stdio channels that differ by a shell prefix are two
783
+ * transports, and the bridge picks one by probing the other.
784
+ *
785
+ * Failure diagnostics (daemon down, socket permission, `docker: command not
786
+ * found`, an sshd ForceCommand banner) ride ON the returned channel — see
787
+ * `attachDialStdioDiagnostics`. They used to go to `console.warn` while the
788
+ * caller got a bare socket reset, which is how a host with no running dockerd
789
+ * reported itself as `socket hang up`.
694
790
  */
695
791
  async openDockerDialStdio() {
696
792
  const client = await this.connect();
697
- return new Promise((resolve, reject) => {
698
- client.exec(_SshExecutor.ENV_PREFIX + "docker system dial-stdio", (err, stream) => {
699
- if (err) return reject(err);
700
- stream.stderr?.on("data", (d) => {
701
- const text = d.toString().trim();
702
- if (text) console.warn(`[docker-ssh] dial-stdio stderr: ${text}`);
703
- });
704
- stream.on("exit", (code) => {
705
- if (code) console.warn(`[docker-ssh] dial-stdio exited early (code=${code})`);
706
- });
707
- resolve(stream);
708
- });
709
- });
793
+ return openDockerDialStdioChannel(client);
710
794
  }
711
795
  async forwardPort(remoteHost, remotePort) {
712
796
  const client = await this.connect();
@@ -1183,7 +1267,7 @@ var SystemSshExecutor = class _SystemSshExecutor {
1183
1267
  const timeout = opts?.timeout ?? 3e4;
1184
1268
  const res = await this.runSsh(_SystemSshExecutor.ENV_PREFIX + command, { timeout });
1185
1269
  if (res.timedOut) {
1186
- throw new Error(`Command timed out after ${timeout}ms: ${command}`);
1270
+ throw new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`);
1187
1271
  }
1188
1272
  if (res.code !== 0) {
1189
1273
  if (res.code === 255) throw describeSshFailure(res.stderr, `Exit code ${res.code}`);
@@ -1191,14 +1275,22 @@ var SystemSshExecutor = class _SystemSshExecutor {
1191
1275
  }
1192
1276
  return res.stdout.trim();
1193
1277
  }
1194
- async streamExec(command, onLog) {
1278
+ async streamExec(command, onLog, opts) {
1195
1279
  await this.ensureMaster();
1280
+ const signal = opts?.signal;
1281
+ if (signal?.aborted) return { code: 0, output: "" };
1196
1282
  return new Promise((resolve) => {
1197
1283
  const child = spawn2(
1198
1284
  "ssh",
1199
1285
  [...this.baseArgs(), sshTarget(this.config), _SystemSshExecutor.ENV_PREFIX + command],
1200
1286
  { env: sshChildEnv(this.config), stdio: ["ignore", "pipe", "pipe"] }
1201
1287
  );
1288
+ let aborted = false;
1289
+ const onAbort = () => {
1290
+ aborted = true;
1291
+ child.kill("SIGKILL");
1292
+ };
1293
+ signal?.addEventListener("abort", onAbort, { once: true });
1202
1294
  const chunks = [];
1203
1295
  const onChunk = (chunk, level) => {
1204
1296
  const text = chunk.toString();
@@ -1209,11 +1301,13 @@ var SystemSshExecutor = class _SystemSshExecutor {
1209
1301
  child.stdout.on("data", (chunk) => onChunk(chunk, "info"));
1210
1302
  child.stderr.on("data", (chunk) => onChunk(chunk, "warn"));
1211
1303
  child.on("error", (err) => {
1304
+ signal?.removeEventListener("abort", onAbort);
1212
1305
  onLog(logEntry(`Process error: ${err.message}`, "error"));
1213
1306
  resolve({ code: 1, output: err.message });
1214
1307
  });
1215
1308
  child.on("close", (code) => {
1216
- const c = code ?? 1;
1309
+ signal?.removeEventListener("abort", onAbort);
1310
+ const c = aborted ? 0 : code ?? 1;
1217
1311
  void this.maybeSignalDisconnect(c).catch(() => {
1218
1312
  });
1219
1313
  resolve({ code: c, output: chunks.join("") });
@@ -1507,8 +1601,12 @@ function createExecutor(ssh) {
1507
1601
  }
1508
1602
  return localExecutor;
1509
1603
  }
1604
+ var hostControlOverride = null;
1605
+ function setHostControlOverride(disabled) {
1606
+ hostControlOverride = disabled;
1607
+ }
1510
1608
  function hostControlDisabled() {
1511
- return process.env.OPENSHIP_HOST_CONTROL?.trim().toLowerCase() === "false";
1609
+ return hostControlOverride ?? process.env.OPENSHIP_HOST_CONTROL?.trim().toLowerCase() === "false";
1512
1610
  }
1513
1611
  function runningInContainer() {
1514
1612
  if (process.env.OPENSHIP_IN_CONTAINER?.trim().toLowerCase() === "true") return true;
@@ -1522,39 +1620,163 @@ function runningInContainer() {
1522
1620
  return false;
1523
1621
  }
1524
1622
  }
1623
+ function hostChannelPort() {
1624
+ const raw = Number(process.env.OPENSHIP_HOST_SSH_PORT || "22");
1625
+ return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : 22;
1626
+ }
1627
+ function hostChannelUser() {
1628
+ return process.env.OPENSHIP_HOST_SSH_USER?.trim() || "root";
1629
+ }
1630
+ function readHostChannelKey(keyPath) {
1631
+ let contents;
1632
+ try {
1633
+ contents = readFileSync(keyPath, "utf8");
1634
+ } catch (err) {
1635
+ const detail = err instanceof Error ? err.message : String(err);
1636
+ return { reason: `Cannot read the host SSH key at ${keyPath} (${detail}).` };
1637
+ }
1638
+ if (!contents.trim()) return { reason: `The host SSH key at ${keyPath} is empty.` };
1639
+ return { key: contents };
1640
+ }
1641
+ function containerBridgeCidr() {
1642
+ for (const addrs of Object.values(networkInterfaces())) {
1643
+ for (const addr of addrs ?? []) {
1644
+ if (addr.family !== "IPv4" || addr.internal) continue;
1645
+ const prefix = Number(addr.cidr?.split("/")[1]);
1646
+ if (!Number.isInteger(prefix) || prefix < 8 || prefix > 32) continue;
1647
+ const octets = addr.address.split(".").map(Number);
1648
+ if (octets.length !== 4 || octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) continue;
1649
+ const asInt = (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
1650
+ const mask = 4294967295 << 32 - prefix >>> 0;
1651
+ const net = (asInt & mask) >>> 0;
1652
+ return `${net >>> 24 & 255}.${net >>> 16 & 255}.${net >>> 8 & 255}.${net & 255}/${prefix}`;
1653
+ }
1654
+ }
1655
+ return null;
1656
+ }
1657
+ function probeDetail(probe) {
1658
+ const { code, message } = probe;
1659
+ if (code && message && !message.includes(code)) return `${code}: ${message}`;
1660
+ return code ?? message;
1661
+ }
1662
+ function explainDialFailure(probe, host, port, target) {
1663
+ const { headline, body, firewallShaped } = explainHostChannelCause(probe.reason, {
1664
+ target,
1665
+ host,
1666
+ detail: probeDetail(probe)
1667
+ });
1668
+ const hint = body ? `${headline}. ${body}` : `${headline}.`;
1669
+ if (!firewallShaped) return { hint };
1670
+ const cidr = containerBridgeCidr();
1671
+ return { hint, rule: hostFirewallRule("unknown", cidr ? [cidr] : [], port) };
1672
+ }
1673
+ async function hostChannelHealth(timeoutMs = 2500) {
1674
+ if (hostControlDisabled()) {
1675
+ return {
1676
+ ok: false,
1677
+ code: "disabled",
1678
+ hint: "Host control is off (OPENSHIP_HOST_CONTROL=false). Re-run `openship up` without --no-host-control."
1679
+ };
1680
+ }
1681
+ const host = process.env.OPENSHIP_HOST_SSH_HOST?.trim();
1682
+ if (!host) {
1683
+ if (runningInContainer()) {
1684
+ return {
1685
+ ok: false,
1686
+ code: "not_configured",
1687
+ hint: `${HOST_CHANNEL_UNPROVISIONED} ${HOST_CHANNEL_NOT_PROVISIONED}`
1688
+ };
1689
+ }
1690
+ return { ok: true, code: "not_applicable" };
1691
+ }
1692
+ const port = hostChannelPort();
1693
+ const target = `${hostChannelUser()}@${host}:${port}`;
1694
+ const keyPath = process.env.OPENSHIP_HOST_SSH_KEY?.trim();
1695
+ if (keyPath) {
1696
+ const { reason } = readHostChannelKey(keyPath);
1697
+ if (reason) {
1698
+ return {
1699
+ ok: false,
1700
+ code: "key_unreadable",
1701
+ host,
1702
+ port,
1703
+ target,
1704
+ hint: `${reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
1705
+ };
1706
+ }
1707
+ }
1708
+ const probe = await probeTcpDetailed(host, port, timeoutMs);
1709
+ if (probe.ok) {
1710
+ return { ok: true, code: "ok", host, port, target };
1711
+ }
1712
+ return {
1713
+ ok: false,
1714
+ code: "unreachable",
1715
+ host,
1716
+ port,
1717
+ target,
1718
+ cause: probe.reason,
1719
+ ...explainDialFailure(probe, host, port, target)
1720
+ };
1721
+ }
1722
+ function unavailableExecutor(reason, code = "not_configured") {
1723
+ const refuse = async () => {
1724
+ throw new HostChannelUnavailableError(code, reason);
1725
+ };
1726
+ return {
1727
+ exec: refuse,
1728
+ streamExec: refuse,
1729
+ writeFile: refuse,
1730
+ readFile: refuse,
1731
+ exists: refuse,
1732
+ mkdir: refuse,
1733
+ rm: refuse,
1734
+ transferIn: refuse,
1735
+ // Nothing was ever opened, so releasing must not be the one call that throws.
1736
+ dispose: async () => {
1737
+ }
1738
+ };
1739
+ }
1525
1740
  function createHostExecutor() {
1526
1741
  if (hostControlDisabled()) {
1527
- throw new Error(
1742
+ throw new HostChannelUnavailableError(
1743
+ "disabled",
1528
1744
  "Host control is disabled on this instance (OPENSHIP_HOST_CONTROL=false). Re-run `openship up` without --no-host-control to allow host operations."
1529
1745
  );
1530
1746
  }
1531
1747
  const host = process.env.OPENSHIP_HOST_SSH_HOST?.trim();
1532
1748
  if (!host) {
1533
1749
  if (runningInContainer()) {
1534
- throw new Error(
1535
- "This operation targets the HOST machine, but no host channel is configured (OPENSHIP_HOST_SSH_HOST is unset) and Openship is running in a container \u2014 so it would have run inside the container instead, against the wrong filesystem. Re-run `openship up` to provision the host channel."
1750
+ throw new HostChannelUnavailableError(
1751
+ "not_configured",
1752
+ `This operation targets the HOST machine, but no host channel is configured (OPENSHIP_HOST_SSH_HOST is unset) and Openship is running in a container \u2014 so it would have run inside the container instead, against the wrong filesystem. ${HOST_CHANNEL_NOT_PROVISIONED}`
1536
1753
  );
1537
1754
  }
1538
1755
  return localExecutor;
1539
1756
  }
1540
1757
  const keyPath = process.env.OPENSHIP_HOST_SSH_KEY?.trim();
1541
- const portRaw = Number(process.env.OPENSHIP_HOST_SSH_PORT || "22");
1542
- const port = Number.isInteger(portRaw) && portRaw > 0 && portRaw < 65536 ? portRaw : 22;
1543
- let privateKey;
1544
- if (keyPath) {
1545
- try {
1546
- privateKey = readFileSync(keyPath, "utf8");
1547
- } catch (err) {
1548
- throw new Error(
1549
- `Cannot read host SSH key at OPENSHIP_HOST_SSH_KEY=${keyPath}: ${err instanceof Error ? err.message : String(err)}`
1550
- );
1551
- }
1758
+ if (!keyPath) {
1759
+ throw new HostChannelUnavailableError(
1760
+ "not_configured",
1761
+ `A host channel is configured (OPENSHIP_HOST_SSH_HOST) but OPENSHIP_HOST_SSH_KEY is unset, so there is no credential to connect to the host with. ${HOST_CHANNEL_NOT_PROVISIONED}`
1762
+ );
1763
+ }
1764
+ const read = readHostChannelKey(keyPath);
1765
+ if (read.reason) {
1766
+ throw new HostChannelUnavailableError(
1767
+ "not_configured",
1768
+ `${read.reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
1769
+ );
1552
1770
  }
1553
1771
  return new SshExecutor({
1554
1772
  host,
1555
- port,
1556
- username: process.env.OPENSHIP_HOST_SSH_USER?.trim() || "root",
1557
- privateKey
1773
+ port: hostChannelPort(),
1774
+ username: hostChannelUser(),
1775
+ privateKey: read.key,
1776
+ // One hop over the docker bridge: it answers at once or it is being filtered.
1777
+ // 20s of silence per host operation reads as a hang, not as a failure.
1778
+ readyTimeoutMs: 8e3,
1779
+ hostChannel: true
1558
1780
  });
1559
1781
  }
1560
1782
 
@@ -1562,6 +1784,10 @@ export {
1562
1784
  SshExecutor,
1563
1785
  SystemSshExecutor,
1564
1786
  createExecutor,
1787
+ setHostControlOverride,
1565
1788
  hostControlDisabled,
1789
+ containerBridgeCidr,
1790
+ hostChannelHealth,
1791
+ unavailableExecutor,
1566
1792
  createHostExecutor
1567
1793
  };