@go-to-k/cdkd 0.235.0 → 0.236.0

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/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { A as withErrorHandling, B as generateResourceName, C as StackHasActiveImportsError, F as getLiveRenderer, H as withSkipPrefix, I as PATTERN_B_NAME_OPTIONS, L as PATTERN_B_NAME_PROPERTIES, M as getLogger, P as runStackBuffered, R as PATTERN_B_RESOURCE_TYPES, S as ResourceUpdateNotSupportedError, U as withStackName, V as generateResourceNameWithFallback, _ as MissingCdkCliError, a as normalizeAwsTagsToCfn, b as ProvisioningError, f as LocalInvokeBuildError$1, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, p as LocalMigrateError, r as CDK_PATH_TAG, s as assertRegionMatch, t as disableInstanceApiTermination, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError } from "./ec2-termination-protection-uvVnteGl.js";
3
- import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
4
- import { $ as AssetManifestLoader, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as buildDockerImage, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as runDockerStreaming, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as getDockerCmd, Y as formatDockerLoginError, Z as runDockerForeground, _ as green, _t as uploadCfnTemplate, a as withRetry, at as resolveApp, b as IAMRoleProvider, bt as resolveBucketRegion, c as computeImplicitDeleteEdges, ct as resolveSkipPrefix, d as isStatefulRecreateTargetSync, dt as resolveUseCdkBootstrapAssets, et as getDockerImageBySourceHash, f as renderStatefulReason, ft as warnDeprecatedNoPrefixCliFlag, g as gray, gt as findLargeInlineResources, h as cyan, ht as MIGRATE_TMP_PREFIX, i as withResourceDeadline, it as getLegacyStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveStateBucketWithDefault, m as bold, mt as CFN_TEMPLATE_URL_LIMIT, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as synthesisStatusMessage, o as isRetryableTransientError, ot as resolveAutoAssetStorage, p as formatResourceLine, pt as CFN_TEMPLATE_BODY_LIMIT, q as parseBootstrapMarker, r as DeployEngine, rt as getDefaultStateBucketName, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveCaptureObservedState, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as Synthesizer, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveStateBucketWithDefaultAndSource, v as red, vt as AssemblyReader, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, z as WorkGraph } from "./deploy-engine-Cbi5IzWV.js";
5
- import { t as ASGProvider } from "./asg-provider-Dd9Xze-T.js";
2
+ import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BzO-joNR.js";
3
+ import { A as withErrorHandling, C as StackHasActiveImportsError, S as ResourceUpdateNotSupportedError, _ as MissingCdkCliError, a as normalizeAwsTagsToCfn, b as ProvisioningError, f as LocalInvokeBuildError$1, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, p as LocalMigrateError, r as CDK_PATH_TAG, s as assertRegionMatch, t as disableInstanceApiTermination, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError } from "./ec2-termination-protection-q-WGWgxa.js";
4
+ import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
5
+ import { $ as runDockerStreaming, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as parseBootstrapMarker, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as runDockerForeground, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as formatDockerLoginError, Y as buildDockerImage, Z as getDockerCmd, _ as green, _t as findLargeInlineResources, a as withRetry, at as getLegacyStateBucketName, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as resolveCaptureObservedState, d as isStatefulRecreateTargetSync, dt as resolveStateBucketWithDefaultAndSource, et as AssetManifestLoader, f as renderStatefulReason, ft as resolveUseCdkBootstrapAssets, g as gray, gt as MIGRATE_TMP_PREFIX, h as cyan, ht as CFN_TEMPLATE_URL_LIMIT, i as withResourceDeadline, it as getDefaultStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveSkipPrefix, m as bold, mt as CFN_TEMPLATE_BODY_LIMIT, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as Synthesizer, o as isRetryableTransientError, ot as resolveApp, p as formatResourceLine, pt as warnDeprecatedNoPrefixCliFlag, q as getBootstrapMarkerKey, r as DeployEngine, rt as synthesisStatusMessage, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveAutoAssetStorage, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as getDockerImageBySourceHash, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveStateBucketWithDefault, v as red, vt as uploadCfnTemplate, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, xt as resolveBucketRegion, y as yellow, yt as AssemblyReader, z as WorkGraph } from "./deploy-engine-DqAIXGA_.js";
6
+ import { t as expectedOwnerParam } from "./expected-bucket-owner-BjEfUnkA.js";
7
+ import { t as ASGProvider } from "./asg-provider-Dbl1_mwT.js";
6
8
  import { AsyncLocalStorage } from "node:async_hooks";
