@geekmidas/cli 10.0.0-alpha.85 → 10.0.0-alpha.87

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 (175) 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-CJ2o18nT.mjs → LegacyStateStore-B0R4LlX_.mjs} +2 -2
  6. package/dist/{LegacyStateStore-CJ2o18nT.mjs.map → LegacyStateStore-B0R4LlX_.mjs.map} +1 -1
  7. package/dist/{LegacyStateStore-xGxPwP-m.cjs → LegacyStateStore-CWG_HVC0.cjs} +7 -7
  8. package/dist/{LegacyStateStore-xGxPwP-m.cjs.map → LegacyStateStore-CWG_HVC0.cjs.map} +1 -1
  9. package/dist/{LocalStateStore-CkjFD-4b.cjs → LocalStateStore-CLwa2DpK.cjs} +8 -8
  10. package/dist/{LocalStateStore-CkjFD-4b.cjs.map → LocalStateStore-CLwa2DpK.cjs.map} +1 -1
  11. package/dist/{LocalStateStore-ByVjUIvS.mjs → LocalStateStore-Q4vXg2ua.mjs} +2 -2
  12. package/dist/{LocalStateStore-ByVjUIvS.mjs.map → LocalStateStore-Q4vXg2ua.mjs.map} +1 -1
  13. package/dist/{S3StateStore-iqMezbES.cjs → S3StateStore-Cea9_5gF.cjs} +5 -5
  14. package/dist/{S3StateStore-iqMezbES.cjs.map → S3StateStore-Cea9_5gF.cjs.map} +1 -1
  15. package/dist/{S3StateStore-kpFlGVLE.mjs → S3StateStore-CksKYKRa.mjs} +2 -2
  16. package/dist/{S3StateStore-kpFlGVLE.mjs.map → S3StateStore-CksKYKRa.mjs.map} +1 -1
  17. package/dist/{SSMStateStore-Bkz5ztfo.mjs → SSMStateStore-BszSCAS2.mjs} +2 -2
  18. package/dist/{SSMStateStore-Bkz5ztfo.mjs.map → SSMStateStore-BszSCAS2.mjs.map} +1 -1
  19. package/dist/{SSMStateStore-Z2OMVCtq.cjs → SSMStateStore-aBOTF018.cjs} +9 -9
  20. package/dist/{SSMStateStore-Z2OMVCtq.cjs.map → SSMStateStore-aBOTF018.cjs.map} +1 -1
  21. package/dist/{add-CM7RLj89.cjs → add-5MZAXybv.cjs} +8 -5
  22. package/dist/add-5MZAXybv.cjs.map +1 -0
  23. package/dist/{add-jpX7M8RB.mjs → add-D_jNqnfZ.mjs} +8 -5
  24. package/dist/add-D_jNqnfZ.mjs.map +1 -0
  25. package/dist/{bundler-lEBH8ENP.cjs → bundler-1REMhKps.cjs} +5 -5
  26. package/dist/{bundler-lEBH8ENP.cjs.map → bundler-1REMhKps.cjs.map} +1 -1
  27. package/dist/{bundler-CaAKsbcN.mjs → bundler-BMo-JP5s.mjs} +5 -5
  28. package/dist/{bundler-CaAKsbcN.mjs.map → bundler-BMo-JP5s.mjs.map} +1 -1
  29. package/dist/{cli-BV3jzbk0.mjs → cli-7edNFwGk.mjs} +5 -13
  30. package/dist/cli-7edNFwGk.mjs.map +1 -0
  31. package/dist/{cli-DQCBcNlN.cjs → cli-BL8ZHsLQ.cjs} +5 -13
  32. package/dist/cli-BL8ZHsLQ.cjs.map +1 -0
  33. package/dist/{compose-CQK6StuE.mjs → compose-DbJhRQlh.mjs} +24 -11
  34. package/dist/compose-DbJhRQlh.mjs.map +1 -0
  35. package/dist/{compose-CSR0yEJm.cjs → compose-xw86kVYi.cjs} +34 -21
  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-BuU117CA.mjs → deploy-BTfmXd3g.mjs} +9 -9
  58. package/dist/deploy-BTfmXd3g.mjs.map +1 -0
  59. package/dist/{deploy-Bw2Mm3x3.cjs → deploy-CtbZj4zN.cjs} +13 -13
  60. package/dist/deploy-CtbZj4zN.cjs.map +1 -0
  61. package/dist/{dokploy-C81J1MVK.cjs → dokploy-TGk3oPwr.cjs} +2 -2
  62. package/dist/{dokploy-C81J1MVK.cjs.map → dokploy-TGk3oPwr.cjs.map} +1 -1
  63. package/dist/{dokploy-DWALWk7u.mjs → dokploy-r69yZaIt.mjs} +2 -2
  64. package/dist/{dokploy-DWALWk7u.mjs.map → dokploy-r69yZaIt.mjs.map} +1 -1
  65. package/dist/{encryption-DpAum8W7.mjs → encryption-C-8Tt5Hh.mjs} +3 -3
  66. package/dist/{encryption-DpAum8W7.mjs.map → encryption-C-8Tt5Hh.mjs.map} +1 -1
  67. package/dist/{encryption-B8h8Tna0.cjs → encryption-CDn7BH5O.cjs} +3 -3
  68. package/dist/{encryption-B8h8Tna0.cjs.map → encryption-CDn7BH5O.cjs.map} +1 -1
  69. package/dist/{engine-B01fjhLt.cjs → engine-Bt5k_UMq.cjs} +33 -23
  70. package/dist/engine-Bt5k_UMq.cjs.map +1 -0
  71. package/dist/{engine-BXCtEhSH.mjs → engine-jAINgiOB.mjs} +28 -18
  72. package/dist/engine-jAINgiOB.mjs.map +1 -0
  73. package/dist/{github-8B68qrLs.mjs → github-CZHAtwPT.mjs} +3 -3
  74. package/dist/{github-8B68qrLs.mjs.map → github-CZHAtwPT.mjs.map} +1 -1
  75. package/dist/{github-Cis9tYur.cjs → github-DFhuJQlp.cjs} +3 -3
  76. package/dist/{github-Cis9tYur.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 +106 -68
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.mjs +100 -62
  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-CPWn7A51.cjs → migrate-B16qgYTn.cjs} +3 -3
  90. package/dist/{migrate-CPWn7A51.cjs.map → migrate-B16qgYTn.cjs.map} +1 -1
  91. package/dist/{migrate-BBCp5blp.cjs → migrate-B1rVlwj1.cjs} +3 -3
  92. package/dist/{migrate-BBCp5blp.cjs.map → migrate-B1rVlwj1.cjs.map} +1 -1
  93. package/dist/{migrate-sDq7NApJ.mjs → migrate-Dn7nK6sh.mjs} +3 -3
  94. package/dist/{migrate-sDq7NApJ.mjs.map → migrate-Dn7nK6sh.mjs.map} +1 -1
  95. package/dist/{migrate-BeTWLSeC.mjs → migrate-ggkPVJRC.mjs} +3 -3
  96. package/dist/{migrate-BeTWLSeC.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-CXbwJ_iu.mjs → resolve-BSke45k6.mjs} +4 -4
  104. package/dist/{resolve-CXbwJ_iu.mjs.map → resolve-BSke45k6.mjs.map} +1 -1
  105. package/dist/{resolve-CztIqBOI.cjs → resolve-BXeXzpmU.cjs} +4 -4
  106. package/dist/{resolve-CztIqBOI.cjs.map → resolve-BXeXzpmU.cjs.map} +1 -1
  107. package/dist/{rollback-ByyFE5lV.mjs → rollback-CEE6dse4.mjs} +6 -6
  108. package/dist/{rollback-ByyFE5lV.mjs.map → rollback-CEE6dse4.mjs.map} +1 -1
  109. package/dist/{rollback-CyIJ4xGJ.cjs → rollback-DFMiBWf0.cjs} +8 -8
  110. package/dist/{rollback-CyIJ4xGJ.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-9C5CkDon.mjs → seeds-BYdrtZsC.mjs} +1525 -53
  116. package/dist/seeds-BYdrtZsC.mjs.map +1 -0
  117. package/dist/{seeds-B_Beuv0o.cjs → seeds-CsI0A_Ef.cjs} +1628 -65
  118. package/dist/seeds-CsI0A_Ef.cjs.map +1 -0
  119. package/dist/{sniffer-DgBkaLAu.mjs → sniffer-B0EU73bj.mjs} +5 -360
  120. package/dist/sniffer-B0EU73bj.mjs.map +1 -0
  121. package/dist/{sniffer-3fA0iExx.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/add-CM7RLj89.cjs.map +0 -1
  153. package/dist/add-jpX7M8RB.mjs.map +0 -1
  154. package/dist/cli-BV3jzbk0.mjs.map +0 -1
  155. package/dist/cli-DQCBcNlN.cjs.map +0 -1
  156. package/dist/compose-CQK6StuE.mjs.map +0 -1
  157. package/dist/compose-CSR0yEJm.cjs.map +0 -1
  158. package/dist/credentials-BVs7mrtw.cjs.map +0 -1
  159. package/dist/credentials-pAtB70k3.mjs.map +0 -1
  160. package/dist/deploy-BuU117CA.mjs.map +0 -1
  161. package/dist/deploy-Bw2Mm3x3.cjs.map +0 -1
  162. package/dist/engine-B01fjhLt.cjs.map +0 -1
  163. package/dist/engine-BXCtEhSH.mjs.map +0 -1
  164. package/dist/run-BJLIerMM.mjs +0 -133
  165. package/dist/run-BJLIerMM.mjs.map +0 -1
  166. package/dist/run-fVZ-rLnc.cjs +0 -174
  167. package/dist/run-fVZ-rLnc.cjs.map +0 -1
  168. package/dist/seeds-9C5CkDon.mjs.map +0 -1
  169. package/dist/seeds-B_Beuv0o.cjs.map +0 -1
  170. package/dist/sniffer-3fA0iExx.cjs.map +0 -1
  171. package/dist/sniffer-DgBkaLAu.mjs.map +0 -1
  172. package/dist/types-BZn-8ssD.d.mts.map +0 -1
  173. package/dist/types-DiZj6hqp.d.cts.map +0 -1
  174. package/dist/workspace-Bic_R3tz.cjs.map +0 -1
  175. package/dist/workspace-Drz4jsvy.mjs.map +0 -1
@@ -1,23 +1,23 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
- import { i as run$1, s as gkmHome } from "./run-BJLIerMM.mjs";
2
+ import { r as storedCredentials, s as run$1, v as gkmHome } from "./credentials-BgJ5mS_d.mjs";
3
3
  import { d as output, r as discover, u as ConstructGenerator } from "./discover-BPelzeGW.mjs";
4
4
  import { t as appKey } from "./derive-B4vDpzgP.mjs";
5
- import { I as stageSampleRate, L as telemetryEnv, R as LOGS_PORT, U as UnknownSecretsStoreProvider, g as getPublicEnvPrefix, h as PUBLIC_ENV_PREFIXES, k as TEST_STAGE, r as allConstructGlobs } from "./workspace-Drz4jsvy.mjs";
5
+ import { B as provisioningOf, G as BucketKeySetElsewhere, H as S3_PROVISIONING, I as stageSampleRate, J as IamUserNotOwned, K as BucketNameUnavailable, L as telemetryEnv, Q as S3RegionRequired, R as LOGS_PORT, U as checkS3Config, V as stageProvider, W as AccessKeyLimit, X as RecordedBucketUnreachable, Y as ProvisionedBucketUnreachable, Z as RotationInProgress, g as getPublicEnvPrefix, h as PUBLIC_ENV_PREFIXES, k as TEST_STAGE, q as BucketOwnedByAnotherProject, r as allConstructGlobs, rt as UnknownSecretsStoreProvider } from "./workspace-D2CYLMCW.mjs";
6
6
  import { ConfigNotFound, NotInAnApp, loadWorkspaceAppInfo, loadWorkspaceConfig } from "./config.mjs";
7
7
  import { a as telemetryArgs, c as startTelemetryCode, n as runtimeFor, u as writeTelemetryModule } from "./EndpointGenerator-CVc6Oi1Y.mjs";
8
8
  import { a as DEFAULT_STORAGE, i as DEFAULT_EVENTS$1, n as readFakes, o as holdsEveryCache, r as DEFAULT_CACHE, t as SubscriberGenerator } from "./SubscriberGenerator-BPqQf8fB.mjs";
9
- import { c as runtimeOf } from "./resolve-CXbwJ_iu.mjs";
9
+ import { c as runtimeOf } from "./resolve-BSke45k6.mjs";
10
10
  import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
11
11
  import { constants, existsSync, readFileSync } from "node:fs";
12
12
  import { appendFile, chmod, copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
13
- import { homedir, networkInterfaces } from "node:os";
13
+ import { homedir, hostname, networkInterfaces, userInfo } from "node:os";
14
14
  import { promisify } from "node:util";
15
15
  import { Cron } from "@geekmidas/constructs/crons";
16
16
  import { Queue } from "@geekmidas/constructs/queue";
17
17
  import { CLIENT_IP_HEADER, DEFAULT_POSTGRES_VERSION, TELEMETRY_KEYS, cacheTable, cookieDomain, dependenciesOf, dependentsOf, externalApiUrl, kebabCase, mobileOrigins, provideKey, providedKeyFor, provisionOrder, publicEnvFor, schemeBase, scopedName } from "@geekmidas/manifest";
18
18
  import { createServer } from "node:net";
19
19
  import { execFile, spawn } from "node:child_process";
20
- import { createCipheriv, createDecipheriv, createHash, randomBytes, randomInt } from "node:crypto";
20
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
21
21
  import { config } from "dotenv";
22
22
  import { stringify } from "yaml";
23
23
  import { formatKeyring, generateKeyring } from "@geekmidas/constructs/encryption";
@@ -29,6 +29,7 @@ import { CreateTopicCommand, GetTopicAttributesCommand, SNSClient } from "@aws-s
29
29
  import { CreateQueueCommand, GetQueueUrlCommand, SQSClient } from "@aws-sdk/client-sqs";
30
30
  import { Client } from "pg";
31
31
  import { cacheTableStatements } from "@geekmidas/cache/postgres";
32
+ import * as s3Url from "@geekmidas/storage/s3-url";
32
33
  //#region src/workspace/rootSite.ts
33
34
  /**
34
35
  * Which site the base domain points at — deployed and locally alike.
@@ -4875,7 +4876,7 @@ function bucketPolicies(plan) {
4875
4876
  * client, can be admitted by the policy where the client would have said no.
4876
4877
  * Prefer `**` where crossing segments is what you meant.
4877
4878
  */
4878
- function bucketPolicy(bucket, open) {
4879
+ function bucketPolicy$1(bucket, open) {
4879
4880
  return JSON.stringify({
4880
4881
  Version: "2012-10-17",
4881
4882
  Statement: [{
@@ -4957,7 +4958,7 @@ async function databasesExist(client, names) {
4957
4958
  async function applyPolicies(client, policies) {
4958
4959
  const applied = [];
4959
4960
  for (const { bucket, open } of policies) {
4960
- const wanted = bucketPolicy(bucket, open);
4961
+ const wanted = bucketPolicy$1(bucket, open);
4961
4962
  const describe = `open paths on ${bucket}`;
4962
4963
  if (equivalent(await client.policy(bucket), wanted)) {
4963
4964
  applied.push({
@@ -6605,15 +6606,21 @@ function isUnder(file, dir) {
6605
6606
  * chain applies — the stage's shared `AWS_ACCESS_KEY_ID`/
6606
6607
  * `AWS_SECRET_ACCESS_KEY` when it set them, or a role.
6607
6608
  *
6608
- * `--allow-dev-services minio,mailpit` is the way out for a stage that is
6609
- * not production — a preview, a demo: the target runs the dev service and
6610
- * derives the keys from it. Keys the stage did set still win. Every run that
6611
- * uses one says so, loudly.
6609
+ * `--allow-dev-services` is the way out for a stage that is not production —
6610
+ * a preview, a demo: every construct the stage does not account for gets the
6611
+ * dev service the local stage runs for it — a bucket MinIO, mail Mailpit —
6612
+ * and the target derives the keys from it. What the stage does account for
6613
+ * still wins: a key set in its secrets, or a provider (`deploy.objects`)
6614
+ * that backs the kind. Every run that uses one says so, loudly.
6612
6615
  *
6613
6616
  * Pure: what each target runs is decided here from declarations and the
6614
6617
  * stage's values, and the targets do the running.
6615
6618
  */
6616
- /** The dev services a deployed stage may be allowed to run. */
6619
+ /**
6620
+ * The dev services a deployed stage can run — one per kind that has one on
6621
+ * every server target: a bucket's MinIO and mail's Mailpit. A cache or a
6622
+ * database is not here because a server stage runs its own anyway.
6623
+ */
6617
6624
  const DEV_SERVICES = ["minio", "mailpit"];
6618
6625
  /**
6619
6626
  * The S3 client's shared key pair. Optional: a bucket whose URL carries its own
@@ -6622,27 +6629,45 @@ const DEV_SERVICES = ["minio", "mailpit"];
6622
6629
  * chain finds — a role.
6623
6630
  */
6624
6631
  const STORAGE_KEY_PAIR = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"];
6625
- /** A `--allow-dev-services` value that is not a dev service. */
6626
- var UnknownDevService = class extends Error {
6627
- value;
6628
- constructor(value) {
6629
- 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(",")}`);
6630
- this.value = value;
6631
- this.name = "UnknownDevService";
6632
- }
6633
- };
6634
6632
  /** Dev services asked of a target that runs no containers. */
6635
6633
  var DevServicesNeedServerTarget = class extends Error {
6636
6634
  target;
6637
- services;
6638
- constructor(target, services) {
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 ${services.join(",")}.`);
6635
+ constructor(target) {
6636
+ 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.`);
6640
6637
  this.target = target;
6641
- this.services = services;
6642
6638
  this.name = "DevServicesNeedServerTarget";
6643
6639
  }
6644
6640
  };
6645
6641
  /**
6642
+ * `--allow-dev-services` given a value — the list it used to take. It is a
6643
+ * switch now: every construct the stage does not account for gets its dev
6644
+ * service.
6645
+ */
6646
+ var AllowDevServicesTakesNoValue = class extends Error {
6647
+ value;
6648
+ constructor(value) {
6649
+ 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.`);
6650
+ this.value = value;
6651
+ this.name = "AllowDevServicesTakesNoValue";
6652
+ }
6653
+ };
6654
+ /**
6655
+ * Refuses `--allow-dev-services <value>` and `--allow-dev-services=<value>` on
6656
+ * a command line, with the reason, before the parser calls the value an
6657
+ * excess argument. `gkm deploy` and `gkm compose` take no positional
6658
+ * argument, so whatever follows the flag and is not a flag was meant for it.
6659
+ *
6660
+ * @throws {AllowDevServicesTakesNoValue}
6661
+ */
6662
+ function assertDevServicesFlag(argv) {
6663
+ for (let i = 0; i < argv.length; i++) {
6664
+ const arg = argv[i];
6665
+ if (arg.startsWith("--allow-dev-services=")) throw new AllowDevServicesTakesNoValue(arg.slice(arg.indexOf("=") + 1));
6666
+ const next = argv[i + 1];
6667
+ if (arg === "--allow-dev-services" && next && !next.startsWith("-")) throw new AllowDevServicesTakesNoValue(next);
6668
+ }
6669
+ }
6670
+ /**
6646
6671
  * A deployed stage with mail or object storage that its secrets do not
6647
6672
  * configure — every missing key, across every app, at once.
6648
6673
  */
@@ -6650,33 +6675,18 @@ var ExternalServicesNotConfigured = class extends Error {
6650
6675
  stage;
6651
6676
  missing;
6652
6677
  constructor(stage, missing) {
6653
- const services = [...new Set(missing.map((m) => m.service))].sort();
6678
+ const standIns = missing.some((m) => m.service !== void 0);
6654
6679
  const lines = missing.map((m) => {
6655
6680
  const by = m.apps?.length ? `, read by ${m.apps.join(", ")}` : "";
6656
- return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "");
6681
+ return ` gkm secrets:set ${m.key} '${m.example}' --stage ${stage}\n ${m.what}${by}` + (m.note ? `\n ${m.note}` : "") + (m.hint ? `\n ${m.hint}` : "");
6657
6682
  });
6658
- 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}`);
6683
+ 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}`);
6659
6684
  this.stage = stage;
6660
6685
  this.missing = missing;
6661
6686
  this.name = "ExternalServicesNotConfigured";
6662
6687
  }
6663
6688
  };
6664
6689
  /**
6665
- * The dev services a value names: `'minio,mailpit'`, or a list of them.
6666
- *
6667
- * @throws {UnknownDevService} for anything that is not one
6668
- */
6669
- function parseDevServices(value) {
6670
- if (value === void 0) return [];
6671
- const items = (typeof value === "string" ? [value] : value).flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
6672
- const services = /* @__PURE__ */ new Set();
6673
- for (const item of items) {
6674
- if (!DEV_SERVICES.includes(item)) throw new UnknownDevService(item);
6675
- services.add(item);
6676
- }
6677
- return [...services].sort();
6678
- }
6679
- /**
6680
6690
  * Every mail and storage construct a manifest declares, with the apps that
6681
6691
  * depend on each — what a target that provisions the whole manifest checks.
6682
6692
  */
@@ -6718,6 +6728,8 @@ function requiredStageKeys(input) {
6718
6728
  const apps = (d) => d.apps?.length ? { apps: [...d.apps].sort() } : {};
6719
6729
  if (!input.local) {
6720
6730
  const services = byId(input.services);
6731
+ const objects = input.providers?.objects;
6732
+ const storage = objects?.accounted ? objects.hint ? { hint: objects.hint } : {} : { service: "minio" };
6721
6733
  for (const d of services.filter((d) => d.kind === "email")) {
6722
6734
  add({
6723
6735
  key: provideKey(d.id, "url"),
@@ -6745,7 +6757,7 @@ function requiredStageKeys(input) {
6745
6757
  what: `the bucket '${d.id}' — S3, R2, or any S3-compatible store (add &endpoint=… for one that is not S3)`,
6746
6758
  note: `credentials are optional: a key for this bucket alone in the URL (s3://KEY:SECRET@${appKey(d.id)}?…) wins; without one, the shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or a role signs`,
6747
6759
  example: `s3://${appKey(d.id)}?region=eu-west-1`,
6748
- service: "minio",
6760
+ ...storage,
6749
6761
  ...apps(d)
6750
6762
  });
6751
6763
  for (const d of services.filter((d) => d.kind === "file-server")) add({
@@ -6754,7 +6766,7 @@ function requiredStageKeys(input) {
6754
6766
  kind: "file-server",
6755
6767
  what: `the public address '${d.id}' serves its bucket on — a CDN or the bucket's own domain`,
6756
6768
  example: `https://${appKey(d.id)}.${domain}`,
6757
- service: "minio",
6769
+ ...storage,
6758
6770
  ...d.of ? { of: d.of } : {},
6759
6771
  ...apps(d)
6760
6772
  });
@@ -6803,20 +6815,23 @@ function externalServices(input) {
6803
6815
  const required = requiredStageKeys({
6804
6816
  local: false,
6805
6817
  services: input.declarations,
6806
- ...input.domain ? { domain: input.domain } : {}
6818
+ ...input.domain ? { domain: input.domain } : {},
6819
+ ...input.providers ? { providers: input.providers } : {}
6807
6820
  });
6808
- const mailpit = required.filter((k) => k.kind === "email" && k.key === provideKey(k.id, "url")).filter((k) => !has(k.key) && allow.includes("mailpit")).map((k) => k.id);
6809
- const minio = required.filter((k) => k.kind === "bucket").filter((k) => !has(k.key) && allow.includes("minio")).map((k) => k.id);
6821
+ const standIn = (k) => allow && k.service !== void 0 && !has(k.key);
6822
+ const mailpit = required.filter((k) => k.kind === "email" && k.key === provideKey(k.id, "url")).filter(standIn).map((k) => k.id);
6823
+ const minio = required.filter((k) => k.kind === "bucket").filter(standIn).map((k) => k.id);
6810
6824
  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);
6811
6825
  return {
6812
- missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps }) => ({
6826
+ missing: required.filter((k) => !has(k.key) && !covered(k)).map(({ key, id, what, note, example, service, apps, hint }) => ({
6813
6827
  key,
6814
6828
  id,
6815
6829
  what,
6816
6830
  ...note ? { note } : {},
6817
6831
  example,
6818
- service,
6819
- ...apps ? { apps } : {}
6832
+ ...service ? { service } : {},
6833
+ ...apps ? { apps } : {},
6834
+ ...hint ? { hint } : {}
6820
6835
  })),
6821
6836
  minio,
6822
6837
  mailpit
@@ -6865,6 +6880,360 @@ function reportDevServices(ctx, uses) {
6865
6880
  }
6866
6881
  }
6867
6882
  //#endregion
6883
+ //#region src/deploy/StateProvider.ts
6884
+ /**
6885
+ * Check if value is a StateProvider implementation.
6886
+ */
6887
+ function isStateProvider(value) {
6888
+ return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
6889
+ }
6890
+ //#endregion
6891
+ //#region src/deploy/StateStore.ts
6892
+ /**
6893
+ * Deploy state store.
6894
+ *
6895
+ * `StateProvider` reads and writes a whole stage at once, with nothing to stop
6896
+ * two runs of the same stage from overwriting each other and nowhere to record
6897
+ * a resource the moment it exists. A `StateStore` adds the three things a
6898
+ * deploy needs to be safe against crashes and concurrent runs:
6899
+ *
6900
+ * - `lock(stage)`: one run per stage. A second gets `StateLocked`, naming the
6901
+ * holder, instead of racing the first.
6902
+ * - Versioned `read` and conditional `write`: a write names the version it was
6903
+ * based on, and raises `StateVersionConflict` when the stored state moved on
6904
+ * underneath it, so nothing is lost silently even without the lock.
6905
+ * - Per-resource records: `putResource` journals one resource at a time, so a
6906
+ * run that dies half way leaves behind the ids of everything it created.
6907
+ *
6908
+ * Built-in stores: `LocalStateStore`, `SSMStateStore`, `S3StateStore`. A
6909
+ * custom `StateProvider` is wrapped by `LegacyStateStore`, which keeps it
6910
+ * working but cannot lock.
6911
+ */
6912
+ var StateLocked = class extends Error {
6913
+ stage;
6914
+ holder;
6915
+ location;
6916
+ constructor(stage, holder, location) {
6917
+ 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";
6918
+ 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}\`.`);
6919
+ this.stage = stage;
6920
+ this.holder = holder;
6921
+ this.location = location;
6922
+ this.name = "StateLocked";
6923
+ }
6924
+ };
6925
+ var StateVersionConflict = class extends Error {
6926
+ stage;
6927
+ expectedVersion;
6928
+ actualVersion;
6929
+ constructor(stage, expectedVersion, actualVersion) {
6930
+ 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.`);
6931
+ this.stage = stage;
6932
+ this.expectedVersion = expectedVersion;
6933
+ this.actualVersion = actualVersion;
6934
+ this.name = "StateVersionConflict";
6935
+ }
6936
+ };
6937
+ var StageStateMissing = class extends Error {
6938
+ stage;
6939
+ constructor(stage) {
6940
+ super(`There is no deploy state for stage '${stage}' to record a resource in. Write the stage's state first (write with expectedVersion: null).`);
6941
+ this.stage = stage;
6942
+ this.name = "StageStateMissing";
6943
+ }
6944
+ };
6945
+ var StateUnreadable = class extends Error {
6946
+ stage;
6947
+ location;
6948
+ constructor(stage, location, cause) {
6949
+ 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 });
6950
+ this.stage = stage;
6951
+ this.location = location;
6952
+ this.name = "StateUnreadable";
6953
+ }
6954
+ };
6955
+ var StateSchemaTooNew = class extends Error {
6956
+ stage;
6957
+ schemaVersion;
6958
+ constructor(stage, schemaVersion) {
6959
+ 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.`);
6960
+ this.stage = stage;
6961
+ this.schemaVersion = schemaVersion;
6962
+ this.name = "StateSchemaTooNew";
6963
+ }
6964
+ };
6965
+ /**
6966
+ * Not thrown: emitted (once per store) when a custom `StateProvider` stands
6967
+ * in for a store, so a deploy that cannot be protected from a concurrent one
6968
+ * says so.
6969
+ */
6970
+ var StateStoreWithoutLocking = class extends Error {
6971
+ provider;
6972
+ constructor(provider) {
6973
+ 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.`);
6974
+ this.provider = provider;
6975
+ this.name = "StateStoreWithoutLocking";
6976
+ }
6977
+ };
6978
+ var StateStoreNeedsWorkspaceName = class extends Error {
6979
+ provider;
6980
+ constructor(provider) {
6981
+ super(`The '${provider}' state provider keys state by workspace name. Set "name" in gkm.config.ts.`);
6982
+ this.provider = provider;
6983
+ this.name = "StateStoreNeedsWorkspaceName";
6984
+ }
6985
+ };
6986
+ var UnknownStateProvider = class extends Error {
6987
+ config;
6988
+ constructor(config) {
6989
+ super(`Unknown state provider ${JSON.stringify(config)}. Use 'local', 'ssm', 's3', or an object implementing StateStore or StateProvider.`);
6990
+ this.config = config;
6991
+ this.name = "UnknownStateProvider";
6992
+ }
6993
+ };
6994
+ /** A lock or write that could not get at the state backend in time. */
6995
+ var StateStoreBusy = class extends Error {
6996
+ stage;
6997
+ location;
6998
+ constructor(stage, location) {
6999
+ super(`Timed out waiting to write deploy state for stage '${stage}' (${location}). If no other gkm process is running, remove ${location}.`);
7000
+ this.stage = stage;
7001
+ this.location = location;
7002
+ this.name = "StateStoreBusy";
7003
+ }
7004
+ };
7005
+ function decodeDocument(stage, body, location) {
7006
+ let parsed;
7007
+ try {
7008
+ parsed = JSON.parse(body);
7009
+ } catch (error) {
7010
+ throw new StateUnreadable(stage, location, error);
7011
+ }
7012
+ if (typeof parsed !== "object" || parsed === null) throw new StateUnreadable(stage, location, parsed);
7013
+ const schemaVersion = parsed.schemaVersion;
7014
+ if (schemaVersion === void 0) return {
7015
+ schemaVersion: 1,
7016
+ state: parsed
7017
+ };
7018
+ if (schemaVersion === 2) return {
7019
+ schemaVersion: 2,
7020
+ document: parsed
7021
+ };
7022
+ throw new StateSchemaTooNew(stage, schemaVersion);
7023
+ }
7024
+ function encodeDocument(document) {
7025
+ return JSON.stringify(document, null, 2);
7026
+ }
7027
+ /**
7028
+ * v1 state as a v2 document. The ids v1 kept are seeded as `ready` records,
7029
+ * so the first journaled deploy after an upgrade adopts them rather than
7030
+ * treating them as unknown.
7031
+ */
7032
+ function migrateV1(stage, state) {
7033
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7034
+ const resources = {};
7035
+ const seed = (type, name, id) => {
7036
+ if (!id) return;
7037
+ const key = name ? `${type}:${name}` : type;
7038
+ resources[key] = {
7039
+ key,
7040
+ type,
7041
+ id,
7042
+ status: "ready",
7043
+ updatedAt: now
7044
+ };
7045
+ };
7046
+ seed("project", null, state.projectId);
7047
+ seed("environment", null, state.environmentId);
7048
+ for (const [app, id] of Object.entries(state.applications ?? {})) seed("application", app, id);
7049
+ seed("postgres", null, state.services?.postgresId);
7050
+ seed("redis", null, state.services?.redisId);
7051
+ seed("backup-destination", null, state.backups?.destinationId);
7052
+ return {
7053
+ schemaVersion: 2,
7054
+ stage,
7055
+ serial: 1,
7056
+ state,
7057
+ resources,
7058
+ updatedAt: now
7059
+ };
7060
+ }
7061
+ function newLockHolder(options) {
7062
+ let owner = "unknown";
7063
+ try {
7064
+ owner = userInfo().username;
7065
+ } catch {}
7066
+ return {
7067
+ id: randomUUID(),
7068
+ owner,
7069
+ host: hostname(),
7070
+ pid: process.pid,
7071
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString(),
7072
+ operation: options?.operation
7073
+ };
7074
+ }
7075
+ function contentVersion(body) {
7076
+ return createHash("sha256").update(body).digest("hex").slice(0, 16);
7077
+ }
7078
+ /**
7079
+ * A store over a backend that can read and conditionally write one blob per
7080
+ * stage, and create a lock object only if it does not exist. Migration and
7081
+ * resource records are the same for every such backend, so they live here.
7082
+ */
7083
+ var DocumentStateStore = class {
7084
+ async lock(stage, options) {
7085
+ const holder = newLockHolder(options);
7086
+ await this.createLock(stage, holder);
7087
+ let released = false;
7088
+ return {
7089
+ stage,
7090
+ holder,
7091
+ release: async () => {
7092
+ if (released) return;
7093
+ released = true;
7094
+ await this.removeLock(stage, holder);
7095
+ }
7096
+ };
7097
+ }
7098
+ async forceUnlock(stage) {
7099
+ const holder = await this.readLock(stage);
7100
+ await this.removeLock(stage, null);
7101
+ return holder;
7102
+ }
7103
+ async read(stage) {
7104
+ const current = await this.readDocument(stage);
7105
+ if (!current) return null;
7106
+ return {
7107
+ state: current.document.state,
7108
+ resources: current.document.resources,
7109
+ version: current.version
7110
+ };
7111
+ }
7112
+ async write(stage, state, options) {
7113
+ if (options.expectedVersion === null) {
7114
+ const document = {
7115
+ schemaVersion: 2,
7116
+ stage,
7117
+ serial: 1,
7118
+ state,
7119
+ resources: {},
7120
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7121
+ };
7122
+ return this.writeRaw(stage, encodeDocument(document), null);
7123
+ }
7124
+ return this.mutate(stage, options.expectedVersion, (document) => ({
7125
+ ...document,
7126
+ state
7127
+ }));
7128
+ }
7129
+ async putResource(stage, record, options) {
7130
+ return this.mutate(stage, options?.expectedVersion, (document) => ({
7131
+ ...document,
7132
+ resources: {
7133
+ ...document.resources,
7134
+ [record.key]: {
7135
+ ...record,
7136
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7137
+ }
7138
+ }
7139
+ }));
7140
+ }
7141
+ async deleteResource(stage, key, options) {
7142
+ return this.mutate(stage, options?.expectedVersion, (document) => {
7143
+ const { [key]: _removed, ...resources } = document.resources;
7144
+ return {
7145
+ ...document,
7146
+ resources
7147
+ };
7148
+ });
7149
+ }
7150
+ async mutate(stage, expectedVersion, change) {
7151
+ const current = await this.readDocument(stage);
7152
+ if (!current) {
7153
+ if (expectedVersion !== void 0) throw new StateVersionConflict(stage, expectedVersion, null);
7154
+ throw new StageStateMissing(stage);
7155
+ }
7156
+ if (expectedVersion !== void 0 && current.version !== expectedVersion) throw new StateVersionConflict(stage, expectedVersion, current.version);
7157
+ const next = change(current.document);
7158
+ next.serial = current.document.serial + 1;
7159
+ next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7160
+ return this.writeRaw(stage, encodeDocument(next), current.version);
7161
+ }
7162
+ /**
7163
+ * Reads the stage's document, migrating v1 in place on the way. The
7164
+ * migration is itself a conditional write: if another run migrated (or
7165
+ * wrote) first, its result is read instead.
7166
+ */
7167
+ async readDocument(stage, attempt = 0) {
7168
+ const raw = await this.readRaw(stage);
7169
+ if (!raw) return null;
7170
+ const decoded = decodeDocument(stage, raw.body, this.location(stage));
7171
+ if (decoded.schemaVersion === 2) return {
7172
+ document: decoded.document,
7173
+ version: raw.version
7174
+ };
7175
+ await this.writeV1Backup(stage, raw.body);
7176
+ const document = migrateV1(stage, decoded.state);
7177
+ try {
7178
+ return {
7179
+ document,
7180
+ version: await this.writeRaw(stage, encodeDocument(document), raw.version)
7181
+ };
7182
+ } catch (error) {
7183
+ if (error instanceof StateVersionConflict && attempt < 2) return this.readDocument(stage, attempt + 1);
7184
+ throw error;
7185
+ }
7186
+ }
7187
+ };
7188
+ function isStateStore(value) {
7189
+ return typeof value === "object" && value !== null && typeof value.lock === "function" && typeof value.read === "function" && typeof value.write === "function" && typeof value.putResource === "function";
7190
+ }
7191
+ /**
7192
+ * The store for a workspace's `state` config:
7193
+ *
7194
+ * - none / `'local'`: `LocalStateStore` under `.gkm/`
7195
+ * - `'ssm'`: `SSMStateStore`, with no local cache — SSM is read directly, so
7196
+ * a stale local copy can never win over it
7197
+ * - `'s3'`: `S3StateStore`
7198
+ * - a `StateStore` object: used as is
7199
+ * - a `StateProvider` object: wrapped by `LegacyStateStore`
7200
+ */
7201
+ async function createStateStore(options) {
7202
+ const { config, workspaceRoot, workspaceName } = options;
7203
+ if (!config || config.provider === "local") {
7204
+ const { LocalStateStore } = await import("./LocalStateStore-Q4vXg2ua.mjs").then((n) => n.n);
7205
+ return new LocalStateStore(workspaceRoot);
7206
+ }
7207
+ if (isStateStore(config.provider)) return config.provider;
7208
+ if (isStateProvider(config.provider)) {
7209
+ const { LegacyStateStore } = await import("./LegacyStateStore-B0R4LlX_.mjs");
7210
+ return new LegacyStateStore(config.provider, { warn: options.warn });
7211
+ }
7212
+ if (config.provider === "ssm") {
7213
+ if (!workspaceName) throw new StateStoreNeedsWorkspaceName("ssm");
7214
+ const { SSMStateStore } = await import("./SSMStateStore-BszSCAS2.mjs");
7215
+ const ssm = config;
7216
+ return SSMStateStore.create({
7217
+ workspaceName,
7218
+ region: ssm.region,
7219
+ profile: ssm.profile
7220
+ });
7221
+ }
7222
+ if (config.provider === "s3") {
7223
+ if (!workspaceName) throw new StateStoreNeedsWorkspaceName("s3");
7224
+ const { S3StateStore } = await import("./S3StateStore-CksKYKRa.mjs");
7225
+ const s3 = config;
7226
+ return S3StateStore.create({
7227
+ workspaceName,
7228
+ bucket: s3.bucket,
7229
+ prefix: s3.prefix,
7230
+ region: s3.region,
7231
+ profile: s3.profile
7232
+ });
7233
+ }
7234
+ throw new UnknownStateProvider(config);
7235
+ }
7236
+ //#endregion
6868
7237
  //#region src/docker/imageRef.ts
6869
7238
  /**
6870
7239
  * An image reference, checked before it reaches `docker`.
@@ -7176,6 +7545,71 @@ function printRunInstructions(workspace) {
7176
7545
  logger.log(` docker compose -f ${COMPOSE_PATH}${project} --profile apps up --build`);
7177
7546
  }
7178
7547
  //#endregion
7548
+ //#region src/providers/types.ts
7549
+ /** The manifest kinds a stage can name a provider for. */
7550
+ const PROVIDER_KINDS = ["objects"];
7551
+ //#endregion
7552
+ //#region src/providers/notes.ts
7553
+ /** The constructs of a kind: `objects` is every bucket. */
7554
+ function constructsOf$1(manifest, kind) {
7555
+ return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
7556
+ }
7557
+ /** A stage that names `false` for a kind, and declares constructs of it. */
7558
+ var StageProviderDisabled = class extends Error {
7559
+ kind;
7560
+ stage;
7561
+ ids;
7562
+ constructor(kind, stage, ids) {
7563
+ 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"}.`);
7564
+ this.kind = kind;
7565
+ this.stage = stage;
7566
+ this.ids = ids;
7567
+ this.name = "StageProviderDisabled";
7568
+ }
7569
+ };
7570
+ /** The command a stage's providers are run with. */
7571
+ function provisionCommand(stage) {
7572
+ return `gkm setup --stage ${stage}`;
7573
+ }
7574
+ /**
7575
+ * The line a stage missing a key a provider would write is given: what
7576
+ * creates it, and with which credentials. Undefined when no provider backs
7577
+ * the kind on the stage.
7578
+ */
7579
+ function provisionHint(workspace, kind, stage) {
7580
+ const choice = stageProvider(workspace, kind, stage);
7581
+ if (choice.mode !== "provider") return void 0;
7582
+ const provisioning = provisioningOf(kind, choice.name);
7583
+ if (!provisioning) return void 0;
7584
+ 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}` : "");
7585
+ }
7586
+ /**
7587
+ * What a deploy's checks read off the stage's providers: whether each kind is
7588
+ * accounted for — a provider, or `false` — so no dev service stands in for
7589
+ * it, and what creates a missing key of it.
7590
+ */
7591
+ function stageProviderNotes(workspace, stage) {
7592
+ if (stageProvider(workspace, "objects", stage).mode === "external") return {};
7593
+ const hint = provisionHint(workspace, "objects", stage);
7594
+ return { objects: {
7595
+ accounted: true,
7596
+ ...hint ? { hint } : {}
7597
+ } };
7598
+ }
7599
+ /**
7600
+ * A kind the stage set to `false`, declared anyway — refused before anything
7601
+ * else is checked. Pure: what a stack is composed from.
7602
+ *
7603
+ * @throws {StageProviderDisabled}
7604
+ */
7605
+ function assertStageProvidersEnabled(workspace, manifest, stage) {
7606
+ for (const kind of PROVIDER_KINDS) {
7607
+ const ids = constructsOf$1(manifest, kind);
7608
+ if (ids.length === 0) continue;
7609
+ if (stageProvider(workspace, kind, stage).mode === "none") throw new StageProviderDisabled(kind, stage, ids);
7610
+ }
7611
+ }
7612
+ //#endregion
7179
7613
  //#region src/deploy/journal.ts
7180
7614
  var DeployJournal = class DeployJournal {
7181
7615
  store;
@@ -7426,6 +7860,1044 @@ function setPostgresBackupId(state, backupId) {
7426
7860
  if (state.backups) state.backups.postgresBackupId = backupId;
7427
7861
  }
7428
7862
  //#endregion
7863
+ //#region src/providers/aws.ts
7864
+ /**
7865
+ * The AWS account a provider provisions in — the stage's, found the way the
7866
+ * SSM and Secrets Manager stores find it.
7867
+ *
7868
+ * 1. `--profile`, resolved on its own: exported keys left over from another
7869
+ * account never win over a profile someone named.
7870
+ * 2. `AWS_PROFILE`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` — the
7871
+ * deploy's own `aws` credential kind (`storedCredentials`).
7872
+ * 3. The SDK's default chain: `~/.aws` default profile, SSO, a role. In CI,
7873
+ * whatever `aws-actions/configure-aws-credentials` exported.
7874
+ *
7875
+ * None of them: the provider has no provisioning credentials, and the stage
7876
+ * is treated as `external`.
7877
+ */
7878
+ async function awsProvisioningCredentials(lookup) {
7879
+ const region = lookup.env.AWS_REGION ?? lookup.env.AWS_DEFAULT_REGION;
7880
+ const where = region ? { region } : {};
7881
+ if (lookup.profile) return {
7882
+ profile: lookup.profile,
7883
+ ...where
7884
+ };
7885
+ const stored = await storedCredentials({
7886
+ env: lookup.env,
7887
+ ...lookup.home ? { home: lookup.home } : {}
7888
+ }).get({
7889
+ kind: "aws",
7890
+ stage: lookup.stage
7891
+ });
7892
+ if (stored) return stored;
7893
+ try {
7894
+ const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
7895
+ await fromNodeProviderChain({
7896
+ timeout: 1e3,
7897
+ maxRetries: 0
7898
+ })();
7899
+ return {
7900
+ chain: true,
7901
+ ...where
7902
+ };
7903
+ } catch {
7904
+ return;
7905
+ }
7906
+ }
7907
+ /**
7908
+ * An S3-compatible endpoint the environment points the SDK at — the AWS
7909
+ * emulator in tests. The SDK reads it itself; it is read here too, because
7910
+ * an emulator takes path-style requests, and a key written for one has to
7911
+ * say where it is.
7912
+ */
7913
+ function awsEndpoint(env, service) {
7914
+ return env[`AWS_ENDPOINT_URL_${service}`] ?? env.AWS_ENDPOINT_URL;
7915
+ }
7916
+ /** An SDK client's config for a provisioning credential. */
7917
+ async function awsClientConfig(credential, region) {
7918
+ if ("profile" in credential) {
7919
+ const { fromIni } = await import("@aws-sdk/credential-providers");
7920
+ return {
7921
+ region,
7922
+ credentials: fromIni({ profile: credential.profile })
7923
+ };
7924
+ }
7925
+ if ("accessKeyId" in credential) return {
7926
+ region,
7927
+ credentials: {
7928
+ accessKeyId: credential.accessKeyId,
7929
+ secretAccessKey: credential.secretAccessKey,
7930
+ ...credential.sessionToken ? { sessionToken: credential.sessionToken } : {}
7931
+ }
7932
+ };
7933
+ return { region };
7934
+ }
7935
+ const SUFFIX_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
7936
+ /** A name that S3 would refuse — what the name builder never returns. */
7937
+ var InvalidBucketName = class extends Error {
7938
+ bucket;
7939
+ reason;
7940
+ constructor(bucket, reason) {
7941
+ 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.`);
7942
+ this.bucket = bucket;
7943
+ this.reason = reason;
7944
+ this.name = "InvalidBucketName";
7945
+ }
7946
+ };
7947
+ /** Lowercase `[a-z0-9-]`, runs of anything else one `-`, no edge dashes. */
7948
+ function part(value) {
7949
+ return kebabCase(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7950
+ }
7951
+ /**
7952
+ * `parts` joined by `-`, shortened to `max` characters by trimming the longest
7953
+ * part one character at a time — so a long construct id gives way before a
7954
+ * short stage name does, and every part keeps its start.
7955
+ */
7956
+ function fitParts(parts, max) {
7957
+ const kept = parts.map(part).filter(Boolean);
7958
+ const length = () => kept.reduce((sum, p) => sum + p.length, 0) + kept.length - 1;
7959
+ while (length() > max) {
7960
+ let longest = 0;
7961
+ for (let i = 1; i < kept.length; i++) if (kept[i].length > kept[longest].length) longest = i;
7962
+ if (kept[longest].length <= 1) break;
7963
+ kept[longest] = kept[longest].slice(0, -1).replace(/-+$/, "");
7964
+ }
7965
+ return kept.join("-");
7966
+ }
7967
+ /** Why `name` is not a valid bucket name, or undefined when it is. */
7968
+ function bucketNameProblem(name) {
7969
+ if (name.length < 3 || name.length > 63) return `it is ${name.length} characters, and S3 takes 3 to 63`;
7970
+ if (!/^[a-z0-9-]+$/.test(name)) return "it may hold only lowercase letters, digits and -";
7971
+ if (!/^[a-z0-9].*[a-z0-9]$/.test(name)) return "it must start and end with a letter or a digit";
7972
+ if (name.startsWith("xn--") || name.startsWith("sthree-")) return "it starts with a prefix S3 reserves ('xn--', 'sthree-')";
7973
+ if (name.endsWith("-s3alias") || name.endsWith("--ol-s3")) return "it ends with a suffix S3 reserves ('-s3alias', '--ol-s3')";
7974
+ }
7975
+ /**
7976
+ * The good name: `<namespace>-<project>-<stage>-<bucket id>`, lowercased, no
7977
+ * dots, shortened to leave room for a clash's suffix.
7978
+ *
7979
+ * @throws {InvalidBucketName} when nothing usable is left
7980
+ */
7981
+ function bucketName(input) {
7982
+ const name = fitParts([
7983
+ input.scope,
7984
+ input.stage,
7985
+ input.id
7986
+ ], 56);
7987
+ const problem = bucketNameProblem(name);
7988
+ if (problem) throw new InvalidBucketName(name, problem);
7989
+ return name;
7990
+ }
7991
+ /** Six random `[a-z0-9]` characters. */
7992
+ function randomSuffix(random = randomInt) {
7993
+ let suffix = "";
7994
+ for (let i = 0; i < 6; i++) suffix += SUFFIX_ALPHABET[random(36)];
7995
+ return suffix;
7996
+ }
7997
+ /** The good name with a clash's suffix. */
7998
+ function suffixedBucketName(good, suffix) {
7999
+ return `${good}-${suffix}`;
8000
+ }
8001
+ /**
8002
+ * The IAM user a bucket is reached with on a stage:
8003
+ * `gkm-<project>-<stage>-<bucket id>`, at most 64 characters.
8004
+ */
8005
+ function iamUserName(input) {
8006
+ return fitParts([
8007
+ "gkm",
8008
+ input.scope,
8009
+ input.stage,
8010
+ input.id
8011
+ ], 64);
8012
+ }
8013
+ /** The path every user gkm creates is under, so it finds its own. */
8014
+ const IAM_USER_PATH = "/gkm/";
8015
+ /** The inline policy each user is given. */
8016
+ const IAM_POLICY_NAME = "gkm-bucket";
8017
+ //#endregion
8018
+ //#region src/providers/s3/origins.ts
8019
+ /**
8020
+ * Which browser origins may call a bucket: the stage's sites that depend on
8021
+ * an API using it.
8022
+ *
8023
+ * The same reading of the graph an API's own CORS origins are — its callers —
8024
+ * taken one step further: a site that calls an API which hands it a presigned
8025
+ * upload URL is a site whose browser PUTs to the bucket. A site that reads the
8026
+ * bucket (or a file server over it) itself is one too. Nothing is written
8027
+ * down: add a site that calls the API and the next provision run allows it.
8028
+ */
8029
+ /** The keys a reader of this bucket holds: its URL, and each server's over it. */
8030
+ function bucketKeys(manifest, bucket) {
8031
+ return [provideKey(bucket, "url"), ...Object.entries(manifest).filter(([, d]) => d.kind === "file-server" && d.of === bucket).map(([id]) => provideKey(id, "url"))];
8032
+ }
8033
+ function bucketOrigins(input) {
8034
+ const { manifest, workspace, stage } = input;
8035
+ const keys = bucketKeys(manifest, input.bucket);
8036
+ const readers = Object.entries(manifest).filter(([, d]) => d.kind === "rest-api" || d.kind === "site").filter(([id]) => {
8037
+ const env = appEnvKeys(manifest, appKey(id), input.runnables);
8038
+ return env !== void 0 && keys.some((key) => env.has(key));
8039
+ });
8040
+ const sites = /* @__PURE__ */ new Set();
8041
+ for (const [id, declaration] of readers) {
8042
+ if (declaration.kind === "site") {
8043
+ sites.add(id);
8044
+ continue;
8045
+ }
8046
+ for (const caller of dependentsOf(manifest, id)) if (manifest[caller]?.kind === "site") sites.add(caller);
8047
+ }
8048
+ const origins = /* @__PURE__ */ new Set();
8049
+ const unresolved = [];
8050
+ for (const id of [...sites].sort()) {
8051
+ const name = appKey(id);
8052
+ const app = workspace.apps[name];
8053
+ if (!app) {
8054
+ unresolved.push(name);
8055
+ continue;
8056
+ }
8057
+ try {
8058
+ const host = resolveHost(name, app, stage, workspace.deploy?.domains, isMainFrontendApp(name, app, workspace.apps));
8059
+ origins.add(`https://${host}`);
8060
+ } catch (error) {
8061
+ if (!(error instanceof NoDomainForStage)) throw error;
8062
+ unresolved.push(name);
8063
+ }
8064
+ }
8065
+ return {
8066
+ origins: [...origins].sort(),
8067
+ unresolved
8068
+ };
8069
+ }
8070
+ //#endregion
8071
+ //#region src/providers/s3/policy.ts
8072
+ const bucketArn = (bucket) => `arn:aws:s3:::${bucket}`;
8073
+ /**
8074
+ * What the app's key may do: objects in this bucket, and listing it — and
8075
+ * nothing else in the account.
8076
+ *
8077
+ * Multipart uploads are in it because a presigned upload of a large file is
8078
+ * one, and an abandoned one is cleaned up by the app. Reading old versions is
8079
+ * added only when the bucket keeps them.
8080
+ */
8081
+ function userPolicy(bucket, options = {}) {
8082
+ return {
8083
+ Version: "2012-10-17",
8084
+ Statement: [{
8085
+ Sid: "GkmObjects",
8086
+ Effect: "Allow",
8087
+ Action: [
8088
+ "s3:GetObject",
8089
+ "s3:PutObject",
8090
+ "s3:DeleteObject",
8091
+ "s3:AbortMultipartUpload",
8092
+ "s3:ListMultipartUploadParts",
8093
+ ...options.versioning ? ["s3:GetObjectVersion"] : []
8094
+ ],
8095
+ Resource: `${bucketArn(bucket)}/*`
8096
+ }, {
8097
+ Sid: "GkmBucket",
8098
+ Effect: "Allow",
8099
+ Action: [
8100
+ "s3:ListBucket",
8101
+ "s3:GetBucketLocation",
8102
+ "s3:ListBucketMultipartUploads"
8103
+ ],
8104
+ Resource: bucketArn(bucket)
8105
+ }]
8106
+ };
8107
+ }
8108
+ /**
8109
+ * One open pattern as the resource it is public on. A `**` is S3's `*`; a
8110
+ * single `*` is too, which crosses `/` where the construct's own check does
8111
+ * not — the construct is the stricter of the two (see `bucketPolicy` in
8112
+ * reconcile, which the local MinIO is given the same way).
8113
+ */
8114
+ function openResource(bucket, pattern) {
8115
+ const path = pattern.replace(/^\/+/, "").replace(/\*\*/g, "*");
8116
+ return `${bucketArn(bucket)}/${path}`;
8117
+ }
8118
+ /**
8119
+ * The bucket's own policy: every request refused unless it came over TLS,
8120
+ * and — only when a file server opens paths on it — anonymous `s3:GetObject`
8121
+ * on exactly those prefixes.
8122
+ */
8123
+ function bucketPolicy(bucket, open = []) {
8124
+ const statements = [{
8125
+ Sid: "GkmDenyInsecureTransport",
8126
+ Effect: "Deny",
8127
+ Principal: "*",
8128
+ Action: "s3:*",
8129
+ Resource: [bucketArn(bucket), `${bucketArn(bucket)}/*`],
8130
+ Condition: { Bool: { "aws:SecureTransport": "false" } }
8131
+ }];
8132
+ if (open.length > 0) statements.push({
8133
+ Sid: "GkmOpenPaths",
8134
+ Effect: "Allow",
8135
+ Principal: "*",
8136
+ Action: "s3:GetObject",
8137
+ Resource: [...new Set(open.map((p) => openResource(bucket, p)))].sort()
8138
+ });
8139
+ return {
8140
+ Version: "2012-10-17",
8141
+ Statement: statements
8142
+ };
8143
+ }
8144
+ /**
8145
+ * Block Public Access. All four flags on, unless the bucket has open paths:
8146
+ * then a *policy* may make it public (`BlockPublicPolicy`,
8147
+ * `RestrictPublicBuckets` off), and ACLs still may not.
8148
+ */
8149
+ function publicAccessBlock(open) {
8150
+ return {
8151
+ BlockPublicAcls: true,
8152
+ IgnorePublicAcls: true,
8153
+ BlockPublicPolicy: !open,
8154
+ RestrictPublicBuckets: !open
8155
+ };
8156
+ }
8157
+ function corsRule(origins) {
8158
+ return {
8159
+ AllowedMethods: [
8160
+ "GET",
8161
+ "HEAD",
8162
+ "POST",
8163
+ "PUT"
8164
+ ],
8165
+ AllowedOrigins: [...new Set(origins)].sort(),
8166
+ AllowedHeaders: ["*"],
8167
+ ExposeHeaders: ["ETag"],
8168
+ MaxAgeSeconds: 3e3
8169
+ };
8170
+ }
8171
+ /**
8172
+ * A policy document in one canonical form — keys sorted, a lone string a
8173
+ * one-element list, lists sorted — so one AWS reformatted reads as the same
8174
+ * document rather than as drift.
8175
+ */
8176
+ function canonicalPolicy(value) {
8177
+ return JSON.stringify(canonical(value));
8178
+ }
8179
+ function canonical(value, key) {
8180
+ if (Array.isArray(value)) {
8181
+ const items = value.map((v) => canonical(v));
8182
+ return key === "Statement" ? items : items.map((v) => JSON.stringify(v)).sort();
8183
+ }
8184
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => {
8185
+ const v = value[k];
8186
+ return [k, canonical(k === "Action" || k === "Resource" || k === "AWS" ? Array.isArray(v) ? v : [v] : v, k)];
8187
+ }));
8188
+ return value;
8189
+ }
8190
+ //#endregion
8191
+ //#region src/providers/s3/index.ts
8192
+ /** The tags gkm finds its own buckets and users by. */
8193
+ const TAG_PROJECT = "gkm:project";
8194
+ const TAG_STAGE = "gkm:stage";
8195
+ const TAG_CONSTRUCT = "gkm:construct";
8196
+ function bucketConstructs(manifest) {
8197
+ return Object.entries(manifest).filter(([, d]) => d.kind === "objects").map(([id, d]) => ({
8198
+ id,
8199
+ versioned: d.versioned === true,
8200
+ servers: Object.entries(manifest).filter(([, s]) => s.kind === "file-server" && s.of === id).map(([sid, s]) => ({
8201
+ id: sid,
8202
+ open: s.open ?? []
8203
+ })).sort((a, b) => a.id.localeCompare(b.id))
8204
+ })).sort((a, b) => a.id.localeCompare(b.id));
8205
+ }
8206
+ /**
8207
+ * The region the stage's buckets are created in: the entry's, the secrets
8208
+ * store's when it is in AWS, the credential's, `AWS_REGION`.
8209
+ */
8210
+ function s3Region(ctx, env = process.env) {
8211
+ const store = ctx.workspace.secrets?.store;
8212
+ const storeRegion = store && typeof store === "object" && "region" in store ? store.region : void 0;
8213
+ const region = ctx.config.region ?? storeRegion ?? ctx.credential?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
8214
+ if (!region) throw new S3RegionRequired(ctx.stage);
8215
+ return region;
8216
+ }
8217
+ /**
8218
+ * The address a file server over `bucket` serves on: the bucket's regional
8219
+ * virtual-host endpoint, which is what `FileServer.url(key)` appends a key to
8220
+ * — `https://<bucket>.s3.<region>.amazonaws.com/<key>`. Against an emulator,
8221
+ * its path-style address.
8222
+ */
8223
+ function bucketPublicUrl(bucket, region, endpoint) {
8224
+ if (endpoint) return `${endpoint.replace(/\/+$/, "")}/${bucket}`;
8225
+ return `https://${bucket}.s3.${region}.amazonaws.com`;
8226
+ }
8227
+ /** What an AWS SDK error is called, whichever field carries it. */
8228
+ function errorName(error) {
8229
+ const e = error;
8230
+ return e?.Code ?? e?.name ?? "";
8231
+ }
8232
+ /** A read whose "there is none" answer is one of `missing`. */
8233
+ async function readOr(read, missing) {
8234
+ try {
8235
+ return await read();
8236
+ } catch (error) {
8237
+ if (missing.includes(errorName(error))) return void 0;
8238
+ throw error;
8239
+ }
8240
+ }
8241
+ const defaultClients = async (credential, region) => {
8242
+ const S3 = await import("@aws-sdk/client-s3");
8243
+ const IAM = await import("@aws-sdk/client-iam");
8244
+ const config = await awsClientConfig(credential, region);
8245
+ const endpoint = awsEndpoint(process.env, "S3");
8246
+ return {
8247
+ S3,
8248
+ IAM,
8249
+ s3: new S3.S3Client({
8250
+ ...config,
8251
+ ...endpoint ? {
8252
+ endpoint,
8253
+ forcePathStyle: true
8254
+ } : {}
8255
+ }),
8256
+ iam: new IAM.IAMClient(config),
8257
+ region,
8258
+ ...endpoint ? { endpoint } : {}
8259
+ };
8260
+ };
8261
+ /** For tests: what makes the clients. */
8262
+ let makeClients = defaultClients;
8263
+ async function ensureS3(ctx) {
8264
+ const region = s3Region(ctx);
8265
+ const clients = await makeClients(ctx.credential, region);
8266
+ for (const construct of bucketConstructs(ctx.manifest)) await ensureBucket(ctx, clients, construct);
8267
+ }
8268
+ /** The keys gkm tags a bucket and a user with. */
8269
+ function identityTags(ctx, id) {
8270
+ return [
8271
+ {
8272
+ Key: TAG_PROJECT,
8273
+ Value: ctx.identity.key
8274
+ },
8275
+ {
8276
+ Key: TAG_STAGE,
8277
+ Value: ctx.stage
8278
+ },
8279
+ {
8280
+ Key: TAG_CONSTRUCT,
8281
+ Value: id
8282
+ }
8283
+ ];
8284
+ }
8285
+ async function ensureBucket(ctx, clients, construct) {
8286
+ const { id } = construct;
8287
+ const nameInput = {
8288
+ scope: ctx.identity.scope,
8289
+ stage: ctx.stage,
8290
+ id: appKey(id)
8291
+ };
8292
+ const versioning = ctx.config.versioning ?? construct.versioned;
8293
+ const open = [...new Set(construct.servers.flatMap((s) => s.open))].sort();
8294
+ const { bucket, exists } = await ensureBucketName(ctx, clients, id, bucketName(nameInput));
8295
+ await ensureBucketSettings(ctx, clients, {
8296
+ id,
8297
+ bucket,
8298
+ exists,
8299
+ versioning,
8300
+ open
8301
+ });
8302
+ await ensureAccessKey(ctx, clients, {
8303
+ id,
8304
+ bucket,
8305
+ ...await ensureUser(ctx, clients, id, iamUserName(nameInput), userPolicy(bucket, { versioning }))
8306
+ });
8307
+ const publicUrl = bucketPublicUrl(bucket, clients.region, clients.endpoint);
8308
+ for (const server of construct.servers) {
8309
+ const key = provideKey(server.id, "url");
8310
+ const entry = {
8311
+ key: `file-server:${server.id}`,
8312
+ type: "url"
8313
+ };
8314
+ const recorded = ctx.state.record(entry.key)?.id;
8315
+ const current = ctx.secrets[key];
8316
+ if (current === publicUrl) {
8317
+ await ctx.state.ready(entry, publicUrl);
8318
+ continue;
8319
+ }
8320
+ if (current !== void 0 && current !== recorded) {
8321
+ ctx.log(` ${key} is set to ${current} by you, and kept — the bucket's own address is ${publicUrl}`);
8322
+ continue;
8323
+ }
8324
+ await ctx.change({
8325
+ construct: server.id,
8326
+ resource: key,
8327
+ change: `set to ${publicUrl}`
8328
+ }, async () => {
8329
+ await ctx.writeSecrets({ [key]: publicUrl });
8330
+ await ctx.state.ready(entry, publicUrl);
8331
+ });
8332
+ }
8333
+ }
8334
+ /**
8335
+ * The bucket's name: the recorded one, else the good name, else the good
8336
+ * name with a random suffix — up to {@link SUFFIX_ATTEMPTS} times.
8337
+ */
8338
+ async function ensureBucketName(ctx, clients, id, good) {
8339
+ const { S3, s3, region } = clients;
8340
+ const key = `s3-bucket:${id}`;
8341
+ const record = ctx.state.record(key);
8342
+ const entry = (name) => ({
8343
+ key,
8344
+ type: "s3-bucket",
8345
+ data: {
8346
+ name,
8347
+ region
8348
+ }
8349
+ });
8350
+ /** Whether this account holds `name`: yes, no, or someone else does. */
8351
+ const probe = async (name) => {
8352
+ try {
8353
+ await s3.send(new S3.HeadBucketCommand({ Bucket: name }));
8354
+ return "ours";
8355
+ } catch (error) {
8356
+ const name = errorName(error);
8357
+ if (name === "NotFound" || name === "NoSuchBucket") return "none";
8358
+ if (name === "Forbidden" || name === "AccessDenied") return "taken";
8359
+ throw error;
8360
+ }
8361
+ };
8362
+ const create = async (name) => {
8363
+ if (ctx.dryRun) {
8364
+ const found = await probe(name);
8365
+ if (found === "none") {
8366
+ await ctx.change({
8367
+ construct: id,
8368
+ resource: `bucket ${name}`,
8369
+ change: `create in ${region}`
8370
+ }, async () => {});
8371
+ return "created";
8372
+ }
8373
+ return found;
8374
+ }
8375
+ await ctx.state.pending(entry(name));
8376
+ try {
8377
+ await s3.send(new S3.CreateBucketCommand({
8378
+ Bucket: name,
8379
+ ...region === "us-east-1" ? {} : { CreateBucketConfiguration: { LocationConstraint: region } }
8380
+ }));
8381
+ } catch (error) {
8382
+ const code = errorName(error);
8383
+ if (code === "BucketAlreadyOwnedByYou") return "ours";
8384
+ if (code === "BucketAlreadyExists") return "taken";
8385
+ throw error;
8386
+ }
8387
+ await ctx.change({
8388
+ construct: id,
8389
+ resource: `bucket ${name}`,
8390
+ change: `create in ${region}`
8391
+ }, async () => {});
8392
+ return "created";
8393
+ };
8394
+ const adopt = async (name) => {
8395
+ const owner = (await readOr(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket: name })), ["NoSuchTagSet", "NoSuchTagSetError"]))?.TagSet?.find((t) => t.Key === TAG_PROJECT)?.Value;
8396
+ if (owner && owner !== ctx.identity.key) throw new BucketOwnedByAnotherProject(name, owner, ctx.identity.key);
8397
+ };
8398
+ if (record?.status === "ready" && record.id) {
8399
+ const found = await probe(record.id);
8400
+ if (found === "ours") return {
8401
+ bucket: record.id,
8402
+ exists: true
8403
+ };
8404
+ if (found === "taken") throw new RecordedBucketUnreachable(id, record.id);
8405
+ const made = await create(record.id);
8406
+ if (made === "taken") throw new RecordedBucketUnreachable(id, record.id);
8407
+ if (!ctx.dryRun) await ctx.state.ready(entry(record.id), record.id);
8408
+ return {
8409
+ bucket: record.id,
8410
+ exists: made === "ours"
8411
+ };
8412
+ }
8413
+ const pending = record?.data?.name;
8414
+ const names = [
8415
+ ...typeof pending === "string" && pending !== good ? [pending] : [],
8416
+ good,
8417
+ ...Array.from({ length: 5 }, () => suffixedBucketName(good, randomSuffix()))
8418
+ ];
8419
+ const tried = [];
8420
+ for (const name of names) {
8421
+ tried.push(name);
8422
+ const made = await create(name);
8423
+ if (made === "taken") continue;
8424
+ if (made === "ours") await adopt(name);
8425
+ if (!ctx.dryRun) await ctx.state.ready(entry(name), name);
8426
+ if (name !== good) ctx.log(` '${good}' is taken by another account; '${id}' is '${name}', recorded in the stage's state`);
8427
+ return {
8428
+ bucket: name,
8429
+ exists: made === "ours"
8430
+ };
8431
+ }
8432
+ throw new BucketNameUnavailable(id, tried.filter((n) => n !== pending));
8433
+ }
8434
+ /** Tags, Block Public Access, encryption, policy, versioning and CORS. */
8435
+ async function ensureBucketSettings(ctx, clients, input) {
8436
+ const { S3, s3 } = clients;
8437
+ const { id, bucket } = input;
8438
+ const Bucket = bucket;
8439
+ const fresh = ctx.dryRun && !input.exists;
8440
+ const read = async (fn, missing) => fresh ? void 0 : readOr(fn, missing);
8441
+ const set = (change, apply) => ctx.change({
8442
+ construct: id,
8443
+ resource: `bucket ${bucket}`,
8444
+ change
8445
+ }, () => apply().then(() => void 0));
8446
+ const tagged = await read(() => s3.send(new S3.GetBucketTaggingCommand({ Bucket })), ["NoSuchTagSet", "NoSuchTagSetError"]);
8447
+ const ours = identityTags(ctx, id);
8448
+ const others = (tagged?.TagSet ?? []).filter((t) => !ours.some((o) => o.Key === t.Key));
8449
+ if (ours.some((o) => !tagged?.TagSet?.some((t) => t.Key === o.Key && t.Value === o.Value))) await set("tag", () => s3.send(new S3.PutBucketTaggingCommand({
8450
+ Bucket,
8451
+ Tagging: { TagSet: [...others, ...ours].map((t) => ({
8452
+ Key: t.Key,
8453
+ Value: t.Value
8454
+ })) }
8455
+ })));
8456
+ const isOpen = input.open.length > 0;
8457
+ const block = publicAccessBlock(isOpen);
8458
+ const ensureBlock = async () => {
8459
+ const now = (await read(() => s3.send(new S3.GetPublicAccessBlockCommand({ Bucket })), ["NoSuchPublicAccessBlockConfiguration"]))?.PublicAccessBlockConfiguration;
8460
+ 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({
8461
+ Bucket,
8462
+ PublicAccessBlockConfiguration: block
8463
+ })));
8464
+ };
8465
+ const policy = bucketPolicy(bucket, input.open);
8466
+ const ensurePolicy = async () => {
8467
+ const current = await read(() => s3.send(new S3.GetBucketPolicyCommand({ Bucket })), ["NoSuchBucketPolicy"]);
8468
+ 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({
8469
+ Bucket,
8470
+ Policy: JSON.stringify(policy)
8471
+ })));
8472
+ };
8473
+ if (isOpen) {
8474
+ await ensureBlock();
8475
+ await ensurePolicy();
8476
+ } else {
8477
+ await ensurePolicy();
8478
+ await ensureBlock();
8479
+ }
8480
+ 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({
8481
+ Bucket,
8482
+ ServerSideEncryptionConfiguration: { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } }] }
8483
+ })));
8484
+ const status = (await read(() => s3.send(new S3.GetBucketVersioningCommand({ Bucket })), []))?.Status;
8485
+ if (input.versioning && status !== "Enabled") await set("enable versioning", () => s3.send(new S3.PutBucketVersioningCommand({
8486
+ Bucket,
8487
+ VersioningConfiguration: { Status: "Enabled" }
8488
+ })));
8489
+ else if (!input.versioning && status === "Enabled") await set("suspend versioning", () => s3.send(new S3.PutBucketVersioningCommand({
8490
+ Bucket,
8491
+ VersioningConfiguration: { Status: "Suspended" }
8492
+ })));
8493
+ const { origins, unresolved } = bucketOrigins({
8494
+ workspace: ctx.workspace,
8495
+ manifest: ctx.manifest,
8496
+ runnables: ctx.runnables,
8497
+ stage: ctx.stage,
8498
+ bucket: id
8499
+ });
8500
+ 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`);
8501
+ if (origins.length === 0) return;
8502
+ const rule = corsRule(origins);
8503
+ const cors = await read(() => s3.send(new S3.GetBucketCorsCommand({ Bucket })), ["NoSuchCORSConfiguration"]);
8504
+ if (!(cors?.CORSRules?.length === 1 && canonicalCors(cors.CORSRules[0]) === canonicalCors(rule))) await set(`CORS for ${origins.join(", ")}`, () => s3.send(new S3.PutBucketCorsCommand({
8505
+ Bucket,
8506
+ CORSConfiguration: { CORSRules: [rule] }
8507
+ })));
8508
+ }
8509
+ function canonicalCors(rule) {
8510
+ const sorted = (list) => [...list ?? []].sort();
8511
+ return JSON.stringify({
8512
+ AllowedMethods: sorted(rule.AllowedMethods),
8513
+ AllowedOrigins: sorted(rule.AllowedOrigins),
8514
+ AllowedHeaders: sorted(rule.AllowedHeaders),
8515
+ ExposeHeaders: sorted(rule.ExposeHeaders),
8516
+ MaxAgeSeconds: rule.MaxAgeSeconds ?? null
8517
+ });
8518
+ }
8519
+ /** The bucket's IAM user and its inline policy. */
8520
+ async function ensureUser(ctx, clients, id, user, policy) {
8521
+ const { IAM, iam } = clients;
8522
+ const entry = {
8523
+ key: `iam-user:${id}`,
8524
+ type: "iam-user"
8525
+ };
8526
+ const found = await readOr(() => iam.send(new IAM.GetUserCommand({ UserName: user })), ["NoSuchEntity", "NoSuchEntityException"]);
8527
+ if (found?.User) {
8528
+ const tags = await iam.send(new IAM.ListUserTagsCommand({ UserName: user }));
8529
+ const tag = (key) => tags.Tags?.find((t) => t.Key === key)?.Value;
8530
+ if (found.User.Path !== "/gkm/" || tag("gkm:project") !== ctx.identity.key || tag("gkm:stage") !== ctx.stage) throw new IamUserNotOwned(user, ctx.identity.key, ctx.stage);
8531
+ await ctx.state.ready(entry, user);
8532
+ } else await ctx.change({
8533
+ construct: id,
8534
+ resource: `IAM user ${user}`,
8535
+ change: `create under ${IAM_USER_PATH}`
8536
+ }, async () => {
8537
+ await ctx.state.pending(entry);
8538
+ await iam.send(new IAM.CreateUserCommand({
8539
+ UserName: user,
8540
+ Path: IAM_USER_PATH,
8541
+ Tags: identityTags(ctx, id)
8542
+ }));
8543
+ await ctx.state.ready(entry, user);
8544
+ });
8545
+ const exists = Boolean(found?.User) || !ctx.dryRun;
8546
+ const current = exists ? await readOr(() => iam.send(new IAM.GetUserPolicyCommand({
8547
+ UserName: user,
8548
+ PolicyName: IAM_POLICY_NAME
8549
+ })), ["NoSuchEntity", "NoSuchEntityException"]) : void 0;
8550
+ const document = current?.PolicyDocument ? JSON.parse(decodePolicy(current.PolicyDocument)) : void 0;
8551
+ if (!document || canonicalPolicy(document) !== canonicalPolicy(policy)) await ctx.change({
8552
+ construct: id,
8553
+ resource: `IAM user ${user}`,
8554
+ change: `policy ${IAM_POLICY_NAME}: this bucket only`
8555
+ }, async () => {
8556
+ await iam.send(new IAM.PutUserPolicyCommand({
8557
+ UserName: user,
8558
+ PolicyName: IAM_POLICY_NAME,
8559
+ PolicyDocument: JSON.stringify(policy)
8560
+ }));
8561
+ });
8562
+ return {
8563
+ user,
8564
+ exists
8565
+ };
8566
+ }
8567
+ /** IAM returns a policy document URL-encoded; an emulator may not. */
8568
+ function decodePolicy(document) {
8569
+ try {
8570
+ return decodeURIComponent(document);
8571
+ } catch {
8572
+ return document;
8573
+ }
8574
+ }
8575
+ /**
8576
+ * The user's access key, in the stage's secrets as the bucket's URL.
8577
+ *
8578
+ * - First run: one key, written into the stage's secrets.
8579
+ * - `--rotate-keys`: a second key, written in place of the first; the first
8580
+ * stays active until the stage has deployed with the second, and the next
8581
+ * run after that deploy — or `--retire-old-keys` — deletes it.
8582
+ * - A key the secrets no longer hold (a store emptied by hand): replaced the
8583
+ * same way, since its secret cannot be read back.
8584
+ */
8585
+ async function ensureAccessKey(ctx, clients, input) {
8586
+ const { IAM, iam } = clients;
8587
+ const { id, bucket, user } = input;
8588
+ const urlKey = provideKey(id, "url");
8589
+ const entryKey = `iam-access-key:${id}`;
8590
+ const record = ctx.state.record(entryKey);
8591
+ const given = ctx.secrets[urlKey];
8592
+ let inUrl;
8593
+ if (given !== void 0) {
8594
+ let parsed;
8595
+ try {
8596
+ parsed = s3Url.parse(given);
8597
+ } catch {
8598
+ parsed = void 0;
8599
+ }
8600
+ if (parsed?.bucket !== bucket) throw new BucketKeySetElsewhere(urlKey, bucket, parsed?.bucket ?? given.replace(/\/\/[^@]*@/, "//"), ctx.stage);
8601
+ inUrl = parsed.accessKeyId;
8602
+ }
8603
+ const listed = input.exists ? (await iam.send(new IAM.ListAccessKeysCommand({ UserName: user }))).AccessKeyMetadata?.map((k) => k.AccessKeyId) ?? [] : [];
8604
+ let current = record?.status === "ready" ? record.id : void 0;
8605
+ let previous = typeof record?.data?.previous === "string" ? record.data.previous : void 0;
8606
+ const rotatedAt = typeof record?.data?.rotatedAt === "string" ? record.data.rotatedAt : void 0;
8607
+ const entry = (data = {}) => ({
8608
+ key: entryKey,
8609
+ type: "iam-access-key",
8610
+ data: {
8611
+ user,
8612
+ ...data
8613
+ }
8614
+ });
8615
+ if (previous && current) {
8616
+ const deployed = rotatedAt !== void 0 && ctx.state.lastDeployedAt !== void 0 && ctx.state.lastDeployedAt > rotatedAt;
8617
+ if (ctx.retireOldKeys || deployed) {
8618
+ const old = previous;
8619
+ const keep = current;
8620
+ await ctx.change({
8621
+ construct: id,
8622
+ resource: `IAM user ${user}`,
8623
+ change: `delete the rotated-out access key ${old}`
8624
+ }, async () => {
8625
+ if (listed.includes(old)) await iam.send(new IAM.DeleteAccessKeyCommand({
8626
+ UserName: user,
8627
+ AccessKeyId: old
8628
+ }));
8629
+ await ctx.state.ready(entry(), keep);
8630
+ });
8631
+ previous = void 0;
8632
+ } 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)`);
8633
+ }
8634
+ if (!current && inUrl && listed.includes(inUrl)) {
8635
+ await ctx.state.ready(entry(), inUrl);
8636
+ current = inUrl;
8637
+ }
8638
+ if (current !== void 0 && listed.includes(current) && inUrl === current && !ctx.rotateKeys) return;
8639
+ if (ctx.rotateKeys && previous) throw new RotationInProgress(id, previous, ctx.stage);
8640
+ const outgoing = current && listed.includes(current) ? current : void 0;
8641
+ if (listed.length >= 2) throw new AccessKeyLimit(user, listed);
8642
+ 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)";
8643
+ await ctx.change({
8644
+ construct: id,
8645
+ resource: `IAM user ${user}`,
8646
+ change: why
8647
+ }, async () => {
8648
+ await ctx.state.pending(entry(outgoing ? { previous: outgoing } : {}));
8649
+ const key = (await iam.send(new IAM.CreateAccessKeyCommand({ UserName: user }))).AccessKey;
8650
+ await ctx.writeSecrets({ [urlKey]: s3Url.build({
8651
+ bucket,
8652
+ region: clients.region,
8653
+ ...clients.endpoint ? {
8654
+ endpoint: clients.endpoint,
8655
+ forcePathStyle: true
8656
+ } : {},
8657
+ accessKeyId: key.AccessKeyId,
8658
+ secretAccessKey: key.SecretAccessKey
8659
+ }) });
8660
+ await ctx.state.ready(entry(outgoing ? {
8661
+ previous: outgoing,
8662
+ rotatedAt: (/* @__PURE__ */ new Date()).toISOString()
8663
+ } : {}), key.AccessKeyId);
8664
+ });
8665
+ 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}`);
8666
+ }
8667
+ /**
8668
+ * Each provisioned bucket answers `HeadBucket` to the key in the stage's
8669
+ * secrets — the one the apps will use. A bucket whose URL carries no key is
8670
+ * not one this provider wrote, and the deploy's own check covers a URL that
8671
+ * is missing.
8672
+ */
8673
+ async function verifyS3(ctx) {
8674
+ const S3 = await import("@aws-sdk/client-s3");
8675
+ for (const { id } of bucketConstructs(ctx.manifest)) {
8676
+ const url = ctx.secrets[provideKey(id, "url")];
8677
+ if (url === void 0) continue;
8678
+ const address = s3Url.parse(url);
8679
+ if (!address.accessKeyId || !address.secretAccessKey) continue;
8680
+ const client = new S3.S3Client({
8681
+ region: address.region ?? ctx.config.region ?? "us-east-1",
8682
+ credentials: {
8683
+ accessKeyId: address.accessKeyId,
8684
+ secretAccessKey: address.secretAccessKey
8685
+ },
8686
+ ...address.endpoint ? { endpoint: address.endpoint } : {},
8687
+ ...address.forcePathStyle ? { forcePathStyle: true } : {}
8688
+ });
8689
+ try {
8690
+ await client.send(new S3.HeadBucketCommand({ Bucket: address.bucket }));
8691
+ } catch (error) {
8692
+ const name = errorName(error);
8693
+ throw new ProvisionedBucketUnreachable(id, address.bucket, ctx.stage, name === "NotFound" ? "it does not exist" : name === "Forbidden" ? "the key is refused" : name || String(error));
8694
+ } finally {
8695
+ client.destroy();
8696
+ }
8697
+ }
8698
+ }
8699
+ //#endregion
8700
+ //#region src/providers/registry.ts
8701
+ /**
8702
+ * The providers gkm ships, by kind and name — registered the way a deploy
8703
+ * target's built-ins are (`target/builtins.ts`). A provider from a package is
8704
+ * one more entry here once there is a loading story for it.
8705
+ */
8706
+ const BUILTIN_PROVIDERS = { objects: { s3: {
8707
+ kind: "objects",
8708
+ name: "s3",
8709
+ check: checkS3Config,
8710
+ provisioning: S3_PROVISIONING,
8711
+ credentials: awsProvisioningCredentials,
8712
+ runtimeKeys(manifest) {
8713
+ return bucketConstructs(manifest).flatMap((b) => [provideKey(b.id, "url"), ...b.servers.map((s) => provideKey(s.id, "url"))]);
8714
+ },
8715
+ ensure: ensureS3,
8716
+ verify: verifyS3
8717
+ } } };
8718
+ /** The provider `kind` names `name`, if gkm has one. */
8719
+ function builtinProvider(kind, name) {
8720
+ return Object.hasOwn(BUILTIN_PROVIDERS[kind], name) ? BUILTIN_PROVIDERS[kind][name] : void 0;
8721
+ }
8722
+ //#endregion
8723
+ //#region src/providers/index.ts
8724
+ /** The constructs of a kind: `objects` is every bucket. */
8725
+ function constructsOf(manifest, kind) {
8726
+ return Object.entries(manifest).filter(([, d]) => d.kind === kind).map(([id]) => id).sort();
8727
+ }
8728
+ /**
8729
+ * What every deploy runs: each provider's `verify()` — the cheap check that
8730
+ * what it created is still there and the stage's key reaches it. Nothing on
8731
+ * the local stage, and nothing for a kind on `external`.
8732
+ *
8733
+ * @throws {StageProviderDisabled} for a declared kind the stage has none of
8734
+ */
8735
+ async function verifyStageProviders(input) {
8736
+ assertStageProvidersEnabled(input.workspace, input.manifest, input.stage);
8737
+ const verified = [];
8738
+ for (const kind of PROVIDER_KINDS) {
8739
+ if (constructsOf(input.manifest, kind).length === 0) continue;
8740
+ const choice = stageProvider(input.workspace, kind, input.stage);
8741
+ if (choice.mode !== "provider") continue;
8742
+ const provider = builtinProvider(kind, choice.name);
8743
+ await provider.verify({
8744
+ workspace: input.workspace,
8745
+ manifest: input.manifest,
8746
+ stage: input.stage,
8747
+ config: choice.config,
8748
+ secrets: input.secrets
8749
+ });
8750
+ verified.push(`${kind}: ${provider.name}`);
8751
+ }
8752
+ return verified;
8753
+ }
8754
+ /**
8755
+ * `ensure()` for every provider the stage configures — find or create, fix
8756
+ * drift, record in the stage's state, write the runtime keys into its
8757
+ * secrets. A provider without provisioning credentials leaves the stage as
8758
+ * `external` and says how to supply them.
8759
+ */
8760
+ async function provisionStage(options) {
8761
+ const { workspace, stage } = options;
8762
+ const dryRun = options.dryRun === true;
8763
+ const env = options.env ?? process.env;
8764
+ const log = options.log ?? ((line) => console.log(line));
8765
+ const named = PROVIDER_KINDS.some((kind) => stageProvider(workspace, kind, stage).mode === "provider");
8766
+ let found = options.manifest;
8767
+ let runnables = options.runnables ?? {};
8768
+ if (!found && named) {
8769
+ runnables = {};
8770
+ found = await discover({
8771
+ patterns: constructGlobs(workspace),
8772
+ cwd: workspace.root,
8773
+ runnables
8774
+ });
8775
+ }
8776
+ const manifest = found ?? {};
8777
+ const reports = [];
8778
+ const configured = [];
8779
+ for (const kind of PROVIDER_KINDS) {
8780
+ const choice = stageProvider(workspace, kind, stage);
8781
+ if (choice.mode !== "provider") {
8782
+ reports.push({
8783
+ kind,
8784
+ mode: choice.mode,
8785
+ actions: []
8786
+ });
8787
+ continue;
8788
+ }
8789
+ const provider = builtinProvider(kind, choice.name);
8790
+ const report = {
8791
+ kind,
8792
+ mode: "provider",
8793
+ provider: provider.name,
8794
+ actions: []
8795
+ };
8796
+ reports.push(report);
8797
+ if (constructsOf(manifest, kind).length === 0) {
8798
+ log(` ${kind}: ${provider.name} — nothing declared to provision`);
8799
+ continue;
8800
+ }
8801
+ const credential = await provider.credentials({
8802
+ stage,
8803
+ env,
8804
+ ...options.profile ? { profile: options.profile } : {},
8805
+ ...options.home ? { home: options.home } : {}
8806
+ });
8807
+ report.credentials = credential !== void 0;
8808
+ if (credential === void 0) {
8809
+ 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)}.`);
8810
+ continue;
8811
+ }
8812
+ configured.push({
8813
+ kind,
8814
+ provider,
8815
+ config: choice.config,
8816
+ credential,
8817
+ report
8818
+ });
8819
+ }
8820
+ if (configured.length === 0) return reports;
8821
+ const identity = deployIdentity(workspace, stage);
8822
+ const secretsStore = await secretsStoreFor(workspace, stage, {
8823
+ ...options.profile ? { profile: options.profile } : {},
8824
+ ...options.home ? { home: options.home } : {}
8825
+ });
8826
+ let secrets = (await secretsStore.read(stage))?.custom ?? {};
8827
+ const stateStore = await createStateStore({
8828
+ config: workspace.state,
8829
+ workspaceRoot: workspace.root,
8830
+ workspaceName: workspace.name
8831
+ });
8832
+ const lock = dryRun ? void 0 : await stateStore.lock(stage, { operation: "provision" });
8833
+ try {
8834
+ const state = dryRun ? await readOnlyState(stateStore, stage) : journalState(await DeployJournal.open(stateStore, stage, () => createEmptyState(stage, "", "")));
8835
+ const writeSecrets = async (values) => {
8836
+ if (dryRun) return;
8837
+ const stored = await secretsStore.read(stage) ?? initStageSecrets(stage);
8838
+ const next = {
8839
+ ...stored,
8840
+ custom: {
8841
+ ...stored.custom,
8842
+ ...values
8843
+ },
8844
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8845
+ };
8846
+ await secretsStore.write(stage, next);
8847
+ secrets = next.custom;
8848
+ };
8849
+ for (const entry of configured) {
8850
+ log(` ${entry.kind}: ${entry.provider.name}${dryRun ? " (dry run)" : ""}`);
8851
+ await entry.provider.ensure({
8852
+ workspace,
8853
+ manifest,
8854
+ runnables,
8855
+ stage,
8856
+ identity,
8857
+ config: entry.config,
8858
+ credential: entry.credential,
8859
+ get secrets() {
8860
+ return secrets;
8861
+ },
8862
+ state,
8863
+ dryRun,
8864
+ rotateKeys: options.rotateKeys === true,
8865
+ retireOldKeys: options.retireOldKeys === true,
8866
+ async change(action, apply) {
8867
+ if (!dryRun) await apply();
8868
+ entry.report.actions.push(action);
8869
+ log(` ${dryRun ? "would " : ""}${action.change} — ${action.resource}`);
8870
+ },
8871
+ writeSecrets,
8872
+ log
8873
+ });
8874
+ if (entry.report.actions.length === 0) log(" up to date");
8875
+ }
8876
+ } finally {
8877
+ await lock?.release();
8878
+ }
8879
+ return reports;
8880
+ }
8881
+ /** The journal as `ensure()` sees it. */
8882
+ function journalState(journal) {
8883
+ return {
8884
+ record: (key) => journal.record(key),
8885
+ pending: (entry) => journal.pending(entry),
8886
+ ready: (entry, id) => journal.ready(entry, id),
8887
+ lastDeployedAt: journal.existed ? journal.state.lastDeployedAt : void 0
8888
+ };
8889
+ }
8890
+ /** What a dry run reads, and the writes it does not make. */
8891
+ async function readOnlyState(store, stage) {
8892
+ const stored = await store.read(stage);
8893
+ return {
8894
+ record: (key) => stored?.resources[key],
8895
+ pending: async () => {},
8896
+ ready: async () => {},
8897
+ ...stored ? { lastDeployedAt: stored.state.lastDeployedAt } : {}
8898
+ };
8899
+ }
8900
+ //#endregion
7429
8901
  //#region src/secrets/stale.ts
