openship 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/bare-7QD623QV.js +29 -0
  2. package/dist/chunk-2ANV5ZQ5.js +88 -0
  3. package/dist/{chunk-LEYSL46B.js → chunk-2VSW6Q6C.js} +3911 -898
  4. package/dist/{chunk-5OUWIPOB.js → chunk-5T4UVOM3.js} +16 -86
  5. package/dist/{chunk-EZPPTWJQ.js → chunk-BLEXTYRK.js} +1 -1
  6. package/dist/{chunk-3ZAGC5NT.js → chunk-CIJV43LH.js} +83 -33
  7. package/dist/{chunk-7S4USQYF.js → chunk-DDFUESPJ.js} +67 -124
  8. package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
  9. package/dist/{chunk-XVOBL7BC.js → chunk-FYPFEDPZ.js} +1 -1
  10. package/dist/{chunk-WFRXYWK2.js → chunk-FZ6QCF6B.js} +8 -6
  11. package/dist/{chunk-VHMOQL5Y.js → chunk-GGIAIGUL.js} +125 -80
  12. package/dist/{chunk-WE7BXW6L.js → chunk-GIPJNNI5.js} +38 -44
  13. package/dist/{chunk-ZFJXPCQP.js → chunk-H3WT3A6R.js} +4 -3
  14. package/dist/chunk-H4FS5BED.js +936 -0
  15. package/dist/{chunk-HPXP6LXZ.js → chunk-J2XM3QSP.js} +8 -5
  16. package/dist/{chunk-6JELW75A.js → chunk-J3K6XABC.js} +5 -5
  17. package/dist/{chunk-VDESJSG6.js → chunk-LHFXSC36.js} +558 -109
  18. package/dist/{chunk-MSZUCNPV.js → chunk-MEE6OZFP.js} +3 -3
  19. package/dist/chunk-N5YZMO33.js +111 -0
  20. package/dist/{chunk-VLQ3UIVO.js → chunk-NRTJRNAE.js} +105 -37
  21. package/dist/chunk-PPEBIY5F.js +2331 -0
  22. package/dist/{chunk-RFWJE7ZN.js → chunk-PVP4RGWT.js} +3 -3
  23. package/dist/{chunk-PZRRL5PC.js → chunk-QHMDCYZ3.js} +15 -4
  24. package/dist/chunk-R4VDPFLS.js +106 -0
  25. package/dist/chunk-RDXT4BQB.js +310 -0
  26. package/dist/{chunk-GRTXMUHA.js → chunk-S6B2Z3OO.js} +145 -60
  27. package/dist/chunk-UWEV6XTS.js +249 -0
  28. package/dist/{chunk-QLX7NBEA.js → chunk-VQM6JHJB.js} +1004 -2046
  29. package/dist/{chunk-A7EWM2KA.js → chunk-VSTFHUCV.js} +44 -50
  30. package/dist/chunk-WU54BXFE.js +70 -0
  31. package/dist/{chunk-LFCSC4IU.js → chunk-XWHWTHEL.js} +313 -469
  32. package/dist/{chunk-7334NSJL.js → chunk-ZPJ74LXE.js} +76 -38
  33. package/dist/cloud-KNNABE3O.js +40 -0
  34. package/dist/{detect-P4CIQBRH.js → detect-G4DR3U2N.js} +9 -6
  35. package/dist/{docker-3K2NEBD6.js → docker-YOVPCIB4.js} +11 -7
  36. package/dist/{docker-edge-executor-YZZ6P7IC.js → docker-edge-executor-QFXD3337.js} +8 -6
  37. package/dist/edge-import-CRFVSR6P.js +56 -0
  38. package/dist/{edge-preflight-ES6FH4JK.js → edge-preflight-MXERVAMG.js} +15 -12
  39. package/dist/{ensure-container-edge-YPITNH45.js → ensure-container-edge-F4JUNQIK.js} +13 -10
  40. package/dist/{executor-DZGX55K2.js → executor-CVPAYYHR.js} +10 -5
  41. package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
  42. package/dist/index.js +139 -92
  43. package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
  44. package/dist/{nginx-NSAHWGJV.js → nginx-KLFF43TC.js} +13 -8
  45. package/dist/{openresty-lua-DR4D3EZ6.js → openresty-lua-VKWUV4GB.js} +8 -5
  46. package/dist/{privilege-YTVI4SKF.js → privilege-7D3SSOL3.js} +6 -5
  47. package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
  48. package/dist/server/engine/samples/postfix/master.cf +14 -0
  49. package/dist/server/index.js +44526 -33476
  50. package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
  51. package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
  52. package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
  53. package/dist/server/migrations/0108_credential.sql +68 -0
  54. package/dist/server/migrations/meta/_journal.json +28 -0
  55. package/dist/setup-I5AJC6NJ.js +25 -0
  56. package/dist/ssh-client-625VWGKP.js +29 -0
  57. package/dist/up-QXTOOVXC.js +61 -0
  58. package/package.json +1 -1
  59. package/dist/bare-XQIPDO4R.js +0 -24
  60. package/dist/chunk-6HOA5JT7.js +0 -124
  61. package/dist/chunk-TM2IQPYU.js +0 -434
  62. package/dist/chunk-XRNRT37W.js +0 -316
  63. package/dist/cloud-3DODOWTR.js +0 -36
  64. package/dist/edge-import-MCJNVTZ2.js +0 -51
  65. package/dist/host-channel-preflight-MUXE3KZU.js +0 -49
  66. package/dist/setup-WUUZURHS.js +0 -22
  67. package/dist/up-XEO3SR6Z.js +0 -58
  68. /package/dist/{dist-es-ZP5QDSJ3.js → dist-es-AXFRS7PS.js} +0 -0
