openship 0.6.7 → 0.6.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.
Files changed (67) hide show
  1. package/dist/bare-FA66N7GC.js +29 -0
  2. package/dist/{chunk-LHFXSC36.js → chunk-3MP6ACKM.js} +183 -87
  3. package/dist/{chunk-MEE6OZFP.js → chunk-5DJOB7IX.js} +3 -3
  4. package/dist/{chunk-PPEBIY5F.js → chunk-5I3COAFH.js} +35 -12
  5. package/dist/{chunk-VQM6JHJB.js → chunk-5UXHT6ED.js} +9 -9
  6. package/dist/{chunk-XWHWTHEL.js → chunk-6GU6JSK6.js} +21 -2
  7. package/dist/{chunk-QHMDCYZ3.js → chunk-7PEXM2UI.js} +213 -45
  8. package/dist/{chunk-RDXT4BQB.js → chunk-7YBWXEM6.js} +4 -4
  9. package/dist/{chunk-H4FS5BED.js → chunk-AHJYP7DG.js} +13 -11
  10. package/dist/{chunk-VSTFHUCV.js → chunk-BO6CY4V2.js} +109 -23
  11. package/dist/{chunk-J3K6XABC.js → chunk-D3OEMXYP.js} +3 -3
  12. package/dist/{chunk-UWEV6XTS.js → chunk-D6HU4VUZ.js} +1 -1
  13. package/dist/{chunk-S6B2Z3OO.js → chunk-ENKJ7XIF.js} +21 -11
  14. package/dist/{chunk-BLEXTYRK.js → chunk-GNOMEMNI.js} +1 -1
  15. package/dist/{chunk-NRTJRNAE.js → chunk-IAMOUDHY.js} +138 -54
  16. package/dist/{chunk-CIJV43LH.js → chunk-MUWVC5R4.js} +161 -39
  17. package/dist/{chunk-ZPJ74LXE.js → chunk-NIYD7MD2.js} +197 -45
  18. package/dist/{chunk-FZ6QCF6B.js → chunk-NPSRGIHV.js} +2 -2
  19. package/dist/{chunk-2GWYYHOR.js → chunk-NRU2EBYZ.js} +2 -2
  20. package/dist/{chunk-J2XM3QSP.js → chunk-NUT6HKK6.js} +5 -5
  21. package/dist/{chunk-H3WT3A6R.js → chunk-P7RCYELG.js} +1 -1
  22. package/dist/{chunk-FYPFEDPZ.js → chunk-R5DC5PEF.js} +1 -1
  23. package/dist/{chunk-DDFUESPJ.js → chunk-RCMERVBL.js} +9 -9
  24. package/dist/{chunk-RXQBD4ZY.js → chunk-RHLNVZMS.js} +2 -2
  25. package/dist/{chunk-WU54BXFE.js → chunk-SK4FFCFR.js} +1 -1
  26. package/dist/{chunk-PVP4RGWT.js → chunk-T46BQOLY.js} +3 -3
  27. package/dist/{chunk-E2X26PXR.js → chunk-T6DVUX4F.js} +1 -1
  28. package/dist/{chunk-5T4UVOM3.js → chunk-TAZZZKYO.js} +1 -1
  29. package/dist/{chunk-GGIAIGUL.js → chunk-UAWY6X7V.js} +34 -12
  30. package/dist/{chunk-2ANV5ZQ5.js → chunk-VAI5VOOI.js} +1 -1
  31. package/dist/{chunk-2VSW6Q6C.js → chunk-WEZFTL4V.js} +655 -62
  32. package/dist/{chunk-GIPJNNI5.js → chunk-X53JXRS4.js} +3 -3
  33. package/dist/{cloud-KRTZ2IL5.js → cloud-2CRDKOWQ.js} +1 -1
  34. package/dist/cloud-REV3DPJH.js +42 -0
  35. package/dist/{detect-G4DR3U2N.js → detect-BBFT5GEU.js} +4 -4
  36. package/dist/{docker-YOVPCIB4.js → docker-EG3QSE6I.js} +10 -10
  37. package/dist/{docker-edge-executor-QFXD3337.js → docker-edge-executor-E5TOTLMR.js} +7 -7
  38. package/dist/edge-import-YJVJUGYC.js +56 -0
  39. package/dist/{edge-preflight-MXERVAMG.js → edge-preflight-QUW4BM7N.js} +13 -13
  40. package/dist/{ensure-container-edge-F4JUNQIK.js → ensure-container-edge-FOEFQ3PI.js} +13 -11
  41. package/dist/{executor-CVPAYYHR.js → executor-NTWDJ3DB.js} +6 -6
  42. package/dist/host-channel-preflight-OMCZMMKR.js +54 -0
  43. package/dist/index.js +158 -72
  44. package/dist/{local-executor-ODC6RANU.js → local-executor-OQ2GRN7K.js} +3 -3
  45. package/dist/{nginx-KLFF43TC.js → nginx-4U4C2XUS.js} +9 -9
  46. package/dist/{noop-NJ72F46T.js → noop-CLLJHCPL.js} +1 -1
  47. package/dist/{openresty-lua-VKWUV4GB.js → openresty-lua-LLWT5XQW.js} +7 -5
  48. package/dist/{privilege-7D3SSOL3.js → privilege-MNDVMDST.js} +5 -5
  49. package/dist/server/index.js +31643 -25553
  50. package/dist/server/migrations/0109_service_build_args.sql +9 -0
  51. package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
  52. package/dist/server/migrations/0111_host_port_claim.sql +381 -0
  53. package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
  54. package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
  55. package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
  56. package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
  57. package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
  58. package/dist/server/migrations/meta/_journal.json +56 -0
  59. package/dist/{setup-I5AJC6NJ.js → setup-IFIMPQZJ.js} +12 -12
  60. package/dist/{ssh-client-625VWGKP.js → ssh-client-2XWZCMGI.js} +2 -2
  61. package/dist/up-HQS7IR7H.js +61 -0
  62. package/package.json +1 -1
  63. package/dist/bare-7QD623QV.js +0 -29
  64. package/dist/cloud-KNNABE3O.js +0 -40
  65. package/dist/edge-import-CRFVSR6P.js +0 -56
  66. package/dist/host-channel-preflight-QTDNGLCK.js +0 -54
  67. package/dist/up-QXTOOVXC.js +0 -61
