@geekmidas/cli 10.0.0-alpha.84 → 10.0.0-alpha.86

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 (177) hide show
  1. package/dist/{HostingerProvider-dxkwynSs.cjs → HostingerProvider-BCB1QJj1.cjs} +2 -2
  2. package/dist/{HostingerProvider-dxkwynSs.cjs.map → HostingerProvider-BCB1QJj1.cjs.map} +1 -1
  3. package/dist/{HostingerProvider-D85ayzTi.mjs → HostingerProvider-CVc3rLPB.mjs} +2 -2
  4. package/dist/{HostingerProvider-D85ayzTi.mjs.map → HostingerProvider-CVc3rLPB.mjs.map} +1 -1
  5. package/dist/{LegacyStateStore-CTsUFLNQ.mjs → LegacyStateStore-B0R4LlX_.mjs} +2 -2
  6. package/dist/{LegacyStateStore-CTsUFLNQ.mjs.map → LegacyStateStore-B0R4LlX_.mjs.map} +1 -1
  7. package/dist/{LegacyStateStore-EDT2TUSe.cjs → LegacyStateStore-CWG_HVC0.cjs} +7 -7
  8. package/dist/{LegacyStateStore-EDT2TUSe.cjs.map → LegacyStateStore-CWG_HVC0.cjs.map} +1 -1
  9. package/dist/{LocalStateStore-CdpzTvPn.cjs → LocalStateStore-CLwa2DpK.cjs} +16 -14
  10. package/dist/LocalStateStore-CLwa2DpK.cjs.map +1 -0
  11. package/dist/{LocalStateStore-D1VfxRuK.mjs → LocalStateStore-Q4vXg2ua.mjs} +12 -10
  12. package/dist/LocalStateStore-Q4vXg2ua.mjs.map +1 -0
  13. package/dist/{S3StateStore-BPM74zwv.cjs → S3StateStore-Cea9_5gF.cjs} +5 -5
  14. package/dist/{S3StateStore-BPM74zwv.cjs.map → S3StateStore-Cea9_5gF.cjs.map} +1 -1
  15. package/dist/{S3StateStore-DVZ1YuGJ.mjs → S3StateStore-CksKYKRa.mjs} +2 -2
  16. package/dist/{S3StateStore-DVZ1YuGJ.mjs.map → S3StateStore-CksKYKRa.mjs.map} +1 -1
  17. package/dist/{SSMStateStore-DLvnGyyl.mjs → SSMStateStore-BszSCAS2.mjs} +2 -2
  18. package/dist/{SSMStateStore-DLvnGyyl.mjs.map → SSMStateStore-BszSCAS2.mjs.map} +1 -1
  19. package/dist/{SSMStateStore-qerm5c36.cjs → SSMStateStore-aBOTF018.cjs} +9 -9
  20. package/dist/{SSMStateStore-qerm5c36.cjs.map → SSMStateStore-aBOTF018.cjs.map} +1 -1
  21. package/dist/{add-BQ8v1ZII.cjs → add-5MZAXybv.cjs} +8 -5
  22. package/dist/add-5MZAXybv.cjs.map +1 -0
  23. package/dist/{add-DbnzC7Kl.mjs → add-D_jNqnfZ.mjs} +8 -5
  24. package/dist/add-D_jNqnfZ.mjs.map +1 -0
  25. package/dist/{bundler-DjrRffko.cjs → bundler-1REMhKps.cjs} +5 -5
  26. package/dist/{bundler-DjrRffko.cjs.map → bundler-1REMhKps.cjs.map} +1 -1
  27. package/dist/{bundler-D4030Afw.mjs → bundler-BMo-JP5s.mjs} +5 -5
  28. package/dist/{bundler-D4030Afw.mjs.map → bundler-BMo-JP5s.mjs.map} +1 -1
  29. package/dist/{cli-DwTa9d_8.mjs → cli-7edNFwGk.mjs} +5 -13
  30. package/dist/cli-7edNFwGk.mjs.map +1 -0
  31. package/dist/{cli-D8YQAYUX.cjs → cli-BL8ZHsLQ.cjs} +5 -13
  32. package/dist/cli-BL8ZHsLQ.cjs.map +1 -0
  33. package/dist/{compose-C5wz8mf9.mjs → compose-DbJhRQlh.mjs} +46 -15
  34. package/dist/compose-DbJhRQlh.mjs.map +1 -0
  35. package/dist/{compose-DqWh1TaO.cjs → compose-xw86kVYi.cjs} +55 -24
  36. package/dist/compose-xw86kVYi.cjs.map +1 -0
  37. package/dist/{config-BzIfP8a-.d.cts → config-CpjsVxru.d.cts} +3 -3
  38. package/dist/{config-BzIfP8a-.d.cts.map → config-CpjsVxru.d.cts.map} +1 -1
  39. package/dist/{config-DhdzmndX.d.mts → config-DrCAyoH5.d.mts} +3 -3
  40. package/dist/{config-DhdzmndX.d.mts.map → config-DrCAyoH5.d.mts.map} +1 -1
  41. package/dist/config.cjs +1 -1
  42. package/dist/config.d.cts +3 -3
  43. package/dist/config.d.mts +3 -3
  44. package/dist/config.mjs +1 -1
  45. package/dist/{credentials-pAtB70k3.mjs → credentials-BgJ5mS_d.mjs} +91 -181
  46. package/dist/credentials-BgJ5mS_d.mjs.map +1 -0
  47. package/dist/{credentials-BVs7mrtw.cjs → credentials-tr26-KML.cjs} +122 -188
  48. package/dist/credentials-tr26-KML.cjs.map +1 -0
  49. package/dist/deploy/public.cjs +24 -20
  50. package/dist/deploy/public.cjs.map +1 -1
  51. package/dist/deploy/public.d.cts +95 -13
  52. package/dist/deploy/public.d.cts.map +1 -1
  53. package/dist/deploy/public.d.mts +95 -13
  54. package/dist/deploy/public.d.mts.map +1 -1
  55. package/dist/deploy/public.mjs +11 -10
  56. package/dist/deploy/public.mjs.map +1 -1
  57. package/dist/{deploy-BwIKGxWH.mjs → deploy-BTfmXd3g.mjs} +9 -9
  58. package/dist/deploy-BTfmXd3g.mjs.map +1 -0
  59. package/dist/{deploy-C8sWg81_.cjs → deploy-CtbZj4zN.cjs} +13 -13
  60. package/dist/deploy-CtbZj4zN.cjs.map +1 -0
  61. package/dist/{dokploy-BkRxJbfi.cjs → dokploy-TGk3oPwr.cjs} +2 -2
  62. package/dist/{dokploy-BkRxJbfi.cjs.map → dokploy-TGk3oPwr.cjs.map} +1 -1
  63. package/dist/{dokploy-ijjms1-h.mjs → dokploy-r69yZaIt.mjs} +2 -2
  64. package/dist/{dokploy-ijjms1-h.mjs.map → dokploy-r69yZaIt.mjs.map} +1 -1
  65. package/dist/{encryption-BRcBAmjL.mjs → encryption-C-8Tt5Hh.mjs} +3 -3
  66. package/dist/{encryption-BRcBAmjL.mjs.map → encryption-C-8Tt5Hh.mjs.map} +1 -1
  67. package/dist/{encryption-g6PzwwfL.cjs → encryption-CDn7BH5O.cjs} +3 -3
  68. package/dist/{encryption-g6PzwwfL.cjs.map → encryption-CDn7BH5O.cjs.map} +1 -1
  69. package/dist/{engine-DLEJsZ3i.cjs → engine-Bt5k_UMq.cjs} +33 -23
  70. package/dist/engine-Bt5k_UMq.cjs.map +1 -0
  71. package/dist/{engine-BP0CZ5fh.mjs → engine-jAINgiOB.mjs} +28 -18
  72. package/dist/engine-jAINgiOB.mjs.map +1 -0
  73. package/dist/{github-Zs-dVJbR.mjs → github-CZHAtwPT.mjs} +3 -3
  74. package/dist/{github-Zs-dVJbR.mjs.map → github-CZHAtwPT.mjs.map} +1 -1
  75. package/dist/{github-B8YjwdLK.cjs → github-DFhuJQlp.cjs} +3 -3
  76. package/dist/{github-B8YjwdLK.cjs.map → github-DFhuJQlp.cjs.map} +1 -1
  77. package/dist/{index-f77HdqN5.d.mts → index-MnUJuUlj.d.mts} +2 -2
  78. package/dist/{index-D1ODeM9s.d.cts.map → index-MnUJuUlj.d.mts.map} +1 -1
  79. package/dist/{index-D1ODeM9s.d.cts → index-elsLJTUv.d.cts} +2 -2
  80. package/dist/{index-f77HdqN5.d.mts.map → index-elsLJTUv.d.cts.map} +1 -1
  81. package/dist/index.cjs +105 -67
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.mjs +99 -61
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/local-Ciq-bTPr.mjs +224 -0
  86. package/dist/local-Ciq-bTPr.mjs.map +1 -0
  87. package/dist/local-Cq8qdgNX.cjs +241 -0
  88. package/dist/local-Cq8qdgNX.cjs.map +1 -0
  89. package/dist/{migrate-FoL_mZIz.cjs → migrate-B16qgYTn.cjs} +3 -3
  90. package/dist/{migrate-FoL_mZIz.cjs.map → migrate-B16qgYTn.cjs.map} +1 -1
  91. package/dist/{migrate-BfZN1S32.cjs → migrate-B1rVlwj1.cjs} +3 -3
  92. package/dist/{migrate-BfZN1S32.cjs.map → migrate-B1rVlwj1.cjs.map} +1 -1
  93. package/dist/{migrate-DOMU4uKe.mjs → migrate-Dn7nK6sh.mjs} +3 -3
  94. package/dist/{migrate-DOMU4uKe.mjs.map → migrate-Dn7nK6sh.mjs.map} +1 -1
  95. package/dist/{migrate-CdXcX0XR.mjs → migrate-ggkPVJRC.mjs} +3 -3
  96. package/dist/{migrate-CdXcX0XR.mjs.map → migrate-ggkPVJRC.mjs.map} +1 -1
  97. package/dist/openapi.cjs +1 -1
  98. package/dist/openapi.d.cts +1 -1
  99. package/dist/openapi.d.mts +1 -1
  100. package/dist/openapi.mjs +1 -1
  101. package/dist/reconcile/public.d.cts +1 -1
  102. package/dist/reconcile/public.d.mts +1 -1
  103. package/dist/{resolve-r7PAs6ng.mjs → resolve-BSke45k6.mjs} +4 -4
  104. package/dist/{resolve-r7PAs6ng.mjs.map → resolve-BSke45k6.mjs.map} +1 -1
  105. package/dist/{resolve-T2I-5FcT.cjs → resolve-BXeXzpmU.cjs} +4 -4
  106. package/dist/{resolve-T2I-5FcT.cjs.map → resolve-BXeXzpmU.cjs.map} +1 -1
  107. package/dist/{rollback-BSdxnW3X.mjs → rollback-CEE6dse4.mjs} +6 -6
  108. package/dist/{rollback-BSdxnW3X.mjs.map → rollback-CEE6dse4.mjs.map} +1 -1
  109. package/dist/{rollback-CPSDjGse.cjs → rollback-DFMiBWf0.cjs} +8 -8
  110. package/dist/{rollback-CPSDjGse.cjs.map → rollback-DFMiBWf0.cjs.map} +1 -1
  111. package/dist/{runtime-CNu2C2hB.d.mts → runtime-D48ZKk8_.d.cts} +2 -2
  112. package/dist/{runtime-CNu2C2hB.d.mts.map → runtime-D48ZKk8_.d.cts.map} +1 -1
  113. package/dist/{runtime-DMLgL26v.d.cts → runtime-HabiNBtJ.d.mts} +2 -2
  114. package/dist/{runtime-DMLgL26v.d.cts.map → runtime-HabiNBtJ.d.mts.map} +1 -1
  115. package/dist/{seeds-BK8caVMm.mjs → seeds-BYdrtZsC.mjs} +1554 -54
  116. package/dist/seeds-BYdrtZsC.mjs.map +1 -0
  117. package/dist/{seeds-DQ0FG57Z.cjs → seeds-CsI0A_Ef.cjs} +1656 -65
  118. package/dist/seeds-CsI0A_Ef.cjs.map +1 -0
  119. package/dist/{sniffer-CqE_KTTp.mjs → sniffer-B0EU73bj.mjs} +5 -360
  120. package/dist/sniffer-B0EU73bj.mjs.map +1 -0
  121. package/dist/{sniffer-hSf5mRgV.cjs → sniffer-DJYupBeK.cjs} +4 -413
  122. package/dist/sniffer-DJYupBeK.cjs.map +1 -0
  123. package/dist/{sst-DRi3H5TB.mjs → sst-D1Qj4sIs.mjs} +3 -3
  124. package/dist/{sst-DRi3H5TB.mjs.map → sst-D1Qj4sIs.mjs.map} +1 -1
  125. package/dist/{sst-hBrKouBy.cjs → sst-DAqUoZKG.cjs} +5 -5
  126. package/dist/{sst-hBrKouBy.cjs.map → sst-DAqUoZKG.cjs.map} +1 -1
  127. package/dist/{stages-CS1sjcRb.cjs → stages-Brn3vcdy.cjs} +2 -2
  128. package/dist/{stages-CS1sjcRb.cjs.map → stages-Brn3vcdy.cjs.map} +1 -1
  129. package/dist/{stages-CSXt7N2b.mjs → stages-DoHHz6pS.mjs} +2 -2
  130. package/dist/{stages-CSXt7N2b.mjs.map → stages-DoHHz6pS.mjs.map} +1 -1
  131. package/dist/target/public.cjs +1 -1
  132. package/dist/target/public.d.cts +2 -2
  133. package/dist/target/public.d.mts +2 -2
  134. package/dist/target/public.mjs +1 -1
  135. package/dist/{terminal-BdvrmkaC.mjs → terminal-BTbZlrFV.mjs} +2 -2
  136. package/dist/{terminal-BdvrmkaC.mjs.map → terminal-BTbZlrFV.mjs.map} +1 -1
  137. package/dist/{terminal-BL5E0xbR.cjs → terminal-COv23heb.cjs} +2 -2
  138. package/dist/{terminal-BL5E0xbR.cjs.map → terminal-COv23heb.cjs.map} +1 -1
  139. package/dist/{types-BZn-8ssD.d.mts → types-0oAyFOfR.d.cts} +48 -49
  140. package/dist/types-0oAyFOfR.d.cts.map +1 -0
  141. package/dist/{types-DiZj6hqp.d.cts → types-BZ01tZs-.d.mts} +47 -50
  142. package/dist/types-BZ01tZs-.d.mts.map +1 -0
  143. package/dist/workspace/index.cjs +1 -1
  144. package/dist/workspace/index.d.cts +2 -2
  145. package/dist/workspace/index.d.mts +2 -2
  146. package/dist/workspace/index.mjs +1 -1
  147. package/dist/{workspace-Bic_R3tz.cjs → workspace-CT0JM3Y8.cjs} +333 -1
  148. package/dist/workspace-CT0JM3Y8.cjs.map +1 -0
  149. package/dist/{workspace-Drz4jsvy.mjs → workspace-D2CYLMCW.mjs} +250 -2
  150. package/dist/workspace-D2CYLMCW.mjs.map +1 -0
  151. package/package.json +15 -15
  152. package/dist/LocalStateStore-CdpzTvPn.cjs.map +0 -1
  153. package/dist/LocalStateStore-D1VfxRuK.mjs.map +0 -1
  154. package/dist/add-BQ8v1ZII.cjs.map +0 -1
  155. package/dist/add-DbnzC7Kl.mjs.map +0 -1
  156. package/dist/cli-D8YQAYUX.cjs.map +0 -1
  157. package/dist/cli-DwTa9d_8.mjs.map +0 -1
  158. package/dist/compose-C5wz8mf9.mjs.map +0 -1
  159. package/dist/compose-DqWh1TaO.cjs.map +0 -1
  160. package/dist/credentials-BVs7mrtw.cjs.map +0 -1
  161. package/dist/credentials-pAtB70k3.mjs.map +0 -1
  162. package/dist/deploy-BwIKGxWH.mjs.map +0 -1
  163. package/dist/deploy-C8sWg81_.cjs.map +0 -1
  164. package/dist/engine-BP0CZ5fh.mjs.map +0 -1
  165. package/dist/engine-DLEJsZ3i.cjs.map +0 -1
  166. package/dist/run-BJLIerMM.mjs +0 -133
  167. package/dist/run-BJLIerMM.mjs.map +0 -1
  168. package/dist/run-fVZ-rLnc.cjs +0 -174
  169. package/dist/run-fVZ-rLnc.cjs.map +0 -1
  170. package/dist/seeds-BK8caVMm.mjs.map +0 -1
  171. package/dist/seeds-DQ0FG57Z.cjs.map +0 -1
  172. package/dist/sniffer-CqE_KTTp.mjs.map +0 -1
  173. package/dist/sniffer-hSf5mRgV.cjs.map +0 -1
  174. package/dist/types-BZn-8ssD.d.mts.map +0 -1
  175. package/dist/types-DiZj6hqp.d.cts.map +0 -1
  176. package/dist/workspace-Bic_R3tz.cjs.map +0 -1
  177. package/dist/workspace-Drz4jsvy.mjs.map +0 -1
