openship 0.6.5 → 0.6.7

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 (68) hide show
  1. package/dist/bare-7QD623QV.js +29 -0
  2. package/dist/chunk-2ANV5ZQ5.js +88 -0
  3. package/dist/{chunk-LEYSL46B.js → chunk-2VSW6Q6C.js} +3911 -898
  4. package/dist/{chunk-5OUWIPOB.js → chunk-5T4UVOM3.js} +16 -86
  5. package/dist/{chunk-EZPPTWJQ.js → chunk-BLEXTYRK.js} +1 -1
  6. package/dist/{chunk-3ZAGC5NT.js → chunk-CIJV43LH.js} +83 -33
  7. package/dist/{chunk-7S4USQYF.js → chunk-DDFUESPJ.js} +67 -124
  8. package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
  9. package/dist/{chunk-XVOBL7BC.js → chunk-FYPFEDPZ.js} +1 -1
  10. package/dist/{chunk-WFRXYWK2.js → chunk-FZ6QCF6B.js} +8 -6
  11. package/dist/{chunk-VHMOQL5Y.js → chunk-GGIAIGUL.js} +125 -80
  12. package/dist/{chunk-WE7BXW6L.js → chunk-GIPJNNI5.js} +38 -44
  13. package/dist/{chunk-ZFJXPCQP.js → chunk-H3WT3A6R.js} +4 -3
  14. package/dist/chunk-H4FS5BED.js +936 -0
  15. package/dist/{chunk-HPXP6LXZ.js → chunk-J2XM3QSP.js} +8 -5
  16. package/dist/{chunk-6JELW75A.js → chunk-J3K6XABC.js} +5 -5
  17. package/dist/{chunk-VDESJSG6.js → chunk-LHFXSC36.js} +558 -109
  18. package/dist/{chunk-MSZUCNPV.js → chunk-MEE6OZFP.js} +3 -3
  19. package/dist/chunk-N5YZMO33.js +111 -0
  20. package/dist/{chunk-VLQ3UIVO.js → chunk-NRTJRNAE.js} +105 -37
  21. package/dist/chunk-PPEBIY5F.js +2331 -0
  22. package/dist/{chunk-RFWJE7ZN.js → chunk-PVP4RGWT.js} +3 -3
  23. package/dist/{chunk-PZRRL5PC.js → chunk-QHMDCYZ3.js} +15 -4
  24. package/dist/chunk-R4VDPFLS.js +106 -0
  25. package/dist/chunk-RDXT4BQB.js +310 -0
  26. package/dist/{chunk-GRTXMUHA.js → chunk-S6B2Z3OO.js} +145 -60
  27. package/dist/chunk-UWEV6XTS.js +249 -0
  28. package/dist/{chunk-QLX7NBEA.js → chunk-VQM6JHJB.js} +1004 -2046
  29. package/dist/{chunk-A7EWM2KA.js → chunk-VSTFHUCV.js} +44 -50
  30. package/dist/chunk-WU54BXFE.js +70 -0
  31. package/dist/{chunk-LFCSC4IU.js → chunk-XWHWTHEL.js} +313 -469
  32. package/dist/{chunk-7334NSJL.js → chunk-ZPJ74LXE.js} +76 -38
  33. package/dist/cloud-KNNABE3O.js +40 -0
  34. package/dist/{detect-P4CIQBRH.js → detect-G4DR3U2N.js} +9 -6
  35. package/dist/{docker-3K2NEBD6.js → docker-YOVPCIB4.js} +11 -7
  36. package/dist/{docker-edge-executor-YZZ6P7IC.js → docker-edge-executor-QFXD3337.js} +8 -6
  37. package/dist/edge-import-CRFVSR6P.js +56 -0
  38. package/dist/{edge-preflight-ES6FH4JK.js → edge-preflight-MXERVAMG.js} +15 -12
  39. package/dist/{ensure-container-edge-YPITNH45.js → ensure-container-edge-F4JUNQIK.js} +13 -10
  40. package/dist/{executor-DZGX55K2.js → executor-CVPAYYHR.js} +10 -5
  41. package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
  42. package/dist/index.js +139 -92
  43. package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
  44. package/dist/{nginx-NSAHWGJV.js → nginx-KLFF43TC.js} +13 -8
  45. package/dist/{openresty-lua-DR4D3EZ6.js → openresty-lua-VKWUV4GB.js} +8 -5
  46. package/dist/{privilege-YTVI4SKF.js → privilege-7D3SSOL3.js} +6 -5
  47. package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
  48. package/dist/server/engine/samples/postfix/master.cf +14 -0
  49. package/dist/server/index.js +44526 -33476
  50. package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
  51. package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
  52. package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
  53. package/dist/server/migrations/0108_credential.sql +68 -0
  54. package/dist/server/migrations/meta/_journal.json +28 -0
  55. package/dist/setup-I5AJC6NJ.js +25 -0
  56. package/dist/ssh-client-625VWGKP.js +29 -0
  57. package/dist/up-QXTOOVXC.js +61 -0
  58. package/package.json +1 -1
  59. package/dist/bare-XQIPDO4R.js +0 -24
  60. package/dist/chunk-6HOA5JT7.js +0 -124
  61. package/dist/chunk-TM2IQPYU.js +0 -434
  62. package/dist/chunk-XRNRT37W.js +0 -316
  63. package/dist/cloud-3DODOWTR.js +0 -36
  64. package/dist/edge-import-MCJNVTZ2.js +0 -51
  65. package/dist/host-channel-preflight-MUXE3KZU.js +0 -49
  66. package/dist/setup-WUUZURHS.js +0 -22
  67. package/dist/up-XEO3SR6Z.js +0 -58
  68. /package/dist/{dist-es-ZP5QDSJ3.js → dist-es-AXFRS7PS.js} +0 -0
