openship 0.4.6 → 0.4.8

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.
@@ -9,7 +9,7 @@ import {
9
9
  import "./chunk-MZ2WEHHY.js";
10
10
  import "./chunk-L2VOGB2X.js";
11
11
  import "./chunk-4Z5QO7N6.js";
12
- import "./chunk-4TJC4JZQ.js";
12
+ import "./chunk-XPPZ7B2J.js";
13
13
  import "./chunk-OXWDY22G.js";
14
14
  import "./chunk-RPHRPFEH.js";
15
15
  import "./chunk-TYVME3XD.js";
@@ -59,6 +59,54 @@ function sq(value) {
59
59
  function isOurEdgeContainer(name, image) {
60
60
  return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
61
61
  }
62
+ function vhostLog(message, level = "info") {
63
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
64
+ }
65
+ async function sanitizeEdgeVhosts(executor, sitesDir, onLog) {
66
+ const script = [
67
+ `for f in ${sq(sitesDir)}/*.conf; do`,
68
+ ` [ -f "$f" ] || continue;`,
69
+ // A real server_name is anything that isn't the `_` wildcard.
70
+ ` if ! grep -qE '^[[:space:]]*server_name[[:space:]]+[^_;[:space:]]' "$f"; then`,
71
+ ` echo "dropped-catchall $f"; rm -f "$f"; continue;`,
72
+ ` fi;`,
73
+ ` if grep -qE '[[:space:]]default_server' "$f"; then`,
74
+ ` sed -E 's/([[:space:]]listen[^;]*)[[:space:]]+default_server/\\1/g' "$f" > "$f.osh" && mv "$f.osh" "$f" && echo "unset-default $f";`,
75
+ ` fi;`,
76
+ `done`
77
+ ].join(" ");
78
+ const out = await executor.exec(script).catch(() => "");
79
+ for (const line of out.split("\n").map((l) => l.trim()).filter(Boolean)) {
80
+ const [action, file] = [line.slice(0, line.indexOf(" ")), line.slice(line.indexOf(" ") + 1)];
81
+ if (action === "dropped-catchall") {
82
+ onLog(vhostLog(`Dropped catch-all vhost ${file} \u2014 the edge image provides it.`, "warn"));
83
+ } else if (action === "unset-default") {
84
+ onLog(vhostLog(`Removed default_server from ${file} \u2014 the edge image owns it.`, "warn"));
85
+ }
86
+ }
87
+ }
88
+ async function edgeIsBroken(executor) {
89
+ const status = await tryExec(
90
+ executor,
91
+ `docker inspect -f '{{.State.Status}}' ${sq(EDGE_CONTAINER_NAME)} 2>/dev/null`
92
+ );
93
+ const state = (status ?? "").trim();
94
+ return state === "restarting" || state === "exited" || state === "dead";
95
+ }
96
+ async function edgeCrashReason(executor) {
97
+ const logs = await tryExec(
98
+ executor,
99
+ `docker logs --tail 40 ${sq(EDGE_CONTAINER_NAME)} 2>&1`
100
+ );
101
+ return logs ? edgeFailureReason(logs) : null;
102
+ }
103
+ function edgeFailureReason(containerLog) {
104
+ const lines = containerLog.split("\n").map((l) => l.trim()).filter(Boolean);
105
+ const emerg = lines.find((l) => l.includes("[emerg]"));
106
+ if (emerg) return emerg.replace(/^.*\[emerg\]\s*\d*#\d*:\s*/, "");
107
+ return null;
108
+ }
109
+ var EDGE_CONTAINER_NAME = "openship-edge";
62
110
  async function ourEdgeContainerRunning(executor) {
63
111
  return Boolean(await resolveOurEdgeContainer(executor));
64
112
  }
@@ -238,6 +286,11 @@ export {
238
286
  classifyProxy,
239
287
  sq,
240
288
  isOurEdgeContainer,
289
+ sanitizeEdgeVhosts,
290
+ edgeIsBroken,
291
+ edgeCrashReason,
292
+ edgeFailureReason,
293
+ EDGE_CONTAINER_NAME,
241
294
  ourEdgeContainerRunning,
242
295
  invalidateEdgeContainer,
243
296
  resolveOurEdgeContainer,
@@ -5,7 +5,7 @@ const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  checkGit,
7
7
  installGit
8
- } from "./chunk-SQ77DZOI.js";
8
+ } from "./chunk-SLTYVHU7.js";
9
9
  import {
10
10
  createDockerBuildContext,
11
11
  normalizeDockerRelativePath,
@@ -101,10 +101,10 @@ import {
101
101
  } from "./chunk-W73X43R7.js";
102
102
  import {
103
103
  CloudRuntime
104
- } from "./chunk-5FNIWBFI.js";
104
+ } from "./chunk-5NYUCONN.js";
105
105
  import {
106
106
  systemCatalog
107
- } from "./chunk-SQ77DZOI.js";
107
+ } from "./chunk-SLTYVHU7.js";
108
108
  import {
109
109
  AwsCrc32,
110
110
  __awaiter,
@@ -139,13 +139,19 @@ import {
139
139
  DockerRuntime,
140
140
  isHostPathSource,
141
141
  scopedVolumeName
142
- } from "./chunk-2ZLR7ULL.js";
142
+ } from "./chunk-SJIFLLX4.js";
143
143
  import {
144
144
  BareRuntime
145
145
  } from "./chunk-VDMQJHPI.js";
146
146
  import {
147
- invalidateEdgeContainer
148
- } from "./chunk-FQGJZ2HS.js";
147
+ LocalExecutor
148
+ } from "./chunk-TYVME3XD.js";
149
+ import {
150
+ EDGE_CONTAINER_NAME,
151
+ edgeCrashReason,
152
+ invalidateEdgeContainer,
153
+ sanitizeEdgeVhosts
154
+ } from "./chunk-2D3I5G6P.js";
149
155
  import {
150
156
  EDGE_CONTAINER_MOUNTS,
151
157
  EDGE_HOST_STATE_DIR
@@ -159,13 +165,6 @@ import {
159
165
  import chalk from "chalk";
160
166
  import ora from "ora";
161
167
 
162
- // src/lib/compose.ts
163
- import { spawnSync as spawnSync3 } from "child_process";
164
- import { randomBytes as randomBytes5 } from "crypto";
165
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
166
- import { homedir as homedir2, userInfo } from "os";
167
- import { join as join6 } from "path";
168
-
169
168
  // ../../packages/adapters/src/runtime/image-transfer.ts
170
169
  import { Transform } from "stream";
171
170
 
@@ -12668,6 +12667,13 @@ var SftpDestinationImpl = class {
12668
12667
  registerDestination("sftp", (row) => new SftpDestinationImpl(row));
12669
12668
  registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
12670
12669
 
12670
+ // src/lib/compose.ts
12671
+ import { spawnSync as spawnSync3 } from "child_process";
12672
+ import { randomBytes as randomBytes5 } from "crypto";
12673
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
12674
+ import { homedir as homedir2, userInfo } from "os";
12675
+ import { join as join6 } from "path";
12676
+
12671
12677
  // src/lib/paths.ts
12672
12678
  import { homedir } from "os";
12673
12679
  import { join as join3, resolve as resolve2 } from "path";
@@ -13229,7 +13235,7 @@ function renderEnv(opts, host, cfg) {
13229
13235
  // (hides the local row). Written explicitly so the policy is visible in .env.
13230
13236
  `OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
13231
13237
  `OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
13232
- `OPENSHIP_VERSION=${opts.version || (true ? "0.4.6" : "latest")}`,
13238
+ `OPENSHIP_VERSION=${opts.version || (true ? "0.4.8" : "latest")}`,
13233
13239
  `POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
13234
13240
  `BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
13235
13241
  `INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
@@ -13302,7 +13308,14 @@ function compose(args, opts) {
13302
13308
  });
13303
13309
  return r2.status ?? 1;
13304
13310
  }
13305
- function composeUp(opts) {
13311
+ function composePrefetch(opts) {
13312
+ const { buildDir } = materialize(opts);
13313
+ if (buildDir) {
13314
+ return compose(["pull", "postgres", "redis"], { withBuildOverride: true }) === 0 && compose(["build"], { withBuildOverride: true }) === 0;
13315
+ }
13316
+ return compose(["pull"]) === 0;
13317
+ }
13318
+ async function composeUp(opts) {
13306
13319
  const { buildDir, cfg, envChanged } = materialize(opts);
13307
13320
  const apiPort = cfg.apiPort;
13308
13321
  const dashPort = cfg.dashPort;
@@ -13318,12 +13331,20 @@ function composeUp(opts) {
13318
13331
  if (dbVolumeExists(project)) {
13319
13332
  reconcileDbPassword(env.POSTGRES_USER || "openship", env.POSTGRES_PASSWORD ?? "");
13320
13333
  }
13334
+ await sanitizeEdgeVhosts(
13335
+ new LocalExecutor(),
13336
+ EDGE_SITES_HOST_DIR,
13337
+ (l2) => console.log(` ${l2.message}`)
13338
+ ).catch(() => {
13339
+ });
13321
13340
  if (buildDir) {
13322
- if (compose(["pull", "postgres", "redis"], { withBuildOverride: true }) !== 0) {
13323
- return { ok: false, apiPort, dashPort };
13324
- }
13325
- if (compose(["build"], { withBuildOverride: true }) !== 0) {
13326
- return { ok: false, apiPort, dashPort };
13341
+ if (!opts.alreadyFetched) {
13342
+ if (compose(["pull", "postgres", "redis"], { withBuildOverride: true }) !== 0) {
13343
+ return { ok: false, apiPort, dashPort };
13344
+ }
13345
+ if (compose(["build"], { withBuildOverride: true }) !== 0) {
13346
+ return { ok: false, apiPort, dashPort };
13347
+ }
13327
13348
  }
13328
13349
  if (up({ withBuildOverride: true }) !== 0) {
13329
13350
  return { ok: false, apiPort, dashPort };
@@ -13332,7 +13353,9 @@ function composeUp(opts) {
13332
13353
  writeInstallMethod("compose");
13333
13354
  return { ok: true, apiPort, dashPort };
13334
13355
  }
13335
- if (compose(["pull"]) !== 0) return { ok: false, apiPort, dashPort };
13356
+ if (!opts.alreadyFetched && compose(["pull"]) !== 0) {
13357
+ return { ok: false, apiPort, dashPort };
13358
+ }
13336
13359
  if (up() !== 0) return { ok: false, apiPort, dashPort };
13337
13360
  onEdgeContainerChanged();
13338
13361
  writeInstallMethod("compose");
@@ -13361,9 +13384,9 @@ function composeUninstall(opts = {}) {
13361
13384
  }
13362
13385
  return { ok, removedImages };
13363
13386
  }
13364
- function composeUpdate(version) {
13387
+ async function composeUpdate(version) {
13365
13388
  if (!existsSync3(COMPOSE_FILE)) return false;
13366
- return composeUp(version ? { version } : {}).ok;
13389
+ return (await composeUp(version ? { version } : {})).ok;
13367
13390
  }
13368
13391
  function composePs() {
13369
13392
  return compose(["ps"]);
@@ -13435,11 +13458,17 @@ async function importMigratedSites(apiPort, sites, certPems) {
13435
13458
  `Migrated NONE of the ${sites.length} site${sites.length === 1 ? "" : "s"} \u2014 Openship holds :80/:443 and ${sites.length === 1 ? "that hostname is" : "those hostnames are"} NOT being served.`
13436
13459
  );
13437
13460
  for (const w2 of warnings.slice(0, 8)) console.log(chalk.red(` \u2022 ${w2}`));
13461
+ const edgeLog = await edgeCrashReason(new LocalExecutor());
13462
+ if (edgeLog) {
13463
+ console.log(chalk.red(`
13464
+ The edge container is not running. Its log says:`));
13465
+ console.log(chalk.red(` ${edgeLog}`));
13466
+ }
13438
13467
  console.log(
13439
13468
  chalk.yellow(
13440
13469
  `
13441
13470
  Retry the import with \`openship up\` once the cause above is fixed, or put your
13442
- previous proxy back: docker stop openship-edge && sudo systemctl enable --now nginx
13471
+ previous proxy back: docker stop ${EDGE_CONTAINER_NAME} && sudo systemctl enable --now nginx
13443
13472
  `
13444
13473
  )
13445
13474
  );
@@ -13477,6 +13506,7 @@ export {
13477
13506
  composeIsViableDefault,
13478
13507
  ensureDocker,
13479
13508
  sourceBuildDir,
13509
+ composePrefetch,
13480
13510
  composeUp,
13481
13511
  composeDown,
13482
13512
  composeUninstall,
@@ -6,7 +6,7 @@ import {
6
6
  COMPONENT_INSTALLERS,
7
7
  checkAll,
8
8
  checkComponents
9
- } from "./chunk-SQ77DZOI.js";
9
+ } from "./chunk-SLTYVHU7.js";
10
10
 
11
11
  // ../../packages/adapters/src/system/state.ts
12
12
  var DEFAULT_STATE_PATH = "/etc/openship/setup-state.json";
@@ -207,6 +207,8 @@ var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
207
207
  var resolvedDockerSocketPathCache = /* @__PURE__ */ new WeakMap();
208
208
  var DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio";
209
209
  var STREAMLOCAL_PROBE_TIMEOUT_MS = 8e3;
210
+ var STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3e3;
211
+ var VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
210
212
  function toSshConfig(opts) {
211
213
  return {
212
214
  host: opts.host ?? "",
@@ -304,13 +306,107 @@ async function openStreamlocalUpstream(opts) {
304
306
  throw error;
305
307
  }
306
308
  }
309
+ function pipeThrough(client, upstream) {
310
+ const teardown = () => {
311
+ client.destroy();
312
+ upstream.destroy();
313
+ };
314
+ client.on("error", teardown);
315
+ upstream.on("error", teardown);
316
+ client.once("close", () => upstream.destroy());
317
+ upstream.once("close", () => client.destroy());
318
+ client.pipe(upstream);
319
+ upstream.pipe(client);
320
+ }
321
+ function awaitFirstByte(upstream, ms) {
322
+ return new Promise((resolve) => {
323
+ const finish = (chunk) => {
324
+ clearTimeout(timer);
325
+ upstream.removeListener("data", onData);
326
+ upstream.removeListener("error", onFail);
327
+ upstream.removeListener("close", onFail);
328
+ resolve(chunk);
329
+ };
330
+ const onData = (chunk) => finish(chunk);
331
+ const onFail = () => finish(null);
332
+ const timer = setTimeout(() => finish(null), ms);
333
+ upstream.on("data", onData);
334
+ upstream.once("error", onFail);
335
+ upstream.once("close", onFail);
336
+ });
337
+ }
338
+ function captureClient(client) {
339
+ const chunks = [];
340
+ let bytes = 0;
341
+ let overflowed = false;
342
+ let closed = false;
343
+ let live = null;
344
+ const onData = (chunk) => {
345
+ if (live) live.write(chunk);
346
+ if (overflowed) return;
347
+ chunks.push(chunk);
348
+ bytes += chunk.length;
349
+ if (bytes >= VERIFY_BUFFER_CAP_BYTES) {
350
+ overflowed = true;
351
+ client.pause();
352
+ }
353
+ };
354
+ const onGone = () => {
355
+ closed = true;
356
+ };
357
+ client.on("data", onData);
358
+ client.once("error", onGone);
359
+ client.once("close", onGone);
360
+ const detach = () => {
361
+ client.removeListener("data", onData);
362
+ client.removeListener("error", onGone);
363
+ client.removeListener("close", onGone);
364
+ };
365
+ return {
366
+ get closed() {
367
+ return closed;
368
+ },
369
+ get overflowed() {
370
+ return overflowed;
371
+ },
372
+ /** Replay everything buffered so far onto `upstream` and forward the rest
373
+ * live. Safe to call again on a different upstream (the fallback): the full
374
+ * buffer is retained until `commit`, so the replay is complete each time. */
375
+ feed(upstream) {
376
+ for (const chunk of chunks) upstream.write(chunk);
377
+ live = upstream;
378
+ },
379
+ /** Stop forwarding to the current upstream (keep buffering) — used before
380
+ * destroying a dead channel so late bytes never hit a torn-down stream. */
381
+ stop() {
382
+ live = null;
383
+ },
384
+ /** Terminal: detach and hand the socket to the (already-fed) `upstream` via a
385
+ * bidi pipe, or destroy it if the client already went away. */
386
+ commit(upstream) {
387
+ detach();
388
+ chunks.length = 0;
389
+ if (closed) {
390
+ upstream.destroy();
391
+ return;
392
+ }
393
+ pipeThrough(client, upstream);
394
+ },
395
+ /** Terminal: no usable upstream — surface the error to dockerode. */
396
+ fail(err) {
397
+ detach();
398
+ client.destroy(err instanceof Error ? err : new Error(String(err)));
399
+ }
400
+ };
401
+ }
307
402
  function createDockerSshBridge(opts) {
308
403
  const clients = /* @__PURE__ */ new Set();
309
404
  let upstreamMode = null;
310
405
  let modeDecision = null;
311
406
  let dialClient = null;
312
- const openDialStdioUpstream = async () => {
313
- if (opts.executor?.openDockerDialStdio) {
407
+ let pooledUnreliable = false;
408
+ const openDialStdioUpstream = async (forceEphemeral = false) => {
409
+ if (!forceEphemeral && opts.executor?.openDockerDialStdio) {
314
410
  const stream = await opts.executor.openDockerDialStdio();
315
411
  stream.once(
316
412
  "error",
@@ -373,29 +469,57 @@ function createDockerSshBridge(opts) {
373
469
  modeDecision ??= decideMode().then((mode) => upstreamMode = mode);
374
470
  return modeDecision;
375
471
  };
376
- const openUpstream = async () => {
377
- const mode = await ensureMode();
378
- return mode === "dialstdio" ? openDialStdioUpstream() : openStreamlocalUpstream(opts);
472
+ const downgradeToDialStdio = async (capture, reason) => {
473
+ console.warn(
474
+ `[docker-ssh] streamlocal unusable (${opts.host ?? "?"}): ${reason} \u2014 downgrading this bridge to dial-stdio (fresh connection) and replaying the buffered request.`
475
+ );
476
+ upstreamMode = "dialstdio";
477
+ pooledUnreliable = true;
478
+ const upstream = await openDialStdioUpstream(true);
479
+ capture.feed(upstream);
480
+ capture.commit(upstream);
481
+ };
482
+ const bridgeClient = async (client) => {
483
+ const capture = captureClient(client);
484
+ try {
485
+ const mode = await ensureMode();
486
+ if (mode === "dialstdio") {
487
+ const upstream = await openDialStdioUpstream(pooledUnreliable);
488
+ capture.feed(upstream);
489
+ capture.commit(upstream);
490
+ return;
491
+ }
492
+ let channel;
493
+ try {
494
+ channel = await withTimeout(
495
+ openStreamlocalUpstream(opts),
496
+ STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS,
497
+ `channel open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1e3}s`
498
+ );
499
+ } catch (err) {
500
+ await downgradeToDialStdio(capture, safeErrorMessage(err));
501
+ return;
502
+ }
503
+ capture.feed(channel);
504
+ const firstByte = capture.overflowed ? null : await awaitFirstByte(channel, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
505
+ if (capture.overflowed || firstByte) {
506
+ if (firstByte) client.write(firstByte);
507
+ capture.commit(channel);
508
+ return;
509
+ }
510
+ capture.stop();
511
+ channel.destroy();
512
+ await downgradeToDialStdio(capture, "channel opened but no data flowed");
513
+ } catch (err) {
514
+ console.warn(`[docker-ssh] bridge client failed (${opts.host ?? "?"}): ${safeErrorMessage(err)}`);
515
+ capture.fail(err);
516
+ }
379
517
  };
380
518
  const server = net2.createServer((client) => {
381
519
  clients.add(client);
382
520
  client.setNoDelay(true);
383
521
  client.once("close", () => clients.delete(client));
384
- openUpstream().then((upstream) => {
385
- const teardown = () => {
386
- client.destroy();
387
- upstream.destroy();
388
- };
389
- client.on("error", teardown);
390
- upstream.on("error", teardown);
391
- client.once("close", () => upstream.destroy());
392
- upstream.once("close", () => client.destroy());
393
- client.pipe(upstream);
394
- upstream.pipe(client);
395
- }).catch((error) => {
396
- console.warn(`[docker-ssh] bridge upstream open failed: ${safeErrorMessage(error)}`);
397
- client.destroy(error instanceof Error ? error : new Error(String(error)));
398
- });
522
+ void bridgeClient(client);
399
523
  });
400
524
  return {
401
525
  start: () => new Promise((resolve, reject) => {
@@ -22,16 +22,19 @@ import {
22
22
  sq
23
23
  } from "./chunk-FPRHYBY2.js";
24
24
  import {
25
+ EDGE_CONTAINER_NAME,
25
26
  EdgeConflictError,
26
27
  EdgeMigrateRequested,
28
+ edgeFailureReason,
27
29
  freeEdgeTargets,
28
30
  invalidateEdgeContainer,
29
31
  ourLuaOnHost,
30
32
  probeEdge,
31
33
  resolveOurEdgeContainer,
34
+ sanitizeEdgeVhosts,
32
35
  sq as sq2,
33
36
  stopTargetsForStatus
34
- } from "./chunk-FQGJZ2HS.js";
37
+ } from "./chunk-2D3I5G6P.js";
35
38
  import {
36
39
  waitForPortListening
37
40
  } from "./chunk-ILFETZOM.js";
@@ -976,6 +979,8 @@ async function readJournal(executor) {
976
979
  }
977
980
  async function rollback(executor, journal, onLog) {
978
981
  onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
982
+ await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
983
+ await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
979
984
  await tryExec2(
980
985
  executor,
981
986
  "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
@@ -1000,6 +1005,24 @@ async function rollback(executor, journal, onLog) {
1000
1005
  onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1001
1006
  }
1002
1007
  }
1008
+ const restored = await portIsServed(executor, 80);
1009
+ if (!restored) {
1010
+ onLog(
1011
+ log(
1012
+ "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>`).",
1013
+ "error"
1014
+ )
1015
+ );
1016
+ }
1017
+ return restored;
1018
+ }
1019
+ async function portIsServed(executor, port) {
1020
+ const hex = port.toString(16).toUpperCase().padStart(4, "0");
1021
+ const out = await tryExec2(
1022
+ executor,
1023
+ `(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`
1024
+ );
1025
+ return (out ?? "").includes("yes");
1003
1026
  }
1004
1027
  async function beginEdgeTakeover(executor, status, onLog) {
1005
1028
  await writeJournal(executor, await buildJournal(executor, status));
@@ -1008,9 +1031,9 @@ async function beginEdgeTakeover(executor, status, onLog) {
1008
1031
  async function rollbackEdgeTakeover(executor, onLog) {
1009
1032
  const journal = await readJournal(executor);
1010
1033
  if (!journal || journal.completed) return false;
1011
- await rollback(executor, journal, onLog);
1034
+ const restored = await rollback(executor, journal, onLog);
1012
1035
  await clearJournal(executor);
1013
- return true;
1036
+ return restored;
1014
1037
  }
1015
1038
  async function completeEdgeTakeover(executor) {
1016
1039
  const journal = await readJournal(executor);
@@ -1802,7 +1825,6 @@ async function ensureEdgeClear(executor, config, onLog) {
1802
1825
  }
1803
1826
 
1804
1827
  // ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
1805
- var EDGE_CONTAINER_NAME = "openship-edge";
1806
1828
  function log4(message, level = "info") {
1807
1829
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1808
1830
  }
@@ -1821,7 +1843,7 @@ async function runningImage(executor, container) {
1821
1843
  return out.trim() || null;
1822
1844
  }
1823
1845
  async function containerEdgeProvider(executor, container, opts) {
1824
- const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1846
+ const { NginxProvider: Provider } = await import("./nginx-3L4SIBJA.js");
1825
1847
  return new Provider({
1826
1848
  ...opts,
1827
1849
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
@@ -1836,7 +1858,7 @@ async function containerEdgeProvider(executor, container, opts) {
1836
1858
  }
1837
1859
  async function localContainerEdgeProvider(container, opts) {
1838
1860
  const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
1839
- const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1861
+ const { NginxProvider: Provider } = await import("./nginx-3L4SIBJA.js");
1840
1862
  return new Provider({
1841
1863
  ...opts,
1842
1864
  paths: OPENRESTY_DEFAULT_PATHS,
@@ -1846,6 +1868,18 @@ async function localContainerEdgeProvider(container, opts) {
1846
1868
  pinPaths: true
1847
1869
  });
1848
1870
  }
1871
+ async function startEdgeContainer(executor, container, image, onLog) {
1872
+ await sanitizeEdgeVhosts(executor, EDGE_HOST_PATHS.sitesDir, onLog).catch(() => {
1873
+ });
1874
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1875
+ });
1876
+ const run = await executor.streamExec(
1877
+ buildEdgeRunCommand(container, image),
1878
+ onLog
1879
+ );
1880
+ invalidateEdgeContainer(executor);
1881
+ return run.code === 0;
1882
+ }
1849
1883
  function buildEdgeRunCommand(container, image) {
1850
1884
  const mounts = EDGE_CONTAINER_MOUNTS.map(
1851
1885
  // `:z` relabels for SELinux-enforcing hosts; a no-op elsewhere.
@@ -1869,14 +1903,7 @@ async function swapEdgeImage(executor, container, from, to, opts) {
1869
1903
  return { swapped: false, edgeDown: false };
1870
1904
  }
1871
1905
  const start = async (image) => {
1872
- await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1873
- });
1874
- const run = await executor.streamExec(
1875
- buildEdgeRunCommand(container, image),
1876
- onLog
1877
- );
1878
- invalidateEdgeContainer(executor);
1879
- if (run.code !== 0) return false;
1906
+ if (!await startEdgeContainer(executor, container, image, onLog)) return false;
1880
1907
  const listening = await waitForPortListening(executor, 80, {
1881
1908
  timeoutMs: opts.verifyTimeoutMs ?? 3e4
1882
1909
  });
@@ -1893,6 +1920,12 @@ async function swapEdgeImage(executor, container, from, to, opts) {
1893
1920
  onLog(log4(`Rollback to ${from} ALSO failed \u2014 the edge is down on this server.`, "error"));
1894
1921
  return { swapped: false, edgeDown: true };
1895
1922
  }
1923
+ async function listDir(executor, dir) {
1924
+ const out = await executor.exec(`ls -1 ${sq(dir)} 2>/dev/null || true`).catch(() => "");
1925
+ return new Set(
1926
+ out.split("\n").map((l) => l.trim()).filter(Boolean)
1927
+ );
1928
+ }
1896
1929
  async function ensureContainerEdge(executor, opts) {
1897
1930
  const { onLog } = opts;
1898
1931
  const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
@@ -1931,16 +1964,27 @@ async function ensureContainerEdge(executor, opts) {
1931
1964
  });
1932
1965
  }
1933
1966
  const sitesTarget = EDGE_HOST_PATHS.sitesDir;
1967
+ let beforeCarry = null;
1934
1968
  if (bareWasOurs) {
1935
1969
  const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
1936
1970
  if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
1937
1971
  onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
1972
+ beforeCarry = await listDir(executor, sitesTarget);
1938
1973
  await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
1939
1974
  });
1940
1975
  }
1941
1976
  }
1942
1977
  const restoreBare = async () => {
1943
1978
  if (!bareWasOurs) return;
1979
+ if (beforeCarry) {
1980
+ const now = await listDir(executor, sitesTarget);
1981
+ const added = [...now].filter((name) => !beforeCarry.has(name));
1982
+ if (added.length > 0) {
1983
+ await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
1984
+ });
1985
+ onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
1986
+ }
1987
+ }
1944
1988
  onLog(log4("Restoring the host OpenResty edge...", "warn"));
1945
1989
  await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
1946
1990
  });
@@ -1953,15 +1997,10 @@ async function ensureContainerEdge(executor, opts) {
1953
1997
  await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
1954
1998
  });
1955
1999
  }
1956
- await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1957
- });
1958
2000
  onLog(log4("Starting the edge container..."));
1959
- const run = await executor.streamExec(
1960
- buildEdgeRunCommand(container, image),
1961
- onLog
1962
- );
1963
- invalidateEdgeContainer(executor);
1964
- if (run.code !== 0) throw new Error("the edge container failed to start");
2001
+ if (!await startEdgeContainer(executor, container, image, onLog)) {
2002
+ throw new Error("the edge container failed to start");
2003
+ }
1965
2004
  await executor.exec(containerCommand(container, "openresty -t"));
1966
2005
  const listening = await waitForPortListening(executor, 80, {
1967
2006
  timeoutMs: opts.verifyTimeoutMs ?? 3e4
@@ -1982,7 +2021,8 @@ ${logs}`, "error"));
1982
2021
  });
1983
2022
  invalidateEdgeContainer(executor);
1984
2023
  await restoreBare();
1985
- throw new Error(`Edge container setup failed: ${msg}`);
2024
+ const reason = edgeFailureReason(logs);
2025
+ throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
1986
2026
  }
1987
2027
  }
1988
2028
 
@@ -2000,7 +2040,6 @@ export {
2000
2040
  rollbackEdgeTakeover,
2001
2041
  completeEdgeTakeover,
2002
2042
  recoverInterruptedTakeover,
2003
- EDGE_CONTAINER_NAME,
2004
2043
  setDefaultEdgeImage,
2005
2044
  resolveEdgeImage,
2006
2045
  dockerAvailable,