7
9
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
10
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -38,9 +40,9 @@ import graphlib from "graphlib";
38
40
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
39
41
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$9, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-elasticache";
40
42
  import { Command, Option } from "commander";
41
- import { writeFileSync as writeFileSync$1 } from "fs";
42
43
  import * as readline$1 from "node:readline/promises";
43
44
  import readline from "node:readline/promises";
45
+ import { writeFileSync as writeFileSync$1 } from "fs";
44
46
  import * as zlib from "node:zlib";
45
47
  import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
46
48
  import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetApisCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
@@ -608,6 +610,310 @@ const destroyOptions = [
608
610
  allowUnsupportedTypesOption
609
611
  ];
610
612
 
613
+ //#endregion
614
+ //#region src/cli/commands/bootstrap-destroy.ts
615
+ /**
616
+ * `cdkd bootstrap --destroy` — teardown of cdkd-created account resources
617
+ * (issue #1010, the reverse of `cdkd bootstrap` / `ensureAssetStorage`).
618
+ *
619
+ * Scope per invocation is ONE region's asset storage (`--region`, same
620
+ * option the create side uses): empty + delete the asset bucket, force-
621
+ * delete the container-asset ECR repo, then delete the region's bootstrap
622
+ * marker LAST — the mirror image of the create side's marker-written-last
623
+ * ordering, so a crash mid-teardown leaves the region still consistently
624
+ * opted in (marker present → deploys keep hard-erroring at the missing
625
+ * resources with a re-bootstrap hint, never a silent legacy fallback).
626
+ *
627
+ * The asset bucket / repo NAMES are read from the region's bootstrap
628
+ * marker, NOT recomputed from the naming convention — the marker is the
629
+ * source of truth for names (design §4.1), which keeps this teardown
630
+ * compatible with custom asset-storage names (issue #1011).
631
+ *
632
+ * The state bucket is NOT deleted by default: it is the account's source
633
+ * of truth. `--include-state-bucket` opts in, and even then the deletion
634
+ * is refused while ANY stack state exists in the bucket or any OTHER
635
+ * region is still opted in to asset storage (deleting their markers would
636
+ * silently flip those regions back to legacy mode).
637
+ */
638
+ /** S3 key prefix for state files — same fixed value the create side uses. */
639
+ const STATE_PREFIX = "cdkd";
640
+ /**
641
+ * Every cdkd state file ends with this suffix, regardless of the
642
+ * `--state-prefix` it was deployed under (`{prefix}/{stack}/{region}/state.json`,
643
+ * or legacy `{prefix}/{stack}/state.json`).
644
+ */
645
+ const STATE_FILE_SUFFIX = "/state.json";
646
+ /** `us-east-1` / `ap-northeast-1` / `us-gov-west-1` — a region-shaped segment. */
647
+ const REGION_SEGMENT = /^[a-z]{2}(-[a-z]+)+-\d+$/;
648
+ /**
649
+ * List every state file in the bucket — the WHOLE bucket, not just the
650
+ * default `cdkd/` prefix. Other commands accept `--state-prefix`, so live
651
+ * stack state may exist under ANY prefix in this bucket; scoping this
652
+ * listing to the default prefix would let the reference scan and the
653
+ * `--include-state-bucket` guard silently miss those stacks and delete
654
+ * live state.
655
+ */
656
+ async function listAllStateKeys(stateBackend) {
657
+ return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(STATE_FILE_SUFFIX));
658
+ }
659
+ /**
660
+ * Interactive confirmation for the teardown. Follows the repo's
661
+ * destructive-prompt convention (see `recreate-confirm-prompt.ts`, the
662
+ * same pattern family as `confirm-prompt.ts`): print the deletion plan as
663
+ * a WARN block, `--yes` skips the prompt, a non-TTY stdin without `--yes`
664
+ * is a hard error (never hang / never silently decline in CI), and the
665
+ * prompt itself defaults to NO because the side effect is destructive.
666
+ */
667
+ async function promptBootstrapDestroyConfirm(input) {
668
+ const logger = getLogger();
669
+ logger.warn("");
670
+ logger.warn("cdkd bootstrap --destroy will delete the following:");
671
+ for (const line of input.planLines) logger.warn(` - ${line}`);
672
+ if (input.yes) return true;
673
+ if (process.stdin.isTTY !== true) throw new CdkdError("The bootstrap --destroy confirmation prompt cannot run in a non-interactive environment. Pass --yes / -y to confirm the teardown, or run the command from a real terminal.", "NON_INTERACTIVE_CONFIRM");
674
+ const rl = readline.createInterface({
675
+ input: process.stdin,
676
+ output: process.stdout
677
+ });
678
+ try {
679
+ const trimmed = (await rl.question("\nContinue? (y/N): ")).trim().toLowerCase();
680
+ return trimmed === "y" || trimmed === "yes";
681
+ } finally {
682
+ rl.close();
683
+ }
684
+ }
685
+ /**
686
+ * Scan every state file in the state bucket for references to the target
687
+ * region's asset bucket / container repo. A pragmatic string scan of the
688
+ * serialized state (design rationale in issue #1010): asset references
689
+ * appear in many property shapes (`Code.S3Bucket`, `ImageUri`, nested
690
+ * `TemplateURL`, `s3.Asset` env-var URLs, …) but all of them carry the
691
+ * bucket / repo NAME verbatim, so a substring match over the raw JSON body
692
+ * catches every shape — including ones future providers add — with zero
693
+ * per-shape maintenance. Covers both key layouts (region-prefixed and
694
+ * legacy), nested-stack children, and stacks deployed under a custom
695
+ * `--state-prefix` (the listing spans the whole bucket, not just the
696
+ * default `cdkd/` prefix).
697
+ *
698
+ * Returns a human-readable `stack (region)` descriptor per referencing
699
+ * state file.
700
+ */
701
+ async function scanStateReferences(stateBackend, names) {
702
+ const stateKeys = await listAllStateKeys(stateBackend);
703
+ const referencing = [];
704
+ for (const key of stateKeys) {
705
+ const body = await stateBackend.getRawObject(key);
706
+ if (body === null) continue;
707
+ if (names.some((name) => body.includes(name))) referencing.push(describeStateKey(key));
708
+ }
709
+ return referencing;
710
+ }
711
+ /**
712
+ * `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
713
+ * `{prefix}/{stack}/state.json` → `stack`. The prefix is arbitrary (custom
714
+ * `--state-prefix` values included), so the stack/region pair is derived
715
+ * from the key's TAIL: the last segment is treated as a region only when
716
+ * it is region-shaped.
717
+ */
718
+ function describeStateKey(key) {
719
+ const segments = key.slice(0, -11).split("/");
720
+ const last = segments[segments.length - 1] ?? key;
721
+ const secondLast = segments[segments.length - 2];
722
+ if (secondLast !== void 0 && REGION_SEGMENT.test(last)) return `${secondLast} (${last})`;
723
+ return last;
724
+ }
725
+ /**
726
+ * Empty (all versions + delete markers) and delete an S3 bucket, with
727
+ * `ExpectedBucketOwner` pinned on every call — deleting a foreign bucket
728
+ * that squatted the predictable name would be catastrophic, so the same
729
+ * ownership defense as the create side applies in reverse. Returns `false`
730
+ * (skip, info line) when the bucket does not exist — the idempotency
731
+ * mirror of `ensureAssetStorage`.
732
+ */
733
+ async function emptyAndDeleteBucket(s3Client, bucket, accountId, label, logger) {
734
+ try {
735
+ await s3Client.send(new HeadBucketCommand({
736
+ Bucket: bucket,
737
+ ExpectedBucketOwner: accountId
738
+ }));
739
+ } catch (error) {
740
+ const err = error;
741
+ if (err.name === "NotFound" || err.name === "NoSuchBucket") {
742
+ logger.info(`${label} ${bucket} does not exist — skipping`);
743
+ return false;
744
+ }
745
+ if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`${label} '${bucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to delete it.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
746
+ throw normalizeAwsError(error, {
747
+ bucket,
748
+ operation: "HeadBucket"
749
+ });
750
+ }
751
+ logger.info(`Emptying ${label.toLowerCase()}: ${bucket}`);
752
+ let keyMarker;
753
+ let versionIdMarker;
754
+ do {
755
+ const page = await s3Client.send(new ListObjectVersionsCommand({
756
+ Bucket: bucket,
757
+ ExpectedBucketOwner: accountId,
758
+ ...keyMarker && { KeyMarker: keyMarker },
759
+ ...versionIdMarker && { VersionIdMarker: versionIdMarker }
760
+ }));
761
+ const entries = [...page.Versions ?? [], ...page.DeleteMarkers ?? []].filter((v) => v.Key).map((v) => ({
762
+ Key: v.Key,
763
+ ...v.VersionId && { VersionId: v.VersionId }
764
+ }));
765
+ if (entries.length > 0) {
766
+ const failures = ((await s3Client.send(new DeleteObjectsCommand({
767
+ Bucket: bucket,
768
+ ExpectedBucketOwner: accountId,
769
+ Delete: {
770
+ Objects: entries,
771
+ Quiet: true
772
+ }
773
+ }))).Errors ?? []).map((e) => `${e.Key ?? "<unknown>"} (${e.Code ?? "Error"}: ${e.Message ?? ""})`);
774
+ if (failures.length > 0) throw new CdkdError(`Failed to delete ${failures.length} object(s) from ${label.toLowerCase()} '${bucket}': ${failures.join("; ")}`, "BUCKET_EMPTY_FAILED");
775
+ }
776
+ keyMarker = page.IsTruncated ? page.NextKeyMarker : void 0;
777
+ versionIdMarker = page.IsTruncated ? page.NextVersionIdMarker : void 0;
778
+ } while (keyMarker || versionIdMarker);
779
+ await s3Client.send(new DeleteBucketCommand({
780
+ Bucket: bucket,
781
+ ExpectedBucketOwner: accountId
782
+ }));
783
+ logger.info(`✓ Deleted ${label.toLowerCase()}: ${bucket}`);
784
+ return true;
785
+ }
786
+ /**
787
+ * Force-delete the container-asset ECR repository. Missing repo is an
788
+ * idempotent skip with an info line.
789
+ */
790
+ async function deleteContainerRepo(containerRepo, region, profile, logger) {
791
+ const ecrClient = new ECRClient({
792
+ region,
793
+ ...profile && { profile }
794
+ });
795
+ try {
796
+ await ecrClient.send(new DeleteRepositoryCommand({
797
+ repositoryName: containerRepo,
798
+ force: true
799
+ }));
800
+ logger.info(`✓ Deleted container-asset ECR repository: ${containerRepo}`);
801
+ } catch (error) {
802
+ if (error.name === "RepositoryNotFoundException") logger.info(`Container-asset repository ${containerRepo} does not exist — skipping`);
803
+ else throw normalizeAwsError(error, { operation: "DeleteRepository" });
804
+ } finally {
805
+ ecrClient.destroy();
806
+ }
807
+ }
808
+ /**
809
+ * List OTHER regions that still have a bootstrap marker in the state
810
+ * bucket (i.e. are still opted in to cdkd asset storage).
811
+ */
812
+ async function listOtherBootstrapRegions(stateBackend, region) {
813
+ return (await stateBackend.listRawKeys(BOOTSTRAP_MARKER_PREFIX)).filter((k) => k.endsWith(".json")).map((k) => k.slice(BOOTSTRAP_MARKER_PREFIX.length, -5)).filter((r) => r.length > 0 && r !== region);
814
+ }
815
+ /**
816
+ * `cdkd bootstrap --destroy` implementation. Dispatched from the bootstrap
817
+ * command action when `--destroy` is passed.
818
+ */
819
+ async function bootstrapDestroyCommand(options) {
820
+ const logger = getLogger();
821
+ if (options.verbose) logger.setLevel("debug");
822
+ logger.info("Starting cdkd bootstrap --destroy...");
823
+ logger.debug("Options:", options);
824
+ await applyRoleArnIfSet({
825
+ roleArn: options.roleArn,
826
+ region: options.region
827
+ });
828
+ const awsClients = new AwsClients({
829
+ ...options.region && { region: options.region },
830
+ ...options.profile && { profile: options.profile }
831
+ });
832
+ setAwsClients(awsClients);
833
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
834
+ const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
835
+ const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
836
+ const markerS3Client = new S3Client({
837
+ region,
838
+ ...options.profile && { profile: options.profile }
839
+ });
840
+ const stateBackend = new S3StateBackend(markerS3Client, {
841
+ bucket: bucketName,
842
+ prefix: STATE_PREFIX
843
+ }, {
844
+ region,
845
+ ...options.profile && { profile: options.profile }
846
+ });
847
+ try {
848
+ const markerKey = getBootstrapMarkerKey(region);
849
+ let markerBody;
850
+ try {
851
+ markerBody = await stateBackend.getRawObject(markerKey);
852
+ } catch (error) {
853
+ if (error.name === "NoSuchBucket") {
854
+ logger.info(`State bucket '${bucketName}' does not exist — this account/region was never bootstrapped; nothing to delete.`);
855
+ return;
856
+ }
857
+ throw error;
858
+ }
859
+ let marker;
860
+ if (markerBody === null) {
861
+ logger.info(`No bootstrap marker for region '${region}' (${markerKey}) — asset storage is not opted in for this region (or was already destroyed); nothing to delete.`);
862
+ if (!options.includeStateBucket) {
863
+ logger.info(`If the asset bucket / ECR repo still exist without a marker, re-run 'cdkd bootstrap --region ${region}' to recreate the marker, then destroy again.`);
864
+ return;
865
+ }
866
+ } else marker = parseBootstrapMarker(markerBody, markerKey);
867
+ if (marker && !options.force) {
868
+ logger.info("Scanning stack state for references to the asset storage...");
869
+ const referencing = await scanStateReferences(stateBackend, [marker.assetBucket, marker.containerRepo]);
870
+ if (referencing.length > 0) throw new CdkdError(`Refusing to destroy asset storage for region '${region}': ${referencing.length} deployed stack(s) still reference '${marker.assetBucket}' / '${marker.containerRepo}':\n` + referencing.map((s) => ` - ${s}`).join("\n") + "\nDestroy (or re-deploy off the storage) these stacks first, or pass --force to delete anyway.", "ASSET_STORAGE_IN_USE");
871
+ }
872
+ if (options.includeStateBucket) {
873
+ const stateKeys = await listAllStateKeys(stateBackend);
874
+ if (stateKeys.length > 0) {
875
+ const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
876
+ throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
877
+ }
878
+ const otherRegions = await listOtherBootstrapRegions(stateBackend, region);
879
+ if (otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
880
+ }
881
+ const planLines = [];
882
+ if (marker) {
883
+ planLines.push(`Asset bucket: s3://${marker.assetBucket} (region ${region}, all contents)`);
884
+ planLines.push(`Container-asset ECR repository: ${marker.containerRepo} (region ${region}, all images)`);
885
+ planLines.push(`Bootstrap marker: s3://${bucketName}/${markerKey} (deleted last)`);
886
+ }
887
+ if (options.includeStateBucket) planLines.push(`State bucket: s3://${bucketName} (ALL contents, including all versions)`);
888
+ if (!await promptBootstrapDestroyConfirm({
889
+ planLines,
890
+ yes: options.yes
891
+ })) {
892
+ logger.info("Bootstrap teardown cancelled — nothing deleted.");
893
+ return;
894
+ }
895
+ if (marker) {
896
+ await emptyAndDeleteBucket(awsClients.s3, marker.assetBucket, accountId, "Asset bucket", logger);
897
+ await deleteContainerRepo(marker.containerRepo, region, options.profile, logger);
898
+ await stateBackend.deleteRawObjects([markerKey]);
899
+ logger.info(`✓ Deleted bootstrap marker (${markerKey})`);
900
+ logger.info(`\ncdkd asset storage is now OFF for region ${region}: future deploys in this region fall back to legacy mode (CDK bootstrap destinations) unless the region is bootstrapped again.`);
901
+ }
902
+ if (options.includeStateBucket) {
903
+ const rebuiltStateBucketClient = await rebuildClientForBucketRegion(awsClients.s3, bucketName, { ...options.profile && { profile: options.profile } });
904
+ try {
905
+ await emptyAndDeleteBucket(rebuiltStateBucketClient ?? awsClients.s3, bucketName, accountId, "State bucket", logger);
906
+ } finally {
907
+ rebuiltStateBucketClient?.destroy();
908
+ }
909
+ }
910
+ logger.info("\n✓ Bootstrap teardown completed");
911
+ } finally {
912
+ markerS3Client.destroy();
913
+ awsClients.destroy();
914
+ }
915
+ }
916
+
611
917
  //#endregion
