@geekmidas/cli 10.0.0-alpha.76 → 10.0.0-alpha.78

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 (133) hide show
  1. package/dist/{LegacyStateStore-dyq07_Ir.mjs → LegacyStateStore-Ceq7A_75.mjs} +2 -2
  2. package/dist/{LegacyStateStore-dyq07_Ir.mjs.map → LegacyStateStore-Ceq7A_75.mjs.map} +1 -1
  3. package/dist/{LegacyStateStore-DwngfJxr.cjs → LegacyStateStore-Z-EKUGSX.cjs} +2 -2
  4. package/dist/{LegacyStateStore-DwngfJxr.cjs.map → LegacyStateStore-Z-EKUGSX.cjs.map} +1 -1
  5. package/dist/{LocalStateStore-B0NCde8Z.cjs → LocalStateStore-Bocb8dmZ.cjs} +2 -2
  6. package/dist/{LocalStateStore-B0NCde8Z.cjs.map → LocalStateStore-Bocb8dmZ.cjs.map} +1 -1
  7. package/dist/{LocalStateStore-DLxCCatI.mjs → LocalStateStore-C72bwpG2.mjs} +2 -2
  8. package/dist/{LocalStateStore-DLxCCatI.mjs.map → LocalStateStore-C72bwpG2.mjs.map} +1 -1
  9. package/dist/{S3StateStore-BOenb-3o.cjs → S3StateStore-35YfcdL_.cjs} +2 -2
  10. package/dist/{S3StateStore-BOenb-3o.cjs.map → S3StateStore-35YfcdL_.cjs.map} +1 -1
  11. package/dist/{S3StateStore-CGK_4hlo.mjs → S3StateStore-C9upSW5o.mjs} +2 -2
  12. package/dist/{S3StateStore-CGK_4hlo.mjs.map → S3StateStore-C9upSW5o.mjs.map} +1 -1
  13. package/dist/{SSMStateStore-DYeD__7p.cjs → SSMStateStore-W-Ps22AQ.cjs} +2 -2
  14. package/dist/{SSMStateStore-DYeD__7p.cjs.map → SSMStateStore-W-Ps22AQ.cjs.map} +1 -1
  15. package/dist/{SSMStateStore-B_5_RqY-.mjs → SSMStateStore-dOmFO6g1.mjs} +2 -2
  16. package/dist/{SSMStateStore-B_5_RqY-.mjs.map → SSMStateStore-dOmFO6g1.mjs.map} +1 -1
  17. package/dist/{add-j2yI3cbS.cjs → add-BvOICikp.cjs} +8 -8
  18. package/dist/{add-j2yI3cbS.cjs.map → add-BvOICikp.cjs.map} +1 -1
  19. package/dist/{add-B7IMXzou.mjs → add-D2XKszYP.mjs} +3 -3
  20. package/dist/{add-B7IMXzou.mjs.map → add-D2XKszYP.mjs.map} +1 -1
  21. package/dist/{bundler-Cf6OgaOe.cjs → bundler-C6T-1OLT.cjs} +5 -5
  22. package/dist/{bundler-Cf6OgaOe.cjs.map → bundler-C6T-1OLT.cjs.map} +1 -1
  23. package/dist/{bundler-DO6bHhnb.mjs → bundler-CtHzKqnZ.mjs} +5 -5
  24. package/dist/{bundler-DO6bHhnb.mjs.map → bundler-CtHzKqnZ.mjs.map} +1 -1
  25. package/dist/{cli-BCPOHmg9.mjs → cli-BgPyhx1i.mjs} +3 -3
  26. package/dist/{cli-BCPOHmg9.mjs.map → cli-BgPyhx1i.mjs.map} +1 -1
  27. package/dist/{cli-CNanUB7a.cjs → cli-K6Vj_pPW.cjs} +4 -4
  28. package/dist/{cli-CNanUB7a.cjs.map → cli-K6Vj_pPW.cjs.map} +1 -1
  29. package/dist/{compose-h_6oNnHC.cjs → compose-B7MC19eC.cjs} +668 -110
  30. package/dist/compose-B7MC19eC.cjs.map +1 -0
  31. package/dist/{compose-Dq4VCtu5.mjs → compose-DByK353T.mjs} +606 -66
  32. package/dist/compose-DByK353T.mjs.map +1 -0
  33. package/dist/{config-CqAvPP-f.d.mts → config-Bql0AmJt.d.mts} +3 -3
  34. package/dist/{config-CWyhLSY1.d.cts.map → config-Bql0AmJt.d.mts.map} +1 -1
  35. package/dist/{config-CWyhLSY1.d.cts → config-DlNpHK8p.d.cts} +3 -3
  36. package/dist/{config-CqAvPP-f.d.mts.map → config-DlNpHK8p.d.cts.map} +1 -1
  37. package/dist/config.cjs +1 -1
  38. package/dist/config.d.cts +3 -3
  39. package/dist/config.d.mts +3 -3
  40. package/dist/config.mjs +1 -1
  41. package/dist/deploy/public.cjs +12 -12
  42. package/dist/deploy/public.d.cts +3 -3
  43. package/dist/deploy/public.d.cts.map +1 -1
  44. package/dist/deploy/public.d.mts +3 -3
  45. package/dist/deploy/public.d.mts.map +1 -1
  46. package/dist/deploy/public.mjs +6 -6
  47. package/dist/{deploy-CVXEx7Nm.mjs → deploy-BerP7ppB.mjs} +5 -5
  48. package/dist/{deploy-CVXEx7Nm.mjs.map → deploy-BerP7ppB.mjs.map} +1 -1
  49. package/dist/{deploy-CT3R9de9.cjs → deploy-Dt42xiQA.cjs} +11 -11
  50. package/dist/{deploy-CT3R9de9.cjs.map → deploy-Dt42xiQA.cjs.map} +1 -1
  51. package/dist/{dokploy-eSGmlhhj.cjs → dokploy-NG9UrVrP.cjs} +2 -2
  52. package/dist/{dokploy-eSGmlhhj.cjs.map → dokploy-NG9UrVrP.cjs.map} +1 -1
  53. package/dist/{dokploy-CYm2ozFs.mjs → dokploy-ssZQRK2C.mjs} +2 -2
  54. package/dist/{dokploy-CYm2ozFs.mjs.map → dokploy-ssZQRK2C.mjs.map} +1 -1
  55. package/dist/{encryption-vS3Qzw73.cjs → encryption-CbMI24fI.cjs} +6 -6
  56. package/dist/{encryption-vS3Qzw73.cjs.map → encryption-CbMI24fI.cjs.map} +1 -1
  57. package/dist/{encryption-DC0rsR1W.mjs → encryption-De6MN0O5.mjs} +3 -3
  58. package/dist/{encryption-DC0rsR1W.mjs.map → encryption-De6MN0O5.mjs.map} +1 -1
  59. package/dist/{engine-C0N2d1i5.cjs → engine-CoCsNoW_.cjs} +73 -68
  60. package/dist/{engine-C0N2d1i5.cjs.map → engine-CoCsNoW_.cjs.map} +1 -1
  61. package/dist/{engine-BOO5TWZd.mjs → engine-DOjGIgl4.mjs} +9 -4
  62. package/dist/{engine-BOO5TWZd.mjs.map → engine-DOjGIgl4.mjs.map} +1 -1
  63. package/dist/{github-CEaFDL6t.mjs → github-CwX9z89E.mjs} +3 -3
  64. package/dist/{github-CEaFDL6t.mjs.map → github-CwX9z89E.mjs.map} +1 -1
  65. package/dist/{github-ClkdCOL9.cjs → github-D9vGqdLk.cjs} +7 -7
  66. package/dist/{github-ClkdCOL9.cjs.map → github-D9vGqdLk.cjs.map} +1 -1
  67. package/dist/{index-DOxmDKqv.d.mts → index-BXO0vlme.d.mts} +2 -2
  68. package/dist/{index-Ccu0f_2m.d.cts.map → index-BXO0vlme.d.mts.map} +1 -1
  69. package/dist/{index-Ccu0f_2m.d.cts → index-Rs0Fua-q.d.cts} +2 -2
  70. package/dist/{index-DOxmDKqv.d.mts.map → index-Rs0Fua-q.d.cts.map} +1 -1
  71. package/dist/index.cjs +180 -219
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.mjs +108 -147
  74. package/dist/index.mjs.map +1 -1
  75. package/dist/{migrate-BRbYtFXG.cjs → migrate-B4GBvCNY.cjs} +5 -5
  76. package/dist/{migrate-BRbYtFXG.cjs.map → migrate-B4GBvCNY.cjs.map} +1 -1
  77. package/dist/{migrate-D3NAT7Ns.mjs → migrate-BQ4wVKq_.mjs} +3 -3
  78. package/dist/{migrate-D3NAT7Ns.mjs.map → migrate-BQ4wVKq_.mjs.map} +1 -1
  79. package/dist/{migrate-DrO2p9Rl.mjs → migrate-CNKaQl3K.mjs} +3 -3
  80. package/dist/{migrate-DrO2p9Rl.mjs.map → migrate-CNKaQl3K.mjs.map} +1 -1
  81. package/dist/{migrate-D5_-ylI1.cjs → migrate-Cgoy58Sh.cjs} +8 -8
  82. package/dist/{migrate-D5_-ylI1.cjs.map → migrate-Cgoy58Sh.cjs.map} +1 -1
  83. package/dist/openapi.cjs +1 -1
  84. package/dist/openapi.d.cts +1 -1
  85. package/dist/openapi.d.mts +1 -1
  86. package/dist/openapi.mjs +1 -1
  87. package/dist/reconcile/public.d.cts +1 -1
  88. package/dist/reconcile/public.d.mts +1 -1
  89. package/dist/{resolve-Fa1Ii0BK.cjs → resolve-CEaqRBzY.cjs} +3 -3
  90. package/dist/{resolve-Fa1Ii0BK.cjs.map → resolve-CEaqRBzY.cjs.map} +1 -1
  91. package/dist/{resolve-C1muFos5.mjs → resolve-mvaystiE.mjs} +3 -3
  92. package/dist/{resolve-C1muFos5.mjs.map → resolve-mvaystiE.mjs.map} +1 -1
  93. package/dist/{rollback-C9N5lKVf.cjs → rollback-F3P-rA-r.cjs} +7 -7
  94. package/dist/{rollback-C9N5lKVf.cjs.map → rollback-F3P-rA-r.cjs.map} +1 -1
  95. package/dist/{rollback-DeQzbRqM.mjs → rollback-S2C-Y9pB.mjs} +5 -5
  96. package/dist/{rollback-DeQzbRqM.mjs.map → rollback-S2C-Y9pB.mjs.map} +1 -1
  97. package/dist/{runtime-Cm7QA53H.d.cts → runtime-D9AxGzrw.d.cts} +2 -2
  98. package/dist/{runtime-BSvzPqXW.d.mts.map → runtime-D9AxGzrw.d.cts.map} +1 -1
  99. package/dist/{runtime-BSvzPqXW.d.mts → runtime-Dflki3Vs.d.mts} +2 -2
  100. package/dist/{runtime-Cm7QA53H.d.cts.map → runtime-Dflki3Vs.d.mts.map} +1 -1
  101. package/dist/{sniffer-Y6GQOCXB.cjs → sniffer-BjG-1mlD.cjs} +6 -6
  102. package/dist/{sniffer-Y6GQOCXB.cjs.map → sniffer-BjG-1mlD.cjs.map} +1 -1
  103. package/dist/{sniffer-DR8AlRCk.mjs → sniffer-ByWtH28i.mjs} +6 -6
  104. package/dist/{sniffer-DR8AlRCk.mjs.map → sniffer-ByWtH28i.mjs.map} +1 -1
  105. package/dist/{state-C2qi0iQw.cjs → stale-BsiPJI7Y.cjs} +111 -25
  106. package/dist/stale-BsiPJI7Y.cjs.map +1 -0
  107. package/dist/{state-CUecOCNJ.mjs → stale-C5FqIVc8.mjs} +106 -26
  108. package/dist/stale-C5FqIVc8.mjs.map +1 -0
  109. package/dist/target/public.cjs +1 -1
  110. package/dist/target/public.d.cts +2 -2
  111. package/dist/target/public.d.mts +2 -2
  112. package/dist/target/public.mjs +1 -1
  113. package/dist/{types-BdptGBFS.d.cts → types-ByY0zi3n.d.cts} +42 -3
  114. package/dist/types-ByY0zi3n.d.cts.map +1 -0
  115. package/dist/{types-OxE0OAn2.d.mts → types-DKxkXsKC.d.mts} +42 -3
  116. package/dist/types-DKxkXsKC.d.mts.map +1 -0
  117. package/dist/workspace/index.cjs +1 -1
  118. package/dist/workspace/index.d.cts +2 -2
  119. package/dist/workspace/index.d.mts +2 -2
  120. package/dist/workspace/index.mjs +1 -1
  121. package/dist/{workspace-QUTqYmVy.mjs → workspace-BBdqmkch.mjs} +114 -17
  122. package/dist/workspace-BBdqmkch.mjs.map +1 -0
  123. package/dist/{workspace-DOm81oUo.cjs → workspace-Bp8FEkF-.cjs} +130 -15
  124. package/dist/workspace-Bp8FEkF-.cjs.map +1 -0
  125. package/package.json +15 -15
  126. package/dist/compose-Dq4VCtu5.mjs.map +0 -1
  127. package/dist/compose-h_6oNnHC.cjs.map +0 -1
  128. package/dist/state-C2qi0iQw.cjs.map +0 -1
  129. package/dist/state-CUecOCNJ.mjs.map +0 -1
  130. package/dist/types-BdptGBFS.d.cts.map +0 -1
  131. package/dist/types-OxE0OAn2.d.mts.map +0 -1
  132. package/dist/workspace-DOm81oUo.cjs.map +0 -1
  133. package/dist/workspace-QUTqYmVy.mjs.map +0 -1
