@geekmidas/cli 10.0.0-alpha.6 → 10.0.0-alpha.61
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/README.md +225 -268
- package/bin/adjacent-tsconfig.mjs +97 -0
- package/bin/gkm.mjs +16 -2
- package/bin/owning-tsconfig-jsx-register.mjs +18 -0
- package/bin/owning-tsconfig-jsx.mjs +108 -0
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-CgDaD8VR.cjs} +21 -22
- package/dist/HostingerProvider-CgDaD8VR.cjs.map +1 -0
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-DI_Pr2VE.mjs} +20 -21
- package/dist/HostingerProvider-DI_Pr2VE.mjs.map +1 -0
- package/dist/LegacyStateStore-C7vFAxbu.mjs +89 -0
- package/dist/LegacyStateStore-C7vFAxbu.mjs.map +1 -0
- package/dist/LegacyStateStore-JbBVFp_2.cjs +89 -0
- package/dist/LegacyStateStore-JbBVFp_2.cjs.map +1 -0
- package/dist/LocalStateStore-DHj_-CQB.cjs +176 -0
- package/dist/LocalStateStore-DHj_-CQB.cjs.map +1 -0
- package/dist/LocalStateStore-DfUwQsL8.mjs +165 -0
- package/dist/LocalStateStore-DfUwQsL8.mjs.map +1 -0
- package/dist/{openapi-D-dx9Akj.cjs → OpenApiTsGenerator-BSrgw7B0.cjs} +360 -470
- package/dist/OpenApiTsGenerator-BSrgw7B0.cjs.map +1 -0
- package/dist/{openapi-Bw8DWFWz.mjs → OpenApiTsGenerator-q5B0z8Xg.mjs} +325 -404
- package/dist/OpenApiTsGenerator-q5B0z8Xg.mjs.map +1 -0
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
- package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
- package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
- package/dist/S3StateStore-Dq6hHtzY.mjs +161 -0
- package/dist/S3StateStore-Dq6hHtzY.mjs.map +1 -0
- package/dist/S3StateStore-xqnpUrzy.cjs +160 -0
- package/dist/S3StateStore-xqnpUrzy.cjs.map +1 -0
- package/dist/SSMStateStore-5dBRwVlJ.cjs +153 -0
- package/dist/SSMStateStore-5dBRwVlJ.cjs.map +1 -0
- package/dist/SSMStateStore-SUmvAbLi.mjs +154 -0
- package/dist/SSMStateStore-SUmvAbLi.mjs.map +1 -0
- package/dist/appPorts-C9z9Pzn9.mjs +223 -0
- package/dist/appPorts-C9z9Pzn9.mjs.map +1 -0
- package/dist/appPorts-Cu79hJF6.cjs +264 -0
- package/dist/appPorts-Cu79hJF6.cjs.map +1 -0
- package/dist/aws-DkHdTV-p.cjs +61 -0
- package/dist/aws-DkHdTV-p.cjs.map +1 -0
- package/dist/aws-DojzQnLd.mjs +61 -0
- package/dist/aws-DojzQnLd.mjs.map +1 -0
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
- package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler-B5VsKW-_.mjs} +18 -29
- package/dist/bundler-B5VsKW-_.mjs.map +1 -0
- package/dist/{bundler-BY1OmV1e.cjs → bundler-DXATpO1T.cjs} +20 -32
- package/dist/bundler-DXATpO1T.cjs.map +1 -0
- package/dist/cli-DEA_J3Sa.mjs +69 -0
- package/dist/cli-DEA_J3Sa.mjs.map +1 -0
- package/dist/cli-DQdrVkj2.cjs +69 -0
- package/dist/cli-DQdrVkj2.cjs.map +1 -0
- package/dist/{config-CxP7P-qG.mjs → config-C5-EEdmr.mjs} +144 -127
- package/dist/config-C5-EEdmr.mjs.map +1 -0
- package/dist/{config-CNz5KaSR.cjs → config-DXnkCEq0.cjs} +212 -184
- package/dist/config-DXnkCEq0.cjs.map +1 -0
- package/dist/config.cjs +9 -6
- package/dist/config.d.cts +124 -33
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +124 -33
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -5
- package/dist/deploy/public.cjs +22 -0
- package/dist/deploy/public.d.cts +459 -0
- package/dist/deploy/public.d.cts.map +1 -0
- package/dist/deploy/public.d.mts +459 -0
- package/dist/deploy/public.d.mts.map +1 -0
- package/dist/deploy/public.mjs +5 -0
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +10 -3
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +14 -3
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +3 -3
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/deploy-BJcLmVTc.mjs +3282 -0
- package/dist/deploy-BJcLmVTc.mjs.map +1 -0
- package/dist/deploy-BbJdgQ2b.cjs +3365 -0
- package/dist/deploy-BbJdgQ2b.cjs.map +1 -0
- package/dist/{discover-DTmFeloL.cjs → discover-Bc-4eA_p.cjs} +131 -103
- package/dist/discover-Bc-4eA_p.cjs.map +1 -0
- package/dist/{discover-CAlFgYec.mjs → discover-FrYvzkeC.mjs} +91 -58
- package/dist/discover-FrYvzkeC.mjs.map +1 -0
- package/dist/{emit-CiZS_5Ub.mjs → emit-C1e8qD08.mjs} +13 -26
- package/dist/emit-C1e8qD08.mjs.map +1 -0
- package/dist/{emit-CbcRolsa.cjs → emit-Dmc1nvdS.cjs} +31 -57
- package/dist/emit-Dmc1nvdS.cjs.map +1 -0
- package/dist/encryption-DaA9gRXr.mjs +90 -0
- package/dist/encryption-DaA9gRXr.mjs.map +1 -0
- package/dist/encryption-ojzUo7-z.cjs +91 -0
- package/dist/encryption-ojzUo7-z.cjs.map +1 -0
- package/dist/fakes-C9RiT6XG.mjs +125 -0
- package/dist/fakes-C9RiT6XG.mjs.map +1 -0
- package/dist/fakes-zW-jNAbQ.cjs +148 -0
- package/dist/fakes-zW-jNAbQ.cjs.map +1 -0
- package/dist/github-Cq57poPj.cjs +213 -0
- package/dist/github-Cq57poPj.cjs.map +1 -0
- package/dist/github-DmCj2u95.mjs +213 -0
- package/dist/github-DmCj2u95.mjs.map +1 -0
- package/dist/home-DOzBaWaL.cjs +33 -0
- package/dist/home-DOzBaWaL.cjs.map +1 -0
- package/dist/home-GI7pJl1M.mjs +22 -0
- package/dist/home-GI7pJl1M.mjs.map +1 -0
- package/dist/{index-DEbaleny.d.mts → index-Da1-oa9W.d.mts} +4 -23
- package/dist/index-Da1-oa9W.d.mts.map +1 -0
- package/dist/{index-BGGdjueF.d.cts → index-UiWCaGzU.d.cts} +4 -23
- package/dist/index-UiWCaGzU.d.cts.map +1 -0
- package/dist/index.cjs +10712 -12248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +10480 -12040
- package/dist/index.mjs.map +1 -1
- package/dist/migrate-5_J5JySj.mjs +265 -0
- package/dist/migrate-5_J5JySj.mjs.map +1 -0
- package/dist/migrate-BScRpCEx.cjs +270 -0
- package/dist/migrate-BScRpCEx.cjs.map +1 -0
- package/dist/openapi.cjs +198 -10
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +21 -19
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +21 -19
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +193 -5
- package/dist/openapi.mjs.map +1 -0
- package/dist/ready-DQIy21W5.mjs +760 -0
- package/dist/ready-DQIy21W5.mjs.map +1 -0
- package/dist/ready-XlNx-Gbo.cjs +807 -0
- package/dist/ready-XlNx-Gbo.cjs.map +1 -0
- package/dist/reconcile/public.cjs +3 -5
- package/dist/reconcile/public.d.cts +30 -33
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +30 -33
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
- package/dist/reconcile--I7V4Klj.mjs.map +1 -0
- package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
- package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/sniffer-CQmJnNJM.cjs +7115 -0
- package/dist/sniffer-CQmJnNJM.cjs.map +1 -0
- package/dist/sniffer-bdleks5l.mjs +6561 -0
- package/dist/sniffer-bdleks5l.mjs.map +1 -0
- package/dist/storage-BzaUxmqe.mjs +124 -0
- package/dist/storage-BzaUxmqe.mjs.map +1 -0
- package/dist/storage-CSGoC3bK.cjs +152 -0
- package/dist/storage-CSGoC3bK.cjs.map +1 -0
- package/dist/store-BS8A7NcA.mjs +415 -0
- package/dist/store-BS8A7NcA.mjs.map +1 -0
- package/dist/store-KvNQwYaB.cjs +510 -0
- package/dist/store-KvNQwYaB.cjs.map +1 -0
- package/dist/types-Bp1mVxG0.d.mts +6410 -0
- package/dist/types-Bp1mVxG0.d.mts.map +1 -0
- package/dist/types-efNLu9xu.d.cts +6411 -0
- package/dist/types-efNLu9xu.d.cts.map +1 -0
- package/dist/vitest.cjs +65 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +29 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +29 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +65 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/workspace/index.cjs +3 -4
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/{workspace-LKiNyaDy.cjs → workspace-B64Xk1Mb.cjs} +1070 -1118
- package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
- package/dist/{workspace-DIuZx-cc.mjs → workspace-Bp1xVaOz.mjs} +901 -972
- package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
- package/package.json +38 -20
- package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
- package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
- package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
- package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
- package/dist/LocalStateProvider-Bn6tS-oL.mjs +0 -42
- package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
- package/dist/LocalStateProvider-Db9SEjyH.cjs +0 -43
- package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
- package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
- package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
- package/dist/SSMStateProvider-BRt2py-A.mjs +0 -66
- package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
- package/dist/SSMStateProvider-DyX8s9HI.cjs +0 -66
- package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
- package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
- package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
- package/dist/bundler-BY1OmV1e.cjs.map +0 -1
- package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-CNz5KaSR.cjs.map +0 -1
- package/dist/config-CxP7P-qG.mjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BK3Z93MX.mjs +0 -2548
- package/dist/credentials-BK3Z93MX.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-DzsXZzkx.cjs +0 -2680
- package/dist/credentials-DzsXZzkx.cjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
- package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
- package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
- package/dist/emit-CbcRolsa.cjs.map +0 -1
- package/dist/emit-CiZS_5Ub.mjs.map +0 -1
- package/dist/encryption-C6jaU0hE.cjs +0 -54
- package/dist/encryption-C6jaU0hE.cjs.map +0 -1
- package/dist/encryption-CHzzeR8a.cjs +0 -4
- package/dist/encryption-CQUncu7v.mjs +0 -3
- package/dist/encryption-D-abyhZx.mjs +0 -42
- package/dist/encryption-D-abyhZx.mjs.map +0 -1
- package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
- package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
- package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs +0 -289
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs +0 -253
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/index-BGGdjueF.d.cts.map +0 -1
- package/dist/index-DEbaleny.d.mts.map +0 -1
- package/dist/openapi-Bw8DWFWz.mjs.map +0 -1
- package/dist/openapi-D-dx9Akj.cjs.map +0 -1
- package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
- package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
- package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
- package/dist/reconcile-D0pimNiy.cjs.map +0 -1
- package/dist/setup-8IWPsRIN.cjs +0 -11
- package/dist/setup-BWMhbW-p.mjs +0 -443
- package/dist/setup-BWMhbW-p.mjs.map +0 -1
- package/dist/setup-D7L4BgBT.cjs +0 -473
- package/dist/setup-D7L4BgBT.cjs.map +0 -1
- package/dist/setup-DGIPd2fh.mjs +0 -11
- package/dist/storage-BJmzYxt8.cjs +0 -363
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-BM9hGpgv.cjs +0 -7
- package/dist/storage-Bi24WfoW.mjs +0 -297
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/storage-CYlGwWTj.mjs +0 -3
- package/dist/sync-CmEuNZsB.mjs +0 -76
- package/dist/sync-CmEuNZsB.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- package/dist/sync-SizaChmC.cjs +0 -93
- package/dist/sync-SizaChmC.cjs.map +0 -1
- package/dist/sync-hOqAWHR6.mjs +0 -3
- package/dist/types-BLSNdrk0.d.cts +0 -2176
- package/dist/types-BLSNdrk0.d.cts.map +0 -1
- package/dist/types-C3POXhw3.d.mts +0 -2176
- package/dist/types-C3POXhw3.d.mts.map +0 -1
- package/dist/workspace-CGaUAw5A.cjs +0 -9
- package/dist/workspace-CnTqIlB3.mjs +0 -8
- package/dist/workspace-DIuZx-cc.mjs.map +0 -1
- package/dist/workspace-LKiNyaDy.cjs.map +0 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let node_child_process = require("node:child_process");
|
|
6
|
+
//#region src/dev/appPorts.ts
|
|
7
|
+
/**
|
|
8
|
+
* Which local port each app answers on — and what to do when it is taken.
|
|
9
|
+
*
|
|
10
|
+
* An app's port used to be fixed: 3000, 3001, … and `gkm dev` refused to start
|
|
11
|
+
* if anything held one. That is right when the holder is this same app, left
|
|
12
|
+
* running by a previous `gkm dev` — moving would start a second copy beside it.
|
|
13
|
+
* It is wrong when the holder is another project's dev server, which also
|
|
14
|
+
* defaults to 3000: two projects could not run at once, and nothing about the
|
|
15
|
+
* app needs that port. A browser reaches it through the edge, whose URL carries
|
|
16
|
+
* no app port, and every address a phone or a route uses is derived on each
|
|
17
|
+
* start.
|
|
18
|
+
*
|
|
19
|
+
* So the holder is asked who it is. Every app process gkm starts is tagged with
|
|
20
|
+
* {@link APP_TAG_ENV} — the workspace and the app — and children inherit it, so
|
|
21
|
+
* whatever actually binds the port (`next-server`, a Metro worker, the tsx
|
|
22
|
+
* server) carries it — or, when its own can't be read, one of its parents does.
|
|
23
|
+
* A held port whose holder carries this app's tag is a
|
|
24
|
+
* leftover, and is refused; anything else — untagged, another workspace's,
|
|
25
|
+
* unreadable — is not ours, and the app moves to the next free port.
|
|
26
|
+
*
|
|
27
|
+
* The choice is kept in `.gkm/app-ports.json`, not recomputed: an app that
|
|
28
|
+
* moved stays where it moved, so a bookmark or a phone's saved address does not
|
|
29
|
+
* break the next time the other project happens to be stopped. Local only —
|
|
30
|
+
* `gkm deploy` and `gkm docker` never read it.
|
|
31
|
+
*/
|
|
32
|
+
var appPorts_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
33
|
+
APP_TAG_ENV: () => APP_TAG_ENV,
|
|
34
|
+
appTag: () => appTag,
|
|
35
|
+
assignAppPorts: () => assignAppPorts,
|
|
36
|
+
describeHolder: () => describeHolder,
|
|
37
|
+
holderOf: () => holderOf,
|
|
38
|
+
savedAppPorts: () => savedAppPorts,
|
|
39
|
+
withAppPorts: () => withAppPorts
|
|
40
|
+
});
|
|
41
|
+
/** Carried by every app process gkm starts: `<workspace root>#<app>`. */
|
|
42
|
+
const APP_TAG_ENV = "GKM_DEV_APP";
|
|
43
|
+
const APP_PORTS_PATH = ".gkm/app-ports.json";
|
|
44
|
+
/** The tag an app's processes carry. The root is encoded, so it has no spaces. */
|
|
45
|
+
function appTag(root, app) {
|
|
46
|
+
return `${encodeURIComponent((0, node_path.resolve)(root))}#${app}`;
|
|
47
|
+
}
|
|
48
|
+
/** Says, as `command (pid N)`, what holds a port. */
|
|
49
|
+
function describeHolder(holder) {
|
|
50
|
+
if (!holder) return "another process";
|
|
51
|
+
return `${holder.command ?? "a process"} (pid ${holder.pid})`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Who is listening on a port, read the way a person would: `lsof` for the
|
|
55
|
+
* pid, then that process's environment for its tag — `/proc/<pid>/environ` on
|
|
56
|
+
* Linux, `ps eww` elsewhere. Both work for the user's own processes, which is
|
|
57
|
+
* every process gkm starts; one that cannot be read has no tag, so is not ours.
|
|
58
|
+
*/
|
|
59
|
+
function holderOf(port) {
|
|
60
|
+
let out;
|
|
61
|
+
try {
|
|
62
|
+
out = (0, node_child_process.execFileSync)("lsof", [
|
|
63
|
+
"-nP",
|
|
64
|
+
`-iTCP:${port}`,
|
|
65
|
+
"-sTCP:LISTEN",
|
|
66
|
+
"-Fpc"
|
|
67
|
+
], {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
stdio: [
|
|
70
|
+
"ignore",
|
|
71
|
+
"pipe",
|
|
72
|
+
"ignore"
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const pid = Number(out.match(/^p(\d+)/m)?.[1]);
|
|
79
|
+
if (!pid) return void 0;
|
|
80
|
+
const command = out.match(/^c(.+)$/m)?.[1];
|
|
81
|
+
const tag = lineageTagOf(pid);
|
|
82
|
+
return {
|
|
83
|
+
pid,
|
|
84
|
+
...command ? { command } : {},
|
|
85
|
+
...tag ? { tag } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** How far up a holder's parents to look before calling it untagged. */
|
|
89
|
+
const MAX_ANCESTORS = 8;
|
|
90
|
+
/**
|
|
91
|
+
* The tag of `pid`, or of its nearest parent that has one.
|
|
92
|
+
*
|
|
93
|
+
* A process can hide the tag it inherited: `next-server` renames itself, which
|
|
94
|
+
* on macOS overwrites the memory `ps eww` reads its environment from, so `ps`
|
|
95
|
+
* shows only its title. Its parent, the `next dev` gkm started, still carries
|
|
96
|
+
* the tag. A child is only ever started by its parent, so a tagged parent makes
|
|
97
|
+
* the holder this app's.
|
|
98
|
+
*/
|
|
99
|
+
function lineageTagOf(pid) {
|
|
100
|
+
let current = pid;
|
|
101
|
+
for (let depth = 0; current && current > 1; depth += 1) {
|
|
102
|
+
const tag = tagOf(current);
|
|
103
|
+
if (tag || depth === MAX_ANCESTORS) return tag;
|
|
104
|
+
current = parentOf(current);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function tagOf(pid) {
|
|
108
|
+
const prefix = `${APP_TAG_ENV}=`;
|
|
109
|
+
try {
|
|
110
|
+
return (process.platform === "linux" ? (0, node_fs.readFileSync)(`/proc/${pid}/environ`, "utf8").split("\0") : (0, node_child_process.execFileSync)("ps", [
|
|
111
|
+
"eww",
|
|
112
|
+
"-o",
|
|
113
|
+
"command=",
|
|
114
|
+
"-p",
|
|
115
|
+
String(pid)
|
|
116
|
+
], {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
stdio: [
|
|
119
|
+
"ignore",
|
|
120
|
+
"pipe",
|
|
121
|
+
"ignore"
|
|
122
|
+
]
|
|
123
|
+
}).split(/\s+/)).find((entry) => entry.startsWith(prefix))?.slice(prefix.length);
|
|
124
|
+
} catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function parentOf(pid) {
|
|
129
|
+
try {
|
|
130
|
+
const ppid = Number((0, node_child_process.execFileSync)("ps", [
|
|
131
|
+
"-o",
|
|
132
|
+
"ppid=",
|
|
133
|
+
"-p",
|
|
134
|
+
String(pid)
|
|
135
|
+
], {
|
|
136
|
+
encoding: "utf8",
|
|
137
|
+
stdio: [
|
|
138
|
+
"ignore",
|
|
139
|
+
"pipe",
|
|
140
|
+
"ignore"
|
|
141
|
+
]
|
|
142
|
+
}).trim());
|
|
143
|
+
return ppid > 0 ? ppid : void 0;
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** The ports apps were given before, by app name. */
|
|
149
|
+
async function savedAppPorts(root) {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(await (0, node_fs_promises.readFile)((0, node_path.join)(root, APP_PORTS_PATH), "utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function saveAppPorts(root, ports) {
|
|
157
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(root, ".gkm"), { recursive: true });
|
|
158
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(root, APP_PORTS_PATH), `${JSON.stringify(ports, null, 2)}\n`);
|
|
159
|
+
}
|
|
160
|
+
/** The workspace, with each app on the port it was given locally. */
|
|
161
|
+
function withAppPorts(workspace, ports) {
|
|
162
|
+
const apps = Object.fromEntries(Object.entries(workspace.apps).map(([name, app]) => [name, ports[name] ? {
|
|
163
|
+
...app,
|
|
164
|
+
port: ports[name]
|
|
165
|
+
} : app]));
|
|
166
|
+
return {
|
|
167
|
+
...workspace,
|
|
168
|
+
apps
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Give each named app a local port: the one it had, or its usual one, unless
|
|
173
|
+
* another project holds it — then the next free one, which is kept.
|
|
174
|
+
*
|
|
175
|
+
* @returns every app's port, the apps that moved, and the apps this workspace
|
|
176
|
+
* already has running (the caller refuses to start those).
|
|
177
|
+
*/
|
|
178
|
+
async function assignAppPorts(workspace, names, deps) {
|
|
179
|
+
const saved = await savedAppPorts(workspace.root);
|
|
180
|
+
const wanted = {};
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
const port = saved[name] ?? workspace.apps[name]?.port;
|
|
183
|
+
if (port) wanted[name] = port;
|
|
184
|
+
}
|
|
185
|
+
const taken = /* @__PURE__ */ new Set([...Object.values(saved), ...Object.values(wanted)]);
|
|
186
|
+
const ports = { ...saved };
|
|
187
|
+
const moved = [];
|
|
188
|
+
const running = [];
|
|
189
|
+
for (const name of Object.keys(wanted).sort()) {
|
|
190
|
+
const want = wanted[name];
|
|
191
|
+
ports[name] = want;
|
|
192
|
+
if (await deps.free(want)) continue;
|
|
193
|
+
const holder = deps.holder(want);
|
|
194
|
+
if (holder?.tag === appTag(workspace.root, name)) {
|
|
195
|
+
running.push({
|
|
196
|
+
app: name,
|
|
197
|
+
port: want,
|
|
198
|
+
holder
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let next = want + 1;
|
|
203
|
+
while (taken.has(next) || !await deps.free(next)) next += 1;
|
|
204
|
+
taken.add(next);
|
|
205
|
+
ports[name] = next;
|
|
206
|
+
moved.push({
|
|
207
|
+
app: name,
|
|
208
|
+
from: want,
|
|
209
|
+
to: next,
|
|
210
|
+
holder
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
await saveAppPorts(workspace.root, ports);
|
|
214
|
+
return {
|
|
215
|
+
ports,
|
|
216
|
+
moved,
|
|
217
|
+
running
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
Object.defineProperty(exports, "APP_TAG_ENV", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
get: function() {
|
|
224
|
+
return APP_TAG_ENV;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(exports, "appPorts_exports", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get: function() {
|
|
230
|
+
return appPorts_exports;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
Object.defineProperty(exports, "appTag", {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
get: function() {
|
|
236
|
+
return appTag;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(exports, "assignAppPorts", {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
get: function() {
|
|
242
|
+
return assignAppPorts;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(exports, "describeHolder", {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
get: function() {
|
|
248
|
+
return describeHolder;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
Object.defineProperty(exports, "holderOf", {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
get: function() {
|
|
254
|
+
return holderOf;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(exports, "withAppPorts", {
|
|
258
|
+
enumerable: true,
|
|
259
|
+
get: function() {
|
|
260
|
+
return withAppPorts;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=appPorts-Cu79hJF6.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appPorts-Cu79hJF6.cjs","names":["resolve","execFileSync","readFileSync","readFile","join","mkdir","writeFile"],"sources":["../src/dev/appPorts.ts"],"sourcesContent":["/**\n * Which local port each app answers on — and what to do when it is taken.\n *\n * An app's port used to be fixed: 3000, 3001, … and `gkm dev` refused to start\n * if anything held one. That is right when the holder is this same app, left\n * running by a previous `gkm dev` — moving would start a second copy beside it.\n * It is wrong when the holder is another project's dev server, which also\n * defaults to 3000: two projects could not run at once, and nothing about the\n * app needs that port. A browser reaches it through the edge, whose URL carries\n * no app port, and every address a phone or a route uses is derived on each\n * start.\n *\n * So the holder is asked who it is. Every app process gkm starts is tagged with\n * {@link APP_TAG_ENV} — the workspace and the app — and children inherit it, so\n * whatever actually binds the port (`next-server`, a Metro worker, the tsx\n * server) carries it — or, when its own can't be read, one of its parents does.\n * A held port whose holder carries this app's tag is a\n * leftover, and is refused; anything else — untagged, another workspace's,\n * unreadable — is not ours, and the app moves to the next free port.\n *\n * The choice is kept in `.gkm/app-ports.json`, not recomputed: an app that\n * moved stays where it moved, so a bookmark or a phone's saved address does not\n * break the next time the other project happens to be stopped. Local only —\n * `gkm deploy` and `gkm docker` never read it.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/** Carried by every app process gkm starts: `<workspace root>#<app>`. */\nexport const APP_TAG_ENV = 'GKM_DEV_APP';\n\nconst APP_PORTS_PATH = '.gkm/app-ports.json';\n\n/** The tag an app's processes carry. The root is encoded, so it has no spaces. */\nexport function appTag(root: string, app: string): string {\n\treturn `${encodeURIComponent(resolve(root))}#${app}`;\n}\n\n/** What listens on a port, and the tag it carries if it can be read. */\nexport interface PortHolder {\n\tpid: number;\n\tcommand?: string;\n\ttag?: string;\n}\n\n/** Says, as `command (pid N)`, what holds a port. */\nexport function describeHolder(holder: PortHolder | undefined): string {\n\tif (!holder) return 'another process';\n\treturn `${holder.command ?? 'a process'} (pid ${holder.pid})`;\n}\n\n/**\n * Who is listening on a port, read the way a person would: `lsof` for the\n * pid, then that process's environment for its tag — `/proc/<pid>/environ` on\n * Linux, `ps eww` elsewhere. Both work for the user's own processes, which is\n * every process gkm starts; one that cannot be read has no tag, so is not ours.\n */\nexport function holderOf(port: number): PortHolder | undefined {\n\tlet out: string;\n\ttry {\n\t\tout = execFileSync(\n\t\t\t'lsof',\n\t\t\t['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc'],\n\t\t\t{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },\n\t\t);\n\t} catch {\n\t\treturn undefined;\n\t}\n\n\tconst pid = Number(out.match(/^p(\\d+)/m)?.[1]);\n\tif (!pid) return undefined;\n\tconst command = out.match(/^c(.+)$/m)?.[1];\n\tconst tag = lineageTagOf(pid);\n\n\treturn {\n\t\tpid,\n\t\t...(command ? { command } : {}),\n\t\t...(tag ? { tag } : {}),\n\t};\n}\n\n/** How far up a holder's parents to look before calling it untagged. */\nconst MAX_ANCESTORS = 8;\n\n/**\n * The tag of `pid`, or of its nearest parent that has one.\n *\n * A process can hide the tag it inherited: `next-server` renames itself, which\n * on macOS overwrites the memory `ps eww` reads its environment from, so `ps`\n * shows only its title. Its parent, the `next dev` gkm started, still carries\n * the tag. A child is only ever started by its parent, so a tagged parent makes\n * the holder this app's.\n */\nfunction lineageTagOf(pid: number): string | undefined {\n\tlet current: number | undefined = pid;\n\tfor (let depth = 0; current && current > 1; depth += 1) {\n\t\tconst tag = tagOf(current);\n\t\tif (tag || depth === MAX_ANCESTORS) return tag;\n\t\tcurrent = parentOf(current);\n\t}\n\treturn undefined;\n}\n\nfunction tagOf(pid: number): string | undefined {\n\tconst prefix = `${APP_TAG_ENV}=`;\n\ttry {\n\t\tconst entries =\n\t\t\tprocess.platform === 'linux'\n\t\t\t\t? readFileSync(`/proc/${pid}/environ`, 'utf8').split('\\0')\n\t\t\t\t: execFileSync('ps', ['eww', '-o', 'command=', '-p', String(pid)], {\n\t\t\t\t\t\tencoding: 'utf8',\n\t\t\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t\t\t}).split(/\\s+/);\n\t\treturn entries\n\t\t\t.find((entry) => entry.startsWith(prefix))\n\t\t\t?.slice(prefix.length);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction parentOf(pid: number): number | undefined {\n\ttry {\n\t\tconst ppid = Number(\n\t\t\texecFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t}).trim(),\n\t\t);\n\t\treturn ppid > 0 ? ppid : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** The ports apps were given before, by app name. */\nexport async function savedAppPorts(\n\troot: string,\n): Promise<Record<string, number>> {\n\ttry {\n\t\treturn JSON.parse(await readFile(join(root, APP_PORTS_PATH), 'utf8'));\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nasync function saveAppPorts(\n\troot: string,\n\tports: Record<string, number>,\n): Promise<void> {\n\tawait mkdir(join(root, '.gkm'), { recursive: true });\n\tawait writeFile(\n\t\tjoin(root, APP_PORTS_PATH),\n\t\t`${JSON.stringify(ports, null, 2)}\\n`,\n\t);\n}\n\n/** The workspace, with each app on the port it was given locally. */\nexport function withAppPorts(\n\tworkspace: NormalizedWorkspace,\n\tports: Readonly<Record<string, number>>,\n): NormalizedWorkspace {\n\tconst apps = Object.fromEntries(\n\t\tObject.entries(workspace.apps).map(([name, app]) => [\n\t\t\tname,\n\t\t\tports[name] ? { ...app, port: ports[name] } : app,\n\t\t]),\n\t);\n\treturn { ...workspace, apps };\n}\n\n/** An app this workspace already has running — left by a previous start. */\nexport interface AppRunning {\n\tapp: string;\n\tport: number;\n\tholder: PortHolder;\n}\n\n/** An app that moved, because another project holds its port. */\nexport interface AppMoved {\n\tapp: string;\n\tfrom: number;\n\tto: number;\n\tholder: PortHolder | undefined;\n}\n\nexport interface AppPortsDeps {\n\t/** Whether a port can be bound right now. */\n\tfree: (port: number) => Promise<boolean>;\n\t/** Who holds a port. */\n\tholder: (port: number) => PortHolder | undefined;\n}\n\n/**\n * Give each named app a local port: the one it had, or its usual one, unless\n * another project holds it — then the next free one, which is kept.\n *\n * @returns every app's port, the apps that moved, and the apps this workspace\n * already has running (the caller refuses to start those).\n */\nexport async function assignAppPorts(\n\tworkspace: NormalizedWorkspace,\n\tnames: readonly string[],\n\tdeps: AppPortsDeps,\n): Promise<{\n\tports: Record<string, number>;\n\tmoved: AppMoved[];\n\trunning: AppRunning[];\n}> {\n\tconst saved = await savedAppPorts(workspace.root);\n\tconst wanted: Record<string, number> = {};\n\tfor (const name of names) {\n\t\tconst port = saved[name] ?? workspace.apps[name]?.port;\n\t\tif (port) wanted[name] = port;\n\t}\n\n\t// Every app's wanted port is reserved before any moves, so one that moves\n\t// never lands on a sibling's.\n\tconst taken = new Set([...Object.values(saved), ...Object.values(wanted)]);\n\tconst ports: Record<string, number> = { ...saved };\n\tconst moved: AppMoved[] = [];\n\tconst running: AppRunning[] = [];\n\n\t// Sorted, so which app moves first — and where — does not depend on order.\n\tfor (const name of Object.keys(wanted).sort()) {\n\t\tconst want = wanted[name]!;\n\t\tports[name] = want;\n\t\tif (await deps.free(want)) continue;\n\n\t\tconst holder = deps.holder(want);\n\t\tif (holder?.tag === appTag(workspace.root, name)) {\n\t\t\trunning.push({ app: name, port: want, holder });\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet next = want + 1;\n\t\twhile (taken.has(next) || !(await deps.free(next))) next += 1;\n\t\ttaken.add(next);\n\t\tports[name] = next;\n\t\tmoved.push({ app: name, from: want, to: next, holder });\n\t}\n\n\tawait saveAppPorts(workspace.root, ports);\n\treturn { ports, moved, running };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAa,cAAc;AAE3B,MAAM,iBAAiB;;AAGvB,SAAgB,OAAO,MAAc,KAAqB;CACzD,OAAO,GAAG,oBAAA,GAAmBA,UAAAA,QAAAA,CAAQ,IAAI,CAAC,EAAE,GAAG;AAChD;;AAUA,SAAgB,eAAe,QAAwC;CACtE,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,GAAG,OAAO,WAAW,YAAY,QAAQ,OAAO,IAAI;AAC5D;;;;;;;AAQA,SAAgB,SAAS,MAAsC;CAC9D,IAAI;CACJ,IAAI;EACH,OAAA,GAAMC,mBAAAA,aAAAA,CACL,QACA;GAAC;GAAO,SAAS;GAAQ;GAAgB;EAAM,GAC/C;GAAE,UAAU;GAAQ,OAAO;IAAC;IAAU;IAAQ;GAAQ;EAAE,CACzD;CACD,QAAQ;EACP;CACD;CAEA,MAAM,MAAM,OAAO,IAAI,MAAM,UAAU,CAAC,GAAG,EAAE;CAC7C,IAAI,CAAC,KAAK,OAAO,KAAA;CACjB,MAAM,UAAU,IAAI,MAAM,UAAU,CAAC,GAAG;CACxC,MAAM,MAAM,aAAa,GAAG;CAE5B,OAAO;EACN;EACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC7B,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;CACtB;AACD;;AAGA,MAAM,gBAAgB;;;;;;;;;;AAWtB,SAAS,aAAa,KAAiC;CACtD,IAAI,UAA8B;CAClC,KAAK,IAAI,QAAQ,GAAG,WAAW,UAAU,GAAG,SAAS,GAAG;EACvD,MAAM,MAAM,MAAM,OAAO;EACzB,IAAI,OAAO,UAAU,eAAe,OAAO;EAC3C,UAAU,SAAS,OAAO;CAC3B;AAED;AAEA,SAAS,MAAM,KAAiC;CAC/C,MAAM,SAAS,GAAG,YAAY;CAC9B,IAAI;EAQH,QANC,QAAQ,aAAa,WAAA,GAClBC,QAAAA,aAAAA,CAAa,SAAS,IAAI,WAAW,MAAM,CAAC,CAAC,MAAM,IAAI,KAAA,GACvDD,mBAAAA,aAAAA,CAAa,MAAM;GAAC;GAAO;GAAM;GAAY;GAAM,OAAO,GAAG;EAAC,GAAG;GACjE,UAAU;GACV,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CAAC,CAAC,CAAC,MAAM,KAAK,EAAA,CAEf,MAAM,UAAU,MAAM,WAAW,MAAM,CAAC,CAAC,EACxC,MAAM,OAAO,MAAM;CACvB,QAAQ;EACP;CACD;AACD;AAEA,SAAS,SAAS,KAAiC;CAClD,IAAI;EACH,MAAM,OAAO,QAAA,GACZA,mBAAAA,aAAAA,CAAa,MAAM;GAAC;GAAM;GAAS;GAAM,OAAO,GAAG;EAAC,GAAG;GACtD,UAAU;GACV,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CAAC,CAAC,CAAC,KAAK,CACT;EACA,OAAO,OAAO,IAAI,OAAO,KAAA;CAC1B,QAAQ;EACP;CACD;AACD;;AAGA,eAAsB,cACrB,MACkC;CAClC,IAAI;EACH,OAAO,KAAK,MAAM,OAAA,GAAME,iBAAAA,SAAAA,EAAAA,GAASC,UAAAA,KAAAA,CAAK,MAAM,cAAc,GAAG,MAAM,CAAC;CACrE,QAAQ;EACP,OAAO,CAAC;CACT;AACD;AAEA,eAAe,aACd,MACA,OACgB;CAChB,OAAA,GAAMC,iBAAAA,MAAAA,EAAAA,GAAMD,UAAAA,KAAAA,CAAK,MAAM,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;CACnD,OAAA,GAAME,iBAAAA,UAAAA,EAAAA,GACLF,UAAAA,KAAAA,CAAK,MAAM,cAAc,GACzB,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,EAAE,GACnC;AACD;;AAGA,SAAgB,aACf,WACA,OACsB;CACtB,MAAM,OAAO,OAAO,YACnB,OAAO,QAAQ,UAAU,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,SAAS,CACnD,MACA,MAAM,QAAQ;EAAE,GAAG;EAAK,MAAM,MAAM;CAAM,IAAI,GAC/C,CAAC,CACF;CACA,OAAO;EAAE,GAAG;EAAW;CAAK;AAC7B;;;;;;;;AA+BA,eAAsB,eACrB,WACA,OACA,MAKE;CACF,MAAM,QAAQ,MAAM,cAAc,UAAU,IAAI;CAChD,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,OAAO;EACzB,MAAM,OAAO,MAAM,SAAS,UAAU,KAAK,KAAK,EAAE;EAClD,IAAI,MAAM,OAAO,QAAQ;CAC1B;CAIA,MAAM,wBAAQ,IAAI,IAAI,CAAC,GAAG,OAAO,OAAO,KAAK,GAAG,GAAG,OAAO,OAAO,MAAM,CAAC,CAAC;CACzE,MAAM,QAAgC,EAAE,GAAG,MAAM;CACjD,MAAM,QAAoB,CAAC;CAC3B,MAAM,UAAwB,CAAC;CAG/B,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,GAAG;EAC9C,MAAM,OAAO,OAAO;EACpB,MAAM,QAAQ;EACd,IAAI,MAAM,KAAK,KAAK,IAAI,GAAG;EAE3B,MAAM,SAAS,KAAK,OAAO,IAAI;EAC/B,IAAI,QAAQ,QAAQ,OAAO,UAAU,MAAM,IAAI,GAAG;GACjD,QAAQ,KAAK;IAAE,KAAK;IAAM,MAAM;IAAM;GAAO,CAAC;GAC9C;EACD;EAEA,IAAI,OAAO,OAAO;EAClB,OAAO,MAAM,IAAI,IAAI,KAAK,CAAE,MAAM,KAAK,KAAK,IAAI,GAAI,QAAQ;EAC5D,MAAM,IAAI,IAAI;EACd,MAAM,QAAQ;EACd,MAAM,KAAK;GAAE,KAAK;GAAM,MAAM;GAAM,IAAI;GAAM;EAAO,CAAC;CACvD;CAEA,MAAM,aAAa,UAAU,MAAM,KAAK;CACxC,OAAO;EAAE;EAAO;EAAO;CAAQ;AAChC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
|
|
2
|
+
//#region src/secrets/aws.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stage's secrets in SSM Parameter Store, in the stage's own AWS account.
|
|
5
|
+
*
|
|
6
|
+
* One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —
|
|
7
|
+
* encrypted at rest by KMS, so nothing about it needs a key file anywhere.
|
|
8
|
+
* Which account it is in is whichever credentials are active: the stage's
|
|
9
|
+
* OIDC role in a deploy job, or `--profile` on a developer's machine. With
|
|
10
|
+
* staging and production in different accounts, each stage's secrets sit
|
|
11
|
+
* beside its infrastructure, and a staging role cannot read production's.
|
|
12
|
+
*/
|
|
13
|
+
/** The parameter a stage's secrets are kept in. */
|
|
14
|
+
function secretsParameterName(project, stage) {
|
|
15
|
+
return `/gkm/${project}/${stage}/secrets`;
|
|
16
|
+
}
|
|
17
|
+
var AwsSecretsStore = class {
|
|
18
|
+
options;
|
|
19
|
+
name = "ssm";
|
|
20
|
+
client;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
async ssm() {
|
|
25
|
+
if (this.client) return this.client;
|
|
26
|
+
const config = { region: this.options.region };
|
|
27
|
+
if (this.options.profile) {
|
|
28
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
29
|
+
config.credentials = fromIni({ profile: this.options.profile });
|
|
30
|
+
}
|
|
31
|
+
if (this.options.endpoint) config.endpoint = this.options.endpoint;
|
|
32
|
+
this.client = new _aws_sdk_client_ssm.SSMClient(config);
|
|
33
|
+
return this.client;
|
|
34
|
+
}
|
|
35
|
+
async read(stage) {
|
|
36
|
+
const ssm = await this.ssm();
|
|
37
|
+
try {
|
|
38
|
+
const { Parameter } = await ssm.send(new _aws_sdk_client_ssm.GetParameterCommand({
|
|
39
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
40
|
+
WithDecryption: true
|
|
41
|
+
}));
|
|
42
|
+
return Parameter?.Value ? JSON.parse(Parameter.Value) : null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof _aws_sdk_client_ssm.ParameterNotFound) return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async write(stage, secrets) {
|
|
49
|
+
await (await this.ssm()).send(new _aws_sdk_client_ssm.PutParameterCommand({
|
|
50
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
51
|
+
Value: JSON.stringify(secrets),
|
|
52
|
+
Type: "SecureString",
|
|
53
|
+
Overwrite: true,
|
|
54
|
+
Description: `gkm secrets for ${this.options.project}/${stage}`
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
exports.AwsSecretsStore = AwsSecretsStore;
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=aws-DkHdTV-p.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws-DkHdTV-p.cjs","names":["SSMClient","GetParameterCommand","ParameterNotFound","PutParameterCommand"],"sources":["../src/secrets/aws.ts"],"sourcesContent":["/**\n * A stage's secrets in SSM Parameter Store, in the stage's own AWS account.\n *\n * One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —\n * encrypted at rest by KMS, so nothing about it needs a key file anywhere.\n * Which account it is in is whichever credentials are active: the stage's\n * OIDC role in a deploy job, or `--profile` on a developer's machine. With\n * staging and production in different accounts, each stage's secrets sit\n * beside its infrastructure, and a staging role cannot read production's.\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface AwsSecretsStoreOptions {\n\t/** The workspace name, which scopes the parameter path. */\n\tproject: string;\n\tregion: string;\n\t/**\n\t * A named AWS profile, resolved with `fromIni` only. The default chain\n\t * reads AWS_* from the environment first, so exported staging keys would\n\t * win over `--profile prod` and put production's secrets in staging.\n\t */\n\tprofile?: string;\n\t/** For tests: an endpoint such as the AWS emulator. */\n\tendpoint?: string;\n}\n\n/** The parameter a stage's secrets are kept in. */\nexport function secretsParameterName(project: string, stage: string): string {\n\treturn `/gkm/${project}/${stage}/secrets`;\n}\n\nexport class AwsSecretsStore implements SecretsStore {\n\treadonly name = 'ssm';\n\n\tprivate client?: SSMClient;\n\n\tconstructor(private readonly options: AwsSecretsStoreOptions) {}\n\n\tprivate async ssm(): Promise<SSMClient> {\n\t\tif (this.client) return this.client;\n\n\t\tconst config: SSMClientConfig = { region: this.options.region };\n\t\tif (this.options.profile) {\n\t\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: this.options.profile });\n\t\t}\n\t\tif (this.options.endpoint) {\n\t\t\tconfig.endpoint = this.options.endpoint;\n\t\t}\n\n\t\tthis.client = new SSMClient(config);\n\t\treturn this.client;\n\t}\n\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst ssm = await this.ssm();\n\n\t\ttry {\n\t\t\tconst { Parameter } = await ssm.send(\n\t\t\t\tnew GetParameterCommand({\n\t\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\t\tWithDecryption: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Parameter?.Value\n\t\t\t\t? (JSON.parse(Parameter.Value) as StageSecrets)\n\t\t\t\t: null;\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\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst ssm = await this.ssm();\n\n\t\tawait ssm.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\tValue: JSON.stringify(secrets),\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription: `gkm secrets for ${this.options.project}/${stage}`,\n\t\t\t}),\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,SAAiB,OAAuB;CAC5E,OAAO,QAAQ,QAAQ,GAAG,MAAM;AACjC;AAEA,IAAa,kBAAb,MAAqD;CAKvB;CAJ7B,OAAgB;CAEhB;CAEA,YAAY,SAAkD;EAAjC,KAAA,UAAA;CAAkC;CAE/D,MAAc,MAA0B;EACvC,IAAI,KAAK,QAAQ,OAAO,KAAK;EAE7B,MAAM,SAA0B,EAAE,QAAQ,KAAK,QAAQ,OAAO;EAC9D,IAAI,KAAK,QAAQ,SAAS;GACzB,MAAM,EAAE,YAAY,MAAM,OAAO;GACjC,OAAO,cAAc,QAAQ,EAAE,SAAS,KAAK,QAAQ,QAAQ,CAAC;EAC/D;EACA,IAAI,KAAK,QAAQ,UAChB,OAAO,WAAW,KAAK,QAAQ;EAGhC,KAAK,SAAS,IAAIA,oBAAAA,UAAU,MAAM;EAClC,OAAO,KAAK;CACb;CAEA,MAAM,KAAK,OAA6C;EACvD,MAAM,MAAM,MAAM,KAAK,IAAI;EAE3B,IAAI;GACH,MAAM,EAAE,cAAc,MAAM,IAAI,KAC/B,IAAIC,oBAAAA,oBAAoB;IACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;IACtD,gBAAgB;GACjB,CAAC,CACF;GACA,OAAO,WAAW,QACd,KAAK,MAAM,UAAU,KAAK,IAC3B;EACJ,SAAS,OAAO;GACf,IAAI,iBAAiBC,oBAAAA,mBAAmB,OAAO;GAC/C,MAAM;EACP;CACD;CAEA,MAAM,MAAM,OAAe,SAAsC;EAGhE,OAAM,MAFY,KAAK,IAAI,EAAA,CAEjB,KACT,IAAIC,oBAAAA,oBAAoB;GACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;GACtD,OAAO,KAAK,UAAU,OAAO;GAC7B,MAAM;GACN,WAAW;GACX,aAAa,mBAAmB,KAAK,QAAQ,QAAQ,GAAG;EACzD,CAAC,CACF;CACD;AACD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GetParameterCommand, ParameterNotFound, PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
2
|
+
//#region src/secrets/aws.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stage's secrets in SSM Parameter Store, in the stage's own AWS account.
|
|
5
|
+
*
|
|
6
|
+
* One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —
|
|
7
|
+
* encrypted at rest by KMS, so nothing about it needs a key file anywhere.
|
|
8
|
+
* Which account it is in is whichever credentials are active: the stage's
|
|
9
|
+
* OIDC role in a deploy job, or `--profile` on a developer's machine. With
|
|
10
|
+
* staging and production in different accounts, each stage's secrets sit
|
|
11
|
+
* beside its infrastructure, and a staging role cannot read production's.
|
|
12
|
+
*/
|
|
13
|
+
/** The parameter a stage's secrets are kept in. */
|
|
14
|
+
function secretsParameterName(project, stage) {
|
|
15
|
+
return `/gkm/${project}/${stage}/secrets`;
|
|
16
|
+
}
|
|
17
|
+
var AwsSecretsStore = class {
|
|
18
|
+
options;
|
|
19
|
+
name = "ssm";
|
|
20
|
+
client;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
async ssm() {
|
|
25
|
+
if (this.client) return this.client;
|
|
26
|
+
const config = { region: this.options.region };
|
|
27
|
+
if (this.options.profile) {
|
|
28
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
29
|
+
config.credentials = fromIni({ profile: this.options.profile });
|
|
30
|
+
}
|
|
31
|
+
if (this.options.endpoint) config.endpoint = this.options.endpoint;
|
|
32
|
+
this.client = new SSMClient(config);
|
|
33
|
+
return this.client;
|
|
34
|
+
}
|
|
35
|
+
async read(stage) {
|
|
36
|
+
const ssm = await this.ssm();
|
|
37
|
+
try {
|
|
38
|
+
const { Parameter } = await ssm.send(new GetParameterCommand({
|
|
39
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
40
|
+
WithDecryption: true
|
|
41
|
+
}));
|
|
42
|
+
return Parameter?.Value ? JSON.parse(Parameter.Value) : null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof ParameterNotFound) return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async write(stage, secrets) {
|
|
49
|
+
await (await this.ssm()).send(new PutParameterCommand({
|
|
50
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
51
|
+
Value: JSON.stringify(secrets),
|
|
52
|
+
Type: "SecureString",
|
|
53
|
+
Overwrite: true,
|
|
54
|
+
Description: `gkm secrets for ${this.options.project}/${stage}`
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { AwsSecretsStore };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=aws-DojzQnLd.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws-DojzQnLd.mjs","names":[],"sources":["../src/secrets/aws.ts"],"sourcesContent":["/**\n * A stage's secrets in SSM Parameter Store, in the stage's own AWS account.\n *\n * One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —\n * encrypted at rest by KMS, so nothing about it needs a key file anywhere.\n * Which account it is in is whichever credentials are active: the stage's\n * OIDC role in a deploy job, or `--profile` on a developer's machine. With\n * staging and production in different accounts, each stage's secrets sit\n * beside its infrastructure, and a staging role cannot read production's.\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface AwsSecretsStoreOptions {\n\t/** The workspace name, which scopes the parameter path. */\n\tproject: string;\n\tregion: string;\n\t/**\n\t * A named AWS profile, resolved with `fromIni` only. The default chain\n\t * reads AWS_* from the environment first, so exported staging keys would\n\t * win over `--profile prod` and put production's secrets in staging.\n\t */\n\tprofile?: string;\n\t/** For tests: an endpoint such as the AWS emulator. */\n\tendpoint?: string;\n}\n\n/** The parameter a stage's secrets are kept in. */\nexport function secretsParameterName(project: string, stage: string): string {\n\treturn `/gkm/${project}/${stage}/secrets`;\n}\n\nexport class AwsSecretsStore implements SecretsStore {\n\treadonly name = 'ssm';\n\n\tprivate client?: SSMClient;\n\n\tconstructor(private readonly options: AwsSecretsStoreOptions) {}\n\n\tprivate async ssm(): Promise<SSMClient> {\n\t\tif (this.client) return this.client;\n\n\t\tconst config: SSMClientConfig = { region: this.options.region };\n\t\tif (this.options.profile) {\n\t\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: this.options.profile });\n\t\t}\n\t\tif (this.options.endpoint) {\n\t\t\tconfig.endpoint = this.options.endpoint;\n\t\t}\n\n\t\tthis.client = new SSMClient(config);\n\t\treturn this.client;\n\t}\n\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst ssm = await this.ssm();\n\n\t\ttry {\n\t\t\tconst { Parameter } = await ssm.send(\n\t\t\t\tnew GetParameterCommand({\n\t\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\t\tWithDecryption: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Parameter?.Value\n\t\t\t\t? (JSON.parse(Parameter.Value) as StageSecrets)\n\t\t\t\t: null;\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\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst ssm = await this.ssm();\n\n\t\tawait ssm.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\tValue: JSON.stringify(secrets),\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription: `gkm secrets for ${this.options.project}/${stage}`,\n\t\t\t}),\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,SAAiB,OAAuB;CAC5E,OAAO,QAAQ,QAAQ,GAAG,MAAM;AACjC;AAEA,IAAa,kBAAb,MAAqD;CAKvB;CAJ7B,OAAgB;CAEhB;CAEA,YAAY,SAAkD;EAAjC,KAAA,UAAA;CAAkC;CAE/D,MAAc,MAA0B;EACvC,IAAI,KAAK,QAAQ,OAAO,KAAK;EAE7B,MAAM,SAA0B,EAAE,QAAQ,KAAK,QAAQ,OAAO;EAC9D,IAAI,KAAK,QAAQ,SAAS;GACzB,MAAM,EAAE,YAAY,MAAM,OAAO;GACjC,OAAO,cAAc,QAAQ,EAAE,SAAS,KAAK,QAAQ,QAAQ,CAAC;EAC/D;EACA,IAAI,KAAK,QAAQ,UAChB,OAAO,WAAW,KAAK,QAAQ;EAGhC,KAAK,SAAS,IAAI,UAAU,MAAM;EAClC,OAAO,KAAK;CACb;CAEA,MAAM,KAAK,OAA6C;EACvD,MAAM,MAAM,MAAM,KAAK,IAAI;EAE3B,IAAI;GACH,MAAM,EAAE,cAAc,MAAM,IAAI,KAC/B,IAAI,oBAAoB;IACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;IACtD,gBAAgB;GACjB,CAAC,CACF;GACA,OAAO,WAAW,QACd,KAAK,MAAM,UAAU,KAAK,IAC3B;EACJ,SAAS,OAAO;GACf,IAAI,iBAAiB,mBAAmB,OAAO;GAC/C,MAAM;EACP;CACD;CAEA,MAAM,MAAM,OAAe,SAAsC;EAGhE,OAAM,MAFY,KAAK,IAAI,EAAA,CAEjB,KACT,IAAI,oBAAoB;GACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;GACtD,OAAO,KAAK,UAAU,OAAO;GAC7B,MAAM;GACN,WAAW;GACX,aAAa,mBAAmB,KAAK,QAAQ,QAAQ,GAAG;EACzD,CAAC,CACF;CACD;AACD"}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const __aws_sdk_client_iam = require_chunk.__toESM(require("@aws-sdk/client-iam"));
|
|
4
|
-
|
|
1
|
+
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
2
|
+
let _aws_sdk_client_iam = require("@aws-sdk/client-iam");
|
|
5
3
|
//#region src/deploy/backup-provisioner.ts
|
|
6
4
|
/**
|
|
5
|
+
* Backup Destination Provisioner
|
|
6
|
+
*
|
|
7
|
+
* Creates AWS resources (S3 bucket, IAM user, access keys) and configures
|
|
8
|
+
* Dokploy backup destinations for database backups.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
7
11
|
* Generate a random suffix for unique resource names
|
|
8
12
|
*/
|
|
9
13
|
function randomSuffix() {
|
|
@@ -33,8 +37,8 @@ async function createAwsClients(region, profile, endpoint) {
|
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
return {
|
|
36
|
-
s3: new
|
|
37
|
-
iam: new
|
|
40
|
+
s3: new _aws_sdk_client_s3.S3Client(config),
|
|
41
|
+
iam: new _aws_sdk_client_iam.IAMClient(config)
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
@@ -42,7 +46,7 @@ async function createAwsClients(region, profile, endpoint) {
|
|
|
42
46
|
*/
|
|
43
47
|
async function bucketExists(s3, bucketName) {
|
|
44
48
|
try {
|
|
45
|
-
await s3.send(new
|
|
49
|
+
await s3.send(new _aws_sdk_client_s3.HeadBucketCommand({ Bucket: bucketName }));
|
|
46
50
|
return true;
|
|
47
51
|
} catch (error) {
|
|
48
52
|
if (error.name === "NotFound") return false;
|
|
@@ -55,7 +59,7 @@ async function bucketExists(s3, bucketName) {
|
|
|
55
59
|
*/
|
|
56
60
|
async function userExists(iam, userName) {
|
|
57
61
|
try {
|
|
58
|
-
await iam.send(new
|
|
62
|
+
await iam.send(new _aws_sdk_client_iam.GetUserCommand({ UserName: userName }));
|
|
59
63
|
return true;
|
|
60
64
|
} catch (error) {
|
|
61
65
|
const errorName = error.name;
|
|
@@ -81,22 +85,20 @@ async function provisionBackupDestination(options) {
|
|
|
81
85
|
const aws = await createAwsClients(config.region, config.profile, awsEndpoint);
|
|
82
86
|
const sanitizedProject = sanitizeName(projectName);
|
|
83
87
|
const bucketName = existingState?.bucketName ?? `${sanitizedProject}-${stage}-backups-${randomSuffix()}`;
|
|
84
|
-
|
|
85
|
-
if (!bucketAlreadyExists) {
|
|
88
|
+
if (!await bucketExists(aws.s3, bucketName)) {
|
|
86
89
|
logger.log(` Creating S3 bucket: ${bucketName}`);
|
|
87
90
|
const createBucketParams = { Bucket: bucketName };
|
|
88
91
|
if (config.region !== "us-east-1") createBucketParams.CreateBucketConfiguration = { LocationConstraint: config.region };
|
|
89
|
-
await aws.s3.send(new
|
|
90
|
-
await aws.s3.send(new
|
|
92
|
+
await aws.s3.send(new _aws_sdk_client_s3.CreateBucketCommand(createBucketParams));
|
|
93
|
+
await aws.s3.send(new _aws_sdk_client_s3.PutBucketVersioningCommand({
|
|
91
94
|
Bucket: bucketName,
|
|
92
95
|
VersioningConfiguration: { Status: "Enabled" }
|
|
93
96
|
}));
|
|
94
97
|
} else logger.log(` Using existing S3 bucket: ${bucketName}`);
|
|
95
98
|
const iamUserName = existingState?.iamUserName ?? `dokploy-backup-${sanitizedProject}-${stage}`;
|
|
96
|
-
|
|
97
|
-
if (!iamUserAlreadyExists) {
|
|
99
|
+
if (!await userExists(aws.iam, iamUserName)) {
|
|
98
100
|
logger.log(` Creating IAM user: ${iamUserName}`);
|
|
99
|
-
await aws.iam.send(new
|
|
101
|
+
await aws.iam.send(new _aws_sdk_client_iam.CreateUserCommand({ UserName: iamUserName }));
|
|
100
102
|
} else logger.log(` Using existing IAM user: ${iamUserName}`);
|
|
101
103
|
const policyDocument = {
|
|
102
104
|
Version: "2012-10-17",
|
|
@@ -113,7 +115,7 @@ async function provisionBackupDestination(options) {
|
|
|
113
115
|
}]
|
|
114
116
|
};
|
|
115
117
|
logger.log(" Updating IAM policy");
|
|
116
|
-
await aws.iam.send(new
|
|
118
|
+
await aws.iam.send(new _aws_sdk_client_iam.PutUserPolicyCommand({
|
|
117
119
|
UserName: iamUserName,
|
|
118
120
|
PolicyName: "DokployBackupAccess",
|
|
119
121
|
PolicyDocument: JSON.stringify(policyDocument)
|
|
@@ -126,7 +128,7 @@ async function provisionBackupDestination(options) {
|
|
|
126
128
|
secretAccessKey = existingState.iamSecretAccessKey;
|
|
127
129
|
} else {
|
|
128
130
|
logger.log(" Creating IAM access key");
|
|
129
|
-
const accessKeyResult = await aws.iam.send(new
|
|
131
|
+
const accessKeyResult = await aws.iam.send(new _aws_sdk_client_iam.CreateAccessKeyCommand({ UserName: iamUserName }));
|
|
130
132
|
if (!accessKeyResult.AccessKey) throw new Error("Failed to create IAM access key");
|
|
131
133
|
accessKeyId = accessKeyResult.AccessKey.AccessKeyId;
|
|
132
134
|
secretAccessKey = accessKeyResult.AccessKey.SecretAccessKey;
|
|
@@ -158,7 +160,7 @@ async function provisionBackupDestination(options) {
|
|
|
158
160
|
createdAt: existingState?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
|
-
|
|
162
163
|
//#endregion
|
|
163
164
|
exports.provisionBackupDestination = provisionBackupDestination;
|
|
164
|
-
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=backup-provisioner-BmhIO_Yr.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup-provisioner-BmhIO_Yr.cjs","names":["S3Client","IAMClient","HeadBucketCommand","GetUserCommand","CreateBucketCommand","PutBucketVersioningCommand","CreateUserCommand","PutUserPolicyCommand","CreateAccessKeyCommand"],"sources":["../src/deploy/backup-provisioner.ts"],"sourcesContent":["/**\n * Backup Destination Provisioner\n *\n * Creates AWS resources (S3 bucket, IAM user, access keys) and configures\n * Dokploy backup destinations for database backups.\n */\n\nimport {\n\tCreateAccessKeyCommand,\n\tCreateUserCommand,\n\tGetUserCommand,\n\tIAMClient,\n\ttype IAMClientConfig,\n\tPutUserPolicyCommand,\n} from '@aws-sdk/client-iam';\nimport {\n\ttype BucketLocationConstraint,\n\tCreateBucketCommand,\n\tHeadBucketCommand,\n\tPutBucketVersioningCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport type { BackupsConfig } from '../workspace/types.js';\nimport type { DokployApi } from './dokploy-api.js';\nimport type { BackupState } from './state.js';\n\nexport interface ProvisionBackupOptions {\n\t/** Dokploy API client */\n\tapi: DokployApi;\n\t/** Dokploy project ID */\n\tprojectId: string;\n\t/** Workspace name (used for resource naming) */\n\tprojectName: string;\n\t/** Deploy stage (e.g., 'production', 'staging') */\n\tstage: string;\n\t/** Backup configuration */\n\tconfig: BackupsConfig;\n\t/** Existing backup state (if any) */\n\texistingState?: BackupState;\n\t/** Logger for progress output */\n\tlogger: { log: (msg: string) => void };\n\t/** AWS endpoint override (for testing with LocalStack) */\n\tawsEndpoint?: string;\n}\n\n/**\n * Generate a random suffix for unique resource names\n */\nfunction randomSuffix(): string {\n\treturn Math.random().toString(36).substring(2, 8);\n}\n\n/**\n * Sanitize a name for AWS resources (lowercase alphanumeric and hyphens)\n */\nfunction sanitizeName(name: string): string {\n\treturn name.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n}\n\n/**\n * Create AWS clients with optional profile credentials\n */\nasync function createAwsClients(\n\tregion: string,\n\tprofile?: string,\n\tendpoint?: string,\n): Promise<{ s3: S3Client; iam: IAMClient }> {\n\tconst config: S3ClientConfig & IAMClientConfig = { region };\n\n\tif (profile) {\n\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\tconfig.credentials = fromIni({ profile });\n\t}\n\n\t// Support custom endpoint for testing (e.g., LocalStack)\n\tif (endpoint) {\n\t\tconfig.endpoint = endpoint;\n\t\t(config as S3ClientConfig).forcePathStyle = true;\n\t\t// Use test credentials when endpoint is specified\n\t\tconfig.credentials = {\n\t\t\taccessKeyId: 'test',\n\t\t\tsecretAccessKey: 'test',\n\t\t};\n\t}\n\n\treturn {\n\t\ts3: new S3Client(config),\n\t\tiam: new IAMClient(config),\n\t};\n}\n\n/**\n * Check if an S3 bucket exists\n */\nasync function bucketExists(\n\ts3: S3Client,\n\tbucketName: string,\n): Promise<boolean> {\n\ttry {\n\t\tawait s3.send(new HeadBucketCommand({ Bucket: bucketName }));\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as { name?: string }).name === 'NotFound') {\n\t\t\treturn false;\n\t\t}\n\t\t// 403 means bucket exists but we don't have access\n\t\tif (\n\t\t\t(error as { $metadata?: { httpStatusCode?: number } }).$metadata\n\t\t\t\t?.httpStatusCode === 403\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Check if an IAM user exists\n */\nasync function userExists(iam: IAMClient, userName: string): Promise<boolean> {\n\ttry {\n\t\tawait iam.send(new GetUserCommand({ UserName: userName }));\n\t\treturn true;\n\t} catch (error) {\n\t\tconst errorName = (error as { name?: string }).name;\n\t\t// AWS returns 'NoSuchEntity', LocalStack returns 'NoSuchEntityException'\n\t\tif (errorName === 'NoSuchEntity' || errorName === 'NoSuchEntityException') {\n\t\t\treturn false;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Provision backup destination for a deployment.\n *\n * Creates AWS resources (S3 bucket, IAM user) and Dokploy destination if needed.\n * Reuses existing resources from state when possible.\n */\nexport async function provisionBackupDestination(\n\toptions: ProvisionBackupOptions,\n): Promise<BackupState> {\n\tconst {\n\t\tapi,\n\t\tprojectName,\n\t\tstage,\n\t\tconfig,\n\t\texistingState,\n\t\tlogger,\n\t\tawsEndpoint,\n\t} = options;\n\n\t// If we have existing state, verify the Dokploy destination still exists\n\tif (existingState?.destinationId) {\n\t\ttry {\n\t\t\tawait api.getDestination(existingState.destinationId);\n\t\t\tlogger.log(' Using existing backup destination');\n\t\t\treturn existingState;\n\t\t} catch {\n\t\t\tlogger.log(' Existing destination not found, recreating...');\n\t\t}\n\t}\n\n\t// Create AWS clients\n\tconst aws = await createAwsClients(\n\t\tconfig.region,\n\t\tconfig.profile,\n\t\tawsEndpoint,\n\t);\n\tconst sanitizedProject = sanitizeName(projectName);\n\n\t// 1. Create or verify S3 bucket\n\tconst bucketName =\n\t\texistingState?.bucketName ??\n\t\t`${sanitizedProject}-${stage}-backups-${randomSuffix()}`;\n\n\tconst bucketAlreadyExists = await bucketExists(aws.s3, bucketName);\n\tif (!bucketAlreadyExists) {\n\t\tlogger.log(` Creating S3 bucket: ${bucketName}`);\n\n\t\t// CreateBucket needs LocationConstraint for non-us-east-1 regions\n\t\tconst createBucketParams: {\n\t\t\tBucket: string;\n\t\t\tCreateBucketConfiguration?: {\n\t\t\t\tLocationConstraint: BucketLocationConstraint;\n\t\t\t};\n\t\t} = {\n\t\t\tBucket: bucketName,\n\t\t};\n\t\tif (config.region !== 'us-east-1') {\n\t\t\tcreateBucketParams.CreateBucketConfiguration = {\n\t\t\t\tLocationConstraint: config.region as BucketLocationConstraint,\n\t\t\t};\n\t\t}\n\n\t\tawait aws.s3.send(new CreateBucketCommand(createBucketParams));\n\n\t\t// Enable versioning for backup integrity\n\t\tawait aws.s3.send(\n\t\t\tnew PutBucketVersioningCommand({\n\t\t\t\tBucket: bucketName,\n\t\t\t\tVersioningConfiguration: { Status: 'Enabled' },\n\t\t\t}),\n\t\t);\n\t} else {\n\t\tlogger.log(` Using existing S3 bucket: ${bucketName}`);\n\t}\n\n\t// 2. Create or verify IAM user\n\tconst iamUserName =\n\t\texistingState?.iamUserName ?? `dokploy-backup-${sanitizedProject}-${stage}`;\n\n\tconst iamUserAlreadyExists = await userExists(aws.iam, iamUserName);\n\tif (!iamUserAlreadyExists) {\n\t\tlogger.log(` Creating IAM user: ${iamUserName}`);\n\t\tawait aws.iam.send(new CreateUserCommand({ UserName: iamUserName }));\n\t} else {\n\t\tlogger.log(` Using existing IAM user: ${iamUserName}`);\n\t}\n\n\t// 3. Attach bucket policy to IAM user\n\tconst policyDocument = {\n\t\tVersion: '2012-10-17',\n\t\tStatement: [\n\t\t\t{\n\t\t\t\tEffect: 'Allow',\n\t\t\t\tAction: [\n\t\t\t\t\t's3:GetObject',\n\t\t\t\t\t's3:PutObject',\n\t\t\t\t\t's3:DeleteObject',\n\t\t\t\t\t's3:ListBucket',\n\t\t\t\t\t's3:GetBucketLocation',\n\t\t\t\t],\n\t\t\t\tResource: [\n\t\t\t\t\t`arn:aws:s3:::${bucketName}`,\n\t\t\t\t\t`arn:aws:s3:::${bucketName}/*`,\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t};\n\n\tlogger.log(' Updating IAM policy');\n\tawait aws.iam.send(\n\t\tnew PutUserPolicyCommand({\n\t\t\tUserName: iamUserName,\n\t\t\tPolicyName: 'DokployBackupAccess',\n\t\t\tPolicyDocument: JSON.stringify(policyDocument),\n\t\t}),\n\t);\n\n\t// 4. Create access key (or reuse existing if state has it and destination needs recreation)\n\tlet accessKeyId: string;\n\tlet secretAccessKey: string;\n\n\tif (existingState?.iamAccessKeyId && existingState?.iamSecretAccessKey) {\n\t\t// Reuse existing credentials\n\t\tlogger.log(' Using existing IAM access key');\n\t\taccessKeyId = existingState.iamAccessKeyId;\n\t\tsecretAccessKey = existingState.iamSecretAccessKey;\n\t} else {\n\t\t// Create new access key\n\t\tlogger.log(' Creating IAM access key');\n\t\tconst accessKeyResult = await aws.iam.send(\n\t\t\tnew CreateAccessKeyCommand({ UserName: iamUserName }),\n\t\t);\n\n\t\tif (!accessKeyResult.AccessKey) {\n\t\t\tthrow new Error('Failed to create IAM access key');\n\t\t}\n\n\t\taccessKeyId = accessKeyResult.AccessKey.AccessKeyId!;\n\t\tsecretAccessKey = accessKeyResult.AccessKey.SecretAccessKey!;\n\t}\n\n\t// 5. Create Dokploy destination\n\tconst destinationName = `${sanitizedProject}-${stage}-s3`;\n\tlogger.log(` Creating Dokploy destination: ${destinationName}`);\n\n\tconst { destination, created } = await api.findOrCreateDestination(\n\t\tdestinationName,\n\t\t{\n\t\t\taccessKey: accessKeyId,\n\t\t\tsecretAccessKey: secretAccessKey,\n\t\t\tbucket: bucketName,\n\t\t\tregion: config.region,\n\t\t},\n\t);\n\n\tif (created) {\n\t\tlogger.log(' ✓ Dokploy destination created');\n\t} else {\n\t\tlogger.log(' ✓ Using existing Dokploy destination');\n\t}\n\n\t// 6. Test connection\n\ttry {\n\t\tawait api.testDestinationConnection(destination.destinationId);\n\t\tlogger.log(' ✓ Destination connection verified');\n\t} catch (error) {\n\t\tlogger.log(\n\t\t\t` ⚠ Warning: Could not verify destination connection: ${error}`,\n\t\t);\n\t}\n\n\treturn {\n\t\tbucketName,\n\t\tbucketArn: `arn:aws:s3:::${bucketName}`,\n\t\tiamUserName,\n\t\tiamAccessKeyId: accessKeyId,\n\t\tiamSecretAccessKey: secretAccessKey,\n\t\tdestinationId: destination.destinationId,\n\t\tregion: config.region,\n\t\tcreatedAt: existingState?.createdAt ?? new Date().toISOString(),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { CreateBucketCommand, HeadBucketCommand, PutBucketVersioningCommand, S3Client } from "@aws-sdk/client-s3";
|
|
2
2
|
import { CreateAccessKeyCommand, CreateUserCommand, GetUserCommand, IAMClient, PutUserPolicyCommand } from "@aws-sdk/client-iam";
|
|
3
|
-
|
|
4
3
|
//#region src/deploy/backup-provisioner.ts
|
|
5
4
|
/**
|
|
5
|
+
* Backup Destination Provisioner
|
|
6
|
+
*
|
|
7
|
+
* Creates AWS resources (S3 bucket, IAM user, access keys) and configures
|
|
8
|
+
* Dokploy backup destinations for database backups.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
6
11
|
* Generate a random suffix for unique resource names
|
|
7
12
|
*/
|
|
8
13
|
function randomSuffix() {
|
|
@@ -80,8 +85,7 @@ async function provisionBackupDestination(options) {
|
|
|
80
85
|
const aws = await createAwsClients(config.region, config.profile, awsEndpoint);
|
|
81
86
|
const sanitizedProject = sanitizeName(projectName);
|
|
82
87
|
const bucketName = existingState?.bucketName ?? `${sanitizedProject}-${stage}-backups-${randomSuffix()}`;
|
|
83
|
-
|
|
84
|
-
if (!bucketAlreadyExists) {
|
|
88
|
+
if (!await bucketExists(aws.s3, bucketName)) {
|
|
85
89
|
logger.log(` Creating S3 bucket: ${bucketName}`);
|
|
86
90
|
const createBucketParams = { Bucket: bucketName };
|
|
87
91
|
if (config.region !== "us-east-1") createBucketParams.CreateBucketConfiguration = { LocationConstraint: config.region };
|
|
@@ -92,8 +96,7 @@ async function provisionBackupDestination(options) {
|
|
|
92
96
|
}));
|
|
93
97
|
} else logger.log(` Using existing S3 bucket: ${bucketName}`);
|
|
94
98
|
const iamUserName = existingState?.iamUserName ?? `dokploy-backup-${sanitizedProject}-${stage}`;
|
|
95
|
-
|
|
96
|
-
if (!iamUserAlreadyExists) {
|
|
99
|
+
if (!await userExists(aws.iam, iamUserName)) {
|
|
97
100
|
logger.log(` Creating IAM user: ${iamUserName}`);
|
|
98
101
|
await aws.iam.send(new CreateUserCommand({ UserName: iamUserName }));
|
|
99
102
|
} else logger.log(` Using existing IAM user: ${iamUserName}`);
|
|
@@ -157,7 +160,7 @@ async function provisionBackupDestination(options) {
|
|
|
157
160
|
createdAt: existingState?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
|
-
|
|
161
163
|
//#endregion
|
|
162
164
|
export { provisionBackupDestination };
|
|
163
|
-
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=backup-provisioner-DIVCbfee.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup-provisioner-DIVCbfee.mjs","names":[],"sources":["../src/deploy/backup-provisioner.ts"],"sourcesContent":["/**\n * Backup Destination Provisioner\n *\n * Creates AWS resources (S3 bucket, IAM user, access keys) and configures\n * Dokploy backup destinations for database backups.\n */\n\nimport {\n\tCreateAccessKeyCommand,\n\tCreateUserCommand,\n\tGetUserCommand,\n\tIAMClient,\n\ttype IAMClientConfig,\n\tPutUserPolicyCommand,\n} from '@aws-sdk/client-iam';\nimport {\n\ttype BucketLocationConstraint,\n\tCreateBucketCommand,\n\tHeadBucketCommand,\n\tPutBucketVersioningCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport type { BackupsConfig } from '../workspace/types.js';\nimport type { DokployApi } from './dokploy-api.js';\nimport type { BackupState } from './state.js';\n\nexport interface ProvisionBackupOptions {\n\t/** Dokploy API client */\n\tapi: DokployApi;\n\t/** Dokploy project ID */\n\tprojectId: string;\n\t/** Workspace name (used for resource naming) */\n\tprojectName: string;\n\t/** Deploy stage (e.g., 'production', 'staging') */\n\tstage: string;\n\t/** Backup configuration */\n\tconfig: BackupsConfig;\n\t/** Existing backup state (if any) */\n\texistingState?: BackupState;\n\t/** Logger for progress output */\n\tlogger: { log: (msg: string) => void };\n\t/** AWS endpoint override (for testing with LocalStack) */\n\tawsEndpoint?: string;\n}\n\n/**\n * Generate a random suffix for unique resource names\n */\nfunction randomSuffix(): string {\n\treturn Math.random().toString(36).substring(2, 8);\n}\n\n/**\n * Sanitize a name for AWS resources (lowercase alphanumeric and hyphens)\n */\nfunction sanitizeName(name: string): string {\n\treturn name.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n}\n\n/**\n * Create AWS clients with optional profile credentials\n */\nasync function createAwsClients(\n\tregion: string,\n\tprofile?: string,\n\tendpoint?: string,\n): Promise<{ s3: S3Client; iam: IAMClient }> {\n\tconst config: S3ClientConfig & IAMClientConfig = { region };\n\n\tif (profile) {\n\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\tconfig.credentials = fromIni({ profile });\n\t}\n\n\t// Support custom endpoint for testing (e.g., LocalStack)\n\tif (endpoint) {\n\t\tconfig.endpoint = endpoint;\n\t\t(config as S3ClientConfig).forcePathStyle = true;\n\t\t// Use test credentials when endpoint is specified\n\t\tconfig.credentials = {\n\t\t\taccessKeyId: 'test',\n\t\t\tsecretAccessKey: 'test',\n\t\t};\n\t}\n\n\treturn {\n\t\ts3: new S3Client(config),\n\t\tiam: new IAMClient(config),\n\t};\n}\n\n/**\n * Check if an S3 bucket exists\n */\nasync function bucketExists(\n\ts3: S3Client,\n\tbucketName: string,\n): Promise<boolean> {\n\ttry {\n\t\tawait s3.send(new HeadBucketCommand({ Bucket: bucketName }));\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as { name?: string }).name === 'NotFound') {\n\t\t\treturn false;\n\t\t}\n\t\t// 403 means bucket exists but we don't have access\n\t\tif (\n\t\t\t(error as { $metadata?: { httpStatusCode?: number } }).$metadata\n\t\t\t\t?.httpStatusCode === 403\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Check if an IAM user exists\n */\nasync function userExists(iam: IAMClient, userName: string): Promise<boolean> {\n\ttry {\n\t\tawait iam.send(new GetUserCommand({ UserName: userName }));\n\t\treturn true;\n\t} catch (error) {\n\t\tconst errorName = (error as { name?: string }).name;\n\t\t// AWS returns 'NoSuchEntity', LocalStack returns 'NoSuchEntityException'\n\t\tif (errorName === 'NoSuchEntity' || errorName === 'NoSuchEntityException') {\n\t\t\treturn false;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Provision backup destination for a deployment.\n *\n * Creates AWS resources (S3 bucket, IAM user) and Dokploy destination if needed.\n * Reuses existing resources from state when possible.\n */\nexport async function provisionBackupDestination(\n\toptions: ProvisionBackupOptions,\n): Promise<BackupState> {\n\tconst {\n\t\tapi,\n\t\tprojectName,\n\t\tstage,\n\t\tconfig,\n\t\texistingState,\n\t\tlogger,\n\t\tawsEndpoint,\n\t} = options;\n\n\t// If we have existing state, verify the Dokploy destination still exists\n\tif (existingState?.destinationId) {\n\t\ttry {\n\t\t\tawait api.getDestination(existingState.destinationId);\n\t\t\tlogger.log(' Using existing backup destination');\n\t\t\treturn existingState;\n\t\t} catch {\n\t\t\tlogger.log(' Existing destination not found, recreating...');\n\t\t}\n\t}\n\n\t// Create AWS clients\n\tconst aws = await createAwsClients(\n\t\tconfig.region,\n\t\tconfig.profile,\n\t\tawsEndpoint,\n\t);\n\tconst sanitizedProject = sanitizeName(projectName);\n\n\t// 1. Create or verify S3 bucket\n\tconst bucketName =\n\t\texistingState?.bucketName ??\n\t\t`${sanitizedProject}-${stage}-backups-${randomSuffix()}`;\n\n\tconst bucketAlreadyExists = await bucketExists(aws.s3, bucketName);\n\tif (!bucketAlreadyExists) {\n\t\tlogger.log(` Creating S3 bucket: ${bucketName}`);\n\n\t\t// CreateBucket needs LocationConstraint for non-us-east-1 regions\n\t\tconst createBucketParams: {\n\t\t\tBucket: string;\n\t\t\tCreateBucketConfiguration?: {\n\t\t\t\tLocationConstraint: BucketLocationConstraint;\n\t\t\t};\n\t\t} = {\n\t\t\tBucket: bucketName,\n\t\t};\n\t\tif (config.region !== 'us-east-1') {\n\t\t\tcreateBucketParams.CreateBucketConfiguration = {\n\t\t\t\tLocationConstraint: config.region as BucketLocationConstraint,\n\t\t\t};\n\t\t}\n\n\t\tawait aws.s3.send(new CreateBucketCommand(createBucketParams));\n\n\t\t// Enable versioning for backup integrity\n\t\tawait aws.s3.send(\n\t\t\tnew PutBucketVersioningCommand({\n\t\t\t\tBucket: bucketName,\n\t\t\t\tVersioningConfiguration: { Status: 'Enabled' },\n\t\t\t}),\n\t\t);\n\t} else {\n\t\tlogger.log(` Using existing S3 bucket: ${bucketName}`);\n\t}\n\n\t// 2. Create or verify IAM user\n\tconst iamUserName =\n\t\texistingState?.iamUserName ?? `dokploy-backup-${sanitizedProject}-${stage}`;\n\n\tconst iamUserAlreadyExists = await userExists(aws.iam, iamUserName);\n\tif (!iamUserAlreadyExists) {\n\t\tlogger.log(` Creating IAM user: ${iamUserName}`);\n\t\tawait aws.iam.send(new CreateUserCommand({ UserName: iamUserName }));\n\t} else {\n\t\tlogger.log(` Using existing IAM user: ${iamUserName}`);\n\t}\n\n\t// 3. Attach bucket policy to IAM user\n\tconst policyDocument = {\n\t\tVersion: '2012-10-17',\n\t\tStatement: [\n\t\t\t{\n\t\t\t\tEffect: 'Allow',\n\t\t\t\tAction: [\n\t\t\t\t\t's3:GetObject',\n\t\t\t\t\t's3:PutObject',\n\t\t\t\t\t's3:DeleteObject',\n\t\t\t\t\t's3:ListBucket',\n\t\t\t\t\t's3:GetBucketLocation',\n\t\t\t\t],\n\t\t\t\tResource: [\n\t\t\t\t\t`arn:aws:s3:::${bucketName}`,\n\t\t\t\t\t`arn:aws:s3:::${bucketName}/*`,\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t};\n\n\tlogger.log(' Updating IAM policy');\n\tawait aws.iam.send(\n\t\tnew PutUserPolicyCommand({\n\t\t\tUserName: iamUserName,\n\t\t\tPolicyName: 'DokployBackupAccess',\n\t\t\tPolicyDocument: JSON.stringify(policyDocument),\n\t\t}),\n\t);\n\n\t// 4. Create access key (or reuse existing if state has it and destination needs recreation)\n\tlet accessKeyId: string;\n\tlet secretAccessKey: string;\n\n\tif (existingState?.iamAccessKeyId && existingState?.iamSecretAccessKey) {\n\t\t// Reuse existing credentials\n\t\tlogger.log(' Using existing IAM access key');\n\t\taccessKeyId = existingState.iamAccessKeyId;\n\t\tsecretAccessKey = existingState.iamSecretAccessKey;\n\t} else {\n\t\t// Create new access key\n\t\tlogger.log(' Creating IAM access key');\n\t\tconst accessKeyResult = await aws.iam.send(\n\t\t\tnew CreateAccessKeyCommand({ UserName: iamUserName }),\n\t\t);\n\n\t\tif (!accessKeyResult.AccessKey) {\n\t\t\tthrow new Error('Failed to create IAM access key');\n\t\t}\n\n\t\taccessKeyId = accessKeyResult.AccessKey.AccessKeyId!;\n\t\tsecretAccessKey = accessKeyResult.AccessKey.SecretAccessKey!;\n\t}\n\n\t// 5. Create Dokploy destination\n\tconst destinationName = `${sanitizedProject}-${stage}-s3`;\n\tlogger.log(` Creating Dokploy destination: ${destinationName}`);\n\n\tconst { destination, created } = await api.findOrCreateDestination(\n\t\tdestinationName,\n\t\t{\n\t\t\taccessKey: accessKeyId,\n\t\t\tsecretAccessKey: secretAccessKey,\n\t\t\tbucket: bucketName,\n\t\t\tregion: config.region,\n\t\t},\n\t);\n\n\tif (created) {\n\t\tlogger.log(' ✓ Dokploy destination created');\n\t} else {\n\t\tlogger.log(' ✓ Using existing Dokploy destination');\n\t}\n\n\t// 6. Test connection\n\ttry {\n\t\tawait api.testDestinationConnection(destination.destinationId);\n\t\tlogger.log(' ✓ Destination connection verified');\n\t} catch (error) {\n\t\tlogger.log(\n\t\t\t` ⚠ Warning: Could not verify destination connection: ${error}`,\n\t\t);\n\t}\n\n\treturn {\n\t\tbucketName,\n\t\tbucketArn: `arn:aws:s3:::${bucketName}`,\n\t\tiamUserName,\n\t\tiamAccessKeyId: accessKeyId,\n\t\tiamSecretAccessKey: secretAccessKey,\n\t\tdestinationId: destination.destinationId,\n\t\tregion: config.region,\n\t\tcreatedAt: existingState?.createdAt ?? new Date().toISOString(),\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAiDA,SAAS,eAAuB;CAC/B,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC;AACjD;;;;AAKA,SAAS,aAAa,MAAsB;CAC3C,OAAO,KAAK,YAAY,CAAC,CAAC,QAAQ,eAAe,GAAG;AACrD;;;;AAKA,eAAe,iBACd,QACA,SACA,UAC4C;CAC5C,MAAM,SAA2C,EAAE,OAAO;CAE1D,IAAI,SAAS;EACZ,MAAM,EAAE,YAAY,MAAM,OAAO;EACjC,OAAO,cAAc,QAAQ,EAAE,QAAQ,CAAC;CACzC;CAGA,IAAI,UAAU;EACb,OAAO,WAAW;EAClB,OAA2B,iBAAiB;EAE5C,OAAO,cAAc;GACpB,aAAa;GACb,iBAAiB;EAClB;CACD;CAEA,OAAO;EACN,IAAI,IAAI,SAAS,MAAM;EACvB,KAAK,IAAI,UAAU,MAAM;CAC1B;AACD;;;;AAKA,eAAe,aACd,IACA,YACmB;CACnB,IAAI;EACH,MAAM,GAAG,KAAK,IAAI,kBAAkB,EAAE,QAAQ,WAAW,CAAC,CAAC;EAC3D,OAAO;CACR,SAAS,OAAO;EACf,IAAK,MAA4B,SAAS,YACzC,OAAO;EAGR,IACE,MAAsD,WACpD,mBAAmB,KAEtB,OAAO;EAER,MAAM;CACP;AACD;;;;AAKA,eAAe,WAAW,KAAgB,UAAoC;CAC7E,IAAI;EACH,MAAM,IAAI,KAAK,IAAI,eAAe,EAAE,UAAU,SAAS,CAAC,CAAC;EACzD,OAAO;CACR,SAAS,OAAO;EACf,MAAM,YAAa,MAA4B;EAE/C,IAAI,cAAc,kBAAkB,cAAc,yBACjD,OAAO;EAER,MAAM;CACP;AACD;;;;;;;AAQA,eAAsB,2BACrB,SACuB;CACvB,MAAM,EACL,KACA,aACA,OACA,QACA,eACA,QACA,gBACG;CAGJ,IAAI,eAAe,eAClB,IAAI;EACH,MAAM,IAAI,eAAe,cAAc,aAAa;EACpD,OAAO,IAAI,sCAAsC;EACjD,OAAO;CACR,QAAQ;EACP,OAAO,IAAI,kDAAkD;CAC9D;CAID,MAAM,MAAM,MAAM,iBACjB,OAAO,QACP,OAAO,SACP,WACD;CACA,MAAM,mBAAmB,aAAa,WAAW;CAGjD,MAAM,aACL,eAAe,cACf,GAAG,iBAAiB,GAAG,MAAM,WAAW,aAAa;CAGtD,IAAI,CAAC,MAD6B,aAAa,IAAI,IAAI,UAAU,GACvC;EACzB,OAAO,IAAI,0BAA0B,YAAY;EAGjD,MAAM,qBAKF,EACH,QAAQ,WACT;EACA,IAAI,OAAO,WAAW,aACrB,mBAAmB,4BAA4B,EAC9C,oBAAoB,OAAO,OAC5B;EAGD,MAAM,IAAI,GAAG,KAAK,IAAI,oBAAoB,kBAAkB,CAAC;EAG7D,MAAM,IAAI,GAAG,KACZ,IAAI,2BAA2B;GAC9B,QAAQ;GACR,yBAAyB,EAAE,QAAQ,UAAU;EAC9C,CAAC,CACF;CACD,OACC,OAAO,IAAI,gCAAgC,YAAY;CAIxD,MAAM,cACL,eAAe,eAAe,kBAAkB,iBAAiB,GAAG;CAGrE,IAAI,CAAC,MAD8B,WAAW,IAAI,KAAK,WAAW,GACvC;EAC1B,OAAO,IAAI,yBAAyB,aAAa;EACjD,MAAM,IAAI,IAAI,KAAK,IAAI,kBAAkB,EAAE,UAAU,YAAY,CAAC,CAAC;CACpE,OACC,OAAO,IAAI,+BAA+B,aAAa;CAIxD,MAAM,iBAAiB;EACtB,SAAS;EACT,WAAW,CACV;GACC,QAAQ;GACR,QAAQ;IACP;IACA;IACA;IACA;IACA;GACD;GACA,UAAU,CACT,gBAAgB,cAChB,gBAAgB,WAAW,GAC5B;EACD,CACD;CACD;CAEA,OAAO,IAAI,wBAAwB;CACnC,MAAM,IAAI,IAAI,KACb,IAAI,qBAAqB;EACxB,UAAU;EACV,YAAY;EACZ,gBAAgB,KAAK,UAAU,cAAc;CAC9C,CAAC,CACF;CAGA,IAAI;CACJ,IAAI;CAEJ,IAAI,eAAe,kBAAkB,eAAe,oBAAoB;EAEvE,OAAO,IAAI,kCAAkC;EAC7C,cAAc,cAAc;EAC5B,kBAAkB,cAAc;CACjC,OAAO;EAEN,OAAO,IAAI,4BAA4B;EACvC,MAAM,kBAAkB,MAAM,IAAI,IAAI,KACrC,IAAI,uBAAuB,EAAE,UAAU,YAAY,CAAC,CACrD;EAEA,IAAI,CAAC,gBAAgB,WACpB,MAAM,IAAI,MAAM,iCAAiC;EAGlD,cAAc,gBAAgB,UAAU;EACxC,kBAAkB,gBAAgB,UAAU;CAC7C;CAGA,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,MAAM;CACrD,OAAO,IAAI,oCAAoC,iBAAiB;CAEhE,MAAM,EAAE,aAAa,YAAY,MAAM,IAAI,wBAC1C,iBACA;EACC,WAAW;EACM;EACjB,QAAQ;EACR,QAAQ,OAAO;CAChB,CACD;CAEA,IAAI,SACH,OAAO,IAAI,kCAAkC;MAE7C,OAAO,IAAI,yCAAyC;CAIrD,IAAI;EACH,MAAM,IAAI,0BAA0B,YAAY,aAAa;EAC7D,OAAO,IAAI,sCAAsC;CAClD,SAAS,OAAO;EACf,OAAO,IACN,0DAA0D,OAC3D;CACD;CAEA,OAAO;EACN;EACA,WAAW,gBAAgB;EAC3B;EACA,gBAAgB;EAChB,oBAAoB;EACpB,eAAe,YAAY;EAC3B,QAAQ,OAAO;EACf,WAAW,eAAe,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;CAC/D;AACD"}
|