@geekmidas/cli 10.0.0-alpha.123 → 10.0.0-alpha.125
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.
- package/dist/{EndpointGenerator-BDa6pPgE.cjs → EndpointGenerator-BGxf9jWE.cjs} +8 -4
- package/dist/{EndpointGenerator-BDa6pPgE.cjs.map → EndpointGenerator-BGxf9jWE.cjs.map} +1 -1
- package/dist/{EndpointGenerator-CP_z_E3e.mjs → EndpointGenerator-errMeldx.mjs} +8 -4
- package/dist/{EndpointGenerator-CP_z_E3e.mjs.map → EndpointGenerator-errMeldx.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-BlB5nJky.mjs → GoDaddyProvider-BN1dODm9.mjs} +3 -3
- package/dist/{GoDaddyProvider-BlB5nJky.mjs.map → GoDaddyProvider-BN1dODm9.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-Dubq-pCc.cjs → GoDaddyProvider-BTdtqhI_.cjs} +3 -3
- package/dist/{GoDaddyProvider-Dubq-pCc.cjs.map → GoDaddyProvider-BTdtqhI_.cjs.map} +1 -1
- package/dist/{HostingerProvider-BNpS9aRJ.mjs → HostingerProvider-Cjh_2Ml6.mjs} +2 -2
- package/dist/{HostingerProvider-BNpS9aRJ.mjs.map → HostingerProvider-Cjh_2Ml6.mjs.map} +1 -1
- package/dist/{HostingerProvider-DR1iws1P.cjs → HostingerProvider-OdQmKXE3.cjs} +2 -2
- package/dist/{HostingerProvider-DR1iws1P.cjs.map → HostingerProvider-OdQmKXE3.cjs.map} +1 -1
- package/dist/{LegacyStateStore-ClvUC8qy.cjs → LegacyStateStore-C2S8TGwx.cjs} +3 -3
- package/dist/{LegacyStateStore-ClvUC8qy.cjs.map → LegacyStateStore-C2S8TGwx.cjs.map} +1 -1
- package/dist/{LegacyStateStore-DGALQGZu.mjs → LegacyStateStore-DgXmuG87.mjs} +3 -3
- package/dist/{LegacyStateStore-DGALQGZu.mjs.map → LegacyStateStore-DgXmuG87.mjs.map} +1 -1
- package/dist/{LocalStateStore-43kSYNpw.cjs → LocalStateStore-CaJw9H-3.cjs} +2 -2
- package/dist/{LocalStateStore-43kSYNpw.cjs.map → LocalStateStore-CaJw9H-3.cjs.map} +1 -1
- package/dist/{LocalStateStore-CBYUzVR5.mjs → LocalStateStore-ojY_BiMK.mjs} +2 -2
- package/dist/{LocalStateStore-CBYUzVR5.mjs.map → LocalStateStore-ojY_BiMK.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CsrukCE-.mjs → OpenApiTsGenerator-W_HRXH1X.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-CsrukCE-.mjs.map → OpenApiTsGenerator-W_HRXH1X.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-FNhY7Ssz.cjs → OpenApiTsGenerator-cQgtxWQo.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-FNhY7Ssz.cjs.map → OpenApiTsGenerator-cQgtxWQo.cjs.map} +1 -1
- package/dist/{S3StateStore-DLHjDYYP.mjs → S3StateStore-BSPkxhrx.mjs} +2 -2
- package/dist/{S3StateStore-DLHjDYYP.mjs.map → S3StateStore-BSPkxhrx.mjs.map} +1 -1
- package/dist/{S3StateStore-ahE7hSNK.cjs → S3StateStore-BqugXIcb.cjs} +2 -2
- package/dist/{S3StateStore-ahE7hSNK.cjs.map → S3StateStore-BqugXIcb.cjs.map} +1 -1
- package/dist/{SSMStateStore-qIzIuazA.cjs → SSMStateStore-B0OsuT_k.cjs} +2 -2
- package/dist/{SSMStateStore-qIzIuazA.cjs.map → SSMStateStore-B0OsuT_k.cjs.map} +1 -1
- package/dist/{SSMStateStore-j5RuMkch.mjs → SSMStateStore-DM85OiH0.mjs} +2 -2
- package/dist/{SSMStateStore-j5RuMkch.mjs.map → SSMStateStore-DM85OiH0.mjs.map} +1 -1
- package/dist/{SubscriberGenerator-C8CPxrCK.cjs → SubscriberGenerator-BDUp7OeY.cjs} +237 -54
- package/dist/SubscriberGenerator-BDUp7OeY.cjs.map +1 -0
- package/dist/{SubscriberGenerator-y3pncae_.mjs → SubscriberGenerator-ocQ2yj0K.mjs} +227 -56
- package/dist/SubscriberGenerator-ocQ2yj0K.mjs.map +1 -0
- package/dist/{add-Bm-C56lH.mjs → add-C7ZBXIzM.mjs} +6 -6
- package/dist/{add-Bm-C56lH.mjs.map → add-C7ZBXIzM.mjs.map} +1 -1
- package/dist/{add-C2CvXrYw.cjs → add-CrxvmSpq.cjs} +7 -7
- package/dist/{add-C2CvXrYw.cjs.map → add-CrxvmSpq.cjs.map} +1 -1
- package/dist/{bundler-Dk7tAvAX.cjs → bundler-DVUXW95p.cjs} +5 -5
- package/dist/{bundler-Dk7tAvAX.cjs.map → bundler-DVUXW95p.cjs.map} +1 -1
- package/dist/{bundler-DnkFt4mn.mjs → bundler-DpZ9uBGN.mjs} +5 -5
- package/dist/{bundler-DnkFt4mn.mjs.map → bundler-DpZ9uBGN.mjs.map} +1 -1
- package/dist/{cli-Di464j1O.cjs → cli-BMRhE6cZ.cjs} +4 -4
- package/dist/{cli-Di464j1O.cjs.map → cli-BMRhE6cZ.cjs.map} +1 -1
- package/dist/{cli-CVeHFXNE.mjs → cli-BWfYyy1k.mjs} +4 -4
- package/dist/{cli-CVeHFXNE.mjs.map → cli-BWfYyy1k.mjs.map} +1 -1
- package/dist/{compose-cHkF---f.mjs → compose-BFmdLig3.mjs} +525 -55
- package/dist/compose-BFmdLig3.mjs.map +1 -0
- package/dist/{compose-ChVvb3xM.cjs → compose-Bqcy0lqo.cjs} +554 -84
- package/dist/compose-Bqcy0lqo.cjs.map +1 -0
- package/dist/{config-DrOX29sT.d.mts → config-CdytFgSR.d.cts} +3 -4
- package/dist/config-CdytFgSR.d.cts.map +1 -0
- package/dist/{config-B3iz9BtK.d.cts → config-_y1FGBWR.d.mts} +3 -4
- package/dist/config-_y1FGBWR.d.mts.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +2 -2
- package/dist/{credentialSchemas-B67WJOOm.cjs → credentialSchemas-DFxvGEwa.cjs} +2 -2
- package/dist/{credentialSchemas-B67WJOOm.cjs.map → credentialSchemas-DFxvGEwa.cjs.map} +1 -1
- package/dist/{credentialSchemas-DDOLRCLF.mjs → credentialSchemas-DaMpt-Dx.mjs} +2 -2
- package/dist/{credentialSchemas-DDOLRCLF.mjs.map → credentialSchemas-DaMpt-Dx.mjs.map} +1 -1
- package/dist/{credentials-Zqr3QQYB.cjs → credentials-D7XJypNd.cjs} +173 -3
- package/dist/credentials-D7XJypNd.cjs.map +1 -0
- package/dist/{credentials-B5N5yKk6.mjs → credentials-DpfnUvIR.mjs} +132 -4
- package/dist/credentials-DpfnUvIR.mjs.map +1 -0
- package/dist/deploy/public.cjs +16 -17
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +3 -5
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +3 -5
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +11 -12
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/{deploy-DO4djCHb.mjs → deploy-D8pUA9Y0.mjs} +1240 -130
- package/dist/deploy-D8pUA9Y0.mjs.map +1 -0
- package/dist/{deploy-BffR_uEN.cjs → deploy-DVrNjX1X.cjs} +1261 -169
- package/dist/deploy-DVrNjX1X.cjs.map +1 -0
- package/dist/{discover-C5roJuc9.mjs → discover-CzGM5P3p.mjs} +136 -14
- package/dist/discover-CzGM5P3p.mjs.map +1 -0
- package/dist/{discover-DozUKs8f.cjs → discover-DL6h-J97.cjs} +158 -12
- package/dist/discover-DL6h-J97.cjs.map +1 -0
- package/dist/{dokploy-BcZm93Cc.mjs → dokploy-CzDGNB6A.mjs} +2 -2
- package/dist/{dokploy-BcZm93Cc.mjs.map → dokploy-CzDGNB6A.mjs.map} +1 -1
- package/dist/{dokploy-CsdvfH3t.cjs → dokploy-TtK6Zyl3.cjs} +2 -2
- package/dist/{dokploy-CsdvfH3t.cjs.map → dokploy-TtK6Zyl3.cjs.map} +1 -1
- package/dist/{drivers-BjGDYfnC.mjs → drivers-C-VqMtld.mjs} +2 -2
- package/dist/{drivers-BjGDYfnC.mjs.map → drivers-C-VqMtld.mjs.map} +1 -1
- package/dist/{drivers-DbjiYpW6.cjs → drivers-L29WvE95.cjs} +2 -2
- package/dist/{drivers-DbjiYpW6.cjs.map → drivers-L29WvE95.cjs.map} +1 -1
- package/dist/{eas-CN_cUXth.cjs → eas-D3clzkni.cjs} +2 -2
- package/dist/{eas-CN_cUXth.cjs.map → eas-D3clzkni.cjs.map} +1 -1
- package/dist/{eas-CBShP20v.mjs → eas-DaobzDks.mjs} +2 -2
- package/dist/{eas-CBShP20v.mjs.map → eas-DaobzDks.mjs.map} +1 -1
- package/dist/{emit-Dmc1nvdS.cjs → emit-CYloptRS.cjs} +34 -1
- package/dist/emit-CYloptRS.cjs.map +1 -0
- package/dist/{emit-C1e8qD08.mjs → emit-CiJGdy_f.mjs} +34 -1
- package/dist/emit-CiJGdy_f.mjs.map +1 -0
- package/dist/{encryption-CPDM49Hi.cjs → encryption-DBkIFiR2.cjs} +3 -3
- package/dist/{encryption-CPDM49Hi.cjs.map → encryption-DBkIFiR2.cjs.map} +1 -1
- package/dist/{encryption-CITa6dXL.mjs → encryption-DV31gEY8.mjs} +3 -3
- package/dist/{encryption-CITa6dXL.mjs.map → encryption-DV31gEY8.mjs.map} +1 -1
- package/dist/{engine-CTnHmHjz.mjs → engine-CFOR9h6X.mjs} +140 -16
- package/dist/engine-CFOR9h6X.mjs.map +1 -0
- package/dist/{engine-D-jfMVj-.cjs → engine-CuZUGbEh.cjs} +143 -19
- package/dist/engine-CuZUGbEh.cjs.map +1 -0
- package/dist/{github-D56PSK39.cjs → github-CsvtWSBW.cjs} +115 -47
- package/dist/github-CsvtWSBW.cjs.map +1 -0
- package/dist/{github-9RlA7EjN.mjs → github-Nep8pI4_.mjs} +113 -45
- package/dist/github-Nep8pI4_.mjs.map +1 -0
- package/dist/{googlePlay-SX_jQeJf.cjs → googlePlay-C-UfBXJU.cjs} +5 -5
- package/dist/{googlePlay-SX_jQeJf.cjs.map → googlePlay-C-UfBXJU.cjs.map} +1 -1
- package/dist/{googlePlay-COi5LXON.mjs → googlePlay-lF7Atm_N.mjs} +4 -4
- package/dist/{googlePlay-COi5LXON.mjs.map → googlePlay-lF7Atm_N.mjs.map} +1 -1
- package/dist/{index-DcyRA5Rf.d.mts → index-DrkV7N5w.d.mts} +2 -2
- package/dist/{index-DcyRA5Rf.d.mts.map → index-DrkV7N5w.d.mts.map} +1 -1
- package/dist/{index-DjLSMgAL.d.cts → index-yaXZ9m-J.d.cts} +2 -2
- package/dist/{index-DjLSMgAL.d.cts.map → index-yaXZ9m-J.d.cts.map} +1 -1
- package/dist/index.cjs +166 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +149 -68
- package/dist/index.mjs.map +1 -1
- package/dist/{local-BwC2v2Y8.mjs → local-B7Zv1NoA.mjs} +2 -2
- package/dist/{local-BwC2v2Y8.mjs.map → local-B7Zv1NoA.mjs.map} +1 -1
- package/dist/{local-DNhnkugT.cjs → local-OPC9BCZt.cjs} +3 -3
- package/dist/{local-DNhnkugT.cjs.map → local-OPC9BCZt.cjs.map} +1 -1
- package/dist/{migrate-CrtxXeGY.cjs → migrate-Dp3GhFnf.cjs} +4 -4
- package/dist/{migrate-CrtxXeGY.cjs.map → migrate-Dp3GhFnf.cjs.map} +1 -1
- package/dist/{migrate-0J6gsfrQ.mjs → migrate-NfbPVhi9.mjs} +4 -4
- package/dist/{migrate-0J6gsfrQ.mjs.map → migrate-NfbPVhi9.mjs.map} +1 -1
- package/dist/{migrate-DJa_Dlsl.mjs → migrate-_sBX0pve.mjs} +3 -3
- package/dist/{migrate-DJa_Dlsl.mjs.map → migrate-_sBX0pve.mjs.map} +1 -1
- package/dist/{migrate-BCz1lkh_.cjs → migrate-hlNKNWmg.cjs} +3 -3
- package/dist/{migrate-BCz1lkh_.cjs.map → migrate-hlNKNWmg.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{publish-CuQlOW5p.mjs → publish-BuUVaZVy.mjs} +2 -2
- package/dist/{publish-CuQlOW5p.mjs.map → publish-BuUVaZVy.mjs.map} +1 -1
- package/dist/{publish-qSpnvmdX.cjs → publish-ZOo4xEuZ.cjs} +2 -2
- package/dist/{publish-qSpnvmdX.cjs.map → publish-ZOo4xEuZ.cjs.map} +1 -1
- package/dist/{ready-BOfogqzr.mjs → ready-Ca883OvY.mjs} +6 -6
- package/dist/{ready-BOfogqzr.mjs.map → ready-Ca883OvY.mjs.map} +1 -1
- package/dist/{ready-BFFP838V.cjs → ready-qnqp6JAY.cjs} +6 -6
- package/dist/{ready-BFFP838V.cjs.map → ready-qnqp6JAY.cjs.map} +1 -1
- package/dist/reconcile/public.cjs +2 -2
- package/dist/reconcile/public.d.cts +1 -2
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +1 -2
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +2 -2
- package/dist/{resolve-j8hUcbGz.mjs → resolve-BCU9pYDx.mjs} +4 -4
- package/dist/{resolve-j8hUcbGz.mjs.map → resolve-BCU9pYDx.mjs.map} +1 -1
- package/dist/{resolve-AsfohXd1.cjs → resolve-C9QqOl5t.cjs} +4 -4
- package/dist/{resolve-AsfohXd1.cjs.map → resolve-C9QqOl5t.cjs.map} +1 -1
- package/dist/{rollback-Bxq_URgL.cjs → rollback-CTWdDBgB.cjs} +7 -7
- package/dist/{rollback-Bxq_URgL.cjs.map → rollback-CTWdDBgB.cjs.map} +1 -1
- package/dist/{rollback-JgbAY43C.mjs → rollback-CWyyTQ0B.mjs} +7 -7
- package/dist/{rollback-JgbAY43C.mjs.map → rollback-CWyyTQ0B.mjs.map} +1 -1
- package/dist/{runtime-DIZTeGJV.d.cts → runtime-DCkfI2E7.d.mts} +2 -2
- package/dist/{runtime-DIZTeGJV.d.cts.map → runtime-DCkfI2E7.d.mts.map} +1 -1
- package/dist/{runtime-WMLJY-db.d.mts → runtime-Ie4znWIB.d.cts} +2 -2
- package/dist/{runtime-WMLJY-db.d.mts.map → runtime-Ie4znWIB.d.cts.map} +1 -1
- package/dist/{s3-B4FTQQkV.mjs → s3-BZzSnker.mjs} +2 -2
- package/dist/{s3-B4FTQQkV.mjs.map → s3-BZzSnker.mjs.map} +1 -1
- package/dist/{s3-DwKLXwUT.cjs → s3-pqgmCTOq.cjs} +2 -2
- package/dist/{s3-DwKLXwUT.cjs.map → s3-pqgmCTOq.cjs.map} +1 -1
- package/dist/sandbox/credentials-worker.cjs +2 -2
- package/dist/sandbox/credentials-worker.mjs +2 -2
- package/dist/sandbox/discover-worker.cjs +1 -1
- package/dist/sandbox/discover-worker.mjs +1 -1
- package/dist/sandbox/migrate-worker.cjs +1 -1
- package/dist/sandbox/migrate-worker.mjs +1 -1
- package/dist/{seeds-Dv78P9LS.mjs → seeds-CK7w79VC.mjs} +848 -61
- package/dist/seeds-CK7w79VC.mjs.map +1 -0
- package/dist/{seeds-BWC-qjSI.cjs → seeds-CPEcmoPn.cjs} +953 -64
- package/dist/seeds-CPEcmoPn.cjs.map +1 -0
- package/dist/{serverSetup-DB6sKwAy.mjs → serverSetup-BAPNuNEX.mjs} +6 -6
- package/dist/{serverSetup-DB6sKwAy.mjs.map → serverSetup-BAPNuNEX.mjs.map} +1 -1
- package/dist/{serverSetup-CtV80a5M.cjs → serverSetup-C6t6Vtt7.cjs} +6 -6
- package/dist/{serverSetup-CtV80a5M.cjs.map → serverSetup-C6t6Vtt7.cjs.map} +1 -1
- package/dist/{sniffer-ctpwBBkT.cjs → sniffer-DdX_WN_-.cjs} +9 -9
- package/dist/{sniffer-ctpwBBkT.cjs.map → sniffer-DdX_WN_-.cjs.map} +1 -1
- package/dist/{sniffer-qDFUQmWP.mjs → sniffer-g9v7cqDN.mjs} +9 -9
- package/dist/{sniffer-qDFUQmWP.mjs.map → sniffer-g9v7cqDN.mjs.map} +1 -1
- package/dist/{dns-Bmnuttja.cjs → snsSubscriptions-5Cqww8S7.cjs} +351 -3
- package/dist/snsSubscriptions-5Cqww8S7.cjs.map +1 -0
- package/dist/{dns-DFvO5a53.mjs → snsSubscriptions-BmYeeTH8.mjs} +280 -4
- package/dist/snsSubscriptions-BmYeeTH8.mjs.map +1 -0
- package/dist/{sst-C1pxbFEd.mjs → sst-CTqZZl1_.mjs} +3 -4
- package/dist/{sst-C1pxbFEd.mjs.map → sst-CTqZZl1_.mjs.map} +1 -1
- package/dist/{sst-B-Uz4cvm.cjs → sst-lEkBwzGc.cjs} +4 -5
- package/dist/{sst-B-Uz4cvm.cjs.map → sst-lEkBwzGc.cjs.map} +1 -1
- package/dist/{stage-C7lf36az.mjs → stage-C-I2dVw1.mjs} +6 -6
- package/dist/{stage-C7lf36az.mjs.map → stage-C-I2dVw1.mjs.map} +1 -1
- package/dist/{stage-DzALYUwY.cjs → stage-CUE5pMmw.cjs} +7 -7
- package/dist/{stage-DzALYUwY.cjs.map → stage-CUE5pMmw.cjs.map} +1 -1
- package/dist/{stages-B7mCWI_R.cjs → stages-3yUZaZeA.cjs} +2 -2
- package/dist/{stages-B7mCWI_R.cjs.map → stages-3yUZaZeA.cjs.map} +1 -1
- package/dist/{stages-DSltTW2d.mjs → stages-DTTv0NZW.mjs} +2 -2
- package/dist/{stages-DSltTW2d.mjs.map → stages-DTTv0NZW.mjs.map} +1 -1
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +1 -1
- package/dist/{terminal-DJlqcW_U.cjs → terminal-PKhPAajN.cjs} +3 -3
- package/dist/{terminal-DJlqcW_U.cjs.map → terminal-PKhPAajN.cjs.map} +1 -1
- package/dist/{terminal-CW_dWeTe.mjs → terminal-bqY33lpN.mjs} +3 -3
- package/dist/{terminal-CW_dWeTe.mjs.map → terminal-bqY33lpN.mjs.map} +1 -1
- package/dist/{types-Q2SC_ly2.d.cts → types-Bmjs4jZV.d.mts} +248 -4
- package/dist/types-Bmjs4jZV.d.mts.map +1 -0
- package/dist/{types-C8gXOLWL.d.mts → types-D0IsQxES.d.cts} +249 -3
- package/dist/types-D0IsQxES.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-DUnnVjqC.mjs → workspace-DBHF9Gba.mjs} +5 -3
- package/dist/workspace-DBHF9Gba.mjs.map +1 -0
- package/dist/{workspace-C82GLPnf.cjs → workspace-Du4Afb03.cjs} +5 -3
- package/dist/workspace-Du4Afb03.cjs.map +1 -0
- package/package.json +17 -15
- package/dist/SubscriberGenerator-C8CPxrCK.cjs.map +0 -1
- package/dist/SubscriberGenerator-y3pncae_.mjs.map +0 -1
- package/dist/compose-ChVvb3xM.cjs.map +0 -1
- package/dist/compose-cHkF---f.mjs.map +0 -1
- package/dist/config-B3iz9BtK.d.cts.map +0 -1
- package/dist/config-DrOX29sT.d.mts.map +0 -1
- package/dist/credentials-B5N5yKk6.mjs.map +0 -1
- package/dist/credentials-Zqr3QQYB.cjs.map +0 -1
- package/dist/deploy-BffR_uEN.cjs.map +0 -1
- package/dist/deploy-DO4djCHb.mjs.map +0 -1
- package/dist/discover-C08meHhu.d.mts +0 -125
- package/dist/discover-C08meHhu.d.mts.map +0 -1
- package/dist/discover-C5roJuc9.mjs.map +0 -1
- package/dist/discover-DipvfWpK.d.cts +0 -125
- package/dist/discover-DipvfWpK.d.cts.map +0 -1
- package/dist/discover-DozUKs8f.cjs.map +0 -1
- package/dist/dns-Bmnuttja.cjs.map +0 -1
- package/dist/dns-DFvO5a53.mjs.map +0 -1
- package/dist/emit-C1e8qD08.mjs.map +0 -1
- package/dist/emit-Dmc1nvdS.cjs.map +0 -1
- package/dist/engine-CTnHmHjz.mjs.map +0 -1
- package/dist/engine-D-jfMVj-.cjs.map +0 -1
- package/dist/github-9RlA7EjN.mjs.map +0 -1
- package/dist/github-D56PSK39.cjs.map +0 -1
- package/dist/run-BJLIerMM.mjs +0 -133
- package/dist/run-BJLIerMM.mjs.map +0 -1
- package/dist/run-fVZ-rLnc.cjs +0 -174
- package/dist/run-fVZ-rLnc.cjs.map +0 -1
- package/dist/sandbox-Co1y2CmF.d.mts +0 -104
- package/dist/sandbox-Co1y2CmF.d.mts.map +0 -1
- package/dist/sandbox-DvNqr6od.d.cts +0 -104
- package/dist/sandbox-DvNqr6od.d.cts.map +0 -1
- package/dist/seeds-BWC-qjSI.cjs.map +0 -1
- package/dist/seeds-Dv78P9LS.mjs.map +0 -1
- package/dist/types-C8gXOLWL.d.mts.map +0 -1
- package/dist/types-Q2SC_ly2.d.cts.map +0 -1
- package/dist/workspace-C82GLPnf.cjs.map +0 -1
- package/dist/workspace-DUnnVjqC.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSMStateStore-qIzIuazA.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":";;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,MAAa,gBAAgB;;;;;AAQ7B,SAAgB,YAAY,MAAsB;CACjD,IAAI,OAAO,WAAW,IAAI,KAAA,MAAwB,OAAO;CACzD,IAAI,UAAU;CACd,IAAI;EACH,UAAU,KAAK,UAAU,KAAK,MAAM,IAAI,CAAC;CAC1C,QAAQ,CAER;CACA,OAAOA,gBAAAA,aAAa,OAAO;AAC5B;;AAGA,SAAS,aAAa,OAAyB;CAC9C,OAAQ,OAA6B,SAAS;AAC/C;AAYA,IAAa,gBAAb,MAAa,sBAAsBC,gBAAAA,mBAAmB;CAE3C;CACQ;CAER;CAJV,YACC,eACA,QAEA,SAAkB,YACjB;EACD,MAAM;EALG,KAAA,gBAAA;EACQ,KAAA,SAAA;EAER,KAAA,SAAA;CAGV;CAEA,OAAO,OAAO,SAA8C;EAC3D,MAAM,SAA0B;GAC/B,QAAQ,QAAQ;GAChB,UAAU,QAAQ;EACnB;EACA,IAAI,QAAQ,SAAS;GAEpB,MAAM,EAAE,YAAY,QAAQ,+BAA+B;GAC3D,OAAO,cAAc,QAAQ,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAC1D,OAAO,IAAI,QAAQ,aAClB,OAAO,cAAc,QAAQ;EAE9B,OAAO,IAAI,cACV,QAAQ,eACR,IAAIC,oBAAAA,UAAU,MAAM,GACpB,QAAQ,MACT;CACD;CAEA,KAAa,OAAe,MAAsB;EACjD,OAAO,QAAQ,KAAK,cAAc,GAAG,MAAM,GAAG;CAC/C;CAEA,SAAmB,OAAuB;EACzC,OAAO,OAAO,KAAK,KAAK,OAAO,OAAO;CACvC;CAEA,MAAgB,QAAQ,OAAyC;EAChE,MAAM,YAAY,MAAM,KAAK,IAAI,KAAK,KAAK,OAAO,OAAO,CAAC;EAC1D,IAAI,CAAC,WAAW,OAAO;EACvB,OAAO;GACN,MAAMC,gBAAAA,eAAe,UAAU,KAAK;GACpC,SAAS,OAAO,UAAU,OAAO;EAClC;CACD;;CAGA,MAAM,SAAS,OAA8C;EAC5D,MAAM,MAAM,MAAM,KAAK,QAAQ,KAAK;EACpC,IAAI,CAAC,KAAK,OAAO;EACjB,OAAO;GACN,OAAO,OAAO,WAAW,YAAY,IAAI,IAAI,CAAC;GAC9C,OAAO;GACP,UAAU,KAAK,SAAS,KAAK;GAC7B,QAAQ,KAAK;EACd;CACD;CAEA,MAAgB,SACf,OACA,UACA,iBACwB;EACxB,IAAI;GACH,OAAO,MAAM,KAAK,IAAI,OAAO,YAAY,QAAQ,GAAG,eAAe;EACpE,SAAS,OAAO;GACf,IAAI,CAAC,aAAa,KAAK,GAAG,MAAM;GAChC,MAAM,IAAIC,gBAAAA,mBACT,OACA,OAAO,WAAW,YAAY,QAAQ,CAAC,GACvC,KAAK,SAAS,KAAK,GACnB,KAAK,QACL,KACD;EACD;CACD;;CAGA,MAAc,IACb,OACA,MACA,iBACwB;EACxB,MAAM,OAAO,KAAK,KAAK,OAAO,OAAO;EACrC,MAAM,cAAc,4BAA4B,KAAK,cAAc,GAAG;EAEtE,IAAI,oBAAoB,MACvB,IAAI;GACH,MAAM,EAAE,YAAY,MAAM,KAAK,OAAO,KACrC,IAAIC,oBAAAA,oBAAoB;IACvB;IACA,OAAO;IACP,MAAM;IACN,MAAM;IACN;GACD,CAAC,CACF;GACA,OAAO,OAAO,OAAO;EACtB,SAAS,OAAO;GACf,IAAI,iBAAiBC,oBAAAA,wBAAwB;IAC5C,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI;IACnC,MAAM,IAAIC,gBAAAA,qBACT,OACA,MACA,UAAU,OAAO,QAAQ,OAAO,IAAI,IACrC;GACD;GACA,MAAM;EACP;EAGD,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI;EACnC,MAAM,SAAS,UAAU,OAAO,QAAQ,OAAO,IAAI;EACnD,IAAI,WAAW,iBACd,MAAM,IAAIA,gBAAAA,qBAAqB,OAAO,iBAAiB,MAAM;EAG9D,MAAM,EAAE,YAAY,MAAM,KAAK,OAAO,KACrC,IAAIF,oBAAAA,oBAAoB;GACvB;GACA,OAAO;GACP,MAAM;GACN,MAAM;GACN,WAAW;GACX;EACD,CAAC,CACF;EACA,IAAI,YAAY,OAAO,eAAe,IAAI,GAKzC,MAAM,IAAIE,gBAAAA,qBACT,OACA,iBACA,QAAQ,WAAW,KAAK,CAAC,CAC1B;EAQD,MAAM,SAAS,MAAM,KAAK,IAAI,IAAI;EAClC,IAAI,UAAU,OAAO,YAAY,WAAW,OAAO,UAAU,MAC5D,MAAM,IAAIA,gBAAAA,qBAAqB,OAAO,iBAAiB,OAAO,OAAO,CAAC;EAEvE,OAAO,OAAO,OAAO;CACtB;CAEA,MAAgB,cAAc,OAAe,MAA6B;EACzE,IAAI;GACH,MAAM,KAAK,OAAO,KACjB,IAAIF,oBAAAA,oBAAoB;IACvB,MAAM,KAAK,KAAK,OAAO,UAAU;IAGjC,OAAO,YAAY,IAAI;IACvB,MAAM;IACN,MAAM;IACN,aAAa,+BAA+B,KAAK,cAAc,GAAG,MAAM;GACzE,CAAC,CACF;EACD,SAAS,OAAO;GAEf,IAAI,EAAE,iBAAiBC,oBAAAA,yBAAyB,MAAM;EACvD;CACD;CAEA,MAAgB,WAAW,OAAe,QAAmC;EAC5E,MAAM,OAAO,KAAK,KAAK,OAAO,MAAM;EACpC,IAAI;GACH,MAAM,KAAK,OAAO,KACjB,IAAID,oBAAAA,oBAAoB;IACvB;IACA,OAAO,KAAK,UAAU,MAAM;IAE5B,MAAM;IACN,aAAa,uBAAuB,KAAK,cAAc,GAAG;GAC3D,CAAC,CACF;EACD,SAAS,OAAO;GACf,IAAI,iBAAiBC,oBAAAA,wBACpB,MAAM,IAAIE,gBAAAA,YAAY,OAAO,MAAM,KAAK,SAAS,KAAK,GAAG,OAAO,MAAM;GAEvE,MAAM;EACP;EAOA,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI;EACnC,MAAM,WAAW,UAAU,YAAY,QAAQ,KAAK,IAAI;EACxD,IAAI,SAAS,YAAY,KAAK,UAAU,OAAO,OAAO,IACrD,MAAM,IAAIA,gBAAAA,YAAY,OAAO,UAAU,OAAO,MAAM;CAEtD;CAEA,MAAgB,SAAS,OAA2C;EACnE,MAAM,YAAY,MAAM,KAAK,IAAI,KAAK,KAAK,OAAO,MAAM,CAAC;EACzD,OAAO,YAAY,YAAY,UAAU,KAAK,IAAI;CACnD;CAEA,MAAgB,WACf,OACA,QACgB;EAChB,IAAI,QAEC;QAAA,MADkB,KAAK,SAAS,KAAK,EAAA,EAC5B,OAAO,OAAO,IAAI;EAAA;EAEhC,IAAI;GACH,MAAM,KAAK,OAAO,KACjB,IAAIC,oBAAAA,uBAAuB,EAAE,MAAM,KAAK,KAAK,OAAO,MAAM,EAAE,CAAC,CAC9D;EACD,SAAS,OAAO;GACf,IAAI,EAAE,iBAAiBC,oBAAAA,oBAAoB,MAAM;EAClD;CACD;CAEA,MAAc,IACb,MACqD;EACrD,IAAI;GACH,MAAM,EAAE,cAAc,MAAM,KAAK,OAAO,KACvC,IAAIC,oBAAAA,oBAAoB;IAAE;IAAM,gBAAgB;GAAK,CAAC,CACvD;GACA,IAAI,WAAW,UAAU,KAAA,GAAW,OAAO;GAC3C,OAAO;IAAE,OAAO,UAAU;IAAO,SAAS,UAAU,WAAW;GAAE;EAClE,SAAS,OAAO;GACf,IAAI,iBAAiBD,oBAAAA,mBAAmB,OAAO;GAC/C,MAAM;EACP;CACD;AACD;AAEA,SAAS,YAAY,OAAkC;CACtD,IAAI;EACH,OAAO,KAAK,MAAM,KAAK;CACxB,QAAQ;EACP,OAAO;CACR;AACD"}
|
|
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,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-
|
|
2
|
+
import { C as decompressBody, b as compressBody, f as StateLocked, g as StateVersionConflict, l as DocumentStateStore, p as StateRejectedBySsm } from "./sniffer-g9v7cqDN.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-
|
|
205
|
+
//# sourceMappingURL=SSMStateStore-DM85OiH0.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSMStateStore-j5RuMkch.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-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,6 +1,6 @@
|
|
|
1
1
|
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
2
|
-
const require_EndpointGenerator = require("./EndpointGenerator-
|
|
3
|
-
const require_discover = require("./discover-
|
|
2
|
+
const require_EndpointGenerator = require("./EndpointGenerator-BGxf9jWE.cjs");
|
|
3
|
+
const require_discover = require("./discover-DL6h-J97.cjs");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
@@ -64,6 +64,38 @@ const DEFAULT_STORAGE = {
|
|
|
64
64
|
server: "minio"
|
|
65
65
|
};
|
|
66
66
|
//#endregion
|
|
67
|
+
//#region src/generators/subscriberRoute.ts
|
|
68
|
+
/**
|
|
69
|
+
* Where SNS pushes a topic subscriber's messages, on the server that runs it:
|
|
70
|
+
* named after the subscriber's export.
|
|
71
|
+
*
|
|
72
|
+
* One definition for both sides — the generated server mounts the route, and
|
|
73
|
+
* the stack exposes exactly these paths on a worker's host — so the path the
|
|
74
|
+
* edge lets through and the path the worker answers cannot drift.
|
|
75
|
+
*/
|
|
76
|
+
const SUBSCRIBER_ROUTE_PREFIX = "/__gkm/subscribers/";
|
|
77
|
+
/** The route a subscriber's pushes arrive on. */
|
|
78
|
+
function subscriberRoute(exportName) {
|
|
79
|
+
return `${SUBSCRIBER_ROUTE_PREFIX}${exportName}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Where the deploy hands a server the SNS subscriptions it made for its
|
|
83
|
+
* routes: a JSON object of subscriber export → subscription ARN. Against SNS
|
|
84
|
+
* itself a server mounts a route only for a subscription recorded here, and
|
|
85
|
+
* refuses a notification that names another.
|
|
86
|
+
*/
|
|
87
|
+
const SNS_SUBSCRIPTIONS_KEY = "GKM_SNS_SUBSCRIPTIONS";
|
|
88
|
+
/**
|
|
89
|
+
* Where a local object store posts a bucket's events, on the process that runs
|
|
90
|
+
* the bucket's subscribers: named after the bucket. Never on a public host —
|
|
91
|
+
* MinIO reaches it over the stack's network, or the host's.
|
|
92
|
+
*/
|
|
93
|
+
const OBJECTS_ROUTE_PREFIX = "/__gkm/objects/";
|
|
94
|
+
/** The route a bucket's events are posted to. */
|
|
95
|
+
function objectsRoute(bucketId) {
|
|
96
|
+
return `${OBJECTS_ROUTE_PREFIX}${bucketId}`;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
67
99
|
//#region src/reconcile/fakes.ts
|
|
68
100
|
/**
|
|
69
101
|
* Fakes, read from where the convention puts them.
|
|
@@ -209,21 +241,6 @@ var NotAFake = class extends require_errors.GkmError {
|
|
|
209
241
|
}
|
|
210
242
|
};
|
|
211
243
|
//#endregion
|
|
212
|
-
//#region src/generators/subscriberRoute.ts
|
|
213
|
-
/**
|
|
214
|
-
* Where SNS pushes a topic subscriber's messages, on the server that runs it:
|
|
215
|
-
* named after the subscriber's export.
|
|
216
|
-
*
|
|
217
|
-
* One definition for both sides — the generated server mounts the route, and
|
|
218
|
-
* the stack exposes exactly these paths on a worker's host — so the path the
|
|
219
|
-
* edge lets through and the path the worker answers cannot drift.
|
|
220
|
-
*/
|
|
221
|
-
const SUBSCRIBER_ROUTE_PREFIX = "/__gkm/subscribers/";
|
|
222
|
-
/** The route a subscriber's pushes arrive on. */
|
|
223
|
-
function subscriberRoute(exportName) {
|
|
224
|
-
return `${SUBSCRIBER_ROUTE_PREFIX}${exportName}`;
|
|
225
|
-
}
|
|
226
|
-
//#endregion
|
|
227
244
|
//#region src/generators/SubscriberGenerator.ts
|
|
228
245
|
var SubscriberGenerator = class extends require_discover.ConstructGenerator {
|
|
229
246
|
isConstruct(value) {
|
|
@@ -233,8 +250,9 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
|
|
|
233
250
|
const target = options?.target ?? "aws";
|
|
234
251
|
const logger = console;
|
|
235
252
|
const subscriberInfos = [];
|
|
253
|
+
for (const { key, construct } of constructs) assertDoesNotFeedItself(key, construct);
|
|
236
254
|
if (target === "server") {
|
|
237
|
-
await this.generateServerSubscribersFile(outputDir, constructs, context.eventsBackends?.includes("sns") ?? false);
|
|
255
|
+
await this.generateServerSubscribersFile(outputDir, constructs, (context.eventsBackends?.includes("sns") ?? false) || constructs.some(({ construct }) => construct.bucketName));
|
|
238
256
|
logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
|
|
239
257
|
return subscriberInfos;
|
|
240
258
|
}
|
|
@@ -251,6 +269,11 @@ var SubscriberGenerator = class extends require_discover.ConstructGenerator {
|
|
|
251
269
|
transport: "topic",
|
|
252
270
|
topic: construct.topicName
|
|
253
271
|
} : {},
|
|
272
|
+
...construct.bucketName ? {
|
|
273
|
+
transport: "topic",
|
|
274
|
+
bucket: construct.bucketName,
|
|
275
|
+
...construct.objectFilter
|
|
276
|
+
} : {},
|
|
254
277
|
timeout: construct.timeout,
|
|
255
278
|
memorySize: construct.memorySize,
|
|
256
279
|
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
@@ -309,6 +332,14 @@ export async function stopSubscribers(): Promise<void> {}
|
|
|
309
332
|
const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
|
|
310
333
|
importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
|
|
311
334
|
}
|
|
335
|
+
const imports = Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n");
|
|
336
|
+
const entries = subscribers.map(({ key, construct }) => {
|
|
337
|
+
const connectionKey = require_discover.subscriberConnectionKey(construct);
|
|
338
|
+
const tokenKey = require_discover.subscriberTokenKey(construct);
|
|
339
|
+
const topic = construct.topicName ?? construct.bucketName;
|
|
340
|
+
const bucket = construct.bucketName ? (0, _geekmidas_manifest.canonicalId)(construct.bucketName) : void 0;
|
|
341
|
+
return ` { id: '${key}', subscriber: ${key}, topic: ${topic ? `'${topic}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"}, filtered: ${construct.bucketName ? "false" : "true"}, bucket: ${bucket ? `'${bucket}'` : "undefined"}, tokenKey: ${tokenKey ? `'${tokenKey}'` : "undefined"} },`;
|
|
342
|
+
}).join("\n");
|
|
312
343
|
const content = `/**
|
|
313
344
|
* Generated subscribers setup.
|
|
314
345
|
*
|
|
@@ -316,30 +347,38 @@ export async function stopSubscribers(): Promise<void> {}
|
|
|
316
347
|
* topic's own connection string:
|
|
317
348
|
*
|
|
318
349
|
* - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
|
|
319
|
-
* server,
|
|
320
|
-
*
|
|
321
|
-
*
|
|
350
|
+
* server, subscribed to the topic with a filter policy of the events it
|
|
351
|
+
* named, so SNS fans out to each subscriber. The route runs the subscriber
|
|
352
|
+
* through the same adaptor a Lambda subscription does. Against SNS itself
|
|
353
|
+
* the deploy made the subscription and recorded its ARN in
|
|
354
|
+
* ${SNS_SUBSCRIPTIONS_KEY}: a route is mounted only for a recorded one, its
|
|
355
|
+
* confirmation goes through the API, and nothing is subscribed from here.
|
|
356
|
+
* Against an emulator, where nothing deploys, the route subscribes itself.
|
|
322
357
|
* - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
|
|
323
358
|
* each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
|
|
324
359
|
* every subscriber sees every event; replicas of one subscriber share it.
|
|
325
360
|
*
|
|
361
|
+
* A bucket's subscriber is polled the same way, under the bucket's name, when
|
|
362
|
+
* the bucket is in a local object store: the store posts each event to
|
|
363
|
+
* \`${OBJECTS_ROUTE_PREFIX}<Bucket>\` on this server, with the bucket's token, and
|
|
364
|
+
* the event is relayed onto the broker as \`object.created\` or
|
|
365
|
+
* \`object.deleted\` — so every subscriber of the bucket sees it.
|
|
366
|
+
*
|
|
326
367
|
* GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
|
|
327
|
-
* GKM_SUBSCRIBER_PUSH_URL is the address
|
|
328
|
-
*
|
|
368
|
+
* GKM_SUBSCRIBER_PUSH_URL is the address an emulator pushes to, defaulting to
|
|
369
|
+
* this server through host.docker.internal.
|
|
329
370
|
*/
|
|
371
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
330
372
|
import { connect } from 'node:net';
|
|
331
373
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
332
374
|
import type { Logger } from '@geekmidas/logger';
|
|
333
|
-
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
375
|
+
import { EventConnectionFactory, Publisher, Subscriber } from '@geekmidas/events';
|
|
334
376
|
import type { EventConnection } from '@geekmidas/events';
|
|
335
377
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
336
|
-
${
|
|
378
|
+
${imports}
|
|
337
379
|
|
|
338
380
|
const subscribers = [
|
|
339
|
-
${
|
|
340
|
-
const connectionKey = construct.topicName ? (0, _geekmidas_manifest.provideKey)((0, _geekmidas_manifest.canonicalId)(construct.topicName), "publisherConnectionString") : void 0;
|
|
341
|
-
return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
|
|
342
|
-
}).join("\n")}
|
|
381
|
+
${entries}
|
|
343
382
|
];
|
|
344
383
|
|
|
345
384
|
// What was opened, and what could not be: a worker's health check reads both,
|
|
@@ -347,9 +386,84 @@ ${subscribers.map(({ key, construct }) => {
|
|
|
347
386
|
const connections = new Map<string, EventConnection>();
|
|
348
387
|
const failed: string[] = [];
|
|
349
388
|
|
|
389
|
+
/**
|
|
390
|
+
* The subscriptions the deploy made against SNS itself, by subscriber: which
|
|
391
|
+
* routes to mount, and the ARN each one's notifications must name.
|
|
392
|
+
*/
|
|
393
|
+
function recordedSubscriptions(logger: Logger): Record<string, string> {
|
|
394
|
+
const raw = process.env.${SNS_SUBSCRIPTIONS_KEY};
|
|
395
|
+
if (!raw) return {};
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(raw);
|
|
398
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
399
|
+
} catch {}
|
|
400
|
+
logger.error('${SNS_SUBSCRIPTIONS_KEY} is not a JSON object of subscriber → subscription ARN');
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
|
|
350
404
|
/** The route a subscriber's pushes arrive on. */
|
|
351
405
|
export const SUBSCRIBER_ROUTE = (id: string) => \`${SUBSCRIBER_ROUTE_PREFIX}\${id}\`;
|
|
352
406
|
|
|
407
|
+
/** The route a local object store posts a bucket's events to. */
|
|
408
|
+
export const OBJECTS_ROUTE = (bucket: string) => \`${OBJECTS_ROUTE_PREFIX}\${bucket}\`;
|
|
409
|
+
|
|
410
|
+
// The buckets whose events this server relays: one route each, however many
|
|
411
|
+
// subscribers watch the bucket.
|
|
412
|
+
const relays = new Set<string>();
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Mount the route a local object store posts one bucket's events to, and
|
|
416
|
+
* relay each event onto the broker under the bucket's name.
|
|
417
|
+
*
|
|
418
|
+
* Answered 2xx only once every event is published: the store keeps what it
|
|
419
|
+
* was not told it delivered, and sends it again.
|
|
420
|
+
*/
|
|
421
|
+
async function mountObjectRelay(
|
|
422
|
+
app: { post: (path: string, handler: (c: any) => unknown) => unknown },
|
|
423
|
+
relay: { bucket: string; topic: string; connection: EventConnection; tokenKey: string },
|
|
424
|
+
logger: Logger,
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
const token = process.env[relay.tokenKey];
|
|
427
|
+
if (!token) {
|
|
428
|
+
logger.error(
|
|
429
|
+
{ bucket: relay.bucket, tokenKey: relay.tokenKey },
|
|
430
|
+
'No token for this bucket: the events its object store posts cannot be received',
|
|
431
|
+
);
|
|
432
|
+
failed.push(\`\${relay.bucket}#events\`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const { toObjectEvents } = await import('@geekmidas/storage/events');
|
|
436
|
+
const publisher = await Publisher.fromConnection(relay.connection, { topic: relay.topic });
|
|
437
|
+
const expected = Buffer.from(\`Bearer \${token}\`);
|
|
438
|
+
|
|
439
|
+
app.post(OBJECTS_ROUTE(relay.bucket), async (c: any) => {
|
|
440
|
+
// Compared in constant time: the route is not public, but it is the one
|
|
441
|
+
// way to put an event in front of every subscriber of the bucket.
|
|
442
|
+
const given = Buffer.from(c.req.header('authorization') ?? '');
|
|
443
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
|
|
444
|
+
return c.json({ status: 'refused' }, 401);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let messages: Awaited<ReturnType<typeof toObjectEvents>>;
|
|
448
|
+
try {
|
|
449
|
+
messages = toObjectEvents(JSON.parse(await c.req.text()));
|
|
450
|
+
} catch (error) {
|
|
451
|
+
logger.warn({ err: error, bucket: relay.bucket }, 'Refused an object notification that did not parse');
|
|
452
|
+
return c.json({ status: 'malformed' }, 400);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
if (messages.length > 0) await publisher.publish(messages as any);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// The store sends it again.
|
|
459
|
+
logger.error({ err: error, bucket: relay.bucket }, 'Failed to relay object events');
|
|
460
|
+
return c.json({ status: 'failed' }, 500);
|
|
461
|
+
}
|
|
462
|
+
return c.json({ status: 'ok', relayed: messages.length }, 200);
|
|
463
|
+
});
|
|
464
|
+
logger.info({ bucket: relay.bucket }, 'Relaying object events');
|
|
465
|
+
}
|
|
466
|
+
|
|
353
467
|
/** Resolves once something is accepting connections on the port. */
|
|
354
468
|
async function listening(port: number): Promise<void> {
|
|
355
469
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
@@ -379,9 +493,10 @@ export async function setupSubscribers(
|
|
|
379
493
|
}
|
|
380
494
|
|
|
381
495
|
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
496
|
+
const recorded = recordedSubscriptions(logger);
|
|
382
497
|
const afterListening: ((port: number) => Promise<void>)[] = [];
|
|
383
498
|
|
|
384
|
-
for (const { id, subscriber, topic, connectionKey } of subscribers) {
|
|
499
|
+
for (const { id, subscriber, topic, connectionKey, filtered, bucket, tokenKey } of subscribers) {
|
|
385
500
|
const events = subscriber.subscribedEvents || [];
|
|
386
501
|
if (events.length === 0) {
|
|
387
502
|
logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
|
|
@@ -391,7 +506,7 @@ export async function setupSubscribers(
|
|
|
391
506
|
if (!connectionString) {
|
|
392
507
|
logger.error(
|
|
393
508
|
{ subscriber: id, connectionKey },
|
|
394
|
-
'No connection string for the topic this subscriber is bound to',
|
|
509
|
+
'No connection string for the topic or bucket this subscriber is bound to',
|
|
395
510
|
);
|
|
396
511
|
failed.push(id);
|
|
397
512
|
continue;
|
|
@@ -399,39 +514,65 @@ export async function setupSubscribers(
|
|
|
399
514
|
|
|
400
515
|
try {
|
|
401
516
|
${push ? ` if (connectionString.startsWith('sns:')) {
|
|
402
|
-
const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
|
|
517
|
+
const { SNSConnection, snsUrl, subscribeHttpEndpoint, confirmSubscription } = await import('@geekmidas/events/sns');
|
|
403
518
|
const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/subscribers');
|
|
404
519
|
const address = snsUrl.parse(connectionString);
|
|
405
520
|
const route = SUBSCRIBER_ROUTE(id);
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
app.post(route, async (c: any) => {
|
|
415
|
-
const body = JSON.parse(await c.req.text());
|
|
416
|
-
const response = await adaptor.handle(body);
|
|
417
|
-
return c.json(response.body, response.status);
|
|
418
|
-
});
|
|
521
|
+
const mount = (adaptor: InstanceType<typeof SnsPushSubscriberAdaptor>) =>
|
|
522
|
+
app.post(route, async (c: any) => {
|
|
523
|
+
const body = JSON.parse(await c.req.text());
|
|
524
|
+
const response = await adaptor.handle(body, {
|
|
525
|
+
subscriptionArn: c.req.header('x-amz-sns-subscription-arn'),
|
|
526
|
+
});
|
|
527
|
+
return c.json(response.body, response.status);
|
|
528
|
+
});
|
|
419
529
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
530
|
+
if (!address.endpoint) {
|
|
531
|
+
// SNS itself: the deploy subscribed this route and recorded the ARN.
|
|
532
|
+
// Nothing is subscribed from here, so this server's credentials need
|
|
533
|
+
// only sns:ConfirmSubscription on the topic.
|
|
534
|
+
const subscriptionArn = recorded[id];
|
|
535
|
+
if (!subscriptionArn) {
|
|
425
536
|
logger.error(
|
|
426
537
|
{ subscriber: id },
|
|
427
|
-
'
|
|
538
|
+
'No subscription recorded for this subscriber in ${SNS_SUBSCRIPTIONS_KEY}: deploy it so SNS has a route to push to',
|
|
428
539
|
);
|
|
429
|
-
|
|
540
|
+
failed.push(id);
|
|
541
|
+
continue;
|
|
430
542
|
}
|
|
431
543
|
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
544
|
+
mount(
|
|
545
|
+
new SnsPushSubscriberAdaptor(envParser, subscriber, {
|
|
546
|
+
topicArn: address.topicArn,
|
|
547
|
+
verify: true,
|
|
548
|
+
subscriptionArn,
|
|
549
|
+
// As the topic's owner, so only the owner can unsubscribe the
|
|
550
|
+
// route — not whoever holds a notification's UnsubscribeURL.
|
|
551
|
+
confirm: (message) => confirmSubscription(connection, message),
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
logger.info({ subscriber: id, events }, 'Subscriber mounted for push');
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// An emulator signs nothing, and nothing deploys its subscriptions:
|
|
559
|
+
// decided by the address this server was configured with, never by
|
|
560
|
+
// anything a request says about itself.
|
|
561
|
+
mount(
|
|
562
|
+
new SnsPushSubscriberAdaptor(envParser, subscriber, {
|
|
563
|
+
topicArn: address.topicArn,
|
|
564
|
+
verify: false,
|
|
565
|
+
emulatorEndpoint: address.endpoint,
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
afterListening.push(async (port) => {
|
|
570
|
+
const base =
|
|
571
|
+
process.env.GKM_SUBSCRIBER_PUSH_URL ?? \`http://host.docker.internal:\${port}\`;
|
|
572
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
432
573
|
await subscribeHttpEndpoint(connection, {
|
|
433
574
|
endpoint: new URL(route, base).toString(),
|
|
434
|
-
events,
|
|
575
|
+
...(filtered ? { events } : {}),
|
|
435
576
|
});
|
|
436
577
|
logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
|
|
437
578
|
});
|
|
@@ -487,6 +628,13 @@ ${push ? ` if (connectionString.startsWith('sns:')) {
|
|
|
487
628
|
}
|
|
488
629
|
});
|
|
489
630
|
logger.info({ subscriber: id, events }, 'Subscriber started polling');
|
|
631
|
+
|
|
632
|
+
// A bucket in a local object store publishes nothing for this to poll:
|
|
633
|
+
// the store posts its events here, and they are relayed onto the broker.
|
|
634
|
+
if (bucket && tokenKey && topic && !relays.has(bucket)) {
|
|
635
|
+
relays.add(bucket);
|
|
636
|
+
await mountObjectRelay(app, { bucket, topic, connection, tokenKey }, logger);
|
|
637
|
+
}
|
|
490
638
|
} catch (error) {
|
|
491
639
|
// The message too: a missing module's code alone does not say which.
|
|
492
640
|
logger.error(
|
|
@@ -527,6 +675,29 @@ export async function stopSubscribers(): Promise<void> {
|
|
|
527
675
|
return subscribersPath;
|
|
528
676
|
}
|
|
529
677
|
};
|
|
678
|
+
/**
|
|
679
|
+
* A bucket's subscriber that writes back into the keys it watches.
|
|
680
|
+
*
|
|
681
|
+
* Every write it makes is a new object event, which runs it again — forever,
|
|
682
|
+
* and billed per run. The rule is the one the build can check: a subscriber
|
|
683
|
+
* that depends on the bucket it watches must watch a prefix, so what it writes
|
|
684
|
+
* can go elsewhere. A thumbnailer watching `uploads/` writes `thumbnails/`.
|
|
685
|
+
*/
|
|
686
|
+
var ObjectSubscriberFeedsItself = class extends Error {
|
|
687
|
+
subscriber;
|
|
688
|
+
bucket;
|
|
689
|
+
constructor(subscriber, bucket) {
|
|
690
|
+
super(`'${subscriber}' watches every key in '${bucket}' and depends on it, so each object it writes runs it again. Watch a prefix — .on([...], { prefix: 'uploads/' }) — and write outside it.`);
|
|
691
|
+
this.subscriber = subscriber;
|
|
692
|
+
this.bucket = bucket;
|
|
693
|
+
this.name = "ObjectSubscriberFeedsItself";
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
function assertDoesNotFeedItself(key, construct) {
|
|
697
|
+
const bucket = construct.bucketName;
|
|
698
|
+
if (!bucket || construct.objectFilter?.prefix) return;
|
|
699
|
+
if ((construct.constructs ?? []).some((id) => (0, _geekmidas_manifest.canonicalId)(id) === (0, _geekmidas_manifest.canonicalId)(bucket))) throw new ObjectSubscriberFeedsItself(key, bucket);
|
|
700
|
+
}
|
|
530
701
|
//#endregion
|
|
531
702
|
Object.defineProperty(exports, "DEFAULT_CACHE", {
|
|
532
703
|
enumerable: true,
|
|
@@ -546,6 +717,12 @@ Object.defineProperty(exports, "DEFAULT_STORAGE", {
|
|
|
546
717
|
return DEFAULT_STORAGE;
|
|
547
718
|
}
|
|
548
719
|
});
|
|
720
|
+
Object.defineProperty(exports, "SNS_SUBSCRIPTIONS_KEY", {
|
|
721
|
+
enumerable: true,
|
|
722
|
+
get: function() {
|
|
723
|
+
return SNS_SUBSCRIPTIONS_KEY;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
549
726
|
Object.defineProperty(exports, "SubscriberGenerator", {
|
|
550
727
|
enumerable: true,
|
|
551
728
|
get: function() {
|
|
@@ -570,6 +747,12 @@ Object.defineProperty(exports, "isCacheBackend", {
|
|
|
570
747
|
return isCacheBackend;
|
|
571
748
|
}
|
|
572
749
|
});
|
|
750
|
+
Object.defineProperty(exports, "objectsRoute", {
|
|
751
|
+
enumerable: true,
|
|
752
|
+
get: function() {
|
|
753
|
+
return objectsRoute;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
573
756
|
Object.defineProperty(exports, "readCredentialFakes", {
|
|
574
757
|
enumerable: true,
|
|
575
758
|
get: function() {
|
|
@@ -589,4 +772,4 @@ Object.defineProperty(exports, "subscriberRoute", {
|
|
|
589
772
|
}
|
|
590
773
|
});
|
|
591
774
|
|
|
592
|
-
//# sourceMappingURL=SubscriberGenerator-
|
|
775
|
+
//# sourceMappingURL=SubscriberGenerator-BDUp7OeY.cjs.map
|