openship 0.6.7 → 0.6.9

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-VQM6JHJB.js → chunk-5UXHT6ED.js} +9 -9
  5. package/dist/{chunk-PPEBIY5F.js → chunk-634DDFT5.js} +35 -12
  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-DDFUESPJ.js → chunk-FCGJN5Q5.js} +9 -9
  15. package/dist/{chunk-BLEXTYRK.js → chunk-GNOMEMNI.js} +1 -1
  16. package/dist/{chunk-NRTJRNAE.js → chunk-IAMOUDHY.js} +138 -54
  17. package/dist/{chunk-CIJV43LH.js → chunk-MUWVC5R4.js} +161 -39
  18. package/dist/{chunk-ZPJ74LXE.js → chunk-NIYD7MD2.js} +197 -45
  19. package/dist/{chunk-FZ6QCF6B.js → chunk-NPSRGIHV.js} +2 -2
  20. package/dist/{chunk-2GWYYHOR.js → chunk-NRU2EBYZ.js} +2 -2
  21. package/dist/{chunk-J2XM3QSP.js → chunk-NUT6HKK6.js} +5 -5
  22. package/dist/{chunk-H3WT3A6R.js → chunk-P7RCYELG.js} +1 -1
  23. package/dist/{chunk-FYPFEDPZ.js → chunk-R5DC5PEF.js} +1 -1
  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-OI2TRHSG.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 +31801 -25560
  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-RC5ZQIAD.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
@@ -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}`);
@@ -5,16 +5,17 @@ const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  checkGit,
7
7
  installGit
8
- } from "./chunk-ZPJ74LXE.js";
8
+ } from "./chunk-NIYD7MD2.js";
9
9
  import {
10
- createDockerBuildContext
11
- } from "./chunk-XWHWTHEL.js";
10
+ createDockerBuildContext,
11
+ resolveDockerBuildArgs
12
+ } from "./chunk-6GU6JSK6.js";
12
13
  import {
13
14
  prepareStackOutput,
14
15
  resolveProjectDir,
15
16
  resolveStaticOutputPath,
16
17
  runLocalBuild
17
- } from "./chunk-MEE6OZFP.js";
18
+ } from "./chunk-5DJOB7IX.js";
18
19
  import {
19
20
  BuildLogger,
20
21
  dockerBuildContextDirectory,
@@ -28,7 +29,7 @@ import {
28
29
  splitRuntimeEnv,
29
30
  sq,
30
31
  transferLocalDirectory
31
- } from "./chunk-H4FS5BED.js";
32
+ } from "./chunk-AHJYP7DG.js";
32
33
  import {
33
34
  isRuntimeNotFoundError
34
35
  } from "./chunk-N5YZMO33.js";
@@ -43,7 +44,7 @@ import {
43
44
  packageManagerEnsureCommand,
44
45
  safeErrorMessage,
45
46
  shellSplitWords
46
- } from "./chunk-2VSW6Q6C.js";
47
+ } from "./chunk-WEZFTL4V.js";
47
48
 
48
49
  // ../../packages/adapters/src/runtime/cloud.ts
49
50
  import { readFile } from "fs/promises";
@@ -1332,6 +1333,11 @@ function resolveCopyDestinationPath(copy, sourcePath, destination, destinationIs
1332
1333
  function cloudBuildContextPath(config) {
1333
1334
  return dockerBuildContextDirectory(config) || normalizeDockerRelativePath(config.rootDirectory);
1334
1335
  }
1336
+ function compileCloudDockerfilePlan(source, config) {
1337
+ return compileDockerfileToWorkspacePlan(source, {
1338
+ buildArgs: resolveDockerBuildArgs(config)
1339
+ });
1340
+ }
1335
1341
  var PAGE_CONTAINER_PREFIX = "page:";
1336
1342
  async function provisionCloudWorkspace(client, config, logger) {
1337
1343
  logger?.log(`Creating workspace from image "${config.image}"...
@@ -1350,15 +1356,22 @@ async function provisionCloudWorkspace(client, config, logger) {
1350
1356
  }
1351
1357
  });
1352
1358
  } catch (err) {
1353
- logger?.log(`Failed to create workspace from image "${config.image}": ${formatCloudError(err)}
1354
- `, "error");
1359
+ logger?.log(
1360
+ `Failed to create workspace from image "${config.image}": ${formatCloudError(err)}
1361
+ `,
1362
+ "error"
1363
+ );
1355
1364
  throw err;
1356
1365
  }
1357
1366
  const ws = client.workspace(wsData.id);