@@ -6,13 +6,13 @@ import {
6
6
  detectBuildKillHint,
7
7
  runBuildPipeline,
8
8
  transferLocalDirectory
9
- } from "./chunk-TM2IQPYU.js";
9
+ } from "./chunk-H4FS5BED.js";
10
10
  import {
11
11
  LocalExecutor
12
- } from "./chunk-6FH4AXVY.js";
12
+ } from "./chunk-E2X26PXR.js";
13
13
  import {
14
14
  wrapLocalBuildCommand
15
- } from "./chunk-ZFJXPCQP.js";
15
+ } from "./chunk-H3WT3A6R.js";
16
16
 
17
17
  // ../../packages/adapters/src/runtime/local-build.ts
18
18
  var LOCAL_BUILD_DIR = "/tmp/openship/.builds";
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env sh
2
+ ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
+ import { createRequire as __ospCreateRequire } from "node:module";
4
+ const require = __ospCreateRequire(import.meta.url);
5
+
6
+ // ../../packages/adapters/src/system/errors.ts
7
+ var SSH_AUTH_ERROR_PATTERNS = [
8
+ "All configured authentication methods failed"
9
+ ];
10
+ var RETRYABLE_CONNECTION_ERROR_PATTERNS = [
11
+ "ECONNRESET",
12
+ "ECONNREFUSED",
13
+ "ETIMEDOUT",
14
+ "EHOSTUNREACH",
15
+ "ENETUNREACH",
16
+ "Timed out",
17
+ "Not connected",
18
+ "Connection lost",
19
+ "read ECONNRESET",
20
+ "Handshake failed",
21
+ "keepalive timeout",
22
+ "SSH connection closed before ready",
23
+ // ssh2 emits "Channel open failure: open failed" / "Channel open failure: …"
24
+ // when the SSH SERVER refuses a new channel - typically because the cached
25
+ // connection has gone half-dead (peer-side LOGOUT, network blip,
26
+ // remote sshd MaxSessions). The inner SshExecutor retries `exec`/
27
+ // `streamExec` on this, but `pipeLocal`, `transferIn`, and any other
28
+ // channel-bearing op bubble it up. Listing it here lets `withExecutor`
29
+ // drop the dead connection and retry transparently - fixes the
30
+ // "first redeploy fails, second click works" pattern.
31
+ "Channel open failure",
32
+ "open failed"
33
+ // NOT "socket hang up": it is what an HTTP client reports for ANY far end that vanished
34
+ // mid-request, including a permission-denied Docker socket and a wedged channel, neither
35
+ // of which a fresh connection fixes. A match here makes `withExecutor` replay its whole
36
+ // callback — and those callbacks install mail, create mailboxes and ensure the edge, so a
37
+ // non-retryable cause becomes a repeated mutation. `core/connectivity.ts` labels it
38
+ // without retrying, `api/lib/remote-state.ts` still maps it to a 503, and the bridge's
39
+ // 502 carries the real reason — so leaving it out here costs no diagnosis.
40
+ ];
41
+ var SshDisconnectedError = class extends Error {
42
+ constructor(message = "SSH connection lost") {
43
+ super(message);
44
+ this.name = "SshDisconnectedError";
45
+ }
46
+ };
47
+ function isSshDisconnectedError(err) {
48
+ return err instanceof SshDisconnectedError || err instanceof Error && err.name === "SshDisconnectedError";
49
+ }
50
+ function isSshAuthError(err) {
51
+ if (!(err instanceof Error)) return false;
52
+ return SSH_AUTH_ERROR_PATTERNS.some((pattern) => err.message.includes(pattern));
53
+ }
54
+ var HostChannelUnavailableError = class extends Error {
55
+ code;
56
+ constructor(code, message) {
57
+ super(message);
58
+ this.name = "HostChannelUnavailableError";
59
+ this.code = code;
60
+ }
61
+ };
62
+ function isRetryableRemoteConnectionError(err) {
63
+ if (isSshDisconnectedError(err)) return true;
64
+ if (!(err instanceof Error)) return false;
65
+ return RETRYABLE_CONNECTION_ERROR_PATTERNS.some(
66
+ (pattern) => err.message.includes(pattern)
67
+ );
68
+ }
69
+ function isRemoteConnectionError(err) {
70
+ return isSshAuthError(err) || isRetryableRemoteConnectionError(err);
71
+ }
72
+ var MAX_COMMAND_CHARS = 120;
73
+ function commandForError(command) {
74
+ const oneLine = command.trim().replace(/\s+/g, " ");
75
+ return oneLine.length > MAX_COMMAND_CHARS ? `${oneLine.slice(0, MAX_COMMAND_CHARS)}\u2026 (${command.length} chars)` : oneLine;
76
+ }
77
+ function isRuntimeNotFoundError(err) {
78
+ if (!err || typeof err !== "object") return false;
79
+ const e = err;
80
+ if (e.statusCode === 404) return true;
81
+ return typeof e.message === "string" && /no such (container|image|volume|network)/i.test(e.message);
82
+ }
83
+
84
+ // ../../packages/adapters/src/system/debug.ts
85
+ function isTruthy(value) {
86
+ if (!value) return false;
87
+ return ["1", "true", "yes", "on", "debug"].includes(value.toLowerCase());
88
+ }
89
+ function isSystemDebugEnabled() {
90
+ return isTruthy(process.env.SYSTEM_DEBUG_LOGS);
91
+ }
92
+ function systemDebug(scope, message) {
93
+ if (!isSystemDebugEnabled()) return;
94
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
95
+ console.log(`[system-debug] ${timestamp} [${scope}] ${message}`);
96
+ }
97
+ function formatDuration(startMs) {
98
+ return `${Date.now() - startMs}ms`;
99
+ }
100
+
101
+ export {
102
+ SshDisconnectedError,
103
+ isSshAuthError,
104
+ HostChannelUnavailableError,
105
+ isRetryableRemoteConnectionError,
106
+ isRemoteConnectionError,
107
+ commandForError,
108
+ isRuntimeNotFoundError,
109
+ systemDebug,
110
+ formatDuration
111
+ };
@@ -2,14 +2,6 @@
2
2
  ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