@@ -7,13 +7,13 @@ import {
7
7
  isContainerPortForwarder,
8
8
  managedDeploymentIdFromUnit,
9
9
  tryExec
10
- } from "./chunk-UWEV6XTS.js";
10
+ } from "./chunk-D6HU4VUZ.js";
11
11
  import {
12
12
  shellQuote
13
- } from "./chunk-2VSW6Q6C.js";
13
+ } from "./chunk-WEZFTL4V.js";
14
14
 
15
15
  // ../../packages/adapters/src/system/port-listen.ts
16
- var PROC_NET_TCP_CMD = "cat /proc/net/tcp 2>/dev/null; cat /proc/net/tcp6 2>/dev/null; true";
16
+ var PROC_NET_TCP_FILES = ["/proc/net/tcp", "/proc/net/tcp6"];
17
17
  var TCP_LISTEN = "0A";
18
18
  function parseListeningPorts(procText) {
19
19
  const ports = /* @__PURE__ */ new Set();
@@ -34,12 +34,15 @@ function parseListeningPorts(procText) {
34
34
  return ports;
35
35
  }
36
36
  async function probePortListeningOnce(executor, port) {
37
- try {
38
- const out = await executor.exec(PROC_NET_TCP_CMD, { timeout: 5e3 });
39
- return parseListeningPorts(out).has(port);
40
- } catch {
41
- return null;
37
+ const readable = [];
38
+ for (const file of PROC_NET_TCP_FILES) {
39
+ try {
40
+ readable.push(await executor.exec(`cat ${file} 2>/dev/null`, { timeout: 5e3 }));
41
+ } catch {
42
+ }
42
43
  }
44
+ if (readable.length === 0) return null;
45
+ return parseListeningPorts(readable.join("\n")).has(port);
43
46
  }
44
47
  function delay(ms) {
45
48
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -85,12 +88,14 @@ async function waitForPortFree(executor, port, opts = {}) {
85
88
 
86
89
  // ../../packages/adapters/src/runtime/port-conflict.ts
87
90
  async function resolveListenerOwner(executor, pid) {
88
- const cgroup = await tryExec(executor, `cat /proc/${pid}/cgroup 2>/dev/null || true`);
91
+ const cgroup = (await tryExec(executor, `cat /proc/${pid}/cgroup 2>/dev/null`))?.trim();
92
+ if (!cgroup) return { cgroupVerified: false };
93
+ const evidence = { cgroupVerified: true, processCgroup: cgroup };
89
94
  const containerId = containerIdFromCgroup(cgroup);
90
- if (containerId) return { containerId };
95
+ if (containerId) return { ...evidence, containerId };
91
96
  const systemdUnit = cgroup?.match(/(?:^|\/)([^/\n]+\.service)(?:$|\n|\/)/m)?.[1]?.trim();
92
97
  if (!systemdUnit || !/^[A-Za-z0-9@._:-]+\.service$/.test(systemdUnit)) {
93
- return {};
98
+ return evidence;
94
99
  }
95
100
  const description = await tryExec(
96
101
  executor,
@@ -98,6 +103,7 @@ async function resolveListenerOwner(executor, pid) {
98
103
  );
99
104
  const deploymentId = managedDeploymentIdFromUnit(systemdUnit);
100
105
  return {
106
+ ...evidence,
101
107
  systemdUnit,
102
108
  systemdDescription: description?.trim() || void 0,
103
109
  ...deploymentId ? { deploymentId, isManagedDeployment: true } : { isManagedDeployment: false }
@@ -108,25 +114,104 @@ async function resolveListener(executor, port) {
108
114
  executor,
109
115
  `ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`
110
116
  );
111
- if (out) {
112
- const ssMatch = out.match(/pid=(\d+)/);
113
- const lsofMatch = !ssMatch ? out.match(/^\s*(\d+)\s*$/m) : null;
114
- const pid = ssMatch ? parseInt(ssMatch[1], 10) : lsofMatch ? parseInt(lsofMatch[1], 10) : null;
115
- if (pid) return { pid, occupied: true };
117
+ const listenerPid = (output) => {
118
+ if (!output) return null;
119
+ const ssMatch = output.match(/pid=(\d+)/);
120
+ const lsofMatch = !ssMatch ? output.match(/^\s*(\d+)\s*$/m) : null;
121
+ const parsed = ssMatch?.[1] ?? lsofMatch?.[1];
122
+ if (!parsed) return null;
123
+ const pid2 = Number.parseInt(parsed, 10);
124
+ return Number.isSafeInteger(pid2) && pid2 > 0 ? pid2 : null;
125
+ };
126
+ const pid = listenerPid(out);
127
+ if (pid) return { pid, occupied: true, checked: true };
128
+ const observedListener = Boolean(out?.trim());
129
+ if (observedListener) {
130
+ const privilegedOut = await tryExec(
131
+ executor,
132
+ `sudo -n ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || sudo -n lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`,
133
+ { timeout: 5e3 }
134
+ );
135
+ const privilegedPid = listenerPid(privilegedOut);
136
+ if (privilegedPid) return { pid: privilegedPid, occupied: true, checked: true };
116
137
  }
117
138
  const listening = await probePortListeningOnce(executor, port);
118
- return { pid: null, occupied: listening === true };
139
+ return {
140
+ pid: null,
141
+ occupied: observedListener || listening === true,
142
+ checked: observedListener || listening !== null
143
+ };
119
144
  }
120
- async function probeListeningPort(executor, port) {
145
+ function parseListenerPids(output) {
146
+ const lines = (output ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
147
+ if (lines.length === 0) return { observed: false, complete: true, pids: [] };
148
+ const pids = /* @__PURE__ */ new Set();
149
+ let complete = true;
150
+ for (const line of lines) {
151
+ const matches = [...line.matchAll(/pid=(\d+)/g)].map((match) => match[1]);
152
+ const bare = matches.length === 0 ? line.match(/^(\d+)$/)?.[1] : void 0;
153
+ const values = bare ? [bare] : matches;
154
+ if (values.length === 0) complete = false;
155
+ for (const value of values) {
156
+ const pid = Number.parseInt(value, 10);
157
+ if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
158
+ else complete = false;
159
+ }
160
+ }
161
+ return { observed: true, complete, pids: [...pids] };
162
+ }
163
+ async function probeListeningPortOwners(executor, port) {
164
+ const command = `ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true`;
165
+ const ordinary = parseListenerPids(await tryExec(executor, command));
166
+ let evidence = ordinary;
167
+ if (ordinary.observed && !ordinary.complete) {
168
+ const privileged = parseListenerPids(
169
+ await tryExec(
170
+ executor,
171
+ `sudo -n ss -tlnp sport = :${port} 2>/dev/null | grep LISTEN || sudo -n lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null`,
172
+ { timeout: 5e3 }
173
+ )
174
+ );
175
+ if (privileged.observed && privileged.complete) evidence = privileged;
176
+ else {
177
+ evidence = {
178
+ observed: true,
179
+ complete: false,
180
+ pids: [.../* @__PURE__ */ new Set([...ordinary.pids, ...privileged.pids])]
181
+ };
182
+ }
183
+ }
184
+ if (!evidence.observed) {
185
+ const listening = await probePortListeningOnce(executor, port);
186
+ if (listening === null) return { occupants: [], checked: false, ownershipComplete: false };
187
+ if (!listening) return { occupants: [], checked: true, ownershipComplete: true };
188
+ return { occupants: [], checked: true, ownershipComplete: false };
189
+ }
190
+ const described = await Promise.all(
191
+ evidence.pids.map((pid) => describeProcess(executor, pid).catch(() => null))
192
+ );
193
+ const occupants = described.filter((row) => row !== null);
194
+ return {
195
+ occupants,
196
+ checked: true,
197
+ ownershipComplete: evidence.complete && occupants.length === evidence.pids.length
198
+ };
199
+ }
200
+ async function probeListeningPortState(executor, port) {
121
201
  try {
122
- const { pid, occupied } = await resolveListener(executor, port);
123
- if (!occupied) return null;
124
- if (pid === null) return { pid: null, command: "unknown listener" };
125
- return await describeProcess(executor, pid);
202
+ const { pid, occupied, checked } = await resolveListener(executor, port);
203
+ if (!checked || !occupied) return { occupant: null, checked };
204
+ if (pid === null) {
205
+ return { occupant: { pid: null, command: "unknown listener" }, checked: true };
206
+ }
207
+ return { occupant: await describeProcess(executor, pid), checked: true };
126
208
  } catch {
127
- return null;
209
+ return { occupant: null, checked: false };
128
210
  }
129
211
  }
212
+ async function probeListeningPort(executor, port) {
213
+ return (await probeListeningPortState(executor, port)).occupant;
214
+ }
130
215
  async function describeProcess(executor, pid) {
131
216
  let command = `PID ${pid}`;
132
217
  let rawCommand;
@@ -173,6 +258,7 @@ function portOccupantDetails(port, occupant, stopTarget) {
173
258
  export {
174
259
  waitForPortListening,
175
260
  waitForPortFree,
261
+ probeListeningPortOwners,
176
262
  probeListeningPort,
177
263
  describeProcess,
178
264
  portOccupantDetails
@@ -8,14 +8,14 @@ import {
8
8
  } from "./chunk-I3DPMTSF.js";
9
9
  import {
10
10
  LocalExecutor
11
- } from "./chunk-E2X26PXR.js";
11
+ } from "./chunk-T6DVUX4F.js";
12
12
  import {
13
13
  logEntry,
14
14
  sq
15
- } from "./chunk-H3WT3A6R.js";
15
+ } from "./chunk-P7RCYELG.js";
16
16
  import {
17
17
  edgeFailureReason
18
- } from "./chunk-GIPJNNI5.js";
18
+ } from "./chunk-X53JXRS4.js";
19
19
 
20
20
  // ../../packages/adapters/src/system/docker-edge-executor.ts
21
21
  import Dockerode from "dockerode";
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  safeErrorMessage,
12
12
  shellQuote
13
- } from "./chunk-2VSW6Q6C.js";
13
+ } from "./chunk-WEZFTL4V.js";
14
14
 
15
15
  // ../../packages/adapters/src/system/probe-exec.ts
16
16
  async function tryExec(executor, command, opts) {
@@ -7,11 +7,11 @@ import {
7
7
  resolveProjectDir,
8
8
  resolveStaticOutputPath,
9
9
  runLocalBuild
10
- } from "./chunk-MEE6OZFP.js";
10
+ } from "./chunk-5DJOB7IX.js";
11
11
  import {
12
12
  isArtifactPathRef,
13
13
  removeManagedArtifact
14
- } from "./chunk-WU54BXFE.js";
14
+ } from "./chunk-SK4FFCFR.js";
15
15
  import {
16
16
  BuildCancelledError,
17
17
  BuildLogger,
@@ -24,36 +24,36 @@ import {
24
24
  splitRuntimeEnv,
25
25
  sq,
26
26
  transferLocalDirectory
27
- } from "./chunk-H4FS5BED.js";
27
+ } from "./chunk-AHJYP7DG.js";
28
28
  import {
29
29
  LocalExecutor
30
- } from "./chunk-E2X26PXR.js";
30
+ } from "./chunk-T6DVUX4F.js";
31
31
  import {
32
32
  privilegedExecutor
33
- } from "./chunk-PVP4RGWT.js";
33
+ } from "./chunk-T46BQOLY.js";
34
34
  import {
35
35
  HOST_STATE_DIR,
36
36
  envOps,
37
37
  hostRefusal,
38
38
  opScript
39
- } from "./chunk-BLEXTYRK.js";
39
+ } from "./chunk-GNOMEMNI.js";
40
40
  import {
41
41
  ensureOwnedDir,
42
42
  resolveEnvironment
43
- } from "./chunk-FZ6QCF6B.js";
43
+ } from "./chunk-NPSRGIHV.js";
44
44
  import {
45
45
  sq as sq2,
46
46
  wrapLocalBuildCommand
47
- } from "./chunk-H3WT3A6R.js";
47
+ } from "./chunk-P7RCYELG.js";
48
48
  import {
49
49
  portOccupantDetails,
50
50
  probeListeningPort
51
- } from "./chunk-VSTFHUCV.js";
51
+ } from "./chunk-BO6CY4V2.js";
52
52
  import {
53
53
  managedDeploymentUnitName,
54
54
  probeExec,
55
55
  withReason
56
- } from "./chunk-UWEV6XTS.js";
56
+ } from "./chunk-D6HU4VUZ.js";
57
57
  import {
58
58
  formatDuration,
59
59
  isRemoteConnectionError,
@@ -73,7 +73,7 @@ import {
73
73
  refused,
74
74
  safeErrorMessage,
75
75
  shellQuote
76
- } from "./chunk-2VSW6Q6C.js";
76
+ } from "./chunk-WEZFTL4V.js";
77
77
 