1358
1367
  try {
1359
1368
  if (config.mode === "temporary" && config.ttl) {
1360
1369
  try {
1361
- await ws.lifecycle.makeTemporary({ ttl: config.ttl, ttl_action: "remove", remove_on_exit: true });
1370
+ await ws.lifecycle.makeTemporary({
1371
+ ttl: config.ttl,
1372
+ ttl_action: "remove",
1373
+ remove_on_exit: true
1374
+ });
1362
1375
  } catch {
1363
1376
  }
1364
1377
  }
@@ -1398,6 +1411,7 @@ var CloudRuntime = class {
1398
1411
  name = "cloud";
1399
1412
  capabilities = /* @__PURE__ */ new Set([
1400
1413
  "build",
1414
+ "prebuiltImage",
1401
1415
  "deploy",
1402
1416
  "multiServiceDeploy",
1403
1417
  "stop",
@@ -1549,6 +1563,94 @@ var CloudRuntime = class {
1549
1563
  this.activeBuilds.get(sessionId)?.workspaceIds.delete(workspaceId);
1550
1564
  }
1551
1565
  // ── Build lifecycle ────────────────────────────────────────────────────
1566
+ /**
1567
+ * Materialize a prebuilt container image as the same opaque artifact Cloud's
1568
+ * deploy path already understands: a temporary workspace id.
1569
+ *
1570
+ * Unlike a source build, no command is executed in the workspace. Creating it
1571
+ * directly from the application image preserves the image's own ENTRYPOINT,
1572
+ * CMD and WORKDIR; deploy keeps those defaults when `prebuiltImage` is set.
1573
+ */
1574
+ async prepareImage(config, logger) {
1575
+ const log = logger ?? new BuildLogger();
1576
+ const startedAt = Date.now();
1577
+ const activeBuild = this.createActiveBuild(config.sessionId);
1578
+ let workspaceId;
1579
+ const discardWorkspace = async () => {
1580
+ if (!workspaceId) return;
1581
+ await this.ws(workspaceId).delete().catch(() => {
1582
+ });
1583
+ this.builtArtifacts.delete(workspaceId);
1584
+ this.untrackActiveBuildWorkspace(config.sessionId, workspaceId);
1585
+ };
1586
+ const cancelled = async () => {
1587
+ await discardWorkspace();
1588
+ log.step("build", "failed", "Image preparation cancelled");
1589
+ return {
1590
+ sessionId: config.sessionId,
1591
+ status: "cancelled",
1592
+ // Keep the ref even after our best-effort delete: the pipeline can make
1593
+ // an idempotent cleanup retry if that delete failed mid-cancellation.
1594
+ imageRef: workspaceId,
1595
+ durationMs: Date.now() - startedAt,
1596
+ artifactOwned: true
1597
+ };
1598
+ };
1599
+ try {
1600
+ if (activeBuild.abort.signal.aborted) return await cancelled();
1601
+ const imageRef = config.imageRef.trim();
1602
+ if (!imageRef) throw new Error("Prebuilt image reference is required");
1603
+ log.step("build", "running", `Creating workspace from prebuilt image ${imageRef}`);
1604
+ const provisioned = await this.provisionWorkspace(
1605
+ {
1606
+ name: config.slug ?? `image-${config.projectId.slice(0, 20)}`,
1607
+ image: imageRef,
1608
+ mode: "temporary",
1609
+ resources: config.resources,
1610
+ env: config.envVars,
1611
+ ttl: "15m"
1612
+ },
1613
+ log
1614
+ );
1615
+ workspaceId = provisioned.workspaceId;
1616
+ this.trackActiveBuildWorkspace(config.sessionId, workspaceId);
1617
+ if (activeBuild.abort.signal.aborted) return await cancelled();
1618
+ this.builtArtifacts.set(workspaceId, {
1619
+ workspaceId,
1620
+ runtime: {
1621
+ workdir: "/",
1622
+ env: {},
1623
+ exposedPorts: [],
1624
+ baseImage: imageRef
1625
+ }
1626
+ });
1627
+ log.step("build", "completed", `Prebuilt image workspace ${workspaceId} is ready`);
1628
+ return {
1629
+ sessionId: config.sessionId,
1630
+ status: "deploying",
1631
+ imageRef: workspaceId,
1632
+ durationMs: Date.now() - startedAt,
1633
+ artifactOwned: true
1634
+ };
1635
+ } catch (err) {
1636
+ if (activeBuild.abort.signal.aborted) return await cancelled();
1637
+ await discardWorkspace();
1638
+ const message = formatCloudError(err);
1639
+ log.step("build", "failed", `Image preparation failed: ${message}`);
1640
+ return {
1641
+ sessionId: config.sessionId,
1642
+ status: "failed",
1643
+ // Same retry contract as cancellation. Undefined means provisioning
1644
+ // failed before Oblien handed us an owned workspace.
1645
+ imageRef: workspaceId,
1646
+ durationMs: Date.now() - startedAt,
1647
+ errorMessage: `Image preparation failed: ${message}`,
1648
+ artifactOwned: true
1649
+ };
1650
+ } finally {
1651
+ this.activeBuilds.delete(config.sessionId);
1652
+ }
1653
+ }
1552
1654
  async build(config, logger) {
1553
1655
  const log = logger ?? new BuildLogger();
1554
1656
  const activeBuild = this.createActiveBuild(config.sessionId);
@@ -1607,7 +1709,9 @@ var CloudRuntime = class {
1607
1709
  resolveProjectDir(buildDir, config.rootDirectory)
1608
1710
  );
1609
1711
  if (selfContained) {
1610
- log.log("Detected self-contained build output \u2014 uploading the bundle (no install on cloud).\n");
1712
+ log.log(
1713
+ "Detected self-contained build output \u2014 uploading the bundle (no install on cloud).\n"
1714
+ );
1611
1715
  await transferLocalDirectory(
1612
1716
  selfContained.bundleDir,
1613
1717
  { kind: "cloud-runtime", runtime: rt, path: "/app" },
@@ -1645,7 +1749,11 @@ var CloudRuntime = class {
1645
1749
  log.log("Installing production dependencies on cloud...\n");
1646
1750
  const pmEnsure = packageManagerEnsureCommand(config.packageManager);
1647
1751
  const fullInstall = pmEnsure ? `${pmEnsure} && ${installCmd}` : installCmd;
1648
- await this.execAndStream(rt, ["sh", "-c", `cd /app && ${fullInstall}`], log.callback);
1752
+ await this.execAndStream(
1753
+ rt,
1754
+ ["sh", "-c", `cd /app && ${fullInstall}`],
1755
+ log.callback
1756
+ );
1649
1757
  }
1650
1758
  }
1651
1759
  });
@@ -1730,7 +1838,7 @@ var CloudRuntime = class {
1730
1838
  try {
1731
1839
  log.log("Build strategy: Dockerfile plan (build in Oblien workspaces)\n");
1732
1840
  source = await this.resolveDockerfileBuildSource(config, log);
1733
- const plan = compileDockerfileToWorkspacePlan(source.dockerfile);
1841
+ const plan = compileCloudDockerfilePlan(source.dockerfile, config);
1734
1842
  const blocking = plan.diagnostics.filter(
1735
1843
  (item) => item.severity === "error" || item.severity === "unsupported"
1736
1844
  );
@@ -1819,10 +1927,7 @@ var CloudRuntime = class {
1819
1927
  requireRepositoryDockerfile: true,
1820
1928
  onLog: logger.callback
1821
1929
  });
1822
- const dockerfilePath = resolveWithinDirectory(
1823
- context.buildContextDir,
1824
- context.dockerfileName
1825
- );
1930
+ const dockerfilePath = resolveWithinDirectory(context.buildContextDir, context.dockerfileName);
1826
1931
  const dockerfile = await readFile(dockerfilePath, "utf-8");
1827
1932
  const contextRoot = context.contextSubdir ? context.buildContextDir : resolveWithinDirectory(context.contextDir, context.rootDirectory);
1828
1933
  return {
@@ -1885,11 +1990,9 @@ var CloudRuntime = class {
1885
1990
  logger.log(`Checking Dockerfile candidate: ${candidate}
1886
1991
  `);
1887
1992
  const dockerfile = await executor.exec(
1888
- [
1889
- `cd ${sq(sourceDir)}`,
1890
- `test -f ${sq(candidate)}`,
1891
- `cat ${sq(candidate)}`
1892
- ].join(" && "),
1993
+ [`cd ${sq(sourceDir)}`, `test -f ${sq(candidate)}`, `cat ${sq(candidate)}`].join(
1994
+ " && "
1995
+ ),
1893
1996
  { timeout: 3e4 }
1894
1997
  ).catch(() => null);
1895
1998
  if (dockerfile !== null) {
@@ -2119,8 +2222,10 @@ var CloudRuntime = class {
2119
2222
  ...prepareContextCommands,
2120
2223
  'echo "Dockerfile context prepared."'
2121
2224
  ].join("\n");
2122
- logger.log(`Preparing Dockerfile build workspace for repository clone (branch: ${config.branch})...
2123
- `);
2225
+ logger.log(
2226
+ `Preparing Dockerfile build workspace for repository clone (branch: ${config.branch})...
2227
+ `
2228
+ );
2124
2229
  await this.ensureWorkspaceGit(targetRuntime, logger, "Dockerfile build workspace");
2125
2230
  logger.log(`Cloning Dockerfile context in build workspace (branch: ${config.branch})...
2126
2231
  `);
@@ -2373,8 +2478,8 @@ var CloudRuntime = class {
2373
2478
  }
2374
2479
  }
2375
2480
  const builtArtifact = this.builtArtifacts.get(workspaceId);
2376
- const prodPaths = config.productionPaths;
2377
- const workDir = builtArtifact?.runtime.workdir ?? (prodPaths?.length ? "/app/production" : "/app");
2481
+ const prodPaths = config.prebuiltImage ? void 0 : config.productionPaths;
2482
+ const workDir = config.prebuiltImage ? "/" : builtArtifact?.runtime.workdir ?? (prodPaths?.length ? "/app/production" : "/app");
2378
2483
  if (prodPaths?.length) {
2379
2484
  try {
2380
2485
  const rt = await ws.runtime();
@@ -2421,7 +2526,7 @@ fi`;
2421
2526
  );
2422
2527
  }
2423
2528
  }
2424
- const startCommand = builtArtifact?.runtime.startCommand || config.startCommand || "npm start";
2529
+ const startCommand = builtArtifact?.runtime.startCommand || config.startCommand || (config.prebuiltImage ? void 0 : "npm start");
2425
2530
  const projectEnv = splitRuntimeEnv(config.envVars);
2426
2531
  if (projectEnv.dropped.length > 0) {
2427
2532
  onLog?.({
@@ -2438,19 +2543,31 @@ fi`;
2438
2543
  const workloadCmd = resolveCloudWorkloadCmd({
2439
2544
  startCommand,
2440
2545
  workdir: workDir,
2441
- binPathExport: builtArtifact ? "" : nodeBinPathExport(config.packageManager, [workDir, "/app"])
2546
+ binPathExport: builtArtifact || config.prebuiltImage ? "" : nodeBinPathExport(config.packageManager, [workDir, "/app"])
2442
2547
  });
2443
- try {
2444
- await ws.workloads.create({
2445
- name: "app",
2446
- cmd: workloadCmd,
2447
- working_dir: workDir,
2448
- env: [...envArray, `PORT=${config.port}`],
2449
- restart_policy: restartPolicy,
2450
- max_restarts: 10
2548
+ if (workloadCmd) {
2549
+ try {
2550
+ if (config.prebuiltImage) {
2551
+ await ws.workloads.delete("app").catch(() => {
2552
+ });
2553
+ }
2554
+ await ws.workloads.create({
2555
+ name: "app",
2556
+ cmd: workloadCmd,
2557
+ working_dir: workDir,
2558
+ env: [...envArray, `PORT=${config.port}`],
2559
+ restart_policy: restartPolicy,
2560
+ max_restarts: 10
2561
+ });
2562
+ } catch (err) {
2563
+ throw new Error(`Failed to create workload: ${err instanceof Error ? err.message : err}`);
2564
+ }
2565
+ } else {
2566
+ log({
2567
+ timestamp: now2(),
2568
+ level: "info",
2569
+ message: "Using the prebuilt image's default command and working directory.\n"
2451
2570
  });
2452
- } catch (err) {
2453
- throw new Error(`Failed to create workload: ${err instanceof Error ? err.message : err}`);
2454
2571
  }
2455
2572
  let url;
2456
2573
  const primaryEndpoint = primaryPublicEndpoint(config);
@@ -2588,7 +2705,9 @@ fi`;
2588
2705
  pg = result.page;
2589
2706
  } catch (err) {
2590
2707
  if (isOutputPathError(err)) throw outputDirError();
2591
- throw new Error(`Failed to create static page for slug "${pageSlug}": ${safeErrorMessage(err)}`);
2708
+ throw new Error(
2709
+ `Failed to create static page for slug "${pageSlug}": ${safeErrorMessage(err)}`
2710
+ );
2592
2711
  }
2593
2712
  return { ...pg, url: void 0 };
2594
2713
  };
@@ -2603,7 +2722,9 @@ fi`;
2603
2722
  });
2604
2723
  page = result.page;
2605
2724
  } catch (err) {
2606
- throw new Error(`Failed to redeploy static page for slug "${pageSlug}": ${safeErrorMessage(err)}`);
2725
+ throw new Error(
2726
+ `Failed to redeploy static page for slug "${pageSlug}": ${safeErrorMessage(err)}`
2727
+ );
2607
2728
  }
2608
2729
  } else {
2609
2730
  if (existingPage) {
@@ -3222,6 +3343,7 @@ function clampShellWindow(value, fallback, min, max) {
3222
3343
  }
3223
3344
 
3224
3345
  export {
3346
+ compileCloudDockerfilePlan,
3225
3347
  PAGE_CONTAINER_PREFIX,
3226
3348
  provisionCloudWorkspace,
3227
3349
  HostBuildForbiddenError,