openship 0.5.0 → 0.6.0

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 (103) hide show
  1. package/README.md +26 -1
  2. package/dist/bare-FSUAM5V5.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-3FYIXVX5.js +8051 -0
  6. package/dist/{chunk-ILFETZOM.js → chunk-3LDWWY7U.js} +21 -1
  7. package/dist/{chunk-W73X43R7.js → chunk-3NQQXMDR.js} +3 -3
  8. package/dist/{chunk-OWED2GRM.js → chunk-42WQABXM.js} +2 -2
  9. package/dist/{chunk-TYVME3XD.js → chunk-6FH4AXVY.js} +20 -3
  10. package/dist/{chunk-ZCOQJCZO.js → chunk-6ODUBKUB.js} +3 -3
  11. package/dist/{chunk-DYPH4HCI.js → chunk-AO252K7Z.js} +1 -1
  12. package/dist/{chunk-K2RPVGPN.js → chunk-B3CE2OMR.js} +14 -12
  13. package/dist/{chunk-IXCHURVK.js → chunk-DOD2BXHQ.js} +4 -4
  14. package/dist/{chunk-VWPEKOH6.js → chunk-DRK3MEDS.js} +3 -3
  15. package/dist/{chunk-V7BZMGOT.js → chunk-EMXI4UCU.js} +5 -5
  16. package/dist/{chunk-NNPEG6HM.js → chunk-EP2TNALB.js} +2 -2
  17. package/dist/{chunk-NXFIRK2Z.js → chunk-GUQ3QCJ7.js} +573 -459
  18. package/dist/chunk-I3DPMTSF.js +29 -0
  19. package/dist/{chunk-KZOBYJUW.js → chunk-IW2UKBVF.js} +3 -3
  20. package/dist/{chunk-YWMODKWL.js → chunk-IXM5245F.js} +165 -13
  21. package/dist/{chunk-KNTJ7PSF.js → chunk-JIMUYEKN.js} +4 -4
  22. package/dist/chunk-OGY63DGD.js +705 -0
  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-JLKKYUZA.js → chunk-PSZSKEPA.js} +7 -7
  27. package/dist/{chunk-KI2EY3WB.js → chunk-PXKCZB2I.js} +1 -1
  28. package/dist/{chunk-RB35YN3J.js → chunk-QEH4QUBX.js} +391 -63
  29. package/dist/{chunk-6APIPIJY.js → chunk-QWHBRY2K.js} +5 -5
  30. package/dist/{chunk-6B5YCFFV.js → chunk-RVER2EOO.js} +5 -5
  31. package/dist/{chunk-Q2CIRAOK.js → chunk-RXQBD4ZY.js} +1 -1
  32. package/dist/{chunk-6LKHXB6T.js → chunk-TWJCLD4W.js} +48 -13
  33. package/dist/{chunk-I6PSTQIR.js → chunk-VDDMADOF.js} +3 -3
  34. package/dist/{chunk-BS2AXYZX.js → chunk-VE2YOK4I.js} +626 -214
  35. package/dist/{chunk-6PIYRG5I.js → chunk-VZ2ZHPS2.js} +52 -42
  36. package/dist/{chunk-AVIB7CT7.js → chunk-WNWQBB7F.js} +2 -2
  37. package/dist/{chunk-FRNAR33O.js → chunk-XZYFZ6A5.js} +3 -3
  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-E5M6VMCG.js +31 -0
  42. package/dist/{cloud-7QYOGF7F.js → cloud-KRTZ2IL5.js} +3 -3
  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-CQWHN2Q3.js +48 -0
  55. package/dist/ensure-container-edge-MJVXJQG5.js +32 -0
  56. package/dist/{event-streams-EHRAKL75.js → event-streams-TR4GAC53.js} +4 -4
  57. package/dist/{executor-LOYMPU67.js → executor-JELARVKC.js} +8 -8
  58. package/dist/index.js +1170 -459
  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-AXGNWRKZ.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-IE4P5YQX.js +81 -0
  65. package/dist/server/index.js +23172 -7994
  66. package/dist/server/lua/geo_country.lua +31 -6
  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_log.lua +10 -4
  70. package/dist/server/lua/site_logger.lua +220 -12
  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-FA4PW4NU.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/setup-KELALK7H.js +0 -20
@@ -1,25 +1,40 @@
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
- sq
7
- } from "./chunk-FPRHYBY2.js";
6
+ edgeDownExplanation,
7
+ explainEdgeDown,
8
+ isEdgeDownFailure
9
+ } from "./chunk-I3DPMTSF.js";
10
+ import {
11
+ edgeFailureReason
12
+ } from "./chunk-VZ2ZHPS2.js";
8
13
  import {
9
14
  ACME_CHALLENGE_LOCATION,
10
15
  ACME_HTTP01_PORT,
16
+ EDGE_CHALLENGE_DIR,
17
+ EDGE_CHALLENGE_LOCATION,
18
+ EDGE_CHALLENGE_URL_PREFIX,
11
19
  LUA_LOGGER_PATH,
12
20
  RULES_GUARD_PATH,
13
21
  buildReloadCommand,
14
22
  detectOpenRestyPaths,
23
+ edgeChallengeVhostConf,
15
24
  luaSourceAvailable
16
- } from "./chunk-EAAGSQXN.js";
25
+ } from "./chunk-OGY63DGD.js";
26
+ import {
27
+ logEntry,
28
+ sq
29
+ } from "./chunk-ZFJXPCQP.js";
17
30
  import {
18
- PROXY_GZIP_TYPES,
31
+ PROXY_DIRECTIVES,
32
+ parseProxyValue,
33
+ resolveProxyDirectives,
19
34
  resolveRedirectStatus,
20
35
  safeErrorMessage,
21
36
  sanitizeProxySettings
22
- } from "./chunk-UMQP7SYT.js";
37
+ } from "./chunk-OTLSJYAC.js";
23
38
 