78
78
  // ../../packages/adapters/src/system/remote-journal.ts
79
79
  var OPSH_RUN_VERSION = 1;
@@ -1608,6 +1608,16 @@ var BareRuntime = class {
1608
1608
  async inContainerExecutor() {
1609
1609
  return this.executor;
1610
1610
  }
1611
+ /**
1612
+ * Resolve a retained application release for an env-only refresh.
1613
+ * Deployment ids originate in our database, but reject path-shaped input at
1614
+ * this boundary so an imported/corrupt row cannot escape releases/.
1615
+ */
1616
+ async retainedReleaseArtifact(deploymentId) {
1617
+ if (!deploymentId || deploymentId.includes("/") || deploymentId.includes("\\")) return null;
1618
+ const path = this.releaseDir(deploymentId);
1619
+ return await this.executor.exists(path).catch(() => false) ? path : null;
1620
+ }
1611
1621
  /** Get or lazily initialise the process supervisor. */
1612
1622
  async supervisor() {
1613
1623
  if (this._supervisor) return this._supervisor;
@@ -9,7 +9,7 @@ import {
9
9
  firewallPersists,
10
10
  refused,
11
11
  shellQuote
12
- } from "./chunk-2VSW6Q6C.js";
12
+ } from "./chunk-WEZFTL4V.js";
13
13
 
14
14
  // ../../packages/adapters/src/system/environment-ops.ts
15
15
  function opScript(steps) {
@@ -17,8 +17,9 @@ import {
17
17
  detectOpenRestyPaths,
18
18
  edgeChallengeVhostConf,
19
19
  isCloudFrontedHost,
20
- luaSourceAvailable
21
- } from "./chunk-QHMDCYZ3.js";
20
+ luaSourceAvailable,
21
+ reloadBareOpenResty
22
+ } from "./chunk-7PEXM2UI.js";
22
23
  import {
23
24
  edgeDownExplanation,
24
25
  explainEdgeDown,
@@ -29,17 +30,17 @@ import {
29
30
  } from "./chunk-R4VDPFLS.js";
30
31
  import {
31
32
  EXECUTOR_DELEGATE
32
- } from "./chunk-FZ6QCF6B.js";
33
+ } from "./chunk-NPSRGIHV.js";
33
34
  import {
34
35
  logEntry,
35
36
  sq
36
- } from "./chunk-H3WT3A6R.js";
37
+ } from "./chunk-P7RCYELG.js";
37
38
  import {
38
39
  edgeFailureReason
39
- } from "./chunk-GIPJNNI5.js";
40
+ } from "./chunk-X53JXRS4.js";
40
41
  import {
41
42
  probeListeningPort
42
- } from "./chunk-VSTFHUCV.js";
43
+ } from "./chunk-BO6CY4V2.js";
43
44
  import {
44
45
  PROXY_DIRECTIVES,
45
46
  parseProxyValue,
@@ -47,7 +48,7 @@ import {
47
48
  resolveRedirectStatus,
48
49
  safeErrorMessage,
49
50
  sanitizeProxySettings
50
- } from "./chunk-2VSW6Q6C.js";
51
+ } from "./chunk-WEZFTL4V.js";
51
52
 