@@ -2,9 +2,9 @@ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
2
  const require_run = require("./run-fVZ-rLnc.cjs");
3
3
  const require_discover = require("./discover-D1It5r_h.cjs");
4
4
  const require_derive = require("./derive-DAbo49sm.cjs");
5
- const require_workspace = require("./workspace-DOm81oUo.cjs");
5
+ const require_workspace = require("./workspace-Bp8FEkF-.cjs");
6
6
  const require_define = require("./define-DzRz8FgH.cjs");
7
- const require_state = require("./state-C2qi0iQw.cjs");
7
+ const require_stale = require("./stale-BsiPJI7Y.cjs");
8
8
  const require_SubscriberGenerator = require("./SubscriberGenerator-CajwOa5W.cjs");
9
9
  const require_drivers = require("./drivers-BdvtCKc7.cjs");
10
10
  const require_databases = require("./databases-C2p5LGAp.cjs");
@@ -49,6 +49,27 @@ function runOptions(stack, env) {
49
49
  ...stack.signal ? { signal: stack.signal } : {}
50
50
  };
51
51
  }
52
+ /**
53
+ * The host ports a `docker ps` Ports column publishes: `0.0.0.0:443->443/tcp,
54
+ * [::]:443->443/tcp, 0.0.0.0:9000-9001->9000-9001/tcp`.
55
+ */
56
+ function publishedPorts(column) {
57
+ const ports = /* @__PURE__ */ new Set();
58
+ for (const match of column.matchAll(/(?:[\d.]+|\[[^\]]*\]):(\d+)(?:-(\d+))?->/g)) {
59
+ const first = Number(match[1]);
60
+ const last = match[2] ? Number(match[2]) : first;
61
+ for (let port = first; port <= last; port++) ports.add(port);
62
+ }
63
+ return [...ports];
64
+ }
65
+ /** Each line `docker ps` printed, as the containers holding `port`. */
66
+ function portHolders(output, port) {
67
+ return output.split("\n").filter(Boolean).map((line) => line.split(" ")).filter(([, , , column = ""]) => publishedPorts(column).includes(port)).map(([container = "", project, service]) => ({
68
+ container,
69
+ ...project ? { project } : {},
70
+ ...service ? { service } : {}
71
+ }));
72
+ }
52
73
  /** How long a stack has to pass its health checks. */
53
74
  const WAIT_TIMEOUT_S = 180;
54
75
  /** `docker compose port` printed nothing a port can be read from. */
@@ -249,8 +270,389 @@ const dockerCompose = {
249
270
  if (match) return match.slice(repository.length + 1);
250
271
  } catch {}
251
272
  return id || void 0;
