@geekmidas/cli 10.0.0-alpha.122 → 10.0.0-alpha.124
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-B2MUa4M2.cjs} +2 -2
- package/dist/{EndpointGenerator-BDa6pPgE.cjs.map → EndpointGenerator-B2MUa4M2.cjs.map} +1 -1
- package/dist/{EndpointGenerator-CP_z_E3e.mjs → EndpointGenerator-Z_BUTocT.mjs} +2 -2
- package/dist/{EndpointGenerator-CP_z_E3e.mjs.map → EndpointGenerator-Z_BUTocT.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-BJ9j9mR7.cjs → GoDaddyProvider-0z5RAN1k.cjs} +3 -3
- package/dist/{GoDaddyProvider-BJ9j9mR7.cjs.map → GoDaddyProvider-0z5RAN1k.cjs.map} +1 -1
- package/dist/{GoDaddyProvider-wIKYdm7V.mjs → GoDaddyProvider-55x4o-4z.mjs} +3 -3
- package/dist/{GoDaddyProvider-wIKYdm7V.mjs.map → GoDaddyProvider-55x4o-4z.mjs.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-CkXUJP7i.mjs → LegacyStateStore-C-vumKSN.mjs} +3 -3
- package/dist/{LegacyStateStore-CkXUJP7i.mjs.map → LegacyStateStore-C-vumKSN.mjs.map} +1 -1
- package/dist/{LegacyStateStore-D1S87I_B.cjs → LegacyStateStore-DRpB-yC5.cjs} +3 -3
- package/dist/{LegacyStateStore-D1S87I_B.cjs.map → LegacyStateStore-DRpB-yC5.cjs.map} +1 -1
- package/dist/{LocalStateStore-DmXfpvu4.cjs → LocalStateStore-C3ummuDT.cjs} +2 -2
- package/dist/{LocalStateStore-DmXfpvu4.cjs.map → LocalStateStore-C3ummuDT.cjs.map} +1 -1
- package/dist/{LocalStateStore-BWoWeSq0.mjs → LocalStateStore-CtOjpV1w.mjs} +2 -2
- package/dist/{LocalStateStore-BWoWeSq0.mjs.map → LocalStateStore-CtOjpV1w.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-CsrukCE-.mjs → OpenApiTsGenerator-MtnMQL_g.mjs} +2 -2
- package/dist/{OpenApiTsGenerator-CsrukCE-.mjs.map → OpenApiTsGenerator-MtnMQL_g.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-FNhY7Ssz.cjs → OpenApiTsGenerator-t3_E98Fi.cjs} +2 -2
- package/dist/{OpenApiTsGenerator-FNhY7Ssz.cjs.map → OpenApiTsGenerator-t3_E98Fi.cjs.map} +1 -1
- package/dist/{S3StateStore-D19JSGE9.mjs → S3StateStore-BC4Jm3Tw.mjs} +2 -2
- package/dist/{S3StateStore-D19JSGE9.mjs.map → S3StateStore-BC4Jm3Tw.mjs.map} +1 -1
- package/dist/{S3StateStore-BEdsjjP4.cjs → S3StateStore-BeArp0aF.cjs} +2 -2
- package/dist/{S3StateStore-BEdsjjP4.cjs.map → S3StateStore-BeArp0aF.cjs.map} +1 -1
- package/dist/{SSMStateStore-7xKw0Zz9.mjs → SSMStateStore-Crl76aYZ.mjs} +2 -2
- package/dist/{SSMStateStore-7xKw0Zz9.mjs.map → SSMStateStore-Crl76aYZ.mjs.map} +1 -1
- package/dist/{SSMStateStore--2bcxoiH.cjs → SSMStateStore-DcstR6cW.cjs} +2 -2
- package/dist/{SSMStateStore--2bcxoiH.cjs.map → SSMStateStore-DcstR6cW.cjs.map} +1 -1
- package/dist/{SubscriberGenerator-C8CPxrCK.cjs → SubscriberGenerator-C3QTae-R.cjs} +237 -54
- package/dist/SubscriberGenerator-C3QTae-R.cjs.map +1 -0
- package/dist/{SubscriberGenerator-y3pncae_.mjs → SubscriberGenerator-COAVPgM-.mjs} +227 -56
- package/dist/SubscriberGenerator-COAVPgM-.mjs.map +1 -0
- package/dist/{add-DlFUKdjX.cjs → add-BFjb6mMq.cjs} +7 -7
- package/dist/{add-DlFUKdjX.cjs.map → add-BFjb6mMq.cjs.map} +1 -1
- package/dist/{add-Bu6FkaTn.mjs → add-CIjmgzkb.mjs} +6 -6
- package/dist/{add-Bu6FkaTn.mjs.map → add-CIjmgzkb.mjs.map} +1 -1
- package/dist/{bundler-Bg0lPDoi.cjs → bundler-CiUoIsku.cjs} +5 -5
- package/dist/{bundler-Bg0lPDoi.cjs.map → bundler-CiUoIsku.cjs.map} +1 -1
- package/dist/{bundler-CgE5XTyr.mjs → bundler-D6KFNpQk.mjs} +5 -5
- package/dist/{bundler-CgE5XTyr.mjs.map → bundler-D6KFNpQk.mjs.map} +1 -1
- package/dist/{cli-z5S3bHyK.mjs → cli-DNtSTtMc.mjs} +4 -4
- package/dist/{cli-z5S3bHyK.mjs.map → cli-DNtSTtMc.mjs.map} +1 -1
- package/dist/{cli-Cvjlnpon.cjs → cli-r4eUr98C.cjs} +4 -4
- package/dist/{cli-Cvjlnpon.cjs.map → cli-r4eUr98C.cjs.map} +1 -1
- package/dist/{compose-bKLAtHLj.cjs → compose-BfXXWJSp.cjs} +362 -59
- package/dist/compose-BfXXWJSp.cjs.map +1 -0
- package/dist/{compose-CgBPA44v.mjs → compose-DJvvmF3X.mjs} +336 -33
- package/dist/compose-DJvvmF3X.mjs.map +1 -0
- package/dist/{config-D48erO2j.d.mts → config-IgK6YaGp.d.cts} +3 -4
- package/dist/config-IgK6YaGp.d.cts.map +1 -0
- package/dist/{config-DHDtsQrB.d.cts → config-kE-p5hxq.d.mts} +3 -4
- package/dist/config-kE-p5hxq.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-AhIBuQxQ.cjs} +2 -2
- package/dist/{credentialSchemas-B67WJOOm.cjs.map → credentialSchemas-AhIBuQxQ.cjs.map} +1 -1
- package/dist/{credentialSchemas-DDOLRCLF.mjs → credentialSchemas-DLKbxYBy.mjs} +2 -2
- package/dist/{credentialSchemas-DDOLRCLF.mjs.map → credentialSchemas-DLKbxYBy.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-CI85F6dr.cjs → deploy-BbI1UMh3.cjs} +477 -156
- package/dist/deploy-BbI1UMh3.cjs.map +1 -0
- package/dist/{deploy-Ddy8UBoE.mjs → deploy-CiRYwFmr.mjs} +456 -117
- package/dist/deploy-CiRYwFmr.mjs.map +1 -0
- package/dist/{discover-C5roJuc9.mjs → discover-BvJOVsYG.mjs} +66 -14
- package/dist/discover-BvJOVsYG.mjs.map +1 -0
- package/dist/{discover-DozUKs8f.cjs → discover-ojkcbxHw.cjs} +88 -12
- package/dist/discover-ojkcbxHw.cjs.map +1 -0
- package/dist/{dokploy-CiozgdKO.cjs → dokploy-C0aQrCgs.cjs} +2 -2
- package/dist/{dokploy-CiozgdKO.cjs.map → dokploy-C0aQrCgs.cjs.map} +1 -1
- package/dist/{dokploy-Boj1NSHV.mjs → dokploy-MM7c3IE0.mjs} +2 -2
- package/dist/{dokploy-Boj1NSHV.mjs.map → dokploy-MM7c3IE0.mjs.map} +1 -1
- package/dist/{drivers-DbjiYpW6.cjs → drivers-CQ32PB5w.cjs} +2 -2
- package/dist/{drivers-DbjiYpW6.cjs.map → drivers-CQ32PB5w.cjs.map} +1 -1
- package/dist/{drivers-BjGDYfnC.mjs → drivers-Chb5Akov.mjs} +2 -2
- package/dist/{drivers-BjGDYfnC.mjs.map → drivers-Chb5Akov.mjs.map} +1 -1
- package/dist/{eas-DCg41jKK.mjs → eas-1Ee6kql0.mjs} +2 -2
- package/dist/{eas-DCg41jKK.mjs.map → eas-1Ee6kql0.mjs.map} +1 -1
- package/dist/{eas-BmBjIFf_.cjs → eas-CJoys9yh.cjs} +2 -2
- package/dist/{eas-BmBjIFf_.cjs.map → eas-CJoys9yh.cjs.map} +1 -1
- package/dist/{emit-C1e8qD08.mjs → emit-CNpuV_6Q.mjs} +17 -1
- package/dist/emit-CNpuV_6Q.mjs.map +1 -0
- package/dist/{emit-Dmc1nvdS.cjs → emit-qvHxMV9b.cjs} +17 -1
- package/dist/emit-qvHxMV9b.cjs.map +1 -0
- package/dist/{encryption-QOQbGXgV.mjs → encryption-B8Bk1Mvu.mjs} +3 -3
- package/dist/{encryption-QOQbGXgV.mjs.map → encryption-B8Bk1Mvu.mjs.map} +1 -1
- package/dist/{encryption-CwLWV2ig.cjs → encryption-Bo9VlRnl.cjs} +3 -3
- package/dist/{encryption-CwLWV2ig.cjs.map → encryption-Bo9VlRnl.cjs.map} +1 -1
- package/dist/{engine-HVqo3mQu.mjs → engine-BuinpJfS.mjs} +7 -8
- package/dist/{engine-HVqo3mQu.mjs.map → engine-BuinpJfS.mjs.map} +1 -1
- package/dist/{engine-D7YNY3v7.cjs → engine-esxTxcIx.cjs} +10 -11
- package/dist/{engine-D7YNY3v7.cjs.map → engine-esxTxcIx.cjs.map} +1 -1
- package/dist/{github-CO8W9SDs.cjs → github-BEA4K3_X.cjs} +80 -47
- package/dist/github-BEA4K3_X.cjs.map +1 -0
- package/dist/{github-CQt3xc3o.mjs → github-BUlKJfOV.mjs} +78 -45
- package/dist/github-BUlKJfOV.mjs.map +1 -0
- package/dist/{googlePlay-Cfu17aEp.mjs → googlePlay-CK05QiTq.mjs} +4 -4
- package/dist/{googlePlay-Cfu17aEp.mjs.map → googlePlay-CK05QiTq.mjs.map} +1 -1
- package/dist/{googlePlay-DSuF6jbF.cjs → googlePlay-CiqAS2ma.cjs} +5 -5
- package/dist/{googlePlay-DSuF6jbF.cjs.map → googlePlay-CiqAS2ma.cjs.map} +1 -1
- package/dist/{index-8OfpOtSI.d.cts → index-4CO0HD27.d.cts} +2 -2
- package/dist/{index-8OfpOtSI.d.cts.map → index-4CO0HD27.d.cts.map} +1 -1
- package/dist/{index-Dpji6Bxq.d.mts → index-DhUf0B0m.d.mts} +2 -2
- package/dist/{index-Dpji6Bxq.d.mts.map → index-DhUf0B0m.d.mts.map} +1 -1
- package/dist/index.cjs +81 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +64 -65
- 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-CGkir5LM.mjs → migrate-BzCHT9qs.mjs} +3 -3
- package/dist/{migrate-CGkir5LM.mjs.map → migrate-BzCHT9qs.mjs.map} +1 -1
- package/dist/{migrate-CXsMaHbb.cjs → migrate-C-hmbjpG.cjs} +3 -3
- package/dist/{migrate-CXsMaHbb.cjs.map → migrate-C-hmbjpG.cjs.map} +1 -1
- package/dist/{migrate-Bkz8BGaZ.mjs → migrate-CnbvrVag.mjs} +4 -4
- package/dist/{migrate-Bkz8BGaZ.mjs.map → migrate-CnbvrVag.mjs.map} +1 -1
- package/dist/{migrate-dbAI2XF8.cjs → migrate-DeQJBexh.cjs} +4 -4
- package/dist/{migrate-dbAI2XF8.cjs.map → migrate-DeQJBexh.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-eUcWvSdP.mjs → publish-BISBrrqR.mjs} +2 -2
- package/dist/{publish-eUcWvSdP.mjs.map → publish-BISBrrqR.mjs.map} +1 -1
- package/dist/{publish-Bbd-2PI2.cjs → publish-DtSIB-hZ.cjs} +2 -2
- package/dist/{publish-Bbd-2PI2.cjs.map → publish-DtSIB-hZ.cjs.map} +1 -1
- package/dist/{ready-BOfogqzr.mjs → ready-DqsyadOs.mjs} +6 -6
- package/dist/{ready-BOfogqzr.mjs.map → ready-DqsyadOs.mjs.map} +1 -1
- package/dist/{ready-BFFP838V.cjs → ready-IVGu-q18.cjs} +6 -6
- package/dist/{ready-BFFP838V.cjs.map → ready-IVGu-q18.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-DSilYpzm.mjs → resolve-BT7fkPN5.mjs} +4 -4
- package/dist/{resolve-DSilYpzm.mjs.map → resolve-BT7fkPN5.mjs.map} +1 -1
- package/dist/{resolve-CyprRcQo.cjs → resolve-C7GKQcE4.cjs} +4 -4
- package/dist/{resolve-CyprRcQo.cjs.map → resolve-C7GKQcE4.cjs.map} +1 -1
- package/dist/{rollback-DfwsnwGO.cjs → rollback-BEiB-Pje.cjs} +7 -7
- package/dist/{rollback-DfwsnwGO.cjs.map → rollback-BEiB-Pje.cjs.map} +1 -1
- package/dist/{rollback-C4ri3h7i.mjs → rollback-DbP0KuDw.mjs} +7 -7
- package/dist/{rollback-C4ri3h7i.mjs.map → rollback-DbP0KuDw.mjs.map} +1 -1
- package/dist/{runtime-ACdfK59n.d.cts → runtime-BFJQNqwd.d.mts} +2 -2
- package/dist/{runtime-ACdfK59n.d.cts.map → runtime-BFJQNqwd.d.mts.map} +1 -1
- package/dist/{runtime-YyQuekxi.d.mts → runtime-BJO-FFef.d.cts} +2 -2
- package/dist/{runtime-YyQuekxi.d.mts.map → runtime-BJO-FFef.d.cts.map} +1 -1
- package/dist/{s3-DQXZdmCM.mjs → s3-B4FTQQkV.mjs} +2 -2
- package/dist/{s3-DQXZdmCM.mjs.map → s3-B4FTQQkV.mjs.map} +1 -1
- package/dist/{s3-CrPoA89T.cjs → s3-DwKLXwUT.cjs} +2 -2
- package/dist/{s3-CrPoA89T.cjs.map → s3-DwKLXwUT.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-CFG-f24B.cjs → seeds-DOfxmDRO.cjs} +362 -34
- package/dist/seeds-DOfxmDRO.cjs.map +1 -0
- package/dist/{seeds-oY0qDqui.mjs → seeds-rG-Evfz9.mjs} +341 -31
- package/dist/seeds-rG-Evfz9.mjs.map +1 -0
- package/dist/{serverSetup-CQZt0OT6.cjs → serverSetup-CWWVcU6Y.cjs} +6 -6
- package/dist/{serverSetup-CQZt0OT6.cjs.map → serverSetup-CWWVcU6Y.cjs.map} +1 -1
- package/dist/{serverSetup-D8nngcNL.mjs → serverSetup-xGw1fxuZ.mjs} +6 -6
- package/dist/{serverSetup-D8nngcNL.mjs.map → serverSetup-xGw1fxuZ.mjs.map} +1 -1
- package/dist/{sniffer-BAmtXzou.cjs → sniffer-C7O9Pna_.cjs} +9 -9
- package/dist/{sniffer-BAmtXzou.cjs.map → sniffer-C7O9Pna_.cjs.map} +1 -1
- package/dist/{sniffer-Cgx608Tl.mjs → sniffer-DVPgfyHr.mjs} +9 -9
- package/dist/{sniffer-Cgx608Tl.mjs.map → sniffer-DVPgfyHr.mjs.map} +1 -1
- package/dist/{dns-CfEZHt7r.cjs → snsSubscriptions-CSkteK2J.cjs} +520 -3
- package/dist/snsSubscriptions-CSkteK2J.cjs.map +1 -0
- package/dist/{dns-BgxvvcQa.mjs → snsSubscriptions-CvBIGGp0.mjs} +431 -6
- package/dist/snsSubscriptions-CvBIGGp0.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-BKDdQm65.mjs → stage-2EKpzPQ1.mjs} +6 -6
- package/dist/{stage-BKDdQm65.mjs.map → stage-2EKpzPQ1.mjs.map} +1 -1
- package/dist/{stage-CJysBXWB.cjs → stage-fRfFtLwj.cjs} +7 -7
- package/dist/{stage-CJysBXWB.cjs.map → stage-fRfFtLwj.cjs.map} +1 -1
- package/dist/{stages-D4SnG5yl.cjs → stages-B7mCWI_R.cjs} +2 -2
- package/dist/{stages-D4SnG5yl.cjs.map → stages-B7mCWI_R.cjs.map} +1 -1
- package/dist/{stages-Cb9rqRbN.mjs → stages-DSltTW2d.mjs} +2 -2
- package/dist/{stages-Cb9rqRbN.mjs.map → stages-DSltTW2d.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-CW_dWeTe.mjs → terminal-BAVrDhhN.mjs} +3 -3
- package/dist/{terminal-CW_dWeTe.mjs.map → terminal-BAVrDhhN.mjs.map} +1 -1
- package/dist/{terminal-DJlqcW_U.cjs → terminal-D1aeTkgv.cjs} +3 -3
- package/dist/{terminal-DJlqcW_U.cjs.map → terminal-D1aeTkgv.cjs.map} +1 -1
- package/dist/{types-HqVPddRK.d.mts → types-CFU2uvrz.d.cts} +263 -4
- package/dist/types-CFU2uvrz.d.cts.map +1 -0
- package/dist/{types-B6UXlEkf.d.cts → types-D3BeGucy.d.mts} +262 -5
- package/dist/types-D3BeGucy.d.mts.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-BwqQnool.cjs → workspace-C82GLPnf.cjs} +8 -2
- package/dist/workspace-C82GLPnf.cjs.map +1 -0
- package/dist/{workspace-D1QbNd55.mjs → workspace-DUnnVjqC.mjs} +8 -2
- package/dist/workspace-DUnnVjqC.mjs.map +1 -0
- package/package.json +17 -16
- package/dist/SubscriberGenerator-C8CPxrCK.cjs.map +0 -1
- package/dist/SubscriberGenerator-y3pncae_.mjs.map +0 -1
- package/dist/compose-CgBPA44v.mjs.map +0 -1
- package/dist/compose-bKLAtHLj.cjs.map +0 -1
- package/dist/config-D48erO2j.d.mts.map +0 -1
- package/dist/config-DHDtsQrB.d.cts.map +0 -1
- package/dist/credentials-B5N5yKk6.mjs.map +0 -1
- package/dist/credentials-Zqr3QQYB.cjs.map +0 -1
- package/dist/deploy-CI85F6dr.cjs.map +0 -1
- package/dist/deploy-Ddy8UBoE.mjs.map +0 -1
- package/dist/discover-C5roJuc9.mjs.map +0 -1
- package/dist/discover-CSW_BMWa.d.cts +0 -125
- package/dist/discover-CSW_BMWa.d.cts.map +0 -1
- package/dist/discover-DozUKs8f.cjs.map +0 -1
- package/dist/discover-xtT7fgWg.d.mts +0 -125
- package/dist/discover-xtT7fgWg.d.mts.map +0 -1
- package/dist/dns-BgxvvcQa.mjs.map +0 -1
- package/dist/dns-CfEZHt7r.cjs.map +0 -1
- package/dist/emit-C1e8qD08.mjs.map +0 -1
- package/dist/emit-Dmc1nvdS.cjs.map +0 -1
- package/dist/github-CO8W9SDs.cjs.map +0 -1
- package/dist/github-CQt3xc3o.mjs.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-C4g1eG31.d.mts +0 -104
- package/dist/sandbox-C4g1eG31.d.mts.map +0 -1
- package/dist/sandbox-CdKgQ3XU.d.cts +0 -104
- package/dist/sandbox-CdKgQ3XU.d.cts.map +0 -1
- package/dist/seeds-CFG-f24B.cjs.map +0 -1
- package/dist/seeds-oY0qDqui.mjs.map +0 -1
- package/dist/types-B6UXlEkf.d.cts.map +0 -1
- package/dist/types-HqVPddRK.d.mts.map +0 -1
- package/dist/workspace-BwqQnool.cjs.map +0 -1
- package/dist/workspace-D1QbNd55.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscriberGenerator-C3QTae-R.cjs","names":["join","GkmError","kebabCase","access","pathToFileURL","ConstructGenerator","Subscriber","join","mkdir","relative","dirname","runtimeFor","writeFile","subscriberConnectionKey","subscriberTokenKey","canonicalId"],"sources":["../src/types.ts","../src/generators/subscriberRoute.ts","../src/reconcile/fakes.ts","../src/generators/SubscriberGenerator.ts"],"sourcesContent":["import type {\n\tDevConfig,\n\tSecretsConfig,\n\tStagesConfig,\n\tTestConfig,\n} from './workspace/types.js';\n\nexport type MainProvider = 'aws' | 'server';\n\nexport type Routes = string | string[];\n\n/**\n * Supported event backend types.\n *\n * - `pgboss`: Reuses PostgreSQL with a dedicated user/schema. Auto-enables db service.\n * - `sns`: Adds the AWS emulator (floci) container; each topic and queue is\n * created on it and addressed by its own string.\n * - `rabbitmq`: Adds RabbitMQ container.\n *\n * Each topic and queue resolves its own `<ID>_PUBLISHER_CONNECTION_STRING`,\n * which its producers and its consumers both read. pg-boss and RabbitMQ also\n * resolve `EVENT_PUBLISHER_CONNECTION_STRING`, the one broker crons schedule\n * through.\n */\nexport type EventsBackend = 'pgboss' | 'sns' | 'rabbitmq';\n\n/**\n * Where a declared cache actually lives.\n *\n * A deployment choice, not an application one — the same code caches into any\n * of these — so it is config here beside `events` rather than a field on the\n * construct. Each backend is consistent between local and deployed, which is\n * the property worth having: a cache that behaves differently in the two places\n * is worse than a slower one.\n *\n * - `upstash` (default) — HTTP with a token. Reachable from a Lambda with no\n * VPC and no connection pool, which is why it is the default; locally the\n * `serverless-redis-http` proxy in front of Redis speaks the same protocol.\n * - `elasticache` — the Redis wire protocol, inside a VPC. Locally, plain Redis.\n * - `db` — a table in the database the app already declared. No infrastructure\n * at all, and the same relationship pg-boss has to Postgres: it creates its\n * own table on first use and needs a declared database to live in.\n * - `redis` — a Redis the stack runs beside the apps, on its network. It holds\n * *every* declared cache, one declared from a database included, so the\n * cache never competes with the database for its connections. Only the\n * compose target selects it today (`gkm compose`).\n */\nexport type CacheBackend = 'upstash' | 'elasticache' | 'db' | 'redis';\n\n/**\n * The backends a deploy target defaults to — every one but the compose\n * stack's own Redis, which only that target runs.\n */\nexport type TargetCacheBackend = Exclude<CacheBackend, 'redis'>;\n\n/** Whether `value` names a cache backend. */\nexport function isCacheBackend(value: string): value is CacheBackend {\n\treturn (\n\t\tvalue === 'upstash' ||\n\t\tvalue === 'elasticache' ||\n\t\tvalue === 'db' ||\n\t\tvalue === 'redis'\n\t);\n}\n\n/**\n * Whether a backend holds every declared cache, a cache declared from a\n * database (`database.cache('Sessions')`) included.\n *\n * Every other backend leaves such a cache where its declaration put it: the\n * declaration is the stronger statement. The stack's own Redis is the\n * exception, because a stack that runs one runs it *for* the cache — a table\n * beside it would be a second cache nobody asked for.\n */\nexport function holdsEveryCache(backend: CacheBackend): boolean {\n\treturn backend === 'redis';\n}\n\n/**\n * Where a declared bucket actually lives.\n *\n * The fourth of these selectors and the last to arrive, which is why\n * `UPLOADS_URL` had no answer on a container target: there was nothing to\n * choose with. Like the others it changes no application code — the `s3://`\n * scheme picks the driver, and every one of these speaks it.\n *\n * - `s3` — a real bucket. Nothing is created on the host; the URL is composed\n * against credentials you already hold, the same way `resend` is an API key\n * rather than infrastructure.\n * - `r2` — Cloudflare's, reached through the same S3 API and differing only in\n * its endpoint.\n * - `minio` — a container. Everything the app declares lives on the box, which\n * is what makes a single-host deploy self-contained, and what makes it a\n * single disk with no replication. Locally this is what every backend is.\n */\nexport type StorageBackend = 'minio' | 's3' | 'r2';\n\n/**\n * The backend a project gets when it says nothing, by where it deploys.\n *\n * Flat defaults could not be right for both: `upstash` on a single box means a\n * SaaS round-trip for a cache the database could have held, and `minio` on AWS\n * means running a bucket beside S3. So the default is a function of the target,\n * and an app that says nothing gets the obvious answer in both places — which\n * is the whole of what \"zero config\" can mean once there is more than one\n * target.\n *\n * `server` covers Dokploy and a bare host alike: the question these answer is\n * whether the app is next to a managed cloud or next to its own containers, and\n * on that question the two are the same.\n *\n * A per-construct, per-stage override is the layer above this — see\n * `constructs-outstanding.md` — so two buckets in one app can differ. This is\n * only what happens when nobody has said anything at all.\n */\nexport const DEFAULT_CACHE: Record<MainProvider, TargetCacheBackend> = {\n\t// Reachable from a Lambda with no VPC and no connection pool.\n\taws: 'upstash',\n\t// The database is already there and already has a connection pool open to\n\t// it. Anything else is a second service to run for a table.\n\tserver: 'db',\n};\n\n/**\n * Which broker carries a declared queue or topic, by target.\n *\n * Whether there are events at all is the manifest's answer — a declared\n * `Topic` or `Queue` — so this only ever answers the second question. AWS\n * deploys them to SNS and SQS, and locally the AWS emulator stands in, so the\n * broker a developer runs is the one production has. A server has a Postgres\n * already, and pg-boss keeps its queues there.\n */\nexport const DEFAULT_EVENTS: Record<MainProvider, EventsBackend> = {\n\taws: 'sns',\n\tserver: 'pgboss',\n};\n\n/** Where a bucket lives when nobody said — see {@link DEFAULT_CACHE}. */\nexport const DEFAULT_STORAGE: Record<MainProvider, StorageBackend> = {\n\taws: 's3',\n\tserver: 'minio',\n};\n\n/**\n * Who delivers a declared app's mail.\n *\n * Unlike a cache, this changes *nothing* about the client. Every backend speaks\n * SMTP, so the declaration's `smtp://` URL is true of all of them and only the\n * provisioner differs — which is the same reason the declaration has no\n * `provider` field and there is no `ses://` scheme.\n *\n * - `ses` (default) — the only backend that can be a *chain* of resources: an\n * identity, a user, an access key, and an SMTP password derived from it,\n * because SES issues IAM credentials rather than SMTP ones. Hand it a URL and\n * it uses that instead, which is what you want when the credentials already\n * exist — provisioning a second set of sending credentials for an identity\n * that already has them is not a thing to do by default.\n * - `resend` — an API key you hold, composed into an SMTP URL. Nothing to\n * provision: a SaaS account is not infrastructure.\n * - `smtp` — a URL supplied whole, for a relay somebody else runs.\n *\n * Locally every one of them is Mailpit, because locally none of it matters.\n */\nexport type EmailBackend = 'resend' | 'ses' | 'smtp';\n\n/**\n * Who delivers mail when nobody said.\n *\n * Flat, unlike the others, and deliberately: mail is a SaaS on every target.\n * There is no self-hosted preset to default a container deploy to — Mailpit is\n * a development inbox, not a sender — so `server` has no answer of its own to\n * give and would only be `ses` written twice.\n */\nexport const DEFAULT_EMAIL: EmailBackend = 'ses';\n\n/** Supported docker-compose service names */\nexport type ComposeServiceName =\n\t| 'postgres'\n\t| 'redis'\n\t| 'rabbitmq'\n\t| 'minio'\n\t| 'mailpit'\n\t| 'localstack';\n\nexport interface DockerConfig {\n\t/** Container registry URL (e.g., 'ghcr.io/myorg') */\n\tregistry?: string;\n\t/** Docker image name (default: derived from package.json name) */\n\timageName?: string;\n\t/** Base Docker image (default: 'node:22-alpine') */\n\tbaseImage?: string;\n\t/** Container port (default: 3000) */\n\tport?: number;\n}\n\nexport type Runtime = 'node' | 'bun';\n\nexport interface TelescopeConfig {\n\t/** Enable/disable telescope (default: true in development) */\n\tenabled?: boolean;\n\t/** Port for telescope to use (defaults to server port) */\n\tport?: number;\n\t/** Path prefix for telescope UI (default: /__telescope) */\n\tpath?: string;\n\t/** Ignore patterns for telescope (e.g., ['/health', '/metrics']) */\n\tignore?: string[];\n\t/** Record request/response bodies (default: true) */\n\trecordBody?: boolean;\n\t/** Maximum entries to keep in memory (default: 1000) */\n\tmaxEntries?: number;\n\t/** Enable WebSocket for real-time updates (default: true, requires @hono/node-ws for Node.js) */\n\twebsocket?: boolean;\n}\n\nexport interface OpenApiConfig {\n\t/** Enable OpenAPI generation (default: true) */\n\tenabled?: boolean;\n\t/** API title */\n\ttitle?: string;\n\t/** API version */\n\tversion?: string;\n\t/** API description */\n\tdescription?: string;\n}\n\nexport interface HooksConfig {\n\t/**\n\t * Path to a module exporting server lifecycle hooks.\n\t * The module should export `beforeSetup` and/or `afterSetup` functions.\n\t *\n\t * @example\n\t * ```typescript\n\t * // src/config/hooks.ts\n\t * import type { Hono } from 'hono';\n\t * import type { Logger } from '@geekmidas/logger';\n\t * import type { EnvironmentParser } from '@geekmidas/envkit';\n\t *\n\t * // Called BEFORE gkm endpoints are registered\n\t * export function beforeSetup(app: Hono, ctx: { envParser: EnvironmentParser; logger: Logger }) {\n\t * app.use('*', cors());\n\t * app.get('/custom/health', (c) => c.json({ status: 'ok' }));\n\t * }\n\t *\n\t * // Called AFTER gkm endpoints are registered\n\t * export function afterSetup(app: Hono, ctx: { envParser: EnvironmentParser; logger: Logger }) {\n\t * app.notFound((c) => c.json({ error: 'Not found' }, 404));\n\t * app.onError((err, c) => c.json({ error: err.message }, 500));\n\t * }\n\t * ```\n\t */\n\tserver?: string;\n}\n\nexport interface GkmConfig {\n\t/**\n\t * What this app is called, and therefore how everything it declares is named.\n\t *\n\t * The same field `sst.config.ts` carries, doing the same job: every physical\n\t * name is `{stage}-{name}-{construct}`, so `Database` in the `production`\n\t * stage of `kitchen-sink` is `production-kitchen-sink-database` on AWS and on\n\t * Dokploy alike. One statement, in the config, rather than a rule each\n\t * provider restates.\n\t *\n\t * `defineWorkspace` has always had this. A single-app config did not, so its\n\t * names fell back to whatever the directory happened to be called — the\n\t * monorepo's package name for the project and the app's for the application,\n\t * two accidents of layout standing in for a decision.\n\t *\n\t * Defaults to the package.json name with any scope stripped, then the\n\t * directory name.\n\t */\n\tname?: string;\n\t/** The project's stages: which one is local, which deploy */\n\tstages: StagesConfig;\n\t/** Encrypted secrets, and where deployed stages' secrets are stored */\n\tsecrets?: SecretsConfig;\n\t/** What `gkm test` hands a feature test: where the factories are. */\n\ttest?: TestConfig;\n\t/** How `gkm dev` is reached: the discovery endpoint's port and origins. */\n\tdev?: DevConfig;\n\t/**\n\t * Constructs glob pattern — one glob, every kind.\n\t *\n\t * What reconcile reads to derive the containers a stage needs: a database\n\t * implies Postgres, objects imply MinIO, mail implies Mailpit. A glob per\n\t * kind cannot find a resource, because a declared `ObjectStorage` has no\n\t * kind to be listed under.\n\t *\n\t * @example './src/**\\/*.ts'\n\t */\n\tconstructs: Routes;\n\t/**\n\t * Server lifecycle hooks for customizing the Hono app.\n\t * Allows adding custom routes, middleware, error handlers, etc.\n\t *\n\t * @example\n\t * hooks: {\n\t * server: './src/config/hooks'\n\t * }\n\t */\n\thooks?: HooksConfig;\n\t/**\n\t * Telescope configuration for debugging/monitoring.\n\t * Can be:\n\t * - A string path to a module that exports a Telescope instance (recommended)\n\t * - A boolean to enable/disable with defaults\n\t * - A TelescopeConfig object for inline configuration\n\t */\n\ttelescope?: string | boolean | TelescopeConfig;\n\t/**\n\t * OpenAPI generation configuration.\n\t * Can be:\n\t * - A boolean to enable/disable with defaults (output: ./src/api/openapi.ts)\n\t * - An OpenApiConfig object for customization\n\t *\n\t * When enabled, OpenAPI spec is generated on startup and regenerated on route changes.\n\t *\n\t * @example\n\t * openapi: true\n\t *\n\t * @example\n\t * openapi: {\n\t * output: './src/api/openapi.ts',\n\t * title: 'My API',\n\t * version: '1.0.0',\n\t * }\n\t */\n\topenapi?: boolean | OpenApiConfig;\n\t/** Runtime to use for dev server (default: 'node') */\n\truntime?: Runtime;\n\t/**\n\t * Environment file(s) to load for development.\n\t * Can be:\n\t * - A string path to a single env file (e.g., '.env.local')\n\t * - An array of paths to load in order (later files override earlier)\n\t * - Defaults to '.env' if not specified\n\t *\n\t * @example\n\t * env: '.env.local'\n\t *\n\t * @example\n\t * env: ['.env', '.env.local']\n\t */\n\tenv?: string | string[];\n\t/**\n\t * Docker deployment configuration.\n\t * Used by `gkm docker` and `gkm prepack` commands.\n\t *\n\t * @example\n\t * docker: {\n\t * registry: 'ghcr.io/myorg',\n\t * imageName: 'my-api',\n\t * compose: {\n\t * services: ['postgres', 'redis']\n\t * }\n\t * }\n\t */\n\tdocker?: DockerConfig;\n\t/**\n\t * Where this app deploys, and what the target needs to know.\n\t *\n\t * Absent until now, which meant a single-app project could not configure a\n\t * Dokploy deploy at all: the wrap that turns one into a workspace hardcoded\n\t * `{ default: 'dokploy' }` and dropped everything else, so there was no\n\t * endpoint, no registry and no domain — and `resolveHost` failed with \"no\n\t * domain configured for stage\" on a config that had nowhere to put one.\n\t *\n\t * The same shape a workspace config uses, so moving from one to the other is\n\t * a rename rather than a rewrite.\n\t */\n\tdeploy?: import('./workspace/types').DeployConfig;\n\t/** Each deployed stage's base domain — the same field a workspace has. */\n\tdomains?: import('./workspace/types').DomainsConfig;\n\t/** Each root domain's DNS provider — the same field a workspace has. */\n\tdns?: import('./workspace/types').WorkspaceDnsConfig;\n}\n\nexport interface BuildOptions {\n\t/** Overrides the deploy target's: a Dockerfile builds a server regardless. */\n\tprovider?: MainProvider;\n\t/**\n\t * Overrides the target's cache backend for the drivers the entry\n\t * registers — set by the Dockerfiles `gkm compose` writes, whose stack runs\n\t * its own Redis.\n\t */\n\tcache?: CacheBackend;\n\tenableOpenApi?: boolean;\n\t/** Build for production (no dev tools, bundled output) */\n\tproduction?: boolean;\n\t/** Skip bundling step in production build */\n\tskipBundle?: boolean;\n\t/** Stage for secrets injection (e.g., 'production', 'staging') */\n\tstage?: string;\n\t/**\n\t * When true, optional environment variables (those with `.optional()` or\n\t * `.default()` in the envParser) are suffixed with `?` within each\n\t * construct's `environment` array in the manifest (e.g. `PORT?`).\n\t * Defaults to false.\n\t */\n\tmarkOptional?: boolean;\n\t/**\n\t * Where the apps' own build scripts (turbo) run. Defaults to a\n\t * `LocalSandbox` on the workspace: this machine, with an allowlisted\n\t * environment.\n\t */\n\tsandbox?: import('./sandbox/sandbox').Sandbox;\n}\n\n/** Result from build command when secrets are injected */\nexport interface BuildResult {\n\t/** Ephemeral master key for deployment (only if stage was provided) */\n\tmasterKey?: string;\n}\n\n// The deployment manifest types are a shared, dependency-free data contract\n// in `@geekmidas/manifest`, re-exported here for back-compat with existing\n// `@geekmidas/cli` imports.\nexport type {\n\tCronInfo,\n\tFunctionInfo,\n\tQueueInfo,\n\tRouteInfo,\n\tSubscriberInfo,\n\tTopicInfo,\n} from '@geekmidas/manifest';\n","/**\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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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
|
|
2
|
-
import { n as runtimeFor } from "./EndpointGenerator-
|
|
3
|
-
import { d as ConstructGenerator } from "./discover-
|
|
2
|
+
import { n as runtimeFor } from "./EndpointGenerator-Z_BUTocT.mjs";
|
|
3
|
+
import { _ as subscriberTokenKey, d as ConstructGenerator, g as subscriberConnectionKey } from "./discover-BvJOVsYG.mjs";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
6
|
-
import { canonicalId, kebabCase
|
|
6
|
+
import { canonicalId, kebabCase } from "@geekmidas/manifest";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
9
9
|
//#region src/types.ts
|
|
@@ -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 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 ConstructGenerator {
|
|
229
246
|
isConstruct(value) {
|
|
@@ -233,8 +250,9 @@ var SubscriberGenerator = class extends 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 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 = relative(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 = subscriberConnectionKey(construct);
|
|
338
|
+
const tokenKey = subscriberTokenKey(construct);
|
|
339
|
+
const topic = construct.topicName ?? construct.bucketName;
|
|
340
|
+
const bucket = construct.bucketName ? 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 ? provideKey(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,7 +675,30 @@ 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) => canonicalId(id) === canonicalId(bucket))) throw new ObjectSubscriberFeedsItself(key, bucket);
|
|
700
|
+
}
|
|
530
701
|
//#endregion
|
|
531
|
-
export {
|
|
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 };
|
|
532
703
|
|
|
533
|
-
//# sourceMappingURL=SubscriberGenerator-
|
|
704
|
+
//# sourceMappingURL=SubscriberGenerator-COAVPgM-.mjs.map
|