- import {
6
- edgeDownExplanation,
7
- explainEdgeDown,
8
- isEdgeDownFailure
9
- } from "./chunk-I3DPMTSF.js";
10
- import {
11
- edgeFailureReason
12
- } from "./chunk-WE7BXW6L.js";
13
5
  import {
14
6
  ACME_CHALLENGE_LOCATION,
15
7
  ACME_HTTP01_PORT,
@@ -26,14 +18,28 @@ import {
26
18
  edgeChallengeVhostConf,
27
19
  isCloudFrontedHost,
28
20
  luaSourceAvailable
29
- } from "./chunk-PZRRL5PC.js";
21
+ } from "./chunk-QHMDCYZ3.js";
22
+ import {
23
+ edgeDownExplanation,
24
+ explainEdgeDown,
25
+ isEdgeDownFailure
26
+ } from "./chunk-I3DPMTSF.js";
27
+ import {
28
+ probeHostedHttp
29
+ } from "./chunk-R4VDPFLS.js";
30
30
  import {
31
31
  EXECUTOR_DELEGATE
32
- } from "./chunk-WFRXYWK2.js";
32
+ } from "./chunk-FZ6QCF6B.js";
33
33
  import {
34
34
  logEntry,
35
35
  sq
36
- } from "./chunk-ZFJXPCQP.js";
36
+ } from "./chunk-H3WT3A6R.js";
37
+ import {
38
+ edgeFailureReason
39
+ } from "./chunk-GIPJNNI5.js";
40
+ import {
41
+ probeListeningPort
42
+ } from "./chunk-VSTFHUCV.js";
37
43
  import {
38
44
  PROXY_DIRECTIVES,
39
45
  parseProxyValue,
@@ -41,7 +47,7 @@ import {
41
47
  resolveRedirectStatus,
42
48
  safeErrorMessage,
43
49
  sanitizeProxySettings
44
- } from "./chunk-LEYSL46B.js";
50
+ } from "./chunk-2VSW6Q6C.js";
45
51
 
