@geekmidas/cli 10.0.0-alpha.98 → 10.0.0-alpha.99
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/{GoDaddyProvider-C2fK-zeQ.mjs → GoDaddyProvider-206NguW1.mjs} +2 -2
- package/dist/{GoDaddyProvider-C2fK-zeQ.mjs.map → GoDaddyProvider-206NguW1.mjs.map} +1 -1
- package/dist/{GoDaddyProvider-SLlyDwD1.cjs → GoDaddyProvider-D1aQy2i1.cjs} +2 -2
- package/dist/{GoDaddyProvider-SLlyDwD1.cjs.map → GoDaddyProvider-D1aQy2i1.cjs.map} +1 -1
- package/dist/{LegacyStateStore-CAvxM8U5.cjs → LegacyStateStore-CgJEr1G7.cjs} +3 -3
- package/dist/{LegacyStateStore-CAvxM8U5.cjs.map → LegacyStateStore-CgJEr1G7.cjs.map} +1 -1
- package/dist/{LegacyStateStore-D9QDWCst.mjs → LegacyStateStore-wmFfdzuh.mjs} +3 -3
- package/dist/{LegacyStateStore-D9QDWCst.mjs.map → LegacyStateStore-wmFfdzuh.mjs.map} +1 -1
- package/dist/{LocalStateStore-QcrdjCl6.cjs → LocalStateStore-BD14kra9.cjs} +2 -2
- package/dist/{LocalStateStore-QcrdjCl6.cjs.map → LocalStateStore-BD14kra9.cjs.map} +1 -1
- package/dist/{LocalStateStore-DPQSqkSb.mjs → LocalStateStore-C5mNp91D.mjs} +2 -2
- package/dist/{LocalStateStore-DPQSqkSb.mjs.map → LocalStateStore-C5mNp91D.mjs.map} +1 -1
- package/dist/{S3StateStore-jorgKaee.mjs → S3StateStore-CEGbsDHi.mjs} +4 -4
- package/dist/{S3StateStore-jorgKaee.mjs.map → S3StateStore-CEGbsDHi.mjs.map} +1 -1
- package/dist/{S3StateStore-C16tVikU.cjs → S3StateStore-Csu_kJCy.cjs} +5 -5
- package/dist/{S3StateStore-C16tVikU.cjs.map → S3StateStore-Csu_kJCy.cjs.map} +1 -1
- package/dist/{SSMStateStore-Dvjt_INw.mjs → SSMStateStore-Bd62qwp4.mjs} +2 -2
- package/dist/{SSMStateStore-Dvjt_INw.mjs.map → SSMStateStore-Bd62qwp4.mjs.map} +1 -1
- package/dist/{SSMStateStore-mPAEr2P2.cjs → SSMStateStore-Rp21jtP5.cjs} +2 -2
- package/dist/{SSMStateStore-mPAEr2P2.cjs.map → SSMStateStore-Rp21jtP5.cjs.map} +1 -1
- package/dist/{add-CZIIsTcv.mjs → add-DyIEYI97.mjs} +4 -4
- package/dist/{add-CZIIsTcv.mjs.map → add-DyIEYI97.mjs.map} +1 -1
- package/dist/{add-SRk65Udu.cjs → add-Iuo3ao5E.cjs} +5 -5
- package/dist/{add-SRk65Udu.cjs.map → add-Iuo3ao5E.cjs.map} +1 -1
- package/dist/{bundler-B0Pj5FXT.cjs → bundler-Bgoq8IyR.cjs} +5 -5
- package/dist/{bundler-B0Pj5FXT.cjs.map → bundler-Bgoq8IyR.cjs.map} +1 -1
- package/dist/{bundler-DxJ-cIEt.mjs → bundler-Q6Kp8b0r.mjs} +5 -5
- package/dist/{bundler-DxJ-cIEt.mjs.map → bundler-Q6Kp8b0r.mjs.map} +1 -1
- package/dist/{cli-CV85bY3-.mjs → cli-B9BudUV0.mjs} +2 -4
- package/dist/cli-B9BudUV0.mjs.map +1 -0
- package/dist/{cli-CRDJWWmZ.cjs → cli-W2DMRIbP.cjs} +2 -4
- package/dist/cli-W2DMRIbP.cjs.map +1 -0
- package/dist/compose-CIrFNWHb.cjs +3147 -0
- package/dist/compose-CIrFNWHb.cjs.map +1 -0
- package/dist/compose-CLsRcHkA.mjs +3052 -0
- package/dist/compose-CLsRcHkA.mjs.map +1 -0
- package/dist/{config-pBzLfNef.d.mts → config-BVdHfj8b.d.cts} +4 -4
- package/dist/{config-CS76CzXM.d.cts.map → config-BVdHfj8b.d.cts.map} +1 -1
- package/dist/{config-CS76CzXM.d.cts → config-D2OqCmAM.d.mts} +4 -4
- package/dist/{config-pBzLfNef.d.mts.map → config-D2OqCmAM.d.mts.map} +1 -1
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +1 -1
- package/dist/deploy/public.cjs +9 -8
- package/dist/deploy/public.cjs.map +1 -1
- package/dist/deploy/public.d.cts +5 -16
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +5 -16
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +8 -7
- package/dist/deploy/public.mjs.map +1 -1
- package/dist/{deploy-277jek46.cjs → deploy-CbJMJsgX.cjs} +26 -62
- package/dist/deploy-CbJMJsgX.cjs.map +1 -0
- package/dist/{deploy-Dw8g3sLY.mjs → deploy-n_IB8r6H.mjs} +17 -59
- package/dist/deploy-n_IB8r6H.mjs.map +1 -0
- package/dist/{discover-DJCCgimv.d.mts → discover-Caf6Hnv1.d.cts} +2 -2
- package/dist/{discover-CU1LjCOf.d.cts.map → discover-Caf6Hnv1.d.cts.map} +1 -1
- package/dist/{discover-CU1LjCOf.d.cts → discover-Cd5M2jPv.d.mts} +2 -2
- package/dist/{discover-DJCCgimv.d.mts.map → discover-Cd5M2jPv.d.mts.map} +1 -1
- package/dist/{dns-LqWzPb8l.mjs → dns-C_LPS3qV.mjs} +3 -3
- package/dist/{dns-LqWzPb8l.mjs.map → dns-C_LPS3qV.mjs.map} +1 -1
- package/dist/{dns-Dhjbnjgl.cjs → dns-kp8To8Qj.cjs} +3 -3
- package/dist/{dns-Dhjbnjgl.cjs.map → dns-kp8To8Qj.cjs.map} +1 -1
- package/dist/{dokploy-D_uCbZQ7.mjs → dokploy-Dl_kNVeE.mjs} +2 -2
- package/dist/{dokploy-D_uCbZQ7.mjs.map → dokploy-Dl_kNVeE.mjs.map} +1 -1
- package/dist/{dokploy-Dw4oCXqR.cjs → dokploy-gRIae9Ht.cjs} +2 -2
- package/dist/{dokploy-Dw4oCXqR.cjs.map → dokploy-gRIae9Ht.cjs.map} +1 -1
- package/dist/{encryption-C0uKYE9v.mjs → encryption-C3DcVZQs.mjs} +3 -3
- package/dist/{encryption-C0uKYE9v.mjs.map → encryption-C3DcVZQs.mjs.map} +1 -1
- package/dist/{encryption-CtVPHWWq.cjs → encryption-CmQvQOSe.cjs} +3 -3
- package/dist/{encryption-CtVPHWWq.cjs.map → encryption-CmQvQOSe.cjs.map} +1 -1
- package/dist/{engine-BE5PdE8S.mjs → engine-DBXuhHyf.mjs} +5 -4
- package/dist/{engine-BE5PdE8S.mjs.map → engine-DBXuhHyf.mjs.map} +1 -1
- package/dist/{engine-HI8aLw1M.cjs → engine-Dk19Mxc4.cjs} +27 -26
- package/dist/{engine-HI8aLw1M.cjs.map → engine-Dk19Mxc4.cjs.map} +1 -1
- package/dist/{github-BCdfZ9zO.cjs → github-Ds2RYnSU.cjs} +9 -8
- package/dist/{github-BCdfZ9zO.cjs.map → github-Ds2RYnSU.cjs.map} +1 -1
- package/dist/{github-CT3X720_.mjs → github-RawRLERq.mjs} +7 -6
- package/dist/{github-CT3X720_.mjs.map → github-RawRLERq.mjs.map} +1 -1
- package/dist/identity-CBXr4wNi.mjs +129 -0
- package/dist/identity-CBXr4wNi.mjs.map +1 -0
- package/dist/identity-CbLfgQNT.cjs +182 -0
- package/dist/identity-CbLfgQNT.cjs.map +1 -0
- package/dist/{index-DKtB01YC.d.cts → index-BU4G20G6.d.cts} +2 -2
- package/dist/{index-DKtB01YC.d.cts.map → index-BU4G20G6.d.cts.map} +1 -1
- package/dist/{index-ZAUrXH6Q.d.mts → index-yDbd1WcM.d.mts} +2 -2
- package/dist/{index-ZAUrXH6Q.d.mts.map → index-yDbd1WcM.d.mts.map} +1 -1
- package/dist/index.cjs +131 -141
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +128 -138
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CCZUgxMK.mjs → migrate-CZIGT3kB.mjs} +3 -3
- package/dist/{migrate-CCZUgxMK.mjs.map → migrate-CZIGT3kB.mjs.map} +1 -1
- package/dist/{migrate-DBGyoBvZ.cjs → migrate-CxZYzF_u.cjs} +3 -3
- package/dist/{migrate-DBGyoBvZ.cjs.map → migrate-CxZYzF_u.cjs.map} +1 -1
- package/dist/{migrate-DH-lOK3j.mjs → migrate-DR1x96I4.mjs} +3 -3
- package/dist/{migrate-DH-lOK3j.mjs.map → migrate-DR1x96I4.mjs.map} +1 -1
- package/dist/{migrate-Cs7kQxD5.cjs → migrate-DjrTwWJA.cjs} +3 -3
- package/dist/{migrate-Cs7kQxD5.cjs.map → migrate-DjrTwWJA.cjs.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/{projectBucket-DrkEht-j.mjs → projectBucket-BYOxRnHY.mjs} +2 -2
- package/dist/{projectBucket-DrkEht-j.mjs.map → projectBucket-BYOxRnHY.mjs.map} +1 -1
- package/dist/{projectBucket-BjeDcAYy.cjs → projectBucket-CDu5rXjp.cjs} +2 -2
- package/dist/{projectBucket-BjeDcAYy.cjs.map → projectBucket-CDu5rXjp.cjs.map} +1 -1
- package/dist/reconcile/public.d.cts +2 -2
- package/dist/reconcile/public.d.mts +2 -2
- package/dist/{resolve-OfLaxaxY.mjs → resolve-CNYbGH8g.mjs} +3 -3
- package/dist/{resolve-OfLaxaxY.mjs.map → resolve-CNYbGH8g.mjs.map} +1 -1
- package/dist/{resolve-CScrqgdC.cjs → resolve-DC98Rl1t.cjs} +3 -3
- package/dist/{resolve-CScrqgdC.cjs.map → resolve-DC98Rl1t.cjs.map} +1 -1
- package/dist/{rollback-DQBSN-i7.mjs → rollback-BkIkE8Qe.mjs} +5 -5
- package/dist/{rollback-DQBSN-i7.mjs.map → rollback-BkIkE8Qe.mjs.map} +1 -1
- package/dist/{rollback-D7p4wJNO.cjs → rollback-DBfLvC8k.cjs} +5 -5
- package/dist/{rollback-D7p4wJNO.cjs.map → rollback-DBfLvC8k.cjs.map} +1 -1
- package/dist/{runtime-BlHOVx8L.d.cts → runtime-CLz3bf07.d.mts} +2 -2
- package/dist/{runtime-BlHOVx8L.d.cts.map → runtime-CLz3bf07.d.mts.map} +1 -1
- package/dist/{runtime-DMq9C3KB.d.mts → runtime-CUWf0IUk.d.cts} +2 -2
- package/dist/{runtime-DMq9C3KB.d.mts.map → runtime-CUWf0IUk.d.cts.map} +1 -1
- package/dist/{s3-CUKzBBae.mjs → s3-BwhQk3QM.mjs} +3 -3
- package/dist/{s3-CUKzBBae.mjs.map → s3-BwhQk3QM.mjs.map} +1 -1
- package/dist/{s3-QQi4sNOQ.cjs → s3-DLp0V-Ox.cjs} +3 -3
- package/dist/{s3-QQi4sNOQ.cjs.map → s3-DLp0V-Ox.cjs.map} +1 -1
- package/dist/{sandbox-Cm4X6HRh.d.mts → sandbox-CfpLWTPP.d.mts} +2 -2
- package/dist/{sandbox-ArJ_hEKk.d.cts.map → sandbox-CfpLWTPP.d.mts.map} +1 -1
- package/dist/{sandbox-ArJ_hEKk.d.cts → sandbox-D1XeNEJ9.d.cts} +2 -2
- package/dist/{sandbox-Cm4X6HRh.d.mts.map → sandbox-D1XeNEJ9.d.cts.map} +1 -1
- package/dist/{seeds-Df4BI-T3.cjs → seeds-BNHSpjNP.cjs} +9 -179
- package/dist/seeds-BNHSpjNP.cjs.map +1 -0
- package/dist/{seeds-BvxWUGKJ.mjs → seeds-C4bZ8L-T.mjs} +10 -132
- package/dist/seeds-C4bZ8L-T.mjs.map +1 -0
- package/dist/{sniffer-BqjXvcaK.mjs → sniffer-BIBm9dAb.mjs} +7 -7
- package/dist/{sniffer-BqjXvcaK.mjs.map → sniffer-BIBm9dAb.mjs.map} +1 -1
- package/dist/{sniffer-DZeQJKty.cjs → sniffer-Cylp9W2D.cjs} +7 -7
- package/dist/{sniffer-DZeQJKty.cjs.map → sniffer-Cylp9W2D.cjs.map} +1 -1
- package/dist/{stages-6SEoRA4x.mjs → stages-CcS9vnNu.mjs} +3 -17
- package/dist/stages-CcS9vnNu.mjs.map +1 -0
- package/dist/{stages-DH5z89eT.cjs → stages-DdBV7msu.cjs} +3 -17
- package/dist/stages-DdBV7msu.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/{types-Drpl9HX9.d.mts → types-BhThob0H.d.mts} +29 -13
- package/dist/types-BhThob0H.d.mts.map +1 -0
- package/dist/{types-DipUP9zp.d.cts → types-DwoLHelT.d.cts} +29 -13
- package/dist/types-DwoLHelT.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BBeT6Pil.mjs → workspace-D5CpOp1y.mjs} +22 -2
- package/dist/workspace-D5CpOp1y.mjs.map +1 -0
- package/dist/{workspace-BJ78Bgvm.cjs → workspace-DRjdu1OY.cjs} +22 -2
- package/dist/workspace-DRjdu1OY.cjs.map +1 -0
- package/package.json +15 -15
- package/dist/cli-CRDJWWmZ.cjs.map +0 -1
- package/dist/cli-CV85bY3-.mjs.map +0 -1
- package/dist/compose-CQQRRJC3.mjs +0 -1511
- package/dist/compose-CQQRRJC3.mjs.map +0 -1
- package/dist/compose-DMY4ouEk.cjs +0 -1558
- package/dist/compose-DMY4ouEk.cjs.map +0 -1
- package/dist/deploy-277jek46.cjs.map +0 -1
- package/dist/deploy-Dw8g3sLY.mjs.map +0 -1
- package/dist/dns-4RAiVFa7.cjs +0 -55
- package/dist/dns-4RAiVFa7.cjs.map +0 -1
- package/dist/dns-CU22K_i0.mjs +0 -44
- package/dist/dns-CU22K_i0.mjs.map +0 -1
- package/dist/secrets-AA_tQUl-.mjs +0 -36
- package/dist/secrets-AA_tQUl-.mjs.map +0 -1
- package/dist/secrets-B_biKsKx.cjs +0 -47
- package/dist/secrets-B_biKsKx.cjs.map +0 -1
- package/dist/seeds-BvxWUGKJ.mjs.map +0 -1
- package/dist/seeds-Df4BI-T3.cjs.map +0 -1
- package/dist/stack-A6MAFZ6u.mjs +0 -1201
- package/dist/stack-A6MAFZ6u.mjs.map +0 -1
- package/dist/stack-BpkZEEfB.cjs +0 -1308
- package/dist/stack-BpkZEEfB.cjs.map +0 -1
- package/dist/stages-6SEoRA4x.mjs.map +0 -1
- package/dist/stages-DH5z89eT.cjs.map +0 -1
- package/dist/types-DipUP9zp.d.cts.map +0 -1
- package/dist/types-Drpl9HX9.d.mts.map +0 -1
- package/dist/workspace-BBeT6Pil.mjs.map +0 -1
- package/dist/workspace-BJ78Bgvm.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3-CUKzBBae.mjs","names":[],"sources":["../src/secrets/s3.ts"],"sourcesContent":["/**\n * A stage's secrets in S3: one object per stage,\n * `<prefix>/<project>/<stage>/secrets.json`, in the project bucket beside the\n * deploy state (`providers/projectBucket.ts`) — or in a bucket the config\n * names, which must exist.\n *\n * Encrypted at rest with SSE-S3, asked for on every write although the\n * project bucket defaults to it, so no KMS key is another place to keep. The\n * bucket's versioning keeps every past document. Unlike an SSM parameter\n * there is no size to outgrow.\n *\n * The project bucket is created by the first write, never by a read: a stage\n * whose bucket or object does not exist yet has no secrets. Every write is\n * conditional — `If-Match` on the ETag this store last read for the stage,\n * `If-None-Match: *` when it read none — so two `secrets:set` runs at once\n * cannot each overwrite the other's key: the second is refused with\n * {@link StageSecretsChanged}.\n */\n\nimport {\n\tGetObjectCommand,\n\tHeadObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport type { DeployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket.js';\nimport {\n\ttype AwsStoreOptions,\n\tawsClientConfig,\n\twithStageCredentials,\n} from './awsStore.js';\nimport { s3SecretsKey } from './providers.js';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface S3SecretsStoreOptions extends AwsStoreOptions {\n\t/** The deploy identity: the project bucket's name and its tag. */\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\t/** A bucket that exists; omitted, the project bucket. */\n\tbucket?: string;\n\t/** Key prefix inside the bucket, without a trailing slash. */\n\tprefix: string;\n\t/** For tests: credentials to use instead of the default chain. */\n\tcredentials?: S3ClientConfig['credentials'];\n}\n\n/**\n * Another run wrote the stage's secrets after this one read them. Nothing was\n * written: the change is to be made again, on top of theirs.\n */\nexport class StageSecretsChanged extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\t/** `s3://<bucket>/<key>` */\n\t\treadonly location: string,\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" at ${location} changed after this command read them — ` +\n\t\t\t\t'another secrets command or deploy wrote them first. Nothing was written; run the command ' +\n\t\t\t\t'again to make the change on top of theirs.',\n\t\t);\n\t\tthis.name = 'StageSecretsChanged';\n\t}\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\nexport class S3SecretsStore implements SecretsStore {\n\treadonly name = 's3';\n\n\tprivate clients?: { s3: S3Client; sts: STSClient };\n\t/** The bucket's name, once known. */\n\tprivate bucketName?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\t/** The ETag each stage was last read at — null: read, and not there. */\n\tprivate readonly etags = new Map<string, string | null>();\n\n\tconstructor(private readonly options: S3SecretsStoreOptions) {\n\t\tif (options.bucket) {\n\t\t\tthis.bucketName = options.bucket;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; the project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\treturn (\n\t\t\tthis.bucketName ??\n\t\t\tprojectBucketName(this.options.identity.scope, '<account id>')\n\t\t);\n\t}\n\n\t/** Where a stage's secrets are kept: `s3://<bucket>/<key>`. */\n\tlocation(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage)}`;\n\t}\n\n\tprivate key(stage: string): string {\n\t\treturn s3SecretsKey(this.options.prefix, this.options.project, stage);\n\t}\n\n\tprivate async client(): Promise<{ s3: S3Client; sts: STSClient }> {\n\t\tif (this.clients) return this.clients;\n\t\tconst base = await awsClientConfig(this.options);\n\t\tconst endpoint =\n\t\t\tbase.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\tconst credentials = this.options.credentials ?? base.credentials;\n\t\tthis.clients = {\n\t\t\ts3: new S3Client({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(endpoint ? { endpoint, forcePathStyle: true } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t\tsts: new STSClient({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(base.endpoint ? { endpoint: base.endpoint } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t};\n\t\treturn this.clients;\n\t}\n\n\t/** One AWS call, a missing or expired credential refused by name. */\n\tprivate call<T>(\n\t\tstage: string,\n\t\taccess: 'read' | 'write',\n\t\trun: () => Promise<T>,\n\t): Promise<T> {\n\t\treturn withStageCredentials(\n\t\t\t{ stage, store: this.name, profile: this.options.profile, access },\n\t\t\trun,\n\t\t);\n\t}\n\n\t/**\n\t * The bucket, or null for a project bucket that does not exist yet and is\n\t * not to be created (a read).\n\t */\n\tprivate async use(\n\t\tstage: string,\n\t\tcreate: boolean,\n\t): Promise<{ s3: S3Client; bucket: string | null }> {\n\t\tconst { s3, sts } = await this.client();\n\t\tif (this.exists) return { s3, bucket: this.bucketName! };\n\t\tconst access = create ? 'write' : 'read';\n\t\tthis.bucketName ??= projectBucketName(\n\t\t\tthis.options.identity.scope,\n\t\t\tawait this.call(stage, access, () => callerAccountId(sts)),\n\t\t);\n\t\tconst bucket = this.bucketName;\n\t\tif (create) {\n\t\t\tawait this.call(stage, access, () =>\n\t\t\t\tensureProjectBucket(s3, {\n\t\t\t\t\tbucket,\n\t\t\t\t\tregion: this.options.region,\n\t\t\t\t\tidentity: this.options.identity,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (\n\t\t\t(await this.call(stage, access, () => findProjectBucket(s3, bucket))) ===\n\t\t\t'none'\n\t\t) {\n\t\t\treturn { s3, bucket: null };\n\t\t}\n\t\tthis.exists = true;\n\t\treturn { s3, bucket };\n\t}\n\n\t/**\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * read it with, or they have expired\n\t */\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst { s3, bucket } = await this.use(stage, false);\n\t\tif (!bucket) {\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tconst response = await this.call(stage, 'read', () =>\n\t\t\t\ts3.send(new GetObjectCommand({ Bucket: bucket, Key: this.key(stage) })),\n\t\t\t);\n\t\t\tconst body = await (\n\t\t\t\tresponse.Body as {\n\t\t\t\t\ttransformToString(encoding: string): Promise<string>;\n\t\t\t\t}\n\t\t\t).transformToString('utf-8');\n\t\t\tthis.remember(stage, response.ETag);\n\t\t\treturn JSON.parse(body) as StageSecrets;\n\t\t} catch (error) {\n\t\t\tif (!notFound(error)) throw error;\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Replace the stage's secrets — on the condition that they are still what\n\t * this store last read. A stage never read here is replaced as it is now.\n\t *\n\t * @throws {StageSecretsChanged} when another run wrote them in between\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * write it with, or they have expired\n\t */\n\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst { s3, bucket } = await this.use(stage, true);\n\t\tconst Bucket = bucket!;\n\t\tconst Key = this.key(stage);\n\t\tconst expected = this.etags.has(stage)\n\t\t\t? (this.etags.get(stage) ?? null)\n\t\t\t: await this.currentEtag(s3, stage, Bucket, Key);\n\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(\n\t\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\t\tBucket,\n\t\t\t\t\t\tKey,\n\t\t\t\t\t\tBody: JSON.stringify(secrets),\n\t\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t\tServerSideEncryption: 'AES256',\n\t\t\t\t\t\t...(expected === null\n\t\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t\t: { IfMatch: expected }),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.remember(stage, ETag);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthis.etags.delete(stage);\n\t\t\t\tthrow new StageSecretsChanged(stage, `s3://${Bucket}/${Key}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * The ETag the stage is now at. Some S3-compatible servers answer without\n\t * one: then nothing is remembered, and the next write asks for it first.\n\t */\n\tprivate remember(stage: string, etag: string | undefined): void {\n\t\tif (etag) this.etags.set(stage, etag);\n\t\telse this.etags.delete(stage);\n\t}\n\n\tprivate async currentEtag(\n\t\ts3: S3Client,\n\t\tstage: string,\n\t\tBucket: string,\n\t\tKey: string,\n\t): Promise<string | null> {\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(new HeadObjectCommand({ Bucket, Key })),\n\t\t\t);\n\t\t\treturn ETag ?? null;\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"s3-BwhQk3QM.mjs","names":[],"sources":["../src/secrets/s3.ts"],"sourcesContent":["/**\n * A stage's secrets in S3: one object per stage,\n * `<prefix>/<project>/<stage>/secrets.json`, in the project bucket beside the\n * deploy state (`providers/projectBucket.ts`) — or in a bucket the config\n * names, which must exist.\n *\n * Encrypted at rest with SSE-S3, asked for on every write although the\n * project bucket defaults to it, so no KMS key is another place to keep. The\n * bucket's versioning keeps every past document. Unlike an SSM parameter\n * there is no size to outgrow.\n *\n * The project bucket is created by the first write, never by a read: a stage\n * whose bucket or object does not exist yet has no secrets. Every write is\n * conditional — `If-Match` on the ETag this store last read for the stage,\n * `If-None-Match: *` when it read none — so two `secrets:set` runs at once\n * cannot each overwrite the other's key: the second is refused with\n * {@link StageSecretsChanged}.\n */\n\nimport {\n\tGetObjectCommand,\n\tHeadObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport type { DeployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket.js';\nimport {\n\ttype AwsStoreOptions,\n\tawsClientConfig,\n\twithStageCredentials,\n} from './awsStore.js';\nimport { s3SecretsKey } from './providers.js';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface S3SecretsStoreOptions extends AwsStoreOptions {\n\t/** The deploy identity: the project bucket's name and its tag. */\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\t/** A bucket that exists; omitted, the project bucket. */\n\tbucket?: string;\n\t/** Key prefix inside the bucket, without a trailing slash. */\n\tprefix: string;\n\t/** For tests: credentials to use instead of the default chain. */\n\tcredentials?: S3ClientConfig['credentials'];\n}\n\n/**\n * Another run wrote the stage's secrets after this one read them. Nothing was\n * written: the change is to be made again, on top of theirs.\n */\nexport class StageSecretsChanged extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\t/** `s3://<bucket>/<key>` */\n\t\treadonly location: string,\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" at ${location} changed after this command read them — ` +\n\t\t\t\t'another secrets command or deploy wrote them first. Nothing was written; run the command ' +\n\t\t\t\t'again to make the change on top of theirs.',\n\t\t);\n\t\tthis.name = 'StageSecretsChanged';\n\t}\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\nexport class S3SecretsStore implements SecretsStore {\n\treadonly name = 's3';\n\n\tprivate clients?: { s3: S3Client; sts: STSClient };\n\t/** The bucket's name, once known. */\n\tprivate bucketName?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\t/** The ETag each stage was last read at — null: read, and not there. */\n\tprivate readonly etags = new Map<string, string | null>();\n\n\tconstructor(private readonly options: S3SecretsStoreOptions) {\n\t\tif (options.bucket) {\n\t\t\tthis.bucketName = options.bucket;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; the project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\treturn (\n\t\t\tthis.bucketName ??\n\t\t\tprojectBucketName(this.options.identity.scope, '<account id>')\n\t\t);\n\t}\n\n\t/** Where a stage's secrets are kept: `s3://<bucket>/<key>`. */\n\tlocation(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage)}`;\n\t}\n\n\tprivate key(stage: string): string {\n\t\treturn s3SecretsKey(this.options.prefix, this.options.project, stage);\n\t}\n\n\tprivate async client(): Promise<{ s3: S3Client; sts: STSClient }> {\n\t\tif (this.clients) return this.clients;\n\t\tconst base = await awsClientConfig(this.options);\n\t\tconst endpoint =\n\t\t\tbase.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\tconst credentials = this.options.credentials ?? base.credentials;\n\t\tthis.clients = {\n\t\t\ts3: new S3Client({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(endpoint ? { endpoint, forcePathStyle: true } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t\tsts: new STSClient({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(base.endpoint ? { endpoint: base.endpoint } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t};\n\t\treturn this.clients;\n\t}\n\n\t/** One AWS call, a missing or expired credential refused by name. */\n\tprivate call<T>(\n\t\tstage: string,\n\t\taccess: 'read' | 'write',\n\t\trun: () => Promise<T>,\n\t): Promise<T> {\n\t\treturn withStageCredentials(\n\t\t\t{ stage, store: this.name, profile: this.options.profile, access },\n\t\t\trun,\n\t\t);\n\t}\n\n\t/**\n\t * The bucket, or null for a project bucket that does not exist yet and is\n\t * not to be created (a read).\n\t */\n\tprivate async use(\n\t\tstage: string,\n\t\tcreate: boolean,\n\t): Promise<{ s3: S3Client; bucket: string | null }> {\n\t\tconst { s3, sts } = await this.client();\n\t\tif (this.exists) return { s3, bucket: this.bucketName! };\n\t\tconst access = create ? 'write' : 'read';\n\t\tthis.bucketName ??= projectBucketName(\n\t\t\tthis.options.identity.scope,\n\t\t\tawait this.call(stage, access, () => callerAccountId(sts)),\n\t\t);\n\t\tconst bucket = this.bucketName;\n\t\tif (create) {\n\t\t\tawait this.call(stage, access, () =>\n\t\t\t\tensureProjectBucket(s3, {\n\t\t\t\t\tbucket,\n\t\t\t\t\tregion: this.options.region,\n\t\t\t\t\tidentity: this.options.identity,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (\n\t\t\t(await this.call(stage, access, () => findProjectBucket(s3, bucket))) ===\n\t\t\t'none'\n\t\t) {\n\t\t\treturn { s3, bucket: null };\n\t\t}\n\t\tthis.exists = true;\n\t\treturn { s3, bucket };\n\t}\n\n\t/**\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * read it with, or they have expired\n\t */\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst { s3, bucket } = await this.use(stage, false);\n\t\tif (!bucket) {\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tconst response = await this.call(stage, 'read', () =>\n\t\t\t\ts3.send(new GetObjectCommand({ Bucket: bucket, Key: this.key(stage) })),\n\t\t\t);\n\t\t\tconst body = await (\n\t\t\t\tresponse.Body as {\n\t\t\t\t\ttransformToString(encoding: string): Promise<string>;\n\t\t\t\t}\n\t\t\t).transformToString('utf-8');\n\t\t\tthis.remember(stage, response.ETag);\n\t\t\treturn JSON.parse(body) as StageSecrets;\n\t\t} catch (error) {\n\t\t\tif (!notFound(error)) throw error;\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Replace the stage's secrets — on the condition that they are still what\n\t * this store last read. A stage never read here is replaced as it is now.\n\t *\n\t * @throws {StageSecretsChanged} when another run wrote them in between\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * write it with, or they have expired\n\t */\n\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst { s3, bucket } = await this.use(stage, true);\n\t\tconst Bucket = bucket!;\n\t\tconst Key = this.key(stage);\n\t\tconst expected = this.etags.has(stage)\n\t\t\t? (this.etags.get(stage) ?? null)\n\t\t\t: await this.currentEtag(s3, stage, Bucket, Key);\n\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(\n\t\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\t\tBucket,\n\t\t\t\t\t\tKey,\n\t\t\t\t\t\tBody: JSON.stringify(secrets),\n\t\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t\tServerSideEncryption: 'AES256',\n\t\t\t\t\t\t...(expected === null\n\t\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t\t: { IfMatch: expected }),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.remember(stage, ETag);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthis.etags.delete(stage);\n\t\t\t\tthrow new StageSecretsChanged(stage, `s3://${Bucket}/${Key}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * The ETag the stage is now at. Some S3-compatible servers answer without\n\t * one: then nothing is remembered, and the next write asks for it first.\n\t */\n\tprivate remember(stage: string, etag: string | undefined): void {\n\t\tif (etag) this.etags.set(stage, etag);\n\t\telse this.etags.delete(stage);\n\t}\n\n\tprivate async currentEtag(\n\t\ts3: S3Client,\n\t\tstage: string,\n\t\tBucket: string,\n\t\tKey: string,\n\t): Promise<string | null> {\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(new HeadObjectCommand({ Bucket, Key })),\n\t\t\t);\n\t\t\treturn ETag ?? null;\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
2
|
-
const require_workspace = require("./workspace-
|
|
3
|
-
const require_projectBucket = require("./projectBucket-
|
|
2
|
+
const require_workspace = require("./workspace-DRjdu1OY.cjs");
|
|
3
|
+
const require_projectBucket = require("./projectBucket-CDu5rXjp.cjs");
|
|
4
4
|
const require_awsStore = require("./awsStore-u3ehhMoY.cjs");
|
|
5
5
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
6
6
|
let _aws_sdk_client_sts = require("@aws-sdk/client-sts");
|
|
@@ -212,4 +212,4 @@ var S3SecretsStore = class {
|
|
|
212
212
|
//#endregion
|
|
213
213
|
exports.S3SecretsStore = S3SecretsStore;
|
|
214
214
|
|
|
215
|
-
//# sourceMappingURL=s3-
|
|
215
|
+
//# sourceMappingURL=s3-DLp0V-Ox.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3-QQi4sNOQ.cjs","names":["GkmError","projectBucketName","s3SecretsKey","awsClientConfig","S3Client","STSClient","withStageCredentials","callerAccountId","ensureProjectBucket","findProjectBucket","GetObjectCommand","PutObjectCommand","HeadObjectCommand"],"sources":["../src/secrets/s3.ts"],"sourcesContent":["/**\n * A stage's secrets in S3: one object per stage,\n * `<prefix>/<project>/<stage>/secrets.json`, in the project bucket beside the\n * deploy state (`providers/projectBucket.ts`) — or in a bucket the config\n * names, which must exist.\n *\n * Encrypted at rest with SSE-S3, asked for on every write although the\n * project bucket defaults to it, so no KMS key is another place to keep. The\n * bucket's versioning keeps every past document. Unlike an SSM parameter\n * there is no size to outgrow.\n *\n * The project bucket is created by the first write, never by a read: a stage\n * whose bucket or object does not exist yet has no secrets. Every write is\n * conditional — `If-Match` on the ETag this store last read for the stage,\n * `If-None-Match: *` when it read none — so two `secrets:set` runs at once\n * cannot each overwrite the other's key: the second is refused with\n * {@link StageSecretsChanged}.\n */\n\nimport {\n\tGetObjectCommand,\n\tHeadObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport type { DeployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket.js';\nimport {\n\ttype AwsStoreOptions,\n\tawsClientConfig,\n\twithStageCredentials,\n} from './awsStore.js';\nimport { s3SecretsKey } from './providers.js';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface S3SecretsStoreOptions extends AwsStoreOptions {\n\t/** The deploy identity: the project bucket's name and its tag. */\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\t/** A bucket that exists; omitted, the project bucket. */\n\tbucket?: string;\n\t/** Key prefix inside the bucket, without a trailing slash. */\n\tprefix: string;\n\t/** For tests: credentials to use instead of the default chain. */\n\tcredentials?: S3ClientConfig['credentials'];\n}\n\n/**\n * Another run wrote the stage's secrets after this one read them. Nothing was\n * written: the change is to be made again, on top of theirs.\n */\nexport class StageSecretsChanged extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\t/** `s3://<bucket>/<key>` */\n\t\treadonly location: string,\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" at ${location} changed after this command read them — ` +\n\t\t\t\t'another secrets command or deploy wrote them first. Nothing was written; run the command ' +\n\t\t\t\t'again to make the change on top of theirs.',\n\t\t);\n\t\tthis.name = 'StageSecretsChanged';\n\t}\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\nexport class S3SecretsStore implements SecretsStore {\n\treadonly name = 's3';\n\n\tprivate clients?: { s3: S3Client; sts: STSClient };\n\t/** The bucket's name, once known. */\n\tprivate bucketName?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\t/** The ETag each stage was last read at — null: read, and not there. */\n\tprivate readonly etags = new Map<string, string | null>();\n\n\tconstructor(private readonly options: S3SecretsStoreOptions) {\n\t\tif (options.bucket) {\n\t\t\tthis.bucketName = options.bucket;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; the project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\treturn (\n\t\t\tthis.bucketName ??\n\t\t\tprojectBucketName(this.options.identity.scope, '<account id>')\n\t\t);\n\t}\n\n\t/** Where a stage's secrets are kept: `s3://<bucket>/<key>`. */\n\tlocation(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage)}`;\n\t}\n\n\tprivate key(stage: string): string {\n\t\treturn s3SecretsKey(this.options.prefix, this.options.project, stage);\n\t}\n\n\tprivate async client(): Promise<{ s3: S3Client; sts: STSClient }> {\n\t\tif (this.clients) return this.clients;\n\t\tconst base = await awsClientConfig(this.options);\n\t\tconst endpoint =\n\t\t\tbase.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\tconst credentials = this.options.credentials ?? base.credentials;\n\t\tthis.clients = {\n\t\t\ts3: new S3Client({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(endpoint ? { endpoint, forcePathStyle: true } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t\tsts: new STSClient({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(base.endpoint ? { endpoint: base.endpoint } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t};\n\t\treturn this.clients;\n\t}\n\n\t/** One AWS call, a missing or expired credential refused by name. */\n\tprivate call<T>(\n\t\tstage: string,\n\t\taccess: 'read' | 'write',\n\t\trun: () => Promise<T>,\n\t): Promise<T> {\n\t\treturn withStageCredentials(\n\t\t\t{ stage, store: this.name, profile: this.options.profile, access },\n\t\t\trun,\n\t\t);\n\t}\n\n\t/**\n\t * The bucket, or null for a project bucket that does not exist yet and is\n\t * not to be created (a read).\n\t */\n\tprivate async use(\n\t\tstage: string,\n\t\tcreate: boolean,\n\t): Promise<{ s3: S3Client; bucket: string | null }> {\n\t\tconst { s3, sts } = await this.client();\n\t\tif (this.exists) return { s3, bucket: this.bucketName! };\n\t\tconst access = create ? 'write' : 'read';\n\t\tthis.bucketName ??= projectBucketName(\n\t\t\tthis.options.identity.scope,\n\t\t\tawait this.call(stage, access, () => callerAccountId(sts)),\n\t\t);\n\t\tconst bucket = this.bucketName;\n\t\tif (create) {\n\t\t\tawait this.call(stage, access, () =>\n\t\t\t\tensureProjectBucket(s3, {\n\t\t\t\t\tbucket,\n\t\t\t\t\tregion: this.options.region,\n\t\t\t\t\tidentity: this.options.identity,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (\n\t\t\t(await this.call(stage, access, () => findProjectBucket(s3, bucket))) ===\n\t\t\t'none'\n\t\t) {\n\t\t\treturn { s3, bucket: null };\n\t\t}\n\t\tthis.exists = true;\n\t\treturn { s3, bucket };\n\t}\n\n\t/**\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * read it with, or they have expired\n\t */\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst { s3, bucket } = await this.use(stage, false);\n\t\tif (!bucket) {\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tconst response = await this.call(stage, 'read', () =>\n\t\t\t\ts3.send(new GetObjectCommand({ Bucket: bucket, Key: this.key(stage) })),\n\t\t\t);\n\t\t\tconst body = await (\n\t\t\t\tresponse.Body as {\n\t\t\t\t\ttransformToString(encoding: string): Promise<string>;\n\t\t\t\t}\n\t\t\t).transformToString('utf-8');\n\t\t\tthis.remember(stage, response.ETag);\n\t\t\treturn JSON.parse(body) as StageSecrets;\n\t\t} catch (error) {\n\t\t\tif (!notFound(error)) throw error;\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Replace the stage's secrets — on the condition that they are still what\n\t * this store last read. A stage never read here is replaced as it is now.\n\t *\n\t * @throws {StageSecretsChanged} when another run wrote them in between\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * write it with, or they have expired\n\t */\n\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst { s3, bucket } = await this.use(stage, true);\n\t\tconst Bucket = bucket!;\n\t\tconst Key = this.key(stage);\n\t\tconst expected = this.etags.has(stage)\n\t\t\t? (this.etags.get(stage) ?? null)\n\t\t\t: await this.currentEtag(s3, stage, Bucket, Key);\n\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(\n\t\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\t\tBucket,\n\t\t\t\t\t\tKey,\n\t\t\t\t\t\tBody: JSON.stringify(secrets),\n\t\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t\tServerSideEncryption: 'AES256',\n\t\t\t\t\t\t...(expected === null\n\t\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t\t: { IfMatch: expected }),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.remember(stage, ETag);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthis.etags.delete(stage);\n\t\t\t\tthrow new StageSecretsChanged(stage, `s3://${Bucket}/${Key}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * The ETag the stage is now at. Some S3-compatible servers answer without\n\t * one: then nothing is remembered, and the next write asks for it first.\n\t */\n\tprivate remember(stage: string, etag: string | undefined): void {\n\t\tif (etag) this.etags.set(stage, etag);\n\t\telse this.etags.delete(stage);\n\t}\n\n\tprivate async currentEtag(\n\t\ts3: S3Client,\n\t\tstage: string,\n\t\tBucket: string,\n\t\tKey: string,\n\t): Promise<string | null> {\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(new HeadObjectCommand({ Bucket, Key })),\n\t\t\t);\n\t\t\treturn ETag ?? null;\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,IAAa,sBAAb,cAAyCA,eAAAA,SAAS;CAEvC;CAEA;CAHV,YACC,OAEA,UACC;EACD,MACC,0BAA0B,MAAM,OAAO,SAAS,4KAGjD;EARS,KAAA,QAAA;EAEA,KAAA,WAAA;EAOT,KAAK,OAAO;CACb;AACD;;AAQA,SAAS,SAAS,OAAyB;CAC1C,MAAM,EAAE,MAAM,cAAc;CAC5B,OACC,SAAS,wBACT,SAAS,gCACT,WAAW,mBAAmB,OAC9B,WAAW,mBAAmB;AAEhC;AAEA,SAAS,SAAS,OAAyB;CAC1C,MAAM,EAAE,MAAM,cAAc;CAC5B,OACC,SAAS,eACT,SAAS,cACT,WAAW,mBAAmB;AAEhC;AAEA,IAAa,iBAAb,MAAoD;CAWtB;CAV7B,OAAgB;CAEhB;;CAEA;;CAEA,SAAiB;;CAEjB,wBAAyB,IAAI,IAA2B;CAExD,YAAY,SAAiD;EAAhC,KAAA,UAAA;EAC5B,IAAI,QAAQ,QAAQ;GACnB,KAAK,aAAa,QAAQ;GAC1B,KAAK,SAAS;EACf;CACD;;CAGA,IAAI,SAAiB;EACpB,OACC,KAAK,cACLC,sBAAAA,kBAAkB,KAAK,QAAQ,SAAS,OAAO,cAAc;CAE/D;;CAGA,SAAS,OAAuB;EAC/B,OAAO,QAAQ,KAAK,OAAO,GAAG,KAAK,IAAI,KAAK;CAC7C;CAEA,IAAY,OAAuB;EAClC,OAAOC,kBAAAA,aAAa,KAAK,QAAQ,QAAQ,KAAK,QAAQ,SAAS,KAAK;CACrE;CAEA,MAAc,SAAoD;EACjE,IAAI,KAAK,SAAS,OAAO,KAAK;EAC9B,MAAM,OAAO,MAAMC,iBAAAA,gBAAgB,KAAK,OAAO;EAC/C,MAAM,WACL,KAAK,YACL,QAAQ,IAAI,uBACZ,QAAQ,IAAI;EACb,MAAM,cAAc,KAAK,QAAQ,eAAe,KAAK;EACrD,KAAK,UAAU;GACd,IAAI,IAAIC,mBAAAA,SAAS;IAChB,QAAQ,KAAK;IACb,GAAI,WAAW;KAAE;KAAU,gBAAgB;IAAK,IAAI,CAAC;IACrD,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;GACtC,CAAC;GACD,KAAK,IAAIC,oBAAAA,UAAU;IAClB,QAAQ,KAAK;IACb,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;IACnD,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;GACtC,CAAC;EACF;EACA,OAAO,KAAK;CACb;;CAGA,KACC,OACA,QACA,KACa;EACb,OAAOC,iBAAAA,qBACN;GAAE;GAAO,OAAO,KAAK;GAAM,SAAS,KAAK,QAAQ;GAAS;EAAO,GACjE,GACD;CACD;;;;;CAMA,MAAc,IACb,OACA,QACmD;EACnD,MAAM,EAAE,IAAI,QAAQ,MAAM,KAAK,OAAO;EACtC,IAAI,KAAK,QAAQ,OAAO;GAAE;GAAI,QAAQ,KAAK;EAAY;EACvD,MAAM,SAAS,SAAS,UAAU;EAClC,KAAK,eAAeL,sBAAAA,kBACnB,KAAK,QAAQ,SAAS,OACtB,MAAM,KAAK,KAAK,OAAO,cAAcM,sBAAAA,gBAAgB,GAAG,CAAC,CAC1D;EACA,MAAM,SAAS,KAAK;EACpB,IAAI,QACH,MAAM,KAAK,KAAK,OAAO,cACtBC,sBAAAA,oBAAoB,IAAI;GACvB;GACA,QAAQ,KAAK,QAAQ;GACrB,UAAU,KAAK,QAAQ;EACxB,CAAC,CACF;OACM,IACL,MAAM,KAAK,KAAK,OAAO,cAAcC,sBAAAA,kBAAkB,IAAI,MAAM,CAAC,MACnE,QAEA,OAAO;GAAE;GAAI,QAAQ;EAAK;EAE3B,KAAK,SAAS;EACd,OAAO;GAAE;GAAI;EAAO;CACrB;;;;;CAMA,MAAM,KAAK,OAA6C;EACvD,MAAM,EAAE,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,KAAK;EAClD,IAAI,CAAC,QAAQ;GACZ,KAAK,MAAM,IAAI,OAAO,IAAI;GAC1B,OAAO;EACR;EACA,IAAI;GACH,MAAM,WAAW,MAAM,KAAK,KAAK,OAAO,cACvC,GAAG,KAAK,IAAIC,mBAAAA,iBAAiB;IAAE,QAAQ;IAAQ,KAAK,KAAK,IAAI,KAAK;GAAE,CAAC,CAAC,CACvE;GACA,MAAM,OAAO,MACZ,SAAS,KAGR,kBAAkB,OAAO;GAC3B,KAAK,SAAS,OAAO,SAAS,IAAI;GAClC,OAAO,KAAK,MAAM,IAAI;EACvB,SAAS,OAAO;GACf,IAAI,CAAC,SAAS,KAAK,GAAG,MAAM;GAC5B,KAAK,MAAM,IAAI,OAAO,IAAI;GAC1B,OAAO;EACR;CACD;;;;;;;;;CAUA,MAAM,MAAM,OAAe,SAAsC;EAChE,MAAM,EAAE,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,IAAI;EACjD,MAAM,SAAS;EACf,MAAM,MAAM,KAAK,IAAI,KAAK;EAC1B,MAAM,WAAW,KAAK,MAAM,IAAI,KAAK,IACjC,KAAK,MAAM,IAAI,KAAK,KAAK,OAC1B,MAAM,KAAK,YAAY,IAAI,OAAO,QAAQ,GAAG;EAEhD,IAAI;GACH,MAAM,EAAE,SAAS,MAAM,KAAK,KAAK,OAAO,eACvC,GAAG,KACF,IAAIC,mBAAAA,iBAAiB;IACpB;IACA;IACA,MAAM,KAAK,UAAU,OAAO;IAC5B,aAAa;IACb,sBAAsB;IACtB,GAAI,aAAa,OACd,EAAE,aAAa,IAAI,IACnB,EAAE,SAAS,SAAS;GACxB,CAAC,CACF,CACD;GACA,KAAK,SAAS,OAAO,IAAI;EAC1B,SAAS,OAAO;GACf,IAAI,SAAS,KAAK,GAAG;IACpB,KAAK,MAAM,OAAO,KAAK;IACvB,MAAM,IAAI,oBAAoB,OAAO,QAAQ,OAAO,GAAG,KAAK;GAC7D;GACA,MAAM;EACP;CACD;;;;;CAMA,SAAiB,OAAe,MAAgC;EAC/D,IAAI,MAAM,KAAK,MAAM,IAAI,OAAO,IAAI;OAC/B,KAAK,MAAM,OAAO,KAAK;CAC7B;CAEA,MAAc,YACb,IACA,OACA,QACA,KACyB;EACzB,IAAI;GACH,MAAM,EAAE,SAAS,MAAM,KAAK,KAAK,OAAO,eACvC,GAAG,KAAK,IAAIC,mBAAAA,kBAAkB;IAAE;IAAQ;GAAI,CAAC,CAAC,CAC/C;GACA,OAAO,QAAQ;EAChB,SAAS,OAAO;GACf,IAAI,SAAS,KAAK,GAAG,OAAO;GAC5B,MAAM;EACP;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"s3-DLp0V-Ox.cjs","names":["GkmError","projectBucketName","s3SecretsKey","awsClientConfig","S3Client","STSClient","withStageCredentials","callerAccountId","ensureProjectBucket","findProjectBucket","GetObjectCommand","PutObjectCommand","HeadObjectCommand"],"sources":["../src/secrets/s3.ts"],"sourcesContent":["/**\n * A stage's secrets in S3: one object per stage,\n * `<prefix>/<project>/<stage>/secrets.json`, in the project bucket beside the\n * deploy state (`providers/projectBucket.ts`) — or in a bucket the config\n * names, which must exist.\n *\n * Encrypted at rest with SSE-S3, asked for on every write although the\n * project bucket defaults to it, so no KMS key is another place to keep. The\n * bucket's versioning keeps every past document. Unlike an SSM parameter\n * there is no size to outgrow.\n *\n * The project bucket is created by the first write, never by a read: a stage\n * whose bucket or object does not exist yet has no secrets. Every write is\n * conditional — `If-Match` on the ETag this store last read for the stage,\n * `If-None-Match: *` when it read none — so two `secrets:set` runs at once\n * cannot each overwrite the other's key: the second is refused with\n * {@link StageSecretsChanged}.\n */\n\nimport {\n\tGetObjectCommand,\n\tHeadObjectCommand,\n\tPutObjectCommand,\n\tS3Client,\n\ttype S3ClientConfig,\n} from '@aws-sdk/client-s3';\nimport { STSClient } from '@aws-sdk/client-sts';\nimport type { DeployIdentity } from '../deploy/identity.js';\nimport { GkmError } from '../errors';\nimport {\n\tcallerAccountId,\n\tensureProjectBucket,\n\tfindProjectBucket,\n\tprojectBucketName,\n} from '../providers/projectBucket.js';\nimport {\n\ttype AwsStoreOptions,\n\tawsClientConfig,\n\twithStageCredentials,\n} from './awsStore.js';\nimport { s3SecretsKey } from './providers.js';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface S3SecretsStoreOptions extends AwsStoreOptions {\n\t/** The deploy identity: the project bucket's name and its tag. */\n\tidentity: Pick<DeployIdentity, 'scope' | 'key'>;\n\t/** A bucket that exists; omitted, the project bucket. */\n\tbucket?: string;\n\t/** Key prefix inside the bucket, without a trailing slash. */\n\tprefix: string;\n\t/** For tests: credentials to use instead of the default chain. */\n\tcredentials?: S3ClientConfig['credentials'];\n}\n\n/**\n * Another run wrote the stage's secrets after this one read them. Nothing was\n * written: the change is to be made again, on top of theirs.\n */\nexport class StageSecretsChanged extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\t/** `s3://<bucket>/<key>` */\n\t\treadonly location: string,\n\t) {\n\t\tsuper(\n\t\t\t`The secrets for stage \"${stage}\" at ${location} changed after this command read them — ` +\n\t\t\t\t'another secrets command or deploy wrote them first. Nothing was written; run the command ' +\n\t\t\t\t'again to make the change on top of theirs.',\n\t\t);\n\t\tthis.name = 'StageSecretsChanged';\n\t}\n}\n\ninterface S3Failure {\n\tname?: string;\n\t$metadata?: { httpStatusCode?: number };\n}\n\n/** A conditional write that lost: the object is not what the write assumed. */\nfunction lostRace(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'PreconditionFailed' ||\n\t\tname === 'ConditionalRequestConflict' ||\n\t\t$metadata?.httpStatusCode === 412 ||\n\t\t$metadata?.httpStatusCode === 409\n\t);\n}\n\nfunction notFound(error: unknown): boolean {\n\tconst { name, $metadata } = error as S3Failure;\n\treturn (\n\t\tname === 'NoSuchKey' ||\n\t\tname === 'NotFound' ||\n\t\t$metadata?.httpStatusCode === 404\n\t);\n}\n\nexport class S3SecretsStore implements SecretsStore {\n\treadonly name = 's3';\n\n\tprivate clients?: { s3: S3Client; sts: STSClient };\n\t/** The bucket's name, once known. */\n\tprivate bucketName?: string;\n\t/** Whether the bucket is known to exist. */\n\tprivate exists = false;\n\t/** The ETag each stage was last read at — null: read, and not there. */\n\tprivate readonly etags = new Map<string, string | null>();\n\n\tconstructor(private readonly options: S3SecretsStoreOptions) {\n\t\tif (options.bucket) {\n\t\t\tthis.bucketName = options.bucket;\n\t\t\tthis.exists = true;\n\t\t}\n\t}\n\n\t/** The bucket's name; the project bucket's is known after the first call. */\n\tget bucket(): string {\n\t\treturn (\n\t\t\tthis.bucketName ??\n\t\t\tprojectBucketName(this.options.identity.scope, '<account id>')\n\t\t);\n\t}\n\n\t/** Where a stage's secrets are kept: `s3://<bucket>/<key>`. */\n\tlocation(stage: string): string {\n\t\treturn `s3://${this.bucket}/${this.key(stage)}`;\n\t}\n\n\tprivate key(stage: string): string {\n\t\treturn s3SecretsKey(this.options.prefix, this.options.project, stage);\n\t}\n\n\tprivate async client(): Promise<{ s3: S3Client; sts: STSClient }> {\n\t\tif (this.clients) return this.clients;\n\t\tconst base = await awsClientConfig(this.options);\n\t\tconst endpoint =\n\t\t\tbase.endpoint ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL_S3 ??\n\t\t\tprocess.env.AWS_ENDPOINT_URL;\n\t\tconst credentials = this.options.credentials ?? base.credentials;\n\t\tthis.clients = {\n\t\t\ts3: new S3Client({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(endpoint ? { endpoint, forcePathStyle: true } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t\tsts: new STSClient({\n\t\t\t\tregion: base.region,\n\t\t\t\t...(base.endpoint ? { endpoint: base.endpoint } : {}),\n\t\t\t\t...(credentials ? { credentials } : {}),\n\t\t\t}),\n\t\t};\n\t\treturn this.clients;\n\t}\n\n\t/** One AWS call, a missing or expired credential refused by name. */\n\tprivate call<T>(\n\t\tstage: string,\n\t\taccess: 'read' | 'write',\n\t\trun: () => Promise<T>,\n\t): Promise<T> {\n\t\treturn withStageCredentials(\n\t\t\t{ stage, store: this.name, profile: this.options.profile, access },\n\t\t\trun,\n\t\t);\n\t}\n\n\t/**\n\t * The bucket, or null for a project bucket that does not exist yet and is\n\t * not to be created (a read).\n\t */\n\tprivate async use(\n\t\tstage: string,\n\t\tcreate: boolean,\n\t): Promise<{ s3: S3Client; bucket: string | null }> {\n\t\tconst { s3, sts } = await this.client();\n\t\tif (this.exists) return { s3, bucket: this.bucketName! };\n\t\tconst access = create ? 'write' : 'read';\n\t\tthis.bucketName ??= projectBucketName(\n\t\t\tthis.options.identity.scope,\n\t\t\tawait this.call(stage, access, () => callerAccountId(sts)),\n\t\t);\n\t\tconst bucket = this.bucketName;\n\t\tif (create) {\n\t\t\tawait this.call(stage, access, () =>\n\t\t\t\tensureProjectBucket(s3, {\n\t\t\t\t\tbucket,\n\t\t\t\t\tregion: this.options.region,\n\t\t\t\t\tidentity: this.options.identity,\n\t\t\t\t}),\n\t\t\t);\n\t\t} else if (\n\t\t\t(await this.call(stage, access, () => findProjectBucket(s3, bucket))) ===\n\t\t\t'none'\n\t\t) {\n\t\t\treturn { s3, bucket: null };\n\t\t}\n\t\tthis.exists = true;\n\t\treturn { s3, bucket };\n\t}\n\n\t/**\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * read it with, or they have expired\n\t */\n\tasync read(stage: string): Promise<StageSecrets | null> {\n\t\tconst { s3, bucket } = await this.use(stage, false);\n\t\tif (!bucket) {\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t\ttry {\n\t\t\tconst response = await this.call(stage, 'read', () =>\n\t\t\t\ts3.send(new GetObjectCommand({ Bucket: bucket, Key: this.key(stage) })),\n\t\t\t);\n\t\t\tconst body = await (\n\t\t\t\tresponse.Body as {\n\t\t\t\t\ttransformToString(encoding: string): Promise<string>;\n\t\t\t\t}\n\t\t\t).transformToString('utf-8');\n\t\t\tthis.remember(stage, response.ETag);\n\t\t\treturn JSON.parse(body) as StageSecrets;\n\t\t} catch (error) {\n\t\t\tif (!notFound(error)) throw error;\n\t\t\tthis.etags.set(stage, null);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Replace the stage's secrets — on the condition that they are still what\n\t * this store last read. A stage never read here is replaced as it is now.\n\t *\n\t * @throws {StageSecretsChanged} when another run wrote them in between\n\t * @throws {StageSecretsUnreadable} when there are no AWS credentials to\n\t * write it with, or they have expired\n\t */\n\tasync write(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst { s3, bucket } = await this.use(stage, true);\n\t\tconst Bucket = bucket!;\n\t\tconst Key = this.key(stage);\n\t\tconst expected = this.etags.has(stage)\n\t\t\t? (this.etags.get(stage) ?? null)\n\t\t\t: await this.currentEtag(s3, stage, Bucket, Key);\n\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(\n\t\t\t\t\tnew PutObjectCommand({\n\t\t\t\t\t\tBucket,\n\t\t\t\t\t\tKey,\n\t\t\t\t\t\tBody: JSON.stringify(secrets),\n\t\t\t\t\t\tContentType: 'application/json',\n\t\t\t\t\t\tServerSideEncryption: 'AES256',\n\t\t\t\t\t\t...(expected === null\n\t\t\t\t\t\t\t? { IfNoneMatch: '*' }\n\t\t\t\t\t\t\t: { IfMatch: expected }),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.remember(stage, ETag);\n\t\t} catch (error) {\n\t\t\tif (lostRace(error)) {\n\t\t\t\tthis.etags.delete(stage);\n\t\t\t\tthrow new StageSecretsChanged(stage, `s3://${Bucket}/${Key}`);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * The ETag the stage is now at. Some S3-compatible servers answer without\n\t * one: then nothing is remembered, and the next write asks for it first.\n\t */\n\tprivate remember(stage: string, etag: string | undefined): void {\n\t\tif (etag) this.etags.set(stage, etag);\n\t\telse this.etags.delete(stage);\n\t}\n\n\tprivate async currentEtag(\n\t\ts3: S3Client,\n\t\tstage: string,\n\t\tBucket: string,\n\t\tKey: string,\n\t): Promise<string | null> {\n\t\ttry {\n\t\t\tconst { ETag } = await this.call(stage, 'write', () =>\n\t\t\t\ts3.send(new HeadObjectCommand({ Bucket, Key })),\n\t\t\t);\n\t\t\treturn ETag ?? null;\n\t\t} catch (error) {\n\t\t\tif (notFound(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jt as GkmError } from "./types-
|
|
1
|
+
import { jt as GkmError } from "./types-BhThob0H.mjs";
|
|
2
2
|
//#region src/sandbox/sandbox.d.ts
|
|
3
3
|
/** Where a command's output goes. */
|
|
4
4
|
type SandboxOutput = 'capture' | 'inherit' | 'stderr' | 'ignore';
|
|
@@ -101,4 +101,4 @@ declare class SecretNameInvalid extends GkmError {
|
|
|
101
101
|
}
|
|
102
102
|
//#endregion
|
|
103
103
|
export { SandboxOutput as a, confineCwd as c, SandboxExecOptions as i, Sandbox as n, SandboxResult as o, SandboxCwdEscape as r, SecretNameInvalid as s, SECRETS_DIR_ENV as t };
|
|
104
|
-
//# sourceMappingURL=sandbox-
|
|
104
|
+
//# sourceMappingURL=sandbox-CfpLWTPP.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-
|
|
1
|
+
{"version":3,"file":"sandbox-CfpLWTPP.d.mts","names":[],"sources":["../src/sandbox/sandbox.ts"],"mappings":";;;KA8BY;UAEK;;;;;;EAMhB;;;;;EAKA,KAAK,SAAS;;;;;;EAMd;;;;;;;EAOA,UAAU,SAAS;;;;;;EAMnB,SAAS;;EAET,SAAS;;;UAIO;EAChB;EACA;;EAEA;;EAEA;;;;;;;;;;;UAYgB;;WAEP;;;;;;;;;;;;;WAaA;;;;;WAKA,KAAK,SAAS;EACvB,KACC,iBACA,yBACA,SAAS,qBACP,QAAQ;;;cAIC;;cAGA,yBAAyB;WAE3B;WACA;EAFV,YACU,aACA;;;;;;;;;;iBAwBK,WAAW,cAAc;;cAU5B,0BAA0B;WACjB;EAArB,YAAqB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jt as GkmError } from "./types-
|
|
1
|
+
import { jt as GkmError } from "./types-DwoLHelT.cjs";
|
|
2
2
|
//#region src/sandbox/sandbox.d.ts
|
|
3
3
|
/** Where a command's output goes. */
|
|
4
4
|
type SandboxOutput = 'capture' | 'inherit' | 'stderr' | 'ignore';
|
|
@@ -101,4 +101,4 @@ declare class SecretNameInvalid extends GkmError {
|
|
|
101
101
|
}
|
|
102
102
|
//#endregion
|
|
103
103
|
export { SandboxOutput as a, confineCwd as c, SandboxExecOptions as i, Sandbox as n, SandboxResult as o, SandboxCwdEscape as r, SecretNameInvalid as s, SECRETS_DIR_ENV as t };
|
|
104
|
-
//# sourceMappingURL=sandbox-
|
|
104
|
+
//# sourceMappingURL=sandbox-D1XeNEJ9.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-
|
|
1
|
+
{"version":3,"file":"sandbox-D1XeNEJ9.d.cts","names":[],"sources":["../src/sandbox/sandbox.ts"],"mappings":";;;KA8BY;UAEK;;;;;;EAMhB;;;;;EAKA,KAAK,SAAS;;;;;;EAMd;;;;;;;EAOA,UAAU,SAAS;;;;;;EAMnB,SAAS;;EAET,SAAS;;;UAIO;EAChB;EACA;;EAEA;;EAEA;;;;;;;;;;;UAYgB;;WAEP;;;;;;;;;;;;;WAaA;;;;;WAKA,KAAK,SAAS;EACvB,KACC,iBACA,yBACA,SAAS,qBACP,QAAQ;;;cAIC;;cAGA,yBAAyB;WAE3B;WACA;EAFV,YACU,aACA;;;;;;;;;;iBAwBK,WAAW,cAAc;;cAU5B,0BAA0B;WACjB;EAArB,YAAqB"}
|
|
@@ -3,11 +3,12 @@ const require_errors = require("./errors-DMC4Umsb.cjs");
|
|
|
3
3
|
const require_run = require("./run-fVZ-rLnc.cjs");
|
|
4
4
|
const require_discover = require("./discover-DPgu-ScS.cjs");
|
|
5
5
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
6
|
-
const require_workspace = require("./workspace-
|
|
6
|
+
const require_workspace = require("./workspace-DRjdu1OY.cjs");
|
|
7
7
|
const require_config = require("./config.cjs");
|
|
8
|
+
const require_identity = require("./identity-CbLfgQNT.cjs");
|
|
8
9
|
const require_EndpointGenerator = require("./EndpointGenerator-De03t1hd.cjs");
|
|
9
10
|
const require_SubscriberGenerator = require("./SubscriberGenerator-Dvz7EzqK.cjs");
|
|
10
|
-
const require_resolve = require("./resolve-
|
|
11
|
+
const require_resolve = require("./resolve-DC98Rl1t.cjs");
|
|
11
12
|
let node_path = require("node:path");
|
|
12
13
|
let node_fs = require("node:fs");
|
|
13
14
|
let node_fs_promises = require("node:fs/promises");
|
|
@@ -296,7 +297,7 @@ function logsAccess(logs, ctx) {
|
|
|
296
297
|
return [
|
|
297
298
|
`📜 Logs (OpenObserve) on 127.0.0.1:${logs.port} — from your computer:`,
|
|
298
299
|
...ctx.local ? [` on this machine, open http://localhost:${logs.port} directly; from another:`] : [],
|
|
299
|
-
` ssh -N -L ${logs.port}:localhost:${logs.port} ${ctx.user}@${ctx.hostname} (user and host are guesses: this machine's)`,
|
|
300
|
+
` ssh -N -L ${logs.port}:localhost:${logs.port} ${ctx.user}@${ctx.hostname}${ctx.known ? "" : " (user and host are guesses: this machine's)"}`,
|
|
300
301
|
` then open http://localhost:${logs.port} (${login})`,
|
|
301
302
|
`⚠️ Docker-published ports bypass ufw, so OpenObserve is bound to 127.0.0.1 only — reach it through the tunnel, not by opening the port.`
|
|
302
303
|
];
|
|
@@ -423,129 +424,6 @@ function validateEnvironmentVariables(requiredVars, secrets) {
|
|
|
423
424
|
};
|
|
424
425
|
}
|
|
425
426
|
//#endregion
|
|
426
|
-
//#region src/deploy/identity.ts
|
|
427
|
-
/**
|
|
428
|
-
* Who a deploy is, on a target it shares with other workspaces.
|
|
429
|
-
*
|
|
430
|
-
* A Dokploy server is one flat namespace of projects, images and Docker
|
|
431
|
-
* service names. Deploys used to identify everything they made by the
|
|
432
|
-
* workspace name alone, so two workspaces called `shop` — or `Shop` and
|
|
433
|
-
* `shop` — deploying to one server landed in the same project, pushed over
|
|
434
|
-
* each other's images and redeployed each other's applications. A
|
|
435
|
-
* `DeployIdentity` is what a deploy names and claims its resources by instead.
|
|
436
|
-
*/
|
|
437
|
-
var identity_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
438
|
-
DeployNamespaceInvalid: () => DeployNamespaceInvalid,
|
|
439
|
-
DeployProjectNameInvalid: () => DeployProjectNameInvalid,
|
|
440
|
-
applicationName: () => applicationName,
|
|
441
|
-
defaultNamespace: () => defaultNamespace,
|
|
442
|
-
deployIdentity: () => deployIdentity,
|
|
443
|
-
imageName: () => imageName,
|
|
444
|
-
imageRef: () => imageRef,
|
|
445
|
-
isOwnedBy: () => isOwnedBy,
|
|
446
|
-
markerOf: () => markerOf,
|
|
447
|
-
ownershipMarker: () => ownershipMarker,
|
|
448
|
-
projectName: () => projectName
|
|
449
|
-
});
|
|
450
|
-
/** A namespace that cannot be part of a Dokploy name or an image path. */
|
|
451
|
-
var DeployNamespaceInvalid = class extends require_errors.GkmError {
|
|
452
|
-
namespace;
|
|
453
|
-
constructor(namespace) {
|
|
454
|
-
super(`deploy.namespace '${namespace}' is not usable as a name: use lowercase letters, digits and single '-' between them (e.g. 'acme' or 'acme-platform') in gkm.config.ts.`);
|
|
455
|
-
this.namespace = namespace;
|
|
456
|
-
this.name = "DeployNamespaceInvalid";
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
/** A workspace name with nothing a target can name a resource by. */
|
|
460
|
-
var DeployProjectNameInvalid = class extends require_errors.GkmError {
|
|
461
|
-
workspaceName;
|
|
462
|
-
constructor(workspaceName) {
|
|
463
|
-
super(`The workspace name '${workspaceName}' has no letters or digits to name deployed resources by. Set "name" in gkm.config.ts.`);
|
|
464
|
-
this.workspaceName = workspaceName;
|
|
465
|
-
this.name = "DeployProjectNameInvalid";
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
469
|
-
/**
|
|
470
|
-
* Lowercase `[a-z0-9-]`, runs of anything else collapsed to one `-`: valid as
|
|
471
|
-
* a Docker image path component, a Docker service name and a Dokploy name.
|
|
472
|
-
*/
|
|
473
|
-
function slug(value) {
|
|
474
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
475
|
-
}
|
|
476
|
-
/** The namespace a workspace is in when it does not name one. */
|
|
477
|
-
function defaultNamespace(workspaceName) {
|
|
478
|
-
return slug((0, _geekmidas_manifest.kebabCase)(workspaceName));
|
|
479
|
-
}
|
|
480
|
-
function deployIdentity(workspace, stage) {
|
|
481
|
-
const project = slug(workspace.name);
|
|
482
|
-
if (!project) throw new DeployProjectNameInvalid(workspace.name);
|
|
483
|
-
const fallback = defaultNamespace(workspace.name);
|
|
484
|
-
const namespace = workspace.deploy?.namespace ?? fallback;
|
|
485
|
-
if (!NAME.test(namespace)) throw new DeployNamespaceInvalid(namespace);
|
|
486
|
-
return {
|
|
487
|
-
namespace,
|
|
488
|
-
project,
|
|
489
|
-
stage,
|
|
490
|
-
key: `${namespace}/${project}`,
|
|
491
|
-
scope: namespace === fallback ? project : `${namespace}-${project}`
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* The token a Dokploy project's description carries to say which identity
|
|
496
|
-
* created it. A project with the right name and no marker belongs to someone
|
|
497
|
-
* else, and a deploy never adopts it.
|
|
498
|
-
*/
|
|
499
|
-
function ownershipMarker(identity) {
|
|
500
|
-
return `gkm:${identity.key}`;
|
|
501
|
-
}
|
|
502
|
-
/** Whether a project description carries `identity`'s marker. */
|
|
503
|
-
function isOwnedBy(description, identity) {
|
|
504
|
-
const marker = ownershipMarker(identity);
|
|
505
|
-
return (description ?? "").split(/\s+/).includes(marker);
|
|
506
|
-
}
|
|
507
|
-
/** Any `gkm:` marker a description carries — whoever's it is. */
|
|
508
|
-
function markerOf(description) {
|
|
509
|
-
return (description ?? "").split(/\s+/).find((t) => t.startsWith("gkm:"));
|
|
510
|
-
}
|
|
511
|
-
/** What the Dokploy project is called. */
|
|
512
|
-
function projectName(identity) {
|
|
513
|
-
return identity.scope;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* The repository an app's image is pushed to, below the registry:
|
|
517
|
-
* `<namespace>/<project>-<app>`.
|
|
518
|
-
*
|
|
519
|
-
* The namespace is a path segment of its own so that a registry that maps
|
|
520
|
-
* paths to owners (an organisation's packages, a Harbor project) can grant it
|
|
521
|
-
* as one; the app is joined to the project so the two read as one name.
|
|
522
|
-
*/
|
|
523
|
-
function imageName(identity, app) {
|
|
524
|
-
return `${identity.namespace}/${identity.project}-${slug((0, _geekmidas_manifest.kebabCase)(app))}`;
|
|
525
|
-
}
|
|
526
|
-
/** The full image ref an app is built, pushed and deployed as. */
|
|
527
|
-
function imageRef(identity, app, registry, tag) {
|
|
528
|
-
const name = imageName(identity, app);
|
|
529
|
-
return registry ? `${registry.replace(/\/+$/, "")}/${name}:${tag}` : `${name}:${tag}`;
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* What one application is called on the target.
|
|
533
|
-
*
|
|
534
|
-
* The same rule the constructs use, through the same `scopedName`, scoped by
|
|
535
|
-
* the stage and the identity: the application beside
|
|
536
|
-
* `production-kitchen-sink-database` is `production-kitchen-sink-api`. A
|
|
537
|
-
* Dokploy application's name is also its Docker service name, which is unique
|
|
538
|
-
* on the whole server, so the namespace has to be in it too — `acme`'s and
|
|
539
|
-
* `globex`'s `shop` would otherwise both run `production-shop-api`.
|
|
540
|
-
*
|
|
541
|
-
* The app id is dropped when it repeats the project, so a project named for its
|
|
542
|
-
* one application is `production-kitchen-sink` rather than
|
|
543
|
-
* `production-kitchen-sink-kitchen-sink`.
|
|
544
|
-
*/
|
|
545
|
-
function applicationName(identity, app) {
|
|
546
|
-
return slug(app) === identity.project ? `${identity.stage}-${identity.scope}` : (0, _geekmidas_manifest.scopedName)([identity.stage, identity.scope], app);
|
|
547
|
-
}
|
|
548
|
-
//#endregion
|
|
549
427
|
//#region src/secrets/keystore.ts
|
|
550
428
|
/**
|
|
551
429
|
* The keys that open each stage's encrypted secrets file, kept outside the
|
|
@@ -590,7 +468,7 @@ const PROJECT_KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
590
468
|
* a project's keys and its Dokploy project are named by the same thing.
|
|
591
469
|
*/
|
|
592
470
|
function projectKey(workspace) {
|
|
593
|
-
return deployIdentity(workspace, "").key;
|
|
471
|
+
return require_identity.deployIdentity(workspace, "").key;
|
|
594
472
|
}
|
|
595
473
|
/** The keystore project of a loaded workspace. */
|
|
596
474
|
function keystoreProject(workspace, home) {
|
|
@@ -844,8 +722,8 @@ async function storeFromConfig(workspace, config, options = {}) {
|
|
|
844
722
|
const profile = options.profile ? { profile: options.profile } : {};
|
|
845
723
|
switch (config.provider) {
|
|
846
724
|
case "s3": {
|
|
847
|
-
const { S3SecretsStore } = await Promise.resolve().then(() => require("./s3-
|
|
848
|
-
const { deployIdentity } = await Promise.resolve().then(() => identity_exports);
|
|
725
|
+
const { S3SecretsStore } = await Promise.resolve().then(() => require("./s3-DLp0V-Ox.cjs"));
|
|
726
|
+
const { deployIdentity } = await Promise.resolve().then(() => require("./identity-CbLfgQNT.cjs")).then((n) => n.identity_exports);
|
|
849
727
|
const location = require_workspace.s3SecretsLocation(config, workspace.state);
|
|
850
728
|
return new S3SecretsStore({
|
|
851
729
|
project: workspace.name,
|
|
@@ -6708,7 +6586,7 @@ async function createDnsProvider(options) {
|
|
|
6708
6586
|
});
|
|
6709
6587
|
}
|
|
6710
6588
|
if (provider === "godaddy") {
|
|
6711
|
-
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-
|
|
6589
|
+
const { GoDaddyProvider } = await Promise.resolve().then(() => require("./GoDaddyProvider-D1aQy2i1.cjs"));
|
|
6712
6590
|
return new GoDaddyProvider(options.godaddy ?? {});
|
|
6713
6591
|
}
|
|
6714
6592
|
if (provider === "cloudflare") throw new DnsProviderNotImplemented("cloudflare");
|
|
@@ -8476,12 +8354,6 @@ Object.defineProperty(exports, "appTelemetry", {
|
|
|
8476
8354
|
return appTelemetry;
|
|
8477
8355
|
}
|
|
8478
8356
|
});
|
|
8479
|
-
Object.defineProperty(exports, "applicationName", {
|
|
8480
|
-
enumerable: true,
|
|
8481
|
-
get: function() {
|
|
8482
|
-
return applicationName;
|
|
8483
|
-
}
|
|
8484
|
-
});
|
|
8485
8357
|
Object.defineProperty(exports, "applyBuckets", {
|
|
8486
8358
|
enumerable: true,
|
|
8487
8359
|
get: function() {
|
|
@@ -8596,12 +8468,6 @@ Object.defineProperty(exports, "currentActor", {
|
|
|
8596
8468
|
return currentActor;
|
|
8597
8469
|
}
|
|
8598
8470
|
});
|
|
8599
|
-
Object.defineProperty(exports, "deployIdentity", {
|
|
8600
|
-
enumerable: true,
|
|
8601
|
-
get: function() {
|
|
8602
|
-
return deployIdentity;
|
|
8603
|
-
}
|
|
8604
|
-
});
|
|
8605
8471
|
Object.defineProperty(exports, "derivedContainers", {
|
|
8606
8472
|
enumerable: true,
|
|
8607
8473
|
get: function() {
|
|
@@ -8758,18 +8624,6 @@ Object.defineProperty(exports, "imageLayout", {
|
|
|
8758
8624
|
return imageLayout;
|
|
8759
8625
|
}
|
|
8760
8626
|
});
|
|
8761
|
-
Object.defineProperty(exports, "imageName", {
|
|
8762
|
-
enumerable: true,
|
|
8763
|
-
get: function() {
|
|
8764
|
-
return imageName;
|
|
8765
|
-
}
|
|
8766
|
-
});
|
|
8767
|
-
Object.defineProperty(exports, "imageRef", {
|
|
8768
|
-
enumerable: true,
|
|
8769
|
-
get: function() {
|
|
8770
|
-
return imageRef;
|
|
8771
|
-
}
|
|
8772
|
-
});
|
|
8773
8627
|
Object.defineProperty(exports, "initStageSecrets", {
|
|
8774
8628
|
enumerable: true,
|
|
8775
8629
|
get: function() {
|
|
@@ -8794,12 +8648,6 @@ Object.defineProperty(exports, "isMainFrontendApp", {
|
|
|
8794
8648
|
return isMainFrontendApp;
|
|
8795
8649
|
}
|
|
8796
8650
|
});
|
|
8797
|
-
Object.defineProperty(exports, "isOwnedBy", {
|
|
8798
|
-
enumerable: true,
|
|
8799
|
-
get: function() {
|
|
8800
|
-
return isOwnedBy;
|
|
8801
|
-
}
|
|
8802
|
-
});
|
|
8803
8651
|
Object.defineProperty(exports, "isPortAvailable", {
|
|
8804
8652
|
enumerable: true,
|
|
8805
8653
|
get: function() {
|
|
@@ -8866,12 +8714,6 @@ Object.defineProperty(exports, "logsService", {
|
|
|
8866
8714
|
return logsService;
|
|
8867
8715
|
}
|
|
8868
8716
|
});
|
|
8869
|
-
Object.defineProperty(exports, "markerOf", {
|
|
8870
|
-
enumerable: true,
|
|
8871
|
-
get: function() {
|
|
8872
|
-
return markerOf;
|
|
8873
|
-
}
|
|
8874
|
-
});
|
|
8875
8717
|
Object.defineProperty(exports, "maskPassword", {
|
|
8876
8718
|
enumerable: true,
|
|
8877
8719
|
get: function() {
|
|
@@ -8896,12 +8738,6 @@ Object.defineProperty(exports, "orchestrateDns", {
|
|
|
8896
8738
|
return orchestrateDns;
|
|
8897
8739
|
}
|
|
8898
8740
|
});
|
|
8899
|
-
Object.defineProperty(exports, "ownershipMarker", {
|
|
8900
|
-
enumerable: true,
|
|
8901
|
-
get: function() {
|
|
8902
|
-
return ownershipMarker;
|
|
8903
|
-
}
|
|
8904
|
-
});
|
|
8905
8741
|
Object.defineProperty(exports, "pgClient", {
|
|
8906
8742
|
enumerable: true,
|
|
8907
8743
|
get: function() {
|
|
@@ -8962,12 +8798,6 @@ Object.defineProperty(exports, "projectKey", {
|
|
|
8962
8798
|
return projectKey;
|
|
8963
8799
|
}
|
|
8964
8800
|
});
|
|
8965
|
-
Object.defineProperty(exports, "projectName", {
|
|
8966
|
-
enumerable: true,
|
|
8967
|
-
get: function() {
|
|
8968
|
-
return projectName;
|
|
8969
|
-
}
|
|
8970
|
-
});
|
|
8971
8801
|
Object.defineProperty(exports, "providerOf", {
|
|
8972
8802
|
enumerable: true,
|
|
8973
8803
|
get: function() {
|
|
@@ -9263,4 +9093,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
9263
9093
|
}
|
|
9264
9094
|
});
|
|
9265
9095
|
|
|
9266
|
-
//# sourceMappingURL=seeds-
|
|
9096
|
+
//# sourceMappingURL=seeds-BNHSpjNP.cjs.map
|