612
918
  //#region src/cli/commands/bootstrap.ts
613
919
  /**
@@ -651,7 +957,10 @@ async function bootstrapCommand(options) {
651
957
  try {
652
958
  let bucketExists = false;
653
959
  try {
654
- await stateBucketS3.send(new HeadBucketCommand({ Bucket: bucketName }));
960
+ await stateBucketS3.send(new HeadBucketCommand({
961
+ Bucket: bucketName,
962
+ ExpectedBucketOwner: accountId
963
+ }));
655
964
  bucketExists = true;
656
965
  logger.info(`Bucket ${bucketName} already exists`);
657
966
  } catch (error) {
@@ -682,12 +991,14 @@ async function bootstrapCommand(options) {
682
991
  logger.debug("Enabling bucket versioning...");
683
992
  await stateBucketS3.send(new PutBucketVersioningCommand({
684
993
  Bucket: bucketName,
994
+ ExpectedBucketOwner: accountId,
685
995
  VersioningConfiguration: { Status: "Enabled" }
686
996
  }));
687
997
  logger.info("✓ Enabled bucket versioning");
688
998
  logger.debug("Enabling bucket encryption...");
689
999
  await stateBucketS3.send(new PutBucketEncryptionCommand({
690
1000
  Bucket: bucketName,
1001
+ ExpectedBucketOwner: accountId,
691
1002
  ServerSideEncryptionConfiguration: { Rules: [{
692
1003
  ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
693
1004
  BucketKeyEnabled: true
@@ -708,6 +1019,7 @@ async function bootstrapCommand(options) {
708
1019
  };
709
1020
  await stateBucketS3.send(new PutBucketPolicyCommand({
710
1021
  Bucket: bucketName,
1022
+ ExpectedBucketOwner: accountId,
711
1023
  Policy: JSON.stringify(bucketPolicy)
712
1024
  }));
713
1025
  logger.info("✓ Set bucket policy (deny external access)");
@@ -764,7 +1076,24 @@ async function bootstrapCommand(options) {
764
1076
  * Create bootstrap command
765
1077
  */