52
53
  // ../../packages/adapters/src/infra/nginx.ts
53
54
  import {
@@ -593,7 +594,7 @@ function acmeKeyArgs(keyType) {
593
594
  return [];
594
595
  }
595
596
  }
596
- var DOMAIN_RE = /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/;
597
+ var DOMAIN_RE = /^(\*\.)?[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/;
597
598
  function assertValidDomain(domain) {
598
599
  if (!DOMAIN_RE.test(domain) || domain.length > 253) {
599
600
  throw new Error(`Invalid domain: ${domain}`);
@@ -601,7 +602,9 @@ function assertValidDomain(domain) {
601
602
  }
602
603
  function assertNoNginxInjection(value, what) {
603
604
  if (/[\s;{}#\\'"]/.test(value)) {
604
- throw new Error(`Invalid ${what} (contains characters that could inject nginx config): ${value}`);
605
+ throw new Error(
606
+ `Invalid ${what} (contains characters that could inject nginx config): ${value}`
607
+ );
605
608
  }
606
609
  }
607
610
  function assertNoRuntimeVariable(value, what, allowCaptures = false) {
@@ -698,7 +701,9 @@ function certbotDiagnosis(output, domain) {
698
701
  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
702
  } else if (/timeout during connect|connection refused|failed to connect|Timeout/i.test(text)) {
700
703
  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.`;
701
- } else if (/NXDOMAIN|no\s+(A|AAAA)\s+record|DNS problem|could not be resolved|no records? found/i.test(text)) {
704
+ } else if (/NXDOMAIN|no\s+(A|AAAA)\s+record|DNS problem|could not be resolved|no records? found/i.test(
705
+ text
706
+ )) {
702
707
  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.`;
703
708
  } else if (/\b52[56]\b|ssl handshake failed/i.test(text)) {
704
709
  diagnosis = `Cloudflare's proxy is answering the certificate challenge for ${domain} instead of forwarding it to this server: it redirects the request to HTTPS, then rejects the origin's TLS. Fix it one of three ways \u2014 set SSL/TLS \u2192 Overview to "Full" (not "Full (strict)") until the first certificate is issued, or turn the orange cloud off for this record until it is, or upload a Cloudflare Origin CA certificate from the Domains tab and skip Let's Encrypt entirely.`;
@@ -721,6 +726,7 @@ var NginxProvider = class {
721
726
  acmeTosAgreed;
722
727
  certDir;
723
728
  executor;
729
+ paths;
724
730
  reloadCommand;
725
731
  pinPaths;
726
732
  containerEdge;
@@ -744,8 +750,9 @@ var NginxProvider = class {
744
750
  this.acmeTosAgreed = opts.acmeTosAgreed ?? true;
745
751
  this.certDir = opts.certDir ?? DEFAULT_CERT_DIR;
746
752
  this.executor = opts.executor ?? null;
753
+ this.paths = opts.paths;
747
754
  this.containerEdge = opts.containerEdge ?? false;
748
- this.reloadCommand = buildReloadCommand(opts.paths, { containerEdge: this.containerEdge });
755
+ this.reloadCommand = buildReloadCommand(opts.paths);
749
756
  this.pinPaths = opts.pinPaths ?? false;
750
757
  this.challengeDir = opts.challengeDir ?? EDGE_CHALLENGE_DIR;
751
758
  }
@@ -1094,6 +1101,31 @@ eab-hmac-key = ${this.acmeEabHmacKey}
1094
1101
  }
1095
1102
  return path;
1096
1103
  }
1104
+ /** Materialize generated DNS hooks on the same target where Certbot runs. */
1105
+ async createEphemeralDnsHooks(opts) {
1106
+ if (!opts?.dnsAuthHookScript) return null;
1107
+ const dir = join(dirname(this.certDir), `.openship-dns-${randomBytes(12).toString("hex")}`);
1108
+ const authPath = join(dir, "auth.sh");
1109
+ const cleanupPath = join(dir, "cleanup.sh");
1110
+ const recordPath = join(dir, "record-id.txt");
1111
+ await this._mkdir(dir);
1112
+ try {
1113
+ await this._chmod(dir, 448);
1114
+ await this._writeFile(authPath, opts.dnsAuthHookScript, 448);
1115
+ if (opts.dnsCleanupHookScript) {
1116
+ await this._writeFile(cleanupPath, opts.dnsCleanupHookScript, 448);
1117
+ }
1118
+ } catch (err) {
1119
+ await this._rm(dir).catch(() => void 0);
1120
+ throw err;
1121
+ }
1122
+ const recordEnv = `OPENSHIP_DNS_RECORD_FILE=${sq(recordPath)}`;
1123
+ return {
1124
+ dir,
1125
+ authCommand: `${recordEnv} ${sq(authPath)}`,
1126
+ cleanupCommand: opts.dnsCleanupHookScript ? `${recordEnv} ${sq(cleanupPath)}` : void 0
1127
+ };
1128
+ }
1097
1129
  async _captureFile(path) {
1098
1130
  if (!await this._exists(path)) {
1099
1131
  return { exists: false };
@@ -1347,7 +1379,9 @@ ${serveLocation}
1347
1379
  */
1348
1380
  async _lsSites() {
1349
1381
  if (!this.executor) return fsReaddir(this.sitesDir);
1350
- const out = await this.executor.exec(`ls -1 ${sq(this._commandSitesDir())} 2>/dev/null || true`);
1382
+ const out = await this.executor.exec(
1383
+ `ls -1 ${sq(this._commandSitesDir())} 2>/dev/null || true`
1384
+ );
1351
1385
  return out.split("\n").map((l) => l.trim()).filter(Boolean);
1352
1386
  }
1353
1387
  /**
@@ -1410,8 +1444,11 @@ ${serveLocation}
1410
1444
  * from the old in-memory config (an inconsistent half-state that would silently
1411
1445
  * vanish on the next unrelated reload). The error is re-thrown for retry.
1412
1446
  */
1413
- async removeRoute(domain) {
1447
+ async removeRoute(domain, opts) {
1414
1448
  assertValidDomain(domain);
1449
+ if (opts?.signal?.aborted) {
1450
+ throw new Error(`Route removal aborted before it started: ${domain}`);
1451
+ }
1415
1452
  const slug = await this.resolveSlug(domain);
1416
1453
  const configPath = join(this.sitesDir, `${slug}.conf`);
1417
1454
  const statePath = this.routeStatePath(slug);
@@ -1420,11 +1457,20 @@ ${serveLocation}
1420
1457
  await this._rm(configPath);
1421
1458
  await this._rm(statePath).catch(() => void 0);
1422
1459
  try {
1423
- await this.reload();
1460
+ await this.reload({ allowStopped: true });
1424
1461
  } catch (err) {
1425
- await this._restoreFile(configPath, confSnapshot);
1426
- await this._restoreFile(statePath, stateSnapshot);
1427
- await this.reload().catch(() => void 0);
1462
+ try {
1463
+ if (!opts?.signal?.aborted) await this._restoreFile(configPath, confSnapshot);
1464
+ if (!opts?.signal?.aborted) await this._restoreFile(statePath, stateSnapshot);
1465
+ if (!opts?.signal?.aborted) {
1466
+ await this.reload({ allowStopped: true }).catch(() => void 0);
1467
+ }
1468
+ } finally {
1469
+ if (opts?.signal?.aborted) {
1470
+ await this._rm(configPath).catch(() => void 0);
1471
+ await this._rm(statePath).catch(() => void 0);
1472
+ }
1473
+ }
1428
1474
  throw err;
1429
1475
  }
1430
1476
  await this._rm(this.bootstrapCertDir(domain)).catch(() => void 0);
@@ -1531,31 +1577,47 @@ ${serveLocation}
1531
1577
  const emailArgs = validEmail ? ["--email", this.acmeEmail] : ["--register-unsafely-without-email"];
1532
1578
  let certonlyOut = "";
1533
1579
  const eabConfig = await this.createEphemeralEabConfig();
1580
+ let generatedDnsHooks = null;
1534
1581
  try {
1535
- certonlyOut = await this._execCertbot([
1536
- ...eabConfig ? ["--config", eabConfig] : [],
1582
+ generatedDnsHooks = await this.createEphemeralDnsHooks(opts);
1583
+ const isDnsChallenge = opts?.challenge === "dns-01" || domain.startsWith("*.");
1584
+ const challengeArgs = isDnsChallenge ? [
1537
1585
  "certonly",
1538
- "--standalone",
1539
- "--http-01-port",
1540
- String(ACME_HTTP01_PORT),
1541
- "--cert-name",
1542
- domain,
1543
- "-d",
1544
- domain,
1545
- ...this.acmeDirectoryUrl ? ["--server", this.acmeDirectoryUrl] : [],
1546
- ...acmeKeyArgs(this.acmeKeyType),
1547
- ...emailArgs,
1548
- ...this.acmeTosAgreed ? ["--agree-tos"] : [],
1549
- "--non-interactive",
1550
- // Forced reissue: certbot would otherwise print "not due for renewal"
1551
- // (exit 0) when a lineage exists, leaving the stale cert in place.
1552
- ...opts?.force ? ["--force-renewal"] : []
1553
- ], opts?.onLog);
1586
+ "--manual",
1587
+ "--preferred-challenges",
1588
+ "dns",
1589
+ ...generatedDnsHooks?.authCommand || opts?.dnsAuthHook ? ["--manual-auth-hook", generatedDnsHooks?.authCommand ?? opts.dnsAuthHook] : [],
1590
+ ...generatedDnsHooks?.cleanupCommand || opts?.dnsCleanupHook ? [
1591
+ "--manual-cleanup-hook",
1592
+ generatedDnsHooks?.cleanupCommand ?? opts.dnsCleanupHook
1593
+ ] : []
1594
+ ] : ["certonly", "--standalone", "--http-01-port", String(ACME_HTTP01_PORT)];
1595
+ certonlyOut = await this._execCertbot(
1596
+ [
1597
+ ...eabConfig ? ["--config", eabConfig] : [],
1598
+ ...challengeArgs,
1599
+ "--cert-name",
1600
+ domain,
1601
+ "-d",
1602
+ domain,
1603
+ ...this.acmeDirectoryUrl ? ["--server", this.acmeDirectoryUrl] : [],
1604
+ ...acmeKeyArgs(this.acmeKeyType),
1605
+ ...emailArgs,
1606
+ ...this.acmeTosAgreed ? ["--agree-tos"] : [],
1607
+ "--non-interactive",
1608
+ // Forced reissue: certbot would otherwise print "not due for renewal"
1609
+ // (exit 0) when a lineage exists, leaving the stale cert in place.
1610
+ ...opts?.force ? ["--force-renewal"] : []
1611
+ ],
1612
+ opts?.onLog
1613
+ );
1554
1614
  } catch (err) {
1555
1615
  const raw = this.redactAcmeSecrets(safeErrorMessage(err));
1556
1616
  let summary = summarizeCertbotFailure(raw, domain);
1557
1617
  if (isAcmePortBindFailure(raw) && this.executor) {
1558
- const occupant = await probeListeningPort(this.executor, ACME_HTTP01_PORT).catch(() => null);
1618
+ const occupant = await probeListeningPort(this.executor, ACME_HTTP01_PORT).catch(
1619
+ () => null
1620
+ );
1559
1621
  if (occupant) {
1560
1622
  summary += ` Currently held by ${occupant.command}` + (occupant.pid !== null ? ` (pid ${occupant.pid})` : "") + (occupant.systemdUnit ? ` via ${occupant.systemdUnit}` : "") + `.`;
1561
1623
  }
@@ -1563,6 +1625,7 @@ ${serveLocation}
1563
1625
  throw new Error(summary);
1564
1626
  } finally {
1565
1627
  if (eabConfig) await this._rm(dirname(eabConfig)).catch(() => void 0);
1628
+ if (generatedDnsHooks) await this._rm(generatedDnsHooks.dir).catch(() => void 0);
1566
1629
  }
1567
1630
  const slug = await this.resolveSlug(domain);
1568
1631
  const configPath = join(this.sitesDir, `${slug}.conf`);
@@ -1630,24 +1693,24 @@ ${tail}` : "")
1630
1693
  /**
1631
1694
  * Renew a TLS certificate using certbot.
1632
1695
  */
1633
- async renewCert(domain) {
1696
+ async renewCert(domain, opts) {
1634
1697
  assertValidDomain(domain);
1698
+ if (opts?.challenge === "dns-01" || domain.startsWith("*.")) {
1699
+ return this.provisionCert(domain, { ...opts, challenge: "dns-01", force: true });
1700
+ }
1635
1701
  if (!await this.certsExist(domain)) {
1636
- return this.provisionCert(domain);
1702
+ return this.provisionCert(domain, opts);
1637
1703
  }
1638
1704
  if (!await this.hasCertbotLineage(domain)) {
1639
- return this.provisionCert(domain, { force: true });
1705
+ return this.provisionCert(domain, { ...opts, force: true });
1640
1706
  }
1641
1707
  if (!await this.lineageServerMatches(domain)) {
1642
- return this.provisionCert(domain, { force: true });
1708
+ return this.provisionCert(domain, { ...opts, force: true });
1643
1709
  }
1644
- await this._execCertbot([
1645
- "renew",
1646
- "--cert-name",
1647
- domain,
1648
- ...acmeKeyArgs(this.acmeKeyType),
1649
- "--non-interactive"
1650
- ]);
1710
+ await this._execCertbot(
1711
+ ["renew", "--cert-name", domain, ...acmeKeyArgs(this.acmeKeyType), "--non-interactive"],
1712
+ opts?.onLog
1713
+ );
1651
1714
  await this.reload();
1652
1715
  return this.readCertInfo(domain);
1653
1716
  }
@@ -1805,23 +1868,42 @@ ${tail}` : "")
1805
1868
  * config may move). Re-detecting on every reload keeps the provider
1806
1869
  * in sync without requiring an API restart.
1807
1870
  */
1808
- async reload() {
1871
+ async reload(opts = {}) {
1809
1872
  if (this.executor) {
1810
1873
  if (!this.pinPaths) {
1811
1874
  try {
1812
1875
  const freshPaths = await detectOpenRestyPaths(this.executor);
1876
+ this.paths = freshPaths;
1813
1877
  this.sitesDir = freshPaths.sitesDir;
1814
1878
  this.nginxVersion = freshPaths.nginxVersion ?? this.nginxVersion;
1815
- this.reloadCommand = buildReloadCommand(freshPaths, {
1816
- containerEdge: this.containerEdge
1817
- });
1879
+ this.reloadCommand = buildReloadCommand(freshPaths);
1818
1880
  } catch {
1819
1881
  }
1820
1882
  }
1821
- await this.executor.exec(this.reloadCommand);
1883
+ if (this.containerEdge) {
1884
+ await this.executor.exec(this.reloadCommand, { timeout: 15e3 });
1885
+ } else {
1886
+ await reloadBareOpenResty(this.executor, this.paths, {
1887
+ allowStopped: opts.allowStopped ?? false
1888
+ });
1889
+ }
1822
1890
  return;
1823
1891
  }
1824
- await execFileAsync("sh", ["-lc", this.reloadCommand]);
1892
+ if (this.containerEdge) {
1893
+ await execFileAsync("sh", ["-lc", this.reloadCommand], { timeout: 15e3 });
1894
+ return;
1895
+ }
1896
+ const localExecutor = {
1897
+ exec: async (command, execOpts) => {
1898
+ const { stdout } = await execFileAsync("sh", ["-lc", command], {
1899
+ timeout: execOpts?.timeout
1900
+ });
1901
+ return String(stdout);
1902
+ }
1903
+ };
1904
+ await reloadBareOpenResty(localExecutor, this.paths, {
1905
+ allowStopped: opts.allowStopped ?? false
1906
+ });
1825
1907
  }
1826
1908
  async certsExist(domain) {
1827
1909
  const certPath = join(this.certDir, domain, "fullchain.pem");
@@ -2003,7 +2085,9 @@ ${geoEntries.join("\n")}
2003
2085
  if (!foundDesiredInclude) {
2004
2086
  const httpIndex = nextLines.findIndex((line) => /^\s*http\s*\{\s*$/.test(line));
2005
2087
  if (httpIndex === -1) {
2006
- throw new Error(`Failed to ensure rate-limit include: ${confPath} is missing an http block`);
2088
+ throw new Error(
2089
+ `Failed to ensure rate-limit include: ${confPath} is missing an http block`
2090
+ );
2007
2091
  }
2008
2092
  const indent = nextLines[httpIndex].match(/^\s*/)?.[0] ?? "";
2009
2093
  nextLines.splice(httpIndex + 1, 0, `${indent} ${desiredIncludeLine}`);