@geekmidas/cli 10.0.0-alpha.125 → 10.0.0-alpha.126

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 (148) hide show
  1. package/dist/{GoDaddyProvider-BN1dODm9.mjs → GoDaddyProvider-CvRVmN4W.mjs} +2 -2
  2. package/dist/{GoDaddyProvider-BN1dODm9.mjs.map → GoDaddyProvider-CvRVmN4W.mjs.map} +1 -1
  3. package/dist/{GoDaddyProvider-BTdtqhI_.cjs → GoDaddyProvider-DFfpzCOP.cjs} +2 -2
  4. package/dist/{GoDaddyProvider-BTdtqhI_.cjs.map → GoDaddyProvider-DFfpzCOP.cjs.map} +1 -1
  5. package/dist/{LegacyStateStore-C2S8TGwx.cjs → LegacyStateStore-BFrCHrFQ.cjs} +3 -3
  6. package/dist/{LegacyStateStore-C2S8TGwx.cjs.map → LegacyStateStore-BFrCHrFQ.cjs.map} +1 -1
  7. package/dist/{LegacyStateStore-DgXmuG87.mjs → LegacyStateStore-CgfENrXF.mjs} +3 -3
  8. package/dist/{LegacyStateStore-DgXmuG87.mjs.map → LegacyStateStore-CgfENrXF.mjs.map} +1 -1
  9. package/dist/{LocalStateStore-CaJw9H-3.cjs → LocalStateStore-BU5z_IOE.cjs} +2 -2
  10. package/dist/{LocalStateStore-CaJw9H-3.cjs.map → LocalStateStore-BU5z_IOE.cjs.map} +1 -1
  11. package/dist/{LocalStateStore-ojY_BiMK.mjs → LocalStateStore-DxoNpiBg.mjs} +2 -2
  12. package/dist/{LocalStateStore-ojY_BiMK.mjs.map → LocalStateStore-DxoNpiBg.mjs.map} +1 -1
  13. package/dist/{S3StateStore-BSPkxhrx.mjs → S3StateStore-CGMSDilh.mjs} +2 -2
  14. package/dist/{S3StateStore-BSPkxhrx.mjs.map → S3StateStore-CGMSDilh.mjs.map} +1 -1
  15. package/dist/{S3StateStore-BqugXIcb.cjs → S3StateStore-DiekMLpn.cjs} +2 -2
  16. package/dist/{S3StateStore-BqugXIcb.cjs.map → S3StateStore-DiekMLpn.cjs.map} +1 -1
  17. package/dist/{SSMStateStore-B0OsuT_k.cjs → SSMStateStore-BDCOFdKq.cjs} +2 -2
  18. package/dist/{SSMStateStore-B0OsuT_k.cjs.map → SSMStateStore-BDCOFdKq.cjs.map} +1 -1
  19. package/dist/{SSMStateStore-DM85OiH0.mjs → SSMStateStore-Cq2H9MlF.mjs} +2 -2
  20. package/dist/{SSMStateStore-DM85OiH0.mjs.map → SSMStateStore-Cq2H9MlF.mjs.map} +1 -1
  21. package/dist/{SubscriberGenerator-ocQ2yj0K.mjs → SubscriberGenerator-BALddfuW.mjs} +2 -60
  22. package/dist/SubscriberGenerator-BALddfuW.mjs.map +1 -0
  23. package/dist/{SubscriberGenerator-BDUp7OeY.cjs → SubscriberGenerator-LgyTxZ8r.cjs} +1 -89
  24. package/dist/SubscriberGenerator-LgyTxZ8r.cjs.map +1 -0
  25. package/dist/{add-CrxvmSpq.cjs → add-BsiBYkAW.cjs} +3 -3
  26. package/dist/{add-CrxvmSpq.cjs.map → add-BsiBYkAW.cjs.map} +1 -1
  27. package/dist/{add-C7ZBXIzM.mjs → add-CamA9Uqv.mjs} +3 -3
  28. package/dist/{add-C7ZBXIzM.mjs.map → add-CamA9Uqv.mjs.map} +1 -1
  29. package/dist/{bundler-DpZ9uBGN.mjs → bundler-2V2PR_E-.mjs} +5 -5
  30. package/dist/{bundler-DpZ9uBGN.mjs.map → bundler-2V2PR_E-.mjs.map} +1 -1
  31. package/dist/{bundler-DVUXW95p.cjs → bundler-CijtdIZ2.cjs} +5 -5
  32. package/dist/{bundler-DVUXW95p.cjs.map → bundler-CijtdIZ2.cjs.map} +1 -1
  33. package/dist/{cli-BMRhE6cZ.cjs → cli-7gc1QG8X.cjs} +2 -2
  34. package/dist/{cli-BMRhE6cZ.cjs.map → cli-7gc1QG8X.cjs.map} +1 -1
  35. package/dist/{cli-BWfYyy1k.mjs → cli-PiyYqHGx.mjs} +2 -2
  36. package/dist/{cli-BWfYyy1k.mjs.map → cli-PiyYqHGx.mjs.map} +1 -1
  37. package/dist/{compose-Bqcy0lqo.cjs → compose-8e2xJ61E.cjs} +9 -8
  38. package/dist/compose-8e2xJ61E.cjs.map +1 -0
  39. package/dist/{compose-BFmdLig3.mjs → compose-CvHIjMDd.mjs} +8 -7
  40. package/dist/{compose-BFmdLig3.mjs.map → compose-CvHIjMDd.mjs.map} +1 -1
  41. package/dist/{databases-BPOgTn1u.mjs → databases-6GD0tI2F.mjs} +65 -3
  42. package/dist/databases-6GD0tI2F.mjs.map +1 -0
  43. package/dist/{databases-fOaoBInm.cjs → databases-CuVGf7-M.cjs} +75 -1
  44. package/dist/databases-CuVGf7-M.cjs.map +1 -0
  45. package/dist/deploy/public.cjs +6 -6
  46. package/dist/deploy/public.d.cts.map +1 -1
  47. package/dist/deploy/public.d.mts.map +1 -1
  48. package/dist/deploy/public.mjs +6 -6
  49. package/dist/{deploy-D8pUA9Y0.mjs → deploy-CIPwTJ47.mjs} +6 -6
  50. package/dist/{deploy-D8pUA9Y0.mjs.map → deploy-CIPwTJ47.mjs.map} +1 -1
  51. package/dist/{deploy-DVrNjX1X.cjs → deploy-wSlIRPn5.cjs} +6 -6
  52. package/dist/{deploy-DVrNjX1X.cjs.map → deploy-wSlIRPn5.cjs.map} +1 -1
  53. package/dist/{dokploy-TtK6Zyl3.cjs → dokploy-BKbUrz05.cjs} +2 -2
  54. package/dist/{dokploy-TtK6Zyl3.cjs.map → dokploy-BKbUrz05.cjs.map} +1 -1
  55. package/dist/{dokploy-CzDGNB6A.mjs → dokploy-BZf1AHd0.mjs} +2 -2
  56. package/dist/{dokploy-CzDGNB6A.mjs.map → dokploy-BZf1AHd0.mjs.map} +1 -1
  57. package/dist/{drivers-L29WvE95.cjs → drivers-C47nm-wB.cjs} +91 -4
  58. package/dist/drivers-C47nm-wB.cjs.map +1 -0
  59. package/dist/{drivers-C-VqMtld.mjs → drivers-CDjpjakg.mjs} +60 -3
  60. package/dist/drivers-CDjpjakg.mjs.map +1 -0
  61. package/dist/{eas-DaobzDks.mjs → eas-CxX2RCE_.mjs} +2 -2
  62. package/dist/{eas-DaobzDks.mjs.map → eas-CxX2RCE_.mjs.map} +1 -1
  63. package/dist/{eas-D3clzkni.cjs → eas-DzDMGzfc.cjs} +2 -2
  64. package/dist/{eas-D3clzkni.cjs.map → eas-DzDMGzfc.cjs.map} +1 -1
  65. package/dist/{encryption-DV31gEY8.mjs → encryption-BFSrt89F.mjs} +2 -2
  66. package/dist/{encryption-DV31gEY8.mjs.map → encryption-BFSrt89F.mjs.map} +1 -1
  67. package/dist/{encryption-DBkIFiR2.cjs → encryption-RNcBnG4Z.cjs} +2 -2
  68. package/dist/{encryption-DBkIFiR2.cjs.map → encryption-RNcBnG4Z.cjs.map} +1 -1
  69. package/dist/{engine-CuZUGbEh.cjs → engine-Dqe9KQCm.cjs} +7 -5
  70. package/dist/engine-Dqe9KQCm.cjs.map +1 -0
  71. package/dist/{engine-CFOR9h6X.mjs → engine-_0pjyCd_.mjs} +7 -5
  72. package/dist/engine-_0pjyCd_.mjs.map +1 -0
  73. package/dist/{github-CsvtWSBW.cjs → github-Csev5EFf.cjs} +4 -4
  74. package/dist/{github-CsvtWSBW.cjs.map → github-Csev5EFf.cjs.map} +1 -1
  75. package/dist/{github-Nep8pI4_.mjs → github-Dck0nLW9.mjs} +4 -4
  76. package/dist/{github-Nep8pI4_.mjs.map → github-Dck0nLW9.mjs.map} +1 -1
  77. package/dist/{googlePlay-C-UfBXJU.cjs → googlePlay-DBtDAGBk.cjs} +3 -3
  78. package/dist/{googlePlay-C-UfBXJU.cjs.map → googlePlay-DBtDAGBk.cjs.map} +1 -1
  79. package/dist/{googlePlay-lF7Atm_N.mjs → googlePlay-DTuvDHi5.mjs} +3 -3
  80. package/dist/{googlePlay-lF7Atm_N.mjs.map → googlePlay-DTuvDHi5.mjs.map} +1 -1
  81. package/dist/index.cjs +58 -57
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.mjs +57 -56
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/{migrate-hlNKNWmg.cjs → migrate-3ZEoqx5q.cjs} +2 -2
  86. package/dist/{migrate-hlNKNWmg.cjs.map → migrate-3ZEoqx5q.cjs.map} +1 -1
  87. package/dist/{migrate-_sBX0pve.mjs → migrate-BABIuzQj.mjs} +2 -2
  88. package/dist/{migrate-_sBX0pve.mjs.map → migrate-BABIuzQj.mjs.map} +1 -1
  89. package/dist/{migrate-NfbPVhi9.mjs → migrate-DZeNpuX2.mjs} +6 -4
  90. package/dist/migrate-DZeNpuX2.mjs.map +1 -0
  91. package/dist/{migrate-Dp3GhFnf.cjs → migrate-pGiUqvBi.cjs} +6 -4
  92. package/dist/migrate-pGiUqvBi.cjs.map +1 -0
  93. package/dist/{publish-BuUVaZVy.mjs → publish-BsnDeOVL.mjs} +2 -2
  94. package/dist/{publish-BuUVaZVy.mjs.map → publish-BsnDeOVL.mjs.map} +1 -1
  95. package/dist/{publish-ZOo4xEuZ.cjs → publish-CS6xDvGT.cjs} +2 -2
  96. package/dist/{publish-ZOo4xEuZ.cjs.map → publish-CS6xDvGT.cjs.map} +1 -1
  97. package/dist/{ready-qnqp6JAY.cjs → ready-CxPJm0Hc.cjs} +3 -3
  98. package/dist/{ready-qnqp6JAY.cjs.map → ready-CxPJm0Hc.cjs.map} +1 -1
  99. package/dist/{ready-Ca883OvY.mjs → ready-DrvaAwKv.mjs} +3 -3
  100. package/dist/{ready-Ca883OvY.mjs.map → ready-DrvaAwKv.mjs.map} +1 -1
  101. package/dist/{resolve-BCU9pYDx.mjs → resolve-fCGJmM-R.mjs} +3 -3
  102. package/dist/{resolve-BCU9pYDx.mjs.map → resolve-fCGJmM-R.mjs.map} +1 -1
  103. package/dist/{resolve-C9QqOl5t.cjs → resolve-zqCMFio0.cjs} +3 -3
  104. package/dist/{resolve-C9QqOl5t.cjs.map → resolve-zqCMFio0.cjs.map} +1 -1
  105. package/dist/{rollback-CWyyTQ0B.mjs → rollback-BkhVrtAf.mjs} +4 -4
  106. package/dist/{rollback-CWyyTQ0B.mjs.map → rollback-BkhVrtAf.mjs.map} +1 -1
  107. package/dist/{rollback-CTWdDBgB.cjs → rollback-qc79mj6L.cjs} +4 -4
  108. package/dist/{rollback-CTWdDBgB.cjs.map → rollback-qc79mj6L.cjs.map} +1 -1
  109. package/dist/sandbox/migrate-worker.cjs +4 -3
  110. package/dist/sandbox/migrate-worker.cjs.map +1 -1
  111. package/dist/sandbox/migrate-worker.mjs +4 -3
  112. package/dist/sandbox/migrate-worker.mjs.map +1 -1
  113. package/dist/{seeds-CPEcmoPn.cjs → seeds-3awh33Wz.cjs} +12 -11
  114. package/dist/{seeds-CPEcmoPn.cjs.map → seeds-3awh33Wz.cjs.map} +1 -1
  115. package/dist/{seeds-CK7w79VC.mjs → seeds-Cfejy8wj.mjs} +5 -4
  116. package/dist/{seeds-CK7w79VC.mjs.map → seeds-Cfejy8wj.mjs.map} +1 -1
  117. package/dist/{serverSetup-C6t6Vtt7.cjs → serverSetup-BFQuwUeL.cjs} +5 -5
  118. package/dist/{serverSetup-C6t6Vtt7.cjs.map → serverSetup-BFQuwUeL.cjs.map} +1 -1
  119. package/dist/{serverSetup-BAPNuNEX.mjs → serverSetup-C8ZbHTm_.mjs} +5 -5
  120. package/dist/{serverSetup-BAPNuNEX.mjs.map → serverSetup-C8ZbHTm_.mjs.map} +1 -1
  121. package/dist/{sniffer-g9v7cqDN.mjs → sniffer-CCkdzjiU.mjs} +6 -6
  122. package/dist/{sniffer-g9v7cqDN.mjs.map → sniffer-CCkdzjiU.mjs.map} +1 -1
  123. package/dist/{sniffer-DdX_WN_-.cjs → sniffer-Cq1nhxIB.cjs} +6 -6
  124. package/dist/{sniffer-DdX_WN_-.cjs.map → sniffer-Cq1nhxIB.cjs.map} +1 -1
  125. package/dist/{snsSubscriptions-5Cqww8S7.cjs → snsSubscriptions-DYyYP0Xf.cjs} +2 -2
  126. package/dist/{snsSubscriptions-5Cqww8S7.cjs.map → snsSubscriptions-DYyYP0Xf.cjs.map} +1 -1
  127. package/dist/{snsSubscriptions-BmYeeTH8.mjs → snsSubscriptions-R_AKBZYK.mjs} +2 -2
  128. package/dist/{snsSubscriptions-BmYeeTH8.mjs.map → snsSubscriptions-R_AKBZYK.mjs.map} +1 -1
  129. package/dist/{stage-CUE5pMmw.cjs → stage-BkgbVJ9Z.cjs} +4 -4
  130. package/dist/{stage-CUE5pMmw.cjs.map → stage-BkgbVJ9Z.cjs.map} +1 -1
  131. package/dist/{stage-C-I2dVw1.mjs → stage-DCaxqm-G.mjs} +4 -4
  132. package/dist/{stage-C-I2dVw1.mjs.map → stage-DCaxqm-G.mjs.map} +1 -1
  133. package/dist/target/public.cjs +1 -1
  134. package/dist/target/public.mjs +1 -1
  135. package/dist/vitest.cjs +1 -1
  136. package/dist/vitest.mjs +1 -1
  137. package/package.json +15 -15
  138. package/dist/SubscriberGenerator-BDUp7OeY.cjs.map +0 -1
  139. package/dist/SubscriberGenerator-ocQ2yj0K.mjs.map +0 -1
  140. package/dist/compose-Bqcy0lqo.cjs.map +0 -1
  141. package/dist/databases-BPOgTn1u.mjs.map +0 -1
  142. package/dist/databases-fOaoBInm.cjs.map +0 -1
  143. package/dist/drivers-C-VqMtld.mjs.map +0 -1
  144. package/dist/drivers-L29WvE95.cjs.map +0 -1
  145. package/dist/engine-CFOR9h6X.mjs.map +0 -1
  146. package/dist/engine-CuZUGbEh.cjs.map +0 -1
  147. package/dist/migrate-Dp3GhFnf.cjs.map +0 -1
  148. package/dist/migrate-NfbPVhi9.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"S3StateStore-BSPkxhrx.mjs","names":[],"sources":["../src/deploy/S3StateStore.ts"],"sourcesContent":["/**\n * Amazon S3 state store.\n *\n * In the bucket the config names, which must exist — or, with none named, in\n * the project bucket (`providers/projectBucket.ts`): found by its name, which\n * is worked out from the project and the account, and created by the first\n * write (a deploy's lock, `state:push`). A read of a project bucket that does\n * not exist yet is a stage with no state; it creates nothing.\n *\n * - `<prefix>/<workspace>/<stage>/state.json`: the state.\n * - `<prefix>/<workspace>/<stage>/lock.json`: the lock.\n * - `<prefix>/<workspace>/<stage>/state.v1.json`: the v1 state, kept on\n * migration.\n *\n * Every write is conditional on the server: `If-None-Match: *` to create,\n * `If-Match: <etag>` to replace. S3 answers a lost race with 412 (or 409 when\n * two conditional writes are in flight at once), so there is no window\n * between a check and a write for another run to slip into.\n */\n\nimport {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket';\nimport { type DeployIdentity, deployIdentity } from './identity';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface S3StateStoreOptions {\n\tworkspaceName: string;\n\t/** Omitted, the project bucket — created by the first write. */\n\tbucket?: string;\n\t/** `deploy.namespace`, for the project bucket's name. */\n\tnamespace?: string;\n\t/** Key prefix inside the bucket; `gkm` by default. */\n\tprefix?: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: S3ClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\n/** The project bucket, for a store that names no bucket of its own. */\nexport interface ProjectBucketSource {\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\tregion: string;\n\t/** What the account id — and so the bucket's name — is read with. */\n\tsts: STSClient;\n}\n\nexport class S3StateStore extends DocumentStateStore {\n\tprivate readonly prefix: string;\n\t/** The bucket's name, once known. */\n\tprivate name?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly source: string | ProjectBucketSource,\n\t\tprivate readonly client: S3Client,\n\t\tprefix = 'gkm',\n\t) {\n\t\tsuper();\n\t\tthis.prefix = prefix.replace(/\\/+$/, '');\n\t\tif (typeof source === 'string') {\n\t\t\tthis.name = source;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; a project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\tif (this.name) return this.name;\n\t\tconst { identity } = this.source as ProjectBucketSource;\n\t\treturn projectBucketName(identity.scope, '<account id>');\n\t}\n\n\t/**\n\t * The bucket, or `null` for a project bucket that does not exist yet and\n\t * is not to be created (`create: false`, a read).\n\t */\n\tprivate async use(create: boolean): Promise<string | null> {\n\t\tif (this.exists) return this.name!;\n\t\tconst source = this.source as ProjectBucketSource;\n\t\tthis.name ??= projectBucketName(\n\t\t\tsource.identity.scope,\n\t\t\tawait callerAccountId(source.sts),\n\t\t);\n\t\tif (create) {\n\t\t\tawait ensureProjectBucket(this.client, {\n\t\t\t\tbucket: this.name,\n\t\t\t\tregion: source.region,\n\t\t\t\tidentity: source.identity,\n\t\t\t});\n\t\t} else if ((await findProjectBucket(this.client, this.name)) === 'none') {\n\t\t\treturn null;\n\t\t}\n\t\tthis.exists = true;\n\t\treturn this.name;\n\t}\n\n\tstatic create(options: S3StateStoreOptions): S3StateStore {\n\t\tconst endpoint =\n\t\t\toptions.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\t// The region the project bucket is created in is the one it is reached in.\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\t\tconst config: S3ClientConfig = {\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\t// The emulator serves buckets by path, not by subdomain.\n\t\t\tforcePathStyle: endpoint ? true : undefined,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\tlet source: string | ProjectBucketSource;\n\t\tif (options.bucket) {\n\t\t\tsource = options.bucket;\n\t\t} else {\n\t\t\tsource = {\n\t\t\t\tidentity: deployIdentity(\n\t\t\t\t\t{\n\t\t\t\t\t\tname: options.workspaceName,\n\t\t\t\t\t\t...(options.namespace\n\t\t\t\t\t\t\t? { deploy: { namespace: options.namespace } }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t},\n\t\t\t\t\t'',\n\t\t\t\t),\n\t\t\t\tregion,\n\t\t\t\tsts: new STSClient({\n\t\t\t\t\tregion,\n\t\t\t\t\t...(options.endpoint ? { endpoint: options.endpoint } : {}),\n\t\t\t\t\t...(config.credentials ? { credentials: config.credentials } : {}),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\treturn new S3StateStore(\n\t\t\toptions.workspaceName,\n\t\t\tsource,\n\t\t\tnew S3Client(config),\n\t\t\toptions.prefix,\n\t\t);\n\t}\n\n\tprivate key(stage: string, leaf: string): string {\n\t\tconst path = `${this.workspaceName}/${stage}/${leaf}`;\n\t\treturn this.prefix ? `${this.prefix}/${path}` : path;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage, 'state.json')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'state.json'));\n\t\tif (!object) return null;\n\t\treturn { body: object.body, version: object.etag };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'state.json');\n\t\ttry {\n\t\t\tconst { ETag } = await this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t...(expectedVersion === null\n\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t: { IfMatch: expectedVersion }),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ETag) return ETag;\n\t\t\t// Some S3-compatible servers omit the ETag on a conditional put.\n\t\t\treturn (await this.get(Key))?.etag ?? '';\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tconst current = await this.get(Key);\n\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\tstage,\n\t\t\t\t\texpectedVersion,\n\t\t\t\t\tcurrent?.etag ?? null,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\tawait this.use(true);\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey: this.key(stage, 'state.v1.json'),\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!lostRace(error)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: JSON.stringify(holder),\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthrow new StateLocked(\n\t\t\t\t\tstage,\n\t\t\t\t\tawait this.readLock(stage),\n\t\t\t\t\t`s3://${this.bucket}/${Key}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'lock.json'));\n\t\tif (!object) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(object.body) as LockHolder;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (!(await this.use(false))) return;\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\tconst object = await this.get(Key);\n\t\tif (!object) return;\n\t\tif (holder) {\n\t\t\ttry {\n\t\t\t\tif ((JSON.parse(object.body) as LockHolder).id !== holder.id) return;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\t// Conditional on the lock read above, so a lock taken by another\n\t\t\t// run in the meantime survives.\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tIfMatch: object.etag,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!lostRace(error) && !notFound(error)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tKey: string,\n\t): Promise<{ body: string; etag: string } | null> {\n\t\ttry {\n\t\t\tconst response = await this.client.send(\n\t\t\t\tnew GetObjectCommand({ Bucket: this.bucket, Key }),\n\t\t\t);\n\t\t\tconst body = (await response.Body?.transformToString('utf-8')) ?? '';\n\t\t\treturn { body, etag: response.ETag ?? '' };\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"S3StateStore-CGMSDilh.mjs","names":[],"sources":["../src/deploy/S3StateStore.ts"],"sourcesContent":["/**\n * Amazon S3 state store.\n *\n * In the bucket the config names, which must exist — or, with none named, in\n * the project bucket (`providers/projectBucket.ts`): found by its name, which\n * is worked out from the project and the account, and created by the first\n * write (a deploy's lock, `state:push`). A read of a project bucket that does\n * not exist yet is a stage with no state; it creates nothing.\n *\n * - `<prefix>/<workspace>/<stage>/state.json`: the state.\n * - `<prefix>/<workspace>/<stage>/lock.json`: the lock.\n * - `<prefix>/<workspace>/<stage>/state.v1.json`: the v1 state, kept on\n * migration.\n *\n * Every write is conditional on the server: `If-None-Match: *` to create,\n * `If-Match: <etag>` to replace. S3 answers a lost race with 412 (or 409 when\n * two conditional writes are in flight at once), so there is no window\n * between a check and a write for another run to slip into.\n */\n\nimport {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket';\nimport { type DeployIdentity, deployIdentity } from './identity';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface S3StateStoreOptions {\n\tworkspaceName: string;\n\t/** Omitted, the project bucket — created by the first write. */\n\tbucket?: string;\n\t/** `deploy.namespace`, for the project bucket's name. */\n\tnamespace?: string;\n\t/** Key prefix inside the bucket; `gkm` by default. */\n\tprefix?: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: S3ClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\n/** The project bucket, for a store that names no bucket of its own. */\nexport interface ProjectBucketSource {\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\tregion: string;\n\t/** What the account id — and so the bucket's name — is read with. */\n\tsts: STSClient;\n}\n\nexport class S3StateStore extends DocumentStateStore {\n\tprivate readonly prefix: string;\n\t/** The bucket's name, once known. */\n\tprivate name?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly source: string | ProjectBucketSource,\n\t\tprivate readonly client: S3Client,\n\t\tprefix = 'gkm',\n\t) {\n\t\tsuper();\n\t\tthis.prefix = prefix.replace(/\\/+$/, '');\n\t\tif (typeof source === 'string') {\n\t\t\tthis.name = source;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; a project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\tif (this.name) return this.name;\n\t\tconst { identity } = this.source as ProjectBucketSource;\n\t\treturn projectBucketName(identity.scope, '<account id>');\n\t}\n\n\t/**\n\t * The bucket, or `null` for a project bucket that does not exist yet and\n\t * is not to be created (`create: false`, a read).\n\t */\n\tprivate async use(create: boolean): Promise<string | null> {\n\t\tif (this.exists) return this.name!;\n\t\tconst source = this.source as ProjectBucketSource;\n\t\tthis.name ??= projectBucketName(\n\t\t\tsource.identity.scope,\n\t\t\tawait callerAccountId(source.sts),\n\t\t);\n\t\tif (create) {\n\t\t\tawait ensureProjectBucket(this.client, {\n\t\t\t\tbucket: this.name,\n\t\t\t\tregion: source.region,\n\t\t\t\tidentity: source.identity,\n\t\t\t});\n\t\t} else if ((await findProjectBucket(this.client, this.name)) === 'none') {\n\t\t\treturn null;\n\t\t}\n\t\tthis.exists = true;\n\t\treturn this.name;\n\t}\n\n\tstatic create(options: S3StateStoreOptions): S3StateStore {\n\t\tconst endpoint =\n\t\t\toptions.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\t// The region the project bucket is created in is the one it is reached in.\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\t\tconst config: S3ClientConfig = {\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\t// The emulator serves buckets by path, not by subdomain.\n\t\t\tforcePathStyle: endpoint ? true : undefined,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\tlet source: string | ProjectBucketSource;\n\t\tif (options.bucket) {\n\t\t\tsource = options.bucket;\n\t\t} else {\n\t\t\tsource = {\n\t\t\t\tidentity: deployIdentity(\n\t\t\t\t\t{\n\t\t\t\t\t\tname: options.workspaceName,\n\t\t\t\t\t\t...(options.namespace\n\t\t\t\t\t\t\t? { deploy: { namespace: options.namespace } }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t},\n\t\t\t\t\t'',\n\t\t\t\t),\n\t\t\t\tregion,\n\t\t\t\tsts: new STSClient({\n\t\t\t\t\tregion,\n\t\t\t\t\t...(options.endpoint ? { endpoint: options.endpoint } : {}),\n\t\t\t\t\t...(config.credentials ? { credentials: config.credentials } : {}),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\treturn new S3StateStore(\n\t\t\toptions.workspaceName,\n\t\t\tsource,\n\t\t\tnew S3Client(config),\n\t\t\toptions.prefix,\n\t\t);\n\t}\n\n\tprivate key(stage: string, leaf: string): string {\n\t\tconst path = `${this.workspaceName}/${stage}/${leaf}`;\n\t\treturn this.prefix ? `${this.prefix}/${path}` : path;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage, 'state.json')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'state.json'));\n\t\tif (!object) return null;\n\t\treturn { body: object.body, version: object.etag };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'state.json');\n\t\ttry {\n\t\t\tconst { ETag } = await this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t...(expectedVersion === null\n\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t: { IfMatch: expectedVersion }),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ETag) return ETag;\n\t\t\t// Some S3-compatible servers omit the ETag on a conditional put.\n\t\t\treturn (await this.get(Key))?.etag ?? '';\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tconst current = await this.get(Key);\n\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\tstage,\n\t\t\t\t\texpectedVersion,\n\t\t\t\t\tcurrent?.etag ?? null,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\tawait this.use(true);\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey: this.key(stage, 'state.v1.json'),\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!lostRace(error)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: JSON.stringify(holder),\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthrow new StateLocked(\n\t\t\t\t\tstage,\n\t\t\t\t\tawait this.readLock(stage),\n\t\t\t\t\t`s3://${this.bucket}/${Key}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'lock.json'));\n\t\tif (!object) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(object.body) as LockHolder;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (!(await this.use(false))) return;\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\tconst object = await this.get(Key);\n\t\tif (!object) return;\n\t\tif (holder) {\n\t\t\ttry {\n\t\t\t\tif ((JSON.parse(object.body) as LockHolder).id !== holder.id) return;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\t// Conditional on the lock read above, so a lock taken by another\n\t\t\t// run in the meantime survives.\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tIfMatch: object.etag,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!lostRace(error) && !notFound(error)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tKey: string,\n\t): Promise<{ body: string; etag: string } | null> {\n\t\ttry {\n\t\t\tconst response = await this.client.send(\n\t\t\t\tnew GetObjectCommand({ Bucket: this.bucket, Key }),\n\t\t\t);\n\t\t\tconst body = (await response.Body?.transformToString('utf-8')) ?? '';\n\t\t\treturn { body, etag: response.ETag ?? '' };\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  const require_identity = require("./identity-CbLfgQNT.cjs");
2
- const require_sniffer = require("./sniffer-DdX_WN_-.cjs");
2
+ const require_sniffer = require("./sniffer-Cq1nhxIB.cjs");
3
3
  const require_projectBucket = require("./projectBucket-BBbQ1lHF.cjs");
4
4
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
5
5
  let _aws_sdk_client_sts = require("@aws-sdk/client-sts");
@@ -219,4 +219,4 @@ var S3StateStore = class S3StateStore extends require_sniffer.DocumentStateStore
219
219
  //#endregion
220
220
  exports.S3StateStore = S3StateStore;
221
221
 
222
- //# sourceMappingURL=S3StateStore-BqugXIcb.cjs.map
222
+ //# sourceMappingURL=S3StateStore-DiekMLpn.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"S3StateStore-BqugXIcb.cjs","names":["DocumentStateStore","projectBucketName","callerAccountId","ensureProjectBucket","findProjectBucket","deployIdentity","STSClient","S3Client","PutObjectCommand","StateVersionConflict","StateLocked","DeleteObjectCommand","GetObjectCommand"],"sources":["../src/deploy/S3StateStore.ts"],"sourcesContent":["/**\n * Amazon S3 state store.\n *\n * In the bucket the config names, which must exist — or, with none named, in\n * the project bucket (`providers/projectBucket.ts`): found by its name, which\n * is worked out from the project and the account, and created by the first\n * write (a deploy's lock, `state:push`). A read of a project bucket that does\n * not exist yet is a stage with no state; it creates nothing.\n *\n * - `<prefix>/<workspace>/<stage>/state.json`: the state.\n * - `<prefix>/<workspace>/<stage>/lock.json`: the lock.\n * - `<prefix>/<workspace>/<stage>/state.v1.json`: the v1 state, kept on\n * migration.\n *\n * Every write is conditional on the server: `If-None-Match: *` to create,\n * `If-Match: <etag>` to replace. S3 answers a lost race with 412 (or 409 when\n * two conditional writes are in flight at once), so there is no window\n * between a check and a write for another run to slip into.\n */\n\nimport {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket';\nimport { type DeployIdentity, deployIdentity } from './identity';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface S3StateStoreOptions {\n\tworkspaceName: string;\n\t/** Omitted, the project bucket — created by the first write. */\n\tbucket?: string;\n\t/** `deploy.namespace`, for the project bucket's name. */\n\tnamespace?: string;\n\t/** Key prefix inside the bucket; `gkm` by default. */\n\tprefix?: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: S3ClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\n/** The project bucket, for a store that names no bucket of its own. */\nexport interface ProjectBucketSource {\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\tregion: string;\n\t/** What the account id — and so the bucket's name — is read with. */\n\tsts: STSClient;\n}\n\nexport class S3StateStore extends DocumentStateStore {\n\tprivate readonly prefix: string;\n\t/** The bucket's name, once known. */\n\tprivate name?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly source: string | ProjectBucketSource,\n\t\tprivate readonly client: S3Client,\n\t\tprefix = 'gkm',\n\t) {\n\t\tsuper();\n\t\tthis.prefix = prefix.replace(/\\/+$/, '');\n\t\tif (typeof source === 'string') {\n\t\t\tthis.name = source;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; a project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\tif (this.name) return this.name;\n\t\tconst { identity } = this.source as ProjectBucketSource;\n\t\treturn projectBucketName(identity.scope, '<account id>');\n\t}\n\n\t/**\n\t * The bucket, or `null` for a project bucket that does not exist yet and\n\t * is not to be created (`create: false`, a read).\n\t */\n\tprivate async use(create: boolean): Promise<string | null> {\n\t\tif (this.exists) return this.name!;\n\t\tconst source = this.source as ProjectBucketSource;\n\t\tthis.name ??= projectBucketName(\n\t\t\tsource.identity.scope,\n\t\t\tawait callerAccountId(source.sts),\n\t\t);\n\t\tif (create) {\n\t\t\tawait ensureProjectBucket(this.client, {\n\t\t\t\tbucket: this.name,\n\t\t\t\tregion: source.region,\n\t\t\t\tidentity: source.identity,\n\t\t\t});\n\t\t} else if ((await findProjectBucket(this.client, this.name)) === 'none') {\n\t\t\treturn null;\n\t\t}\n\t\tthis.exists = true;\n\t\treturn this.name;\n\t}\n\n\tstatic create(options: S3StateStoreOptions): S3StateStore {\n\t\tconst endpoint =\n\t\t\toptions.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\t// The region the project bucket is created in is the one it is reached in.\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\t\tconst config: S3ClientConfig = {\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\t// The emulator serves buckets by path, not by subdomain.\n\t\t\tforcePathStyle: endpoint ? true : undefined,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\tlet source: string | ProjectBucketSource;\n\t\tif (options.bucket) {\n\t\t\tsource = options.bucket;\n\t\t} else {\n\t\t\tsource = {\n\t\t\t\tidentity: deployIdentity(\n\t\t\t\t\t{\n\t\t\t\t\t\tname: options.workspaceName,\n\t\t\t\t\t\t...(options.namespace\n\t\t\t\t\t\t\t? { deploy: { namespace: options.namespace } }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t},\n\t\t\t\t\t'',\n\t\t\t\t),\n\t\t\t\tregion,\n\t\t\t\tsts: new STSClient({\n\t\t\t\t\tregion,\n\t\t\t\t\t...(options.endpoint ? { endpoint: options.endpoint } : {}),\n\t\t\t\t\t...(config.credentials ? { credentials: config.credentials } : {}),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\treturn new S3StateStore(\n\t\t\toptions.workspaceName,\n\t\t\tsource,\n\t\t\tnew S3Client(config),\n\t\t\toptions.prefix,\n\t\t);\n\t}\n\n\tprivate key(stage: string, leaf: string): string {\n\t\tconst path = `${this.workspaceName}/${stage}/${leaf}`;\n\t\treturn this.prefix ? `${this.prefix}/${path}` : path;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage, 'state.json')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'state.json'));\n\t\tif (!object) return null;\n\t\treturn { body: object.body, version: object.etag };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'state.json');\n\t\ttry {\n\t\t\tconst { ETag } = await this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t...(expectedVersion === null\n\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t: { IfMatch: expectedVersion }),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ETag) return ETag;\n\t\t\t// Some S3-compatible servers omit the ETag on a conditional put.\n\t\t\treturn (await this.get(Key))?.etag ?? '';\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tconst current = await this.get(Key);\n\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\tstage,\n\t\t\t\t\texpectedVersion,\n\t\t\t\t\tcurrent?.etag ?? null,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\tawait this.use(true);\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey: this.key(stage, 'state.v1.json'),\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!lostRace(error)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: JSON.stringify(holder),\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthrow new StateLocked(\n\t\t\t\t\tstage,\n\t\t\t\t\tawait this.readLock(stage),\n\t\t\t\t\t`s3://${this.bucket}/${Key}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'lock.json'));\n\t\tif (!object) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(object.body) as LockHolder;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (!(await this.use(false))) return;\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\tconst object = await this.get(Key);\n\t\tif (!object) return;\n\t\tif (holder) {\n\t\t\ttry {\n\t\t\t\tif ((JSON.parse(object.body) as LockHolder).id !== holder.id) return;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\t// Conditional on the lock read above, so a lock taken by another\n\t\t\t// run in the meantime survives.\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tIfMatch: object.etag,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!lostRace(error) && !notFound(error)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tKey: string,\n\t): Promise<{ body: string; etag: string } | null> {\n\t\ttry {\n\t\t\tconst response = await this.client.send(\n\t\t\t\tnew GetObjectCommand({ Bucket: this.bucket, Key }),\n\t\t\t);\n\t\t\tconst body = (await response.Body?.transformToString('utf-8')) ?? '';\n\t\t\treturn { body, etag: response.ETag ?? '' };\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"S3StateStore-DiekMLpn.cjs","names":["DocumentStateStore","projectBucketName","callerAccountId","ensureProjectBucket","findProjectBucket","deployIdentity","STSClient","S3Client","PutObjectCommand","StateVersionConflict","StateLocked","DeleteObjectCommand","GetObjectCommand"],"sources":["../src/deploy/S3StateStore.ts"],"sourcesContent":["/**\n * Amazon S3 state store.\n *\n * In the bucket the config names, which must exist — or, with none named, in\n * the project bucket (`providers/projectBucket.ts`): found by its name, which\n * is worked out from the project and the account, and created by the first\n * write (a deploy's lock, `state:push`). A read of a project bucket that does\n * not exist yet is a stage with no state; it creates nothing.\n *\n * - `<prefix>/<workspace>/<stage>/state.json`: the state.\n * - `<prefix>/<workspace>/<stage>/lock.json`: the lock.\n * - `<prefix>/<workspace>/<stage>/state.v1.json`: the v1 state, kept on\n * migration.\n *\n * Every write is conditional on the server: `If-None-Match: *` to create,\n * `If-Match: <etag>` to replace. S3 answers a lost race with 412 (or 409 when\n * two conditional writes are in flight at once), so there is no window\n * between a check and a write for another run to slip into.\n */\n\nimport {\n\tDeleteObjectCommand,\n\tGetObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket';\nimport { type DeployIdentity, deployIdentity } from './identity';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface S3StateStoreOptions {\n\tworkspaceName: string;\n\t/** Omitted, the project bucket — created by the first write. */\n\tbucket?: string;\n\t/** `deploy.namespace`, for the project bucket's name. */\n\tnamespace?: string;\n\t/** Key prefix inside the bucket; `gkm` by default. */\n\tprefix?: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: S3ClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\n/** The project bucket, for a store that names no bucket of its own. */\nexport interface ProjectBucketSource {\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\tregion: string;\n\t/** What the account id — and so the bucket's name — is read with. */\n\tsts: STSClient;\n}\n\nexport class S3StateStore extends DocumentStateStore {\n\tprivate readonly prefix: string;\n\t/** The bucket's name, once known. */\n\tprivate name?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly source: string | ProjectBucketSource,\n\t\tprivate readonly client: S3Client,\n\t\tprefix = 'gkm',\n\t) {\n\t\tsuper();\n\t\tthis.prefix = prefix.replace(/\\/+$/, '');\n\t\tif (typeof source === 'string') {\n\t\t\tthis.name = source;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; a project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\tif (this.name) return this.name;\n\t\tconst { identity } = this.source as ProjectBucketSource;\n\t\treturn projectBucketName(identity.scope, '<account id>');\n\t}\n\n\t/**\n\t * The bucket, or `null` for a project bucket that does not exist yet and\n\t * is not to be created (`create: false`, a read).\n\t */\n\tprivate async use(create: boolean): Promise<string | null> {\n\t\tif (this.exists) return this.name!;\n\t\tconst source = this.source as ProjectBucketSource;\n\t\tthis.name ??= projectBucketName(\n\t\t\tsource.identity.scope,\n\t\t\tawait callerAccountId(source.sts),\n\t\t);\n\t\tif (create) {\n\t\t\tawait ensureProjectBucket(this.client, {\n\t\t\t\tbucket: this.name,\n\t\t\t\tregion: source.region,\n\t\t\t\tidentity: source.identity,\n\t\t\t});\n\t\t} else if ((await findProjectBucket(this.client, this.name)) === 'none') {\n\t\t\treturn null;\n\t\t}\n\t\tthis.exists = true;\n\t\treturn this.name;\n\t}\n\n\tstatic create(options: S3StateStoreOptions): S3StateStore {\n\t\tconst endpoint =\n\t\t\toptions.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\t// The region the project bucket is created in is the one it is reached in.\n\t\tconst region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';\n\t\tconst config: S3ClientConfig = {\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t\t// The emulator serves buckets by path, not by subdomain.\n\t\t\tforcePathStyle: endpoint ? true : undefined,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\tlet source: string | ProjectBucketSource;\n\t\tif (options.bucket) {\n\t\t\tsource = options.bucket;\n\t\t} else {\n\t\t\tsource = {\n\t\t\t\tidentity: deployIdentity(\n\t\t\t\t\t{\n\t\t\t\t\t\tname: options.workspaceName,\n\t\t\t\t\t\t...(options.namespace\n\t\t\t\t\t\t\t? { deploy: { namespace: options.namespace } }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t},\n\t\t\t\t\t'',\n\t\t\t\t),\n\t\t\t\tregion,\n\t\t\t\tsts: new STSClient({\n\t\t\t\t\tregion,\n\t\t\t\t\t...(options.endpoint ? { endpoint: options.endpoint } : {}),\n\t\t\t\t\t...(config.credentials ? { credentials: config.credentials } : {}),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t\treturn new S3StateStore(\n\t\t\toptions.workspaceName,\n\t\t\tsource,\n\t\t\tnew S3Client(config),\n\t\t\toptions.prefix,\n\t\t);\n\t}\n\n\tprivate key(stage: string, leaf: string): string {\n\t\tconst path = `${this.workspaceName}/${stage}/${leaf}`;\n\t\treturn this.prefix ? `${this.prefix}/${path}` : path;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage, 'state.json')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'state.json'));\n\t\tif (!object) return null;\n\t\treturn { body: object.body, version: object.etag };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'state.json');\n\t\ttry {\n\t\t\tconst { ETag } = await this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t...(expectedVersion === null\n\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t: { IfMatch: expectedVersion }),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tif (ETag) return ETag;\n\t\t\t// Some S3-compatible servers omit the ETag on a conditional put.\n\t\t\treturn (await this.get(Key))?.etag ?? '';\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tconst current = await this.get(Key);\n\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\tstage,\n\t\t\t\t\texpectedVersion,\n\t\t\t\t\tcurrent?.etag ?? null,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\tawait this.use(true);\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey: this.key(stage, 'state.v1.json'),\n\t\t\t\t\tBody: body,\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!lostRace(error)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tawait this.use(true);\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tBody: JSON.stringify(holder),\n\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\tIfNoneMatch: '*',\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthrow new StateLocked(\n\t\t\t\t\tstage,\n\t\t\t\t\tawait this.readLock(stage),\n\t\t\t\t\t`s3://${this.bucket}/${Key}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tif (!(await this.use(false))) return null;\n\t\tconst object = await this.get(this.key(stage, 'lock.json'));\n\t\tif (!object) return null;\n\t\ttry {\n\t\t\treturn JSON.parse(object.body) as LockHolder;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (!(await this.use(false))) return;\n\t\tconst Key = this.key(stage, 'lock.json');\n\t\tconst object = await this.get(Key);\n\t\tif (!object) return;\n\t\tif (holder) {\n\t\t\ttry {\n\t\t\t\tif ((JSON.parse(object.body) as LockHolder).id !== holder.id) return;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\t// Conditional on the lock read above, so a lock taken by another\n\t\t\t// run in the meantime survives.\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteObjectCommand({\n\t\t\t\t\tBucket: this.bucket,\n\t\t\t\t\tKey,\n\t\t\t\t\tIfMatch: object.etag,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!lostRace(error) && !notFound(error)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tKey: string,\n\t): Promise<{ body: string; etag: string } | null> {\n\t\ttry {\n\t\t\tconst response = await this.client.send(\n\t\t\t\tnew GetObjectCommand({ Bucket: this.bucket, Key }),\n\t\t\t);\n\t\t\tconst body = (await response.Body?.transformToString('utf-8')) ?? '';\n\t\t\treturn { body, etag: response.ETag ?? '' };\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const require_sniffer = require("./sniffer-DdX_WN_-.cjs");
1
+ const require_sniffer = require("./sniffer-Cq1nhxIB.cjs");
2
2
  let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
3
3
  //#region src/deploy/SSMStateStore.ts
4
4
  /**
@@ -201,4 +201,4 @@ function parseHolder(value) {
201
201
  //#endregion
202
202
  exports.SSMStateStore = SSMStateStore;
203
203
 
204
- //# sourceMappingURL=SSMStateStore-B0OsuT_k.cjs.map
204
+ //# sourceMappingURL=SSMStateStore-BDCOFdKq.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSMStateStore-B0OsuT_k.cjs","names":["compressBody","DocumentStateStore","SSMClient","decompressBody","StateRejectedBySsm","PutParameterCommand","ParameterAlreadyExists","StateVersionConflict","StateLocked","DeleteParameterCommand","ParameterNotFound","GetParameterCommand"],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n *\n * Size: the state is written with the Intelligent-Tiering tier, so SSM picks\n * the advanced tier (8 KB) once a value passes the standard tier's 4 KB, and a\n * document over {@link SSM_COMPRESS_OVER} bytes is stored gzipped, as `gz:`\n * and base64. Both forms are read, so a document written before either is\n * read as it was. A deploy checks before it starts that the stage's document\n * leaves room under {@link SSM_MAX_BYTES} (`assertStateRoom`); a write SSM\n * refuses anyway raises `StateRejectedBySsm` rather than the SDK's error.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tcompressBody,\n\tDocumentStateStore,\n\tdecompressBody,\n\ttype LockHolder,\n\ttype RawState,\n\ttype StateCapacity,\n\tStateLocked,\n\tStateRejectedBySsm,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\n/** The most an advanced-tier parameter holds — what the state must fit. */\nexport const SSM_MAX_BYTES = 8192;\n/** A body past this is stored compressed: well under the standard 4 KB. */\nexport const SSM_COMPRESS_OVER = 3072;\n\n/**\n * `body` as the state parameter stores it: past the threshold, without its\n * indentation and compressed.\n */\nexport function packSsmBody(body: string): string {\n\tif (Buffer.byteLength(body) <= SSM_COMPRESS_OVER) return body;\n\tlet compact = body;\n\ttry {\n\t\tcompact = JSON.stringify(JSON.parse(body));\n\t} catch {\n\t\t// Not JSON: stored as it was given, compressed.\n\t}\n\treturn compressBody(compact);\n}\n\n/** A refusal of the value itself — its size, past the tier's limit. */\nfunction refusedValue(error: unknown): boolean {\n\treturn (error as { name?: string })?.name === 'ValidationException';\n}\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t\t/** For messages; the client's own region is used for every call. */\n\t\treadonly region = '<region>',\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(\n\t\t\toptions.workspaceName,\n\t\t\tnew SSMClient(config),\n\t\t\toptions.region,\n\t\t);\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn {\n\t\t\tbody: decompressBody(parameter.value),\n\t\t\tversion: String(parameter.version),\n\t\t};\n\t}\n\n\t/** The stage's document as the next write would store it. */\n\tasync capacity(stage: string): Promise<StateCapacity | null> {\n\t\tconst raw = await this.readRaw(stage);\n\t\tif (!raw) return null;\n\t\treturn {\n\t\t\tbytes: Buffer.byteLength(packSsmBody(raw.body)),\n\t\t\tlimit: SSM_MAX_BYTES,\n\t\t\tlocation: this.location(stage),\n\t\t\tregion: this.region,\n\t\t};\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tdocument: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\ttry {\n\t\t\treturn await this.put(stage, packSsmBody(document), expectedVersion);\n\t\t} catch (error) {\n\t\t\tif (!refusedValue(error)) throw error;\n\t\t\tthrow new StateRejectedBySsm(\n\t\t\t\tstage,\n\t\t\t\tBuffer.byteLength(packSsmBody(document)),\n\t\t\t\tthis.location(stage),\n\t\t\t\tthis.region,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** The conditional put of `body`, stored as it is given. */\n\tprivate async put(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\t// Kept as it was — the v1 state a person restores by hand —\n\t\t\t\t\t// unless it is too big to store that way.\n\t\t\t\t\tValue: packSsmBody(body),\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\t// A few hundred bytes: the standard tier, which costs nothing.\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SSMStateStore-BDCOFdKq.cjs","names":["compressBody","DocumentStateStore","SSMClient","decompressBody","StateRejectedBySsm","PutParameterCommand","ParameterAlreadyExists","StateVersionConflict","StateLocked","DeleteParameterCommand","ParameterNotFound","GetParameterCommand"],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n *\n * Size: the state is written with the Intelligent-Tiering tier, so SSM picks\n * the advanced tier (8 KB) once a value passes the standard tier's 4 KB, and a\n * document over {@link SSM_COMPRESS_OVER} bytes is stored gzipped, as `gz:`\n * and base64. Both forms are read, so a document written before either is\n * read as it was. A deploy checks before it starts that the stage's document\n * leaves room under {@link SSM_MAX_BYTES} (`assertStateRoom`); a write SSM\n * refuses anyway raises `StateRejectedBySsm` rather than the SDK's error.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tcompressBody,\n\tDocumentStateStore,\n\tdecompressBody,\n\ttype LockHolder,\n\ttype RawState,\n\ttype StateCapacity,\n\tStateLocked,\n\tStateRejectedBySsm,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\n/** The most an advanced-tier parameter holds — what the state must fit. */\nexport const SSM_MAX_BYTES = 8192;\n/** A body past this is stored compressed: well under the standard 4 KB. */\nexport const SSM_COMPRESS_OVER = 3072;\n\n/**\n * `body` as the state parameter stores it: past the threshold, without its\n * indentation and compressed.\n */\nexport function packSsmBody(body: string): string {\n\tif (Buffer.byteLength(body) <= SSM_COMPRESS_OVER) return body;\n\tlet compact = body;\n\ttry {\n\t\tcompact = JSON.stringify(JSON.parse(body));\n\t} catch {\n\t\t// Not JSON: stored as it was given, compressed.\n\t}\n\treturn compressBody(compact);\n}\n\n/** A refusal of the value itself — its size, past the tier's limit. */\nfunction refusedValue(error: unknown): boolean {\n\treturn (error as { name?: string })?.name === 'ValidationException';\n}\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t\t/** For messages; the client's own region is used for every call. */\n\t\treadonly region = '<region>',\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(\n\t\t\toptions.workspaceName,\n\t\t\tnew SSMClient(config),\n\t\t\toptions.region,\n\t\t);\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn {\n\t\t\tbody: decompressBody(parameter.value),\n\t\t\tversion: String(parameter.version),\n\t\t};\n\t}\n\n\t/** The stage's document as the next write would store it. */\n\tasync capacity(stage: string): Promise<StateCapacity | null> {\n\t\tconst raw = await this.readRaw(stage);\n\t\tif (!raw) return null;\n\t\treturn {\n\t\t\tbytes: Buffer.byteLength(packSsmBody(raw.body)),\n\t\t\tlimit: SSM_MAX_BYTES,\n\t\t\tlocation: this.location(stage),\n\t\t\tregion: this.region,\n\t\t};\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tdocument: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\ttry {\n\t\t\treturn await this.put(stage, packSsmBody(document), expectedVersion);\n\t\t} catch (error) {\n\t\t\tif (!refusedValue(error)) throw error;\n\t\t\tthrow new StateRejectedBySsm(\n\t\t\t\tstage,\n\t\t\t\tBuffer.byteLength(packSsmBody(document)),\n\t\t\t\tthis.location(stage),\n\t\t\t\tthis.region,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** The conditional put of `body`, stored as it is given. */\n\tprivate async put(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\t// Kept as it was — the v1 state a person restores by hand —\n\t\t\t\t\t// unless it is too big to store that way.\n\t\t\t\t\tValue: packSsmBody(body),\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\t// A few hundred bytes: the standard tier, which costs nothing.\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { n as __require } from "./rolldown-runtime-BhDjJH2R.mjs";
2
- import { C as decompressBody, b as compressBody, f as StateLocked, g as StateVersionConflict, l as DocumentStateStore, p as StateRejectedBySsm } from "./sniffer-g9v7cqDN.mjs";
2
+ import { C as decompressBody, b as compressBody, f as StateLocked, g as StateVersionConflict, l as DocumentStateStore, p as StateRejectedBySsm } from "./sniffer-CCkdzjiU.mjs";
3
3
  import { DeleteParameterCommand, GetParameterCommand, ParameterAlreadyExists, ParameterNotFound, PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
4
4
  //#region src/deploy/SSMStateStore.ts
5
5
  /**
@@ -202,4 +202,4 @@ function parseHolder(value) {
202
202
  //#endregion
203
203
  export { SSMStateStore };
204
204
 
205
- //# sourceMappingURL=SSMStateStore-DM85OiH0.mjs.map
205
+ //# sourceMappingURL=SSMStateStore-Cq2H9MlF.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"SSMStateStore-DM85OiH0.mjs","names":[],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n *\n * Size: the state is written with the Intelligent-Tiering tier, so SSM picks\n * the advanced tier (8 KB) once a value passes the standard tier's 4 KB, and a\n * document over {@link SSM_COMPRESS_OVER} bytes is stored gzipped, as `gz:`\n * and base64. Both forms are read, so a document written before either is\n * read as it was. A deploy checks before it starts that the stage's document\n * leaves room under {@link SSM_MAX_BYTES} (`assertStateRoom`); a write SSM\n * refuses anyway raises `StateRejectedBySsm` rather than the SDK's error.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tcompressBody,\n\tDocumentStateStore,\n\tdecompressBody,\n\ttype LockHolder,\n\ttype RawState,\n\ttype StateCapacity,\n\tStateLocked,\n\tStateRejectedBySsm,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\n/** The most an advanced-tier parameter holds — what the state must fit. */\nexport const SSM_MAX_BYTES = 8192;\n/** A body past this is stored compressed: well under the standard 4 KB. */\nexport const SSM_COMPRESS_OVER = 3072;\n\n/**\n * `body` as the state parameter stores it: past the threshold, without its\n * indentation and compressed.\n */\nexport function packSsmBody(body: string): string {\n\tif (Buffer.byteLength(body) <= SSM_COMPRESS_OVER) return body;\n\tlet compact = body;\n\ttry {\n\t\tcompact = JSON.stringify(JSON.parse(body));\n\t} catch {\n\t\t// Not JSON: stored as it was given, compressed.\n\t}\n\treturn compressBody(compact);\n}\n\n/** A refusal of the value itself — its size, past the tier's limit. */\nfunction refusedValue(error: unknown): boolean {\n\treturn (error as { name?: string })?.name === 'ValidationException';\n}\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t\t/** For messages; the client's own region is used for every call. */\n\t\treadonly region = '<region>',\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(\n\t\t\toptions.workspaceName,\n\t\t\tnew SSMClient(config),\n\t\t\toptions.region,\n\t\t);\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn {\n\t\t\tbody: decompressBody(parameter.value),\n\t\t\tversion: String(parameter.version),\n\t\t};\n\t}\n\n\t/** The stage's document as the next write would store it. */\n\tasync capacity(stage: string): Promise<StateCapacity | null> {\n\t\tconst raw = await this.readRaw(stage);\n\t\tif (!raw) return null;\n\t\treturn {\n\t\t\tbytes: Buffer.byteLength(packSsmBody(raw.body)),\n\t\t\tlimit: SSM_MAX_BYTES,\n\t\t\tlocation: this.location(stage),\n\t\t\tregion: this.region,\n\t\t};\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tdocument: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\ttry {\n\t\t\treturn await this.put(stage, packSsmBody(document), expectedVersion);\n\t\t} catch (error) {\n\t\t\tif (!refusedValue(error)) throw error;\n\t\t\tthrow new StateRejectedBySsm(\n\t\t\t\tstage,\n\t\t\t\tBuffer.byteLength(packSsmBody(document)),\n\t\t\t\tthis.location(stage),\n\t\t\t\tthis.region,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** The conditional put of `body`, stored as it is given. */\n\tprivate async put(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\t// Kept as it was — the v1 state a person restores by hand —\n\t\t\t\t\t// unless it is too big to store that way.\n\t\t\t\t\tValue: packSsmBody(body),\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\t// A few hundred bytes: the standard tier, which costs nothing.\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SSMStateStore-Cq2H9MlF.mjs","names":[],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n *\n * Size: the state is written with the Intelligent-Tiering tier, so SSM picks\n * the advanced tier (8 KB) once a value passes the standard tier's 4 KB, and a\n * document over {@link SSM_COMPRESS_OVER} bytes is stored gzipped, as `gz:`\n * and base64. Both forms are read, so a document written before either is\n * read as it was. A deploy checks before it starts that the stage's document\n * leaves room under {@link SSM_MAX_BYTES} (`assertStateRoom`); a write SSM\n * refuses anyway raises `StateRejectedBySsm` rather than the SDK's error.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tcompressBody,\n\tDocumentStateStore,\n\tdecompressBody,\n\ttype LockHolder,\n\ttype RawState,\n\ttype StateCapacity,\n\tStateLocked,\n\tStateRejectedBySsm,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\n/** The most an advanced-tier parameter holds — what the state must fit. */\nexport const SSM_MAX_BYTES = 8192;\n/** A body past this is stored compressed: well under the standard 4 KB. */\nexport const SSM_COMPRESS_OVER = 3072;\n\n/**\n * `body` as the state parameter stores it: past the threshold, without its\n * indentation and compressed.\n */\nexport function packSsmBody(body: string): string {\n\tif (Buffer.byteLength(body) <= SSM_COMPRESS_OVER) return body;\n\tlet compact = body;\n\ttry {\n\t\tcompact = JSON.stringify(JSON.parse(body));\n\t} catch {\n\t\t// Not JSON: stored as it was given, compressed.\n\t}\n\treturn compressBody(compact);\n}\n\n/** A refusal of the value itself — its size, past the tier's limit. */\nfunction refusedValue(error: unknown): boolean {\n\treturn (error as { name?: string })?.name === 'ValidationException';\n}\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t\t/** For messages; the client's own region is used for every call. */\n\t\treadonly region = '<region>',\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(\n\t\t\toptions.workspaceName,\n\t\t\tnew SSMClient(config),\n\t\t\toptions.region,\n\t\t);\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn {\n\t\t\tbody: decompressBody(parameter.value),\n\t\t\tversion: String(parameter.version),\n\t\t};\n\t}\n\n\t/** The stage's document as the next write would store it. */\n\tasync capacity(stage: string): Promise<StateCapacity | null> {\n\t\tconst raw = await this.readRaw(stage);\n\t\tif (!raw) return null;\n\t\treturn {\n\t\t\tbytes: Buffer.byteLength(packSsmBody(raw.body)),\n\t\t\tlimit: SSM_MAX_BYTES,\n\t\t\tlocation: this.location(stage),\n\t\t\tregion: this.region,\n\t\t};\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tdocument: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\ttry {\n\t\t\treturn await this.put(stage, packSsmBody(document), expectedVersion);\n\t\t} catch (error) {\n\t\t\tif (!refusedValue(error)) throw error;\n\t\t\tthrow new StateRejectedBySsm(\n\t\t\t\tstage,\n\t\t\t\tBuffer.byteLength(packSsmBody(document)),\n\t\t\t\tthis.location(stage),\n\t\t\t\tthis.region,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\t/** The conditional put of `body`, stored as it is given. */\n\tprivate async put(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\t// Kept as it was — the v1 state a person restores by hand —\n\t\t\t\t\t// unless it is too big to store that way.\n\t\t\t\t\tValue: packSsmBody(body),\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tTier: 'Intelligent-Tiering',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\t// A few hundred bytes: the standard tier, which costs nothing.\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -6,64 +6,6 @@ import { access, mkdir, writeFile } from "node:fs/promises";
6
6
  import { canonicalId, kebabCase } from "@geekmidas/manifest";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Subscriber } from "@geekmidas/constructs/subscribers";
9
- //#region src/types.ts
10
- /** Whether `value` names a cache backend. */
11
- function isCacheBackend(value) {
12
- return value === "upstash" || value === "elasticache" || value === "db" || value === "redis";
13
- }
14
- /**
15
- * Whether a backend holds every declared cache, a cache declared from a
16
- * database (`database.cache('Sessions')`) included.
17
- *
18
- * Every other backend leaves such a cache where its declaration put it: the
19
- * declaration is the stronger statement. The stack's own Redis is the
20
- * exception, because a stack that runs one runs it *for* the cache — a table
21
- * beside it would be a second cache nobody asked for.
22
- */
23
- function holdsEveryCache(backend) {
24
- return backend === "redis";
25
- }
26
- /**
27
- * The backend a project gets when it says nothing, by where it deploys.
28
- *
29
- * Flat defaults could not be right for both: `upstash` on a single box means a
30
- * SaaS round-trip for a cache the database could have held, and `minio` on AWS
31
- * means running a bucket beside S3. So the default is a function of the target,
32
- * and an app that says nothing gets the obvious answer in both places — which
33
- * is the whole of what "zero config" can mean once there is more than one
34
- * target.
35
- *
36
- * `server` covers Dokploy and a bare host alike: the question these answer is
37
- * whether the app is next to a managed cloud or next to its own containers, and
38
- * on that question the two are the same.
39
- *
40
- * A per-construct, per-stage override is the layer above this — see
41
- * `constructs-outstanding.md` — so two buckets in one app can differ. This is
42
- * only what happens when nobody has said anything at all.
43
- */
44
- const DEFAULT_CACHE = {
45
- aws: "upstash",
46
- server: "db"
47
- };
48
- /**
49
- * Which broker carries a declared queue or topic, by target.
50
- *
51
- * Whether there are events at all is the manifest's answer — a declared
52
- * `Topic` or `Queue` — so this only ever answers the second question. AWS
53
- * deploys them to SNS and SQS, and locally the AWS emulator stands in, so the
54
- * broker a developer runs is the one production has. A server has a Postgres
55
- * already, and pg-boss keeps its queues there.
56
- */
57
- const DEFAULT_EVENTS = {
58
- aws: "sns",
59
- server: "pgboss"
60
- };
61
- /** Where a bucket lives when nobody said — see {@link DEFAULT_CACHE}. */
62
- const DEFAULT_STORAGE = {
63
- aws: "s3",
64
- server: "minio"
65
- };
66
- //#endregion
67
9
  //#region src/generators/subscriberRoute.ts
68
10
  /**
69
11
  * Where SNS pushes a topic subscriber's messages, on the server that runs it:
@@ -699,6 +641,6 @@ function assertDoesNotFeedItself(key, construct) {
699
641
  if ((construct.constructs ?? []).some((id) => canonicalId(id) === canonicalId(bucket))) throw new ObjectSubscriberFeedsItself(key, bucket);
700
642
  }
701
643
  //#endregion
702
- export { SNS_SUBSCRIPTIONS_KEY as a, DEFAULT_CACHE as c, holdsEveryCache as d, isCacheBackend as f, readFakes as i, DEFAULT_EVENTS as l, assertTestCredentials as n, objectsRoute as o, readCredentialFakes as r, subscriberRoute as s, SubscriberGenerator as t, DEFAULT_STORAGE as u };
644
+ export { SNS_SUBSCRIPTIONS_KEY as a, readFakes as i, assertTestCredentials as n, objectsRoute as o, readCredentialFakes as r, subscriberRoute as s, SubscriberGenerator as t };
703
645
 
704
- //# sourceMappingURL=SubscriberGenerator-ocQ2yj0K.mjs.map
646
+ //# sourceMappingURL=SubscriberGenerator-BALddfuW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubscriberGenerator-BALddfuW.mjs","names":[],"sources":["../src/generators/subscriberRoute.ts","../src/reconcile/fakes.ts","../src/generators/SubscriberGenerator.ts"],"sourcesContent":["/**\n * Where SNS pushes a topic subscriber's messages, on the server that runs it:\n * named after the subscriber's export.\n *\n * One definition for both sides — the generated server mounts the route, and\n * the stack exposes exactly these paths on a worker's host — so the path the\n * edge lets through and the path the worker answers cannot drift.\n */\nexport const SUBSCRIBER_ROUTE_PREFIX = '/__gkm/subscribers/';\n\n/** The route a subscriber's pushes arrive on. */\nexport function subscriberRoute(exportName: string): string {\n\treturn `${SUBSCRIBER_ROUTE_PREFIX}${exportName}`;\n}\n\n/**\n * Where the deploy hands a server the SNS subscriptions it made for its\n * routes: a JSON object of subscriber export → subscription ARN. Against SNS\n * itself a server mounts a route only for a subscription recorded here, and\n * refuses a notification that names another.\n */\nexport const SNS_SUBSCRIPTIONS_KEY = 'GKM_SNS_SUBSCRIPTIONS';\n\n/**\n * Where a local object store posts a bucket's events, on the process that runs\n * the bucket's subscribers: named after the bucket. Never on a public host —\n * MinIO reaches it over the stack's network, or the host's.\n */\nexport const OBJECTS_ROUTE_PREFIX = '/__gkm/objects/';\n\n/** The route a bucket's events are posted to. */\nexport function objectsRoute(bucketId: string): string {\n\treturn `${OBJECTS_ROUTE_PREFIX}${bucketId}`;\n}\n","/**\n * Fakes, read from where the convention puts them.\n *\n * `test/fakes/<id>.ts` default-exports `fake.app(…)` or `fake.image(…)` for an\n * external API, and `fake.credential(…)` for a credential. The construct never\n * names it — that is what keeps a fake out of a deployed bundle — so this is\n * the one place it is found, by the same rule that finds a database's factory\n * in `test/factories/<id>.ts`.\n */\n\nimport { access } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { ConstructManifest } from '@geekmidas/manifest';\nimport { kebabCase } from '@geekmidas/manifest';\nimport { GkmError } from '../errors';\nimport type { Plan } from './plan';\n\n/** Where a project keeps its external APIs' fakes, relative to its root. */\nexport const DEFAULT_FAKES_DIR = 'test/fakes';\n\n/** One external API's fake, as the local target needs it. */\nexport interface LocalFake {\n\t/** The file it was read from — what the harness and `gkm dev` import. */\n\tfile: string;\n\t/** The image to run, for a fake the provider publishes. */\n\timage?: string;\n\t/** The port that image listens on inside its container. */\n\tport?: number;\n\t/** What the fake accepts, as the JSON `<ID>_CREDENTIALS` holds. */\n\tcredentials: string;\n}\n\n/** The marker `fake.app` and `fake.image` set — the same registered symbol. */\nconst FAKE = Symbol.for('@geekmidas/constructs/fake');\n\ninterface FakeExport {\n\tkind: 'app' | 'image' | 'credential';\n\timage?: string;\n\tport?: number;\n\tcredentials: unknown;\n}\n\n/**\n * The fake for every external API the manifest declares, keyed by id.\n *\n * @throws {NoFake} when an external API has no file, since a local stage would\n * otherwise have nothing to call.\n * @throws {NotAFake} when the file's default export is not one.\n */\nexport async function readFakes(\n\troot: string,\n\tmanifest: ConstructManifest,\n): Promise<Record<string, LocalFake>> {\n\tconst fakes: Record<string, LocalFake> = {};\n\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\tif (declaration?.kind !== 'external-api') continue;\n\n\t\tconst file = await fakeFile(join(root, DEFAULT_FAKES_DIR), id);\n\t\tif (!file) throw new NoFake(id, `${fakeBase(root, id)}.ts`);\n\t\tconst value = await importFake(file);\n\t\tif (value.kind === 'credential') throw new NotAFake(file);\n\n\t\tfakes[id] = {\n\t\t\tfile,\n\t\t\t...(value.kind === 'image'\n\t\t\t\t? { image: value.image, port: value.port }\n\t\t\t\t: {}),\n\t\t\tcredentials: JSON.stringify(value.credentials),\n\t\t};\n\t}\n\n\treturn fakes;\n}\n\n/**\n * Each credential's fake value, as the JSON `<ID>_CREDENTIALS` holds, keyed by\n * id — for the credentials that have a fake. Unlike an external API's, a\n * credential's fake is optional here: a stage that stores the real value\n * needs none. The test stage's own check (`assertTestCredentials`) is what\n * refuses one with neither.\n *\n * @throws {NotAFake} when the file's default export is not a credential's fake.\n */\nexport async function readCredentialFakes(\n\troot: string,\n\tmanifest: ConstructManifest,\n): Promise<Record<string, string>> {\n\tconst values: Record<string, string> = {};\n\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\tif (declaration?.kind !== 'credential') continue;\n\n\t\tconst file = await fakeFile(join(root, DEFAULT_FAKES_DIR), id);\n\t\tif (!file) continue;\n\t\tconst value = await importFake(file);\n\t\tif (value.kind !== 'credential') throw new NotAFake(file);\n\n\t\tvalues[id] = JSON.stringify(value.credentials);\n\t}\n\n\treturn values;\n}\n\n/**\n * A credential the test stage has no value for: no fake, and nothing stored.\n * Raised when the stage is set up, before a test reaches it — not as a schema\n * error inside the handler that first asked.\n */\nexport class CredentialHasNoTestValue extends GkmError {\n\tconstructor(\n\t\treadonly id: string,\n\t\treadonly key: string,\n\t\treadonly file: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${id}' is a credential, and the test stage has no value for ${key}: ` +\n\t\t\t\t`no fake, and none stored — a stage set up fresh, as CI's ` +\n\t\t\t\t`auto-setup does, holds no third party's credentials. Give it a ` +\n\t\t\t\t`fake: create ${file}, default-exporting ` +\n\t\t\t\t`fake.credential(…) from @geekmidas/constructs/credential. It is ` +\n\t\t\t\t`read only by gkm test and gkm dev --fake; a deployed stage still ` +\n\t\t\t\t`needs the real value (gkm secrets:add --stage <stage>).`,\n\t\t);\n\t\tthis.name = 'CredentialHasNoTestValue';\n\t}\n}\n\n/**\n * Every credential the test stage must hand its suite, with the value it\n * resolved — a fake's, or one the stage stores.\n *\n * @throws {CredentialHasNoTestValue} for the first one with neither.\n */\nexport function assertTestCredentials(\n\troot: string,\n\tplan: Pick<Plan, 'resources'>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): Record<string, string> {\n\tconst values: Record<string, string> = {};\n\n\tfor (const { id, kind, envKey } of plan.resources) {\n\t\tif (kind !== 'credential') continue;\n\n\t\tconst value = env[envKey];\n\t\tif (value === undefined) {\n\t\t\tthrow new CredentialHasNoTestValue(\n\t\t\t\tid,\n\t\t\t\tenvKey,\n\t\t\t\t`${fakeBase(root, id)}.ts`,\n\t\t\t);\n\t\t}\n\t\tvalues[envKey] = value;\n\t}\n\n\treturn values;\n}\n\n/** `<root>/test/fakes/<id>`, without an extension. */\nfunction fakeBase(root: string, id: string): string {\n\treturn join(root, DEFAULT_FAKES_DIR, kebabCase(id));\n}\n\n/** The first of `<id>.ts`, `.mts`, `.js`, `.mjs` that exists. */\nasync function fakeFile(\n\tfolder: string,\n\tid: string,\n): Promise<string | undefined> {\n\tconst base = join(folder, kebabCase(id));\n\tfor (const extension of ['.ts', '.mts', '.js', '.mjs']) {\n\t\tconst file = `${base}${extension}`;\n\t\tconst exists = await access(file).then(\n\t\t\t() => true,\n\t\t\t() => false,\n\t\t);\n\t\tif (exists) return file;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * A fake file's default export.\n *\n * @throws {NotAFake} when it is not a fake.\n */\nasync function importFake(file: string): Promise<FakeExport> {\n\tconst module = (await import(pathToFileURL(file).href)) as {\n\t\tdefault?: unknown;\n\t};\n\tconst value = module.default;\n\tif (!isFakeExport(value)) throw new NotAFake(file);\n\treturn value;\n}\n\nfunction isFakeExport(value: unknown): value is FakeExport {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { [FAKE]?: unknown })[FAKE] === true\n\t);\n}\n\n/** An external API with no fake for a local stage to call. */\nexport class NoFake extends GkmError {\n\tconstructor(\n\t\treadonly id: string,\n\t\treadonly file: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${id}' is an external API, and a local stage calls its fake rather ` +\n\t\t\t\t`than the provider — but there is none. Create ${file}, ` +\n\t\t\t\t`default-exporting fake.app(…) or fake.image(…) from ` +\n\t\t\t\t`@geekmidas/constructs/external-api.`,\n\t\t);\n\t\tthis.name = 'NoFake';\n\t}\n}\n\n/** A file in the fakes folder whose default export is not a fake. */\nexport class NotAFake extends GkmError {\n\tconstructor(readonly file: string) {\n\t\tsuper(\n\t\t\t`${file} is where a fake lives, and its default export is not one ` +\n\t\t\t\t`for its construct. For an external API, export default ` +\n\t\t\t\t`fake.app(handler, { credentials }) or ` +\n\t\t\t\t`fake.image(image, { port, credentials }); for a credential, ` +\n\t\t\t\t`fake.credential(value).`,\n\t\t);\n\t\tthis.name = 'NotAFake';\n\t}\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative } from 'node:path';\nimport { Subscriber } from '@geekmidas/constructs/subscribers';\nimport { canonicalId } from '@geekmidas/manifest';\nimport type { BuildContext } from '../build/types';\nimport {\n\tsubscriberConnectionKey,\n\tsubscriberTokenKey,\n} from '../reconcile/edges.js';\nimport type { SubscriberInfo } from '../types';\nimport { runtimeFor } from './EndpointGenerator.js';\nimport {\n\tConstructGenerator,\n\ttype GeneratedConstruct,\n\ttype GeneratorOptions,\n} from './Generator';\nimport {\n\tOBJECTS_ROUTE_PREFIX,\n\tSNS_SUBSCRIPTIONS_KEY,\n\tSUBSCRIBER_ROUTE_PREFIX,\n} from './subscriberRoute.js';\n\nexport class SubscriberGenerator extends ConstructGenerator<\n\tSubscriber<any, any, any, any, any, any, any>,\n\tSubscriberInfo[]\n> {\n\tisConstruct(\n\t\tvalue: any,\n\t): value is Subscriber<any, any, any, any, any, any, any> {\n\t\treturn Subscriber.isSubscriber(value);\n\t}\n\n\tasync build(\n\t\tcontext: BuildContext,\n\t\tconstructs: GeneratedConstruct<\n\t\t\tSubscriber<any, any, any, any, any, any, any>\n\t\t>[],\n\t\toutputDir: string,\n\t\toptions?: GeneratorOptions,\n\t): Promise<SubscriberInfo[]> {\n\t\tconst target = options?.target ?? 'aws';\n\t\tconst logger = console;\n\t\tconst subscriberInfos: SubscriberInfo[] = [];\n\n\t\tfor (const { key, construct } of constructs) {\n\t\t\tassertDoesNotFeedItself(key, construct);\n\t\t}\n\n\t\tif (target === 'server') {\n\t\t\t// Generate subscribers.ts for server-based polling (even if empty).\n\t\t\t// A bucket's subscriber is pushed to by the topic the storage\n\t\t\t// publishes to, whatever broker carries the app's own events.\n\t\t\tawait this.generateServerSubscribersFile(\n\t\t\t\toutputDir,\n\t\t\t\tconstructs,\n\t\t\t\t(context.eventsBackends?.includes('sns') ?? false) ||\n\t\t\t\t\tconstructs.some(({ construct }) => construct.bucketName),\n\t\t\t);\n\n\t\t\tlogger.log(\n\t\t\t\t`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`,\n\t\t\t);\n\n\t\t\t// Return empty array as server subscribers don't have individual handlers\n\t\t\treturn subscriberInfos;\n\t\t}\n\n\t\tif (constructs.length === 0) {\n\t\t\treturn subscriberInfos;\n\t\t}\n\n\t\t// Create subscribers subdirectory\n\t\tconst subscribersDir = join(outputDir, 'subscribers');\n\t\tawait mkdir(subscribersDir, { recursive: true });\n\n\t\t// Generate subscriber handlers\n\t\tfor (const { key, construct, path } of constructs) {\n\t\t\tconst handlerFile = await this.generateSubscriberHandler(\n\t\t\t\tsubscribersDir,\n\t\t\t\tpath.relative,\n\t\t\t\tkey,\n\t\t\t\tconstruct,\n\t\t\t\tcontext,\n\t\t\t\tconstruct.owner,\n\t\t\t);\n\n\t\t\tsubscriberInfos.push({\n\t\t\t\tname: key,\n\t\t\t\thandler: relative(options?.root ?? process.cwd(), handlerFile).replace(\n\t\t\t\t\t/\\.ts$/,\n\t\t\t\t\t'.handler',\n\t\t\t\t),\n\t\t\t\tsubscribedEvents: construct.subscribedEvents || [],\n\t\t\t\t// A subscriber bound to a topic (via `s.topic(topic)`) records the\n\t\t\t\t// binding so infra wires the SNS subscription.\n\t\t\t\t...(construct.topicName\n\t\t\t\t\t? { transport: 'topic' as const, topic: construct.topicName }\n\t\t\t\t\t: {}),\n\t\t\t\t// A subscriber bound to a bucket records it, and the keys it\n\t\t\t\t// watches, so infra wires the bucket's notifications.\n\t\t\t\t...(construct.bucketName\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttransport: 'topic' as const,\n\t\t\t\t\t\t\tbucket: construct.bucketName,\n\t\t\t\t\t\t\t...construct.objectFilter,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t\ttimeout: construct.timeout,\n\t\t\t\tmemorySize: construct.memorySize,\n\t\t\t\tenvironment: await construct.getEnvironment({\n\t\t\t\t\tmarkOptional: context.markOptional,\n\t\t\t\t}),\n\t\t\t\tdependencies: construct.constructs,\n\t\t\t});\n\n\t\t\tlogger.log(`Generated subscriber handler: ${key}`);\n\t\t}\n\n\t\treturn subscriberInfos;\n\t}\n\n\tprivate async generateSubscriberHandler(\n\t\toutputDir: string,\n\t\tsourceFile: string,\n\t\texportName: string,\n\t\t_subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tcontext: BuildContext,\n\t\t/** The construct that owns this — its worker, or its surface. */\n\t\towner: string | undefined,\n\t): Promise<string> {\n\t\tconst handlerFileName = `${exportName}.ts`;\n\t\tconst handlerPath = join(outputDir, handlerFileName);\n\n\t\tconst relativePath = relative(dirname(handlerPath), sourceFile);\n\t\tconst importPath = relativePath.replace(/\\.ts$/, '.js');\n\n\t\t// Imports the construct that owns this, and binds `envParser` and\n\t\t// `logger` off it — the objects it was declared with, rather than a\n\t\t// module path printed in from config.\n\t\tconst runtime = runtimeFor(context, dirname(handlerPath), owner);\n\n\t\tconst content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';\nimport { ${exportName} } from '${importPath}';\n${runtime.imports}\n${context.storageDrivers?.imports ?? ''}\n${runtime.bindings}\n${context.storageDrivers?.setup ? `\\n// The handler registers the drivers its target needs.\\n${context.storageDrivers.setup}\\n` : ''}\nconst adapter = new AWSLambdaSubscriber(envParser, ${exportName});\n\nexport const handler = adapter.handler;\n`;\n\n\t\tawait writeFile(handlerPath, content);\n\t\treturn handlerPath;\n\t}\n\n\tprivate async generateServerSubscribersFile(\n\t\toutputDir: string,\n\t\tsubscribers: GeneratedConstruct<\n\t\t\tSubscriber<any, any, any, any, any, any, any>\n\t\t>[],\n\t\t/**\n\t\t * The broker is SNS, so a subscriber is pushed to over HTTP. Only then is\n\t\t * the push code generated: it imports the SNS client, which a bundle for\n\t\t * any other broker must never have to resolve.\n\t\t */\n\t\tpush: boolean,\n\t): Promise<string> {\n\t\tawait mkdir(outputDir, { recursive: true });\n\t\tconst subscribersPath = join(outputDir, 'subscribers.ts');\n\n\t\t// Nothing to run, so nothing to import. Every app gets this file, and\n\t\t// `@geekmidas/events` is only installed by one that declared a Topic or\n\t\t// a Queue — importing it here crashed every dev server that had not.\n\t\tif (subscribers.length === 0) {\n\t\t\tawait writeFile(\n\t\t\t\tsubscribersPath,\n\t\t\t\t`/**\n * Generated subscribers setup — this app declares none.\n */\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\n\nexport async function setupSubscribers(\n _app: unknown,\n _envParser: EnvironmentParser<any>,\n _logger: Logger,\n): Promise<(port: number) => Promise<void>> {\n return async () => {};\n}\n\nexport function subscribersStatus(): { failed: string[]; connections: unknown[] } {\n return { failed: [], connections: [] };\n}\n\nexport async function stopSubscribers(): Promise<void> {}\n`,\n\t\t\t);\n\t\t\treturn subscribersPath;\n\t\t}\n\n\t\tconst importsByFile = new Map<string, string[]>();\n\t\tfor (const { path, key } of subscribers) {\n\t\t\tconst importPath = relative(\n\t\t\t\tdirname(subscribersPath),\n\t\t\t\tpath.relative,\n\t\t\t).replace(/\\.ts$/, '.js');\n\t\t\timportsByFile.set(importPath, [\n\t\t\t\t...(importsByFile.get(importPath) ?? []),\n\t\t\t\tkey,\n\t\t\t]);\n\t\t}\n\t\tconst imports = Array.from(importsByFile.entries())\n\t\t\t.map(\n\t\t\t\t([importPath, exports]) =>\n\t\t\t\t\t`import { ${exports.join(', ')} } from '${importPath}';`,\n\t\t\t)\n\t\t\t.join('\\n');\n\n\t\t// Each subscriber reaches the topic it is bound to, by that topic's own\n\t\t// key — resolved here, at build time, from the binding. A bucket's\n\t\t// subscriber reaches the bucket's events, which carry no `type`\n\t\t// attribute for SNS to filter on: it filters what it is sent itself.\n\t\tconst entries = subscribers\n\t\t\t.map(({ key, construct }) => {\n\t\t\t\tconst connectionKey = subscriberConnectionKey(construct);\n\t\t\t\tconst tokenKey = subscriberTokenKey(construct);\n\t\t\t\tconst topic = construct.topicName ?? construct.bucketName;\n\t\t\t\tconst bucket = construct.bucketName\n\t\t\t\t\t? canonicalId(construct.bucketName)\n\t\t\t\t\t: undefined;\n\t\t\t\treturn ` { id: '${key}', subscriber: ${key}, topic: ${\n\t\t\t\t\ttopic ? `'${topic}'` : 'undefined'\n\t\t\t\t}, connectionKey: ${\n\t\t\t\t\tconnectionKey ? `'${connectionKey}'` : 'undefined'\n\t\t\t\t}, filtered: ${construct.bucketName ? 'false' : 'true'}, bucket: ${\n\t\t\t\t\tbucket ? `'${bucket}'` : 'undefined'\n\t\t\t\t}, tokenKey: ${tokenKey ? `'${tokenKey}'` : 'undefined'} },`;\n\t\t\t})\n\t\t\t.join('\\n');\n\n\t\tconst content = `/**\n * Generated subscribers setup.\n *\n * Each subscriber is run against the topic it is bound to, through that\n * topic's own connection string:\n *\n * - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this\n * server, subscribed to the topic with a filter policy of the events it\n * named, so SNS fans out to each subscriber. The route runs the subscriber\n * through the same adaptor a Lambda subscription does. Against SNS itself\n * the deploy made the subscription and recorded its ARN in\n * ${SNS_SUBSCRIPTIONS_KEY}: a route is mounted only for a recorded one, its\n * confirmation goes through the API, and nothing is subscribed from here.\n * Against an emulator, where nothing deploys, the route subscribes itself.\n * - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss\n * each subscriber drains a queue of its own, \\`<topic>/<subscriber>\\`, so\n * every subscriber sees every event; replicas of one subscriber share it.\n *\n * A bucket's subscriber is polled the same way, under the bucket's name, when\n * the bucket is in a local object store: the store posts each event to\n * \\`${OBJECTS_ROUTE_PREFIX}<Bucket>\\` on this server, with the bucket's token, and\n * the event is relayed onto the broker as \\`object.created\\` or\n * \\`object.deleted\\` — so every subscriber of the bucket sees it.\n *\n * GKM_SUBSCRIBERS=off (\\`gkm dev --no-subscribers\\`) runs none of them.\n * GKM_SUBSCRIBER_PUSH_URL is the address an emulator pushes to, defaulting to\n * this server through host.docker.internal.\n */\nimport { timingSafeEqual } from 'node:crypto';\nimport { connect } from 'node:net';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { EventConnectionFactory, Publisher, Subscriber } from '@geekmidas/events';\nimport type { EventConnection } from '@geekmidas/events';\nimport { ServiceDiscovery } from '@geekmidas/services';\n${imports}\n\nconst subscribers = [\n${entries}\n];\n\n// What was opened, and what could not be: a worker's health check reads both,\n// and its shutdown closes what was opened.\nconst connections = new Map<string, EventConnection>();\nconst failed: string[] = [];\n\n/**\n * The subscriptions the deploy made against SNS itself, by subscriber: which\n * routes to mount, and the ARN each one's notifications must name.\n */\nfunction recordedSubscriptions(logger: Logger): Record<string, string> {\n const raw = process.env.${SNS_SUBSCRIPTIONS_KEY};\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;\n } catch {}\n logger.error('${SNS_SUBSCRIPTIONS_KEY} is not a JSON object of subscriber → subscription ARN');\n return {};\n}\n\n/** The route a subscriber's pushes arrive on. */\nexport const SUBSCRIBER_ROUTE = (id: string) => \\`${SUBSCRIBER_ROUTE_PREFIX}\\${id}\\`;\n\n/** The route a local object store posts a bucket's events to. */\nexport const OBJECTS_ROUTE = (bucket: string) => \\`${OBJECTS_ROUTE_PREFIX}\\${bucket}\\`;\n\n// The buckets whose events this server relays: one route each, however many\n// subscribers watch the bucket.\nconst relays = new Set<string>();\n\n/**\n * Mount the route a local object store posts one bucket's events to, and\n * relay each event onto the broker under the bucket's name.\n *\n * Answered 2xx only once every event is published: the store keeps what it\n * was not told it delivered, and sends it again.\n */\nasync function mountObjectRelay(\n app: { post: (path: string, handler: (c: any) => unknown) => unknown },\n relay: { bucket: string; topic: string; connection: EventConnection; tokenKey: string },\n logger: Logger,\n): Promise<void> {\n const token = process.env[relay.tokenKey];\n if (!token) {\n logger.error(\n { bucket: relay.bucket, tokenKey: relay.tokenKey },\n 'No token for this bucket: the events its object store posts cannot be received',\n );\n failed.push(\\`\\${relay.bucket}#events\\`);\n return;\n }\n const { toObjectEvents } = await import('@geekmidas/storage/events');\n const publisher = await Publisher.fromConnection(relay.connection, { topic: relay.topic });\n const expected = Buffer.from(\\`Bearer \\${token}\\`);\n\n app.post(OBJECTS_ROUTE(relay.bucket), async (c: any) => {\n // Compared in constant time: the route is not public, but it is the one\n // way to put an event in front of every subscriber of the bucket.\n const given = Buffer.from(c.req.header('authorization') ?? '');\n if (given.length !== expected.length || !timingSafeEqual(given, expected)) {\n return c.json({ status: 'refused' }, 401);\n }\n\n let messages: Awaited<ReturnType<typeof toObjectEvents>>;\n try {\n messages = toObjectEvents(JSON.parse(await c.req.text()));\n } catch (error) {\n logger.warn({ err: error, bucket: relay.bucket }, 'Refused an object notification that did not parse');\n return c.json({ status: 'malformed' }, 400);\n }\n\n try {\n if (messages.length > 0) await publisher.publish(messages as any);\n } catch (error) {\n // The store sends it again.\n logger.error({ err: error, bucket: relay.bucket }, 'Failed to relay object events');\n return c.json({ status: 'failed' }, 500);\n }\n return c.json({ status: 'ok', relayed: messages.length }, 200);\n });\n logger.info({ bucket: relay.bucket }, 'Relaying object events');\n}\n\n/** Resolves once something is accepting connections on the port. */\nasync function listening(port: number): Promise<void> {\n for (let attempt = 0; attempt < 100; attempt++) {\n const open = await new Promise<boolean>((resolve) => {\n const socket = connect(port, '127.0.0.1');\n socket.once('connect', () => { socket.end(); resolve(true); });\n socket.once('error', () => resolve(false));\n });\n if (open) return;\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n}\n\n/**\n * Mount each pushed subscriber's route and start each polled one. Returns what\n * to run once the server is listening: subscribing a route makes SNS send its\n * confirmation at once, so the route has to be reachable first.\n */\nexport async function setupSubscribers(\n app: { post: (path: string, handler: (c: any) => unknown) => unknown },\n envParser: EnvironmentParser<any>,\n logger: Logger,\n): Promise<(port: number) => Promise<void>> {\n if (process.env.GKM_SUBSCRIBERS === 'off') {\n logger.info('Subscribers are off (--no-subscribers)');\n return async () => {};\n }\n\n const serviceDiscovery = ServiceDiscovery.getInstance(envParser);\n const recorded = recordedSubscriptions(logger);\n const afterListening: ((port: number) => Promise<void>)[] = [];\n\n for (const { id, subscriber, topic, connectionKey, filtered, bucket, tokenKey } of subscribers) {\n const events = subscriber.subscribedEvents || [];\n if (events.length === 0) {\n logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');\n continue;\n }\n const connectionString = connectionKey ? process.env[connectionKey] : undefined;\n if (!connectionString) {\n logger.error(\n { subscriber: id, connectionKey },\n 'No connection string for the topic or bucket this subscriber is bound to',\n );\n failed.push(id);\n continue;\n }\n\n try {\n${\n\tpush\n\t\t? ` if (connectionString.startsWith('sns:')) {\n const { SNSConnection, snsUrl, subscribeHttpEndpoint, confirmSubscription } = await import('@geekmidas/events/sns');\n const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/subscribers');\n const address = snsUrl.parse(connectionString);\n const route = SUBSCRIBER_ROUTE(id);\n const mount = (adaptor: InstanceType<typeof SnsPushSubscriberAdaptor>) =>\n app.post(route, async (c: any) => {\n const body = JSON.parse(await c.req.text());\n const response = await adaptor.handle(body, {\n subscriptionArn: c.req.header('x-amz-sns-subscription-arn'),\n });\n return c.json(response.body, response.status);\n });\n\n if (!address.endpoint) {\n // SNS itself: the deploy subscribed this route and recorded the ARN.\n // Nothing is subscribed from here, so this server's credentials need\n // only sns:ConfirmSubscription on the topic.\n const subscriptionArn = recorded[id];\n if (!subscriptionArn) {\n logger.error(\n { subscriber: id },\n 'No subscription recorded for this subscriber in ${SNS_SUBSCRIPTIONS_KEY}: deploy it so SNS has a route to push to',\n );\n failed.push(id);\n continue;\n }\n const connection = await SNSConnection.fromConnectionString(connectionString);\n mount(\n new SnsPushSubscriberAdaptor(envParser, subscriber, {\n topicArn: address.topicArn,\n verify: true,\n subscriptionArn,\n // As the topic's owner, so only the owner can unsubscribe the\n // route — not whoever holds a notification's UnsubscribeURL.\n confirm: (message) => confirmSubscription(connection, message),\n }),\n );\n logger.info({ subscriber: id, events }, 'Subscriber mounted for push');\n continue;\n }\n\n // An emulator signs nothing, and nothing deploys its subscriptions:\n // decided by the address this server was configured with, never by\n // anything a request says about itself.\n mount(\n new SnsPushSubscriberAdaptor(envParser, subscriber, {\n topicArn: address.topicArn,\n verify: false,\n emulatorEndpoint: address.endpoint,\n }),\n );\n\n afterListening.push(async (port) => {\n const base =\n process.env.GKM_SUBSCRIBER_PUSH_URL ?? \\`http://host.docker.internal:\\${port}\\`;\n const connection = await SNSConnection.fromConnectionString(connectionString);\n await subscribeHttpEndpoint(connection, {\n endpoint: new URL(route, base).toString(),\n ...(filtered ? { events } : {}),\n });\n logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');\n });\n continue;\n }\n`\n\t\t: ` if (connectionString.startsWith('sns:')) {\n // Built for a broker other than SNS, so this entry registered no SNS\n // driver and has no push route to offer.\n logger.error(\n { subscriber: id, connectionKey },\n 'This build has no SNS driver: it was built for a target whose broker is not SNS',\n );\n failed.push(id);\n continue;\n }\n`\n}\n\n let connection = connections.get(connectionString);\n if (!connection) {\n connection = await EventConnectionFactory.fromConnectionString(connectionString);\n connections.set(connectionString, connection);\n }\n // Named, so a broker with one address for everything (pg-boss) gives\n // this subscriber a queue of its own: fan-out, not competition.\n const eventSubscriber = await Subscriber.fromConnection(connection, {\n topic,\n subscription: id,\n });\n const services = subscriber.services.length > 0\n ? await serviceDiscovery.register(subscriber.services)\n : {};\n // The subscriber's database — the worker's, or its own — as \\`db\\`. A\n // pushed subscriber gets it from the adaptor above instead.\n const db = subscriber.databaseService\n ? (await serviceDiscovery.register([subscriber.databaseService]))[\n subscriber.databaseService.serviceName\n ]\n : undefined;\n\n await eventSubscriber.subscribe(events, async (event) => {\n try {\n await subscriber.handler({\n events: [event],\n services: services as any,\n logger: subscriber.logger,\n db,\n } as any);\n } catch (error) {\n logger.error({ err: error, event, subscriber: id }, 'Failed to process event');\n // Rethrown so the transport keeps the event for a retry — as a queue\n // consumer's failure does, and as a pushed subscriber's 500 does.\n throw error;\n }\n });\n logger.info({ subscriber: id, events }, 'Subscriber started polling');\n\n // A bucket in a local object store publishes nothing for this to poll:\n // the store posts its events here, and they are relayed onto the broker.\n if (bucket && tokenKey && topic && !relays.has(bucket)) {\n relays.add(bucket);\n await mountObjectRelay(app, { bucket, topic, connection, tokenKey }, logger);\n }\n } catch (error) {\n // The message too: a missing module's code alone does not say which.\n logger.error(\n { err: error, subscriber: id },\n 'Failed to set up subscriber',\n );\n failed.push(id);\n }\n }\n\n return async (port) => {\n if (afterListening.length === 0) return;\n await listening(port);\n for (const subscribe of afterListening) {\n await subscribe(port).catch((error) => {\n logger.error({ err: error }, 'Failed to subscribe for push');\n });\n }\n };\n}\n\n/** The subscribers that could not start, and the connections that did. */\nexport function subscribersStatus(): { failed: string[]; connections: EventConnection[] } {\n return { failed: [...failed], connections: [...connections.values()] };\n}\n\n/**\n * Stop polling, let the events in flight finish, and close each broker\n * connection — pg-boss's graceful stop does all three. The caller bounds it.\n */\nexport async function stopSubscribers(): Promise<void> {\n const open = [...connections.values()];\n connections.clear();\n await Promise.all(open.map((connection) => connection.close()));\n}\n`;\n\n\t\tawait writeFile(subscribersPath, content);\n\t\treturn subscribersPath;\n\t}\n}\n\n/**\n * A bucket's subscriber that writes back into the keys it watches.\n *\n * Every write it makes is a new object event, which runs it again — forever,\n * and billed per run. The rule is the one the build can check: a subscriber\n * that depends on the bucket it watches must watch a prefix, so what it writes\n * can go elsewhere. A thumbnailer watching `uploads/` writes `thumbnails/`.\n */\nexport class ObjectSubscriberFeedsItself extends Error {\n\tconstructor(\n\t\treadonly subscriber: string,\n\t\treadonly bucket: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${subscriber}' watches every key in '${bucket}' and depends on it, ` +\n\t\t\t\t`so each object it writes runs it again. Watch a prefix — ` +\n\t\t\t\t`.on([...], { prefix: 'uploads/' }) — and write outside it.`,\n\t\t);\n\t\tthis.name = 'ObjectSubscriberFeedsItself';\n\t}\n}\n\nfunction assertDoesNotFeedItself(\n\tkey: string,\n\tconstruct: Subscriber<any, any, any, any, any, any, any>,\n): void {\n\tconst bucket = construct.bucketName;\n\tif (!bucket || construct.objectFilter?.prefix) return;\n\tconst depends = (construct.constructs ?? []).some(\n\t\t(id: string) => canonicalId(id) === canonicalId(bucket),\n\t);\n\tif (depends) throw new ObjectSubscriberFeedsItself(key, bucket);\n}\n"],"mappings":"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"}