@@ -1,12 +1,12 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
- const require_run = require("./run-fVZ-rLnc.cjs");
2
+ const require_credentials = require("./credentials-tr26-KML.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-Bic_R3tz.cjs");
5
+ const require_workspace = require("./workspace-CT0JM3Y8.cjs");
6
6
  const require_config = require("./config.cjs");
7
7
  const require_EndpointGenerator = require("./EndpointGenerator-CwLjBCFE.cjs");
8
8
  const require_SubscriberGenerator = require("./SubscriberGenerator-BVn7cQ-2.cjs");
9
- const require_resolve = require("./resolve-T2I-5FcT.cjs");
9
+ const require_resolve = require("./resolve-BXeXzpmU.cjs");
10
10
  let node_path = require("node:path");
11
11
  let node_fs = require("node:fs");
12
12
  let node_fs_promises = require("node:fs/promises");
@@ -31,6 +31,8 @@ let _aws_sdk_client_sns = require("@aws-sdk/client-sns");
31
31
  let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
32
32
  let pg = require("pg");
33
33
  let _geekmidas_cache_postgres = require("@geekmidas/cache/postgres");
34
+ let _geekmidas_storage_s3_url = require("@geekmidas/storage/s3-url");
35
+ _geekmidas_storage_s3_url = require_rolldown_runtime.__toESM(_geekmidas_storage_s3_url, 1);
34
36
  //#region src/workspace/rootSite.ts
35
37
  /**
36
38
  * Which site the base domain points at — deployed and locally alike.
@@ -602,7 +604,7 @@ function folderName(path) {
602
604
  function getKeystoreDir(project) {
603
605
  if (!PROJECT_KEY.test(project.key)) throw new KeystoreProjectInvalid(project.key);
604
606
  const [namespace, name] = project.key.split("/");
605
- return (0, node_path.join)(project.home ?? require_run.gkmHome(), "keys", namespace, name);
607
+ return (0, node_path.join)(project.home ?? require_credentials.gkmHome(), "keys", namespace, name);
606
608
  }
607
609
  /** Get the path to a stage's encryption key. */
608
610
  function getKeyPath(stage, project) {
@@ -619,7 +621,7 @@ function legacyKeyPaths(stage, project) {
619
621
  key,
620
622
  home: project.home
621
623
  }));
622
- const homes = [.../* @__PURE__ */ new Set([project.home ?? require_run.gkmHome(), (0, node_path.join)((0, node_os.homedir)(), ".gkm")])];
624
+ const homes = [.../* @__PURE__ */ new Set([project.home ?? require_credentials.gkmHome(), (0, node_path.join)((0, node_os.homedir)(), ".gkm")])];
623
625
  const names = (project.legacy ?? []).filter(isLegacyName);
624
626
  return [...aliases, ...names.flatMap((name) => homes.map((home) => (0, node_path.join)(home, name, `${stage}.key`)))];
625
627
  }
