@geekmidas/cli 10.0.0-alpha.68 → 10.0.0-alpha.70
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/{LegacyStateStore-BZiO9-Lw.cjs → LegacyStateStore-Dd5IwiyC.cjs} +2 -2
- package/dist/{LegacyStateStore-BZiO9-Lw.cjs.map → LegacyStateStore-Dd5IwiyC.cjs.map} +1 -1
- package/dist/{LegacyStateStore-C39UKwmf.mjs → LegacyStateStore-NSHEinQi.mjs} +2 -2
- package/dist/{LegacyStateStore-C39UKwmf.mjs.map → LegacyStateStore-NSHEinQi.mjs.map} +1 -1
- package/dist/{LocalStateStore-DHVsZLyF.cjs → LocalStateStore-DTylG4T4.cjs} +2 -2
- package/dist/{LocalStateStore-DHVsZLyF.cjs.map → LocalStateStore-DTylG4T4.cjs.map} +1 -1
- package/dist/{LocalStateStore-Dk3Wbvzk.mjs → LocalStateStore-DUoG2wA_.mjs} +2 -2
- package/dist/{LocalStateStore-Dk3Wbvzk.mjs.map → LocalStateStore-DUoG2wA_.mjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-BmUAcR7d.mjs → OpenApiTsGenerator-BYD9EcJi.mjs} +44 -13
- package/dist/OpenApiTsGenerator-BYD9EcJi.mjs.map +1 -0
- package/dist/{OpenApiTsGenerator-B2zrdyP4.cjs → OpenApiTsGenerator-CpRWM0N6.cjs} +44 -13
- package/dist/OpenApiTsGenerator-CpRWM0N6.cjs.map +1 -0
- package/dist/{S3StateStore--4tYVMAL.cjs → S3StateStore-D7hrU-sy.cjs} +2 -2
- package/dist/{S3StateStore--4tYVMAL.cjs.map → S3StateStore-D7hrU-sy.cjs.map} +1 -1
- package/dist/{S3StateStore-B_ZF9eHt.mjs → S3StateStore-Dxob3PNA.mjs} +2 -2
- package/dist/{S3StateStore-B_ZF9eHt.mjs.map → S3StateStore-Dxob3PNA.mjs.map} +1 -1
- package/dist/{SSMStateStore-ACZ8hRvZ.cjs → SSMStateStore-DGIKJ4NN.cjs} +2 -2
- package/dist/{SSMStateStore-ACZ8hRvZ.cjs.map → SSMStateStore-DGIKJ4NN.cjs.map} +1 -1
- package/dist/{SSMStateStore-DjPuKqrm.mjs → SSMStateStore-Dvl4A8Up.mjs} +2 -2
- package/dist/{SSMStateStore-DjPuKqrm.mjs.map → SSMStateStore-Dvl4A8Up.mjs.map} +1 -1
- package/dist/add-BuviRszv.mjs +566 -0
- package/dist/add-BuviRszv.mjs.map +1 -0
- package/dist/add-DHIk61nK.cjs +568 -0
- package/dist/add-DHIk61nK.cjs.map +1 -0
- package/dist/{bundler-C1pnNrR0.cjs → bundler-D7dcJVgj.cjs} +5 -5
- package/dist/{bundler-C1pnNrR0.cjs.map → bundler-D7dcJVgj.cjs.map} +1 -1
- package/dist/{bundler-C-w7aGoZ.mjs → bundler-DJlsYzm7.mjs} +5 -5
- package/dist/{bundler-C-w7aGoZ.mjs.map → bundler-DJlsYzm7.mjs.map} +1 -1
- package/dist/{cli-BLyD4c0O.cjs → cli-7zCum6uN.cjs} +4 -4
- package/dist/{cli-BLyD4c0O.cjs.map → cli-7zCum6uN.cjs.map} +1 -1
- package/dist/{cli-Ce4pLL5O.mjs → cli-BjnqVnE6.mjs} +4 -4
- package/dist/{cli-Ce4pLL5O.mjs.map → cli-BjnqVnE6.mjs.map} +1 -1
- package/dist/{compose-DvsgumY2.cjs → compose-BJYHmuvx.cjs} +368 -25
- package/dist/compose-BJYHmuvx.cjs.map +1 -0
- package/dist/{compose-DFxuYhn6.mjs → compose-CByVaaeR.mjs} +363 -26
- package/dist/compose-CByVaaeR.mjs.map +1 -0
- package/dist/{config-CxEp_g7j.d.mts → config-BN6iXvyR.d.mts} +3 -3
- package/dist/{config-BnA0kPTY.d.cts.map → config-BN6iXvyR.d.mts.map} +1 -1
- package/dist/{config-BnA0kPTY.d.cts → config-j9zBKaEG.d.cts} +3 -3
- package/dist/{config-CxEp_g7j.d.mts.map → config-j9zBKaEG.d.cts.map} +1 -1
- package/dist/config.cjs +3 -3
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +3 -3
- package/dist/credentialSchemas-BcNL1N5R.cjs +271 -0
- package/dist/credentialSchemas-BcNL1N5R.cjs.map +1 -0
- package/dist/credentialSchemas-BmYxnEz_.mjs +236 -0
- package/dist/credentialSchemas-BmYxnEz_.mjs.map +1 -0
- package/dist/deploy/public.cjs +7 -7
- package/dist/deploy/public.d.cts +3 -3
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +3 -3
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +7 -7
- package/dist/{deploy-C08GONYW.cjs → deploy--ZU9gOOa.cjs} +6 -6
- package/dist/{deploy-C08GONYW.cjs.map → deploy--ZU9gOOa.cjs.map} +1 -1
- package/dist/{deploy-D-EVOWvu.mjs → deploy-CCSGr7be.mjs} +6 -6
- package/dist/{deploy-D-EVOWvu.mjs.map → deploy-CCSGr7be.mjs.map} +1 -1
- package/dist/{discover-OEfZ9OkA.cjs → discover-B4VjaFJk.cjs} +2 -2
- package/dist/{discover-OEfZ9OkA.cjs.map → discover-B4VjaFJk.cjs.map} +1 -1
- package/dist/{discover-BFoJWKGD.mjs → discover-Buwf1z4P.mjs} +2 -2
- package/dist/{discover-BFoJWKGD.mjs.map → discover-Buwf1z4P.mjs.map} +1 -1
- package/dist/{dokploy-CZ6nfSV9.cjs → dokploy-BOupc9PK.cjs} +2 -2
- package/dist/{dokploy-CZ6nfSV9.cjs.map → dokploy-BOupc9PK.cjs.map} +1 -1
- package/dist/{dokploy-DU-XJsoT.mjs → dokploy-Bskfn1g5.mjs} +2 -2
- package/dist/{dokploy-DU-XJsoT.mjs.map → dokploy-Bskfn1g5.mjs.map} +1 -1
- package/dist/{encryption-CCcBSLZs.cjs → encryption-D-VkoJG4.cjs} +3 -3
- package/dist/{encryption-CCcBSLZs.cjs.map → encryption-D-VkoJG4.cjs.map} +1 -1
- package/dist/{encryption-DSiLau1-.mjs → encryption-DR-py9GX.mjs} +3 -3
- package/dist/{encryption-DSiLau1-.mjs.map → encryption-DR-py9GX.mjs.map} +1 -1
- package/dist/{engine-BM9Dy_DT.cjs → engine-Da8dBxt5.cjs} +16 -7
- package/dist/engine-Da8dBxt5.cjs.map +1 -0
- package/dist/{engine-llWnMZH5.mjs → engine-Xbv8NNdX.mjs} +16 -7
- package/dist/{engine-llWnMZH5.mjs.map → engine-Xbv8NNdX.mjs.map} +1 -1
- package/dist/{github-yy6WpHIl.mjs → github-D2pYzY7L.mjs} +3 -3
- package/dist/{github-yy6WpHIl.mjs.map → github-D2pYzY7L.mjs.map} +1 -1
- package/dist/{github-ZoMam0OH.cjs → github-DeXzs11c.cjs} +3 -3
- package/dist/{github-ZoMam0OH.cjs.map → github-DeXzs11c.cjs.map} +1 -1
- package/dist/{index-EvZBkycu.d.cts → index-Bk3NY6DN.d.cts} +2 -2
- package/dist/{index-CkX89m6S.d.mts.map → index-Bk3NY6DN.d.cts.map} +1 -1
- package/dist/{index-CkX89m6S.d.mts → index-DvoxuTqR.d.mts} +2 -2
- package/dist/{index-EvZBkycu.d.cts.map → index-DvoxuTqR.d.mts.map} +1 -1
- package/dist/index.cjs +89 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +89 -48
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-tOwxQBTI.cjs → migrate-BURDS8vy.cjs} +4 -4
- package/dist/{migrate-tOwxQBTI.cjs.map → migrate-BURDS8vy.cjs.map} +1 -1
- package/dist/{migrate-BWPuwiXk.mjs → migrate-DiJT84g9.mjs} +4 -4
- package/dist/{migrate-BWPuwiXk.mjs.map → migrate-DiJT84g9.mjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{ready-B6uHr4sm.cjs → ready-C-qNcux-.cjs} +3 -3
- package/dist/{ready-B6uHr4sm.cjs.map → ready-C-qNcux-.cjs.map} +1 -1
- package/dist/{ready-2s3-CndS.mjs → ready-CfIiMts4.mjs} +3 -3
- package/dist/{ready-2s3-CndS.mjs.map → ready-CfIiMts4.mjs.map} +1 -1
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/{resolve-BymzGtuT.cjs → resolve-B7jJQmTo.cjs} +3 -3
- package/dist/{resolve-BymzGtuT.cjs.map → resolve-B7jJQmTo.cjs.map} +1 -1
- package/dist/{resolve-pIvXlZ1h.mjs → resolve-Dj5TQKoD.mjs} +3 -3
- package/dist/{resolve-pIvXlZ1h.mjs.map → resolve-Dj5TQKoD.mjs.map} +1 -1
- package/dist/{rollback-ZlsbQOvj.mjs → rollback-Bw9kESjz.mjs} +5 -5
- package/dist/{rollback-ZlsbQOvj.mjs.map → rollback-Bw9kESjz.mjs.map} +1 -1
- package/dist/{rollback-zdfi6Reg.cjs → rollback-DNK1HEHY.cjs} +5 -5
- package/dist/{rollback-zdfi6Reg.cjs.map → rollback-DNK1HEHY.cjs.map} +1 -1
- package/dist/{runtime-DWThEP-r.d.cts → runtime-CYAO4iZG.d.cts} +2 -2
- package/dist/{runtime-B3jNgeP-.d.mts.map → runtime-CYAO4iZG.d.cts.map} +1 -1
- package/dist/{runtime-B3jNgeP-.d.mts → runtime-DHJhe4QI.d.mts} +2 -2
- package/dist/{runtime-DWThEP-r.d.cts.map → runtime-DHJhe4QI.d.mts.map} +1 -1
- package/dist/sandbox/credentials-worker.cjs +54 -0
- package/dist/sandbox/credentials-worker.cjs.map +1 -0
- package/dist/sandbox/credentials-worker.d.cts +1 -0
- package/dist/sandbox/credentials-worker.d.mts +1 -0
- package/dist/sandbox/credentials-worker.mjs +55 -0
- package/dist/sandbox/credentials-worker.mjs.map +1 -0
- 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/{schemas-RUlKHu27.cjs → schemas-BObwxB_t.cjs} +164 -3
- package/dist/schemas-BObwxB_t.cjs.map +1 -0
- package/dist/{schemas-Cfhtpdo0.mjs → schemas-D-xeKJDD.mjs} +153 -4
- package/dist/schemas-D-xeKJDD.mjs.map +1 -0
- package/dist/{sniffer-D66HIvi9.cjs → sniffer-DpHpFZBD.cjs} +7 -7
- package/dist/sniffer-DpHpFZBD.cjs.map +1 -0
- package/dist/{sniffer-BdFeLSmA.mjs → sniffer-vAXsU_4G.mjs} +7 -7
- package/dist/sniffer-vAXsU_4G.mjs.map +1 -0
- package/dist/{state-Bn5JDMFT.mjs → state-527OmqvN.mjs} +156 -64
- package/dist/state-527OmqvN.mjs.map +1 -0
- package/dist/{state-CHZqCDRC.cjs → state-nS6I6s4u.cjs} +179 -63
- package/dist/state-nS6I6s4u.cjs.map +1 -0
- 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-C3cv-OQu.mjs → terminal-C6TBo_SA.mjs} +2 -2
- package/dist/{terminal-C3cv-OQu.mjs.map → terminal-C6TBo_SA.mjs.map} +1 -1
- package/dist/{terminal-DjzgLu6m.cjs → terminal-CFO3xWoF.cjs} +2 -2
- package/dist/{terminal-DjzgLu6m.cjs.map → terminal-CFO3xWoF.cjs.map} +1 -1
- package/dist/{types-CZdCxUCK.d.cts → types-BD1jgA85.d.cts} +70 -2
- package/dist/types-BD1jgA85.d.cts.map +1 -0
- package/dist/{types-Da0E3Qv7.d.mts → types-DkG5WW8l.d.mts} +70 -2
- package/dist/types-DkG5WW8l.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 +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Be6uBhlF.mjs → workspace-C7NqCsQF.mjs} +105 -3
- package/dist/workspace-C7NqCsQF.mjs.map +1 -0
- package/dist/{workspace-ON-bqiJz.cjs → workspace-vdrcRI-m.cjs} +116 -2
- package/dist/workspace-vdrcRI-m.cjs.map +1 -0
- package/package.json +16 -13
- package/dist/OpenApiTsGenerator-B2zrdyP4.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-BmUAcR7d.mjs.map +0 -1
- package/dist/compose-DFxuYhn6.mjs.map +0 -1
- package/dist/compose-DvsgumY2.cjs.map +0 -1
- package/dist/engine-BM9Dy_DT.cjs.map +0 -1
- package/dist/schemas-Cfhtpdo0.mjs.map +0 -1
- package/dist/schemas-RUlKHu27.cjs.map +0 -1
- package/dist/sniffer-BdFeLSmA.mjs.map +0 -1
- package/dist/sniffer-D66HIvi9.cjs.map +0 -1
- package/dist/state-Bn5JDMFT.mjs.map +0 -1
- package/dist/state-CHZqCDRC.cjs.map +0 -1
- package/dist/types-CZdCxUCK.d.cts.map +0 -1
- package/dist/types-Da0E3Qv7.d.mts.map +0 -1
- package/dist/workspace-Be6uBhlF.mjs.map +0 -1
- package/dist/workspace-ON-bqiJz.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSMStateStore-DjPuKqrm.mjs","names":[],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(options.workspaceName, new SSMClient(config));\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn { body: parameter.value, version: String(parameter.version) };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\tValue: body,\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SSMStateStore-Dvl4A8Up.mjs","names":[],"sources":["../src/deploy/SSMStateStore.ts"],"sourcesContent":["/**\n * AWS SSM Parameter Store state store.\n *\n * - `/gkm/<workspace>/<stage>/state`: the state, a SecureString.\n * - `/gkm/<workspace>/<stage>/lock`: the lock, created only if absent.\n * - `/gkm/<workspace>/<stage>/state.v1`: the v1 state, kept on migration.\n *\n * SSM has no conditional put, so a write checks the parameter's version\n * before it puts, and checks the version the put returns after: a put that\n * did not land exactly one version above the one it was based on raced\n * another writer, and raises `StateVersionConflict` rather than reporting\n * success. Both writes stay in the parameter's history.\n */\n\nimport {\n\tDeleteParameterCommand,\n\tGetParameterCommand,\n\tParameterAlreadyExists,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { AwsRegion } from './StateProvider';\nimport {\n\tDocumentStateStore,\n\ttype LockHolder,\n\ttype RawState,\n\tStateLocked,\n\ttype StateVersion,\n\tStateVersionConflict,\n} from './StateStore';\n\nexport interface SSMStateStoreOptions {\n\tworkspaceName: string;\n\tregion?: AwsRegion;\n\t/** Uses the default credential chain when omitted. */\n\tprofile?: string;\n\tcredentials?: SSMClientConfig['credentials'];\n\t/** For the local AWS emulator. */\n\tendpoint?: string;\n}\n\nexport class SSMStateStore extends DocumentStateStore {\n\tconstructor(\n\t\treadonly workspaceName: string,\n\t\tprivate readonly client: SSMClient,\n\t) {\n\t\tsuper();\n\t}\n\n\tstatic create(options: SSMStateStoreOptions): SSMStateStore {\n\t\tconst config: SSMClientConfig = {\n\t\t\tregion: options.region,\n\t\t\tendpoint: options.endpoint,\n\t\t};\n\t\tif (options.profile) {\n\t\t\t// Required lazily: only a profile needs the credential providers.\n\t\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: options.profile });\n\t\t} else if (options.credentials) {\n\t\t\tconfig.credentials = options.credentials;\n\t\t}\n\t\treturn new SSMStateStore(options.workspaceName, new SSMClient(config));\n\t}\n\n\tprivate name(stage: string, leaf: string): string {\n\t\treturn `/gkm/${this.workspaceName}/${stage}/${leaf}`;\n\t}\n\n\tprotected location(stage: string): string {\n\t\treturn `ssm:${this.name(stage, 'state')}`;\n\t}\n\n\tprotected async readRaw(stage: string): Promise<RawState | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'state'));\n\t\tif (!parameter) return null;\n\t\treturn { body: parameter.value, version: String(parameter.version) };\n\t}\n\n\tprotected async writeRaw(\n\t\tstage: string,\n\t\tbody: string,\n\t\texpectedVersion: StateVersion | null,\n\t): Promise<StateVersion> {\n\t\tconst Name = this.name(stage, 'state');\n\t\tconst Description = `GKM deployment state for ${this.workspaceName}/${stage}`;\n\n\t\tif (expectedVersion === null) {\n\t\t\ttry {\n\t\t\t\tconst { Version } = await this.client.send(\n\t\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\t\tName,\n\t\t\t\t\t\tValue: body,\n\t\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\t\tDescription,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn String(Version);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\t\tconst current = await this.get(Name);\n\t\t\t\t\tthrow new StateVersionConflict(\n\t\t\t\t\t\tstage,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcurrent ? String(current.version) : null,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst current = await this.get(Name);\n\t\tconst actual = current ? String(current.version) : null;\n\t\tif (actual !== expectedVersion) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, actual);\n\t\t}\n\n\t\tconst { Version } = await this.client.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName,\n\t\t\t\tValue: body,\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription,\n\t\t\t}),\n\t\t);\n\t\tif (Version !== Number(expectedVersion) + 1) {\n\t\t\t// Another put landed between the check and this one. Ours is now\n\t\t\t// on top, but the caller that wrote the other believes its write\n\t\t\t// is current — its next write will conflict too, so neither is\n\t\t\t// lost silently.\n\t\t\tthrow new StateVersionConflict(\n\t\t\t\tstage,\n\t\t\t\texpectedVersion,\n\t\t\t\tString((Version ?? 1) - 1),\n\t\t\t);\n\t\t}\n\n\t\t// The version alone is not enough under an emulator, and need not be\n\t\t// on SSM either: two overwrites that raced can both be answered with\n\t\t// the same version, and both would report success. If the parameter is\n\t\t// still at the version this put returned, it has to hold this body —\n\t\t// otherwise the other put is the one that stuck.\n\t\tconst landed = await this.get(Name);\n\t\tif (landed && landed.version === Version && landed.value !== body) {\n\t\t\tthrow new StateVersionConflict(stage, expectedVersion, String(Version));\n\t\t}\n\t\treturn String(Version);\n\t}\n\n\tprotected async writeV1Backup(stage: string, body: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName: this.name(stage, 'state.v1'),\n\t\t\t\t\tValue: body,\n\t\t\t\t\tType: 'SecureString',\n\t\t\t\t\tDescription: `GKM v1 deployment state for ${this.workspaceName}/${stage}, kept on migration`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\t// The first migration's backup is the one worth keeping.\n\t\t\tif (!(error instanceof ParameterAlreadyExists)) throw error;\n\t\t}\n\t}\n\n\tprotected async createLock(stage: string, holder: LockHolder): Promise<void> {\n\t\tconst Name = this.name(stage, 'lock');\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew PutParameterCommand({\n\t\t\t\t\tName,\n\t\t\t\t\tValue: JSON.stringify(holder),\n\t\t\t\t\tType: 'String',\n\t\t\t\t\tDescription: `GKM deploy lock for ${this.workspaceName}/${stage}`,\n\t\t\t\t}),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterAlreadyExists) {\n\t\t\t\tthrow new StateLocked(stage, await this.readLock(stage), `ssm:${Name}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\t// A create-only put is atomic on SSM, so the lock is ours. Read it back\n\t\t// anyway: on a store where two racing creates can both succeed (the\n\t\t// local emulator does, a few times in a hundred) the later one has\n\t\t// overwritten the earlier — version 2, someone else's holder — and\n\t\t// taking the lock regardless would give the stage two deploys.\n\t\tconst created = await this.get(Name);\n\t\tconst recorded = created ? parseHolder(created.value) : null;\n\t\tif (created?.version !== 1 || recorded?.id !== holder.id) {\n\t\t\tthrow new StateLocked(stage, recorded, `ssm:${Name}`);\n\t\t}\n\t}\n\n\tprotected async readLock(stage: string): Promise<LockHolder | null> {\n\t\tconst parameter = await this.get(this.name(stage, 'lock'));\n\t\treturn parameter ? parseHolder(parameter.value) : null;\n\t}\n\n\tprotected async removeLock(\n\t\tstage: string,\n\t\tholder: LockHolder | null,\n\t): Promise<void> {\n\t\tif (holder) {\n\t\t\tconst current = await this.readLock(stage);\n\t\t\tif (current?.id !== holder.id) return;\n\t\t}\n\t\ttry {\n\t\t\tawait this.client.send(\n\t\t\t\tnew DeleteParameterCommand({ Name: this.name(stage, 'lock') }),\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ParameterNotFound)) throw error;\n\t\t}\n\t}\n\n\tprivate async get(\n\t\tName: string,\n\t): Promise<{ value: string; version: number } | null> {\n\t\ttry {\n\t\t\tconst { Parameter } = await this.client.send(\n\t\t\t\tnew GetParameterCommand({ Name, WithDecryption: true }),\n\t\t\t);\n\t\t\tif (Parameter?.Value === undefined) return null;\n\t\t\treturn { value: Parameter.Value, version: Parameter.Version ?? 1 };\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction parseHolder(value: string): LockHolder | null {\n\ttry {\n\t\treturn JSON.parse(value) as LockHolder;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { g as url, o as email } from "./schemas-D-xeKJDD.mjs";
|
|
2
|
+
import { r as discover } from "./discover-Buwf1z4P.mjs";
|
|
3
|
+
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
4
|
+
import { k as assertDeployedStage } from "./workspace-C7NqCsQF.mjs";
|
|
5
|
+
import { loadWorkspaceSettings } from "./config.mjs";
|
|
6
|
+
import { G as constructGlobs, It as secretsStoreFor, R as requiredStageKeys, Zt as initStageSecrets, st as appEnvKeys } from "./state-527OmqvN.mjs";
|
|
7
|
+
import { i as issueLine, o as loadCredentialSchemas } from "./credentialSchemas-BmYxnEz_.mjs";
|
|
8
|
+
import { provideKey } from "@geekmidas/manifest";
|
|
9
|
+
import prompts from "prompts";
|
|
10
|
+
import { build, parse } from "@geekmidas/storage/s3-url";
|
|
11
|
+
//#region src/secrets/stageKeys.ts
|
|
12
|
+
/**
|
|
13
|
+
* The keys a workspace's stage must be given — across every app, each once,
|
|
14
|
+
* with who reads it and whether the stage holds it.
|
|
15
|
+
*
|
|
16
|
+
* The keys themselves are `requiredStageKeys`'s, the list a deploy refuses a
|
|
17
|
+
* stage by. What this adds is the workspace: which constructs its apps read
|
|
18
|
+
* (`appEnvKeys`, the keys each app's environment is built from) and which of
|
|
19
|
+
* the keys the stage has set.
|
|
20
|
+
*/
|
|
21
|
+
/** The keys of each kind of construct an app reads, that the stage supplies. */
|
|
22
|
+
function keysOf(id, kind) {
|
|
23
|
+
switch (kind) {
|
|
24
|
+
case "email": return [provideKey(id, "url"), provideKey(id, "from")];
|
|
25
|
+
case "objects":
|
|
26
|
+
case "file-server": return [provideKey(id, "url")];
|
|
27
|
+
case "external-api":
|
|
28
|
+
case "credential": return [provideKey(id, "credentials")];
|
|
29
|
+
default: return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every key the stage must be given for the constructs its apps read — a
|
|
34
|
+
* file server's whenever its bucket is read, since its client reads both —
|
|
35
|
+
* and every file server's bucket, which serves it whether or not an app
|
|
36
|
+
* reads it directly.
|
|
37
|
+
*/
|
|
38
|
+
function workspaceStageKeys(input) {
|
|
39
|
+
const { manifest } = input;
|
|
40
|
+
const reads = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
42
|
+
const { kind } = declaration;
|
|
43
|
+
if (kind !== "rest-api" && kind !== "site" && kind !== "mobile-app") continue;
|
|
44
|
+
const name = appKey(id);
|
|
45
|
+
const keys = appEnvKeys(manifest, name, input.runnables);
|
|
46
|
+
if (keys) reads.set(name, keys);
|
|
47
|
+
}
|
|
48
|
+
const readers = (id, kind) => {
|
|
49
|
+
const keys = keysOf(id, kind);
|
|
50
|
+
return [...reads].filter(([, env]) => keys.some((key) => env.has(key))).map(([app]) => app).sort();
|
|
51
|
+
};
|
|
52
|
+
const services = [];
|
|
53
|
+
const credentials = [];
|
|
54
|
+
const buckets = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
56
|
+
const { kind } = declaration;
|
|
57
|
+
const apps = declaration.kind === "file-server" && declaration.of ? [.../* @__PURE__ */ new Set([...readers(id, kind), ...readers(declaration.of, "objects")])].sort() : readers(id, kind);
|
|
58
|
+
if (kind === "external-api" || kind === "credential") {
|
|
59
|
+
if (apps.length > 0) credentials.push({
|
|
60
|
+
id,
|
|
61
|
+
kind,
|
|
62
|
+
apps
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (kind !== "email" && kind !== "objects" && kind !== "file-server") continue;
|
|
67
|
+
if (apps.length === 0) continue;
|
|
68
|
+
const of = declaration.kind === "file-server" ? declaration.of : void 0;
|
|
69
|
+
services.push({
|
|
70
|
+
id,
|
|
71
|
+
kind,
|
|
72
|
+
...of ? { of } : {},
|
|
73
|
+
apps
|
|
74
|
+
});
|
|
75
|
+
if (of) buckets.add(of);
|
|
76
|
+
}
|
|
77
|
+
for (const of of buckets) {
|
|
78
|
+
if (services.some((s) => s.id === of)) continue;
|
|
79
|
+
if (manifest[of]?.kind !== "objects") continue;
|
|
80
|
+
services.push({
|
|
81
|
+
id: of,
|
|
82
|
+
kind: "objects",
|
|
83
|
+
apps: readers(of, "objects")
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return requiredStageKeys({
|
|
87
|
+
local: input.local,
|
|
88
|
+
services,
|
|
89
|
+
credentials,
|
|
90
|
+
...input.domain ? { domain: input.domain } : {}
|
|
91
|
+
}).map((key) => ({
|
|
92
|
+
...key,
|
|
93
|
+
set: input.supplied[key.key] !== void 0
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/secrets/add.ts
|
|
98
|
+
/**
|
|
99
|
+
* `gkm secrets:add --stage <stage>` — the secrets a stage must be given,
|
|
100
|
+
* built key by key.
|
|
101
|
+
*
|
|
102
|
+
* It offers exactly the keys a deploy would refuse the stage without
|
|
103
|
+
* (`requiredStageKeys`), across every app in the workspace, each once: the
|
|
104
|
+
* stage's buckets, mail and file servers where it is deployed, and every
|
|
105
|
+
* third party's credentials. A key is built by what it is — a bucket from its
|
|
106
|
+
* provider, mail from its SMTP server, credentials field by field from their
|
|
107
|
+
* construct's schema — checked, and saved through the stage's own store, the
|
|
108
|
+
* one `gkm secrets:set` writes. No value is ever printed.
|
|
109
|
+
*
|
|
110
|
+
* `--json` asks nothing: it prints the keys, for a script or an agent to set
|
|
111
|
+
* with `gkm secrets:set`.
|
|
112
|
+
*/
|
|
113
|
+
/** `gkm secrets:add` with nobody at a terminal to answer it. */
|
|
114
|
+
var SecretsAddNeedsTerminal = class extends Error {
|
|
115
|
+
stage;
|
|
116
|
+
constructor(stage) {
|
|
117
|
+
super(`gkm secrets:add asks for each key, and there is no terminal to ask at. List what the stage needs with gkm secrets:add --stage ${stage} --missing --json, and set each with gkm secrets:set <KEY> <value> --stage ${stage}.`);
|
|
118
|
+
this.stage = stage;
|
|
119
|
+
this.name = "SecretsAddNeedsTerminal";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/** The person stopped the builder; nothing was saved. */
|
|
123
|
+
var SecretsAddCancelled = class extends Error {
|
|
124
|
+
stage;
|
|
125
|
+
constructor(stage) {
|
|
126
|
+
super(`Stopped. Nothing was saved to the stage '${stage}'.`);
|
|
127
|
+
this.stage = stage;
|
|
128
|
+
this.name = "SecretsAddCancelled";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const defaultIo = () => ({
|
|
132
|
+
log: (line) => console.log(line),
|
|
133
|
+
write: (chunk) => {
|
|
134
|
+
process.stdout.write(chunk);
|
|
135
|
+
},
|
|
136
|
+
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true
|
|
137
|
+
});
|
|
138
|
+
/** `gkm secrets:add`. */
|
|
139
|
+
async function secretsAddCommand(options, io = defaultIo()) {
|
|
140
|
+
const { stage } = options;
|
|
141
|
+
const workspace = await loadWorkspaceSettings(options.cwd ?? process.cwd());
|
|
142
|
+
const local = stage === workspace.stages.local;
|
|
143
|
+
if (!local) assertDeployedStage(workspace.stages, stage);
|
|
144
|
+
const patterns = constructGlobs(workspace);
|
|
145
|
+
const runnables = {};
|
|
146
|
+
const manifest = await discover({
|
|
147
|
+
patterns,
|
|
148
|
+
cwd: workspace.root,
|
|
149
|
+
runnables
|
|
150
|
+
});
|
|
151
|
+
const store = await secretsStoreFor(workspace, stage, { ...options.home ? { home: options.home } : {} });
|
|
152
|
+
const stored = await store.read(stage);
|
|
153
|
+
const domain = workspace.deploy?.domains?.[stage];
|
|
154
|
+
const all = workspaceStageKeys({
|
|
155
|
+
manifest,
|
|
156
|
+
runnables,
|
|
157
|
+
local,
|
|
158
|
+
supplied: stored?.custom ?? {},
|
|
159
|
+
...domain ? { domain } : {}
|
|
160
|
+
});
|
|
161
|
+
const keys = options.missing ? all.filter((k) => !k.set) : all;
|
|
162
|
+
if (options.json) {
|
|
163
|
+
const listed = keys.map((k) => ({
|
|
164
|
+
key: k.key,
|
|
165
|
+
kind: k.kind,
|
|
166
|
+
construct: k.id,
|
|
167
|
+
apps: [...k.apps ?? []],
|
|
168
|
+
set: k.set
|
|
169
|
+
}));
|
|
170
|
+
io.write(`${JSON.stringify(listed, null, 2)}\n`);
|
|
171
|
+
return {
|
|
172
|
+
keys,
|
|
173
|
+
saved: []
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (!io.interactive) throw new SecretsAddNeedsTerminal(stage);
|
|
177
|
+
if (keys.length === 0) {
|
|
178
|
+
io.log(all.length === 0 ? `Nothing to add: no construct on '${stage}' needs a key only the stage can supply.` : `Nothing to add: the stage '${stage}' has every key it needs.`);
|
|
179
|
+
return {
|
|
180
|
+
keys,
|
|
181
|
+
saved: []
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const ask = asker(stage);
|
|
185
|
+
const chosen = await ask({
|
|
186
|
+
type: "multiselect",
|
|
187
|
+
message: `Which keys for '${stage}'? (${store.name} store)`,
|
|
188
|
+
choices: keys.map((k) => ({
|
|
189
|
+
title: `${k.key} ${KIND_LABEL[k.kind]} '${k.id}'${k.apps?.length ? ` · ${k.apps.join(", ")}` : ""}${k.set ? " · set" : ""}`,
|
|
190
|
+
value: k.key,
|
|
191
|
+
selected: !k.set
|
|
192
|
+
})),
|
|
193
|
+
instructions: false,
|
|
194
|
+
hint: "space to toggle, enter to continue"
|
|
195
|
+
});
|
|
196
|
+
const selected = keys.filter((k) => (chosen ?? []).includes(k.key));
|
|
197
|
+
const values = {};
|
|
198
|
+
const has = (key) => values[key] !== void 0 || stored?.custom?.[key] !== void 0;
|
|
199
|
+
let schemas;
|
|
200
|
+
for (const entry of selected) {
|
|
201
|
+
if (entry.set) {
|
|
202
|
+
if (!await ask({
|
|
203
|
+
type: "confirm",
|
|
204
|
+
message: `${entry.key} is set. Replace it?`,
|
|
205
|
+
initial: false
|
|
206
|
+
})) continue;
|
|
207
|
+
}
|
|
208
|
+
io.log(`\n${entry.key} — ${entry.what}`);
|
|
209
|
+
switch (entry.kind) {
|
|
210
|
+
case "bucket":
|
|
211
|
+
Object.assign(values, await buildBucket(ask, io, entry, has));
|
|
212
|
+
break;
|
|
213
|
+
case "email":
|
|
214
|
+
values[entry.key] = entry.key.endsWith("_FROM") ? await askFrom(ask, io) : await buildSmtpUrl(ask, io);
|
|
215
|
+
break;
|
|
216
|
+
case "file-server":
|
|
217
|
+
values[entry.key] = await askHttpUrl(ask, io, "Its public URL");
|
|
218
|
+
break;
|
|
219
|
+
case "external-api":
|
|
220
|
+
case "credential":
|
|
221
|
+
schemas ??= await loadCredentialSchemas({
|
|
222
|
+
root: workspace.root,
|
|
223
|
+
patterns
|
|
224
|
+
});
|
|
225
|
+
values[entry.key] = await buildCredentials(ask, io, entry, schemas);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const saved = Object.keys(values);
|
|
229
|
+
if (saved.length === 0) {
|
|
230
|
+
io.log("\nNothing was saved.");
|
|
231
|
+
return {
|
|
232
|
+
keys,
|
|
233
|
+
saved
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const base = stored ?? initStageSecrets(stage);
|
|
237
|
+
await store.write(stage, {
|
|
238
|
+
...base,
|
|
239
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
custom: {
|
|
241
|
+
...base.custom,
|
|
242
|
+
...values
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
io.log(`\n✓ Saved ${saved.join(", ")} to the stage '${stage}' (${store.name} store).`);
|
|
246
|
+
return {
|
|
247
|
+
keys,
|
|
248
|
+
saved
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const KIND_LABEL = {
|
|
252
|
+
bucket: "bucket",
|
|
253
|
+
email: "email",
|
|
254
|
+
"file-server": "file server",
|
|
255
|
+
"external-api": "external API",
|
|
256
|
+
credential: "credential"
|
|
257
|
+
};
|
|
258
|
+
function asker(stage) {
|
|
259
|
+
return async (question) => {
|
|
260
|
+
return (await prompts({
|
|
261
|
+
...question,
|
|
262
|
+
name: "value"
|
|
263
|
+
}, { onCancel: () => {
|
|
264
|
+
throw new SecretsAddCancelled(stage);
|
|
265
|
+
} })).value;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Ask until `accept` takes the answer; `accept` returns why it did not. */
|
|
269
|
+
async function askUntil(ask, io, question, accept) {
|
|
270
|
+
for (;;) {
|
|
271
|
+
const value = await ask(question);
|
|
272
|
+
const problem = accept(value);
|
|
273
|
+
if (!problem) return value;
|
|
274
|
+
io.log(` ✗ ${problem}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const required = (what) => (value) => value?.trim() ? void 0 : `${what} is required.`;
|
|
278
|
+
const text = (value) => String(value ?? "").trim();
|
|
279
|
+
/** An `http(s)://` URL. */
|
|
280
|
+
const HttpUrl = url({ protocol: /^https?$/ });
|
|
281
|
+
async function askHttpUrl(ask, io, message) {
|
|
282
|
+
return text(await askUntil(ask, io, {
|
|
283
|
+
type: "text",
|
|
284
|
+
message
|
|
285
|
+
}, (value) => HttpUrl.safeParse(text(value)).success ? void 0 : "An http:// or https:// URL is required."));
|
|
286
|
+
}
|
|
287
|
+
async function askFrom(ask, io) {
|
|
288
|
+
return text(await askUntil(ask, io, {
|
|
289
|
+
type: "text",
|
|
290
|
+
message: "The address it sends from"
|
|
291
|
+
}, (value) => email().safeParse(text(value)).success ? void 0 : "An email address is required, e.g. noreply@example.com."));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* An SMTP server as the URL `@geekmidas/emailkit` reads: `smtp://` for
|
|
295
|
+
* STARTTLS, `smtps://` for TLS from the first byte, with the user and
|
|
296
|
+
* password percent-encoded.
|
|
297
|
+
*/
|
|
298
|
+
async function buildSmtpUrl(ask, io) {
|
|
299
|
+
const host = text(await askUntil(ask, io, {
|
|
300
|
+
type: "text",
|
|
301
|
+
message: "SMTP host"
|
|
302
|
+
}, required("The host")));
|
|
303
|
+
const port = await askUntil(ask, io, {
|
|
304
|
+
type: "text",
|
|
305
|
+
message: "Port",
|
|
306
|
+
initial: "587"
|
|
307
|
+
}, (value) => {
|
|
308
|
+
const n = Number(value);
|
|
309
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? void 0 : "A port number is required.";
|
|
310
|
+
});
|
|
311
|
+
const user = text(await ask({
|
|
312
|
+
type: "text",
|
|
313
|
+
message: "User (empty for none)"
|
|
314
|
+
}));
|
|
315
|
+
const password = user ? await ask({
|
|
316
|
+
type: "password",
|
|
317
|
+
message: "Password"
|
|
318
|
+
}) : "";
|
|
319
|
+
const tls = await ask({
|
|
320
|
+
type: "select",
|
|
321
|
+
message: "TLS",
|
|
322
|
+
choices: [{
|
|
323
|
+
title: "STARTTLS (smtp://, usually port 587)",
|
|
324
|
+
value: "starttls"
|
|
325
|
+
}, {
|
|
326
|
+
title: "TLS on connect (smtps://, usually port 465)",
|
|
327
|
+
value: "tls"
|
|
328
|
+
}],
|
|
329
|
+
initial: 0
|
|
330
|
+
});
|
|
331
|
+
const url = new URL(`${tls === "tls" ? "smtps" : "smtp"}://${host}`);
|
|
332
|
+
url.port = String(Number(port));
|
|
333
|
+
if (user) {
|
|
334
|
+
url.username = encodeURIComponent(user);
|
|
335
|
+
url.password = encodeURIComponent(password ?? "");
|
|
336
|
+
}
|
|
337
|
+
return url.toString().replace(/\/$/, "");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* A bucket's URL, from its provider — and, optionally, a key for it alone in
|
|
341
|
+
* the URL, or the stage's shared pair. Credentials are never required: without
|
|
342
|
+
* them the SDK's own chain signs, a role included.
|
|
343
|
+
*/
|
|
344
|
+
async function buildBucket(ask, io, entry, has) {
|
|
345
|
+
const provider = await ask({
|
|
346
|
+
type: "select",
|
|
347
|
+
message: "Where is the bucket?",
|
|
348
|
+
choices: [
|
|
349
|
+
{
|
|
350
|
+
title: "AWS S3",
|
|
351
|
+
value: "s3"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
title: "Cloudflare R2",
|
|
355
|
+
value: "r2"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
title: "MinIO, or another S3-compatible store",
|
|
359
|
+
value: "compatible"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
title: "Paste an s3:// URL",
|
|
363
|
+
value: "url"
|
|
364
|
+
}
|
|
365
|
+
],
|
|
366
|
+
initial: 0
|
|
367
|
+
});
|
|
368
|
+
const bucketName = async () => text(await askUntil(ask, io, {
|
|
369
|
+
type: "text",
|
|
370
|
+
message: "Bucket name"
|
|
371
|
+
}, required("The bucket name")));
|
|
372
|
+
let address;
|
|
373
|
+
switch (provider) {
|
|
374
|
+
case "s3":
|
|
375
|
+
address = {
|
|
376
|
+
bucket: await bucketName(),
|
|
377
|
+
region: text(await askUntil(ask, io, {
|
|
378
|
+
type: "text",
|
|
379
|
+
message: "Region",
|
|
380
|
+
initial: "eu-west-1"
|
|
381
|
+
}, required("The region")))
|
|
382
|
+
};
|
|
383
|
+
break;
|
|
384
|
+
case "r2": {
|
|
385
|
+
const account = text(await askUntil(ask, io, {
|
|
386
|
+
type: "text",
|
|
387
|
+
message: "Cloudflare account id, or the R2 endpoint URL"
|
|
388
|
+
}, required("The account id or endpoint")));
|
|
389
|
+
address = {
|
|
390
|
+
bucket: await bucketName(),
|
|
391
|
+
region: "auto",
|
|
392
|
+
endpoint: /^https?:\/\//.test(account) ? account : `https://${account}.r2.cloudflarestorage.com`
|
|
393
|
+
};
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case "compatible": {
|
|
397
|
+
const endpoint = await askHttpUrl(ask, io, "Endpoint URL");
|
|
398
|
+
const bucket = await bucketName();
|
|
399
|
+
const region = text(await ask({
|
|
400
|
+
type: "text",
|
|
401
|
+
message: "Region",
|
|
402
|
+
initial: "us-east-1"
|
|
403
|
+
}));
|
|
404
|
+
const forcePathStyle = await ask({
|
|
405
|
+
type: "confirm",
|
|
406
|
+
message: "Path-style addressing? (MinIO needs it)",
|
|
407
|
+
initial: true
|
|
408
|
+
});
|
|
409
|
+
address = {
|
|
410
|
+
bucket,
|
|
411
|
+
...region ? { region } : {},
|
|
412
|
+
endpoint,
|
|
413
|
+
...forcePathStyle ? { forcePathStyle } : {}
|
|
414
|
+
};
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "url": {
|
|
418
|
+
const pasted = await askUntil(ask, io, {
|
|
419
|
+
type: "password",
|
|
420
|
+
message: "The s3:// URL"
|
|
421
|
+
}, (value) => {
|
|
422
|
+
try {
|
|
423
|
+
parse(text(value));
|
|
424
|
+
return;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return `Not an s3:// URL the storage client can read (${error.name}).`;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
address = parse(text(pasted));
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!address.accessKeyId) {
|
|
434
|
+
if (await ask({
|
|
435
|
+
type: "confirm",
|
|
436
|
+
message: `A key for '${entry.id}' alone, in its URL? (optional)`,
|
|
437
|
+
initial: false
|
|
438
|
+
})) {
|
|
439
|
+
const accessKeyId = text(await askUntil(ask, io, {
|
|
440
|
+
type: "text",
|
|
441
|
+
message: "Access key id"
|
|
442
|
+
}, required("The access key id")));
|
|
443
|
+
const secretAccessKey = await askUntil(ask, io, {
|
|
444
|
+
type: "password",
|
|
445
|
+
message: "Secret access key"
|
|
446
|
+
}, required("The secret access key"));
|
|
447
|
+
address = {
|
|
448
|
+
...address,
|
|
449
|
+
accessKeyId,
|
|
450
|
+
secretAccessKey
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const values = { [entry.key]: build(address) };
|
|
455
|
+
if (!address.accessKeyId && !has("AWS_ACCESS_KEY_ID")) {
|
|
456
|
+
if (await ask({
|
|
457
|
+
type: "confirm",
|
|
458
|
+
message: "Set the stage's shared AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY? (optional: without them a role signs)",
|
|
459
|
+
initial: false
|
|
460
|
+
})) {
|
|
461
|
+
values.AWS_ACCESS_KEY_ID = text(await askUntil(ask, io, {
|
|
462
|
+
type: "text",
|
|
463
|
+
message: "AWS_ACCESS_KEY_ID"
|
|
464
|
+
}, required("The access key id")));
|
|
465
|
+
values.AWS_SECRET_ACCESS_KEY = await askUntil(ask, io, {
|
|
466
|
+
type: "password",
|
|
467
|
+
message: "AWS_SECRET_ACCESS_KEY"
|
|
468
|
+
}, required("The secret access key"));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return values;
|
|
472
|
+
}
|
|
473
|
+
/** A field whose name says it is a secret, asked for without echo. */
|
|
474
|
+
const SECRET_FIELD = /secret|key|token|password/i;
|
|
475
|
+
/** The properties of an object schema, or nothing for any other shape. */
|
|
476
|
+
function objectProperties(info) {
|
|
477
|
+
const schema = info?.jsonSchema;
|
|
478
|
+
if (!schema || schema.type !== "object") return void 0;
|
|
479
|
+
const properties = schema.properties;
|
|
480
|
+
if (!properties || typeof properties !== "object") return void 0;
|
|
481
|
+
return {
|
|
482
|
+
properties,
|
|
483
|
+
required: Array.isArray(schema.required) ? schema.required : []
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function typesOf(property) {
|
|
487
|
+
if (Array.isArray(property.type)) return property.type;
|
|
488
|
+
return property.type ? [property.type] : [];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Credentials, field by field where the construct's schema is an object it
|
|
492
|
+
* can describe, and as JSON otherwise — checked against the schema itself
|
|
493
|
+
* before anything is kept, and asked again, with each issue's path, until
|
|
494
|
+
* the schema takes it.
|
|
495
|
+
*/
|
|
496
|
+
async function buildCredentials(ask, io, entry, schemas) {
|
|
497
|
+
const info = schemas.schemas.find((s) => s.key === entry.key);
|
|
498
|
+
const shape = objectProperties(info);
|
|
499
|
+
if (!info) io.log(` '${entry.id}' exposes no schema to check against; the value is stored as given.`);
|
|
500
|
+
for (;;) {
|
|
501
|
+
const raw = shape ? JSON.stringify(await askFields(ask, io, shape)) : text(await ask({
|
|
502
|
+
type: "password",
|
|
503
|
+
message: "The value, as JSON (or one opaque string)"
|
|
504
|
+
}));
|
|
505
|
+
const check = await schemas.check(entry.key, raw);
|
|
506
|
+
if (check.ok) return raw;
|
|
507
|
+
io.log(` ✗ The schema of '${entry.id}' refuses it:`);
|
|
508
|
+
for (const issue of check.issues) io.log(` ${issueLine(entry.key, issue)}`);
|
|
509
|
+
io.log(" Again:");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async function askFields(ask, io, shape) {
|
|
513
|
+
const value = {};
|
|
514
|
+
for (const [name, property] of Object.entries(shape.properties)) {
|
|
515
|
+
const optional = !shape.required.includes(name);
|
|
516
|
+
const label = `${name}${optional ? " (optional)" : ""}${property.description ? ` — ${property.description}` : ""}`;
|
|
517
|
+
const types = typesOf(property);
|
|
518
|
+
if (property.enum?.length) {
|
|
519
|
+
const choices = property.enum.map((option) => ({
|
|
520
|
+
title: String(option),
|
|
521
|
+
value: option
|
|
522
|
+
}));
|
|
523
|
+
const picked = await ask({
|
|
524
|
+
type: "select",
|
|
525
|
+
message: label,
|
|
526
|
+
choices: optional ? [{
|
|
527
|
+
title: "(none)",
|
|
528
|
+
value: void 0
|
|
529
|
+
}, ...choices] : choices
|
|
530
|
+
});
|
|
531
|
+
if (picked !== void 0) value[name] = picked;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (types.includes("boolean")) {
|
|
535
|
+
value[name] = await ask({
|
|
536
|
+
type: "confirm",
|
|
537
|
+
message: label,
|
|
538
|
+
initial: false
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const numeric = types.includes("number") || types.includes("integer");
|
|
543
|
+
const plain = types.length === 0 || types.includes("string");
|
|
544
|
+
const answer = await askUntil(ask, io, {
|
|
545
|
+
type: SECRET_FIELD.test(name) ? "password" : "text",
|
|
546
|
+
message: label
|
|
547
|
+
}, (given) => {
|
|
548
|
+
const typed = text(given);
|
|
549
|
+
if (!typed) return optional ? void 0 : `${name} is required.`;
|
|
550
|
+
if (numeric && !plain && Number.isNaN(Number(typed))) return `${name} is a number.`;
|
|
551
|
+
if (!numeric && !plain) try {
|
|
552
|
+
JSON.parse(typed);
|
|
553
|
+
} catch {
|
|
554
|
+
return `${name} is JSON (${types.join(" or ")}).`;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
const typed = text(answer);
|
|
558
|
+
if (!typed) continue;
|
|
559
|
+
value[name] = plain ? typed : numeric ? Number(typed) : JSON.parse(typed);
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
export { secretsAddCommand };
|
|
565
|
+
|
|
566
|
+
//# sourceMappingURL=add-BuviRszv.mjs.map
|