24
39
  // ../../packages/adapters/src/infra/nginx.ts
25
40
  import {
@@ -34,7 +49,7 @@ import {
34
49
  import { execFile as cpExecFile } from "child_process";
35
50
  import { createHash, randomBytes } from "crypto";
36
51
  import { promisify } from "util";
37
- import { dirname, join } from "path";
52
+ import { posix as edgePath } from "path";
38
53
 
39
54
  // ../../packages/adapters/src/system/proxy/cert-material.ts
40
55
  import { X509Certificate, createPrivateKey } from "crypto";
@@ -43,6 +58,15 @@ import { X509Certificate, createPrivateKey } from "crypto";
43
58
  function containerCommand(container, command) {
44
59
  return `docker exec ${sq(container)} sh -c ${sq(command)}`;
45
60
  }
61
+ async function diagnoseEdgeDown(inner, container) {
62
+ const status = await inner.exec(`docker inspect -f '{{.State.Status}}' ${sq(container)} 2>/dev/null`).catch(() => null);
63
+ const logs = await inner.exec(`docker logs --tail 40 ${sq(container)} 2>&1`).catch(() => null);
64
+ return explainEdgeDown({
65
+ container,
66
+ status: status?.trim() || null,
67
+ reason: logs ? edgeFailureReason(logs) : null
68
+ });
69
+ }
46
70
  function resolveRef(ref) {
47
71
  return typeof ref === "function" ? ref() : ref;
48
72
  }
@@ -69,8 +93,34 @@ async function writeEdgeFileIn(inner, container, path, content) {
69
93
  function edgeContainerExecutor(inner, container, opts) {
70
94
  const inContainer = (command) => containerCommand(container, command);
71
95
  const overrides = {
72
- exec: (command, execOpts) => inner.exec(inContainer(command), execOpts),
73
- streamExec: (command, onLog) => inner.streamExec(inContainer(command), onLog),
96
+ // Both command paths enrich a "the container isn't up" failure with the
97
+ // container's name, state and `[emerg]` — see `diagnoseEdgeDown`. Gated on the
98
+ // narrow daemon/nginx signatures (`isEdgeDownFailure`), because a non-zero exit
99
+ // is ordinary here: the container-file probes report a missing path that way, and
100
+ // paying two `docker` round-trips per miss would be a real cost on every read.
101
+ exec: async (command, execOpts) => {
102
+ try {
103
+ return await inner.exec(inContainer(command), execOpts);
104
+ } catch (err) {
105
+ const message = err?.message ?? String(err);
106
+ if (!isEdgeDownFailure(message)) throw err;
107
+ throw new Error(`${await diagnoseEdgeDown(inner, container)}
108
+ ${message}`);
109
+ }
110
+ },
111
+ // streamExec REPORTS failure instead of throwing it, and its callers build the
112
+ // error they raise out of what was streamed (`_execCertbot` accumulates the log
113
+ // lines and throws that text). So the explanation has to go through `onLog` to
114
+ // reach them — appending it only to the return value would leave certbot's
115
+ // caller raising the daemon's bare container id, which is the original bug.
116
+ streamExec: async (command, onLog, opts2) => {
117
+ const result = await inner.streamExec(inContainer(command), onLog, opts2);
118
+ if (result.code === 0 || !isEdgeDownFailure(result.output)) return result;
119
+ const explanation = await diagnoseEdgeDown(inner, container);
120
+ onLog(logEntry(explanation, "error"));
121
+ return { code: result.code, output: `${explanation}
122
+ ${result.output}` };
123
+ },
74
124
  // A rename is a FILE op, so it follows the files, never the commands. Stated
75
125
  // here rather than left to the Proxy's passthrough: if the inner executor has no
76
126
  // `rename`, the fallback must still be the INNER shell (the host) — routing it
@@ -254,6 +304,7 @@ async function probeStaticOutput(executor, servedPath) {
254
304
  }
255
305
 
256
306
  // ../../packages/adapters/src/infra/nginx.ts
307
+ var { dirname, join } = edgePath;
257
308
  var PROXY_HEADERS = `proxy_set_header Host $host;
258
309
  proxy_set_header X-Real-IP $remote_addr;
259
310
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -301,27 +352,20 @@ function renderServerHeaders(route) {
301
352
  return lines.length > 0 ? `
302
353
  ${lines.join("\n")}` : "";
303
354
  }
304
- function renderProxyOptions(proxy) {
305
- const p = sanitizeProxySettings(proxy);
306
- if (!p) return "";
307
- const lines = [];
308
- if (p.clientMaxBodySize) lines.push(` client_max_body_size ${p.clientMaxBodySize};`);
309
- if (p.proxyReadTimeout) lines.push(` proxy_read_timeout ${p.proxyReadTimeout};`);
310
- if (p.proxySendTimeout) lines.push(` proxy_send_timeout ${p.proxySendTimeout};`);
311
- if (p.clientBodyTimeout) lines.push(` client_body_timeout ${p.clientBodyTimeout};`);
312
- if (p.proxyBuffering !== void 0) {
313
- lines.push(` proxy_buffering ${p.proxyBuffering ? "on" : "off"};`);
314
- }
315
- if (p.gzip !== void 0) {
316
- lines.push(` gzip ${p.gzip ? "on" : "off"};`);
317
- if (p.gzip) {
318
- lines.push(` gzip_types ${PROXY_GZIP_TYPES};`);
319
- lines.push(` gzip_min_length 1024;`);
320
- }
321
- }
355
+ function renderProxyDirectives(proxy, opts) {
356
+ const lines = resolveProxyDirectives(proxy, {
357
+ scope: opts.tlsOnly ? "tls" : "shared",
358
+ ...opts.nginxVersion !== void 0 ? { nginxVersion: opts.nginxVersion } : {}
359
+ }).map((d) => ` ${d.directive} ${d.text};`);
322
360
  return lines.length > 0 ? `
323
361
  ${lines.join("\n")}` : "";
324
362
  }
363
+ function renderProxyOptions(proxy, nginxVersion) {
364
+ return renderProxyDirectives(proxy, { tlsOnly: false, nginxVersion });
365
+ }
366
+ function renderProxyTlsOptions(proxy, nginxVersion) {
367
+ return renderProxyDirectives(proxy, { tlsOnly: true, nginxVersion });
368
+ }
325
369
  function renderGeoEntries(whitelist) {
326
370
  const entries = [
327
371
  " default 0;",
@@ -336,6 +380,46 @@ function renderGeoEntries(whitelist) {
336
380
  return entries;
337
381
  }
338
382
  var DEFAULT_CERT_DIR = "/etc/letsencrypt/live";
383
+ var LETSENCRYPT_PRODUCTION_DIRECTORY = "https://acme-v02.api.letsencrypt.org/directory";
384
+ function assertValidAcmeOptions(opts) {
385
+ if (!!opts.acmeEabKid !== !!opts.acmeEabHmacKey) {
386
+ throw new Error("ACME EAB requires both a key identifier and an HMAC key");
387
+ }
388
+ if (opts.acmeDirectoryUrl) {
389
+ let directory;
390
+ try {
391
+ directory = new URL(opts.acmeDirectoryUrl);
392
+ } catch {
393
+ throw new Error("ACME directory must be a valid http(s) URL");
394
+ }
395
+ if (directory.protocol !== "https:" && directory.protocol !== "http:") {
396
+ throw new Error("ACME directory must use http or https");
397
+ }
398
+ }
399
+ if (opts.acmeEabKid && (!/^[\x20-\x7E]+$/.test(opts.acmeEabKid) || opts.acmeEabKid.length > 512)) {
400
+ throw new Error("ACME EAB key identifier must be printable ASCII (maximum 512 characters)");
401
+ }
402
+ if (opts.acmeEabHmacKey && !/^[A-Za-z0-9_-]+={0,2}$/.test(opts.acmeEabHmacKey)) {
403
+ throw new Error("ACME EAB HMAC key must be base64url encoded");
404
+ }
405
+ if (opts.acmeCaBundle && !opts.acmeCaBundle.startsWith("/")) {
406
+ throw new Error("ACME CA bundle must be an absolute path in the Certbot environment");
407
+ }
408
+ }
409
+ function acmeKeyArgs(keyType) {
410
+ switch (keyType) {
411
+ case "ec256":
412
+ return ["--key-type", "ecdsa", "--elliptic-curve", "secp256r1"];
413
+ case "ec384":
414
+ return ["--key-type", "ecdsa", "--elliptic-curve", "secp384r1"];
415
+ case "rsa2048":
416
+ return ["--key-type", "rsa", "--rsa-key-size", "2048"];
417
+ case "rsa4096":
418
+ return ["--key-type", "rsa", "--rsa-key-size", "4096"];
419
+ default:
420
+ return [];
421
+ }
422
+ }
339
423
  var DOMAIN_RE = /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/;
340
424
  function assertValidDomain(domain) {
341
425
  if (!DOMAIN_RE.test(domain) || domain.length > 253) {
@@ -353,6 +437,12 @@ function assertValidUpstream(targetUrl) {
353
437
  throw new Error(`Invalid proxy target (must be http/https URL): ${targetUrl}`);
354
438
  }
355
439
  }
440
+ var CHALLENGE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._~-]{7,127}$/;
441
+ function assertValidChallengeToken(token) {
442
+ if (!CHALLENGE_TOKEN_RE.test(token) || token.includes("..")) {
443
+ throw new Error(`Invalid edge-target challenge token: ${token}`);
444
+ }
445
+ }
356
446
  var MANAGED_STATIC_BASE = "/opt/openship";
357
447
  function assertValidStaticRoot(root, opts) {
358
448
  assertNoNginxInjection(root, "static root");
@@ -369,50 +459,83 @@ function assertValidStaticRoot(root, opts) {
369
459
  }
370
460
  var execFileAsync = promisify(cpExecFile);
371
461
  function summarizeCertbotFailure(output, domain) {
462
+ const edgeDown = edgeDownExplanation(output);
463
+ if (edgeDown) return edgeDown;
464
+ const structured = certbotDiagnosis(output, domain);
465
+ if (structured) return structured;
466
+ const lines = (output || "").split("\n").map((l) => l.trim()).filter(Boolean);
467
+ return lines.slice(-3).join(" \xB7 ") || `certbot failed to issue a certificate for ${domain}`;
468
+ }
469
+ function certbotDiagnosis(output, domain) {
372
470
  const text = output || "";
373
471
  const pick = (re) => text.match(re)?.[0]?.replace(/\s+/g, " ").trim();
374
472
  const detail = pick(/Detail:\s*[^\n]+/i);
375
473
  const hint = pick(/Hint:\s*[^\n]+/i);
474
+ const challengeUrl = pick(/https?:\/\/\S*\.well-known\/acme-challenge\/\S*/i);
475
+ const redirectedToHttps = /^https:/i.test(challengeUrl ?? "");
376
476
  let diagnosis;
377
477
  if (/timeout during connect|connection refused|failed to connect|Timeout/i.test(text)) {
378
478
  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.`;
379
479
  } else if (/NXDOMAIN|no\s+(A|AAAA)\s+record|DNS problem|could not be resolved|no records? found/i.test(text)) {
380
480
  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.`;
381
- } else if (/\b52[56]\b|ssl handshake failed|cloudflare/i.test(text)) {
481
+ } else if (/\b52[56]\b|ssl handshake failed/i.test(text)) {
382
482
  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.`;
383
483
  } else if (/404|invalid response|unauthorized|"?status"?:?\s*40\d/i.test(text)) {
384
- diagnosis = `Port 80 answered but not with our ACME challenge \u2014 another web server is still serving :80, so the takeover didn't complete. Re-run and choose take-over / migrate.`;
484
+ diagnosis = redirectedToHttps ? `The challenge for ${domain} was redirected from HTTP to HTTPS and then answered with a 404 \u2014 so whatever served it isn't routing that hostname. If ${domain} is behind Cloudflare, its "Always Use HTTPS" is doing the redirect: 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. Otherwise check the A record points at this server and the domain is registered here. Nothing is occupying :80, so take-over / migrate will not help.` : `Port 80 answered but not with our ACME challenge \u2014 another web server is still serving :80, so the takeover didn't complete. Re-run and choose take-over / migrate.`;
385
485
  } else if (/too many certificates|rateLimited|rate limit/i.test(text)) {
386
486
  diagnosis = `Let's Encrypt rate limit reached for ${domain} \u2014 wait before retrying.`;
387
487
  }
388
488
  const parts = [diagnosis, detail, hint].filter(Boolean);
389
- if (parts.length > 0) return parts.join(" \u2014 ");
390
- const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
391
- return lines.slice(-3).join(" \xB7 ") || `certbot failed to issue a certificate for ${domain}`;
489
+ return parts.length > 0 ? parts.join(" \u2014 ") : void 0;
392
490
  }
393
491
  var NginxProvider = class {
394
492
  sitesDir;
395
493
  acmeEmail;
494
+ acmeDirectoryUrl;
495
+ acmeEabKid;
496
+ acmeEabHmacKey;
497
+ acmeKeyType;
498
+ acmeCaBundle;
499
+ acmeTosAgreed;
396
500
  certDir;
397
501
  executor;
398
502
  reloadCommand;
399
503
  pinPaths;
400
504
  containerEdge;
505
+ challengeDir;
506
+ /**
507
+ * nginx version of the edge, when `detectOpenRestyPaths` could read it. Gates the
508
+ * one directive whose syntax is version-dependent (`http2`); undefined = unknown,
509
+ * which `proxyDirectiveAllowed` treats as allow.
510
+ */
511
+ nginxVersion;
401
512
  constructor(opts) {
513
+ assertValidAcmeOptions(opts);
402
514
  this.sitesDir = opts.paths.sitesDir;
515
+ this.nginxVersion = opts.paths.nginxVersion;
403
516
  this.acmeEmail = opts.acmeEmail;
517
+ this.acmeDirectoryUrl = opts.acmeDirectoryUrl;
518
+ this.acmeEabKid = opts.acmeEabKid;
519
+ this.acmeEabHmacKey = opts.acmeEabHmacKey;
520
+ this.acmeKeyType = opts.acmeKeyType;
521
+ this.acmeCaBundle = opts.acmeCaBundle;
522
+ this.acmeTosAgreed = opts.acmeTosAgreed ?? true;
404
523
  this.certDir = opts.certDir ?? DEFAULT_CERT_DIR;
405
524
  this.executor = opts.executor ?? null;
406
525
  this.containerEdge = opts.containerEdge ?? false;
407
526
  this.reloadCommand = buildReloadCommand(opts.paths, { containerEdge: this.containerEdge });
408
527
  this.pinPaths = opts.pinPaths ?? false;
528
+ this.challengeDir = opts.challengeDir ?? EDGE_CHALLENGE_DIR;
409
529
  }
410
530
  // ── File operation helpers (dual-path: local or remote) ──────────────
411
- async _writeFile(path, content) {
531
+ async _writeFile(path, content, mode) {
412
532
  const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
413
533
  if (this.executor) {
414
534
  try {
415
535
  await this.executor.writeFile(tmpPath, content);
536
+ if (mode !== void 0) {
537
+ await this.executor.exec(`chmod ${sq(mode.toString(8))} ${sq(tmpPath)}`);
538
+ }
416
539
  if (this.executor.rename) {
417
540
  await this.executor.rename(tmpPath, path);
418
541
  } else {
@@ -425,7 +548,7 @@ var NginxProvider = class {
425
548
  } else {
426
549
  await fsMkdir(dirname(path), { recursive: true });
427
550
  try {
428
- await fsWriteFile(tmpPath, content, "utf-8");
551
+ await fsWriteFile(tmpPath, content, { encoding: "utf-8", mode });
429
552
  await fsRename(tmpPath, path);
430
553
  } catch (err) {
431
554
  await fsRm(tmpPath).catch(() => void 0);
@@ -511,14 +634,22 @@ var NginxProvider = class {
511
634
  * issued anything.
512
635
  *
513
636
  * Why a missing :443 block is a hard failure, not a soft one: an unmatched SNI
514
- * falls through to the edge's `ssl_reject_handshake` default (correct it stops
515
- * unrouted hosts being cross-served someone else's cert). But that means the
516
- * origin REFUSES the handshake for a domain we do route. Behind Cloudflare's
517
- * proxy every visitor sees error 525, and issuance deadlocks: Cloudflare answers
518
- * the HTTP-01 challenge with a 301 to https, then can't reach an origin that
519
- * refuses TLS, so the challenge fails and the vhost stays HTTP-only — forever
520
- * (#308). A placeholder breaks the loop: Cloudflare "Full" accepts it, the
521
- * challenge completes, and the real cert takes over.
637
+ * falls through to the edge's :443 catch-all, which answers with the "service not
638
+ * found" page (#431) under a placeholder certificate that belongs to no domain.
639
+ * For a domain we DO route that is a broken site: the visitor is told nothing is
640
+ * deployed here, and behind Cloudflare "Full" which accepts any origin cert —
641
+ * they get that page rather than the app. Issuance deadlocks with it: Cloudflare
642
+ * answers the HTTP-01 challenge with a 301 to https, the catch-all deliberately
643
+ * carries no ACME location on :443, so the challenge 404s and the vhost stays
644
+ * HTTP-only forever (#308). A per-domain placeholder breaks the loop: the
645
+ * domain gets its OWN name-bound :443 block, the challenge completes, and the
646
+ * real cert takes over.
647
+ *
648
+ * So the :443 catch-all having gained a certificate of its own does NOT make this
649
+ * redundant — it only changed the symptom from a raw TLS error (#431's old error
650
+ * 525) into a wrong page. The catch-all's cert is deliberately valid for nothing;
651
+ * serving it for a domain we route is the cross-serving that block exists to
652
+ * prevent.
522
653
  *
523
654
  * Deliberately NOT written under `certDir`: `certsExist()` / `readCertInfo()`
524
655
  * read that path, so a self-signed cert there would short-circuit provisionCert
@@ -625,19 +756,65 @@ var NginxProvider = class {
625
756
  * still sees the real cause.
626
757
  */
627
758
  async _execCertbot(args, onLog) {
759
+ const command = this.acmeCaBundle ? "env" : "certbot";
760
+ const commandArgs = this.acmeCaBundle ? [`REQUESTS_CA_BUNDLE=${this.acmeCaBundle}`, "certbot", ...args] : args;
628
761
  if (!onLog || !this.executor) {
629
- const out = await this._exec("certbot", args);
630
- if (out) onLog?.(out);
631
- return out;
762
+ let out;
763
+ try {
764
+ out = await this._exec(command, commandArgs);
765
+ } catch (err) {
766
+ throw new Error(this.redactAcmeSecrets(safeErrorMessage(err)));
767
+ }
768
+ const redacted = this.redactAcmeSecrets(out);
769
+ if (redacted) onLog?.(redacted);
770
+ return redacted;
632
771
  }
633
- const full = `certbot ${args.map(sq).join(" ")}`;
772
+ const full = `${command} ${commandArgs.map(sq).join(" ")}`;
634
773
  let output = "";
635
774
  const { code } = await this.executor.streamExec(full, (log) => {
636
775
  output += log.message;
637
- onLog(log.message);
776
+ onLog(this.redactAcmeSecrets(log.message));
638
777
  });
639
- if (code !== 0) throw new Error(output.trim() || `certbot exited ${code}`);
640
- return output;
778
+ if (code !== 0) {
779
+ throw new Error(this.redactAcmeSecrets(output).trim() || `certbot exited ${code}`);
780
+ }
781
+ return this.redactAcmeSecrets(output);
782
+ }
783
+ redactAcmeSecrets(value) {
784
+ return this.acmeEabHmacKey ? value.split(this.acmeEabHmacKey).join("[REDACTED]") : value;
785
+ }
786
+ /**
787
+ * Keep EAB HMAC material out of argv/process listings and remove it after use.
788
+ *
789
+ * Same layout contract as runtime/git-ssh-material.ts (the repo's other
790
+ * ephemeral-secret-on-disk site): the material lives in its OWN 0700 directory
791
+ * with the file at 0600, the bytes never travel through a shell command line,
792
+ * and cleanup removes the directory as a unit. The 0700 parent goes first, so
793
+ * even the temp file _writeFile stages is born unreadable to other users.
794
+ */
795
+ async createEphemeralEabConfig() {
796
+ if (!this.acmeEabKid || !this.acmeEabHmacKey) return null;
797
+ const dir = join(dirname(this.certDir), `.openship-eab-${randomBytes(12).toString("hex")}`);
798
+ const path = join(dir, "eab.ini");
799
+ if (this.executor) {
800
+ await this.executor.mkdir(dir);
801
+ await this._exec("chmod", ["700", dir]);
802
+ } else {
803
+ await fsMkdir(dir, { recursive: true, mode: 448 });
804
+ }
805
+ try {
806
+ await this._writeFile(
807
+ path,
808
+ `eab-kid = ${this.acmeEabKid}
809
+ eab-hmac-key = ${this.acmeEabHmacKey}
810
+ `,
811
+ 384
812
+ );
813
+ } catch (err) {
814
+ await this._rm(dir).catch(() => void 0);
815
+ throw err;
816
+ }
817
+ return path;
641
818
  }
642
819
  async _captureFile(path) {
643
820
  if (!await this._exists(path)) {
@@ -681,7 +858,8 @@ var NginxProvider = class {
681
858
  ${PROXY_HEADERS}`;
682
859
  const extraLocations = hostRedirect ? "" : `${renderRedirects(route)}${renderProxyLocations(route)}`;
683
860
  const serverHeaders = hostRedirect ? "" : renderServerHeaders(route);
684
- const proxyOpts = renderProxyOptions(route.proxy);
861
+ const proxyOpts = renderProxyOptions(route.proxy, this.nginxVersion);
862
+ const proxyTlsOpts = renderProxyTlsOptions(route.proxy, this.nginxVersion);
685
863
  const webhookLocation = route.webhookProxy && !hostRedirect ? `
686
864
  location /_openship/hooks/ {
687
865
  proxy_pass ${route.webhookProxy};
@@ -716,6 +894,8 @@ ${FORWARD_VARS}
716
894
  ${luaProxy}
717
895
 
718
896
  ${ACME_CHALLENGE_LOCATION}
897
+
898
+ ${EDGE_CHALLENGE_LOCATION}
719
899
  ${sharedBody}
720
900
  ${serveLocation}
721
901
  }
@@ -735,6 +915,8 @@ ${FORWARD_VARS}
735
915
  ${luaProxy}
736
916
 
737
917
  ${ACME_CHALLENGE_LOCATION}
918
+
919
+ ${EDGE_CHALLENGE_LOCATION}
738
920
  ${sharedBody}
739
921
  ${redirectOrServeLocation}
740
922
  }
@@ -748,7 +930,7 @@ ${FORWARD_VARS}
748
930
  ${luaProxy}
749
931
 
750
932
  ssl_certificate ${certPath};
751
- ssl_certificate_key ${keyPath};
933
+ ssl_certificate_key ${keyPath};${proxyTlsOpts}
752
934
  ${sharedBody}
753
935
  ${serveLocation}
754
936
  }
@@ -769,6 +951,8 @@ ${FORWARD_VARS}
769
951
  ${luaProxy}
770
952
 
771
953
  ${ACME_CHALLENGE_LOCATION}
954
+
955
+ ${EDGE_CHALLENGE_LOCATION}
772
956
  ${sharedBody}
773
957
  ${serveLocation}
774
958
  }
@@ -782,7 +966,7 @@ ${FORWARD_VARS}
782
966
  ${luaProxy}
783
967
 
784
968
  ssl_certificate ${bootstrap.certPath};
785
- ssl_certificate_key ${bootstrap.keyPath};
969
+ ssl_certificate_key ${bootstrap.keyPath};${proxyTlsOpts}
786
970
  ${sharedBody}
787
971
  ${serveLocation}
788
972
  }
@@ -807,6 +991,24 @@ ${serveLocation}
807
991
  routeStatePath(slug) {
808
992
  return join(this.sitesDir, `${slug}.route.json`);
809
993
  }
994
+ /**
995
+ * Recover the proxy tunables from a vhost we already wrote, for the legacy
996
+ * no-sidecar re-register below. Rewriting the conf without them would silently
997
+ * revert a site to nginx's 1 MB / 60 s defaults as a side effect of issuing a
998
+ * certificate — the one moment an operator is least likely to look for it.
999
+ *
1000
+ * Same table as the renderer, read backwards, so the two cannot drift.
1001
+ */
1002
+ scrapeProxySettings(conf) {
1003
+ const found = {};
1004
+ for (const spec of PROXY_DIRECTIVES) {
1005
+ const m = conf.match(new RegExp(`^\\s*${spec.directive}\\s+([^;\\n]+);`, "m"));
1006
+ if (!m?.[1]) continue;
1007
+ const parsed = parseProxyValue(spec, m[1].trim());
1008
+ if (parsed !== void 0) found[spec.key] = parsed;
1009
+ }
1010
+ return sanitizeProxySettings(found);
1011
+ }
810
1012
  /**
811
1013
  * Remove a route by deleting its conf + route-state files, then reload.
812
1014
  *
@@ -836,6 +1038,76 @@ ${serveLocation}
836
1038
  }
837
1039
  await this._rm(this.bootstrapCertDir(domain)).catch(() => void 0);
838
1040
  }
1041
+ // ── Edge-target challenge ────────────────────────────────────────────
1042
+ /** Filename for a host's challenge vhost. The leading `_` is a namespace
1043
+ * `resolveSlug` can never produce (`domainSlug` emits only `[A-Za-z0-9-]`), so no
1044
+ * `registerRoute`/`removeRoute` for any domain can overwrite or delete it —
1045
+ * matching the existing `_default.conf` / `_management.conf` convention. */
1046
+ challengeVhostPath(host) {
1047
+ return join(this.sitesDir, `_oblien-challenge-${this.domainSlug(host)}.conf`);
1048
+ }
1049
+ /**
1050
+ * Make `host` able to answer edge-target challenges, and serve any tokens given.
1051
+ *
1052
+ * Called TWO ways, and the split is the point:
1053
+ * - `{ host }` alone, from edge-ensure (install + every deploy) — writes only the
1054
+ * vhost, so the box can answer before any verification exists.
1055
+ * - `{ host, tokens }`, from the verify flow — writes the token files too.
1056
+ *
1057
+ * That the first form is possible at all is what makes this ready from first
1058
+ * install rather than only once a challenge is in flight: the location serves a
1059
+ * DIRECTORY, so it is valid with nothing in it (a missing token is a 404, which is
1060
+ * the right answer). And because the vhost lands in the bind-mounted sites-enabled
1061
+ * written by our own code, it works on ANY edge image — including one predating
1062
+ * this feature, and including a rollback. The baked image's catch-all cannot make
1063
+ * that claim, which is why it is not the mechanism.
1064
+ *
1065
+ * Idempotent: skips the vhost write AND the reload when what is on disk already
1066
+ * matches, so re-asserting on every deploy costs a couple of stats.
1067
+ *
1068
+ * `tokens` is a list because a re-issued verification resets the token while an
1069
+ * older record may still be probed, and because two orgs can target one box. All
1070
+ * are served; none is ever removed — Oblien re-probes the SAME token near its
1071
+ * 90-day expiry, so dropping one silently kills that route ~83 days later.
1072
+ */
1073
+ async serveEdgeChallenge(input) {
1074
+ const host = input.host.trim().toLowerCase().replace(/\.$/, "");
1075
+ assertValidDomain(host);
1076
+ const tokens = input.tokens ?? [];
1077
+ for (const token of tokens) assertValidChallengeToken(token);
1078
+ for (const token of tokens) {
1079
+ await this._writeFile(join(this.challengeDir, token), token);
1080
+ }
1081
+ const base = this.domainSlug(host);
1082
+ for (const stem of [base, `${base}-${this.slugSuffix(host)}`]) {
1083
+ const path2 = join(this.sitesDir, `${stem}.conf`);
1084
+ const conf = await this._readFile(path2).catch(() => "");
1085
+ if (!conf || !this.serverNamesIn(conf).includes(host)) continue;
1086
+ if (conf.includes(`location ${EDGE_CHALLENGE_URL_PREFIX}`)) {
1087
+ return { served: true, via: "existing-vhost" };
1088
+ }
1089
+ return {
1090
+ served: false,
1091
+ via: null,
1092
+ claimedBy: path2,
1093
+ reason: `${host} is already served by ${path2}, which predates edge-target challenge support. Re-register that domain (a deploy or a Domains-tab save) to refresh its vhost, then retry.`
1094
+ };
1095
+ }
1096
+ const vhost = edgeChallengeVhostConf(host);
1097
+ const path = this.challengeVhostPath(host);
1098
+ const existing = await this._readFile(path).catch(() => "");
1099
+ if (existing === vhost) return { served: true, via: "challenge-vhost" };
1100
+ const snapshot = await this._captureFile(path);
1101
+ await this._writeFile(path, vhost);
1102
+ try {
1103
+ await this.reload();
1104
+ } catch (err) {
1105
+ await this._restoreFile(path, snapshot);
1106
+ await this.reload().catch(() => void 0);
1107
+ throw err;
1108
+ }
1109
+ return { served: true, via: "challenge-vhost" };
1110
+ }
839
1111
  // ── SSL ──────────────────────────────────────────────────────────────
840
1112
  /**
841
1113
  * Provision a TLS certificate using certbot.
@@ -861,8 +1133,10 @@ ${serveLocation}
861
1133
  const validEmail = !!this.acmeEmail && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.acmeEmail);
862
1134
  const emailArgs = validEmail ? ["--email", this.acmeEmail] : ["--register-unsafely-without-email"];
863
1135
  let certonlyOut = "";
1136
+ const eabConfig = await this.createEphemeralEabConfig();
864
1137
  try {
865
1138
  certonlyOut = await this._execCertbot([
1139
+ ...eabConfig ? ["--config", eabConfig] : [],
866
1140
  "certonly",
867
1141
  "--standalone",
868
1142
  "--http-01-port",
@@ -871,15 +1145,19 @@ ${serveLocation}
871
1145
  domain,
872
1146
  "-d",
873
1147
  domain,
1148
+ ...this.acmeDirectoryUrl ? ["--server", this.acmeDirectoryUrl] : [],
1149
+ ...acmeKeyArgs(this.acmeKeyType),
874
1150
  ...emailArgs,
875
- "--agree-tos",
1151
+ ...this.acmeTosAgreed ? ["--agree-tos"] : [],
876
1152
  "--non-interactive",
877
1153
  // Forced reissue: certbot would otherwise print "not due for renewal"
878
1154
  // (exit 0) when a lineage exists, leaving the stale cert in place.
879
1155
  ...opts?.force ? ["--force-renewal"] : []
880
1156
  ], opts?.onLog);
881
1157
  } catch (err) {
882
- throw new Error(summarizeCertbotFailure(safeErrorMessage(err), domain));
1158
+ throw new Error(summarizeCertbotFailure(this.redactAcmeSecrets(safeErrorMessage(err)), domain));
1159
+ } finally {
1160
+ if (eabConfig) await this._rm(dirname(eabConfig)).catch(() => void 0);
883
1161
  }
884
1162
  const slug = await this.resolveSlug(domain);
885
1163
  const configPath = join(this.sitesDir, `${slug}.conf`);
@@ -892,9 +1170,15 @@ ${serveLocation}
892
1170
  }
893
1171
  try {
894
1172
  const existing = await this._readFile(configPath);
1173
+ const scraped = this.scrapeProxySettings(existing);
895
1174
  const targetMatch = existing.match(/proxy_pass\s+([^;]+);/);
896
1175
  if (targetMatch) {
897
- await this.registerRoute({ domain, targetUrl: targetMatch[1], tls: true });
1176
+ await this.registerRoute({
1177
+ domain,
1178
+ targetUrl: targetMatch[1],
1179
+ tls: true,
1180
+ ...scraped ? { proxy: scraped } : {}
1181
+ });
898
1182
  return this.ensureIssued(domain, certonlyOut);
899
1183
  }
900
1184
  const rootMatch = existing.match(/root\s+([^;]+);/);
@@ -903,7 +1187,8 @@ ${serveLocation}
903
1187
  domain,
904
1188
  staticRoot: rootMatch[1],
905
1189
  staticRootAdopted: true,
906
- tls: true
1190
+ tls: true,
1191
+ ...scraped ? { proxy: scraped } : {}
907
1192
  });
908
1193
  return this.ensureIssued(domain, certonlyOut);
909
1194
  }
@@ -915,17 +1200,25 @@ ${serveLocation}
915
1200
  * After a certbot run that DIDN'T throw, confirm a usable cert actually landed
916
1201
  * at the expected path. certbot can exit 0 while producing nothing readable
917
1202
  * there (a lineage written elsewhere, "not due for renewal", 0 domains
918
- * authenticated) — a silent `{missing}` hides why. Surface certbot's real
919
- * output so the operator sees the actual reason instead of a generic message.
1203
+ * authenticated) — a silent `{missing}` hides why.
1204
+ *
1205
+ * The exit code plus this read of `live/<domain>` is the WHOLE verdict. certbot's
1206
+ * stdout is evidence for the operator, never the test: a zero-exit run ends with
1207
+ * the donation footer, and running the failure summarizer over it reported
1208
+ * "Donating to ISRG / Let's Encrypt: …" as the cause of a cert that had in fact
1209
+ * been issued. Only a diagnosis that matches a KNOWN failure shape is quoted.
920
1210
  */
921
1211
  async ensureIssued(domain, certbotOutput) {
922
1212
  const result = await this.readCertInfo(domain);
923
1213
  if (result.verified) return result;
1214
+ const dir = join(this.certDir, domain);
1215
+ const onDisk = result.reason === "read_error" ? `a certificate is at ${dir} but couldn't be read (permissions, or a partial write)` : result.reason === "invalid" ? `the certificate at ${dir} isn't usable for ${domain} (expired, wrong hostname, or the key doesn't match)` : `no certificate is at ${dir}`;
1216
+ const diagnosis = certbotDiagnosis(certbotOutput, domain);
924
1217
  const tail = certbotOutput.trim().slice(-1200);
925
1218
  throw new Error(
926
- summarizeCertbotFailure(certbotOutput, domain) + (tail ? `
1219
+ `certbot exited without an error but ${onDisk}.` + (diagnosis ? ` ${diagnosis}` : "") + (tail ? `
927
1220
 
928
- certbot exited without an error but no readable certificate is at ${join(this.certDir, domain)}. certbot said:
1221
+ certbot said:
929
1222
  ${tail}` : "")
930
1223
  );
931
1224
  }
@@ -940,7 +1233,16 @@ ${tail}` : "")
940
1233
  if (!await this.hasCertbotLineage(domain)) {
941
1234
  return this.provisionCert(domain, { force: true });
942
1235
  }
943
- await this._exec("certbot", ["renew", "--cert-name", domain, "--non-interactive"]);
1236
+ if (!await this.lineageServerMatches(domain)) {
1237
+ return this.provisionCert(domain, { force: true });
1238
+ }
1239
+ await this._execCertbot([
1240
+ "renew",
1241
+ "--cert-name",
1242
+ domain,
1243
+ ...acmeKeyArgs(this.acmeKeyType),
1244
+ "--non-interactive"
1245
+ ]);
944
1246
  await this.reload();
945
1247
  return this.readCertInfo(domain);
946
1248
  }
@@ -950,9 +1252,31 @@ ${tail}` : "")
950
1252
  * reads on every `renew`, so its presence is the authoritative answer — the cert
951
1253
  * FILES existing is not (they can be adopted, or copied in by hand).
952
1254
  */
1255
+ renewalConfPath(domain) {
1256
+ return join(dirname(this.certDir), "renewal", `${domain}.conf`);
1257
+ }
953
1258
  async hasCertbotLineage(domain) {
954
- const renewalPath = join(dirname(this.certDir), "renewal", `${domain}.conf`);
955
- return this._exists(renewalPath);
1259
+ return this._exists(this.renewalConfPath(domain));
1260
+ }
1261
+ /**
1262
+ * Was this lineage issued by the CURRENTLY configured ACME directory? The
1263
+ * renewal conf records the issuing `server`, and `certbot renew` only holds an
1264
+ * account for THAT server — after the operator points at a different CA, renewing
1265
+ * a pre-switch lineage would need an account registration `renew` cannot perform
1266
+ * (and EAB material it is never given). An unreadable/unparsable conf answers
1267
+ * `true` so the plain renew still runs and surfaces certbot's own error.
1268
+ */
1269
+ async lineageServerMatches(domain) {
1270
+ let conf;
1271
+ try {
1272
+ conf = await this._readFile(this.renewalConfPath(domain));
1273
+ } catch {
1274
+ return true;
1275
+ }
1276
+ const recorded = conf.match(/^\s*server\s*=\s*(\S+)\s*$/m)?.[1];
1277
+ if (!recorded) return true;
1278
+ const effective = this.acmeDirectoryUrl ?? LETSENCRYPT_PRODUCTION_DIRECTORY;
1279
+ return recorded === effective;
956
1280
  }
957
1281
  /**
958
1282
  * Install an operator-supplied or adopted certificate (no ACME). Validates the
@@ -1082,6 +1406,7 @@ ${tail}` : "")
1082
1406
  try {
1083
1407
  const freshPaths = await detectOpenRestyPaths(this.executor);
1084
1408
  this.sitesDir = freshPaths.sitesDir;
1409
+ this.nginxVersion = freshPaths.nginxVersion ?? this.nginxVersion;
1085
1410
  this.reloadCommand = buildReloadCommand(freshPaths, {
1086
1411
  containerEdge: this.containerEdge
1087
1412
  });
@@ -1295,7 +1620,10 @@ export {
1295
1620
  isSafeCertPath,
1296
1621
  readDeclaredPair,
1297
1622
  renderProxyOptions,
1623
+ renderProxyTlsOptions,
1298
1624
  renderGeoEntries,
1625
+ acmeKeyArgs,
1626
+ assertValidChallengeToken,
1299
1627
  MANAGED_STATIC_BASE,
1300
1628
  summarizeCertbotFailure,
1301
1629
  NginxProvider