273
+ },
274
+ async ensureNetwork(name) {
275
+ const { code } = await capture("docker", [
276
+ "network",
277
+ "inspect",
278
+ name
279
+ ]);
280
+ if (code === 0) return;
281
+ const created = await capture("docker", [
282
+ "network",
283
+ "create",
284
+ name
285
+ ]);
286
+ if (created.code !== 0 && !/already exists/.test(created.stderr)) throw new NetworkCreateFailed(name, created.stderr);
287
+ },
288
+ async publishers(port) {
289
+ const { stdout } = await capture("docker", [
290
+ "ps",
291
+ "--format",
292
+ "{{.Names}} {{.Label \"com.docker.compose.project\"}} {{.Label \"com.docker.compose.service\"}} {{.Ports}}"
293
+ ]);
294
+ return portHolders(stdout, port);
295
+ }
296
+ };
297
+ /** `docker network create` failed. */
298
+ var NetworkCreateFailed = class extends Error {
299
+ network;
300
+ output;
301
+ constructor(network, output) {
302
+ super(`Could not create the Docker network '${network}' (docker said: ${output.trim() || "nothing"}). Create it by hand with \`docker network create ${network}\` and run again.`);
303
+ this.network = network;
304
+ this.output = output;
305
+ this.name = "NetworkCreateFailed";
306
+ }
307
+ };
308
+ //#endregion
309
+ //#region src/compose/traefik.ts
310
+ /**
311
+ * The server's shared edge, rendered for Traefik: what the edge itself runs
312
+ * with, and what one stack registers with it.
313
+ *
314
+ * One Traefik per server owns 80 and 443, the ACME state and the redirect to
315
+ * HTTPS. It is configured through its file provider alone: each stack writes
316
+ * one file — its routers, services and middlewares, every name prefixed with
317
+ * its compose project — into a directory the edge watches, and removes it
318
+ * when it stops. There is no Docker socket: the edge never learns what runs
319
+ * on the server except from those files.
320
+ *
321
+ * The edge and the stacks meet on one external Docker network. Only a
322
+ * stack's public services join it, each under an alias prefixed with its
323
+ * project (`<project>-api`), because the network is shared: a bare service
324
+ * name there would be answered by every stack's `api`.
325
+ *
326
+ * Pure: the documents are data, and the edge's side effects are `edge.ts`'s.
327
+ */
328
+ /**
329
+ * The image, pinned: the edge is every stack's way in, so a new version is a
330
+ * change to this line, not whatever `latest` is on the day a server starts.
331
+ */
332
+ const TRAEFIK_IMAGE = "traefik:v3.7.13";
333
+ /** The edge's compose project, one per server. */
334
+ const EDGE_PROJECT = "gkm-edge";
335
+ /** The external network the edge and every stack's public services share. */
336
+ const EDGE_NETWORK = "gkm-edge";
337
+ /** The edge's compose service. */
338
+ const EDGE_SERVICE = "traefik";
339
+ /** Inside the edge's container: what it watches, and where certificates are. */
340
+ const EDGE_DYNAMIC_DIR = "/etc/gkm-edge/dynamic";
341
+ const EDGE_CERTS_DIR = "/etc/gkm-edge/certs";
342
+ /** The certificate resolver every deployed route without its own certificate uses. */
343
+ const ACME_RESOLVER = "letsencrypt";
344
+ /** The internal entrypoint the edge's health check asks; never published. */
345
+ const PING_PORT = 8082;
346
+ /** A stack's service, as the edge — and the stack itself — reach it. */
347
+ function edgeAlias(project, service) {
348
+ return `${project}-${service}`;
349
+ }
350
+ /** A stack's certificate, as the edge's container reads it. */
351
+ function edgeCertificatePaths(project) {
352
+ return {
353
+ certFile: `${EDGE_CERTS_DIR}/${project}.crt`,
354
+ keyFile: `${EDGE_CERTS_DIR}/${project}.key`
355
+ };
356
+ }
357
+ /**
358
+ * The edge's static configuration: the entrypoints, the redirect, ACME, and
359
+ * the directory it watches. No API and no dashboard — nothing about the edge
360
+ * is reachable but the routes stacks register.
361
+ */
362
+ function traefikStatic(ports) {
363
+ return {
364
+ global: {
365
+ checkNewVersion: false,
366
+ sendAnonymousUsage: false
367
+ },
368
+ entryPoints: {
369
+ web: {
370
+ address: ":80",
371
+ http: { redirections: { entryPoint: {
372
+ to: ports.https === 443 ? "websecure" : `:${ports.https}`,
373
+ scheme: "https",
374
+ permanent: true
375
+ } } }
376
+ },
377
+ websecure: { address: ":443" },
378
+ ping: { address: `:${PING_PORT}` }
379
+ },
380
+ ping: { entryPoint: "ping" },
381
+ providers: { file: {
382
+ directory: EDGE_DYNAMIC_DIR,
383
+ watch: true
384
+ } },
385
+ certificatesResolvers: { [ACME_RESOLVER]: { acme: {
386
+ storage: "/acme/acme.json",
387
+ httpChallenge: { entryPoint: "web" }
388
+ } } },
389
+ log: { level: "INFO" }
390
+ };
391
+ }
392
+ /** The edge's compose document. */
393
+ function edgeCompose(options) {
394
+ const { dir, ports } = options;
395
+ return {
396
+ name: EDGE_PROJECT,
397
+ services: { [EDGE_SERVICE]: {
398
+ image: TRAEFIK_IMAGE,
399
+ restart: "unless-stopped",
400
+ ports: [`${ports.https}:443`, `${ports.http}:80`],
401
+ volumes: [
402
+ `${dir}/traefik.yml:/etc/traefik/traefik.yml:ro`,
403
+ `${dir}/dynamic:${EDGE_DYNAMIC_DIR}:ro`,
404
+ `${dir}/certs:${EDGE_CERTS_DIR}:ro`,
405
+ "acme:/acme"
406
+ ],
407
+ labels: { "dev.geekmidas.edge.config": (0, node_crypto.createHash)("sha256").update(options.staticConfig).digest("hex").slice(0, 16) },
408
+ networks: ["edge"],
409
+ healthcheck: {
410
+ test: [
411
+ "CMD",
412
+ "traefik",
413
+ "healthcheck",
414
+ "--ping"
415
+ ],
416
+ interval: "10s",
417
+ timeout: "5s",
418
+ retries: 5
419
+ },
420
+ logging: options.logging
421
+ } },
422
+ volumes: { acme: {} },
423
+ networks: { edge: {
424
+ name: EDGE_NETWORK,
425
+ external: true
426
+ } }
427
+ };
428
+ }
429
+ /** A stack's dynamic configuration, as an object. */
430
+ function traefikDynamic(routes, options) {
431
+ const { project, tls } = options;
432
+ const name = (route, suffix) => `${project}-${route.name}${suffix ? `-${suffix}` : ""}`;
433
+ const url = ({ service, port }) => `http://${edgeAlias(project, service)}:${port}`;
434
+ const routers = {};
435
+ const services = {};
436
+ const middlewares = {};
437
+ for (const route of routes) {
438
+ const chain = [];
439
+ if (route.allow) {
440
+ middlewares[name(route, "allow")] = { ipAllowList: { sourceRange: [...route.allow] } };
441
+ chain.push(name(route, "allow"));
442
+ }
443
+ if (route.prefix) {
444
+ middlewares[name(route, "prefix")] = { addPrefix: { prefix: route.prefix } };
445
+ chain.push(name(route, "prefix"));
446
+ }
447
+ routers[name(route)] = {
448
+ rule: `Host(\`${route.host}\`)`,
449
+ entryPoints: ["websecure"],
450
+ service: name(route),
451
+ ...chain.length > 0 ? { middlewares: chain } : {},
452
+ tls: tls.kind === "acme" ? { certResolver: ACME_RESOLVER } : {}
453
+ };
454
+ services[name(route)] = { loadBalancer: {
455
+ servers: route.upstreams.map((upstream) => ({ url: url(upstream) })),
456
+ ...route.prefix ? { passHostHeader: false } : {},
457
+ ...route.streaming ? { responseForwarding: { flushInterval: "-1ms" } } : {},
458
+ ...route.health ? { healthCheck: {
459
+ path: route.health,
460
+ interval: "5s",
461
+ timeout: "3s"
462
+ } } : {}
463
+ } };
464
+ }
465
+ return {
466
+ http: {
467
+ routers,
468
+ services,
469
+ ...Object.keys(middlewares).length > 0 ? { middlewares } : {}
470
+ },
471
+ ...tls.kind === "files" ? { tls: { certificates: [{
472
+ certFile: tls.certFile,
473
+ keyFile: tls.keyFile
474
+ }] } } : {}
475
+ };
476
+ }
477
+ /** A stack's dynamic configuration file, as written to the edge's directory. */
478
+ function traefikDynamicFile(routes, options) {
479
+ return `# Generated by gkm compose for ${options.project} — do not edit.
480
+ # The shared edge (${EDGE_PROJECT}) reads every file in this directory; this
481
+ # one is removed by gkm compose --down.
482
+ ${yaml$1(traefikDynamic(routes, options))}`;
483
+ }
484
+ /** The edge's static configuration file. */
485
+ function traefikStaticFile(ports) {
486
+ return `# Generated by gkm compose — the server's shared edge. Do not edit.
487
+ ${yaml$1(traefikStatic(ports))}`;
488
+ }
489
+ /** The edge's compose file. */
490
+ function edgeComposeFile(options) {
491
+ return `# Generated by gkm compose — the server's shared edge. Do not edit.
492
+ #
493
+ # Started by the first stack that runs with proxy: 'traefik', and left
494
+ # running by gkm compose --down. Stop it, once nothing uses it, with:
495
+ #
496
+ # docker compose -p ${EDGE_PROJECT} -f ${options.dir}/docker-compose.yml down
497
+ ${yaml$1(edgeCompose(options))}`;
498
+ }
499
+ function yaml$1(value) {
500
+ return (0, yaml.stringify)(value, {
501
+ lineWidth: 0,
502
+ aliasDuplicateObjects: false
503
+ });
504
+ }
505
+ //#endregion
506
+ //#region src/compose/edge.ts
507
+ /**
508
+ * The server's shared Traefik edge, on disk and in Docker: started when a
509
+ * stack needs it, and each stack's routes written to — and removed from —
510
+ * the directory it watches.
511
+ *
512
+ * The edge lives in the deploy user's gkm home (`$GKM_HOME/edge`, else
513
+ * `~/.gkm/edge`): the user who runs `gkm compose` owns it, so nothing needs
514
+ * root, and it sits beside the stage keys that same user already keeps.
515
+ *
516
+ * ~/.gkm/edge/
517
+ * docker-compose.yml the edge's compose project (gkm-edge)
518
+ * traefik.yml its static configuration
519
+ * dynamic/<project>.yml one per stack: its routers, services, middlewares
520
+ * certs/<project>.crt|.key a stack's own certificate, where it has one
521
+ */
522
+ /** The edge's directory on this machine. */
523
+ function edgeDir(env = process.env) {
524
+ return (0, node_path.join)(require_run.gkmHome(env), "edge");
525
+ }
526
+ /** The edge's compose project, as docker is asked about it. */
527
+ function edgeRef(dir, options = {}) {
528
+ return {
529
+ project: EDGE_PROJECT,
530
+ file: (0, node_path.join)(dir, "docker-compose.yml"),
531
+ cwd: dir,
532
+ ...options
533
+ };
534
+ }
535
+ /**
536
+ * Something other than the proxy a stack is set to holds the edge's ports.
537
+ *
538
+ * With `proxy: 'traefik'`, the stack's own Caddy — still running from before
539
+ * the switch — or any other container; with `proxy: 'caddy'`, the shared
540
+ * edge, or another stack's Caddy. Refused before anything is started, so the
541
+ * stack is never left half on one proxy and half on the other.
542
+ */
543
+ var ComposeProxyClash = class extends Error {
544
+ project;
545
+ proxy;
546
+ port;
547
+ holder;
548
+ constructor(project, proxy, port, holder) {
549
+ const by = holder.project ? `${holder.container} (compose project ${holder.project})` : holder.container;
550
+ const fix = proxy === "traefik" ? holder.project === project && holder.service === "caddy" ? `That is this stack's own Caddy, from before it was switched to proxy: 'traefik'. Move it once: stop it (docker compose -p ${project} stop caddy), then run gkm compose again — the shared edge takes 80/443 and the stack registers with it.` : `The shared edge needs ports 80 and 443. Stop ${holder.container}${holder.project ? ` (docker compose -p ${holder.project} down, or its own gkm compose --down)` : ""}, or move the edge with GKM_COMPOSE_HTTPS_PORT/GKM_COMPOSE_HTTP_PORT.` : holder.project === "gkm-edge" ? `That is the server's shared Traefik edge, which other stacks register with. Set deploy.compose.proxy to 'traefik' for this stage so the stack registers with it too, or stop the edge once nothing uses it (docker compose -p ${EDGE_PROJECT} down).` : `Stop ${holder.container}, or publish this stack's Caddy elsewhere with GKM_COMPOSE_HTTPS_PORT/GKM_COMPOSE_HTTP_PORT.`;
551
+ super(`${project} is set to proxy: '${proxy}', and port ${port} is held by ${by}. ${fix}`);
552
+ this.project = project;
553
+ this.proxy = proxy;
554
+ this.port = port;
555
+ this.holder = holder;
556
+ this.name = "ComposeProxyClash";
252
557
  }
253
558
  };