@@ -2,19 +2,6 @@
2
2
  ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
- import {
6
- importMigratedSites
7
- } from "./chunk-6HOA5JT7.js";
8
- import {
9
- completeHostEdge,
10
- markStoppedProxyImported,
11
- planAndApplyHostEdge,
12
- previewHostEdge,
13
- rollbackHostEdge
14
- } from "./chunk-HPXP6LXZ.js";
15
- import {
16
- verifyHostChannel
17
- } from "./chunk-XRNRT37W.js";
18
5
  import {
19
6
  DEFAULT_REPO,
20
7
  PORTS_FILE,
@@ -40,9 +27,27 @@ import {
40
27
  resolveComposePorts,
41
28
  resolvePorts,
42
29
  sourceBuildDir,
30
+ verifyHostChannel
31
+ } from "./chunk-PPEBIY5F.js";
32
+ import {
33
+ bootstrapAdmin,
34
+ importMigratedSites,
35
+ internalFetch,
36
+ internalPost,
37
+ mintBareInternalToken,
38
+ waitHealthy
39
+ } from "./chunk-RDXT4BQB.js";
40
+ import {
43
41
  startUnitHint,
44
42
  thisHost
45
- } from "./chunk-QLX7NBEA.js";
43
+ } from "./chunk-VQM6JHJB.js";
44
+ import {
45
+ completeHostEdge,
46
+ markStoppedProxyImported,
47
+ planAndApplyHostEdge,
48
+ previewHostEdge,
49
+ rollbackHostEdge
50
+ } from "./chunk-J2XM3QSP.js";
46
51
  import {
47
52
  AUTH_SECRET_FILE,
48
53
  DATA_DIR,
@@ -53,23 +58,25 @@ import {
53
58
  } from "./chunk-ELGRMZOW.js";
54
59
  import {
55
60
  LocalExecutor
56
- } from "./chunk-6FH4AXVY.js";
61
+ } from "./chunk-E2X26PXR.js";
57
62
  import {
58
- EDGE_CONTAINER_NAME,
59
63
  edgeCrashReason,
60
64
  edgeIsBroken
61
- } from "./chunk-WE7BXW6L.js";
65
+ } from "./chunk-GIPJNNI5.js";
66
+ import {
67
+ EDGE_CONTAINER_NAME
68
+ } from "./chunk-UWEV6XTS.js";
62
69
  import {
63
70
  isValidEmail
64
- } from "./chunk-LEYSL46B.js";
71
+ } from "./chunk-2VSW6Q6C.js";
65
72
 
66
73
  // src/commands/up.ts