@@ -1371,6 +1373,30 @@ function siteClientTelemetry(manifest, appName, workspace, stage) {
1371
1373
  //#endregion
1372
1374
  //#region src/reconcile/caddyfile.ts
1373
1375
  /**
1376
+ * The Caddy configuration a plan implies.
1377
+ *
1378
+ * A file server is a *domain that serves a bucket*, and locally it had neither:
1379
+ * `UPLOADS_SERVER_URL` resolved to MinIO's path-style address, which works and
1380
+ * is not the deployed shape. MinIO's own virtual-host mode cannot fix that —
1381
+ * it reads the leading label *as the bucket name*, so it produces the right
1382
+ * shape only when the server's id and the bucket's happen to agree, and never
1383
+ * for a server fronting two buckets.
1384
+ *
1385
+ * So the mapping lives in front of MinIO instead: one host per file server,
1386
+ * rewriting the bucket in as a path prefix. That is the whole of it — no
1387
+ * caching, because caching is not what is missing locally, and a caching proxy
1388
+ * would add an invalidation story a local stack has no use for.
1389
+ *
1390
+ * What it buys beyond the shape is a certificate. Caddy issues one per host
1391
+ * from its own CA, so a local address is `https://` like the deployed one, and
1392
+ * everything downstream of that — `Secure` cookies, `SameSite`, a cookie domain
1393
+ * with more than one host under it — starts behaving the way it will in
1394
+ * production rather than the way `http://localhost` allows.
1395
+ *
1396
+ * Pure, like `composeFor`: what should exist is data, and rendering it is one
1397
+ * line at the edge.
1398
+ */
1399
+ /**
1374
1400
  * The kinds the edge serves.
1375
1401
  *
1376
1402
  * Everything that owns a *public address*. Deployed, each of these has a real
@@ -1517,6 +1543,10 @@ function toCaddyfile(sites) {
1517
1543
  https://${host} {
1518
1544
  tls internal
1519
1545
 
1546
+ # gkm's own header, which only a service of this stack may send: the auth
1547
+ # server rate-limits by the client address it carries.
1548
+ request_header -${_geekmidas_manifest.CLIENT_IP_HEADER}
1549
+
1520
1550
  reverse_proxy ${upstream} {${rewrite ? `
1521
1551
  rewrite ${rewrite}
1522
1552
  # MinIO routes and signs on the Host header, so forwarding the requested
@@ -4849,7 +4879,7 @@ function bucketPolicies(plan) {
4849
4879
  * client, can be admitted by the policy where the client would have said no.
4850
4880
  * Prefer `**` where crossing segments is what you meant.
4851
4881
  */
4852
- function bucketPolicy(bucket, open) {
4882
+ function bucketPolicy$1(bucket, open) {
4853
4883
  return JSON.stringify({
4854
4884
  Version: "2012-10-17",
4855
4885
  Statement: [{
@@ -4931,7 +4961,7 @@ async function databasesExist(client, names) {
4931
4961
  async function applyPolicies(client, policies) {
4932
4962
  const applied = [];
4933
4963
  for (const { bucket, open } of policies) {
4934
- const wanted = bucketPolicy(bucket, open);
4964
+ const wanted = bucketPolicy$1(bucket, open);
4935
4965
  const describe = `open paths on ${bucket}`;
4936
4966
  if (equivalent(await client.policy(bucket), wanted)) {
4937
4967
  applied.push({
@@ -5550,7 +5580,7 @@ function localCredentialsPath(project) {
5550
5580
  const keys = getKeystoreDir(project);
5551
5581
  const name = keys.split(/[\\/]/).at(-1);
5552
5582
  const namespace = keys.split(/[\\/]/).at(-2);
5553
- return (0, node_path.join)(project.home ?? require_run.gkmHome(), "local", namespace, `${name}.json`);
5583
+ return (0, node_path.join)(project.home ?? require_credentials.gkmHome(), "local", namespace, `${name}.json`);
5554
5584
  }
5555
5585
  /** The local stage's logins file is there, and its key is not. */
5556
5586
  var LocalCredentialsKeyMissing = class extends Error {
@@ -6579,15 +6609,21 @@ function isUnder(file, dir) {
6579
6609
  * chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
6580
6610
  * `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
6581
6611
  *
6582
- * `--allow-dev-services minio,mailpit` is the way out for a stage that is
6583
- * not production — a preview, a demo: the target runs the dev service and
6584
- * derives the keys from it. Keys the stage did set still win. Every run that
6585
- * uses one says so, loudly.
6612
+ * `--allow-dev-services` is the way out for a stage that is not production —
6613
+ * a preview, a demo: every construct the stage does not account for gets the
6614
+ * dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
6615
+ * and the target derives the keys from it. What the stage does account for
6616
+ * still wins: a key set in its secrets, or a provider (`deploy.objects`)
6617
+ * that backs the kind. Every run that uses one says so, loudly.
6586
6618
  *
6587
6619
  * Pure: what each target runs is decided here from declarations and the
6588
6620
  * stage's values, and the targets do the running.
6589
6621
  */
6590
- /** The dev services a deployed stage may be allowed to run. */
6622
+ /**
6623
+ * The dev services a deployed stage can run — one per kind that has one on
6624
+ * every server target: a bucket's MinIO and mail's Mailpit. A cache or a
6625
+ * database is not here because a server stage runs its own anyway.
6626
+ */
6591
6627
  const DEV_SERVICES = ["minio", "mailpit"];
6592
6628
  /**
6593
6629
  * The S3 client's shared key pair. Optional: a bucket whose URL carries its own
@@ -6596,27 +6632,45 @@ const DEV_SERVICES = ["minio", "mailpit"];
6596
6632
  * chain finds — a role.
6597
6633
  */
6598
6634
  const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
6599
- /** A `--allow-dev-services` value that is not a dev service. */
6600
- var UnknownDevService = class extends Error {
6601
- value;
6602
- constructor(value) {
6603
- super(`'${value}' is not a dev service. --allow-dev-services takes a comma-separated list of ${DEV_SERVICES.join(", ")}: --allow-dev-services ${DEV_SERVICES.join(",")}`);
6604
- this.value = value;
6605
- this.name = "UnknownDevService";
6606
- }
6607
- };
6608
6635
  /** Dev services asked of a target that runs no containers. */
6609
6636
  var DevServicesNeedServerTarget = class extends Error {
6610
6637
  target;
6611
- services;
6612
- constructor(target, services) {
6613
- super(`--allow-dev-services applies to server targets only, and '${target}' deploys to AWS, where a bucket is S3 and mail is the stage's own. Drop --allow-dev-services ${services.join(",")}.`);
6638
+ constructor(target) {
6639
+ super(`--allow-dev-services applies to server targets only, and '${target}' deploys to AWS, where a bucket is S3 and mail is the stage's own. Drop --allow-dev-services.`);
6614
6640
  this.target = target;
6615
- this.services = services;
6616
6641
  this.name = "DevServicesNeedServerTarget";
6617
6642
  }
6618
6643
  };
6619
6644
  /**
6645
+ * `--allow-dev-services` given a value — the list it used to take. It is a
6646
+ * switch now: every construct the stage does not account for gets its dev
6647
+ * service.
6648
+ */
6649
+ var AllowDevServicesTakesNoValue = class extends Error {
6650
+ value;
6651
+ constructor(value) {
6652
+ super(`--allow-dev-services takes no value (it was given '${value}'). It runs the dev service for every bucket and mail the stage does not account for — no key in its secrets, no provider in deploy.<kind>.<stage> — so there is nothing to list. Pass --allow-dev-services on its own.`);
6653
+ this.value = value;
6654
+ this.name = "AllowDevServicesTakesNoValue";
6655
+ }
6656
+ };
6657
+ /**
6658
+ * Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
6659
+ * a command line, with the reason, before the parser calls the value an
6660
+ * excess argument. `gkm deploy` and `gkm compose` take no positional
6661
+ * argument, so whatever follows the flag and is not a flag was meant for it.
6662
+ *
6663
+ * @throws {AllowDevServicesTakesNoValue}
6664
+ */
6665
+ function assertDevServicesFlag(argv) {
6666
+ for (let i = 0; i < argv.length; i++) {
6667
+ const arg = argv[i];
6668
+ if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
6669
+ const next = argv[i + 1];
6670
+ if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
6671
+ }
6672
+ }
6673
+ /**
6620
6674
  * A deployed stage with mail or object storage that its secrets do not
6621
6675
  * configure — every missing key, across every app, at once.
6622
6676
  */
@@ -6624,33 +6678,18 @@ var ExternalServicesNotConfigured = class extends Error {
6624
6678
  stage;
6625
6679
  missing;
6626
6680
  constructor(stage, missing) {
6627
- const services = [...new Set(missing.map((m) => m.service))].sort();
6681
+ const standIns = missing.some((m) => m.service !== void 0);
6628
6682
  const lines = missing.map((m) => {
6629
6683
  const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
6630
- return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "");
6684
+ return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
6631
6685
  });
6632
- super(`The stage '${stage}' is deployed, and a deployed stage's mail and object storage are real services: gkm runs no Mailpit or MinIO for it. Set ${missing.length === 1 ? "this key" : `these ${missing.length} keys`} in the stage's secrets:\n\n${lines.join("\n")}\n\nFor a stage that is not production — a preview, a demo — the dev services can run instead, with --allow-dev-services ${services.join(",")}. Mailpit delivers no mail, and MinIO keeps every object on one container's disk.\n\nOr run: gkm secrets:add --stage ${stage}`);
6686
+ super(`The stage '${stage}' is deployed, and a deployed stage's mail and object storage are real services: gkm runs no Mailpit or MinIO for it. Set ${missing.length === 1 ? "this key" : `these ${missing.length} keys`} in the stage's secrets:\n\n${lines.join("\n")}\n\n` + (standIns ? "For a stage that is not production — a preview, a demo — the dev services can run instead, with --allow-dev-services. Mailpit delivers no mail, and MinIO keeps every object on one container's disk.\n\n" : "") + `Or run: gkm secrets:add --stage ${stage}`);
6633
6687
  this.stage = stage;
6634
6688
  this.missing = missing;
6635
6689
  this.name = "ExternalServicesNotConfigured";
6636
6690
  }
6637
6691
  };
6638
6692
  /**
6639
- * The dev services a value names: `'minio,mailpit'`, or a list of them.
6640
- *
6641
- * @throws {UnknownDevService} for anything that is not one
6642
- */
6643
- function parseDevServices(value) {
6644
- if (value === void 0) return [];
6645
- const items = (typeof value === "string" ? [value] : value).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
6646
- const services = /* @__PURE__ */ new Set();
6647
- for (const item of items) {
6648
- if (!DEV_SERVICES.includes(item)) throw new UnknownDevService(item);
6649
- services.add(item);
6650
- }
6651
- return [...services].sort();
6652
- }
6653
- /**
6654
6693
  * Every mail and storage construct a manifest declares, with the apps that
6655
6694
  * depend on each — what a target that provisions the whole manifest checks.
6656
6695
  */
@@ -6692,6 +6731,8 @@ function requiredStageKeys(input) {
6692
6731
  const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
6693
6732
  if (!input.local) {
6694
6733
  const services = byId(input.services);
6734
+ const objects = input.providers?.objects;
6735
+ const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
6695
6736
  for (const d of services.filter((d) => d.kind === "email")) {
6696
6737
  add({
6697
6738
  key: (0, _geekmidas_manifest.provideKey)(d.id, "url"),
@@ -6719,7 +6760,7 @@ function requiredStageKeys(input) {
6719
6760
  what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
6720
6761
  note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${require_derive.appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
6721
6762
  example: `s3://${require_derive.appKey(d.id)}?region=eu-west-1`,
6722
- service: "minio",
6763
+ ...storage,
6723
6764
  ...apps(d)
6724
6765
  });
6725
6766
  for (const d of services.filter((d) => d.kind === "file-server")) add({
@@ -6728,7 +6769,7 @@ function requiredStageKeys(input) {
6728
6769
  kind: "file-server",
6729
6770
  what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
6730
6771
  example: `https://${require_derive.appKey(d.id)}.${domain}`,
6731
- service: "minio",
6772
+ ...storage,
6732
6773
  ...d.of ? { of: d.of } : {},
6733
6774
  ...apps(d)
6734
6775
  });
@@ -6777,20 +6818,23 @@ function externalServices(input) {
6777
6818
  const required = requiredStageKeys({
6778
6819
  local: false,
6779
6820
  services: input.declarations,
6780
- ...input.domain ? { domain: input.domain } : {}
6821
+ ...input.domain ? { domain: input.domain } : {},
6822
+ ...input.providers ? { providers: input.providers } : {}
6781
6823
  });
6782
- const mailpit = required.filter((k) => k.kind === "email" && k.key === (0, _geekmidas_manifest.provideKey)(k.id, "url")).filter((k) => !has(k.key) && allow.includes("mailpit")).map((k) => k.id);
6783
- const minio = required.filter((k) => k.kind === "bucket").filter((k) => !has(k.key) && allow.includes("minio")).map((k) => k.id);
6824
+ const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
6825
+ const mailpit = required.filter((k) => k.kind === "email" && k.key === (0, _geekmidas_manifest.provideKey)(k.id, "url")).filter(standIn).map((k) => k.id);
6826
+ const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
6784
6827
  const covered = (k) => k.kind === "email" && mailpit.includes(k.id) || k.kind === "bucket" && minio.includes(k.id) || k.kind === "file-server" && k.of !== void 0 && minio.includes(k.of);
6785
6828
  return {
6786
- missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps }) => ({
6829
+ missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
6787
6830
  key,
6788
6831
  id,
6789
6832
  what,
6790
6833
  ...note ? { note } : {},
6791
6834
  example,
6792
- service,
6793
- ...apps ? { apps } : {}
6835
+ ...service ? { service } : {},
6836
+ ...apps ? { apps } : {},
6837
+ ...hint ? { hint } : {}
6794
6838
  })),
6795
6839
  minio,
6796
6840
  mailpit
@@ -6839,6 +6883,360 @@ function reportDevServices(ctx, uses) {
6839
6883
  }
6840
6884
  }
6841
6885
  //#endregion
6886
+ //#region src/deploy/StateProvider.ts
6887
+ /**
6888
+ * Check if value is a StateProvider implementation.
6889
+ */
6890
+ function isStateProvider(value) {
6891
+ return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
6892
+ }
6893
+ //#endregion
6894
+ //#region src/deploy/StateStore.ts
6895
+ /**
6896
+ * Deploy state store.
6897
+ *
6898
+ * `StateProvider` reads and writes a whole stage at once, with nothing to stop
6899
+ * two runs of the same stage from overwriting each other and nowhere to record
6900
+ * a resource the moment it exists. A `StateStore` adds the three things a
6901
+ * deploy needs to be safe against crashes and concurrent runs:
6902
+ *
6903
+ * - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
6904
+ * holder, instead of racing the first.
6905
+ * - Versioned `read` and conditional `write`: a write names the version it was
6906
+ * based on, and raises `StateVersionConflict` when the stored state moved on
6907
+ * underneath it, so nothing is lost silently even without the lock.
6908
+ * - Per-resource records: `putResource` journals one resource at a time, so a
6909
+ * run that dies half way leaves behind the ids of everything it created.
6910
+ *
6911
+ * Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
6912
+ * custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
6913
+ * working but cannot lock.
6914
+ */
6915
+ var StateLocked = class extends Error {
6916
+ stage;
6917
+ holder;
6918
+ location;
6919
+ constructor(stage, holder, location) {
6920
+ const by = holder ? `${holder.owner}@${holder.host} (pid ${holder.pid}) since ${holder.acquiredAt}${holder.operation ? ` for '${holder.operation}'` : ""}` : "a run that did not record who it was";
6921
+ super(`Deploy state for stage '${stage}' is locked by ${by} (${location}). Wait for that run to finish; if it crashed, release the lock with \`gkm state:unlock --stage ${stage}\`.`);
6922
+ this.stage = stage;
6923
+ this.holder = holder;
6924
+ this.location = location;
6925
+ this.name = "StateLocked";
6926
+ }
6927
+ };
6928
+ var StateVersionConflict = class extends Error {
6929
+ stage;
6930
+ expectedVersion;
6931
+ actualVersion;
6932
+ constructor(stage, expectedVersion, actualVersion) {
6933
+ super(`Deploy state for stage '${stage}' changed since it was read (expected ${expectedVersion ? `version ${expectedVersion}` : "no state"}, found ${actualVersion ? `version ${actualVersion}` : "no state"}). Another run wrote it: read the state again and re-apply the change, and hold the stage's lock so runs take turns.`);
6934
+ this.stage = stage;
6935
+ this.expectedVersion = expectedVersion;
6936
+ this.actualVersion = actualVersion;
6937
+ this.name = "StateVersionConflict";
6938
+ }
6939
+ };
6940
+ var StageStateMissing = class extends Error {
6941
+ stage;
6942
+ constructor(stage) {
6943
+ super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
6944
+ this.stage = stage;
6945
+ this.name = "StageStateMissing";
6946
+ }
6947
+ };
6948
+ var StateUnreadable = class extends Error {
6949
+ stage;
6950
+ location;
6951
+ constructor(stage, location, cause) {
6952
+ super(`Deploy state for stage '${stage}' at ${location} is not valid JSON. Restore it from a backup (or the provider's history) rather than deploying without it — a deploy without state recreates every resource.`, { cause });
6953
+ this.stage = stage;
6954
+ this.location = location;
6955
+ this.name = "StateUnreadable";
6956
+ }
6957
+ };
6958
+ var StateSchemaTooNew = class extends Error {
6959
+ stage;
6960
+ schemaVersion;
6961
+ constructor(stage, schemaVersion) {
6962
+ super(`Deploy state for stage '${stage}' has schema version ${String(schemaVersion)}, newer than this @geekmidas/cli understands (2). Upgrade @geekmidas/cli to the version that wrote it.`);
6963
+ this.stage = stage;
6964
+ this.schemaVersion = schemaVersion;
6965
+ this.name = "StateSchemaTooNew";
6966
+ }
6967
+ };
6968
+ /**
6969
+ * Not thrown: emitted (once per store) when a custom `StateProvider` stands
6970
+ * in for a store, so a deploy that cannot be protected from a concurrent one
6971
+ * says so.
6972
+ */
6973
+ var StateStoreWithoutLocking = class extends Error {
6974
+ provider;
6975
+ constructor(provider) {
6976
+ super(`The custom state provider '${provider}' has no lock and no versions, so two deploys of the same stage can still overwrite each other. Implement StateStore (lock, versioned read, conditional write) or use the 'local', 'ssm' or 's3' state provider.`);
6977
+ this.provider = provider;
6978
+ this.name = "StateStoreWithoutLocking";
6979
+ }
6980
+ };
6981
+ var StateStoreNeedsWorkspaceName = class extends Error {
6982
+ provider;
6983
+ constructor(provider) {
6984
+ super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
6985
+ this.provider = provider;
6986
+ this.name = "StateStoreNeedsWorkspaceName";
6987
+ }
6988
+ };
6989
+ var UnknownStateProvider = class extends Error {
6990
+ config;
6991
+ constructor(config) {
6992
+ super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
6993
+ this.config = config;
6994
+ this.name = "UnknownStateProvider";
6995
+ }
6996
+ };
6997
+ /** A lock or write that could not get at the state backend in time. */
6998
+ var StateStoreBusy = class extends Error {
6999
+ stage;
7000
+ location;
7001
+ constructor(stage, location) {
7002
+ super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
7003
+ this.stage = stage;
7004
+ this.location = location;
7005
+ this.name = "StateStoreBusy";
7006
+ }
7007
+ };
7008
+ function decodeDocument(stage, body, location) {
7009
+ let parsed;
7010
+ try {
7011
+ parsed = JSON.parse(body);
7012
+ } catch (error) {
7013
+ throw new StateUnreadable(stage, location, error);
7014
+ }
7015
+ if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
7016
+ const schemaVersion = parsed.schemaVersion;
7017
+ if (schemaVersion === void 0) return {
7018
+ schemaVersion: 1,
7019
+ state: parsed
7020
+ };
7021
+ if (schemaVersion === 2) return {
7022
+ schemaVersion: 2,
7023
+ document: parsed
7024
+ };
7025
+ throw new StateSchemaTooNew(stage, schemaVersion);
7026
+ }
7027
+ function encodeDocument(document) {
7028
+ return JSON.stringify(document, null, 2);
7029
+ }
7030
+ /**
7031
+ * v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
7032
+ * so the first journaled deploy after an upgrade adopts them rather than
7033
+ * treating them as unknown.
7034
+ */
7035
+ function migrateV1(stage, state) {
7036
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7037
+ const resources = {};
7038
+ const seed = (type, name, id) => {
7039
+ if (!id) return;
7040
+ const key = name ? `${type}:${name}` : type;
7041
+ resources[key] = {
7042
+ key,
7043
+ type,
7044
+ id,
7045
+ status: "ready",
7046
+ updatedAt: now
7047
+ };
7048
+ };
7049
+ seed("project", null, state.projectId);
7050
+ seed("environment", null, state.environmentId);
7051
+ for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
7052
+ seed("postgres", null, state.services?.postgresId);
7053
+ seed("redis", null, state.services?.redisId);
7054
+ seed("backup-destination", null, state.backups?.destinationId);
7055
+ return {
7056
+ schemaVersion: 2,
7057
+ stage,
7058
+ serial: 1,
7059
+ state,
7060
+ resources,
7061
+ updatedAt: now
7062
+ };
7063
+ }
7064
+ function newLockHolder(options) {
7065
+ let owner = "unknown";
7066
+ try {
7067
+ owner = (0, node_os.userInfo)().username;
7068
+ } catch {}
7069
+ return {
7070
+ id: (0, node_crypto.randomUUID)(),
7071
+ owner,
7072
+ host: (0, node_os.hostname)(),
7073
+ pid: process.pid,
7074
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
7075
+ operation: options?.operation
7076
+ };
7077
+ }
7078
+ function contentVersion(body) {
7079
+ return (0, node_crypto.createHash)("sha256").update(body).digest("hex").slice(0, 16);
7080
+ }
7081
+ /**
7082
+ * A store over a backend that can read and conditionally write one blob per
7083
+ * stage, and create a lock object only if it does not exist. Migration and
7084
+ * resource records are the same for every such backend, so they live here.
7085
+ */
7086
+ var DocumentStateStore = class {
7087
+ async lock(stage, options) {
7088
+ const holder = newLockHolder(options);
7089
+ await this.createLock(stage, holder);
7090
+ let released = false;
7091
+ return {
7092
+ stage,
7093
+ holder,
7094
+ release: async () => {
7095
+ if (released) return;
7096
+ released = true;
7097
+ await this.removeLock(stage, holder);
7098
+ }
7099
+ };
7100
+ }
7101
+ async forceUnlock(stage) {
7102
+ const holder = await this.readLock(stage);
7103
+ await this.removeLock(stage, null);
7104
+ return holder;
7105
+ }
7106
+ async read(stage) {
7107
+ const current = await this.readDocument(stage);
7108
+ if (!current) return null;
7109
+ return {
7110
+ state: current.document.state,
7111
+ resources: current.document.resources,
7112
+ version: current.version
7113
+ };
7114
+ }
7115
+ async write(stage, state, options) {
7116
+ if (options.expectedVersion === null) {
7117
+ const document = {
7118
+ schemaVersion: 2,
7119
+ stage,
7120
+ serial: 1,
7121
+ state,
7122
+ resources: {},
7123
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7124
+ };
7125
+ return this.writeRaw(stage, encodeDocument(document), null);
7126
+ }
7127
+ return this.mutate(stage, options.expectedVersion, (document) => ({
7128
+ ...document,
7129
+ state
7130
+ }));
7131
+ }
7132
+ async putResource(stage, record, options) {
7133
+ return this.mutate(stage, options?.expectedVersion, (document) => ({
7134
+ ...document,
7135
+ resources: {
7136
+ ...document.resources,
7137
+ [record.key]: {
7138
+ ...record,
7139
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7140
+ }
7141
+ }
7142
+ }));
7143
+ }
7144
+ async deleteResource(stage, key, options) {
7145
+ return this.mutate(stage, options?.expectedVersion, (document) => {
7146
+ const { [key]: _removed, ...resources } = document.resources;
7147
+ return {
7148
+ ...document,
7149
+ resources
7150
+ };
7151
+ });
7152
+ }
7153
+ async mutate(stage, expectedVersion, change) {
7154
+ const current = await this.readDocument(stage);
7155
+ if (!current) {
7156
+ if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
7157
+ throw new StageStateMissing(stage);
7158
+ }
7159
+ if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
7160
+ const next = change(current.document);
7161
+ next.serial = current.document.serial + 1;
7162
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7163
+ return this.writeRaw(stage, encodeDocument(next), current.version);
7164
+ }
7165
+ /**
7166
+ * Reads the stage's document, migrating v1 in place on the way. The
7167
+ * migration is itself a conditional write: if another run migrated (or
7168
+ * wrote) first, its result is read instead.
7169
+ */
7170
+ async readDocument(stage, attempt = 0) {
7171
+ const raw = await this.readRaw(stage);
7172
+ if (!raw) return null;
7173
+ const decoded = decodeDocument(stage, raw.body, this.location(stage));
7174
+ if (decoded.schemaVersion === 2) return {
7175
+ document: decoded.document,
7176
+ version: raw.version
7177
+ };
7178
+ await this.writeV1Backup(stage, raw.body);
7179
+ const document = migrateV1(stage, decoded.state);
7180
+ try {
7181
+ return {
7182
+ document,
7183
+ version: await this.writeRaw(stage, encodeDocument(document), raw.version)
7184
+ };
7185
+ } catch (error) {
7186
+ if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
7187
+ throw error;
7188
+ }
7189
+ }
7190
+ };
7191
+ function isStateStore(value) {
7192
+ return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
7193
+ }
7194
+ /**
7195
+ * The store for a workspace's `state` config:
7196
+ *
7197
+ * - none / `'local'`: `LocalStateStore` under `.gkm/`
7198
+ * - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
7199
+ * a stale local copy can never win over it
7200
+ * - `'s3'`: `S3StateStore`
7201
+ * - a `StateStore` object: used as is
7202
+ * - a `StateProvider` object: wrapped by `LegacyStateStore`
7203
+ */
7204
+ async function createStateStore(options) {
7205
+ const { config, workspaceRoot, workspaceName } = options;
7206
+ if (!config || config.provider === "local") {
7207
+ const { LocalStateStore } = await Promise.resolve().then(() => require("./LocalStateStore-CLwa2DpK.cjs")).then((n) => n.LocalStateStore_exports);
7208
+ return new LocalStateStore(workspaceRoot);
7209
+ }
7210
+ if (isStateStore(config.provider)) return config.provider;
7211
+ if (isStateProvider(config.provider)) {
7212
+ const { LegacyStateStore } = await Promise.resolve().then(() => require("./LegacyStateStore-CWG_HVC0.cjs"));
7213
+ return new LegacyStateStore(config.provider, { warn: options.warn });
7214
+ }
7215
+ if (config.provider === "ssm") {
7216
+ if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
7217
+ const { SSMStateStore } = await Promise.resolve().then(() => require("./SSMStateStore-aBOTF018.cjs"));
7218
+ const ssm = config;
7219
+ return SSMStateStore.create({
7220
+ workspaceName,
7221
+ region: ssm.region,
7222
+ profile: ssm.profile
7223
+ });
7224
+ }
7225
+ if (config.provider === "s3") {
7226
+ if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
7227
+ const { S3StateStore } = await Promise.resolve().then(() => require("./S3StateStore-Cea9_5gF.cjs"));
7228
+ const s3 = config;
7229
+ return S3StateStore.create({
7230
+ workspaceName,
7231
+ bucket: s3.bucket,
7232
+ prefix: s3.prefix,
7233
+ region: s3.region,
7234
+ profile: s3.profile
7235
+ });
7236
+ }
7237
+ throw new UnknownStateProvider(config);
7238
+ }
7239
+ //#endregion
6842
7240
  //#region src/docker/imageRef.ts
6843
7241
  /**
6844
7242
  * An image reference, checked before it reaches `docker`.
@@ -6926,7 +7324,7 @@ async function buildDockerImage(app, buildRoot, options) {
6926
7324
  const fullImageName = validateImageRef(registry ? `${registry}/${app.imageName}:${tag}` : `${app.imageName}:${tag}`);
6927
7325
  logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
6928
7326
  try {
6929
- await require_run.run("docker", [
7327
+ await require_credentials.run("docker", [
6930
7328
  "build",
6931
7329
  `--file=${(0, node_path.relative)(buildRoot, app.dockerfile)}`,
6932
7330
  `--tag=${fullImageName}`,
@@ -6953,7 +7351,7 @@ async function pushDockerImage(imageName, options, cwd) {
6953
7351
  const fullImageName = validateImageRef(`${registry}/${imageName}:${tag}`);
6954
7352
  logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
6955
7353
  try {
6956
- await require_run.run("docker", ["push", fullImageName], { cwd });
7354
+ await require_credentials.run("docker", ["push", fullImageName], { cwd });
6957
7355
  logger.log(`✅ Docker image pushed: ${fullImageName}`);
6958
7356
  } catch (error) {
6959
7357
  throw new DockerPushFailed(fullImageName, error);
@@ -7150,6 +7548,71 @@ function printRunInstructions(workspace) {
7150
7548
  logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
7151
7549
  }
7152
7550
  //#endregion
7551
+ //#region src/providers/types.ts
7552
+ /** The manifest kinds a stage can name a provider for. */
7553
+ const PROVIDER_KINDS = ["objects"];
7554
+ //#endregion
7555
+ //#region src/providers/notes.ts
7556
+ /** The constructs of a kind: `objects` is every bucket. */
7557
+ function constructsOf$1(manifest, kind) {
7558
+ return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
7559
+ }
7560
+ /** A stage that names `false` for a kind, and declares constructs of it. */
7561
+ var StageProviderDisabled = class extends Error {
7562
+ kind;
7563
+ stage;
7564
+ ids;
7565
+ constructor(kind, stage, ids) {
7566
+ super(`deploy.${kind}.${stage} is false — the stage has no ${kind} — and ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} declared. Name what backs ${ids.length === 1 ? "it" : "them"} on '${stage}' (deploy.${kind}.${stage}: 'external', or a provider), or remove the ${ids.length === 1 ? "construct" : "constructs"}.`);
7567
+ this.kind = kind;
7568
+ this.stage = stage;
7569
+ this.ids = ids;
7570
+ this.name = "StageProviderDisabled";
7571
+ }
7572
+ };
7573
+ /** The command a stage's providers are run with. */
7574
+ function provisionCommand(stage) {
7575
+ return `gkm setup --stage ${stage}`;
7576
+ }
7577
+ /**
7578
+ * The line a stage missing a key a provider would write is given: what
7579
+ * creates it, and with which credentials. Undefined when no provider backs
7580
+ * the kind on the stage.
7581
+ */
7582
+ function provisionHint(workspace, kind, stage) {
7583
+ const choice = require_workspace.stageProvider(workspace, kind, stage);
7584
+ if (choice.mode !== "provider") return void 0;
7585
+ const provisioning = require_workspace.provisioningOf(kind, choice.name);
7586
+ if (!provisioning) return void 0;
7587
+ return `deploy.${kind}.${stage} is ${choice.name}: ${provisionCommand(stage)} creates it and writes this key, with ${provisioning.describe}` + (provisioning.login ? `, or gkm login --provider ${provisioning.login}` : "");
7588
+ }
7589
+ /**
7590
+ * What a deploy's checks read off the stage's providers: whether each kind is
7591
+ * accounted for — a provider, or `false` — so no dev service stands in for
7592
+ * it, and what creates a missing key of it.
7593
+ */
7594
+ function stageProviderNotes(workspace, stage) {
7595
+ if (require_workspace.stageProvider(workspace, "objects", stage).mode === "external") return {};
7596
+ const hint = provisionHint(workspace, "objects", stage);
7597
+ return { objects: {
7598
+ accounted: true,
7599
+ ...hint ? { hint } : {}
7600
+ } };
7601
+ }
7602
+ /**
7603
+ * A kind the stage set to `false`, declared anyway — refused before anything
7604
+ * else is checked. Pure: what a stack is composed from.
7605
+ *
7606
+ * @throws {StageProviderDisabled}
7607
+ */
7608
+ function assertStageProvidersEnabled(workspace, manifest, stage) {
7609
+ for (const kind of PROVIDER_KINDS) {
7610
+ const ids = constructsOf$1(manifest, kind);
7611
+ if (ids.length === 0) continue;
7612
+ if (require_workspace.stageProvider(workspace, kind, stage).mode === "none") throw new StageProviderDisabled(kind, stage, ids);
7613
+ }
7614
+ }
7615
+ //#endregion
7153
7616
  //#region src/deploy/journal.ts
7154
7617
  var DeployJournal = class DeployJournal {
7155
7618
  store;
@@ -7406,6 +7869,1044 @@ function setPostgresBackupId(state, backupId) {
7406
7869
  if (state.backups) state.backups.postgresBackupId = backupId;
7407
7870
  }
7408
7871
  //#endregion
7872
+ //#region src/providers/aws.ts
7873
+ /**
7874
+ * The AWS account a provider provisions in — the stage's, found the way the
7875
+ * SSM and Secrets Manager stores find it.
7876
+ *
7877
+ * 1. `--profile`, resolved on its own: exported keys left over from another
7878
+ * account never win over a profile someone named.
7879
+ * 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
7880
+ * deploy's own `aws` credential kind (`storedCredentials`).
7881
+ * 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
7882
+ * whatever `aws-actions/configure-aws-credentials` exported.
7883
+ *
7884
+ * None of them: the provider has no provisioning credentials, and the stage
7885
+ * is treated as `external`.
7886
+ */
7887
+ async function awsProvisioningCredentials(lookup) {
7888
+ const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
7889
+ const where = region ? { region } : {};
7890
+ if (lookup.profile) return {
7891
+ profile: lookup.profile,
7892
+ ...where
7893
+ };
7894
+ const stored = await require_credentials.storedCredentials({
7895
+ env: lookup.env,
7896
+ ...lookup.home ? { home: lookup.home } : {}
7897
+ }).get({
7898
+ kind: "aws",
7899
+ stage: lookup.stage
7900
+ });
7901
+ if (stored) return stored;
7902
+ try {
7903
+ const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
7904
+ await fromNodeProviderChain({
7905
+ timeout: 1e3,
7906
+ maxRetries: 0
7907
+ })();
7908
+ return {
7909
+ chain: true,
7910
+ ...where
7911
+ };
7912
+ } catch {
7913
+ return;
7914
+ }
7915
+ }
7916
+ /**
7917
+ * An S3-compatible endpoint the environment points the SDK at — the AWS
7918
+ * emulator in tests. The SDK reads it itself; it is read here too, because
7919
+ * an emulator takes path-style requests, and a key written for one has to
7920
+ * say where it is.
7921
+ */
7922
+ function awsEndpoint(env, service) {
7923
+ return env[`AWS_ENDPOINT_URL_${service}`] ?? env.AWS_ENDPOINT_URL;
7924
+ }
7925
+ /** An SDK client's config for a provisioning credential. */
7926
+ async function awsClientConfig(credential, region) {
7927
+ if ("profile" in credential) {
7928
+ const { fromIni } = await import("@aws-sdk/credential-providers");
7929
+ return {
7930
+ region,
7931
+ credentials: fromIni({ profile: credential.profile })
7932
+ };
7933
+ }
7934
+ if ("accessKeyId" in credential) return {
7935
+ region,
7936
+ credentials: {
7937
+ accessKeyId: credential.accessKeyId,
7938
+ secretAccessKey: credential.secretAccessKey,
7939
+ ...credential.sessionToken ? { sessionToken: credential.sessionToken } : {}
7940
+ }
7941
+ };
7942
+ return { region };
7943
+ }
7944
+ const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
7945
+ /** A name that S3 would refuse — what the name builder never returns. */
7946
+ var InvalidBucketName = class extends Error {
7947
+ bucket;
7948
+ reason;
7949
+ constructor(bucket, reason) {
7950
+ super(`'${bucket}' is not a valid S3 bucket name: ${reason}. Rename the construct or set deploy.namespace in gkm.config.ts to something that leaves room for it.`);
7951
+ this.bucket = bucket;
7952
+ this.reason = reason;
7953
+ this.name = "InvalidBucketName";
7954
+ }
7955
+ };
7956
+ /** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
7957
+ function part(value) {
7958
+ return (0, _geekmidas_manifest.kebabCase)(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7959
+ }
7960
+ /**
7961
+ * `parts` joined by `-`, shortened to `max` characters by trimming the longest
7962
+ * part one character at a time — so a long construct id gives way before a
7963
+ * short stage name does, and every part keeps its start.
7964
+ */
7965
+ function fitParts(parts, max) {
7966
+ const kept = parts.map(part).filter(Boolean);
7967
+ const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
7968
+ while (length() > max) {
7969
+ let longest = 0;
7970
+ for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
7971
+ if (kept[longest].length <= 1) break;
7972
+ kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
7973
+ }
7974
+ return kept.join("-");
7975
+ }
7976
+ /** Why `name` is not a valid bucket name, or undefined when it is. */
7977
+ function bucketNameProblem(name) {
7978
+ if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
7979
+ if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
7980
+ if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
7981
+ if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
7982
+ if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
7983
+ }
7984
+ /**
7985
+ * The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
7986
+ * dots, shortened to leave room for a clash's suffix.
7987
+ *
7988
+ * @throws {InvalidBucketName} when nothing usable is left
7989
+ */
7990
+ function bucketName(input) {
7991
+ const name = fitParts([
7992
+ input.scope,
7993
+ input.stage,
7994
+ input.id
7995
+ ], 56);
7996
+ const problem = bucketNameProblem(name);
7997
+ if (problem) throw new InvalidBucketName(name, problem);
7998
+ return name;
7999
+ }
8000
+ /** Six random `[a-z0-9]` characters. */
8001
+ function randomSuffix(random = node_crypto.randomInt) {
8002
+ let suffix = "";
8003
+ for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
8004
+ return suffix;
8005
+ }
8006
+ /** The good name with a clash's suffix. */
8007
+ function suffixedBucketName(good, suffix) {
8008
+ return `${good}-${suffix}`;
8009
+ }
8010
+ /**
8011
+ * The IAM user a bucket is reached with on a stage:
8012
+ * `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
8013
+ */
8014
+ function iamUserName(input) {
8015
+ return fitParts([
8016
+ "gkm",
8017
+ input.scope,
8018
+ input.stage,
8019
+ input.id
8020
+ ], 64);
8021
+ }
8022
+ /** The path every user gkm creates is under, so it finds its own. */
8023
+ const IAM_USER_PATH = "/gkm/";
8024
+ /** The inline policy each user is given. */
8025
+ const IAM_POLICY_NAME = "gkm-bucket";
8026
+ //#endregion
8027
+ //#region src/providers/s3/origins.ts
8028
+ /**
8029
+ * Which browser origins may call a bucket: the stage's sites that depend on
8030
+ * an API using it.
8031
+ *
8032
+ * The same reading of the graph an API's own CORS origins are — its callers —
8033
+ * taken one step further: a site that calls an API which hands it a presigned
8034
+ * upload URL is a site whose browser PUTs to the bucket. A site that reads the
8035
+ * bucket (or a file server over it) itself is one too. Nothing is written
8036
+ * down: add a site that calls the API and the next provision run allows it.
8037
+ */
8038
+ /** The keys a reader of this bucket holds: its URL, and each server's over it. */
8039
+ function bucketKeys(manifest, bucket) {
8040
+ return [(0, _geekmidas_manifest.provideKey)(bucket, "url"), ...Object.entries(manifest).filter(([, d]) => d.kind === "file-server" && d.of === bucket).map(([id]) => (0, _geekmidas_manifest.provideKey)(id, "url"))];
8041
+ }
8042
+ function bucketOrigins(input) {
8043
+ const { manifest, workspace, stage } = input;
8044
+ const keys = bucketKeys(manifest, input.bucket);
8045
+ const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
8046
+ const env = appEnvKeys(manifest, require_derive.appKey(id), input.runnables);
8047
+ return env !== void 0 && keys.some((key) => env.has(key));
8048
+ });
8049
+ const sites = /* @__PURE__ */ new Set();
8050
+ for (const [id, declaration] of readers) {
8051
+ if (declaration.kind === "site") {
8052
+ sites.add(id);
8053
+ continue;
8054
+ }
8055
+ for (const caller of (0, _geekmidas_manifest.dependentsOf)(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
8056
+ }
8057
+ const origins = /* @__PURE__ */ new Set();
8058
+ const unresolved = [];
8059
+ for (const id of [...sites].sort()) {
8060
+ const name = require_derive.appKey(id);
8061
+ const app = workspace.apps[name];
8062
+ if (!app) {
8063
+ unresolved.push(name);
8064
+ continue;
8065
+ }
8066
+ try {
8067
+ const host = resolveHost(name, app, stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
8068
+ origins.add(`https://${host}`);
8069
+ } catch (error) {
8070
+ if (!(error instanceof NoDomainForStage)) throw error;
8071
+ unresolved.push(name);
8072
+ }
8073
+ }
8074
+ return {
8075
+ origins: [...origins].sort(),
8076
+ unresolved
8077
+ };
8078
+ }
8079
+ //#endregion
8080
+ //#region src/providers/s3/policy.ts
8081
+ const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
8082
+ /**
8083
+ * What the app's key may do: objects in this bucket, and listing it — and
8084
+ * nothing else in the account.
8085
+ *
8086
+ * Multipart uploads are in it because a presigned upload of a large file is
8087
+ * one, and an abandoned one is cleaned up by the app. Reading old versions is
8088
+ * added only when the bucket keeps them.
8089
+ */
8090
+ function userPolicy(bucket, options = {}) {
8091
+ return {
8092
+ Version: "2012-10-17",
8093
+ Statement: [{
8094
+ Sid: "GkmObjects",
8095
+ Effect: "Allow",
8096
+ Action: [
8097
+ "s3:GetObject",
8098
+ "s3:PutObject",
8099
+ "s3:DeleteObject",
8100
+ "s3:AbortMultipartUpload",
8101
+ "s3:ListMultipartUploadParts",
8102
+ ...options.versioning ? ["s3:GetObjectVersion"] : []
8103
+ ],
8104
+ Resource: `${bucketArn(bucket)}/*`
8105
+ }, {
8106
+ Sid: "GkmBucket",
8107
+ Effect: "Allow",
8108
+ Action: [
8109
+ "s3:ListBucket",
8110
+ "s3:GetBucketLocation",
8111
+ "s3:ListBucketMultipartUploads"
8112
+ ],
8113
+ Resource: bucketArn(bucket)
8114
+ }]
8115
+ };
8116
+ }
8117
+ /**
8118
+ * One open pattern as the resource it is public on. A `**` is S3's `*`; a
8119
+ * single `*` is too, which crosses `/` where the construct's own check does
8120
+ * not — the construct is the stricter of the two (see `bucketPolicy` in
8121
+ * reconcile, which the local MinIO is given the same way).
8122
+ */
8123
+ function openResource(bucket, pattern) {
8124
+ const path = pattern.replace(/^\/+/, "").replace(/\*\*/g, "*");
8125
+ return `${bucketArn(bucket)}/${path}`;
8126
+ }
8127
+ /**
8128
+ * The bucket's own policy: every request refused unless it came over TLS,
8129
+ * and — only when a file server opens paths on it — anonymous `s3:GetObject`
8130
+ * on exactly those prefixes.
8131
+ */
8132
+ function bucketPolicy(bucket, open = []) {
8133
+ const statements = [{
8134
+ Sid: "GkmDenyInsecureTransport",
8135
+ Effect: "Deny",
8136
+ Principal: "*",
8137
+ Action: "s3:*",
8138
+ Resource: [bucketArn(bucket), `${bucketArn(bucket)}/*`],
8139
+ Condition: { Bool: { "aws:SecureTransport": "false" } }
8140
+ }];
8141
+ if (open.length > 0) statements.push({
8142
+ Sid: "GkmOpenPaths",
8143
+ Effect: "Allow",
8144
+ Principal: "*",
8145
+ Action: "s3:GetObject",
8146
+ Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
8147
+ });
8148
+ return {
8149
+ Version: "2012-10-17",
8150
+ Statement: statements
8151
+ };
8152
+ }
8153
+ /**
8154
+ * Block Public Access. All four flags on, unless the bucket has open paths:
8155
+ * then a *policy* may make it public (`BlockPublicPolicy`,
8156
+ * `RestrictPublicBuckets` off), and ACLs still may not.
8157
+ */
8158
+ function publicAccessBlock(open) {
8159
+ return {
8160
+ BlockPublicAcls: true,
8161
+ IgnorePublicAcls: true,
8162
+ BlockPublicPolicy: !open,
8163
+ RestrictPublicBuckets: !open
8164
+ };
8165
+ }
8166
+ function corsRule(origins) {
8167
+ return {
8168
+ AllowedMethods: [
8169
+ "GET",
8170
+ "HEAD",
8171
+ "POST",
8172
+ "PUT"
8173
+ ],
8174
+ AllowedOrigins: [...new Set(origins)].sort(),
8175
+ AllowedHeaders: ["*"],
8176
+ ExposeHeaders: ["ETag"],
8177
+ MaxAgeSeconds: 3e3
8178
+ };
8179
+ }
8180
+ /**
8181
+ * A policy document in one canonical form — keys sorted, a lone string a
8182
+ * one-element list, lists sorted — so one AWS reformatted reads as the same
8183
+ * document rather than as drift.
8184
+ */
8185
+ function canonicalPolicy(value) {
8186
+ return JSON.stringify(canonical(value));
8187
+ }
8188
+ function canonical(value, key) {
8189
+ if (Array.isArray(value)) {
8190
+ const items = value.map((v) => canonical(v));
8191
+ return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
8192
+ }
8193
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
8194
+ const v = value[k];
8195
+ return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
8196
+ }));
8197
+ return value;
8198
+ }
8199
+ //#endregion
8200
+ //#region src/providers/s3/index.ts
8201
+ /** The tags gkm finds its own buckets and users by. */
8202
+ const TAG_PROJECT = "gkm:project";
8203
+ const TAG_STAGE = "gkm:stage";
8204
+ const TAG_CONSTRUCT = "gkm:construct";
8205
+ function bucketConstructs(manifest) {
8206
+ return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
8207
+ id,
8208
+ versioned: d.versioned === true,
8209
+ servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
8210
+ id: sid,
8211
+ open: s.open ?? []
8212
+ })).sort((a, b) => a.id.localeCompare(b.id))
8213
+ })).sort((a, b) => a.id.localeCompare(b.id));
8214
+ }
8215
+ /**
8216
+ * The region the stage's buckets are created in: the entry's, the secrets
8217
+ * store's when it is in AWS, the credential's, `AWS_REGION`.
8218
+ */
8219
+ function s3Region(ctx, env = process.env) {
8220
+ const store = ctx.workspace.secrets?.store;
8221
+ const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
8222
+ const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
8223
+ if (!region) throw new require_workspace.S3RegionRequired(ctx.stage);
8224
+ return region;
8225
+ }
8226
+ /**
8227
+ * The address a file server over `bucket` serves on: the bucket's regional
8228
+ * virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
8229
+ * — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
8230
+ * its path-style address.
8231
+ */
8232
+ function bucketPublicUrl(bucket, region, endpoint) {
8233
+ if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
8234
+ return `https://${bucket}.s3.${region}.amazonaws.com`;
8235
+ }
8236
+ /** What an AWS SDK error is called, whichever field carries it. */
8237
+ function errorName(error) {
8238
+ const e = error;
8239
+ return e?.Code ?? e?.name ?? "";
8240
+ }
8241
+ /** A read whose "there is none" answer is one of `missing`. */
8242
+ async function readOr(read, missing) {
8243
+ try {
8244
+ return await read();
8245
+ } catch (error) {
8246
+ if (missing.includes(errorName(error))) return void 0;
8247
+ throw error;
8248
+ }
8249
+ }
8250
+ const defaultClients = async (credential, region) => {
8251
+ const S3 = await import("@aws-sdk/client-s3");
8252
+ const IAM = await import("@aws-sdk/client-iam");
8253
+ const config = await awsClientConfig(credential, region);
8254
+ const endpoint = awsEndpoint(process.env, "S3");
8255
+ return {
8256
+ S3,
8257
+ IAM,
8258
+ s3: new S3.S3Client({
8259
+ ...config,
8260
+ ...endpoint ? {
8261
+ endpoint,
8262
+ forcePathStyle: true
8263
+ } : {}
8264
+ }),
8265
+ iam: new IAM.IAMClient(config),
8266
+ region,
8267
+ ...endpoint ? { endpoint } : {}
8268
+ };
8269
+ };
8270
+ /** For tests: what makes the clients. */
8271
+ let makeClients = defaultClients;
8272
+ async function ensureS3(ctx) {
8273
+ const region = s3Region(ctx);
8274
+ const clients = await makeClients(ctx.credential, region);
8275
+ for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
8276
+ }
8277
+ /** The keys gkm tags a bucket and a user with. */
8278
+ function identityTags(ctx, id) {
8279
+ return [
8280
+ {
8281
+ Key: TAG_PROJECT,
8282
+ Value: ctx.identity.key
8283
+ },
8284
+ {
8285
+ Key: TAG_STAGE,
8286
+ Value: ctx.stage
8287
+ },
8288
+ {
8289
+ Key: TAG_CONSTRUCT,
8290
+ Value: id
8291
+ }
8292
+ ];
8293
+ }
8294
+ async function ensureBucket(ctx, clients, construct) {
8295
+ const { id } = construct;
8296
+ const nameInput = {
8297
+ scope: ctx.identity.scope,
8298
+ stage: ctx.stage,
8299
+ id: require_derive.appKey(id)
8300
+ };
8301
+ const versioning = ctx.config.versioning ?? construct.versioned;
8302
+ const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
8303
+ const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
8304
+ await ensureBucketSettings(ctx, clients, {
8305
+ id,
8306
+ bucket,
8307
+ exists,
8308
+ versioning,
8309
+ open
8310
+ });
8311
+ await ensureAccessKey(ctx, clients, {
8312
+ id,
8313
+ bucket,
8314
+ ...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
8315
+ });
8316
+ const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
8317
+ for (const server of construct.servers) {
8318
+ const key = (0, _geekmidas_manifest.provideKey)(server.id, "url");
8319
+ const entry = {
8320
+ key: `file-server:${server.id}`,
8321
+ type: "url"
8322
+ };
8323
+ const recorded = ctx.state.record(entry.key)?.id;
8324
+ const current = ctx.secrets[key];
8325
+ if (current === publicUrl) {
8326
+ await ctx.state.ready(entry, publicUrl);
8327
+ continue;
8328
+ }
8329
+ if (current !== void 0 && current !== recorded) {
8330
+ ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
8331
+ continue;
8332
+ }
8333
+ await ctx.change({
8334
+ construct: server.id,
8335
+ resource: key,
8336
+ change: `set to ${publicUrl}`
8337
+ }, async () => {
8338
+ await ctx.writeSecrets({ [key]: publicUrl });
8339
+ await ctx.state.ready(entry, publicUrl);
8340
+ });
8341
+ }
8342
+ }
8343
+ /**
8344
+ * The bucket's name: the recorded one, else the good name, else the good
8345
+ * name with a random suffix — up to {@link SUFFIX_ATTEMPTS} times.
8346
+ */
8347
+ async function ensureBucketName(ctx, clients, id, good) {
8348
+ const { S3, s3, region } = clients;
8349
+ const key = `s3-bucket:${id}`;
8350
+ const record = ctx.state.record(key);
8351
+ const entry = (name) => ({
8352
+ key,
8353
+ type: "s3-bucket",
8354
+ data: {
8355
+ name,
8356
+ region
8357
+ }
8358
+ });
8359
+ /** Whether this account holds `name`: yes, no, or someone else does. */
8360
+ const probe = async (name) => {
8361
+ try {
8362
+ await s3.send(new S3.HeadBucketCommand({ Bucket: name }));
8363
+ return "ours";
8364
+ } catch (error) {
8365
+ const name = errorName(error);
8366
+ if (name === "NotFound" || name === "NoSuchBucket") return "none";
8367
+ if (name === "Forbidden" || name === "AccessDenied") return "taken";
8368
+ throw error;
8369
+ }
8370
+ };
8371
+ const create = async (name) => {
8372
+ if (ctx.dryRun) {
8373
+ const found = await probe(name);
8374
+ if (found === "none") {
8375
+ await ctx.change({
8376
+ construct: id,
8377
+ resource: `bucket ${name}`,
8378
+ change: `create in ${region}`
8379
+ }, async () => {});
8380
+ return "created";
8381
+ }
8382
+ return found;
8383
+ }
8384
+ await ctx.state.pending(entry(name));
8385
+ try {
8386
+ await s3.send(new S3.CreateBucketCommand({
8387
+ Bucket: name,
8388
+ ...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
8389
+ }));
8390
+ } catch (error) {
8391
+ const code = errorName(error);
8392
+ if (code === "BucketAlreadyOwnedByYou") return "ours";
8393
+ if (code === "BucketAlreadyExists") return "taken";
8394
+ throw error;
8395
+ }
8396
+ await ctx.change({
8397
+ construct: id,
8398
+ resource: `bucket ${name}`,
8399
+ change: `create in ${region}`
8400
+ }, async () => {});
8401
+ return "created";
8402
+ };
8403
+ const adopt = async (name) => {
8404
+ const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
8405
+ if (owner && owner !== ctx.identity.key) throw new require_workspace.BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
8406
+ };
8407
+ if (record?.status === "ready" && record.id) {
8408
+ const found = await probe(record.id);
8409
+ if (found === "ours") return {
8410
+ bucket: record.id,
8411
+ exists: true
8412
+ };
8413
+ if (found === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
8414
+ const made = await create(record.id);
8415
+ if (made === "taken") throw new require_workspace.RecordedBucketUnreachable(id, record.id);
8416
+ if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
8417
+ return {
8418
+ bucket: record.id,
8419
+ exists: made === "ours"
8420
+ };
8421
+ }
8422
+ const pending = record?.data?.name;
8423
+ const names = [
8424
+ ...typeof pending === "string" && pending !== good ? [pending] : [],
8425
+ good,
8426
+ ...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
8427
+ ];
8428
+ const tried = [];
8429
+ for (const name of names) {
8430
+ tried.push(name);
8431
+ const made = await create(name);
8432
+ if (made === "taken") continue;
8433
+ if (made === "ours") await adopt(name);
8434
+ if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
8435
+ if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
8436
+ return {
8437
+ bucket: name,
8438
+ exists: made === "ours"
8439
+ };
8440
+ }
8441
+ throw new require_workspace.BucketNameUnavailable(id, tried.filter((n) => n !== pending));
8442
+ }
8443
+ /** Tags, Block Public Access, encryption, policy, versioning and CORS. */
8444
+ async function ensureBucketSettings(ctx, clients, input) {
8445
+ const { S3, s3 } = clients;
8446
+ const { id, bucket } = input;
8447
+ const Bucket = bucket;
8448
+ const fresh = ctx.dryRun && !input.exists;
8449
+ const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
8450
+ const set = (change, apply) => ctx.change({
8451
+ construct: id,
8452
+ resource: `bucket ${bucket}`,
8453
+ change
8454
+ }, () => apply().then(() => void 0));
8455
+ const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
8456
+ const ours = identityTags(ctx, id);
8457
+ const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
8458
+ if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
8459
+ Bucket,
8460
+ Tagging: { TagSet: [...others, ...ours].map((t) => ({
8461
+ Key: t.Key,
8462
+ Value: t.Value
8463
+ })) }
8464
+ })));
8465
+ const isOpen = input.open.length > 0;
8466
+ const block = publicAccessBlock(isOpen);
8467
+ const ensureBlock = async () => {
8468
+ const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
8469
+ if (!(now && Object.keys(block).every((k) => (now[k] ?? false) === block[k]))) await set(isOpen ? "block public access (ACLs; the policy opens the open paths)" : "block all public access", () => s3.send(new S3.PutPublicAccessBlockCommand({
8470
+ Bucket,
8471
+ PublicAccessBlockConfiguration: block
8472
+ })));
8473
+ };
8474
+ const policy = bucketPolicy(bucket, input.open);
8475
+ const ensurePolicy = async () => {
8476
+ const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
8477
+ if (!(current?.Policy !== void 0 && canonicalPolicy(JSON.parse(current.Policy)) === canonicalPolicy(policy))) await set(isOpen ? `policy: TLS only, public read on ${input.open.join(", ")}` : "policy: TLS only", () => s3.send(new S3.PutBucketPolicyCommand({
8478
+ Bucket,
8479
+ Policy: JSON.stringify(policy)
8480
+ })));
8481
+ };
8482
+ if (isOpen) {
8483
+ await ensureBlock();
8484
+ await ensurePolicy();
8485
+ } else {
8486
+ await ensurePolicy();
8487
+ await ensureBlock();
8488
+ }
8489
+ if ((await read(() => s3.send(new S3.GetBucketEncryptionCommand({ Bucket })), ["ServerSideEncryptionConfigurationNotFoundError"]))?.ServerSideEncryptionConfiguration?.Rules?.[0]?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm !== "AES256" && !fresh) await set("encrypt by default (SSE-S3)", () => s3.send(new S3.PutBucketEncryptionCommand({
8490
+ Bucket,
8491
+ ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
8492
+ })));
8493
+ const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
8494
+ if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
8495
+ Bucket,
8496
+ VersioningConfiguration: { Status: "Enabled" }
8497
+ })));
8498
+ else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
8499
+ Bucket,
8500
+ VersioningConfiguration: { Status: "Suspended" }
8501
+ })));
8502
+ const { origins, unresolved } = bucketOrigins({
8503
+ workspace: ctx.workspace,
8504
+ manifest: ctx.manifest,
8505
+ runnables: ctx.runnables,
8506
+ stage: ctx.stage,
8507
+ bucket: id
8508
+ });
8509
+ if (unresolved.length > 0) ctx.log(` ${unresolved.join(", ")} ${unresolved.length === 1 ? "has" : "have"} no address on '${ctx.stage}' (deploy.domains.${ctx.stage}), so ${unresolved.length === 1 ? "is" : "are"} not in '${bucket}''s CORS`);
8510
+ if (origins.length === 0) return;
8511
+ const rule = corsRule(origins);
8512
+ const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
8513
+ if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
8514
+ Bucket,
8515
+ CORSConfiguration: { CORSRules: [rule] }
8516
+ })));
8517
+ }
8518
+ function canonicalCors(rule) {
8519
+ const sorted = (list) => [...list ?? []].sort();
8520
+ return JSON.stringify({
8521
+ AllowedMethods: sorted(rule.AllowedMethods),
8522
+ AllowedOrigins: sorted(rule.AllowedOrigins),
8523
+ AllowedHeaders: sorted(rule.AllowedHeaders),
8524
+ ExposeHeaders: sorted(rule.ExposeHeaders),
8525
+ MaxAgeSeconds: rule.MaxAgeSeconds ?? null
8526
+ });
8527
+ }
8528
+ /** The bucket's IAM user and its inline policy. */
8529
+ async function ensureUser(ctx, clients, id, user, policy) {
8530
+ const { IAM, iam } = clients;
8531
+ const entry = {
8532
+ key: `iam-user:${id}`,
8533
+ type: "iam-user"
8534
+ };
8535
+ const found = await readOr(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
8536
+ if (found?.User) {
8537
+ const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
8538
+ const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
8539
+ if (found.User.Path !== "/gkm/" || tag("gkm:project") !== ctx.identity.key || tag("gkm:stage") !== ctx.stage) throw new require_workspace.IamUserNotOwned(user, ctx.identity.key, ctx.stage);
8540
+ await ctx.state.ready(entry, user);
8541
+ } else await ctx.change({
8542
+ construct: id,
8543
+ resource: `IAM user ${user}`,
8544
+ change: `create under ${IAM_USER_PATH}`
8545
+ }, async () => {
8546
+ await ctx.state.pending(entry);
8547
+ await iam.send(new IAM.CreateUserCommand({
8548
+ UserName: user,
8549
+ Path: IAM_USER_PATH,
8550
+ Tags: identityTags(ctx, id)
8551
+ }));
8552
+ await ctx.state.ready(entry, user);
8553
+ });
8554
+ const exists = Boolean(found?.User) || !ctx.dryRun;
8555
+ const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
8556
+ UserName: user,
8557
+ PolicyName: IAM_POLICY_NAME
8558
+ })), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
8559
+ const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
8560
+ if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
8561
+ construct: id,
8562
+ resource: `IAM user ${user}`,
8563
+ change: `policy ${IAM_POLICY_NAME}: this bucket only`
8564
+ }, async () => {
8565
+ await iam.send(new IAM.PutUserPolicyCommand({
8566
+ UserName: user,
8567
+ PolicyName: IAM_POLICY_NAME,
8568
+ PolicyDocument: JSON.stringify(policy)
8569
+ }));
8570
+ });
8571
+ return {
8572
+ user,
8573
+ exists
8574
+ };
8575
+ }
8576
+ /** IAM returns a policy document URL-encoded; an emulator may not. */
8577
+ function decodePolicy(document) {
8578
+ try {
8579
+ return decodeURIComponent(document);
8580
+ } catch {
8581
+ return document;
8582
+ }
8583
+ }
8584
+ /**
8585
+ * The user's access key, in the stage's secrets as the bucket's URL.
8586
+ *
8587
+ * - First run: one key, written into the stage's secrets.
8588
+ * - `--rotate-keys`: a second key, written in place of the first; the first
8589
+ * stays active until the stage has deployed with the second, and the next
8590
+ * run after that deploy — or `--retire-old-keys` — deletes it.
8591
+ * - A key the secrets no longer hold (a store emptied by hand): replaced the
8592
+ * same way, since its secret cannot be read back.
8593
+ */
8594
+ async function ensureAccessKey(ctx, clients, input) {
8595
+ const { IAM, iam } = clients;
8596
+ const { id, bucket, user } = input;
8597
+ const urlKey = (0, _geekmidas_manifest.provideKey)(id, "url");
8598
+ const entryKey = `iam-access-key:${id}`;
8599
+ const record = ctx.state.record(entryKey);
8600
+ const given = ctx.secrets[urlKey];
8601
+ let inUrl;
8602
+ if (given !== void 0) {
8603
+ let parsed;
8604
+ try {
8605
+ parsed = _geekmidas_storage_s3_url.parse(given);
8606
+ } catch {
8607
+ parsed = void 0;
8608
+ }
8609
+ if (parsed?.bucket !== bucket) throw new require_workspace.BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
8610
+ inUrl = parsed.accessKeyId;
8611
+ }
8612
+ const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
8613
+ let current = record?.status === "ready" ? record.id : void 0;
8614
+ let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
8615
+ const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
8616
+ const entry = (data = {}) => ({
8617
+ key: entryKey,
8618
+ type: "iam-access-key",
8619
+ data: {
8620
+ user,
8621
+ ...data
8622
+ }
8623
+ });
8624
+ if (previous && current) {
8625
+ const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
8626
+ if (ctx.retireOldKeys || deployed) {
8627
+ const old = previous;
8628
+ const keep = current;
8629
+ await ctx.change({
8630
+ construct: id,
8631
+ resource: `IAM user ${user}`,
8632
+ change: `delete the rotated-out access key ${old}`
8633
+ }, async () => {
8634
+ if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
8635
+ UserName: user,
8636
+ AccessKeyId: old
8637
+ }));
8638
+ await ctx.state.ready(entry(), keep);
8639
+ });
8640
+ previous = void 0;
8641
+ } else ctx.log(` ${id}: the old access key ${previous} stays active until '${ctx.stage}' is deployed with the new one; the next gkm setup --stage ${ctx.stage} after that deletes it (or pass --retire-old-keys)`);
8642
+ }
8643
+ if (!current && inUrl && listed.includes(inUrl)) {
8644
+ await ctx.state.ready(entry(), inUrl);
8645
+ current = inUrl;
8646
+ }
8647
+ if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
8648
+ if (ctx.rotateKeys && previous) throw new require_workspace.RotationInProgress(id, previous, ctx.stage);
8649
+ const outgoing = current && listed.includes(current) ? current : void 0;
8650
+ if (listed.length >= 2) throw new require_workspace.AccessKeyLimit(user, listed);
8651
+ const why = !current ? "create an access key" : ctx.rotateKeys ? "rotate: issue a new access key" : "issue a new access key (the secrets no longer hold the recorded one)";
8652
+ await ctx.change({
8653
+ construct: id,
8654
+ resource: `IAM user ${user}`,
8655
+ change: why
8656
+ }, async () => {
8657
+ await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
8658
+ const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
8659
+ await ctx.writeSecrets({ [urlKey]: _geekmidas_storage_s3_url.build({
8660
+ bucket,
8661
+ region: clients.region,
8662
+ ...clients.endpoint ? {
8663
+ endpoint: clients.endpoint,
8664
+ forcePathStyle: true
8665
+ } : {},
8666
+ accessKeyId: key.AccessKeyId,
8667
+ secretAccessKey: key.SecretAccessKey
8668
+ }) });
8669
+ await ctx.state.ready(entry(outgoing ? {
8670
+ previous: outgoing,
8671
+ rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
8672
+ } : {}), key.AccessKeyId);
8673
+ });
8674
+ if (outgoing) ctx.log(` ${urlKey} holds the new key. Deploy '${ctx.stage}' now; the next gkm setup --stage ${ctx.stage} after that deploy deletes the old key ${outgoing}`);
8675
+ }
8676
+ /**
8677
+ * Each provisioned bucket answers `HeadBucket` to the key in the stage's
8678
+ * secrets — the one the apps will use. A bucket whose URL carries no key is
8679
+ * not one this provider wrote, and the deploy's own check covers a URL that
8680
+ * is missing.
8681
+ */
8682
+ async function verifyS3(ctx) {
8683
+ const S3 = await import("@aws-sdk/client-s3");
8684
+ for (const { id } of bucketConstructs(ctx.manifest)) {
8685
+ const url = ctx.secrets[(0, _geekmidas_manifest.provideKey)(id, "url")];
8686
+ if (url === void 0) continue;
8687
+ const address = _geekmidas_storage_s3_url.parse(url);
8688
+ if (!address.accessKeyId || !address.secretAccessKey) continue;
8689
+ const client = new S3.S3Client({
8690
+ region: address.region ?? ctx.config.region ?? "us-east-1",
8691
+ credentials: {
8692
+ accessKeyId: address.accessKeyId,
8693
+ secretAccessKey: address.secretAccessKey
8694
+ },
8695
+ ...address.endpoint ? { endpoint: address.endpoint } : {},
8696
+ ...address.forcePathStyle ? { forcePathStyle: true } : {}
8697
+ });
8698
+ try {
8699
+ await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
8700
+ } catch (error) {
8701
+ const name = errorName(error);
8702
+ throw new require_workspace.ProvisionedBucketUnreachable(id, address.bucket, ctx.stage, name === "NotFound" ? "it does not exist" : name === "Forbidden" ? "the key is refused" : name || String(error));
8703
+ } finally {
8704
+ client.destroy();
8705
+ }
8706
+ }
8707
+ }
8708
+ //#endregion
8709
+ //#region src/providers/registry.ts
8710
+ /**
8711
+ * The providers gkm ships, by kind and name — registered the way a deploy
8712
+ * target's built-ins are (`target/builtins.ts`). A provider from a package is
8713
+ * one more entry here once there is a loading story for it.
8714
+ */
8715
+ const BUILTIN_PROVIDERS = { objects: { s3: {
8716
+ kind: "objects",
8717
+ name: "s3",
8718
+ check: require_workspace.checkS3Config,
8719
+ provisioning: require_workspace.S3_PROVISIONING,
8720
+ credentials: awsProvisioningCredentials,
8721
+ runtimeKeys(manifest) {
8722
+ return bucketConstructs(manifest).flatMap((b) => [(0, _geekmidas_manifest.provideKey)(b.id, "url"), ...b.servers.map((s) => (0, _geekmidas_manifest.provideKey)(s.id, "url"))]);
8723
+ },
8724
+ ensure: ensureS3,
8725
+ verify: verifyS3
8726
+ } } };
8727
+ /** The provider `kind` names `name`, if gkm has one. */
8728
+ function builtinProvider(kind, name) {
8729
+ return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
8730
+ }
8731
+ //#endregion
8732
+ //#region src/providers/index.ts
8733
+ /** The constructs of a kind: `objects` is every bucket. */
8734
+ function constructsOf(manifest, kind) {
8735
+ return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
8736
+ }
8737
+ /**
8738
+ * What every deploy runs: each provider's `verify()` — the cheap check that
8739
+ * what it created is still there and the stage's key reaches it. Nothing on
8740
+ * the local stage, and nothing for a kind on `external`.
8741
+ *
8742
+ * @throws {StageProviderDisabled} for a declared kind the stage has none of
8743
+ */
8744
+ async function verifyStageProviders(input) {
8745
+ assertStageProvidersEnabled(input.workspace, input.manifest, input.stage);
8746
+ const verified = [];
8747
+ for (const kind of PROVIDER_KINDS) {
8748
+ if (constructsOf(input.manifest, kind).length === 0) continue;
8749
+ const choice = require_workspace.stageProvider(input.workspace, kind, input.stage);
8750
+ if (choice.mode !== "provider") continue;
8751
+ const provider = builtinProvider(kind, choice.name);
8752
+ await provider.verify({
8753
+ workspace: input.workspace,
8754
+ manifest: input.manifest,
8755
+ stage: input.stage,
8756
+ config: choice.config,
8757
+ secrets: input.secrets
8758
+ });
8759
+ verified.push(`${kind}: ${provider.name}`);
8760
+ }
8761
+ return verified;
8762
+ }
8763
+ /**
8764
+ * `ensure()` for every provider the stage configures — find or create, fix
8765
+ * drift, record in the stage's state, write the runtime keys into its
8766
+ * secrets. A provider without provisioning credentials leaves the stage as
8767
+ * `external` and says how to supply them.
8768
+ */
8769
+ async function provisionStage(options) {
8770
+ const { workspace, stage } = options;
8771
+ const dryRun = options.dryRun === true;
8772
+ const env = options.env ?? process.env;
8773
+ const log = options.log ?? ((line) => console.log(line));
8774
+ const named = PROVIDER_KINDS.some((kind) => require_workspace.stageProvider(workspace, kind, stage).mode === "provider");
8775
+ let found = options.manifest;
8776
+ let runnables = options.runnables ?? {};
8777
+ if (!found && named) {
8778
+ runnables = {};
8779
+ found = await require_discover.discover({
8780
+ patterns: constructGlobs(workspace),
8781
+ cwd: workspace.root,
8782
+ runnables
8783
+ });
8784
+ }
8785
+ const manifest = found ?? {};
8786
+ const reports = [];
8787
+ const configured = [];
8788
+ for (const kind of PROVIDER_KINDS) {
8789
+ const choice = require_workspace.stageProvider(workspace, kind, stage);
8790
+ if (choice.mode !== "provider") {
8791
+ reports.push({
8792
+ kind,
8793
+ mode: choice.mode,
8794
+ actions: []
8795
+ });
8796
+ continue;
8797
+ }
8798
+ const provider = builtinProvider(kind, choice.name);
8799
+ const report = {
8800
+ kind,
8801
+ mode: "provider",
8802
+ provider: provider.name,
8803
+ actions: []
8804
+ };
8805
+ reports.push(report);
8806
+ if (constructsOf(manifest, kind).length === 0) {
8807
+ log(` ${kind}: ${provider.name} — nothing declared to provision`);
8808
+ continue;
8809
+ }
8810
+ const credential = await provider.credentials({
8811
+ stage,
8812
+ env,
8813
+ ...options.profile ? { profile: options.profile } : {},
8814
+ ...options.home ? { home: options.home } : {}
8815
+ });
8816
+ report.credentials = credential !== void 0;
8817
+ if (credential === void 0) {
8818
+ log(` ${kind}: ${provider.name} — no credentials to provision with, so '${stage}' is external for now: its keys (${provider.runtimeKeys(manifest).join(", ")}) are yours to set with gkm secrets:add --stage ${stage}. To have gkm create them, supply ${provider.provisioning.describe}` + (provider.provisioning.login ? `, or run gkm login --provider ${provider.provisioning.login}` : "") + `, and run ${provisionCommand(stage)}.`);
8819
+ continue;
8820
+ }
8821
+ configured.push({
8822
+ kind,
8823
+ provider,
8824
+ config: choice.config,
8825
+ credential,
8826
+ report
8827
+ });
8828
+ }
8829
+ if (configured.length === 0) return reports;
8830
+ const identity = deployIdentity(workspace, stage);
8831
+ const secretsStore = await secretsStoreFor(workspace, stage, {
8832
+ ...options.profile ? { profile: options.profile } : {},
8833
+ ...options.home ? { home: options.home } : {}
8834
+ });
8835
+ let secrets = (await secretsStore.read(stage))?.custom ?? {};
8836
+ const stateStore = await createStateStore({
8837
+ config: workspace.state,
8838
+ workspaceRoot: workspace.root,
8839
+ workspaceName: workspace.name
8840
+ });
8841
+ const lock = dryRun ? void 0 : await stateStore.lock(stage, { operation: "provision" });
8842
+ try {
8843
+ const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => createEmptyState(stage, "", "")));
8844
+ const writeSecrets = async (values) => {
8845
+ if (dryRun) return;
8846
+ const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
8847
+ const next = {
8848
+ ...stored,
8849
+ custom: {
8850
+ ...stored.custom,
8851
+ ...values
8852
+ },
8853
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8854
+ };
8855
+ await secretsStore.write(stage, next);
8856
+ secrets = next.custom;
8857
+ };
8858
+ for (const entry of configured) {
8859
+ log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
8860
+ await entry.provider.ensure({
8861
+ workspace,
8862
+ manifest,
8863
+ runnables,
8864
+ stage,
8865
+ identity,
8866
+ config: entry.config,
8867
+ credential: entry.credential,
8868
+ get secrets() {
8869
+ return secrets;
8870
+ },
8871
+ state,
8872
+ dryRun,
8873
+ rotateKeys: options.rotateKeys === true,
8874
+ retireOldKeys: options.retireOldKeys === true,
8875
+ async change(action, apply) {
8876
+ if (!dryRun) await apply();
8877
+ entry.report.actions.push(action);
8878
+ log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
8879
+ },
8880
+ writeSecrets,
8881
+ log
8882
+ });
8883
+ if (entry.report.actions.length === 0) log(" up to date");
8884
+ }
8885
+ } finally {
8886
+ await lock?.release();
8887
+ }
8888
+ return reports;
8889
+ }
8890
+ /** The journal as `ensure()` sees it. */
8891
+ function journalState(journal) {
8892
+ return {
8893
+ record: (key) => journal.record(key),
8894
+ pending: (entry) => journal.pending(entry),
8895
+ ready: (entry, id) => journal.ready(entry, id),
8896
+ lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
8897
+ };
8898
+ }
8899
+ /** What a dry run reads, and the writes it does not make. */
8900
+ async function readOnlyState(store, stage) {
8901
+ const stored = await store.read(stage);
8902
+ return {
8903
+ record: (key) => stored?.resources[key],
8904
+ pending: async () => {},
8905
+ ready: async () => {},
8906
+ ...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
8907
+ };
8908
+ }
8909
+ //#endregion
7409
8910
  //#region src/secrets/stale.ts