559
+ /**
560
+ * Refuse a stack whose edge ports are held by anything but its own proxy:
561
+ * the shared edge for `'traefik'`, the stack's own Caddy for `'caddy'`.
562
+ */
563
+ async function assertEdgePorts(docker, options) {
564
+ const { project, proxy, ports } = options;
565
+ for (const port of [ports.https, ports.http]) for (const holder of await docker.publishers(port)) if (!(proxy === "traefik" ? holder.project === "gkm-edge" : holder.project === project && holder.service === "caddy")) throw new ComposeProxyClash(project, proxy, port, holder);
566
+ }
567
+ /** Write a file whole or not at all: a temporary file, then a rename. */
568
+ async function writeAtomic(path, content, mode = 420) {
569
+ const temporary = `${path}.${process.pid}.tmp`;
570
+ await (0, node_fs_promises.writeFile)(temporary, content, { mode });
571
+ await (0, node_fs_promises.chmod)(temporary, mode);
572
+ await (0, node_fs_promises.rename)(temporary, path);
573
+ }
574
+ /** Write a file only when its content changed; whether it did. */
575
+ async function writeIfChanged(path, content) {
576
+ if (await (0, node_fs_promises.readFile)(path, "utf-8").catch(() => void 0) === content) return false;
577
+ await writeAtomic(path, content);
578
+ return true;
579
+ }
580
+ /**
581
+ * The edge, up: its network, its files, and its container — started when it
582
+ * is not running, and recreated when its configuration changed. Idempotent:
583
+ * on a server where it already runs as configured, `up` changes nothing.
584
+ */
585
+ async function ensureEdge(docker, options) {
586
+ const { dir, ports } = options;
587
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "dynamic"), { recursive: true });
588
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "certs"), {
589
+ recursive: true,
590
+ mode: 448
591
+ });
592
+ const staticConfig = traefikStaticFile(ports);
593
+ await writeIfChanged((0, node_path.join)(dir, "traefik.yml"), staticConfig);
594
+ await writeIfChanged((0, node_path.join)(dir, "docker-compose.yml"), edgeComposeFile({
595
+ dir,
596
+ ports,
597
+ staticConfig,
598
+ logging: options.logging
599
+ }));
600
+ await docker.ensureNetwork(EDGE_NETWORK);
601
+ await docker.up(edgeRef(dir, {
602
+ ...options.output ? { output: options.output } : {},
603
+ ...options.signal ? { signal: options.signal } : {}
604
+ }), [EDGE_SERVICE]);
605
+ return [(0, node_path.join)(dir, "traefik.yml"), (0, node_path.join)(dir, "docker-compose.yml")];
606
+ }
607
+ /** A stack's file in the edge's directory. */
608
+ function routesFile(dir, project) {
609
+ return (0, node_path.join)(dir, "dynamic", `${project}.yml`);
610
+ }
611
+ /**
612
+ * Register a stack with the edge: its certificate first, where it has one,
613
+ * so the routes that need it never load without it — then its routes, in one
614
+ * rename the edge picks up whole.
615
+ */
616
+ async function writeEdgeRoutes(dir, project, routes, certificate) {
617
+ const files = [];
618
+ if (certificate) {
619
+ const certs = (0, node_path.join)(dir, "certs");
620
+ await (0, node_fs_promises.mkdir)(certs, {
621
+ recursive: true,
622
+ mode: 448
623
+ });
624
+ const crt = (0, node_path.join)(certs, `${project}.crt`);
625
+ const key = (0, node_path.join)(certs, `${project}.key`);
626
+ await (0, node_fs_promises.copyFile)(certificate.certFile, `${crt}.tmp`);
627
+ await (0, node_fs_promises.rename)(`${crt}.tmp`, crt);
628
+ await (0, node_fs_promises.copyFile)(certificate.keyFile, `${key}.tmp`);
629
+ await (0, node_fs_promises.chmod)(`${key}.tmp`, 384);
630
+ await (0, node_fs_promises.rename)(`${key}.tmp`, key);
631
+ files.push(crt, key);
632
+ } else await removeCertificate(dir, project);
633
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "dynamic"), { recursive: true });
634
+ const file = routesFile(dir, project);
635
+ await writeAtomic(file, routes);
636
+ files.push(file);
637
+ return files;
638
+ }
639
+ /** What a stack's file holds for the moment between emptying and removal. */
640
+ const UNREGISTERED = "# Unregistered by gkm compose --down.\n";
641
+ async function removeCertificate(dir, project) {
642
+ for (const ext of ["crt", "key"]) await (0, node_fs_promises.rm)((0, node_path.join)(dir, "certs", `${project}.${ext}`), { force: true });
643
+ }
644
+ /**
645
+ * Unregister a stack: its routes, then its certificate. Whether there was
646
+ * anything to remove.
647
+ */
648
+ async function removeEdgeRoutes(dir, project) {
649
+ const file = routesFile(dir, project);
650
+ const existed = (0, node_fs.existsSync)(file);
651
+ if (existed) await writeAtomic(file, UNREGISTERED);
652
+ await (0, node_fs_promises.rm)(file, { force: true });
653
+ await removeCertificate(dir, project);
654
+ return existed;
655
+ }
254
656
  //#endregion
255
657
  //#region src/compose/images.ts
256
658
  /**
@@ -496,9 +898,9 @@ function otlpHeaders(email, password) {
496
898
  /** The stack's OpenObserve for one stage. */
497
899
  function stackLogs(options) {
498
900
  const { config, stage, local, custom } = options;
499
- const conflicting = Object.keys(custom).filter(require_state.isOtlpDestination).sort();
901
+ const conflicting = Object.keys(custom).filter(require_stale.isOtlpDestination).sort();
500
902
  if (conflicting.length > 0) throw new LogsEndpointConflict(stage, conflicting);
501
- if (!local && !options.domain) throw new require_state.NoDomainForStage(stage);
903
+ if (!local && !options.domain) throw new require_stale.NoDomainForStage(stage);
502
904
  const email = custom["ZO_ROOT_USER_EMAIL"] ?? (local ? "admin@gkm.localhost" : `admin@${options.domain}`);
503
905
  const set = custom[LOGS_PASSWORD_KEY];
504
906
  if (!set && !local) throw new LogsPasswordMissing(stage);
@@ -596,7 +998,7 @@ const REDIS_SERVICE = "redis";
596
998
  /** The port it answers on, inside the network. */
597
999
  const REDIS_PORT = 6379;
598
1000
  /** Reconcile's pin, so `gkm dev` and the stack run one Redis. */
599
- const REDIS_IMAGE = require_state.DEFAULT_IMAGES.redis;
1001
+ const REDIS_IMAGE = require_stale.DEFAULT_IMAGES.redis;
600
1002
  /**
601
1003
  * How much it may hold before it evicts. A cache that grows until the box
602
1004
  * runs out of memory takes the apps with it; 256 MB is a lot of sessions and
@@ -715,9 +1117,16 @@ function redisService(redis) {
715
1117
  }
716
1118
  //#endregion
717
1119
  //#region src/compose/caddyfile.ts
1120
+ /** Where Caddy reads a stage's own certificate, inside its container. */
1121
+ const CADDY_TLS_DIR = "/etc/caddy/tls";
1122
+ /** An upstream on the stack's own network: its service name. */
1123
+ function address({ service, port }) {
1124
+ return `${service}:${port}`;
1125
+ }
718
1126
  /** The Caddyfile for a stack's edge. */
719
- function edgeCaddyfile(sites, options) {
720
- const global = options.local ? `{
1127
+ function edgeCaddyfile(routes, options) {
1128
+ const { tls } = options;
1129
+ const global = tls.kind === "internal" ? `{
721
1130
  # No ACME and no public DNS: the CA is Caddy's own and the names resolve
722
1131
  # to this machine.
723
1132
  local_certs
@@ -725,27 +1134,30 @@ function edgeCaddyfile(sites, options) {
725
1134
  }` : options.email ? `{
726
1135
  email ${options.email}
727
1136
  }` : "";
728
- const blocks = sites.map(({ host, upstream, rewrite, allow }) => `https://${host} {${options.local ? `
1137
+ const certificate = tls.kind === "internal" ? `
729
1138
  tls internal
730
- ` : ""}${allow ? `
1139
+ ` : tls.kind === "files" ? `
1140
+ tls ${tls.certFile} ${tls.keyFile}
1141
+ ` : "";
1142
+ const blocks = routes.map(({ host, upstreams, prefix, allow, streaming }) => `https://${host} {${certificate}${allow ? `
731
1143
  # Only these addresses — the peer Caddy sees, never a header a client
732
1144
  # could set. Everything else is refused before it reaches the upstream.
733
1145
  @denied not remote_ip ${allow.join(" ")}
734
1146
  respond @denied 403
735
- ` : ""}${rewrite ? `
1147
+ ` : ""}${prefix ? `
736
1148
  # A bucket, served at a host of its own: the bucket is a prefix on MinIO,
737
1149
  # which routes and signs on the Host header — so it is sent the upstream's.
738
- reverse_proxy ${upstream} {
739
- rewrite ${rewrite}
1150
+ reverse_proxy ${upstreams.map(address).join(" ")} {
1151
+ rewrite ${prefix}{uri}
740
1152
  header_up Host {upstream_hostport}
741
1153
  }` : `
742
1154
  # Host and X-Forwarded-* are passed through as Caddy does by default, so
743
1155
  # an app builds redirects, cookies and links on the address a caller used.
744
- reverse_proxy ${upstream} {
1156
+ reverse_proxy ${upstreams.map(address).join(" ")}${streaming ? ` {
745
1157
  # Never buffer: a streamed response — server-sent events, a Next.js
746
1158
  # RSC payload — reaches the client as each chunk is written.
747
1159
  flush_interval -1
748
- }`}
1160
+ }` : ""}`}
749
1161
  }`);
750
1162
  return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
751
1163
  }
@@ -907,7 +1319,9 @@ function composeStack(input) {
907
1319
  const custom = input.secrets?.custom ?? {};
908
1320
  const https = input.ports?.https ?? 443;
909
1321
  const http = input.ports?.http ?? 80;
910
- const plan = require_state.planFor(manifest, stage, (0, _geekmidas_manifest.provisionOrder)(manifest), {
1322
+ const proxy = require_workspace.proxyFor(workspace.deploy?.compose, stage, local);
1323
+ const certificate = local ? void 0 : require_workspace.certificateFor(workspace.deploy?.compose, stage, workspace.root);
1324
+ const plan = require_stale.planFor(manifest, stage, (0, _geekmidas_manifest.provisionOrder)(manifest), {
911
1325
  localStage: workspace.stages.local,
912
1326
  events: require_SubscriberGenerator.DEFAULT_EVENTS.server,
913
1327
  cache: STACK_CACHE,
@@ -917,7 +1331,7 @@ function composeStack(input) {
917
1331
  const seed = local ? void 0 : input.secrets?.seed;
918
1332
  if (!local && !seed) throw new StageSeedMissing(stage);
919
1333
  const credential = {
920
- master: local ? "geekmidas" : require_state.localRolePassword(workspace.name, plan, "master", seed),
1334
+ master: local ? "geekmidas" : require_stale.localRolePassword(workspace.name, plan, "master", seed),
921
1335
  ...seed ? { seed } : {}
922
1336
  };
923
1337
  const apps = stackApps(workspace, manifest, plan, {
@@ -927,7 +1341,7 @@ function composeStack(input) {
927
1341
  identity,
928
1342
  images: input.images
929
1343
  });
930
- const workers = stackWorkers(require_state.workerUnits(workspace, manifest, input.background ?? {}), {
1344
+ const workers = stackWorkers(require_stale.workerUnits(workspace, manifest, input.background ?? {}), {
931
1345
  stage,
932
1346
  identity,
933
1347
  images: input.images
@@ -945,12 +1359,12 @@ function composeStack(input) {
945
1359
  const reads = /* @__PURE__ */ new Map();
946
1360
  for (const app of apps) {
947
1361
  const site = manifest[app.id];
948
- reads.set(app.name, site?.kind === "site" ? Object.values((0, _geekmidas_manifest.publicEnvFor)(site, manifest)) : [...require_state.appEnvKeys(manifest, app.name, input.runnables) ?? []]);
1362
+ reads.set(app.name, site?.kind === "site" ? Object.values((0, _geekmidas_manifest.publicEnvFor)(site, manifest)) : [...require_stale.appEnvKeys(manifest, app.name, input.runnables) ?? []]);
949
1363
  }
950
- for (const worker of workers) reads.set(worker.name, [...require_state.workerEnvKeys(manifest, worker.id, input.runnables) ?? []]);
1364
+ for (const worker of workers) reads.set(worker.name, [...require_stale.workerEnvKeys(manifest, worker.id, input.runnables) ?? []]);
951
1365
  const domain = workspace.deploy?.domains?.[stage];
952
1366
  const logsConfig = require_workspace.resolveLogs(workspace.deploy?.compose?.logs);
953
- const logs = logsConfig ? stackLogs({
1367
+ const logsBase = logsConfig ? stackLogs({
954
1368
  config: logsConfig,
955
1369
  stage,
956
1370
  local,
@@ -963,7 +1377,7 @@ function composeStack(input) {
963
1377
  missing: [],
964
1378
  minio: plan.resources.filter((r) => r.kind === "objects").map((r) => r.id),
965
1379
  mailpit: plan.resources.filter((r) => r.kind === "email").map((r) => r.id)
966
- } : require_state.assertExternalServices({
1380
+ } : require_stale.assertExternalServices({
967
1381
  stage,
968
1382
  declarations: serviceDeclarations(plan, reads, owners),
969
1383
  supplied: custom,
@@ -976,30 +1390,16 @@ function composeStack(input) {
976
1390
  local,
977
1391
  custom
978
1392
  });
979
- const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logs ? [LOGS_SERVICE] : []].sort();
1393
+ const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logsBase ? [LOGS_SERVICE] : []].sort();
980
1394
  const storage = infra.includes("minio") ? (() => {
981
1395
  const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
982
1396
  return {
983
1397
  user: custom.AWS_ACCESS_KEY_ID ?? (local ? "geekmidas" : `${workspace.name}-minio`),
984
- password: custom.AWS_SECRET_ACCESS_KEY ?? (local ? "geekmidas" : require_state.localRolePassword(workspace.name, plan, "minio", seed)),
1398
+ password: custom.AWS_SECRET_ACCESS_KEY ?? (local ? "geekmidas" : require_stale.localRolePassword(workspace.name, plan, "minio", seed)),
985
1399
  buckets,
986
- policies: require_state.bucketPolicies(plan).filter((p) => buckets.includes(p.bucket))
1400
+ policies: require_stale.bucketPolicies(plan).filter((p) => buckets.includes(p.bucket))
987
1401
  };
988
1402
  })() : void 0;
989
- const derivation = {
990
- project: workspace.name,
991
- master: credential.master,
992
- ...credential.seed ? { seed: credential.seed } : {},
993
- ...!local && domain ? { mailFrom: `noreply@${domain}` } : {}
994
- };
995
- const outside = require_state.networkEnv(plan, {
996
- ...derivation,
997
- addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
998
- });
999
- const inside = require_state.networkEnv(plan, {
1000
- ...derivation,
1001
- addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${app.name}:${app.port}`]))
1002
- });
1003
1403
  const fileServers = /* @__PURE__ */ new Map();