7430
8902
  /**
7431
8903
  * Stored secrets an older gkm generated that now shadow a derived value.
@@ -7574,6 +9046,6 @@ function reportPlannedSeeds(planned, log) {
7574
9046
  for (const { folder, seeds } of planned) log(`🌱 ${folder}: would run ${seeds.length} (${seeds.join(", ")})`);
7575
9047
  }
7576
9048
  //#endregion
7577
- export { postgresSuperuser as $, describeServices as $t, UnknownDevService as A, maskPassword as An, WORKER_PORT as At, WorkerGenerator as B, NoDomainForStage as Bn, composeFor as Bt, workerDockerfile as C, imageName as Cn, networkEnv as Ct, DevServicesNeedServerTarget as D, ownershipMarker as Dn, composeBuildPaths as Dt, DEV_SERVICES as E, markerOf as En, localRolePassword as Et, parseDevServices as F, LOGS_SERVICE as Fn, cacheBackendFor as Ft, FAKE_ENV as G, hostFor as Gt, workerEntryDir as H, resolveHost as Hn, TELEMETRY_CONTAINER as Ht, reportDevServices as I, logsAccess as In, eventsBackendFor as It, derivedContainers as J, scopeTelemetryEnv as Jt, backendsOf as K, appTelemetry as Kt, requiredStageKeys as L, logsService as Ln, providerOf as Lt, devServicesUsed as M, toEmbeddableSecrets as Mn, findBuildRoot as Mt, externalServices as N, withCustomSecret as Nn, imageBuildCredentials as Nt, ExternalServicesNotConfigured as O, projectName as On, imageLayout as Ot, manifestServiceDeclarations as P, LOCAL_LOGS_EMAIL as Pn, isImageBuild as Pt, loadLocalCredentials as Q, DEFAULT_IMAGES as Qt, suppliedOnly as R, stackLogs as Rn, storageBackendFor as Rt, ensureDockerignore as S, deployIdentity as Sn, dockerfileOf as St, validateImageRef as T, isOwnedBy as Tn, envFor as Tt, QueueGenerator as U, planFor as Ut, workerBundleName as V, isMainFrontendApp as Vn, EMULATOR_ACCESS_KEY_ID as Vt, CronGenerator as W, resourceName as Wt, workspace_exports as X, telemetryOf as Xt, reconcileWorkspace as Y, surfaceClientTraced as Yt, LOCAL_RABBITMQ_USER as Z, usesTelemetry as Zt, DeployJournal as _, getKeyPath as _n, LEGACY_LOCAL_LOGIN as _t, createEmptyState as a, createEntryWrapper as an, runsRedis as at, appDockerfile as b, readKey as bn, pgClient as bt, isDnsVerified as c, loadEnvFiles as cn, LOCAL_CA_PATH as ct, setApplicationId as d, prepareEntryCredentials as dn, applyPolicies as dt, fakeKey as en, readLocalCredentials as et, setBackupState as f, isRemoteStore as fn, applyPostgres as ft, setPostgresBackupId as g, FileSecretsStore as gn, quoteIdentifier as gt, setGeneratedSecret as h, store_exports as hn, postgresStatements as ht, assertNoStaleSecrets as i, createCredentialsPreload as in, redisService as it, assertExternalServices as j, storage_exports as jn, detectPackageManager as jt, STORAGE_KEY_PAIR as k, initStageSecrets as kn, TURBO_VERSION as kt, recordRelease as l, loadPortState as ln, forgetState as lt, setDnsVerification as m, storeFromConfig as mn, postgresDatabaseNames as mt, reportDatabaseRuns as n, portsOf as nn, REDIS_SERVICE as nt, getBackupState as o, findAvailablePort as on, stackRedis as ot, setDnsRecord as p, secretsStoreFor as pn, bucketPolicies as pt, constructGlobs as q, isTelemetryKey as qt, reportPlannedSeeds as r, primaryPortKey as rn, STACK_CACHE as rt, getGeneratedSecret as s, isPortAvailable as sn, withRedisPassword as st, DeploySeedsFailed as t, portKeys as tn, withGeneratedSecrets as tt, recordRollback as u, loadSecretsForApp as un, applyBuckets as ut, DockerBuildFailed as v, keystoreProject as vn, ensurePostgresLogin as vt, workerDockerfileOf as w, imageRef as wn, workerEnvKeys as wt, dockerCommand as x, applicationName as xn, appEnvKeys as xt, DockerPushFailed as y, projectKey as yn, bucketClient as yt, workerUnits as z, withLogsPassword as zn, CLUSTER_DATABASE as zt };
9049
+ export { suppliedOnly as $, stackLogs as $n, storageBackendFor as $t, workerDockerfileOf as A, store_exports as An, postgresStatements as At, newLockHolder as B, isOwnedBy as Bn, envFor as Bt, stageProviderNotes as C, loadEnvFiles as Cn, LOCAL_CA_PATH as Ct, dockerCommand as D, isRemoteStore as Dn, applyPostgres as Dt, appDockerfile as E, prepareEntryCredentials as En, applyPolicies as Et, StateStoreBusy as F, readKey as Fn, pgClient as Ft, STORAGE_KEY_PAIR as G, maskPassword as Gn, WORKER_PORT as Gt, DEV_SERVICES as H, ownershipMarker as Hn, composeBuildPaths as Ht, StateStoreWithoutLocking as I, applicationName as In, appEnvKeys as It, devServicesUsed as J, withCustomSecret as Jn, imageBuildCredentials as Jt, assertDevServicesFlag as K, storage_exports as Kn, detectPackageManager as Kt, StateVersionConflict as L, deployIdentity as Ln, dockerfileOf as Lt, DocumentStateStore as M, getKeyPath as Mn, LEGACY_LOCAL_LOGIN as Mt, StageStateMissing as N, keystoreProject as Nn, ensurePostgresLogin as Nt, ensureDockerignore as O, secretsStoreFor as On, bucketPolicies as Ot, StateLocked as P, projectKey as Pn, bucketClient as Pt, requiredStageKeys as Q, logsService as Qn, providerOf as Qt, contentVersion as R, imageName as Rn, networkEnv as Rt, provisionCommand as S, isPortAvailable as Sn, withRedisPassword as St, DockerPushFailed as T, loadSecretsForApp as Tn, applyBuckets as Tt, DevServicesNeedServerTarget as U, projectName as Un, imageLayout as Ut, AllowDevServicesTakesNoValue as V, markerOf as Vn, localRolePassword as Vt, ExternalServicesNotConfigured as W, initStageSecrets as Wn, TURBO_VERSION as Wt, manifestServiceDeclarations as X, LOGS_SERVICE as Xn, cacheBackendFor as Xt, externalServices as Y, LOCAL_LOGS_EMAIL as Yn, isImageBuild as Yt, reportDevServices as Z, logsAccess as Zn, eventsBackendFor as Zt, setGeneratedSecret as _, portsOf as _n, REDIS_SERVICE as _t, provisionStage as a, resourceName as an, CronGenerator as at, StageProviderDisabled as b, createEntryWrapper as bn, runsRedis as bt, getBackupState as c, isTelemetryKey as cn, constructGlobs as ct, recordRelease as d, telemetryOf as dn, workspace_exports as dt, CLUSTER_DATABASE as en, withLogsPassword as er, workerUnits as et, recordRollback as f, usesTelemetry as fn, LOCAL_RABBITMQ_USER as ft, setDnsVerification as g, portKeys as gn, withGeneratedSecrets as gt, setDnsRecord as h, fakeKey as hn, readLocalCredentials as ht, assertNoStaleSecrets as i, planFor as in, QueueGenerator as it, validateImageRef as j, FileSecretsStore as jn, quoteIdentifier as jt, workerDockerfile as k, storeFromConfig as kn, postgresDatabaseNames as kt, getGeneratedSecret as l, scopeTelemetryEnv as ln, derivedContainers as lt, setBackupState as m, describeServices as mn, postgresSuperuser as mt, reportDatabaseRuns as n, EMULATOR_ACCESS_KEY_ID as nn, isMainFrontendApp as nr, workerBundleName as nt, verifyStageProviders as o, hostFor as on, FAKE_ENV as ot, setApplicationId as p, DEFAULT_IMAGES as pn, loadLocalCredentials as pt, assertExternalServices as q, toEmbeddableSecrets as qn, findBuildRoot as qt, reportPlannedSeeds as r, TELEMETRY_CONTAINER as rn, resolveHost as rr, workerEntryDir as rt, createEmptyState as s, appTelemetry as sn, backendsOf as st, DeploySeedsFailed as t, composeFor as tn, NoDomainForStage as tr, WorkerGenerator as tt, isDnsVerified as u, surfaceClientTraced as un, reconcileWorkspace as ut, setPostgresBackupId as v, primaryPortKey as vn, STACK_CACHE as vt, DockerBuildFailed as w, loadPortState as wn, forgetState as wt, assertStageProvidersEnabled as x, findAvailablePort as xn, stackRedis as xt, DeployJournal as y, createCredentialsPreload as yn, redisService as yt, createStateStore as z, imageRef as zn, workerEnvKeys as zt };
7578
9050
 
7579
- //# sourceMappingURL=seeds-9C5CkDon.mjs.map
9051
+ //# sourceMappingURL=seeds-BYdrtZsC.mjs.map