46
52
  // ../../packages/adapters/src/infra/nginx.ts
47
53
  import {
@@ -291,21 +297,49 @@ async function readDeclaredPair(exec, certPath, keyPath, container) {
291
297
  }
292
298
 
293
299
  // ../../packages/adapters/src/system/output-exists.ts
294
- function buildCommand(servedPath) {
300
+ var SAFE_HOST = /^[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/;
301
+ var SAFE_PATH = /^\/[A-Za-z0-9._~/-]*$/;
302
+ function safeRequestPath(path) {
303
+ return SAFE_PATH.test(path) && !path.split("/").includes("..");
304
+ }
305
+ function buildCommand(servedPath, opts) {
295
306
  const p = servedPath.replace(/'/g, "'\\''");
296
- return [
307
+ const parts = [
297
308
  `if [ -e '${p}' ]; then echo FOUND; fi`,
298
- `if [ -f '${p}' ] || [ -f '${p}/index.html' ]; then echo INDEX; fi`,
299
- "true"
300
- ].join("; ");
309
+ `if [ -f '${p}' ] || [ -f '${p}/index.html' ]; then echo INDEX; fi`
310
+ ];
311
+ const host = opts?.hostname;
312
+ const path = opts?.requestPath ?? "/";
313
+ if (host && SAFE_HOST.test(host) && safeRequestPath(path)) {
314
+ const port = opts?.edgePort ?? 80;
315
+ const maxTime = opts?.timeoutSeconds ?? 3;
316
+ parts.push(
317
+ `if command -v curl >/dev/null 2>&1; then echo "HTTP $(curl -sS -o /dev/null -w '%{http_code} %{num_connects}' --max-time ${maxTime} -H 'Host: ${host}' -H 'X-Forwarded-Proto: https' 'http://127.0.0.1:${port}${path}' 2>/dev/null)"; fi`
318
+ );
319
+ }
320
+ parts.push("true");
321
+ return parts.join("; ");
322
+ }
323
+ function statusIsServed(status) {
324
+ if (status >= 200 && status < 400) return true;
325
+ return status === 401 || status === 403;
301
326
  }
302
- async function probeStaticOutput(executor, servedPath) {
327
+ function parseHttp(out) {
328
+ const match = /(^|\n)HTTP (\d+) (\d+)(\r?\n|$)/.exec(out);
329
+ if (!match) return {};
330
+ const status = Number(match[2]);
331
+ const connects = Number(match[3]);
332
+ if (!Number.isFinite(status) || status === 0 || connects === 0) return {};
333
+ return { status, served: statusIsServed(status) };
334
+ }
335
+ async function probeStaticOutput(executor, servedPath, opts) {
303
336
  try {
304
- const out = await executor.exec(buildCommand(servedPath), { timeout: 5e3 });
337
+ const out = await executor.exec(buildCommand(servedPath, opts), { timeout: 8e3 });
305
338
  return {
306
339
  found: /(^|\n)FOUND(\r?\n|$)/.test(out),
307
340
  hasIndex: /(^|\n)INDEX(\r?\n|$)/.test(out),
308
- checked: true
341
+ checked: true,
342
+ ...parseHttp(out)
309
343
  };
310
344
  } catch {
311
345
  return { found: false, hasIndex: false, checked: false };
@@ -321,14 +355,15 @@ var PROXY_HEADERS = `proxy_set_header Host $host;
321
355
  proxy_set_header X-Forwarded-Proto $openship_fwd_proto;
322
356
  proxy_http_version 1.1;
323
357
  proxy_set_header Upgrade $http_upgrade;
324
- proxy_set_header Connection "upgrade";`;
358
+ proxy_set_header Connection "upgrade";
359
+ proxy_pass_header X-Accel-Buffering;`;
325
360
  var FORWARD_VARS = ` set $openship_fwd_proto $scheme;
326
361
  set $openship_redirect_https 1;
327
362
  if ($http_x_forwarded_proto = "https") {
328
363
  set $openship_fwd_proto https;
329
364
  set $openship_redirect_https 0;
330
365
  }`;
331
- var VHOST_GENERATION = 1;
366
+ var VHOST_GENERATION = 2;
332
367
  var GENERATION_MARKER = `# openship-vhost-gen: ${VHOST_GENERATION}`;
333
368
  var GENERATED_HEADER = `# Auto-generated by Openship - do not edit manually
334
369
  ${GENERATION_MARKER}`;
@@ -638,6 +673,11 @@ function assertValidStaticRoot(root, opts) {
638
673
  }
639
674
  }
640
675
  var execFileAsync = promisify(cpExecFile);
676
+ function isAcmePortBindFailure(output) {
677
+ return /could not bind (?:tcp )?port|address already in use|problem binding to port/i.test(
678
+ output || ""
679
+ );
680
+ }
641
681
  function summarizeCertbotFailure(output, domain) {
642
682
  const edgeDown = edgeDownExplanation(output);
643
683
  if (edgeDown) return edgeDown;
@@ -654,7 +694,9 @@ function certbotDiagnosis(output, domain) {
654
694
  const challengeUrl = pick(/https?:\/\/\S*\.well-known\/acme-challenge\/\S*/i);
655
695
  const redirectedToHttps = /^https:/i.test(challengeUrl ?? "");
656
696
  let diagnosis;
657
- if (/timeout during connect|connection refused|failed to connect|Timeout/i.test(text)) {
697
+ if (isAcmePortBindFailure(text)) {
698
+ diagnosis = `Couldn't start the certificate challenge listener for ${domain} on port ${ACME_HTTP01_PORT}: something on this server is already holding it. That port is used only while a certificate is being issued, so this is almost always a certificate run from an earlier attempt that hasn't exited \u2014 retry, and if it persists find the holder with \`ss -ltnp | grep ${ACME_HTTP01_PORT}\` and stop it. Nothing was requested from Let's Encrypt, so this is not a DNS or routing problem.`;
699
+ } else if (/timeout during connect|connection refused|failed to connect|Timeout/i.test(text)) {
658
700
  diagnosis = `Port 80 for ${domain} isn't reachable from the internet \u2014 a firewall / cloud security group is blocking it, the domain doesn't point at this server, or another proxy is still bound to :80.`;
659
701
  } else if (/NXDOMAIN|no\s+(A|AAAA)\s+record|DNS problem|could not be resolved|no records? found/i.test(text)) {
660
702
  diagnosis = `${domain} doesn't resolve to this server yet \u2014 the DNS A record is missing or hasn't propagated. Wait for propagation, then retry from the Domains tab.`;
@@ -968,8 +1010,19 @@ var NginxProvider = class {
968
1010
  *
969
1011
  * Advisory: never throws.
970
1012
  */
971
- async probeStaticRoot(servedPath) {
972
- if (this.executor) return probeStaticOutput(this.executor, servedPath);
1013
+ async probeStaticRoot(servedPath, opts) {
1014
+ if (this.executor) return probeStaticOutput(this.executor, servedPath, opts);
1015
+ const fs = await this.probeStaticRootViaFs(servedPath);
1016
+ if (!opts?.hostname) return fs;
1017
+ const http = await probeHostedHttp({
1018
+ hostname: opts.hostname,
1019
+ path: opts.requestPath ?? "/",
1020
+ port: opts.edgePort ?? 80,
1021
+ timeoutMs: (opts.timeoutSeconds ?? 3) * 1e3
1022
+ });
1023
+ return http ? { ...fs, status: http.status, served: http.served } : fs;
1024
+ }
1025
+ async probeStaticRootViaFs(servedPath) {
973
1026
  const found = await this._exists(servedPath);
974
1027
  if (!found) return { found: false, hasIndex: false, checked: true };
975
1028
  const isFile = await fsStat(servedPath).then((s) => s.isFile()).catch(() => false);
@@ -1450,18 +1503,24 @@ ${serveLocation}
1450
1503
  /**
1451
1504
  * Provision a TLS certificate using certbot.
1452
1505
  *
1453
- * Runs `certbot certonly` in webroot mode using the ACME challenge
1454
- * directory served by OpenResty, then rewrites the config to include
1455
- * SSL and reloads.
1506
+ * HTTP-01 ONLY there is no DNS-01 path anywhere in this file, and the DNS credentials the
1507
+ * product stores are for record MANAGEMENT, not for challenges.
1508
+ *
1509
+ * The authenticator is `--standalone` on {@link ACME_HTTP01_PORT} (a loopback alt-port), with
1510
+ * the edge proxying `/.well-known/acme-challenge/` to it — see `ACME_CHALLENGE_LOCATION`. So
1511
+ * Let's Encrypt fetches the challenge over :80 from OpenResty, which forwards it to certbot's
1512
+ * transient listener. No port-80 fight, no webroot directory to keep in sync, and zero
1513
+ * downtime: certbot holds that port only while issuing.
1456
1514
  *
1457
- * Only --webroot is attempted. The previous --standalone fallback was
1458
- * dead code on any normal install: certbot --standalone binds to port
1459
- * 80 itself, but OpenResty already owns 80 on the same box, so the
1460
- * fallback would always fail with EADDRINUSE amplifying a transient
1461
- * --webroot failure into an immediate hard error. The caller
1462
- * (route-registration.ts) already wraps provisionCert in try/catch so
1463
- * a webroot failure becomes a "deploy continues on HTTP, retry from
1464
- * Domains tab" warning instead of a deploy abort.
1515
+ * THIS COMMENT USED TO DESCRIBE `--webroot`, and argued that `--standalone` was dead code
1516
+ * because "certbot --standalone binds to port 80 itself, but OpenResty already owns 80". That
1517
+ * was true of plain `--standalone`, and it is exactly the problem `--http-01-port` solves
1518
+ * the two sat contradicting each other in one file, with the doc describing a mechanism the
1519
+ * code below had stopped using. Left recorded because the stale version's reasoning is
1520
+ * persuasive enough to "fix" the code back.
1521
+ *
1522
+ * The caller (route-registration.ts) wraps this in try/catch, so a failure becomes a "deploy
1523
+ * continues on HTTP, retry from the Domains tab" warning rather than a deploy abort.
1465
1524
  */
1466
1525
  async provisionCert(domain, opts) {
1467
1526
  assertValidDomain(domain);
@@ -1493,7 +1552,15 @@ ${serveLocation}
1493
1552
  ...opts?.force ? ["--force-renewal"] : []
1494
1553
  ], opts?.onLog);
1495
1554
  } catch (err) {
1496
- throw new Error(summarizeCertbotFailure(this.redactAcmeSecrets(safeErrorMessage(err)), domain));
1555
+ const raw = this.redactAcmeSecrets(safeErrorMessage(err));
1556
+ let summary = summarizeCertbotFailure(raw, domain);
1557
+ if (isAcmePortBindFailure(raw) && this.executor) {
1558
+ const occupant = await probeListeningPort(this.executor, ACME_HTTP01_PORT).catch(() => null);
1559
+ if (occupant) {
1560
+ summary += ` Currently held by ${occupant.command}` + (occupant.pid !== null ? ` (pid ${occupant.pid})` : "") + (occupant.systemdUnit ? ` via ${occupant.systemdUnit}` : "") + `.`;
1561
+ }
1562
+ }
1563
+ throw new Error(summary);
1497
1564
  } finally {
1498
1565
  if (eabConfig) await this._rm(dirname(eabConfig)).catch(() => void 0);
1499
1566
  }
@@ -1965,6 +2032,7 @@ export {
1965
2032
  acmeKeyArgs,
1966
2033
  assertValidChallengeToken,
1967
2034
  MANAGED_STATIC_BASE,
2035
+ isAcmePortBindFailure,
1968
2036
  summarizeCertbotFailure,
1969
2037
  NginxProvider
1970
2038
  };