67
74
  import { Command } from "commander";
68
75
  import chalk2 from "chalk";
69
76
  import ora from "ora";
70
77
  import { spawn } from "child_process";
71
- import { randomBytes as randomBytes2 } from "crypto";
72
- import { createWriteStream as createWriteStream2, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
78
+ import { randomBytes } from "crypto";
79
+ import { createWriteStream as createWriteStream2, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
73
80
  import { dirname as dirname2, join as join5 } from "path";
74
81
  import { fileURLToPath } from "url";
75
82
 
@@ -604,6 +611,7 @@ async function planUp(opts) {
604
611
  ...opts.hostControl === false ? { noHostControl: true } : {},
605
612
  ...opts.hostSshHost ? { hostSshHost: opts.hostSshHost } : {},
606
613
  ...opts.hostSshPort ? { hostSshPort: opts.hostSshPort } : {},
614
+ ...opts.hostSshUser ? { hostSshUser: opts.hostSshUser } : {},
607
615
  ...opts.resetSecrets ? { resetSecrets: true } : {},
608
616
  ...opts.mail ? { mail: true } : {}
609
617
  });
@@ -615,9 +623,23 @@ async function planUp(opts) {
615
623
  ...stack.buildFile ? [stack.buildFile] : [],
616
624
  ...stack.hostChannel ? [
617
625
  stack.hostChannel.keyPath,
618
- `${stack.hostChannel.authKeysPath}${stack.hostChannel.viaSudo ? " (via sudo)" : ""} (authorizes that key for ${stack.hostChannel.user}'s host ops; --no-host-control skips it)`,
626
+ `${stack.hostChannel.authKeysPath} (authorizes that key for ${stack.hostChannel.user}'s host ops; --no-host-control skips it)`,
627
+ // Say which route to root this channel will take, rather than repeating a root
628
+ // warning at a box that reaches root perfectly well via sudo. `sudo` is a
629
+ // supported shape now, not a degradation, and the preview is where an operator
630
+ // decides whether that is what they wanted.
631
+ ...stack.hostChannel.elevation === "sudo" ? [
632
+ `logs in as ${stack.hostChannel.user} and elevates with sudo for root host ops (no root login is created)`
633
+ ] : [],
619
634
  ...stack.hostChannel.rootUnavailable ? [
620
- "\u26A0 host control needs root \u2014 this user isn't root and passwordless sudo isn't available, so mail/edge host ops will fail (re-run as root to fix)"
635
+ "\u26A0 host control needs root \u2014 this user isn't root and passwordless sudo isn't available, so mail/edge host ops will fail (re-run as root, or enable passwordless sudo, to fix)"
636
+ ] : [],
637
+ // A plan that named one account and then settled on another would read as the
638
+ // plan being wrong, and this run genuinely cannot know which: only a dial can
639
+ // establish whether sshd accepts the first account, and dialing means writing
640
+ // the key a dry run exists not to write (#527, previewHostChannel).
641
+ ...stack.hostChannel.fallbacks?.length ? [
642
+ `if this host's sshd refuses that account, falls back to ${stack.hostChannel.fallbacks.join(", ")} (pin one with --host-ssh-user)`
621
643
  ] : []
622
644
  ] : [],
623
645
  ...stack.mountDirs.map((d) => `${d}/ (edge routing state + certs)`),
@@ -768,87 +790,6 @@ function serviceWord(kind) {
768
790
  }
769
791
  }
770
792
 
771
- // src/lib/loopback-api.ts
772
- import { existsSync as existsSync4, readFileSync as readFileSync3, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
773
- import { randomBytes } from "crypto";
774
- function ensureInternalToken() {
775
- const path = INTERNAL_TOKEN_FILE;
776
- if (existsSync4(path)) return readFileSync3(path, "utf8").trim();
777
- mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
778
- const token = randomBytes(32).toString("hex");
779
- writeFileSync4(path, token, { mode: 384 });
780
- return token;
781
- }
782
- async function internalGet(port, path, token) {
783
- try {
784
- const res = await fetch(`http://127.0.0.1:${port}${path}`, {
785
- headers: { "X-Internal-Token": token ?? ensureInternalToken() },
786
- signal: AbortSignal.timeout(1e4)
787
- });
788
- if (!res.ok) return null;
789
- return await res.json();
790
- } catch {
791
- return null;
792
- }
793
- }
794
- async function internalPost(port, path, body, token) {
795
- try {
796
- const res = await fetch(`http://127.0.0.1:${port}${path}`, {
797
- method: "POST",
798
- headers: { "Content-Type": "application/json", "X-Internal-Token": token ?? ensureInternalToken() },
799
- body: JSON.stringify(body),
800
- signal: AbortSignal.timeout(3e4)
801
- });
802
- const data = await res.json().catch(() => ({}));
803
- return { ok: res.ok, data };
804
- } catch (err) {
805
- return { ok: false, data: { error: err.message } };
806
- }
807
- }
808
- async function bootstrapAdmin(apiPort, admin, token) {
809
- const { ok, data } = await internalPost(apiPort, "/api/system/bootstrap-admin", admin, token);
810
- if (ok) return { ok: true };
811
- if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
812
- return { ok: false, message: data?.error || "failed" };
813
- }
814
- async function waitHealthy(apiPort, seconds = 90) {
815
- for (let i = 0; i < seconds; i++) {
816
- await new Promise((r) => setTimeout(r, 1e3));
817
- try {
818
- await fetch(`http://127.0.0.1:${apiPort}/api/health`, { signal: AbortSignal.timeout(2e3) });
819
- return true;
820
- } catch {
821
- }
822
- }
823
- return false;
824
- }
825
- async function waitDashboard(dashPort, seconds = 45) {
826
- for (let i = 0; i < seconds; i++) {
827
- await new Promise((r) => setTimeout(r, 1e3));
828
- try {
829
- const res = await fetch(`http://127.0.0.1:${dashPort}/`, {
830
- redirect: "manual",
831
- signal: AbortSignal.timeout(2e3)
832
- });
833
- if (res.status > 0) return true;
834
- } catch {
835
- }
836
- }
837
- return false;
838
- }
839
- async function detectPublicIp() {
840
- for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
841
- try {
842
- const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
843
- if (!res.ok) continue;
844
- const ip = (await res.text()).trim();
845
- if (/^[0-9.]+$/.test(ip) || ip.includes(":")) return ip;
846
- } catch {
847
- }
848
- }
849
- return null;
850
- }
851
-
852
793
  // src/lib/instance-provision.ts
853
794
  var HeadlessInputError = class extends Error {
854
795
  code = "HEADLESS_INPUT";
@@ -927,10 +868,14 @@ async function bootstrapOrReset(port, admin, token) {
927
868
  async function drainProvisionStream(port, sessionId, token, onLog) {
928
869
  let detail;
929
870
  try {
930
- const res = await fetch(`http://127.0.0.1:${port}/api/system/self-register/stream?id=${sessionId}`, {
931
- headers: { "X-Internal-Token": token ?? ensureInternalToken() },
932
- signal: AbortSignal.timeout(18e4)
933
- });
871
+ const call = await internalFetch(
872
+ port,
873
+ `/api/system/self-register/stream?id=${sessionId}`,
874
+ { signal: AbortSignal.timeout(18e4) },
875
+ token
876
+ );
877
+ if (call.kind !== "response") return { completed: false, detail: call.detail };
878
+ const res = call.res;
934
879
  if (!res.body) return { completed: false };
935
880
  const reader = res.body.getReader();
936
881
  const decoder = new TextDecoder();
@@ -1100,10 +1045,10 @@ var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
1100
1045
  var SERVER_DIR = join5(DIST_DIR, "server");
1101
1046
  function ensureAuthSecret() {
1102
1047
  const path = AUTH_SECRET_FILE;
1103
- if (existsSync5(path)) return readFileSync4(path, "utf8").trim();
1104
- mkdirSync6(OS_DIR, { recursive: true, mode: 448 });
1105
- const secret = randomBytes2(32).toString("hex");
1106
- writeFileSync5(path, secret, { mode: 384 });
1048
+ if (existsSync4(path)) return readFileSync3(path, "utf8").trim();
1049
+ mkdirSync5(OS_DIR, { recursive: true, mode: 448 });
1050
+ const secret = randomBytes(32).toString("hex");
1051
+ writeFileSync4(path, secret, { mode: 384 });
1107
1052
  return secret;
1108
1053
  }
1109
1054
  var upCommand = new Command("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on (default: 4000, or the next free port if it's taken)").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port (default: 3001, or the next free port if it's taken)").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option(
@@ -1136,6 +1081,9 @@ var upCommand = new Command("up").description("Start Openship as a persistent se
1136
1081
  ).option(
1137
1082
  "--host-ssh-port <port>",
1138
1083
  "Compose mode: port the host's sshd listens on for host operations (default 22). Preserved across re-runs."
1084
+ ).option(
1085
+ "--host-ssh-user <name>",
1086
+ "Compose mode: the host account the api container logs in as for host operations. Normally left unset \u2014 provisioning authorizes a key and DIALS to find an account this host's sshd actually accepts (root when invoked as root, otherwise the invoking user, falling back to $SUDO_USER when a root login is refused). Set this when neither is the account you want, or when sshd's rules mean neither works: the named account is used as-is and never falls back, so it must be able to log in over SSH and should have passwordless sudo (host operations elevate per command). Preserved across re-runs."
1139
1087
  ).option(
1140
1088
  "--open-host-firewall",
1141
1089
  "Compose mode: if the api container can't reach this machine's SSH port, add the host firewall rule that allows it (scoped to the container subnet, ufw/firewalld only). Without this the blocked channel is reported and left alone. Not implied by --yes."
@@ -1278,6 +1226,7 @@ async function runCompose(opts) {
1278
1226
  // so the containers would keep an environment the file no longer shows.
1279
1227
  hostSshHost: opts.hostSshHost,
1280
1228
  hostSshPort: opts.hostSshPort,
1229
+ hostSshUser: opts.hostSshUser,
1281
1230
  resetSecrets: opts.resetSecrets,
1282
1231
  mail: opts.mail
1283
1232
  });
@@ -1332,6 +1281,7 @@ async function runCompose(opts) {
1332
1281
  // configured with rather than resetting it to host.docker.internal:22.
1333
1282
  hostSshHost: opts.hostSshHost,
1334
1283
  hostSshPort: opts.hostSshPort,
1284
+ hostSshUser: opts.hostSshUser,
1335
1285
  resetSecrets: opts.resetSecrets,
1336
1286
  // Absent on a re-run keeps the install's mode (resolveEnvConfig), so this
1337
1287
  // never flips a mail box back to the platform shell.
@@ -1471,7 +1421,7 @@ async function runForeground(opts, source) {
1471
1421
  apiCwd = source.apiDir;
1472
1422
  } else {
1473
1423
  const serverEntry = join5(SERVER_DIR, "index.js");
1474
- if (!existsSync5(serverEntry)) {
1424
+ if (!existsSync4(serverEntry)) {
1475
1425
  console.error(
1476
1426
  chalk2.red("\n Bundled server not found in this install.") + chalk2.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
1477
1427
  );
@@ -1488,8 +1438,8 @@ async function runForeground(opts, source) {
1488
1438
  const publicUrl = effectivePublicUrl(opts.publicUrl);
1489
1439
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
1490
1440
  const dataDir = opts.dataDir || DATA_DIR;
1491
- mkdirSync6(dataDir, { recursive: true });
1492
- mkdirSync6(LOG_DIR, { recursive: true });
1441
+ mkdirSync5(dataDir, { recursive: true });
1442
+ mkdirSync5(LOG_DIR, { recursive: true });
1493
1443
  const instanceLogPath = join5(LOG_DIR, "instance.log");
1494
1444
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
1495
1445
  const env = {
@@ -1521,10 +1471,10 @@ async function runForeground(opts, source) {
1521
1471
  env.OPENSHIP_MIGRATIONS_DIR = join5(SERVER_DIR, "migrations");
1522
1472
  env.OPENSHIP_PGLITE_ASSETS_DIR = join5(SERVER_DIR, "pglite");
1523
1473
  const engineDir = join5(SERVER_DIR, "engine");
1524
- if (existsSync5(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
1474
+ if (existsSync4(engineDir)) env.MAIL_SERVER_ENGINE_DIR = engineDir;
1525
1475
  }
1526
1476
  env.OPENSHIP_REQUIRE_AUTH = "true";
1527
- env.INTERNAL_TOKEN = ensureInternalToken();
1477
+ env.INTERNAL_TOKEN = mintBareInternalToken();
1528
1478
  if (opts.mail) env.OPENSHIP_PRODUCT = "mail";
1529
1479
  if (!env.OPENSHIP_AUTH_MODE) env.OPENSHIP_AUTH_MODE = "local";
1530
1480
  env.OPENSHIP_API_HOST = "127.0.0.1";
@@ -1614,7 +1564,7 @@ async function runForeground(opts, source) {
1614
1564
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
1615
1565
  try {
1616
1566
  const bundle = await ensureDashboard({
1617
- tag: source ? "local" : opts.uiVersion || `v${"0.6.5"}`,
1567
+ tag: source ? "local" : opts.uiVersion || `v${"0.6.7"}`,
1618
1568
  onProgress: (received, total) => {
1619
1569
  if (total) {
1620
1570
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -1738,13 +1688,6 @@ export {
1738
1688
  restart,
1739
1689
  serviceStatus,
1740
1690
  stop,
1741
- ensureInternalToken,
1742
- internalGet,
1743
- internalPost,
1744
- bootstrapAdmin,
1745
- waitHealthy,
1746
- waitDashboard,
1747
- detectPublicIp,
1748
1691
  headlessProvision,
1749
1692
  normalizeUrl,
1750
1693
  upCommand,
@@ -8,7 +8,7 @@ import {
8
8
  getLocalShellPath,
9
9
  logEntry,
10
10
  sq
11
- } from "./chunk-ZFJXPCQP.js";
11
+ } from "./chunk-H3WT3A6R.js";
12
12
 
13
13
  // ../../packages/adapters/src/system/local-executor.ts
14
14
  import { exec, spawn } from "child_process";
@@ -7,7 +7,7 @@ import {
7
7
  COMPONENT_INSTALLERS,
8
8
  checkAll,
9
9
  checkComponents
10
- } from "./chunk-7334NSJL.js";
10
+ } from "./chunk-ZPJ74LXE.js";
11
11
 
12
12
  // ../../packages/adapters/src/system/state.ts
13
13
  var DEFAULT_STATE_PATH = "/etc/openship/setup-state.json";
@@ -4,18 +4,20 @@ import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  sq
7
- } from "./chunk-ZFJXPCQP.js";
7
+ } from "./chunk-H3WT3A6R.js";
8
8
  import {
9
- assessHostSupport,
10
- classifyDistro,
11
9
  formatDuration,
12
10
  isRemoteConnectionError,
11
+ systemDebug
12
+ } from "./chunk-N5YZMO33.js";
13
+ import {
14
+ assessHostSupport,
15
+ classifyDistro,
13
16
  parseOsRelease,
14
17
  parseSystemArch,
15
18
  parseSystemOs,
16
- safeErrorMessage,
17
- systemDebug
18
- } from "./chunk-LEYSL46B.js";
19
+ safeErrorMessage
20
+ } from "./chunk-2VSW6Q6C.js";
19
21
 
20
22
  // ../../packages/adapters/src/system/elevated-executor.ts
21
23
  import { randomBytes } from "crypto";
@@ -3,97 +3,51 @@
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
- connectSshClient,
6
+ probeTcpDetailed
7
+ } from "./chunk-R4VDPFLS.js";
8
+ import {
7
9
  getTarCreateEnv,
10
+ prepareSourceTarArgs
11
+ } from "./chunk-2ANV5ZQ5.js";
12
+ import {
13
+ connectSshClient,
8
14
  openDockerDialStdioChannel,
9
15
  openSftp,
10
16
  openSshUnixSocket,
11
- prepareSourceTarArgs,
12
17
  reconcileKnownHosts
13
- } from "./chunk-5OUWIPOB.js";
18
+ } from "./chunk-5T4UVOM3.js";
14
19
  import {
15
20
  LocalExecutor
16
- } from "./chunk-6FH4AXVY.js";
21
+ } from "./chunk-E2X26PXR.js";
17
22
  import {
18
23
  emitBufferedLines,
19
24
  flushBufferedLines,
20
25
  hasLocalCommand,
21
26
  logEntry,
22
27
  sq
23
- } from "./chunk-ZFJXPCQP.js";
28
+ } from "./chunk-H3WT3A6R.js";
24
29
  import {
25
- HOST_CHANNEL_NOT_PROVISIONED,
26
- HOST_CHANNEL_UNPROVISIONED,
27
30
  HostChannelUnavailableError,
28
31
  SshDisconnectedError,
29
- TRANSFER_EXCLUDES,
30
32
  commandForError,
33
+ isSshAuthError
34
+ } from "./chunk-N5YZMO33.js";
35
+ import {
36
+ HOST_CHANNEL_AUTH_REJECTED,
37
+ HOST_CHANNEL_NOT_PROVISIONED,
38
+ HOST_CHANNEL_UNPROVISIONED,
39
+ TRANSFER_EXCLUDES,
31
40
  explainHostChannelCause,
32
41
  formatBytes,
42
+ hostChannelAccount,
33
43
  hostFirewallRule,
34
44
  safeErrorMessage
35
- } from "./chunk-LEYSL46B.js";
45
+ } from "./chunk-2VSW6Q6C.js";
36
46
 
37
47
  // ../../packages/adapters/src/system/executor.ts
38
48
  import { existsSync, readFileSync } from "fs";
39
49
  import { networkInterfaces } from "os";
40
50
 
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
- }
96
-
97
51
  // ../../packages/adapters/src/system/ssh-executor.ts
98
52
  import { createReadStream } from "fs";
99
53
  import { mkdtemp as mkdtemp2, rm as fsRm2, stat } from "fs/promises";
@@ -1625,7 +1579,7 @@ function hostChannelPort() {
1625
1579
  return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : 22;
1626
1580
  }
1627
1581
  function hostChannelUser() {
1628
- return process.env.OPENSHIP_HOST_SSH_USER?.trim() || "root";
1582
+ return hostChannelAccount(process.env);
1629
1583
  }
1630
1584
  function readHostChannelKey(keyPath) {
1631
1585
  let contents;
@@ -1670,6 +1624,80 @@ function explainDialFailure(probe, host, port, target) {
1670
1624
  const cidr = containerBridgeCidr();
1671
1625
  return { hint, rule: hostFirewallRule("unknown", cidr ? [cidr] : [], port) };
1672
1626
  }
1627
+ var AUTH_MEMO_OK_MS = 3e4;
1628
+ var AUTH_MEMO_FAIL_MS = 5e3;
1629
+ var authMemo = null;
1630
+ function invalidateHostChannelAuth() {
1631
+ authMemo = null;
1632
+ }
1633
+ function probeForwarding(client, timeoutMs) {
1634
+ return new Promise((resolve) => {
1635
+ let settled = false;
1636
+ const finish = (verdict) => {
1637
+ if (settled) return;
1638
+ settled = true;
1639
+ clearTimeout(timer);
1640
+ resolve(verdict);
1641
+ };
1642
+ const timer = setTimeout(() => finish("unknown"), timeoutMs);
1643
+ timer.unref?.();
1644
+ try {
1645
+ client.forwardOut("127.0.0.1", 0, "127.0.0.1", 1, (err, stream) => {
1646
+ if (stream) {
1647
+ stream.destroy();
1648
+ finish("ok");
1649
+ return;
1650
+ }
1651
+ const reason = err?.reason;
1652
+ const message = err?.message;
1653
+ if (reason === 1 || reason === 3) return finish("blocked");
1654
+ if (typeof message === "string" && /administratively prohibited/i.test(message)) {
1655
+ return finish("blocked");
1656
+ }
1657
+ finish("ok");
1658
+ });
1659
+ } catch {
1660
+ finish("unknown");
1661
+ }
1662
+ });
1663
+ }
1664
+ async function verifyHostChannelAuth(config) {
1665
+ const memoKey = `${config.username}@${config.host}:${config.port}#${config.privateKey.length}`;
1666
+ if (authMemo?.key === memoKey && authMemo.expires > Date.now()) {
1667
+ return authMemo.hint ? { hint: authMemo.hint, forwarding: authMemo.forwarding } : { forwarding: authMemo.forwarding };
1668
+ }
1669
+ let hint = null;
1670
+ let forwarding = "unknown";
1671
+ try {
1672
+ const { connectSshClient: connectSshClient2 } = await import("./ssh-client-625VWGKP.js");
1673
+ const client = await connectSshClient2({
1674
+ host: config.host,
1675
+ port: config.port,
1676
+ username: config.username,
1677
+ privateKey: config.privateKey,
1678
+ readyTimeoutMs: config.timeoutMs,
1679
+ // Load-bearing: it selects describeSshAuthFailure's host-channel wording, so the
1680
+ // hint an operator reads here is the same sentence the deploy log gives them.
1681
+ hostChannel: true
1682
+ });
1683
+ forwarding = await probeForwarding(
1684
+ client,
1685
+ config.timeoutMs
1686
+ );
1687
+ client.end();
1688
+ } catch (err) {
1689
+ if (isSshAuthError(err)) {
1690
+ hint = err instanceof Error ? err.message : HOST_CHANNEL_AUTH_REJECTED;
1691
+ }
1692
+ }
1693
+ authMemo = {
1694
+ key: memoKey,
1695
+ hint,
1696
+ forwarding,
1697
+ expires: Date.now() + (hint ? AUTH_MEMO_FAIL_MS : AUTH_MEMO_OK_MS)
1698
+ };
1699
+ return hint ? { hint, forwarding } : { forwarding };
1700
+ }
1673
1701
  async function hostChannelHealth(timeoutMs = 2500) {
1674
1702
  if (hostControlDisabled()) {
1675
1703
  return {
@@ -1692,32 +1720,48 @@ async function hostChannelHealth(timeoutMs = 2500) {
1692
1720
  const port = hostChannelPort();
1693
1721
  const target = `${hostChannelUser()}@${host}:${port}`;
1694
1722
  const keyPath = process.env.OPENSHIP_HOST_SSH_KEY?.trim();
1723
+ let privateKey;
1695
1724
  if (keyPath) {
1696
- const { reason } = readHostChannelKey(keyPath);
1697
- if (reason) {
1725
+ const read = readHostChannelKey(keyPath);
1726
+ if (read.reason) {
1698
1727
  return {
1699
1728
  ok: false,
1700
1729
  code: "key_unreadable",
1701
1730
  host,
1702
1731
  port,
1703
1732
  target,
1704
- hint: `${reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
1733
+ hint: `${read.reason} ${HOST_CHANNEL_NOT_PROVISIONED}`
1705
1734
  };
1706
1735
  }
1736
+ privateKey = read.key;
1707
1737
  }
1708
1738
  const probe = await probeTcpDetailed(host, port, timeoutMs);
1709
- if (probe.ok) {
1710
- return { ok: true, code: "ok", host, port, target };
1739
+ if (!probe.ok) {
1740
+ return {
1741
+ ok: false,
1742
+ code: "unreachable",
1743
+ host,
1744
+ port,
1745
+ target,
1746
+ cause: probe.reason,
1747
+ ...explainDialFailure(probe, host, port, target)
1748
+ };
1711
1749
  }
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
- };
1750
+ let forwarding = "unknown";
1751
+ if (privateKey) {
1752
+ const verdict = await verifyHostChannelAuth({
1753
+ host,
1754
+ port,
1755
+ username: hostChannelUser(),
1756
+ privateKey,
1757
+ timeoutMs
1758
+ });
1759
+ if (verdict.hint) {
1760
+ return { ok: false, code: "auth_rejected", host, port, target, hint: verdict.hint };
1761
+ }
1762
+ forwarding = verdict.forwarding;
1763
+ }
1764
+ return { ok: true, code: "ok", host, port, target, forwarding };
1721
1765
  }
1722
1766
  function unavailableExecutor(reason, code = "not_configured") {
1723
1767
  const refuse = async () => {
@@ -1787,6 +1831,7 @@ export {
1787
1831
  setHostControlOverride,
1788
1832
  hostControlDisabled,
1789
1833
  containerBridgeCidr,
1834
+ invalidateHostChannelAuth,
1790
1835
  hostChannelHealth,
1791
1836
  unavailableExecutor,
1792
1837
  createHostExecutor