766
1078
  function createBootstrapCommand() {
767
- const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "Force reconfiguration of existing bucket", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").addOption(new Option("--region <region>", "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)")).action(withErrorHandling(bootstrapCommand));
1079
+ const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region. With --destroy, tear down the region's asset storage instead (issue #1010).").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "Create: force reconfiguration of an existing bucket. Destroy: delete the asset storage even when deployed stacks still reference it", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").option("--destroy", "Tear down the region's cdkd asset storage (empty + delete the asset bucket, force-delete the container-asset ECR repo, then delete the bootstrap marker last). The state bucket is kept unless --include-state-bucket is also passed", false).option("--include-state-bucket", "With --destroy: also delete the S3 state bucket. Refused while any stack state exists or any other region is still opted in to asset storage", false).addOption(new Option("--region <region>", "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)")).action(withErrorHandling(async (options) => {
1080
+ if (options.destroy) {
1081
+ if (!options.assets) throw new CdkdError("--no-assets cannot be combined with --destroy. The teardown scope is the asset storage itself; use --include-state-bucket to also delete the state bucket.", "INVALID_OPTIONS");
1082
+ await bootstrapDestroyCommand({
1083
+ ...options.stateBucket && { stateBucket: options.stateBucket },
1084
+ ...options.region && { region: options.region },
1085
+ ...options.profile && { profile: options.profile },
1086
+ ...options.roleArn && { roleArn: options.roleArn },
1087
+ force: options.force,
1088
+ includeStateBucket: options.includeStateBucket,
1089
+ yes: options.yes,
1090
+ verbose: options.verbose
1091
+ });
1092
+ return;
1093
+ }
1094
+ if (options.includeStateBucket) throw new CdkdError("--include-state-bucket requires --destroy (it opts the state bucket into the teardown).", "INVALID_OPTIONS");
1095
+ await bootstrapCommand(options);
1096
+ }));
768
1097
  commonOptions.forEach((opt) => cmd.addOption(opt));