1004
1404
  if (storage) for (const resource of plan.resources) {
1005
1405
  if (resource.kind !== "file-server" || !resource.of) continue;
@@ -1008,20 +1408,52 @@ function composeStack(input) {
1008
1408
  const bucket = byId.get(resource.of);
1009
1409
  if (!bucket) continue;
1010
1410
  let host;
1011
- if (local) host = require_state.hostFor(resource, workspace.name);
1411
+ if (local) host = require_stale.hostFor(resource, workspace.name);
1012
1412
  else {
1013
- if (!domain) throw new require_state.NoDomainForStage(stage);
1413
+ if (!domain) throw new require_stale.NoDomainForStage(stage);
1014
1414
  host = `${resource.subdomain ?? require_derive.appKey(resource.id)}.${domain}`;
1015
1415
  }
1016
1416
  fileServers.set(resource.id, {
1017
1417
  url: `https://${host}${local && https !== 443 ? `:${https}` : ""}`,
1018
- site: {
1418
+ route: {
1419
+ name: `files-${require_derive.appKey(resource.id)}`,
1019
1420
  host,
1020
- upstream: "minio:9000",
1021
- rewrite: `/${bucket.name}{uri}`
1421
+ upstreams: [{
1422
+ service: "minio",
1423
+ port: 9e3
1424
+ }],
1425
+ streaming: false,
1426
+ prefix: `/${bucket.name}`
1022
1427
  }
1023
1428
  });
1024
1429
  }
1430
+ const edgeServices = new Set(proxy === "traefik" ? [
1431
+ ...apps.map((app) => app.name),
1432
+ ...fileServers.size > 0 ? ["minio"] : [],
1433
+ ...logsBase?.host && logsBase.public ? [LOGS_SERVICE] : []
1434
+ ] : []);
1435
+ const hostOf = (service) => edgeServices.has(service) ? edgeAlias(project, service) : service;
1436
+ const logs = logsBase && {
1437
+ ...logsBase,
1438
+ appEnv: {
1439
+ ...logsBase.appEnv,
1440
+ OTEL_EXPORTER_OTLP_ENDPOINT: logsBase.appEnv.OTEL_EXPORTER_OTLP_ENDPOINT.replace(`//openobserve:`, `//${hostOf("openobserve")}:`)
1441
+ }
1442
+ };
1443
+ const derivation = {
1444
+ project: workspace.name,
1445
+ master: credential.master,
1446
+ ...credential.seed ? { seed: credential.seed } : {},
1447
+ ...!local && domain ? { mailFrom: `noreply@${domain}` } : {}
1448
+ };
1449
+ const outside = require_stale.networkEnv(plan, {
1450
+ ...derivation,
1451
+ addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
1452
+ }, hostOf);
1453
+ const inside = require_stale.networkEnv(plan, {
1454
+ ...derivation,
1455
+ addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${hostOf(app.name)}:${app.port}`]))
1456
+ }, hostOf);
1025
1457
  const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
1026
1458
  const appById = new Map(apps.map((app) => [app.id, app]));
1027
1459
  /**
@@ -1034,7 +1466,7 @@ function composeStack(input) {
1034
1466
  if (owner.kind === "file-server") return fileServers.get(owner.id)?.url;
1035
1467
  return outside[key];
1036
1468
  }
1037
- if (owner && require_state.suppliedOnly(owner.kind, owner.id, key, local)) {
1469
+ if (owner && require_stale.suppliedOnly(owner.kind, owner.id, key, local)) {
1038
1470
  const value = custom[key];
1039
1471
  if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
1040
1472
  return value;
@@ -1074,17 +1506,17 @@ function composeStack(input) {
1074
1506
  continue;
1075
1507
  }
1076
1508
  if (input.buildOnly) continue;
1077
- const keys = require_state.appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
1509
+ const keys = require_stale.appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
1078
1510
  const values = valuesFor(app, keys);
1079
1511
  const trusted = (0, _geekmidas_manifest.provideKey)(app.id, "trustedOrigins");
1080
1512
  if (keys.has(trusted)) {
1081
- const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${caller.name}:${caller.port}`);
1513
+ const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${hostOf(caller.name)}:${caller.port}`);
1082
1514
  const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
1083
1515
  values[trusted] = [...new Set(origins)].join(",");
1084
1516
  }
1085
1517
  app.env = {
1086
1518
  ...values,
1087
- ...require_state.otelEnv({
1519
+ ...require_stale.otelEnv({
1088
1520
  ...custom,
1089
1521
  ...logs?.appEnv
1090
1522
  }, app.name),
@@ -1095,14 +1527,14 @@ function composeStack(input) {
1095
1527
  for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
1096
1528
  }
1097
1529
  for (const worker of input.buildOnly ? [] : workers) {
1098
- const keys = require_state.workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
1530
+ const keys = require_stale.workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
1099
1531
  worker.env = {
1100
1532
  ...valuesFor({
1101
1533
  name: worker.name,
1102
1534
  id: worker.id,
1103
1535
  kind: "rest-api"
1104
1536
  }, keys),
1105
- ...require_state.otelEnv({
1537
+ ...require_stale.otelEnv({
1106
1538
  ...custom,
1107
1539
  ...logs?.appEnv
1108
1540
  }, worker.name),
@@ -1112,6 +1544,14 @@ function composeStack(input) {
1112
1544
  };
1113
1545
  for (const [key, value] of Object.entries(worker.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(worker.name, key);
1114
1546
  }
1547
+ const deployedTls = certificate ? {
1548
+ kind: "files",
1549
+ ...proxy === "caddy" ? {
1550
+ certFile: `${CADDY_TLS_DIR}/cert.pem`,
1551
+ keyFile: `${CADDY_TLS_DIR}/key.pem`
1552
+ } : edgeCertificatePaths(project)
1553
+ } : { kind: "acme" };
1554
+ const tls = local ? { kind: "internal" } : deployedTls;
1115
1555
  const layout = input.layout ?? defaultLayout(workspace.root);
1116
1556
  const compose = stackFile({
1117
1557
  project,
@@ -1128,24 +1568,38 @@ function composeStack(input) {
1128
1568
  composeDir: (0, node_path.join)(workspace.root, stackDir(stage)),
1129
1569
  buildRoot: layout.buildRoot,
1130
1570
  workspaceRoot: workspace.root,
1131
- envFiles: !input.buildOnly
1571
+ envFiles: !input.buildOnly,
1572
+ proxy,
1573
+ edge: new Map([...edgeServices].map((service) => [service, hostOf(service)])),
1574
+ tls: certificate !== void 0
1132
1575
  });
1133
- const caddyfile = edgeCaddyfile([
1576
+ const routes = [
1134
1577
  ...apps.map((app) => ({
1578
+ name: app.name,
1135
1579
  host: app.host,
1136
- upstream: `${app.name}:${app.port}`
1580
+ upstreams: [{
1581
+ service: app.name,
1582
+ port: app.port
1583
+ }],
1584
+ streaming: true,
1585
+ health: app.kind === "site" ? "/" : "/health"
1137
1586
  })),
1138
- ...[...fileServers.values()].map(({ site }) => site),
1587
+ ...[...fileServers.values()].map(({ route }) => route),
1139
1588
  ...logs?.host && logs.public ? [{
1589
+ name: LOGS_SERVICE,
1140
1590
  host: logs.host,
1141
- upstream: `${LOGS_SERVICE}:${require_workspace.LOGS_PORT}`,
1591
+ upstreams: [{
1592
+ service: LOGS_SERVICE,
1593
+ port: require_workspace.LOGS_PORT
1594
+ }],
1595
+ streaming: true,
1142
1596
  allow: logs.public.allow
1143
1597
  }] : []
1144
- ], { local });
1598
+ ];
1145
1599
  const dockerfiles = {};
1146
1600
  for (const app of apps) {
1147
1601
  if (!app.build) continue;
1148
- dockerfiles[app.build.dockerfile] = require_state.appDockerfile(app.name, workspace.apps[app.name], {
1602
+ dockerfiles[app.build.dockerfile] = require_stale.appDockerfile(app.name, workspace.apps[app.name], {
1149
1603
  layout,
1150
1604
  workspaceRoot: workspace.root,
1151
1605
  apps: workspace.apps,
@@ -1155,7 +1609,7 @@ function composeStack(input) {
1155
1609
  }
1156
1610
  for (const worker of workers) {
1157
1611
  if (!worker.build) continue;
1158
- dockerfiles[worker.build.dockerfile] = require_state.workerDockerfile({
1612
+ dockerfiles[worker.build.dockerfile] = require_stale.workerDockerfile({
1159
1613
  id: worker.id,
1160
1614
  name: worker.name,
1161
1615
  app: worker.host
@@ -1173,12 +1627,18 @@ function composeStack(input) {
1173
1627
  workers,
1174
1628
  infra,
1175
1629
  compose,
1176
- caddyfile,
1630
+ proxy,
1631
+ routes,
1632
+ ...proxy === "caddy" ? { caddyfile: edgeCaddyfile(routes, { tls }) } : { traefik: traefikDynamicFile(routes, {
1633
+ project,
1634
+ tls: deployedTls
1635
+ }) },
1636
+ ...certificate ? { tls: certificate } : {},
1177
1637
  dockerfiles,
1178
1638
  plan,
1179
1639
  credential,
1180
1640
  ...storage ? { storage } : {},
1181
- devServices: local ? [] : require_state.devServicesUsed(services),
1641
+ devServices: local ? [] : require_stale.devServicesUsed(services),
1182
1642
  ...logs ? { logs } : {},
1183
1643
  ...redis ? { redis } : {}
1184
1644
  };
@@ -1239,7 +1699,7 @@ function stackApps(workspace, manifest, plan, options) {
1239
1699
  if (!app || !resource || !app.port) continue;
1240
1700
  const host = hostOf(name, app, workspace, resource, options);
1241
1701
  const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
1242
- const ref = require_state.validateImageRef(require_state.imageRef(options.identity, name, options.images.registry, tag));
1702
+ const ref = require_stale.validateImageRef(require_stale.imageRef(options.identity, name, options.images.registry, tag));
1243
1703
  apps.push({
1244
1704
  app: name,
1245
1705
  name,
@@ -1263,8 +1723,8 @@ function stackWorkers(units, options) {
1263
1723
  name: unit.name,
1264
1724
  id: unit.id,
1265
1725
  host: unit.app,
1266
- port: require_state.WORKER_PORT,
1267
- ref: require_state.validateImageRef(require_state.imageRef(options.identity, unit.name, options.images.registry, options.images.tag)),
1726
+ port: require_stale.WORKER_PORT,
1727
+ ref: require_stale.validateImageRef(require_stale.imageRef(options.identity, unit.name, options.images.registry, options.images.tag)),
1268
1728
  tag: options.images.tag,
1269
1729
  ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${unit.name}` } } : {}
1270
1730
  }));
