openship 0.5.0 → 0.6.1

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 (104) hide show
  1. package/README.md +26 -1
  2. package/dist/bare-IWS2QHJ3.js +23 -0
  3. package/dist/{bowser-O3AEA6AN.js → bowser-HNOTPGJO.js} +2 -2
  4. package/dist/{chunk-5MZ624VX.js → chunk-2GWYYHOR.js} +1 -1
  5. package/dist/{chunk-BS2AXYZX.js → chunk-2NZSUXOB.js} +647 -214
  6. package/dist/chunk-3FYIXVX5.js +8051 -0
  7. package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
  8. package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
  9. package/dist/{chunk-NNPEG6HM.js → chunk-3NZICQHF.js} +2 -2
  10. package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
  11. package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
  12. package/dist/{chunk-ZCOQJCZO.js → chunk-6ODUBKUB.js} +3 -3
  13. package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
  14. package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
  15. package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
  16. package/dist/{chunk-V7BZMGOT.js → chunk-EMXI4UCU.js} +5 -5
  17. package/dist/{chunk-RB35YN3J.js → chunk-EW6KB2J7.js} +391 -63
  18. package/dist/chunk-I3DPMTSF.js +29 -0
  19. package/dist/{chunk-NXFIRK2Z.js → chunk-IOT5ZX23.js} +573 -459
  20. package/dist/{chunk-KZOBYJUW.js → chunk-IW2UKBVF.js} +3 -3
  21. package/dist/{chunk-YWMODKWL.js → chunk-IXM5245F.js} +165 -13
  22. package/dist/{chunk-KNTJ7PSF.js → chunk-JIMUYEKN.js} +4 -4
  23. package/dist/{chunk-O6ZZF52V.js → chunk-ONKEYSMY.js} +2 -2
  24. package/dist/{chunk-UMQP7SYT.js → chunk-OTLSJYAC.js} +1316 -53
  25. package/dist/{chunk-EI575GHN.js → chunk-PDW4L3NV.js} +3 -3
  26. package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
  27. package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
  28. package/dist/{chunk-JLKKYUZA.js → chunk-RRJJGVKE.js} +7 -7
  29. package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
  30. package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
  31. package/dist/{chunk-6LKHXB6T.js → chunk-TWJCLD4W.js} +48 -13
  32. package/dist/{chunk-K2RPVGPN.js → chunk-UC7YIEHT.js} +36 -17
  33. package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
  34. package/dist/{chunk-6PIYRG5I.js → chunk-VZ2ZHPS2.js} +52 -42
  35. package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
  36. package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
  37. package/dist/chunk-YIPMDGG6.js +703 -0
  38. package/dist/{chunk-RPHRPFEH.js → chunk-ZCFVHA25.js} +1 -1
  39. package/dist/{chunk-FPRHYBY2.js → chunk-ZFJXPCQP.js} +1 -1
  40. package/dist/{chunk-JYGD5WSN.js → chunk-ZWLDLTJP.js} +1 -1
  41. package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
  42. package/dist/cloud-M2YLWEXO.js +31 -0
  43. package/dist/{detect-BAJMY6KM.js → detect-ALF4GXRX.js} +7 -8
  44. package/dist/{dist-IQJ6H6UZ.js → dist-BM6NPWIH.js} +3 -3
  45. package/dist/{dist-es-X3B5QYKB.js → dist-es-2YRDXMUU.js} +6 -6
  46. package/dist/{dist-es-OSNJBLXB.js → dist-es-CCVZEFRZ.js} +5 -5
  47. package/dist/{dist-es-VNKHQVIB.js → dist-es-IOH6G4GM.js} +4 -4
  48. package/dist/{dist-es-RYHIPOTN.js → dist-es-JGBPABJI.js} +14 -14
  49. package/dist/{dist-es-JSTDUS7Q.js → dist-es-KWSMDPIP.js} +6 -6
  50. package/dist/{dist-es-5DY7K32L.js → dist-es-UYDDOVZ3.js} +6 -6
  51. package/dist/{dist-es-WV4AMOII.js → dist-es-XUOR4NQN.js} +8 -8
  52. package/dist/docker-QECQJ5P4.js +32 -0
  53. package/dist/docker-edge-executor-62N64MGS.js +17 -0
  54. package/dist/edge-import-SAYTRJLO.js +48 -0
  55. package/dist/ensure-container-edge-CFSCPGZK.js +32 -0
  56. package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
  57. package/dist/{executor-LOYMPU67.js → executor-PT6UEJ5M.js} +8 -8
  58. package/dist/index.js +1228 -463
  59. package/dist/{loadSso-46FQRIZS.js → loadSso-UGF4JMRK.js} +10 -10
  60. package/dist/{local-executor-V7U5VOZW.js → local-executor-WOFZD72X.js} +4 -4
  61. package/dist/nginx-YOEEFBTE.js +31 -0
  62. package/dist/node-entry.js +4 -0
  63. package/dist/{noop-FMMFG2EZ.js → noop-NJ72F46T.js} +3 -3
  64. package/dist/openresty-lua-WGBO2M3Z.js +81 -0
  65. package/dist/server/index.js +23276 -8015
  66. package/dist/server/lua/geo_country.lua +34 -8
  67. package/dist/server/lua/maxminddb.lua +403 -0
  68. package/dist/server/lua/mgmt_api.lua +431 -68
  69. package/dist/server/lua/pipe_stream.lua +52 -39
  70. package/dist/server/lua/site_logger.lua +236 -30
  71. package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
  72. package/dist/server/migrations/0083_service_incident.sql +84 -0
  73. package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
  74. package/dist/server/migrations/0085_resource_usage.sql +68 -0
  75. package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
  76. package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
  77. package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
  78. package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
  79. package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
  80. package/dist/server/migrations/0091_project_server_id.sql +26 -0
  81. package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
  82. package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
  83. package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
  84. package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
  85. package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
  86. package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
  87. package/dist/server/migrations/meta/_journal.json +112 -0
  88. package/dist/setup-M2ZDGH3O.js +21 -0
  89. package/dist/{signin-ITOPKKUB.js → signin-3M3UPB6K.js} +10 -10
  90. package/dist/{sso-oidc-NON2PD7Y.js → sso-oidc-3VDIYOEJ.js} +10 -10
  91. package/dist/{sts-HHFFSAY2.js → sts-B3WIYLXR.js} +9 -9
  92. package/package.json +6 -3
  93. package/dist/bare-VRGE5MXH.js +0 -23
  94. package/dist/chunk-CUJXGK7R.js +0 -3167
  95. package/dist/chunk-EAAGSQXN.js +0 -377
  96. package/dist/cloud-3M2WZ624.js +0 -30
  97. package/dist/docker-2NGBRJFJ.js +0 -18
  98. package/dist/docker-edge-executor-5I4M5QT7.js +0 -13
  99. package/dist/edge-import-RIWNEIXQ.js +0 -47
  100. package/dist/ensure-container-edge-3NSYKHRX.js +0 -31
  101. package/dist/nginx-QE4DOM5J.js +0 -24
  102. package/dist/openresty-lua-BTAKECCF.js +0 -42
  103. package/dist/server/lua/pipe_log.lua +0 -71
  104. package/dist/setup-KELALK7H.js +0 -20
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env sh
2
- ":" //# ; exec "$(command -v bun || command -v node)" "$0" "$@"
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
5
  import {
6
6
  resolveEnvironment
7
- } from "./chunk-KZOBYJUW.js";
7
+ } from "./chunk-IW2UKBVF.js";
8
8
  import {
9
9
  formatDuration,
10
10
  isRemoteConnectionError,
11
11
  systemDebug
12
- } from "./chunk-RPHRPFEH.js";
12
+ } from "./chunk-ZCFVHA25.js";
13
13
  import {
14
14
  containerCommand,
15
15
  edgeContainerExecutor,
@@ -17,41 +17,250 @@ import {
17
17
  readDeclaredPair,
18
18
  readMaybeInContainer,
19
19
  validateCertFor
20
- } from "./chunk-RB35YN3J.js";
21
- import {
22
- sq
23
- } from "./chunk-FPRHYBY2.js";
20
+ } from "./chunk-EW6KB2J7.js";
24
21
  import {
25
22
  EDGE_CONTAINER_NAME,
26
23
  EdgeConflictError,
27
24
  EdgeMigrateRequested,
25
+ edgeCrashReason,
28
26
  edgeFailureReason,
27
+ edgeIsBroken,
29
28
  freeEdgeTargets,
30
29
  invalidateEdgeContainer,
31
- ourLuaOnHost,
32
30
  probeEdge,
33
31
  resolveOurEdgeContainer,
34
32
  sanitizeEdgeVhosts,
35
33
  sq as sq2,
36
34
  stopTargetsForStatus
37
- } from "./chunk-6PIYRG5I.js";
35
+ } from "./chunk-VZ2ZHPS2.js";
38
36
  import {
39
37
  waitForPortListening
40
- } from "./chunk-ILFETZOM.js";
38
+ } from "./chunk-3LDWWY7U.js";
41
39
  import {
40
+ EDGE_CHALLENGE_HOST_DIR,
42
41
  EDGE_CONTAINER_MOUNTS,
43
42
  EDGE_HOST_PATHS,
44
- OPENRESTY_DEFAULT_PATHS,
45
- detectOpenRestyPaths
46
- } from "./chunk-EAAGSQXN.js";
43
+ OPENRESTY_DEFAULT_PATHS
44
+ } from "./chunk-YIPMDGG6.js";
45
+ import {
46
+ sq
47
+ } from "./chunk-ZFJXPCQP.js";
47
48
  import {
49
+ AppError,
50
+ PROXY_DIRECTIVES,
48
51
  buildEdgeImageRef,
49
52
  compareSemver,
50
- safeErrorMessage
51
- } from "./chunk-UMQP7SYT.js";
53
+ parseProxyValue,
54
+ safeErrorMessage,
55
+ sanitizeProxySettings
56
+ } from "./chunk-OTLSJYAC.js";
57
+
58
+ // ../../packages/adapters/src/system/managed-image.ts
59
+ import { posix } from "path";
60
+ function log(message, level = "info") {
61
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
62
+ }
63
+ function titleCase(label) {
64
+ return label.charAt(0).toUpperCase() + label.slice(1);
65
+ }
66
+ var fromSourceManaged = { edge: false, mail: false };
67
+ function managedImagesAreFromSource(kind) {
68
+ return fromSourceManaged[kind];
69
+ }
70
+ function fromSourceImageMissingMessage(label, image) {
71
+ return `The from-source ${label} image ${image} isn't on this server \u2014 the control-plane build or transfer didn't complete (see the log above). It's an unpublished dev tag, so there's nothing to pull; fix the build and retry.`;
72
+ }
73
+ async function dockerAvailable(executor) {
74
+ return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
75
+ }
76
+ async function containerState(executor, container) {
77
+ const out = await executor.exec(`docker inspect -f '{{.State.Running}} {{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
78
+ const line = out.split("\n").map((l) => l.trim()).find(Boolean);
79
+ if (!line) return null;
80
+ const [running, image] = line.split(" ");
81
+ return { running: running?.trim() === "true", image: image?.trim() || null };
82
+ }
83
+ async function containerImageRef(executor, container) {
84
+ return (await containerState(executor, container))?.image ?? null;
85
+ }
86
+ async function imageExistsLocally(executor, ref) {
87
+ const out = await executor.exec(`docker image inspect -f '{{.Id}}' ${sq(ref)} 2>/dev/null`).catch(() => "");
88
+ return Boolean(out.trim());
89
+ }
90
+ async function swapManagedImage(executor, opts) {
91
+ const { kind, from, to, label, onLog, start } = opts;
92
+ const Label = titleCase(label);
93
+ onLog(log(`Updating the ${label}: ${from} \u2192 ${to}`));
94
+ if (!await imageExistsLocally(executor, to)) {
95
+ if (managedImagesAreFromSource(kind)) {
96
+ onLog(log(fromSourceImageMissingMessage(label, to), "warn"));
97
+ return { swapped: false, down: false };
98
+ }
99
+ const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
100
+ if (pull.code !== 0) {
101
+ onLog(log(`Could not pull ${to} \u2014 the ${label} stays on ${from}.`, "warn"));
102
+ return { swapped: false, down: false };
103
+ }
104
+ }
105
+ if (await start(to)) {
106
+ onLog(log(`${Label} updated to ${to}`));
107
+ return { swapped: true, down: false };
108
+ }
109
+ onLog(log(`${Label} ${to} failed to come up \u2014 rolling back to ${from}...`, "error"));
110
+ if (await start(from)) {
111
+ return { swapped: false, down: false };
112
+ }
113
+ onLog(log(`Rollback to ${from} ALSO failed \u2014 the ${label} is down on this server.`, "error"));
114
+ return { swapped: false, down: true };
115
+ }
116
+
117
+ // ../../packages/adapters/src/system/proxy/takeover-journal.ts
118
+ var JOURNAL_DIR = "/var/lib/openship";
119
+ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
120
+ function log2(message, level = "info") {
121
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
122
+ }
123
+ async function tryExec(executor, cmd) {
124
+ try {
125
+ return await executor.exec(cmd);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ async function buildJournal(executor, status) {
131
+ const units = /* @__PURE__ */ new Map();
132
+ const containers = /* @__PURE__ */ new Map();
133
+ const processes = /* @__PURE__ */ new Map();
134
+ for (const o of status.occupants) {
135
+ if (o.containerName && !containers.has(o.containerName)) {
136
+ const r = await tryExec(
137
+ executor,
138
+ `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
139
+ );
140
+ containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
141
+ } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
142
+ const en = await tryExec(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
143
+ units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
144
+ } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
145
+ processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
146
+ }
147
+ }
148
+ return {
149
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
150
+ units: [...units.values()],
151
+ containers: [...containers.values()],
152
+ processes: [...processes.values()]
153
+ };
154
+ }
155
+ async function writeJournal(executor, journal) {
156
+ try {
157
+ await executor.mkdir(JOURNAL_DIR);
158
+ await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
159
+ } catch {
160
+ }
161
+ }
162
+ async function clearJournal(executor) {
163
+ await tryExec(executor, `rm -f ${JOURNAL_PATH}`);
164
+ }
165
+ async function readJournal(executor) {
166
+ const raw = await tryExec(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
167
+ if (!raw?.trim()) return null;
168
+ try {
169
+ return JSON.parse(raw);
170
+ } catch {
171
+ await clearJournal(executor);
172
+ return null;
173
+ }
174
+ }
175
+ async function rollback(executor, journal, onLog) {
176
+ onLog(log2("Rolling back \u2014 restoring the previous proxy...", "warn"));
177
+ await tryExec(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
178
+ await tryExec(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
179
+ await tryExec(
180
+ executor,
181
+ "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
182
+ );
183
+ for (const u of journal.units) {
184
+ await tryExec(
185
+ executor,
186
+ u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
187
+ );
188
+ }
189
+ for (const c of journal.containers) {
190
+ await tryExec(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
191
+ await tryExec(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
192
+ }
193
+ for (const p of journal.processes ?? []) {
194
+ if (p.command) {
195
+ await tryExec(
196
+ executor,
197
+ `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
198
+ );
199
+ } else {
200
+ onLog(log2(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
201
+ }
202
+ }
203
+ const restored = await portIsServed(executor, 80);
204
+ if (!restored) {
205
+ onLog(
206
+ log2(
207
+ "Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
208
+ "error"
209
+ )
210
+ );
211
+ }
212
+ return restored;
213
+ }
214
+ async function portIsServed(executor, port) {
215
+ const hex = port.toString(16).toUpperCase().padStart(4, "0");
216
+ const out = await tryExec(
217
+ executor,
218
+ `(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
219
+ );
220
+ return (out ?? "").includes("yes");
221
+ }
222
+ async function beginEdgeTakeover(executor, status, onLog, targets) {
223
+ await writeJournal(executor, await buildJournal(executor, status));
224
+ return freeEdgeTargets(
225
+ executor,
226
+ targets?.length ? targets : stopTargetsForStatus(status),
227
+ (m, l) => onLog(log2(m, l))
228
+ );
229
+ }
230
+ async function rollbackEdgeTakeover(executor, onLog) {
231
+ const journal = await readJournal(executor);
232
+ if (!journal || journal.completed) return false;
233
+ const restored = await rollback(executor, journal, onLog);
234
+ await clearJournal(executor);
235
+ return restored;
236
+ }
237
+ async function completeEdgeTakeover(executor) {
238
+ const journal = await readJournal(executor);
239
+ if (journal) {
240
+ journal.completed = true;
241
+ await writeJournal(executor, journal);
242
+ }
243
+ await clearJournal(executor);
244
+ }
245
+ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
246
+ const journal = await readJournal(executor);
247
+ if (!journal) return;
248
+ if (journal.completed) {
249
+ await clearJournal(executor);
250
+ return;
251
+ }
252
+ if (isEdgeHealthy && await isEdgeHealthy()) {
253
+ onLog(log2("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
254
+ await clearJournal(executor);
255
+ return;
256
+ }
257
+ onLog(log2("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
258
+ await rollback(executor, journal, onLog);
259
+ await clearJournal(executor);
260
+ }
52
261
 
53
262
  // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
54
- async function tryExec(executor, command) {
263
+ async function tryExec2(executor, command) {
55
264
  try {
56
265
  return await executor.exec(command);
57
266
  } catch {
@@ -59,7 +268,21 @@ async function tryExec(executor, command) {
59
268
  }
60
269
  }
61
270
  function stripComments(text) {
62
- return text.replace(/#.*$/gm, "");
271
+ return text.split("\n").map((line) => {
272
+ let quote;
273
+ for (let i = 0; i < line.length; i++) {
274
+ const ch = line[i];
275
+ if (quote) {
276
+ if (ch === "\\") i++;
277
+ else if (ch === quote) quote = void 0;
278
+ } else if (ch === '"' || ch === "'") {
279
+ quote = ch;
280
+ } else if (ch === "#") {
281
+ return line.slice(0, i);
282
+ }
283
+ }
284
+ return line;
285
+ }).join("\n");
63
286
  }
64
287
  function extractBlocks(text, keyword) {
65
288
  const blocks = [];
@@ -105,10 +328,17 @@ function sq3(value) {
105
328
  }
106
329
 
107
330
  // ../../packages/adapters/src/system/proxy/import/nginx.ts
331
+ async function dumpResolvedConfig(executor, bins) {
332
+ for (const bin of bins) {
333
+ const dumped = await tryExec2(executor, `${bin} -T 2>/dev/null`);
334
+ if (dumped && /server\s*\{/.test(dumped)) return dumped;
335
+ }
336
+ return null;
337
+ }
108
338
  async function loadNginxConfig(executor) {
109
- const dumped = await tryExec(executor, "nginx -T 2>/dev/null");
110
- if (dumped && /server\s*\{/.test(dumped)) return dumped;
111
- const cat = await tryExec(
339
+ const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
340
+ if (dumped) return dumped;
341
+ const cat = await tryExec2(
112
342
  executor,
113
343
  "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
114
344
  );
@@ -146,7 +376,7 @@ function resolveProxyTarget(proxyPass, upstreams) {
146
376
  if (isIp || host.includes(".")) return { url: raw };
147
377
  return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
148
378
  }
149
- function extractLocationProxies(serverBody) {
379
+ function locationBlocks(serverBody) {
150
380
  const out = [];
151
381
  const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
152
382
  let m;
@@ -160,13 +390,26 @@ function extractLocationProxies(serverBody) {
160
390
  else if (serverBody[i] === "}") depth--;
161
391
  }
162
392
  if (depth !== 0) break;
163
- const locBody = serverBody.slice(openIdx + 1, i - 1);
393
+ out.push({ path, body: serverBody.slice(openIdx + 1, i - 1) });
164
394
  re.lastIndex = i;
165
- const pp = firstDirective(locBody, "proxy_pass");
166
- if (pp) out.push({ path, proxyPass: pp });
167
395
  }
168
396
  return out;
169
397
  }
398
+ function extractLocationProxies(serverBody) {
399
+ const out = [];
400
+ for (const loc of locationBlocks(serverBody)) {
401
+ const pp = firstDirective(loc.body, "proxy_pass");
402
+ if (pp) out.push({ path: loc.path, proxyPass: pp });
403
+ }
404
+ return out;
405
+ }
406
+ function withoutQuotedValues(body) {
407
+ return body.replace(/'[^'\n]*'/g, "''").replace(/"[^"\n]*"/g, '""');
408
+ }
409
+ var SERVING_DIRECTIVE_RE = /(?:^|[;{}\s])(?:proxy_pass|root|alias|fastcgi_pass|uwsgi_pass|scgi_pass|grpc_pass)\s/;
410
+ function servesNothing(locationBody) {
411
+ return !SERVING_DIRECTIVE_RE.test(withoutQuotedValues(locationBody));
412
+ }
170
413
  function isHttpsUpgradeForSelf(body, names) {
171
414
  const targets = [
172
415
  ...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
@@ -181,15 +424,31 @@ function isHttpsUpgradeForSelf(body, names) {
181
424
  return names.includes(host);
182
425
  });
183
426
  }
427
+ var CHALLENGE_LOCATION_RE = /\.well-known\/(acme-challenge|oblien-proxy-challenge)/i;
184
428
  function isAcmeWebrootOnly(body) {
185
- const paths = [...body.matchAll(/(?:^|[\s;}])location\s+([^{]+?)\s*\{/g)].map(
186
- (m) => m[1].trim()
187
- );
188
- if (paths.length === 0) {
429
+ const locations = locationBlocks(body);
430
+ if (locations.length === 0) {
189
431
  const root = firstDirective(body, "root") ?? "";
190
432
  return /letsencrypt|acme|certbot/i.test(root);
191
433
  }
192
- return paths.every((p) => /\.well-known\/acme-challenge/i.test(p));
434
+ if (!locations.some((l) => CHALLENGE_LOCATION_RE.test(l.path))) return false;
435
+ return locations.every((l) => CHALLENGE_LOCATION_RE.test(l.path) || servesNothing(l.body));
436
+ }
437
+ function parseProxyDirectives(body) {
438
+ const raw = {};
439
+ const candidate = {};
440
+ for (const spec of PROXY_DIRECTIVES) {
441
+ const found = firstDirective(body, spec.directive);
442
+ if (found === void 0) continue;
443
+ raw[spec.key] = found;
444
+ const parsed = parseProxyValue(spec, found);
445
+ if (parsed !== void 0) candidate[spec.key] = parsed;
446
+ }
447
+ const settings = sanitizeProxySettings(candidate);
448
+ return {
449
+ ...settings ? { settings } : {},
450
+ ...Object.keys(raw).length > 0 ? { raw } : {}
451
+ };
193
452
  }
194
453
  function parseServer(body, source, upstreams) {
195
454
  const warnings = [];
@@ -230,6 +489,9 @@ function parseServer(body, source, upstreams) {
230
489
  const site = { serverNames: names, ssl, target, source };
231
490
  if (routes) site.routes = routes;
232
491
  if (certPath && keyPath) site.tls = { certPath, keyPath };
492
+ const tunables = parseProxyDirectives(body);
493
+ if (tunables.settings) site.proxy = tunables.settings;
494
+ if (tunables.raw) site.proxyRaw = tunables.raw;
233
495
  return { site, warnings };
234
496
  }
235
497
  function parseNginxConfig(raw) {
@@ -264,21 +526,31 @@ var OUR_EDGE_SITE_GLOBS = [
264
526
  `${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
265
527
  "/etc/openresty/sites-enabled/*.conf"
266
528
  ];
529
+ async function scanForeignOpenResty(executor) {
530
+ const dump = await dumpResolvedConfig(executor, ["openresty"]);
531
+ if (dump) return parseNginxConfig(dump);
532
+ return scanOpenshipEdge(executor);
533
+ }
267
534
  async function scanOpenshipEdge(executor) {
268
535
  const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
269
- const raw = await tryExec(executor, cat);
536
+ const raw = await tryExec2(executor, cat);
270
537
  if (raw?.trim()) return parseNginxConfig(raw);
271
538
  const container = await resolveOurEdgeContainer(executor);
272
539
  if (!container) return parseNginxConfig("");
273
- const fromContainer = await tryExec(executor, containerCommand(container, cat));
540
+ const fromContainer = await tryExec2(executor, containerCommand(container, cat));
274
541
  return parseNginxConfig(fromContainer ?? "");
275
542
  }
276
543
 
277
544
  // ../../packages/adapters/src/system/proxy/import/caddy.ts
278
545
  var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
546
+ async function loadCaddyAdminConfig(executor) {
547
+ const endpoint = "http://localhost:2019/config/";
548
+ const out = await tryExec2(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec2(executor, `wget -qO- ${endpoint} 2>/dev/null`);
549
+ return out && out.trim().startsWith("{") ? out : null;
550
+ }
279
551
  async function loadCaddyJson(executor) {
280
552
  for (const p of CADDYFILE_PATHS) {
281
- const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
553
+ const out = await tryExec2(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
282
554
  if (out && out.trim().startsWith("{")) return out;
283
555
  }
284
556
  return null;
@@ -393,7 +665,7 @@ function parseCaddyJson(raw) {
393
665
  }
394
666
  async function loadCaddyfile(executor) {
395
667
  for (const p of CADDYFILE_PATHS) {
396
- const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
668
+ const out = await tryExec2(executor, `cat ${p} 2>/dev/null`);
397
669
  if (out && out.trim()) return out;
398
670
  }
399
671
  return "";
@@ -430,8 +702,9 @@ function parseAddresses(header) {
430
702
  }).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
431
703
  }
432
704
  async function scanCaddy(executor) {
433
- const json = await loadCaddyJson(executor);
434
- if (json) {
705
+ for (const load of [loadCaddyAdminConfig, loadCaddyJson]) {
706
+ const json = await load(executor);
707
+ if (!json) continue;
435
708
  const parsed = parseCaddyJson(json);
436
709
  if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
437
710
  return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
@@ -527,7 +800,7 @@ async function scanCaddyText(executor) {
527
800
 
528
801
  // ../../packages/adapters/src/system/proxy/import/apache.ts
529
802
  async function vhostFilesFromDump(executor) {
530
- const dump = await tryExec(
803
+ const dump = await tryExec2(
531
804
  executor,
532
805
  "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
533
806
  );
@@ -542,18 +815,22 @@ async function vhostFilesFromDump(executor) {
542
815
  async function loadApacheConfig(executor) {
543
816
  const files = await vhostFilesFromDump(executor);
544
817
  if (files.length > 0) {
545
- const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
818
+ const cat2 = await tryExec2(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
546
819
  if (cat2 && cat2.trim()) return cat2;
547
820
  }
548
- const cat = await tryExec(
821
+ const cat = await tryExec2(
549
822
  executor,
550
823
  "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
551
824
  );
552
825
  return cat ?? "";
553
826
  }
827
+ function unquote(value) {
828
+ return value.replace(/^["']|["']$/g, "");
829
+ }
554
830
  function directive(body, name) {
555
831
  const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
556
- return m?.[1]?.trim();
832
+ const value = m?.[1]?.trim();
833
+ return value === void 0 ? void 0 : unquote(value);
557
834
  }
558
835
  function directiveAll(body, name) {
559
836
  return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
@@ -570,7 +847,36 @@ function isHttpsRedirectOnly(body) {
570
847
  return redirects.length > 0 && redirects.some((t) => /^https:\/\//i.test(t));
571
848
  }
572
849
  function hostOnly(name) {
573
- return name.trim().replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
850
+ return unquote(name.trim()).replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
851
+ }
852
+ function proxyRoutes(body) {
853
+ const routes = [];
854
+ const scopes = [];
855
+ let regexScoped = false;
856
+ for (const line of body.split("\n")) {
857
+ const open = line.match(/^\s*<Location(Match)?\s+([^>]*)>/i);
858
+ if (open) {
859
+ const regex = Boolean(open[1]);
860
+ const arg = unquote(open[2].trim());
861
+ scopes.push({ path: !regex && arg.startsWith("/") ? arg : null, regex });
862
+ continue;
863
+ }
864
+ if (/^\s*<\/Location(?:Match)?\s*>/i.test(line)) {
865
+ scopes.pop();
866
+ continue;
867
+ }
868
+ const m = line.match(/^\s*ProxyPass\s+(\S+)(?:\s+(\S+))?/i);
869
+ if (!m) continue;
870
+ const first = unquote(m[1]);
871
+ if (first.startsWith("/")) {
872
+ routes.push({ path: first, url: m[2] ? unquote(m[2]) : "" });
873
+ continue;
874
+ }
875
+ const scope = scopes[scopes.length - 1];
876
+ if (scope?.path) routes.push({ path: scope.path, url: first });
877
+ else if (scope?.regex) regexScoped = true;
878
+ }
879
+ return { routes, regexScoped };
574
880
  }
575
881
  function parseVhost(body, portHint) {
576
882
  const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
@@ -580,8 +886,9 @@ function parseVhost(body, portHint) {
580
886
  return { warning: "apache: skipped a VirtualHost with no ServerName" };
581
887
  }
582
888
  if (isHttpsRedirectOnly(body)) return null;
583
- const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
584
- const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
889
+ const proxy = proxyRoutes(body);
890
+ const routes = proxy.routes.filter((p) => p.url && p.url !== "!");
891
+ const docRoot = directive(body, "DocumentRoot");
585
892
  const fcgiMatch = /(?:^|\n)\s*ProxyPassMatch\s+[^\n]*(fcgi:|php-fpm|php\d*-fpm|\.sock)/i.test(body);
586
893
  const certPath = directive(body, "SSLCertificateFile");
587
894
  const keyPath = directive(body, "SSLCertificateKeyFile");
@@ -605,6 +912,10 @@ function parseVhost(body, portHint) {
605
912
  return {
606
913
  warning: `apache: ${names[0]} uses ProxyPassMatch (regex proxying) \u2014 re-add it manually`
607
914
  };
915
+ } else if (proxy.regexScoped) {
916
+ return {
917
+ warning: `apache: ${names[0]} proxies from inside a <LocationMatch> (regex proxying) \u2014 re-add it manually`
918
+ };
608
919
  } else {
609
920
  return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
610
921
  }
@@ -834,7 +1145,7 @@ function parseTraefikLabels(containers) {
834
1145
  };
835
1146
  }
836
1147
  async function scanTraefik(executor) {
837
- const out = await tryExec(
1148
+ const out = await tryExec2(
838
1149
  executor,
839
1150
  `docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range $n, $v := .NetworkSettings.Networks}}{{$n}}={{$v.IPAddress}} {{end}} {{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}} {{if .Config.Cmd}}{{join .Config.Cmd " "}}{{end}}' 2>/dev/null`
840
1151
  );
@@ -916,7 +1227,7 @@ async function scanImportableSites(executor, proxy) {
916
1227
  case "traefik":
917
1228
  return scanTraefik(executor);
918
1229
  case "openresty":
919
- return scanOpenshipEdge(executor);
1230
+ return scanForeignOpenResty(executor);
920
1231
  default:
921
1232
  return {
922
1233
  proxy,
@@ -928,6 +1239,27 @@ async function scanImportableSites(executor, proxy) {
928
1239
  function canImportProxy(proxy) {
929
1240
  return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
930
1241
  }
1242
+ function unreachableStaticRoots(sites, opts) {
1243
+ if (!opts.containerEdge) return [];
1244
+ const visible = EDGE_CONTAINER_MOUNTS.map((m) => m.host.replace(/\/+$/, ""));
1245
+ const out = [];
1246
+ for (const site of sites) {
1247
+ if (site.target.kind !== "static") continue;
1248
+ const root = site.target.root.replace(/\/+$/, "");
1249
+ const reachable = visible.some((v) => root === v || root.startsWith(`${v}/`));
1250
+ if (!reachable) out.push({ host: site.serverNames[0] ?? root, root: site.target.root });
1251
+ }
1252
+ return out;
1253
+ }
1254
+ var EDGE_STATIC_MOUNT = EDGE_CONTAINER_MOUNTS.find((m) => m.host === "/opt/openship/static")?.host ?? "/opt/openship/static";
1255
+ async function copyStaticRootIntoEdge(executor, opts) {
1256
+ const slug = opts.host.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "site";
1257
+ const src = opts.root.replace(/\/+$/, "");
1258
+ const dest = `${EDGE_STATIC_MOUNT}/_adopted/${slug}`;
1259
+ await executor.exec(`mkdir -p ${sq2(dest)}`);
1260
+ await executor.exec(`cp -a ${sq2(`${src}/.`)} ${sq2(`${dest}/`)}`);
1261
+ return dest;
1262
+ }
931
1263
 
932
1264
  // ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
933
1265
  var CADDY_DATA_ROOTS = [
@@ -945,10 +1277,10 @@ function candidateNames(host) {
945
1277
  async function findPairIn(exec, root, host, container) {
946
1278
  for (const name of candidateNames(host)) {
947
1279
  const glob = `${root}/certificates/*/${name}/${name}.crt`;
948
- let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
1280
+ let certPath = firstCrt(await tryExec2(exec, `ls -1 ${glob} 2>/dev/null`));
949
1281
  if (!certPath && container) {
950
1282
  certPath = firstCrt(
951
- await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
1283
+ await tryExec2(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
952
1284
  );
953
1285
  }
954
1286
  if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
@@ -994,7 +1326,7 @@ async function candidateStoragePaths(exec, container) {
994
1326
  };
995
1327
  if (container) {
996
1328
  collect(
997
- await tryExec(
1329
+ await tryExec2(
998
1330
  exec,
999
1331
  `docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
1000
1332
  )
@@ -1098,11 +1430,13 @@ function buildProxyRouteIndex(sites) {
1098
1430
  existing.ssl.keyPath = site.tls.keyPath;
1099
1431
  }
1100
1432
  }
1433
+ if (site.proxy && (site.ssl || !existing.proxy)) existing.proxy = site.proxy;
1101
1434
  } else {
1102
1435
  list.push({
1103
1436
  port,
1104
1437
  path: up.path,
1105
1438
  domains: [...site.serverNames],
1439
+ ...site.proxy ? { proxy: site.proxy } : {},
1106
1440
  ssl: site.ssl ? { enabled: true, certPath: site.tls?.certPath, keyPath: site.tls?.keyPath } : { enabled: false },
1107
1441
  source: site.source
1108
1442
  });
@@ -1159,7 +1493,7 @@ async function edgeProxy(exec, opts = {}) {
1159
1493
  return makeApi(exec, kind, ours, container);
1160
1494
  }
1161
1495
  function edgeProxyFor(exec, kind, opts = {}) {
1162
- return makeApi(exec, kind, opts.ours ?? kind === "openresty", opts.container ?? null);
1496
+ return makeApi(exec, kind, opts.ours ?? false, opts.container ?? null);
1163
1497
  }
1164
1498
  async function collectProxyCerts(api, sites) {
1165
1499
  const certPems = {};
@@ -1223,147 +1557,6 @@ function makeApi(exec, kind, ours, container) {
1223
1557
  };
1224
1558
  }
1225
1559
 
1226
- // ../../packages/adapters/src/system/proxy/takeover-journal.ts
1227
- var JOURNAL_DIR = "/var/lib/openship";
1228
- var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
1229
- function log(message, level = "info") {
1230
- return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1231
- }
1232
- async function tryExec2(executor, cmd) {
1233
- try {
1234
- return await executor.exec(cmd);
1235
- } catch {
1236
- return null;
1237
- }
1238
- }
1239
- async function buildJournal(executor, status) {
1240
- const units = /* @__PURE__ */ new Map();
1241
- const containers = /* @__PURE__ */ new Map();
1242
- const processes = /* @__PURE__ */ new Map();
1243
- for (const o of status.occupants) {
1244
- if (o.containerName && !containers.has(o.containerName)) {
1245
- const r = await tryExec2(
1246
- executor,
1247
- `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
1248
- );
1249
- containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
1250
- } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
1251
- const en = await tryExec2(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
1252
- units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
1253
- } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
1254
- processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
1255
- }
1256
- }
1257
- return {
1258
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1259
- units: [...units.values()],
1260
- containers: [...containers.values()],
1261
- processes: [...processes.values()]
1262
- };
1263
- }
1264
- async function writeJournal(executor, journal) {
1265
- try {
1266
- await executor.mkdir(JOURNAL_DIR);
1267
- await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
1268
- } catch {
1269
- }
1270
- }
1271
- async function clearJournal(executor) {
1272
- await tryExec2(executor, `rm -f ${JOURNAL_PATH}`);
1273
- }
1274
- async function readJournal(executor) {
1275
- const raw = await tryExec2(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
1276
- if (!raw?.trim()) return null;
1277
- try {
1278
- return JSON.parse(raw);
1279
- } catch {
1280
- await clearJournal(executor);
1281
- return null;
1282
- }
1283
- }
1284
- async function rollback(executor, journal, onLog) {
1285
- onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
1286
- await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
1287
- await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
1288
- await tryExec2(
1289
- executor,
1290
- "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
1291
- );
1292
- for (const u of journal.units) {
1293
- await tryExec2(
1294
- executor,
1295
- u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
1296
- );
1297
- }
1298
- for (const c of journal.containers) {
1299
- await tryExec2(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
1300
- await tryExec2(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
1301
- }
1302
- for (const p of journal.processes ?? []) {
1303
- if (p.command) {
1304
- await tryExec2(
1305
- executor,
1306
- `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
1307
- );
1308
- } else {
1309
- onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1310
- }
1311
- }
1312
- const restored = await portIsServed(executor, 80);
1313
- if (!restored) {
1314
- onLog(
1315
- log(
1316
- "Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
1317
- "error"
1318
- )
1319
- );
1320
- }
1321
- return restored;
1322
- }
1323
- async function portIsServed(executor, port) {
1324
- const hex = port.toString(16).toUpperCase().padStart(4, "0");
1325
- const out = await tryExec2(
1326
- executor,
1327
- `(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
1328
- );
1329
- return (out ?? "").includes("yes");
1330
- }
1331
- async function beginEdgeTakeover(executor, status, onLog) {
1332
- await writeJournal(executor, await buildJournal(executor, status));
1333
- await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
1334
- }
1335
- async function rollbackEdgeTakeover(executor, onLog) {
1336
- const journal = await readJournal(executor);
1337
- if (!journal || journal.completed) return false;
1338
- const restored = await rollback(executor, journal, onLog);
1339
- await clearJournal(executor);
1340
- return restored;
1341
- }
1342
- async function completeEdgeTakeover(executor) {
1343
- const journal = await readJournal(executor);
1344
- if (journal) {
1345
- journal.completed = true;
1346
- await writeJournal(executor, journal);
1347
- }
1348
- await clearJournal(executor);
1349
- }
1350
- async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
1351
- const journal = await readJournal(executor);
1352
- if (!journal) return;
1353
- if (journal.completed) {
1354
- await clearJournal(executor);
1355
- return;
1356
- }
1357
- if (isEdgeHealthy && await isEdgeHealthy()) {
1358
- onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
1359
- await clearJournal(executor);
1360
- return;
1361
- }
1362
- onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
1363
- await rollback(executor, journal, onLog);
1364
- await clearJournal(executor);
1365
- }
1366
-
1367
1560
  // ../../packages/adapters/src/system/catalog.ts
1368
1561
  function dockerInstallPlan(profile) {
1369
1562
  if (profile.os !== "linux") {
@@ -1421,120 +1614,15 @@ function rsyncInstallPlan(profile) {
1421
1614
  verifyCommand: "rsync --version | head -n 1"
1422
1615
  };
1423
1616
  }
1424
- var OPENRESTY_APT_SOURCES = [
1425
- // ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
1426
- // (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
1427
- // aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
1428
- // the whole setup (issue #109). Switch the base URL by arch and tag the deb
1429
- // line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
1430
- // returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
1431
- 'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
1432
- 'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
1433
- // `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
1434
- // /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
1435
- // the empty-var fallback below can pick a default.
1436
- 'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
1437
- 'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
1438
- 'OR_CODENAME=""',
1439
- "for c in $REPO_CODENAME $OR_FALLBACKS; do",
1440
- ' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
1441
- "done",
1442
- 'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
1443
- '[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
1444
- 'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
1445
- ];
1446
- function openrestyInstallPlan(profile) {
1447
- if (profile.os !== "linux") {
1448
- return {
1449
- supported: false,
1450
- unsupportedReason: "OpenResty installation is only supported on Linux servers"
1451
- };
1452
- }
1453
- let installCommand;
1454
- if (profile.packageManager === "apt") {
1455
- const distro = profile.distro === "debian" ? "debian" : "ubuntu";
1456
- installCommand = [
1457
- "set -e",
1458
- // Heal a stale/bad openresty.list left by a prior run (e.g. the #86
1459
- // 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
1460
- // set -e aborts here and the repo never gets rewritten.
1461
- "rm -f /etc/apt/sources.list.d/openresty.list",
1462
- "apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
1463
- "wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
1464
- "gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
1465
- `REPO=${distro}`,
1466
- ...OPENRESTY_APT_SOURCES,
1467
- "apt-get update -qq && apt-get install -y -qq openresty"
1468
- ].join("\n");
1469
- } else if (profile.packageManager === "dnf") {
1470
- const distro = profile.distro === "fedora" ? "fedora" : "centos";
1471
- installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
1472
- } else if (profile.packageManager === "yum") {
1473
- installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
1474
- } else if (profile.packageManager === "apk") {
1475
- installCommand = [
1476
- "apk add --no-cache wget",
1477
- "wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
1478
- `. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
1479
- "apk update && apk add openresty"
1480
- ].join(" && ");
1481
- } else {
1482
- installCommand = [
1483
- "set -e",
1484
- "if command -v apt-get >/dev/null 2>&1; then",
1485
- " rm -f /etc/apt/sources.list.d/openresty.list",
1486
- " apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
1487
- " wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
1488
- " gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
1489
- ' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
1490
- ' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
1491
- ...OPENRESTY_APT_SOURCES.map((l) => " " + l),
1492
- " apt-get update -qq && apt-get install -y -qq openresty",
1493
- "elif command -v dnf >/dev/null 2>&1; then",
1494
- " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
1495
- "elif command -v yum >/dev/null 2>&1; then",
1496
- " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
1497
- "elif command -v apk >/dev/null 2>&1; then",
1498
- " apk add --no-cache wget \\",
1499
- " && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
1500
- ' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
1501
- " && apk update && apk add openresty",
1502
- "else",
1503
- ' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
1504
- "fi"
1505
- ].join("\n");
1506
- }
1507
- return {
1508
- supported: true,
1509
- installCommand,
1510
- startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
1511
- verifyCommand: "openresty -v 2>&1"
1512
- };
1513
- }
1514
- function certbotInstallPlan(profile) {
1515
- const commands = {
1516
- apt: "apt-get update -qq && apt-get install -y -qq certbot",
1517
- dnf: "dnf install -y certbot",
1518
- yum: "yum install -y certbot",
1519
- apk: "apk add --no-cache certbot"
1520
- };
1521
- const installCommand = commands[profile.packageManager];
1522
- if (!installCommand) {
1523
- return {
1524
- supported: false,
1525
- unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
1526
- };
1527
- }
1528
- return {
1529
- supported: true,
1530
- installCommand,
1531
- verifyCommand: "certbot --version 2>/dev/null"
1532
- };
1533
- }
1534
1617
  var systemCatalog = {
1535
1618
  checks: {
1536
1619
  docker: {
1537
1620
  versionCommand: "docker --version",
1621
+ // A POSITIVE signal, not a bare exit code: only a daemon that answered can
1622
+ // name its version. `docker info >/dev/null && echo ok` prints ok on ANY
1623
+ // exit-0, so a daemon answering nothing would read as healthy — trading #408's
1624
+ // false negative for a false positive. Leave stderr alone too; `checkDocker`
1625
+ // puts the failure text in the component message.
1538
1626
  daemonCommand: "docker info --format '{{.ServerVersion}}'",
1539
1627
  parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
1540
1628
  missingMessage: "Docker is not installed",
@@ -1565,12 +1653,15 @@ var systemCatalog = {
1565
1653
  missingMessage: "rsync is not installed"
1566
1654
  }
1567
1655
  },
1656
+ // No `openresty` / `certbot` plans: nothing apt-installs an edge any more. Both
1657
+ // shipped inside the `openship-edge` image, and their host-package plans (with the
1658
+ // openresty.org apt-repo codename probing that came with them) went with the
1659
+ // installers. `checks.openresty` stays — it parses the version out of the
1660
+ // CONTAINER. See installer.ts `installContainerEdge`.
1568
1661
  installs: {
1569
1662
  docker: dockerInstallPlan,
1570
1663
  git: gitInstallPlan,
1571
- rsync: rsyncInstallPlan,
1572
- openresty: openrestyInstallPlan,
1573
- certbot: certbotInstallPlan
1664
+ rsync: rsyncInstallPlan
1574
1665
  }
1575
1666
  };
1576
1667
 
@@ -1594,7 +1685,7 @@ function elevatedExecutor(inner) {
1594
1685
  };
1595
1686
  const overrides = {
1596
1687
  exec: (command, opts) => inner.exec(elevateCommand(command), opts),
1597
- streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
1688
+ streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
1598
1689
  writeFile: writeFileElevated,
1599
1690
  // Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
1600
1691
  // and the Proxy would otherwise forward this to the inner executor unelevated —
@@ -1621,7 +1712,7 @@ function elevatedExecutor(inner) {
1621
1712
  }
1622
1713
 
1623
1714
  // ../../packages/adapters/src/system/installer.ts
1624
- function log2(message, level = "info") {
1715
+ function log3(message, level = "info") {
1625
1716
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1626
1717
  }
1627
1718
  async function prepareExecutor(executor, component) {
@@ -1646,21 +1737,21 @@ async function installDocker(executor, onLog) {
1646
1737
  if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1647
1738
  return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
1648
1739
  }
1649
- onLog(log2("Installing Docker Engine..."));
1740
+ onLog(log3("Installing Docker Engine..."));
1650
1741
  try {
1651
1742
  const { code } = await executor.streamExec(plan.installCommand, onLog);
1652
1743
  if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
1653
1744
  if (plan.startCommand) {
1654
- onLog(log2("Starting Docker service..."));
1745
+ onLog(log3("Starting Docker service..."));
1655
1746
  await executor.streamExec(plan.startCommand, onLog);
1656
1747
  }
1657
1748
  const version = await executor.exec(plan.verifyCommand);
1658
1749
  const parsed = systemCatalog.checks.docker.parseVersion(version);
1659
- onLog(log2(`Docker ${parsed} installed`));
1750
+ onLog(log3(`Docker ${parsed} installed`));
1660
1751
  return { component: "docker", success: true, version: parsed };
1661
1752
  } catch (err) {
1662
1753
  const msg = safeErrorMessage(err);
1663
- onLog(log2(`Docker installation failed: ${msg}`, "error"));
1754
+ onLog(log3(`Docker installation failed: ${msg}`, "error"));
1664
1755
  return { component: "docker", success: false, error: msg };
1665
1756
  }
1666
1757
  }
@@ -1673,17 +1764,17 @@ async function installGit(executor, onLog, opts) {
1673
1764
  if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1674
1765
  return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
1675
1766
  }
1676
- onLog(log2(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1767
+ onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1677
1768
  try {
1678
1769
  const { code } = await executor.streamExec(plan.installCommand, onLog);
1679
1770
  if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
1680
1771
  const version = await executor.exec(plan.verifyCommand);
1681
1772
  const parsed = systemCatalog.checks.git.parseVersion(version);
1682
- onLog(log2(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1773
+ onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1683
1774
  return { component: "git", success: true, version: parsed };
1684
1775
  } catch (err) {
1685
1776
  const msg = safeErrorMessage(err);
1686
- onLog(log2(`Git installation failed: ${msg}`, "error"));
1777
+ onLog(log3(`Git installation failed: ${msg}`, "error"));
1687
1778
  return { component: "git", success: false, error: msg };
1688
1779
  }
1689
1780
  }
@@ -1696,17 +1787,17 @@ async function installRsync(executor, onLog) {
1696
1787
  if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1697
1788
  return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
1698
1789
  }
1699
- onLog(log2("Installing rsync..."));
1790
+ onLog(log3("Installing rsync..."));
1700
1791
  try {
1701
1792
  const { code } = await executor.streamExec(plan.installCommand, onLog);
1702
1793
  if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
1703
1794
  const version = await executor.exec(plan.verifyCommand);
1704
1795
  const parsed = systemCatalog.checks.rsync.parseVersion(version);
1705
- onLog(log2(`rsync ${parsed} installed`));
1796
+ onLog(log3(`rsync ${parsed} installed`));
1706
1797
  return { component: "rsync", success: true, version: parsed };
1707
1798
  } catch (err) {
1708
1799
  const msg = safeErrorMessage(err);
1709
- onLog(log2(`rsync installation failed: ${msg}`, "error"));
1800
+ onLog(log3(`rsync installation failed: ${msg}`, "error"));
1710
1801
  return { component: "rsync", success: false, error: msg };
1711
1802
  }
1712
1803
  }
@@ -1716,7 +1807,7 @@ async function installContainerEdge(executor, onLog, config) {
1716
1807
  executor = prep.executor;
1717
1808
  if (!await dockerAvailable(executor)) {
1718
1809
  const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
1719
- onLog(log2(error, "error"));
1810
+ onLog(log3(error, "error"));
1720
1811
  return { component: "edge", success: false, error };
1721
1812
  }
1722
1813
  try {
@@ -1726,7 +1817,7 @@ async function installContainerEdge(executor, onLog, config) {
1726
1817
  image: config?.edgeImage
1727
1818
  });
1728
1819
  if (result.converted) {
1729
- onLog(log2("Edge moved from host OpenResty to the container edge", "warn"));
1820
+ onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
1730
1821
  }
1731
1822
  const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
1732
1823
  return {
@@ -1737,18 +1828,17 @@ async function installContainerEdge(executor, onLog, config) {
1737
1828
  } catch (err) {
1738
1829
  if (err instanceof EdgeMigrateRequested) throw err;
1739
1830
  const msg = safeErrorMessage(err);
1740
- onLog(log2(`Edge setup failed: ${msg}`, "error"));
1831
+ onLog(log3(`Edge setup failed: ${msg}`, "error"));
1741
1832
  return { component: "edge", success: false, error: msg };
1742
1833
  }
1743
1834
  }
1744
1835
  var COMPONENT_INSTALLERS = {
1745
- docker: (exec, log5) => installDocker(exec, log5),
1746
- // The edge is ONE component and it is a container image. `installOpenResty` /
1747
- // `installCertbot` are NOT reachable from here — they remain only for the mail
1748
- // server's own proxy (mail.service.ts) and for converting a legacy host edge.
1749
- edge: (exec, log5, config) => installContainerEdge(exec, log5, config),
1750
- git: (exec, log5) => installGit(exec, log5),
1751
- rsync: (exec, log5) => installRsync(exec, log5)
1836
+ docker: (exec, log6) => installDocker(exec, log6),
1837
+ // The edge is ONE component and it is a container image there is no
1838
+ // host-OpenResty or host-certbot installer to reach for any more.
1839
+ edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
1840
+ git: (exec, log6) => installGit(exec, log6),
1841
+ rsync: (exec, log6) => installRsync(exec, log6)
1752
1842
  };
1753
1843
 
1754
1844
  // ../../packages/adapters/src/system/available-version.ts
@@ -1876,7 +1966,7 @@ async function tryExec4(executor, command) {
1876
1966
  "checks",
1877
1967
  `exec:ok ${command} (${formatDuration(startedAt)})`
1878
1968
  );
1879
- return result;
1969
+ return { output: result.trim(), error: null };
1880
1970
  } catch (err) {
1881
1971
  if (isRemoteConnectionError(err)) {
1882
1972
  systemDebug(
@@ -1890,9 +1980,15 @@ async function tryExec4(executor, command) {
1890
1980
  "checks",
1891
1981
  `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
1892
1982
  );
1893
- return null;
1983
+ return { output: "", error: msg || `\`${command}\` failed` };
1894
1984
  }
1895
1985
  }
1986
+ function withReason(message, error) {
1987
+ if (!error) return message;
1988
+ const first = error.split("\n").map((line) => line.trim()).find(Boolean);
1989
+ if (!first) return message;
1990
+ return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
1991
+ }
1896
1992
  function healthy(name, version, running) {
1897
1993
  const component = getSystemComponentDefinition(name);
1898
1994
  return {
@@ -1925,15 +2021,18 @@ async function checkDocker(executor) {
1925
2021
  const startedAt = Date.now();
1926
2022
  const recipe = systemCatalog.checks.docker;
1927
2023
  const version = await tryExec4(executor, recipe.versionCommand);
1928
- if (!version) {
2024
+ if (!version.output) {
1929
2025
  systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
1930
- return unhealthy("docker", recipe.missingMessage);
2026
+ return unhealthy("docker", withReason(recipe.missingMessage, version.error));
1931
2027
  }
1932
- const parsed = recipe.parseVersion(version);
2028
+ const parsed = recipe.parseVersion(version.output);
1933
2029
  const info = await tryExec4(executor, recipe.daemonCommand);
1934
- if (!info) {
1935
- systemDebug("checks", `docker:not-running (${formatDuration(startedAt)})`);
1936
- return unhealthy("docker", recipe.notRunningMessage, {
2030
+ if (!info.output) {
2031
+ systemDebug(
2032
+ "checks",
2033
+ `docker:not-running (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
2034
+ );
2035
+ return unhealthy("docker", withReason(recipe.notRunningMessage, info.error), {
1937
2036
  version: parsed,
1938
2037
  running: false
1939
2038
  });
@@ -1945,11 +2044,11 @@ async function checkGit(executor) {
1945
2044
  const startedAt = Date.now();
1946
2045
  const recipe = systemCatalog.checks.git;
1947
2046
  const version = await tryExec4(executor, recipe.versionCommand);
1948
- if (!version) {
2047
+ if (!version.output) {
1949
2048
  systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
1950
- return unhealthy("git", recipe.missingMessage);
2049
+ return unhealthy("git", withReason(recipe.missingMessage, version.error));
1951
2050
  }
1952
- const parsed = recipe.parseVersion(version);
2051
+ const parsed = recipe.parseVersion(version.output);
1953
2052
  systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
1954
2053
  return healthy("git", parsed);
1955
2054
  }
@@ -1957,11 +2056,11 @@ async function checkRsync(executor) {
1957
2056
  const startedAt = Date.now();
1958
2057
  const recipe = systemCatalog.checks.rsync;
1959
2058
  const version = await tryExec4(executor, recipe.versionCommand);
1960
- if (!version) {
2059
+ if (!version.output) {
1961
2060
  systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
1962
- return unhealthy("rsync", recipe.missingMessage);
2061
+ return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
1963
2062
  }
1964
- const parsed = recipe.parseVersion(version);
2063
+ const parsed = recipe.parseVersion(version.output);
1965
2064
  systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
1966
2065
  return healthy("rsync", parsed);
1967
2066
  }
@@ -1975,17 +2074,29 @@ async function checkEdge(executor) {
1975
2074
  "No edge on this server. The edge is the openship-edge container \u2014 install it (requires Docker)."
1976
2075
  );
1977
2076
  }
2077
+ const verdict = await verifyEdgeServing(executor, container);
2078
+ if (!verdict.serving) {
2079
+ systemDebug("checks", `edge:not-serving (${formatDuration(startedAt)})`);
2080
+ return unhealthy(
2081
+ "edge",
2082
+ `The edge container ${container} is not serving \u2014 ${verdict.reason ?? "it is not listening on :80"}`,
2083
+ { running: false }
2084
+ );
2085
+ }
1978
2086
  const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
1979
- if (!version) {
2087
+ if (!version.output) {
1980
2088
  systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
1981
2089
  return unhealthy(
1982
2090
  "edge",
1983
- `The edge container ${container} is running but not responding \u2014 check \`docker logs ${container}\``,
2091
+ withReason(
2092
+ `The edge container ${container} is serving on :80 but not answering \u2014 check \`docker logs ${container}\``,
2093
+ version.error
2094
+ ),
1984
2095
  { running: false }
1985
2096
  );
1986
2097
  }
1987
2098
  systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
1988
- return healthy("edge", systemCatalog.checks.openresty.parseVersion(version), true);
2099
+ return healthy("edge", systemCatalog.checks.openresty.parseVersion(version.output), true);
1989
2100
  }
1990
2101
  var COMPONENT_CHECKS = {
1991
2102
  docker: checkDocker,
@@ -2075,24 +2186,36 @@ function describeEdgeOwner(occupants) {
2075
2186
  }
2076
2187
 
2077
2188
  // ../../packages/adapters/src/system/proxy/consent.ts
2078
- function log3(message, level = "info") {
2189
+ function log4(message, level = "info") {
2079
2190
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2080
2191
  }
2081
2192
  async function ensureEdgeClear(executor, config, onLog) {
2082
2193
  const status = await probeEdge(executor);
2083
- if (status.canProceedClean) return { tookOver: false };
2194
+ if (status.canProceedClean) return { tookOver: false, status };
2084
2195
  const takeover = async () => {
2085
- onLog(log3(
2196
+ onLog(log4(
2086
2197
  `Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
2087
2198
  "warn"
2088
2199
  ));
2089
- const configured = config?.edgePolicy?.stopTargets ?? [];
2090
- const targets = configured.length ? configured : stopTargetsForStatus(status);
2091
- await freeEdgeTargets(executor, targets, (m, l) => onLog(log3(m, l)));
2200
+ const freed = await beginEdgeTakeover(
2201
+ executor,
2202
+ status,
2203
+ onLog,
2204
+ config?.edgePolicy?.stopTargets ?? []
2205
+ );
2206
+ if (!freed.freed) {
2207
+ await rollbackEdgeTakeover(executor, onLog);
2208
+ const plural = freed.stillBound.length > 1;
2209
+ throw new AppError(
2210
+ `Stopped the existing proxy, but port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
2211
+ 409,
2212
+ "EDGE_PORTS_STILL_BOUND"
2213
+ );
2214
+ }
2092
2215
  };
2093
2216
  if (config?.edgePolicy?.mode === "takeover") {
2094
2217
  await takeover();
2095
- return { tookOver: true };
2218
+ return { tookOver: true, status };
2096
2219
  }
2097
2220
  if (config?.promptUser) {
2098
2221
  const known = status.classification === "known";
@@ -2121,7 +2244,7 @@ async function ensureEdgeClear(executor, config, onLog) {
2121
2244
  }
2122
2245
  if (action === "override") {
2123
2246
  await takeover();
2124
- return { tookOver: true };
2247
+ return { tookOver: true, status };
2125
2248
  }
2126
2249
  throw new EdgeConflictError(status);
2127
2250
  }
@@ -2129,7 +2252,7 @@ async function ensureEdgeClear(executor, config, onLog) {
2129
2252
  }
2130
2253
 
2131
2254
  // ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
2132
- function log4(message, level = "info") {
2255
+ function log5(message, level = "info") {
2133
2256
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2134
2257
  }
2135
2258
  var injectedDefaultImage;
@@ -2139,15 +2262,8 @@ function setDefaultEdgeImage(image) {
2139
2262
  function resolveEdgeImage(explicit) {
2140
2263
  return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
2141
2264
  }
2142
- async function dockerAvailable(executor) {
2143
- return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
2144
- }
2145
- async function runningImage(executor, container) {
2146
- const out = await executor.exec(`docker inspect -f '{{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
2147
- return out.trim() || null;
2148
- }
2149
2265
  async function containerEdgeProvider(executor, container, opts) {
2150
- const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
2266
+ const { NginxProvider: Provider } = await import("./nginx-YOEEFBTE.js");
2151
2267
  return new Provider({
2152
2268
  ...opts,
2153
2269
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
@@ -2155,6 +2271,13 @@ async function containerEdgeProvider(executor, container, opts) {
2155
2271
  // while reload/certbot run inside the container.
2156
2272
  paths: EDGE_HOST_PATHS,
2157
2273
  executor: edgeContainerExecutor(executor, container),
2274
+ // Challenge tokens go to the HOST side of the ACME mount, for the same reason
2275
+ // vhosts do. This is the ONE mount whose two sides differ
2276
+ // (/var/lib/openship/edge/acme → /var/www/acme), so the write path and the
2277
+ // vhost's `root` are genuinely different strings — `root` stays the container
2278
+ // path. Overridden here and nowhere else, next to the `paths` override it
2279
+ // mirrors, so the two cannot drift.
2280
+ challengeDir: EDGE_CHALLENGE_HOST_DIR,
2158
2281
  // MUST be pinned: re-detection answers from inside the container and would
2159
2282
  // repoint sitesDir at a host dir the edge never reads.
2160
2283
  pinPaths: true,
@@ -2164,8 +2287,8 @@ async function containerEdgeProvider(executor, container, opts) {
2164
2287
  });
2165
2288
  }
2166
2289
  async function localContainerEdgeProvider(container, opts) {
2167
- const { DockerEdgeExecutor } = await import("./docker-edge-executor-5I4M5QT7.js");
2168
- const { NginxProvider: Provider } = await import("./nginx-QE4DOM5J.js");
2290
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-62N64MGS.js");
2291
+ const { NginxProvider: Provider } = await import("./nginx-YOEEFBTE.js");
2169
2292
  return new Provider({
2170
2293
  ...opts,
2171
2294
  paths: OPENRESTY_DEFAULT_PATHS,
@@ -2203,37 +2326,39 @@ function buildEdgeRunCommand(container, image) {
2203
2326
  sq(image)
2204
2327
  ].join(" ");
2205
2328
  }
2206
- async function swapEdgeImage(executor, container, from, to, opts) {
2329
+ function makeEdgeStart(executor, container, opts) {
2207
2330
  const { onLog } = opts;
2208
- onLog(log4(`Updating the edge: ${from} \u2192 ${to}`));
2209
- const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
2210
- if (pull.code !== 0) {
2211
- onLog(log4(`Could not pull ${to} \u2014 the edge stays on ${from}.`, "warn"));
2212
- return { swapped: false, edgeDown: false };
2213
- }
2214
- const start = async (image) => {
2331
+ return async (image) => {
2215
2332
  if (!await startEdgeContainer(executor, container, image, onLog)) return false;
2216
- const listening = await waitForPortListening(executor, 80, {
2333
+ const verdict = await verifyEdgeServing(executor, container, {
2217
2334
  timeoutMs: opts.verifyTimeoutMs ?? 3e4
2218
2335
  });
2219
- return !(listening.checked && !listening.listening);
2336
+ if (!verdict.serving && verdict.reason) onLog(log5(`Edge ${image}: ${verdict.reason}`, "warn"));
2337
+ return verdict.serving;
2220
2338
  };
2221
- if (await start(to)) {
2222
- onLog(log4(`Edge updated to ${to}`));
2223
- return { swapped: true, edgeDown: false };
2339
+ }
2340
+ async function verifyEdgeServing(executor, container, opts = {}) {
2341
+ if (await edgeIsBroken(executor, container)) {
2342
+ const reason = await edgeCrashReason(executor, container);
2343
+ return { serving: false, reason: reason ?? "the edge container is not running" };
2224
2344
  }
2225
- onLog(log4(`Edge ${to} failed to come up \u2014 rolling back to ${from}...`, "error"));
2226
- if (await start(from)) {
2227
- return { swapped: false, edgeDown: false };
2345
+ if (opts.configTest) {
2346
+ try {
2347
+ await executor.exec(containerCommand(container, "openresty -t 2>&1"));
2348
+ } catch (err) {
2349
+ const out = safeErrorMessage(err);
2350
+ return { serving: false, reason: edgeFailureReason(out) ?? out };
2351
+ }
2228
2352
  }
2229
- onLog(log4(`Rollback to ${from} ALSO failed \u2014 the edge is down on this server.`, "error"));
2230
- return { swapped: false, edgeDown: true };
2231
- }
2232
- async function listDir(executor, dir) {
2233
- const out = await executor.exec(`ls -1 ${sq(dir)} 2>/dev/null || true`).catch(() => "");
2234
- return new Set(
2235
- out.split("\n").map((l) => l.trim()).filter(Boolean)
2236
- );
2353
+ const listening = await waitForPortListening(executor, 80, { timeoutMs: opts.timeoutMs ?? 0 });
2354
+ if (listening.checked && !listening.listening) {
2355
+ const reason = await edgeCrashReason(executor, container);
2356
+ return {
2357
+ serving: false,
2358
+ reason: reason ?? "the edge container is up but nothing is listening on :80"
2359
+ };
2360
+ }
2361
+ return { serving: true };
2237
2362
  }
2238
2363
  async function ensureContainerEdge(executor, opts) {
2239
2364
  const { onLog } = opts;
@@ -2241,123 +2366,112 @@ async function ensureContainerEdge(executor, opts) {
2241
2366
  const image = resolveEdgeImage(opts.image);
2242
2367
  const existing = await resolveOurEdgeContainer(executor, { fresh: true });
2243
2368
  if (existing) {
2244
- const current = await runningImage(executor, existing);
2245
- if (current && current !== image) {
2246
- const swap = await swapEdgeImage(executor, existing, current, image, opts);
2247
- return {
2248
- container: existing,
2249
- image,
2250
- converted: false,
2251
- updated: swap.swapped,
2252
- edgeDown: swap.edgeDown
2253
- };
2369
+ const verdict = await verifyEdgeServing(executor, existing);
2370
+ if (!verdict.serving) {
2371
+ onLog(log5(`The edge container exists but isn't serving: ${verdict.reason}`, "warn"));
2372
+ onLog(log5("Recreating it...", "warn"));
2373
+ } else {
2374
+ const current = await containerImageRef(executor, existing);
2375
+ const stale = current != null && current !== image;
2376
+ if (stale) {
2377
+ const start = makeEdgeStart(executor, existing, opts);
2378
+ const swap = await swapManagedImage(executor, {
2379
+ kind: "edge",
2380
+ from: current ?? image,
2381
+ to: image,
2382
+ label: "edge",
2383
+ onLog,
2384
+ start
2385
+ });
2386
+ return {
2387
+ container: existing,
2388
+ image,
2389
+ converted: false,
2390
+ updated: swap.swapped,
2391
+ edgeDown: swap.down
2392
+ };
2393
+ }
2394
+ return { container: existing, image, converted: false };
2254
2395
  }
2255
- return { container: existing, image, converted: false };
2256
2396
  }
2257
- await ensureEdgeClear(executor, opts.config, onLog);
2258
2397
  if (!await dockerAvailable(executor)) {
2259
2398
  throw new Error(
2260
2399
  "Docker isn't available on this server, and the edge now runs as a container. Install the Docker component first, then set up the edge."
2261
2400
  );
2262
2401
  }
2263
- const bareWasOurs = await ourLuaOnHost(executor);
2264
- onLog(log4(`Pulling edge image ${image}...`));
2265
- const pull = await executor.streamExec(`docker pull ${sq(image)}`, onLog);
2266
- if (pull.code !== 0) {
2267
- throw new Error(
2268
- `Could not pull the edge image ${image} \u2014 the box is unchanged and still serving. Check the server's network access to the registry, then retry.`
2269
- );
2402
+ if (!await imageExistsLocally(executor, image)) {
2403
+ if (managedImagesAreFromSource("edge")) {
2404
+ throw new Error(fromSourceImageMissingMessage("edge", image));
2405
+ }
2406
+ onLog(log5(`Pulling edge image ${image}...`));
2407
+ const pull = await executor.streamExec(`docker pull ${sq(image)}`, onLog);
2408
+ if (pull.code !== 0) {
2409
+ throw new Error(
2410
+ `Could not pull the edge image ${image} \u2014 the box is unchanged and still serving. Check the server's network access to the registry, then retry.`
2411
+ );
2412
+ }
2270
2413
  }
2271
2414
  for (const mount of EDGE_CONTAINER_MOUNTS) {
2272
2415
  await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
2273
2416
  });
2274
2417
  }
2275
- const sitesTarget = EDGE_HOST_PATHS.sitesDir;
2276
- let beforeCarry = null;
2277
- if (bareWasOurs) {
2278
- const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
2279
- if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
2280
- onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
2281
- beforeCarry = await listDir(executor, sitesTarget);
2282
- await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
2283
- });
2284
- }
2285
- }
2286
- const restoreBare = async () => {
2287
- if (!bareWasOurs) return;
2288
- if (beforeCarry) {
2289
- const now = await listDir(executor, sitesTarget);
2290
- const added = [...now].filter((name) => !beforeCarry.has(name));
2291
- if (added.length > 0) {
2292
- await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
2293
- });
2294
- onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
2295
- }
2296
- }
2297
- onLog(log4("Restoring the host OpenResty edge...", "warn"));
2298
- await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
2299
- });
2300
- };
2418
+ const clear = await ensureEdgeClear(executor, opts.config, onLog);
2301
2419
  try {
2302
- if (bareWasOurs) {
2303
- onLog(log4("Stopping the host OpenResty edge..."));
2304
- await executor.exec("systemctl disable --now openresty 2>/dev/null || true").catch(() => {
2305
- });
2306
- await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
2307
- });
2308
- }
2309
- onLog(log4("Starting the edge container..."));
2420
+ onLog(log5("Starting the edge container..."));
2310
2421
  if (!await startEdgeContainer(executor, container, image, onLog)) {
2311
2422
  throw new Error("the edge container failed to start");
2312
2423
  }
2313
- await executor.exec(containerCommand(container, "openresty -t"));
2314
- const listening = await waitForPortListening(executor, 80, {
2424
+ const verdict = await verifyEdgeServing(executor, container, {
2425
+ configTest: true,
2315
2426
  timeoutMs: opts.verifyTimeoutMs ?? 3e4
2316
2427
  });
2317
- if (listening.checked && !listening.listening) {
2318
- throw new Error("the edge container started but nothing is listening on :80");
2428
+ if (!verdict.serving) {
2429
+ throw new Error(verdict.reason ?? "the edge container started but isn't serving");
2319
2430
  }
2431
+ if (clear.tookOver) await completeEdgeTakeover(executor);
2320
2432
  const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
2321
- onLog(log4(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
2322
- return { container, image, converted: bareWasOurs };
2433
+ onLog(log5(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
2434
+ return { container, image, converted: clear.tookOver };
2323
2435
  } catch (err) {
2324
2436
  const msg = safeErrorMessage(err);
2325
- onLog(log4(`Edge container setup failed: ${msg}`, "error"));
2437
+ onLog(log5(`Edge container setup failed: ${msg}`, "error"));
2326
2438
  const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
2327
- if (logs.trim()) onLog(log4(`Edge container logs:
2439
+ if (logs.trim()) onLog(log5(`Edge container logs:
2328
2440
  ${logs}`, "error"));
2329
2441
  await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
2330
2442
  });
2331
2443
  invalidateEdgeContainer(executor);
2332
- await restoreBare();
2444
+ if (clear.tookOver) await rollbackEdgeTakeover(executor, onLog);
2333
2445
  const reason = edgeFailureReason(logs);
2334
2446
  throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
2335
2447
  }
2336
2448
  }
2337
2449
 
2338
2450
  export {
2339
- systemCatalog,
2340
- checkGit,
2341
- checkAll,
2342
- checkComponents,
2451
+ beginEdgeTakeover,
2452
+ rollbackEdgeTakeover,
2453
+ completeEdgeTakeover,
2454
+ recoverInterruptedTakeover,
2343
2455
  detectInstalledProxy,
2344
2456
  scanImportableSites,
2457
+ unreachableStaticRoots,
2458
+ copyStaticRootIntoEdge,
2345
2459
  edgeProxy,
2346
2460
  edgeProxyFor,
2347
2461
  collectProxyCerts,
2348
- beginEdgeTakeover,
2349
- rollbackEdgeTakeover,
2350
- completeEdgeTakeover,
2351
- recoverInterruptedTakeover,
2462
+ systemCatalog,
2463
+ installGit,
2464
+ COMPONENT_INSTALLERS,
2465
+ importSites,
2466
+ foreignProxyOnEdge,
2352
2467
  setDefaultEdgeImage,
2353
2468
  resolveEdgeImage,
2354
- dockerAvailable,
2355
2469
  containerEdgeProvider,
2356
2470
  localContainerEdgeProvider,
2357
2471
  buildEdgeRunCommand,
2472
+ verifyEdgeServing,
2358
2473
  ensureContainerEdge,
2359
- importSites,
2360
- foreignProxyOnEdge,
2361
- installGit,
2362
- COMPONENT_INSTALLERS
2474
+ checkGit,
2475
+ checkAll,
2476
+ checkComponents
2363
2477
  };