7410
8911
  /**
7411
8912
  * Stored secrets an older gkm generated that now shadow a derived value.
@@ -7554,6 +9055,12 @@ function reportPlannedSeeds(planned, log) {
7554
9055
  for (const { folder, seeds } of planned) log(`🌱 ${folder}: would run ${seeds.length} (${seeds.join(", ")})`);
7555
9056
  }
7556
9057
  //#endregion
9058
+ Object.defineProperty(exports, "AllowDevServicesTakesNoValue", {
9059
+ enumerable: true,
9060
+ get: function() {
9061
+ return AllowDevServicesTakesNoValue;
9062
+ }
9063
+ });
7557
9064
  Object.defineProperty(exports, "CLUSTER_DATABASE", {
7558
9065
  enumerable: true,
7559
9066
  get: function() {
@@ -7608,6 +9115,12 @@ Object.defineProperty(exports, "DockerPushFailed", {
7608
9115
  return DockerPushFailed;
7609
9116
  }
7610
9117
  });
9118
+ Object.defineProperty(exports, "DocumentStateStore", {
9119
+ enumerable: true,
9120
+ get: function() {
9121
+ return DocumentStateStore;
9122
+ }
9123
+ });
7611
9124
  Object.defineProperty(exports, "EMULATOR_ACCESS_KEY_ID", {
7612
9125
  enumerable: true,
7613
9126
  get: function() {
@@ -7692,22 +9205,52 @@ Object.defineProperty(exports, "STORAGE_KEY_PAIR", {
7692
9205
  return STORAGE_KEY_PAIR;
7693
9206
  }
7694
9207
  });
7695
- Object.defineProperty(exports, "TELEMETRY_CONTAINER", {
9208
+ Object.defineProperty(exports, "StageProviderDisabled", {
7696
9209
  enumerable: true,
7697
9210
  get: function() {
7698
- return TELEMETRY_CONTAINER;
9211
+ return StageProviderDisabled;
7699
9212
  }
7700
9213
  });
7701
- Object.defineProperty(exports, "TURBO_VERSION", {
9214
+ Object.defineProperty(exports, "StageStateMissing", {
7702
9215
  enumerable: true,
7703
9216
  get: function() {
7704
- return TURBO_VERSION;
9217
+ return StageStateMissing;
9218
+ }
9219
+ });
9220
+ Object.defineProperty(exports, "StateLocked", {
9221
+ enumerable: true,
9222
+ get: function() {
9223
+ return StateLocked;
7705
9224
  }
7706
9225
  });
7707
- Object.defineProperty(exports, "UnknownDevService", {
9226
+ Object.defineProperty(exports, "StateStoreBusy", {
7708
9227
  enumerable: true,
7709
9228
  get: function() {
7710
- return UnknownDevService;
9229
+ return StateStoreBusy;
9230
+ }
9231
+ });
9232
+ Object.defineProperty(exports, "StateStoreWithoutLocking", {
9233
+ enumerable: true,
9234
+ get: function() {
9235
+ return StateStoreWithoutLocking;
9236
+ }
9237
+ });
9238
+ Object.defineProperty(exports, "StateVersionConflict", {
9239
+ enumerable: true,
9240
+ get: function() {
9241
+ return StateVersionConflict;
9242
+ }
9243
+ });
9244
+ Object.defineProperty(exports, "TELEMETRY_CONTAINER", {
9245
+ enumerable: true,
9246
+ get: function() {
9247
+ return TELEMETRY_CONTAINER;
9248
+ }
9249
+ });
9250
+ Object.defineProperty(exports, "TURBO_VERSION", {
9251
+ enumerable: true,
9252
+ get: function() {
9253
+ return TURBO_VERSION;
7711
9254
  }
7712
9255
  });
7713
9256
  Object.defineProperty(exports, "WORKER_PORT", {
@@ -7764,6 +9307,12 @@ Object.defineProperty(exports, "applyPostgres", {
7764
9307
  return applyPostgres;
7765
9308
  }
7766
9309
  });
9310
+ Object.defineProperty(exports, "assertDevServicesFlag", {
9311
+ enumerable: true,
9312
+ get: function() {
9313
+ return assertDevServicesFlag;
9314
+ }
9315
+ });
7767
9316
  Object.defineProperty(exports, "assertExternalServices", {
7768
9317
  enumerable: true,
7769
9318
  get: function() {
@@ -7776,6 +9325,12 @@ Object.defineProperty(exports, "assertNoStaleSecrets", {
7776
9325
  return assertNoStaleSecrets;
7777
9326
  }
7778
9327
  });
9328
+ Object.defineProperty(exports, "assertStageProvidersEnabled", {
9329
+ enumerable: true,
9330
+ get: function() {
9331
+ return assertStageProvidersEnabled;
9332
+ }
9333
+ });
7779
9334
  Object.defineProperty(exports, "backendsOf", {
7780
9335
  enumerable: true,
7781
9336
  get: function() {
@@ -7818,6 +9373,12 @@ Object.defineProperty(exports, "constructGlobs", {
7818
9373
  return constructGlobs;
7819
9374
  }
7820
9375
  });
9376
+ Object.defineProperty(exports, "contentVersion", {
9377
+ enumerable: true,
9378
+ get: function() {
9379
+ return contentVersion;
9380
+ }
9381
+ });
7821
9382
  Object.defineProperty(exports, "createCredentialsPreload", {
7822
9383
  enumerable: true,
7823
9384
  get: function() {
@@ -7836,6 +9397,12 @@ Object.defineProperty(exports, "createEntryWrapper", {
7836
9397
  return createEntryWrapper;
7837
9398
  }
7838
9399
  });
9400
+ Object.defineProperty(exports, "createStateStore", {
9401
+ enumerable: true,
9402
+ get: function() {
9403
+ return createStateStore;
9404
+ }
9405
+ });
7839
9406
  Object.defineProperty(exports, "deployIdentity", {
7840
9407
  enumerable: true,
7841
9408
  get: function() {
@@ -8100,16 +9667,16 @@ Object.defineProperty(exports, "networkEnv", {
8100
9667
  return networkEnv;
8101
9668
  }
8102
9669
  });
8103
- Object.defineProperty(exports, "ownershipMarker", {
9670
+ Object.defineProperty(exports, "newLockHolder", {
8104
9671
  enumerable: true,
8105
9672
  get: function() {
8106
- return ownershipMarker;
9673
+ return newLockHolder;
8107
9674
  }
8108
9675
  });
8109
- Object.defineProperty(exports, "parseDevServices", {
9676
+ Object.defineProperty(exports, "ownershipMarker", {
8110
9677
  enumerable: true,
8111
9678
  get: function() {
8112
- return parseDevServices;
9679
+ return ownershipMarker;
8113
9680
  }
8114
9681
  });
8115
9682
  Object.defineProperty(exports, "pgClient", {
@@ -8184,6 +9751,18 @@ Object.defineProperty(exports, "providerOf", {
8184
9751
  return providerOf;
8185
9752
  }
8186
9753
  });
9754
+ Object.defineProperty(exports, "provisionCommand", {
9755
+ enumerable: true,
9756
+ get: function() {
9757
+ return provisionCommand;
9758
+ }
9759
+ });
9760
+ Object.defineProperty(exports, "provisionStage", {
9761
+ enumerable: true,
9762
+ get: function() {
9763
+ return provisionStage;
9764
+ }
9765
+ });
8187
9766
  Object.defineProperty(exports, "quoteIdentifier", {
8188
9767
  enumerable: true,
8189
9768
  get: function() {
@@ -8328,6 +9907,12 @@ Object.defineProperty(exports, "stackRedis", {
8328
9907
  return stackRedis;
8329
9908
  }
8330
9909
  });
9910
+ Object.defineProperty(exports, "stageProviderNotes", {
9911
+ enumerable: true,
9912
+ get: function() {
9913
+ return stageProviderNotes;
9914
+ }
9915
+ });
8331
9916
  Object.defineProperty(exports, "storageBackendFor", {
8332
9917
  enumerable: true,
8333
9918
  get: function() {
@@ -8388,6 +9973,12 @@ Object.defineProperty(exports, "validateImageRef", {
8388
9973
  return validateImageRef;
8389
9974
  }
8390
9975
  });
9976
+ Object.defineProperty(exports, "verifyStageProviders", {
9977
+ enumerable: true,
9978
+ get: function() {
9979
+ return verifyStageProviders;
9980
+ }
9981
+ });
8391
9982
  Object.defineProperty(exports, "withCustomSecret", {
8392
9983
  enumerable: true,
8393
9984
  get: function() {
@@ -8455,4 +10046,4 @@ Object.defineProperty(exports, "workspace_exports", {
8455
10046
  }
8456
10047
  });
8457
10048
 
8458
- //# sourceMappingURL=seeds-DQ0FG57Z.cjs.map
10049
+ //# sourceMappingURL=seeds-CsI0A_Ef.cjs.map