@@ -1275,25 +1735,27 @@ function stackWorkers(units, options) {
1275
1735
  * same rule a deploy follows — for any other.
1276
1736
  */
1277
1737
  function hostOf(name, app, workspace, resource, options) {
1278
- if (options.local) return require_state.hostFor(resource, workspace.name);
1279
- return require_state.resolveHost(name, app, options.stage, workspace.deploy?.domains, require_state.isMainFrontendApp(name, app, workspace.apps));
1738
+ if (options.local) return require_stale.hostFor(resource, workspace.name);
1739
+ return require_stale.resolveHost(name, app, options.stage, workspace.deploy?.domains, require_stale.isMainFrontendApp(name, app, workspace.apps));
1280
1740
  }
1281
1741
  /** The compose document: the infrastructure, the apps, and the edge. */
1282
1742
  function stackFile(options) {
1283
1743
  const { project, plan, apps } = options;
1284
1744
  const infra = options.infra.filter((name) => name !== LOGS_SERVICE);
1285
- const derived = require_state.composeFor({
1745
+ const derived = require_stale.composeFor({
1286
1746
  ...plan,
1287
1747
  containers: [...infra]
1288
1748
  }, {
1289
1749
  project,
1290
- ports: Object.fromEntries(require_state.portKeys(infra, plan.fakes).map((key) => [key, 0]))
1750
+ ports: Object.fromEntries(require_stale.portKeys(infra, plan.fakes).map((key) => [key, 0]))
1291
1751
  });
1292
1752
  const services = {};
1293
1753
  const volumes = {
1294
1754
  ...derived.volumes,
1295
- "caddy-data": {},
1296
- "caddy-config": {},
1755
+ ...options.proxy === "caddy" ? {
1756
+ "caddy-data": {},
1757
+ "caddy-config": {}
1758
+ } : {},
1297
1759
  ...options.logs ? { "openobserve-data": {} } : {}
1298
1760
  };
1299
1761
  for (const name of infra) {
@@ -1325,7 +1787,7 @@ function stackFile(options) {
1325
1787
  for (const app of apps) services[app.name] = {
1326
1788
  image: app.ref,
1327
1789
  ...app.build ? { build: {
1328
- ...require_state.composeBuildPaths({
1790
+ ...require_stale.composeBuildPaths({
1329
1791
  composeDir: options.composeDir,
1330
1792
  buildRoot: options.buildRoot,
1331
1793
  workspaceRoot: options.workspaceRoot,
@@ -1345,7 +1807,7 @@ function stackFile(options) {
1345
1807
  };
1346
1808
  for (const worker of options.workers) services[worker.name] = {
1347
1809
  image: worker.ref,
1348
- ...worker.build ? { build: require_state.composeBuildPaths({
1810
+ ...worker.build ? { build: require_stale.composeBuildPaths({
1349
1811
  composeDir: options.composeDir,
1350
1812
  buildRoot: options.buildRoot,
1351
1813
  workspaceRoot: options.workspaceRoot,
@@ -1359,14 +1821,15 @@ function stackFile(options) {
1359
1821
  ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {},
1360
1822
  healthcheck: probe(worker.port, "/health")
1361
1823
  };
1362
- services.caddy = {
1363
- image: require_state.DEFAULT_IMAGES.caddy,
1824
+ if (options.proxy === "caddy") services.caddy = {
1825
+ image: require_stale.DEFAULT_IMAGES.caddy,
1364
1826
  restart: "unless-stopped",
1365
1827
  ports: [`${options.https}:443`, `${options.http}:80`],
1366
1828
  volumes: [
1367
1829
  "./Caddyfile:/etc/caddy/Caddyfile:ro",
1368
1830
  "caddy-data:/data",
1369
- "caddy-config:/config"
1831
+ "caddy-config:/config",
1832
+ ...options.tls ? [`./tls:${CADDY_TLS_DIR}:ro`] : []
1370
1833
  ],
1371
1834
  depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
1372
1835
  healthcheck: {
@@ -1380,6 +1843,14 @@ function stackFile(options) {
1380
1843
  retries: 5
1381
1844
  }
1382
1845
  };
1846
+ for (const [service, alias] of options.edge) {
1847
+ const definition = services[service];
1848
+ if (!definition) continue;
1849
+ definition.networks = {
1850
+ default: { aliases: [alias] },
1851
+ edge: { aliases: [alias] }
1852
+ };
1853
+ }
1383
1854
  for (const service of Object.values(services)) {
1384
1855
  if (!options.envFiles) delete service.env_file;
1385
1856
  service.logging = {
@@ -1390,7 +1861,11 @@ function stackFile(options) {
1390
1861
  return {
1391
1862
  name: project,
1392
1863
  services,
1393
- volumes
1864
+ volumes,
1865
+ ...options.edge.size > 0 ? { networks: { edge: {
1866
+ name: EDGE_NETWORK,
1867
+ external: true
1868
+ } } } : {}
1394
1869
  };
1395
1870
  }
1396
1871
  /**
@@ -1403,7 +1878,7 @@ function defaultLayout(root) {
1403
1878
  gkmRoot: ".",
1404
1879
  tools: {
1405
1880
  packageManager: "pnpm",
1406
- turboVersion: require_state.TURBO_VERSION,
1881
+ turboVersion: require_stale.TURBO_VERSION,
1407
1882
  monorepo: true
1408
1883
  },
1409
1884
  gkmPaths: ["gkm.config.*"]
@@ -1437,12 +1912,12 @@ var HealthRequestTimedOut = class extends Error {
1437
1912
  }
1438
1913
  };
1439
1914
  /** The real probe: one HTTPS GET, the certificate verified. */
1440
- const httpsProbe = ({ url, ca, connectTo, timeoutMs, signal }) => {
1915
+ const httpsProbe = ({ url, ca, connectTo, connectPort, timeoutMs, signal }) => {
1441
1916
  const target = new URL(url);
1442
1917
  return new Promise((resolve, reject) => {
1443
1918
  const req = (0, node_https.request)({
1444
1919
  host: connectTo ?? target.hostname,
1445
- port: target.port || 443,
1920
+ port: connectPort ?? (target.port || 443),
1446
1921
  servername: target.hostname,
1447
1922
  path: `${target.pathname}${target.search}`,
1448
1923
  method: "GET",
@@ -1468,10 +1943,13 @@ function isHealthy(status) {
1468
1943
  var ComposeAppsUnhealthy = class extends Error {
1469
1944
  project;
1470
1945
  apps;
1471
- constructor(project, apps) {
1472
- super(`${project} started, but ${apps.length === 1 ? "this app does" : "these apps do"} not answer through Caddy:\n` + apps.map(({ url, last }) => ` - ${url}: ${last}`).join("\n") + `\nSee why with \`docker compose -p ${project} logs caddy ${apps.map((a) => a.app).join(" ")}\`.`);
1946
+ proxy;
1947
+ constructor(project, apps, proxy = "caddy") {
1948
+ const names = apps.map((a) => a.app).join(" ");
1949
+ super(`${project} started, but ${apps.length === 1 ? "this app does" : "these apps do"} not answer through ${proxy === "caddy" ? "Caddy" : "the shared Traefik edge"}:\n` + apps.map(({ url, last }) => ` - ${url}: ${last}`).join("\n") + (proxy === "caddy" ? `\nSee why with \`docker compose -p ${project} logs caddy ${names}\`.` : `\nSee why with \`docker compose -p gkm-edge logs traefik\` and \`docker compose -p ${project} logs ${names}\`.`));
1473
1950
  this.project = project;
1474
1951
  this.apps = apps;
1952
+ this.proxy = proxy;
1475
1953
  this.name = "ComposeAppsUnhealthy";
1476
1954
  }
1477
1955
  };
@@ -1583,8 +2061,8 @@ function edgePorts(env = process.env) {
1583
2061
  const defaultDeps = {
1584
2062
  docker: dockerCompose,
1585
2063
  revision: gitRevision,
1586
- sql: require_state.pgClient,
1587
- buckets: require_state.bucketClient,
2064
+ sql: require_stale.pgClient,
2065
+ buckets: require_stale.bucketClient,
1588
2066
  migrate: require_databases.migrateDatabases,
1589
2067
  probe: httpsProbe,
1590
2068
  healthAttempts: 90,
@@ -1604,24 +2082,31 @@ async function validateCompose(ctx, deps, options = {}) {
1604
2082
  if (push && mode !== "build") throw new ComposePushNeedsBuild();
1605
2083
  if (options.pin && mode !== "pull") throw new ComposePinNeedsPull();
1606
2084
  const registry = workspace.deploy?.registry;
1607
- if (!registry && (push || mode === "pull")) throw new RegistryRequired(push ? "push" : "pull", require_state.imageRef(identity, ctx.apps[0] ?? workspace.name, void 0, tag));
2085
+ if (!registry && (push || mode === "pull")) throw new RegistryRequired(push ? "push" : "pull", require_stale.imageRef(identity, ctx.apps[0] ?? workspace.name, void 0, tag));
1608
2086
  const runnables = {};
1609
2087
  const background = {};
1610
2088
  const manifest = await require_discover.discover({
1611
- patterns: require_state.constructGlobs(workspace),
2089
+ patterns: require_stale.constructGlobs(workspace),
1612
2090
  cwd: root,
1613
2091
  runnables,
1614
2092
  background
1615
2093
  });
1616
2094
  const { secrets, generated } = await stageSecrets(ctx, manifest);
1617
- if (!push) await require_credentialSchemas.assertStageCredentials({
1618
- root,
1619
- patterns: require_state.constructGlobs(workspace),
1620
- manifest,
1621
- stage,
1622
- supplied: secrets?.custom ?? {}
1623
- });
1624
- const layout = mode === "build" ? require_state.imageLayout(workspace) : void 0;
2095
+ if (!push) {
2096
+ require_stale.assertNoStaleSecrets({
2097
+ manifest,
2098
+ stage,
2099
+ supplied: secrets?.custom ?? {}
2100
+ });
2101
+ await require_credentialSchemas.assertStageCredentials({
2102
+ root,
2103
+ patterns: require_stale.constructGlobs(workspace),
2104
+ manifest,
2105
+ stage,
2106
+ supplied: secrets?.custom ?? {}
2107
+ });
2108
+ }
2109
+ const layout = mode === "build" ? require_stale.imageLayout(workspace) : void 0;
1625
2110
  const composed = composeStack({
1626
2111
  workspace,
1627
2112
  manifest,
@@ -1640,10 +2125,13 @@ async function validateCompose(ctx, deps, options = {}) {
1640
2125
  ...layout ? { layout } : {},
1641
2126
  ...push ? { buildOnly: true } : {}
1642
2127
  });
1643
- require_state.reportDevServices(ctx, composed.devServices);
2128
+ require_stale.reportDevServices(ctx, composed.devServices);
1644
2129
  if (mode === "build") assertRedisClient(workspace, composed);
1645
2130
  const stack = options.pin ? withPinnedImages(composed, options.pin.file, options.pin.digests) : composed;
1646
2131
  if (mode === "pull" && !ctx.dryRun) await assertImagesExist(deps.docker, tag, [...stack.apps, ...stack.workers]);
2132
+ if (stack.tls && !push) {
2133
+ for (const file of [stack.tls.certFile, stack.tls.keyFile]) if (!(0, node_fs.existsSync)(file)) throw new require_workspace.ComposeTlsFileMissing(stage, file);
2134
+ }
1647
2135
  if (stack.redis && !stack.local) ctx.secrets.mask(stack.redis.password);
1648
2136
  if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
1649
2137
  ctx.secrets.mask(stack.logs.password);
@@ -1717,7 +2205,7 @@ async function stageSecrets(ctx, manifest) {
1717
2205
  secrets: stored,
1718
2206
  generated: []
1719
2207
  };
1720
- const withSeed = require_state.withGeneratedSecrets(stored ?? require_state.initStageSecrets(ctx.stage), manifest);
2208
+ const withSeed = require_stale.withGeneratedSecrets(stored ?? require_stale.initStageSecrets(ctx.stage), manifest);
1721
2209
  const withLogs = require_workspace.resolveLogs(ctx.workspace.deploy?.compose?.logs) ? withLogsPassword(withSeed.secrets) : {
1722
2210
  secrets: withSeed.secrets,
1723
2211
  generated: []
@@ -1812,7 +2300,7 @@ async function planCompose(ctx, run) {
1812
2300
  });
1813
2301
  }
1814
2302
  if (!run.push) planned({
1815
- key: "service:caddy",
2303
+ key: run.stack.proxy === "caddy" ? "service:caddy" : `service:${EDGE_PROJECT}/${EDGE_SERVICE}`,
1816
2304
  resourceType: "service",
1817
2305
  action: "ensure"
1818
2306
  });
@@ -1829,8 +2317,35 @@ async function provisionCompose(ctx, run, deps) {
1829
2317
  await ctx.secrets.write(run.secrets);
1830
2318
  ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
1831
2319
  }
2320
+ const ports = edgePorts(deps.env);
2321
+ await assertEdgePorts(deps.docker, {
2322
+ project: stack.project,
2323
+ proxy: stack.proxy,
2324
+ ports
2325
+ });
1832
2326
  run.files = await writeStack(ctx.cwd, run.dir, stack);
1833
2327
  printPlan(ctx, run);
2328
+ if (stack.proxy === "traefik") {
2329
+ const dir = edgeDir(deps.env);
2330
+ ctx.logger.info(`\n🌐 Starting the shared edge (${EDGE_PROJECT}) from ${dir}…`);
2331
+ run.files.push(...await ensureEdge(deps.docker, {
2332
+ dir,
2333
+ ports,
2334
+ logging: {
2335
+ driver: LOG_ROTATION.driver,
2336
+ options: { ...LOG_ROTATION.options }
2337
+ },
2338
+ ...ref.output ? { output: ref.output } : {},
2339
+ ...ref.signal ? { signal: ref.signal } : {}
2340
+ }));
2341
+ applied(ctx, run, {
2342
+ key: `service:${EDGE_PROJECT}/${EDGE_SERVICE}`,
2343
+ resourceType: "service",
2344
+ action: "ensure",
2345
+ id: `${EDGE_PROJECT}/${EDGE_SERVICE}`,
2346
+ via: "found"
2347
+ });
2348
+ }
1834
2349
  if (stack.infra.length > 0) {
1835
2350
  ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
1836
2351
  await deps.docker.up(ref, stack.infra);
@@ -1855,7 +2370,7 @@ async function prepareBuckets(ctx, run, deps) {
1855
2370
  if (storage.buckets.length === 0) return;
1856
2371
  const port = await deps.docker.port(run.ref, "minio", 9e3);
1857
2372
  const client = deps.buckets(port, storage);
1858
- const created = [...await require_state.applyBuckets(client, storage.buckets), ...await require_state.applyPolicies(client, storage.policies)].filter((entry) => entry.created).length;
2373
+ const created = [...await require_stale.applyBuckets(client, storage.buckets), ...await require_stale.applyPolicies(client, storage.policies)].filter((entry) => entry.created).length;
1859
2374
  ctx.logger.info(`🪣 Buckets: ${storage.buckets.join(", ")}${created ? ` (${created} change(s))` : ""}`);
1860
2375
  }
1861
2376
  /** Create what the plan names in the stack's Postgres, then migrate it. */
@@ -1863,19 +2378,19 @@ async function prepareDatabases(ctx, run, deps) {
1863
2378
  const { stack } = run;
1864
2379
  const workspace = ctx.workspace;
1865
2380
  const port = await deps.docker.port(run.ref, "postgres", 5432);
1866
- const statements = require_state.postgresStatements(stack.plan, workspace.name, stack.credential.seed);
2381
+ const statements = require_stale.postgresStatements(stack.plan, workspace.name, stack.credential.seed);
1867
2382
  if (statements.length > 0) {
1868
2383
  ctx.logger.info("🗄️ Creating databases, roles and grants…");
1869
- await require_state.applyPostgres(deps.sql(port, stack.credential.master), statements);
2384
+ await require_stale.applyPostgres(deps.sql(port, stack.credential.master), statements);
1870
2385
  }
1871
2386
  const sources = {};
1872
2387
  const manifest = await require_discover.discover({
1873
- patterns: require_state.constructGlobs(workspace),
2388
+ patterns: require_stale.constructGlobs(workspace),
1874
2389
  cwd: workspace.root,
1875
2390
  sources
1876
2391
  });
1877
- const env = require_state.envFor(stack.plan, {
1878
- ports: { [require_state.primaryPortKey("postgres")]: port },
2392
+ const env = require_stale.envFor(stack.plan, {
2393
+ ports: { [require_stale.primaryPortKey("postgres")]: port },
1879
2394
  project: workspace.name,
1880
2395
  master: stack.credential.master,
1881
2396
  ...stack.credential.seed ? { seed: stack.credential.seed } : {}
@@ -1940,6 +2455,12 @@ async function releaseCompose(ctx, run, deps) {
1940
2455
  if (run.push) return;
1941
2456
  ctx.logger.info("\n🚀 Starting the stack…");
1942
2457
  await deps.docker.up(ref);
2458
+ const edge = edgeDir(deps.env);
2459
+ if (stack.proxy === "traefik" && stack.traefik) {
2460
+ const written = await writeEdgeRoutes(edge, stack.project, stack.traefik, stack.tls);
2461
+ run.files.push(...written);
2462
+ ctx.logger.info(`🌐 Registered ${stack.routes.length} route(s) with ${EDGE_PROJECT}: ${written.at(-1)}`);
2463
+ } else await removeEdgeRoutes(edge, stack.project);
1943
2464
  if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
1944
2465
  await recordImages(ctx, run);
1945
2466
  for (const app of stack.apps) {
@@ -1977,13 +2498,13 @@ async function releaseCompose(ctx, run, deps) {
1977
2498
  * the tag resolved to.
1978
2499
  */
1979
2500
  async function recordImages(ctx, run) {
1980
- const journal = await require_state.DeployJournal.open(ctx.state, ctx.stage, () => require_state.createEmptyState(ctx.stage, "", ""));
2501
+ const journal = await require_stale.DeployJournal.open(ctx.state, ctx.stage, () => require_stale.createEmptyState(ctx.stage, "", ""));
1981
2502
  for (const app of [...run.stack.apps, ...run.stack.workers]) {
1982
2503
  const image = run.images[app.app] ?? {
1983
2504
  ref: app.ref,
1984
2505
  tag: app.tag
1985
2506
  };
1986
- require_state.recordRelease(journal.state, app.app, image);
2507
+ require_stale.recordRelease(journal.state, app.app, image);
1987
2508
  }
1988
2509
  journal.state.identity = ctx.identity.key;
1989
2510
  await journal.save();
@@ -1998,10 +2519,14 @@ async function verifyCompose(ctx, run, deps) {
1998
2519
  const { stack } = run;
1999
2520
  if (run.push) return;
2000
2521
  const ca = stack.local ? await (0, node_fs_promises.readFile)(caFile(run), "utf-8").catch(() => void 0) : void 0;
2001
- ctx.logger.info("\n🩺 Checking each app through Caddy…");
2522
+ ctx.logger.info(`\n🩺 Checking each app through ${stack.proxy === "caddy" ? "Caddy" : "the shared edge"}…`);
2523
+ const edge = stack.local || stack.proxy === "traefik" ? {
2524
+ connectTo: "127.0.0.1",
2525
+ ...stack.local ? {} : { connectPort: edgePorts(deps.env).https }
2526
+ } : void 0;
2002
2527
  const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
2003
2528
  ca,
2004
- local: stack.local
2529
+ edge
2005
2530
  })));
2006
2531
  results.push(...await Promise.all(stack.workers.map((worker) => checkContainer(ctx, run, deps, worker.name))));
2007
2532
  if (stack.logs) results.push(await checkContainer(ctx, run, deps, LOGS_SERVICE));
@@ -2010,7 +2535,7 @@ async function verifyCompose(ctx, run, deps) {
2010
2535
  url,
2011
2536
  last
2012
2537
  }));
2013
- if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down);
2538
+ if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down, stack.proxy);
2014
2539
  if (stack.logs) reportLogs(ctx, stack.logs, stack.local);
2015
2540
  }
2016
2541
  /**
@@ -2089,7 +2614,7 @@ async function checkApp(ctx, app, deps, options) {
2089
2614
  status = await deps.probe({
2090
2615
  url,
2091
2616
  ...options.ca ? { ca: options.ca } : {},
2092
- ...options.local ? { connectTo: "127.0.0.1" } : {},
2617
+ ...options.edge,
2093
2618
  timeoutMs: 1e4,
2094
2619
  signal: ctx.signal
2095
2620
  });
@@ -2190,19 +2715,34 @@ async function writeStack(root, dir, stack) {
2190
2715
  files.push(path);
2191
2716
  };
2192
2717
  await write((0, node_path.join)(dir, "docker-compose.yml"), composeYaml(stack));
2193
- await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
2718
+ if (stack.caddyfile !== void 0) {
2719
+ await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
2720
+ await (0, node_fs_promises.rm)((0, node_path.join)(dir, "traefik.yml"), { force: true });
2721
+ } else await (0, node_fs_promises.rm)((0, node_path.join)(dir, "Caddyfile"), { force: true });
2722
+ if (stack.traefik !== void 0) await write((0, node_path.join)(dir, "traefik.yml"), stack.traefik);
2723
+ if (stack.proxy === "caddy" && stack.tls) {
2724
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "tls"), {
2725
+ recursive: true,
2726
+ mode: 448
2727
+ });
2728
+ await write((0, node_path.join)(dir, "tls", "cert.pem"), await (0, node_fs_promises.readFile)(stack.tls.certFile, "utf-8"));
2729
+ await write((0, node_path.join)(dir, "tls", "key.pem"), await (0, node_fs_promises.readFile)(stack.tls.keyFile, "utf-8"), 384);
2730
+ } else await (0, node_fs_promises.rm)((0, node_path.join)(dir, "tls"), {
2731
+ recursive: true,
2732
+ force: true
2733
+ });
2194
2734
  for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write((0, node_path.join)(dir, `${app.name}.env`), envFile(app.env), 384);
2195
2735
  if (stack.logs && stack.compose.services["openobserve"]?.env_file) await write((0, node_path.join)(dir, `${LOGS_SERVICE}.env`), envFile(stack.logs.env), 384);
2196
2736
  if (stack.redis && stack.compose.services["redis"]?.env_file) await write((0, node_path.join)(dir, `${REDIS_SERVICE}.env`), envFile(stack.redis.env), 384);
2197
2737
  for (const entry of await (0, node_fs_promises.readdir)(dir)) if (entry.endsWith(".credentials")) await (0, node_fs_promises.rm)((0, node_path.join)(dir, entry));
2198
2738
  for (const [path, content] of Object.entries(stack.dockerfiles)) await write((0, node_path.join)(root, path), content);
2199
- await require_state.ensureDockerignore(require_state.findBuildRoot(root));
2739
+ await require_stale.ensureDockerignore(require_stale.findBuildRoot(root));
2200
2740
  return files;
2201
2741
  }
2202
2742
  function composeYaml(stack) {
2203
2743
  const stage = ` --stage ${stack.stage}`;
2204
2744
  return `# Generated by gkm compose from the construct manifest — do not edit.
2205
- # The ${stack.stage} stage's APIs and sites behind one Caddy, and its workers.
2745
+ # The ${stack.stage} stage's APIs and sites behind ${stack.proxy === "caddy" ? "one Caddy" : `the shared Traefik edge (${EDGE_PROJECT})`}, and its workers.
2206
2746
  # Each backend and worker reads exactly the keys in its own env file beside
2207
2747
  # this one.
2208
2748
  #
@@ -2298,6 +2838,12 @@ Object.defineProperty(exports, "ComposePushNeedsBuild", {
2298
2838
  return ComposePushNeedsBuild;
2299
2839
  }
2300
2840
  });
2841
+ Object.defineProperty(exports, "EDGE_PROJECT", {
2842
+ enumerable: true,
2843
+ get: function() {
2844
+ return EDGE_PROJECT;
2845
+ }
2846
+ });
2301
2847
  Object.defineProperty(exports, "composeProject", {
2302
2848
  enumerable: true,
2303
2849
  get: function() {
@@ -2322,6 +2868,12 @@ Object.defineProperty(exports, "dockerCompose", {
2322
2868
  return dockerCompose;
2323
2869
  }
2324
2870
  });
2871
+ Object.defineProperty(exports, "edgeDir", {
2872
+ enumerable: true,
2873
+ get: function() {
2874
+ return edgeDir;
2875
+ }
2876
+ });
2325
2877
  Object.defineProperty(exports, "parseDigests", {
2326
2878
  enumerable: true,
2327
2879
  get: function() {
@@ -2334,6 +2886,12 @@ Object.defineProperty(exports, "pinnedRef", {
2334
2886
  return pinnedRef;
2335
2887
  }
2336
2888
  });
2889
+ Object.defineProperty(exports, "removeEdgeRoutes", {
2890
+ enumerable: true,
2891
+ get: function() {
2892
+ return removeEdgeRoutes;
2893
+ }
2894
+ });
2337
2895
  Object.defineProperty(exports, "stackDir", {
2338
2896
  enumerable: true,
2339
2897
  get: function() {
@@ -2347,4 +2905,4 @@ Object.defineProperty(exports, "stackOverrideFile", {
2347
2905
  }
2348
2906
  });
2349
2907
 
2350
- //# sourceMappingURL=compose-h_6oNnHC.cjs.map
2908
+ //# sourceMappingURL=compose-B7MC19eC.cjs.map