769
1098
  return cmd;
770
1099
  }
@@ -1526,7 +1855,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1526
1855
  const FLUSH_EVENT_THRESHOLD = 50;
1527
1856
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1528
1857
  function getCdkdVersion() {
1529
- return "0.235.0";
1858
+ return "0.236.0";
1530
1859
  }
1531
1860
  /**
1532
1861
  * Generate a time-sortable unique run id, e.g.
@@ -2157,6 +2486,14 @@ var ExportIndexStore = class {
2157
2486
  * `config.region()` shape (e.g. a hand-rolled test double) the resolution
2158
2487
  * is skipped and the original client is used unchanged.
2159
2488
  */
2489
+ /**
2490
+ * `ExpectedBucketOwner` spread for every state-bucket S3 call (squatting
2491
+ * hardening — see src/utils/expected-bucket-owner.ts). Best-effort:
2492
+ * resolves to an empty object for non-standard clients (test doubles).
2493
+ */
2494
+ async ownerParam() {
2495
+ return expectedOwnerParam(this.s3Client);
2496
+ }
2160
2497
  async ensureClientForBucket() {
2161
2498
  if (this.clientResolved) return;
2162
2499
  if (this.resolveInFlight) return this.resolveInFlight;
@@ -2302,6 +2639,7 @@ var ExportIndexStore = class {
2302
2639
  try {
2303
2640
  const response = await this.s3Client.send(new GetObjectCommand({
2304
2641
  Bucket: this.bucket,
2642
+ ...await this.ownerParam(),
2305
2643
  Key: this.indexKey()
2306
2644
  }));
2307
2645
  return {
@@ -2318,6 +2656,7 @@ var ExportIndexStore = class {
2318
2656
  const body = JSON.stringify(next, null, 2);
2319
2657
  return (await this.s3Client.send(new PutObjectCommand({
2320
2658
  Bucket: this.bucket,
2659
+ ...await this.ownerParam(),
2321
2660
  Key: this.indexKey(),
2322
2661
  Body: body,
2323
2662
  ContentLength: Buffer.byteLength(body),
@@ -41938,6 +42277,8 @@ async function stateMigrateCommand(options) {
41938
42277
  await newS3.send(new CopyObjectCommand({
41939
42278
  Bucket: newBucket,
41940
42279
  Key: obj.Key,
42280
+ ...await expectedOwnerParam(newS3),
42281
+ ExpectedSourceBucketOwner: accountId,
41941
42282
  CopySource: encodeURIComponent(`${legacyBucket}/${obj.Key}`)
41942
42283
  }));
41943
42284
  copied++;
@@ -41951,7 +42292,10 @@ async function stateMigrateCommand(options) {
41951
42292
  logger.info(`Emptying source bucket ${legacyBucket} (all versions + delete markers)...`);
41952
42293
  await emptyBucketAllVersions(legacyS3, legacyBucket);
41953
42294
  logger.info(`Deleting source bucket ${legacyBucket}...`);
41954
- await legacyS3.send(new DeleteBucketCommand({ Bucket: legacyBucket }));
42295
+ await legacyS3.send(new DeleteBucketCommand({
42296
+ Bucket: legacyBucket,
42297
+ ...await expectedOwnerParam(legacyS3)
42298
+ }));
41955
42299
  logger.info(`✓ Deleted source bucket: ${legacyBucket}`);
41956
42300
  } else logger.info(`Source bucket ${legacyBucket} kept. Pass --remove-legacy on a future run to delete it.`);
41957
42301
  logger.info(`✓ Migration complete: ${legacyBucket} -> ${newBucket}`);
@@ -41967,7 +42311,10 @@ async function stateMigrateCommand(options) {
41967
42311
  }
41968
42312
  async function bucketExists(s3, bucketName) {
41969
42313
  try {
41970
- await s3.send(new HeadBucketCommand({ Bucket: bucketName }));
42314
+ await s3.send(new HeadBucketCommand({
42315
+ Bucket: bucketName,
42316
+ ...await expectedOwnerParam(s3)
42317
+ }));
41971
42318
  return true;
41972
42319
  } catch (error) {
41973
42320
  const err = error;
@@ -41983,6 +42330,7 @@ async function listAllObjects(s3, bucket) {
41983
42330
  do {
41984
42331
  const resp = await s3.send(new ListObjectsV2Command({
41985
42332
  Bucket: bucket,
42333
+ ...await expectedOwnerParam(s3),
41986
42334
  ...continuationToken && { ContinuationToken: continuationToken }
41987
42335
  }));
41988
42336
  if (resp.Contents) all.push(...resp.Contents);
@@ -42025,10 +42373,12 @@ async function ensureDestinationBucket(bucketName, region, accountId, logger) {
42025
42373
  logger.info(`✓ Created destination bucket: ${bucketName}`);
42026
42374
  await s3.send(new PutBucketVersioningCommand({
42027
42375
  Bucket: bucketName,
42376
+ ...await expectedOwnerParam(s3),
42028
42377
  VersioningConfiguration: { Status: "Enabled" }
42029
42378
  }));
42030
42379
  await s3.send(new PutBucketEncryptionCommand({
42031
42380
  Bucket: bucketName,
42381
+ ...await expectedOwnerParam(s3),
42032
42382
  ServerSideEncryptionConfiguration: { Rules: [{
42033
42383
  ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
42034
42384
  BucketKeyEnabled: true
@@ -42036,6 +42386,7 @@ async function ensureDestinationBucket(bucketName, region, accountId, logger) {
42036
42386
  }));
42037
42387
  await s3.send(new PutBucketPolicyCommand({
42038
42388
  Bucket: bucketName,
42389
+ ...await expectedOwnerParam(s3),
42039
42390
  Policy: JSON.stringify({
42040
42391
  Version: "2012-10-17",
42041
42392
  Statement: [{
@@ -42062,6 +42413,7 @@ async function emptyBucketAllVersions(s3, bucket) {
42062
42413
  do {
42063
42414
  const resp = await s3.send(new ListObjectVersionsCommand({
42064
42415
  Bucket: bucket,
42416
+ ...await expectedOwnerParam(s3),
42065
42417
  ...keyMarker && { KeyMarker: keyMarker },
42066
42418
  ...versionIdMarker && { VersionIdMarker: versionIdMarker }
42067
42419
  }));
@@ -42078,6 +42430,7 @@ async function emptyBucketAllVersions(s3, bucket) {
42078
42430
  const batch = ids.slice(i, i + 1e3);
42079
42431
  await s3.send(new DeleteObjectsCommand({
42080
42432
  Bucket: bucket,
42433
+ ...await expectedOwnerParam(s3),
42081
42434
  Delete: {
42082
42435
  Objects: batch,
42083
42436
  Quiet: true
@@ -45314,7 +45667,10 @@ function formatBucketSource(source) {
45314
45667
  */
45315
45668
  async function detectBucketRegion(awsClients, bucket) {
45316
45669
  try {
45317
- const constraint = (await awsClients.s3.send(new GetBucketLocationCommand({ Bucket: bucket }))).LocationConstraint;
45670
+ const constraint = (await awsClients.s3.send(new GetBucketLocationCommand({
45671
+ Bucket: bucket,
45672
+ ...await expectedOwnerParam(awsClients.s3)
45673
+ }))).LocationConstraint;
45318
45674
  if (!constraint) return "us-east-1";
45319
45675
  if (constraint === "EU") return "eu-west-1";
45320
45676
  return constraint;
@@ -45362,7 +45718,8 @@ async function readSchemaVersion(awsClients, bucket, keys) {
45362
45718
  try {
45363
45719
  const resp = await awsClients.s3.send(new GetObjectCommand({
45364
45720
  Bucket: bucket,
45365
- Key: keys[0]
45721
+ Key: keys[0],
45722
+ ...await expectedOwnerParam(awsClients.s3)
45366
45723
  }));
45367
45724
  if (!resp.Body) return "unknown";
45368
45725
  const body = await resp.Body.transformToString();
@@ -45397,7 +45754,8 @@ async function listAssetStorageMarkers(awsClients, bucket) {
45397
45754
  try {
45398
45755
  const marker = parseBootstrapMarker(await (await awsClients.s3.send(new GetObjectCommand({
45399
45756
  Bucket: bucket,
45400
- Key: key
45757
+ Key: key,
45758
+ ...await expectedOwnerParam(awsClients.s3)
45401
45759
  }))).Body?.transformToString() ?? "", key);
45402
45760
  entries.push({
45403
45761
  region,
@@ -56046,7 +56404,7 @@ function reorderArgs(argv) {
56046
56404
  async function main() {
56047
56405
  installPipeCloseHandler();
56048
56406
  const program = new Command();
56049
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.235.0");
56407
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.236.0");
56050
56408
  program.addCommand(createBootstrapCommand());
56051
56409
  program.addCommand(createSynthCommand());
56052
56410
  program.addCommand(createListCommand());