@go-to-k/cdkd 0.284.5 → 0.284.7

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-BKSfYWps.js";
2
+ import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-CJncZQEi.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-C1NtROYt.js";
4
+ import { t as ASGProvider } from "./asg-provider-DOdJgPLS.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, 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";
@@ -995,12 +995,66 @@ async function deleteContainerRepo(containerRepo, region, profile, logger) {
995
995
  ecrClient.destroy();
996
996
  }
997
997
  }
998
- /**
999
- * List OTHER regions that still have a bootstrap marker in the state
1000
- * bucket (i.e. are still opted in to cdkd asset storage).
1001
- */
1002
- async function listOtherBootstrapRegions(stateBackend, region) {
1003
- 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);
998
+ /** The region spelling a bootstrap marker key carries, e.g. `Us-East-1`. */
999
+ function markerRegionOfKey(markerKey) {
1000
+ return markerKey.slice(BOOTSTRAP_MARKER_PREFIX.length, -5);
1001
+ }
1002
+ /**
1003
+ * Partition the state bucket's bootstrap markers, relative to the ONE this run
1004
+ * resolved, into the two kinds that must not be silently destroyed with the
1005
+ * bucket:
1006
+ *
1007
+ * - `otherRegions` — markers for a DIFFERENT region, still opted in.
1008
+ * - `sameRegionKeys` — markers for THIS region under a different SPELLING of
1009
+ * its name. The teardown deletes exactly one key, so every other spelling
1010
+ * survives, naming asset storage this run did not touch.
1011
+ *
1012
+ * Both come from ONE listing, and the second kind is why the listing rather
1013
+ * than a per-key probe is the source: a probe is only as deep as the keys it
1014
+ * guesses (`us-east-1` / `US-EAST-1`), while a region can hold a marker under
1015
+ * any spelling (`Us-East-1`), and the listing sees all of them.
1016
+ *
1017
+ * `region` arrives CANONICAL (issue #1995) while the key segments are whatever
1018
+ * `cdkd bootstrap` actually wrote, which is not folded (issue #1820) — so the
1019
+ * two sides are compared through {@link canonicalizeRegion} rather than
1020
+ * directly. A raw compare re-introduces this command's own bug in a different
1021
+ * flag: with a marker at `cdkd-bootstrap/US-EAST-1.json`,
1022
+ * `--destroy --region US-EAST-1 --include-state-bucket` would list `US-EAST-1`
1023
+ * as an "other" region, refuse with `STATE_BUCKET_HOLDS_MARKERS` naming the
1024
+ * very region being torn down, and tell the user to run the command they just
1025
+ * ran. It aborts BEFORE the teardown, so the bucket and repo survive too.
1026
+ *
1027
+ * Other regions are deduplicated by the folded name, so a region bootstrapped
1028
+ * twice under two spellings is reported once rather than twice.
1029
+ *
1030
+ * The comparison folds but the REPORTED value stays the RAW key segment, and
1031
+ * that asymmetry is load-bearing. The error tells the user to run
1032
+ * `cdkd bootstrap --destroy --region <r>` for each name printed, and only the
1033
+ * raw spelling actually reaches the marker: `--region eu-west-1` against a
1034
+ * marker at `cdkd-bootstrap/EU-WEST-1.json` probes the canonical key (absent),
1035
+ * finds `rawMarkerKey === markerKey`, skips the second probe, and reports
1036
+ * "nothing to delete". Printing the folded name would hand the user a command
1037
+ * that cannot work.
1038
+ */
1039
+ async function listBootstrapMarkerSiblings(stateBackend, region, resolvedMarkerKey) {
1040
+ const segments = (await stateBackend.listRawKeys(BOOTSTRAP_MARKER_PREFIX)).filter((k) => k.endsWith(".json")).map((k) => {
1041
+ const raw = k.slice(BOOTSTRAP_MARKER_PREFIX.length, -5);
1042
+ return {
1043
+ key: k,
1044
+ raw,
1045
+ folded: canonicalizeRegion(raw)
1046
+ };
1047
+ }).filter((e) => e.raw.length > 0);
1048
+ const byCanonical = /* @__PURE__ */ new Map();
1049
+ for (const { raw, folded } of segments) {
1050
+ if (folded === region) continue;
1051
+ if (!byCanonical.has(folded)) byCanonical.set(folded, raw);
1052
+ }
1053
+ const sameRegionKeys = segments.filter((e) => e.folded === region && e.key !== resolvedMarkerKey).map((e) => e.key);
1054
+ return {
1055
+ otherRegions: [...byCanonical.values()],
1056
+ sameRegionKeys
1057
+ };
1004
1058
  }
1005
1059
  /**
1006
1060
  * `cdkd bootstrap --destroy` implementation. Dispatched from the bootstrap
@@ -1013,14 +1067,16 @@ async function bootstrapDestroyCommand(options) {
1013
1067
  logger.debug("Options:", options);
1014
1068
  await applyRoleArnIfSet({
1015
1069
  roleArn: options.roleArn,
1016
- region: options.region
1070
+ region: canonicalizeRegion(options.region)
1017
1071
  });
1072
+ const rawRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
1073
+ const region = canonicalizeRegion(rawRegion);
1074
+ const clientRegion = canonicalizeRegion(options.region || process.env["AWS_REGION"]) || void 0;
1018
1075
  const awsClients = new AwsClients({
1019
- ...options.region && { region: options.region },
1076
+ ...clientRegion !== void 0 && { region: clientRegion },
1020
1077
  ...options.profile && { profile: options.profile }
1021
1078
  });
1022
1079
  setAwsClients(awsClients);
1023
- const region = options.region || process.env["AWS_REGION"] || "us-east-1";
1024
1080
  const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
1025
1081
  const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
1026
1082
  const markerS3Client = new S3Client({
@@ -1036,9 +1092,18 @@ async function bootstrapDestroyCommand(options) {
1036
1092
  });
1037
1093
  try {
1038
1094
  const markerKey = getBootstrapMarkerKey(region);
1095
+ const rawMarkerKey = getBootstrapMarkerKey(rawRegion);
1039
1096
  let markerBody;
1097
+ let resolvedMarkerKey = markerKey;
1040
1098
  try {
1041
1099
  markerBody = await stateBackend.getRawObject(markerKey);
1100
+ if (markerBody === null && rawMarkerKey !== markerKey) {
1101
+ markerBody = await stateBackend.getRawObject(rawMarkerKey);
1102
+ if (markerBody !== null) {
1103
+ resolvedMarkerKey = rawMarkerKey;
1104
+ logger.debug(`Bootstrap marker found at the un-folded key '${rawMarkerKey}' (none at '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
1105
+ }
1106
+ }
1042
1107
  } catch (error) {
1043
1108
  if (error.name === "NoSuchBucket") {
1044
1109
  logger.info(`State bucket '${bucketName}' does not exist — this account/region was never bootstrapped; nothing to delete.`);
@@ -1046,14 +1111,29 @@ async function bootstrapDestroyCommand(options) {
1046
1111
  }
1047
1112
  throw error;
1048
1113
  }
1114
+ let markerSiblings = {
1115
+ otherRegions: [],
1116
+ sameRegionKeys: []
1117
+ };
1118
+ let markerListingSucceeded = true;
1119
+ try {
1120
+ markerSiblings = await listBootstrapMarkerSiblings(stateBackend, region, resolvedMarkerKey);
1121
+ } catch (error) {
1122
+ markerListingSucceeded = false;
1123
+ if (options.includeStateBucket) throw new CdkdError(`Cannot verify which bootstrap markers the state bucket '${bucketName}' still holds (listing '${BOOTSTRAP_MARKER_PREFIX}' failed), and --include-state-bucket deletes every one of them. Grant s3:ListBucket on this bucket and re-run.`, "STATE_BUCKET_MARKER_SCAN_FAILED", error);
1124
+ logger.warn(`Could not list bootstrap markers under '${BOOTSTRAP_MARKER_PREFIX}' (${error instanceof Error ? error.message : String(error)}). Continuing — this only means a marker for this region under another spelling of its name would not be reported.`);
1125
+ }
1049
1126
  let marker;
1050
1127
  if (markerBody === null) {
1051
- 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.`);
1128
+ const probed = rawMarkerKey === markerKey ? markerKey : `${markerKey}, ${rawMarkerKey}`;
1129
+ logger.info(`No bootstrap marker for region '${region}' (${probed}) — asset storage is not opted in for this region (or was already destroyed); nothing to delete.`);
1130
+ if (markerSiblings.sameRegionKeys.length > 0) for (const siblingKey of markerSiblings.sameRegionKeys) logger.warn(`A bootstrap marker for this region DOES exist at '${siblingKey}', under a different spelling of the region name — its asset bucket and ECR repository are still alive. Re-run 'cdkd bootstrap --destroy --region ${markerRegionOfKey(siblingKey)}' to tear it down.`);
1052
1131
  if (!options.includeStateBucket) {
1053
- 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.`);
1132
+ if (!markerListingSucceeded) logger.warn(`The bootstrap-marker listing could not be made, so it is NOT known whether this region has a marker under another spelling of its name. Check 's3://${bucketName}/${BOOTSTRAP_MARKER_PREFIX}' before re-bootstrapping — re-running 'cdkd bootstrap' while one exists writes a SECOND marker with DEFAULT names, and the next destroy would then tear down that storage while the storage the existing marker names survives.`);
1133
+ else if (markerSiblings.sameRegionKeys.length === 0) 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.`);
1054
1134
  return;
1055
1135
  }
1056
- } else marker = parseBootstrapMarker(markerBody, markerKey);
1136
+ } else marker = parseBootstrapMarker(markerBody, resolvedMarkerKey);
1057
1137
  if (marker && !options.force) {
1058
1138
  logger.info("Scanning stack state for references to the asset storage...");
1059
1139
  const referencing = await scanStateReferences(stateBackend, [marker.assetBucket, marker.containerRepo]);
@@ -1065,14 +1145,19 @@ async function bootstrapDestroyCommand(options) {
1065
1145
  const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
1066
1146
  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");
1067
1147
  }
1068
- const otherRegions = await listOtherBootstrapRegions(stateBackend, region);
1069
- 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");
1148
+ if (markerSiblings.otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${markerSiblings.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");
1149
+ if (markerSiblings.sameRegionKeys.length > 0) {
1150
+ const canonicalStillPresent = markerBody !== null && resolvedMarkerKey === markerKey;
1151
+ const listing = markerSiblings.sameRegionKeys.map((k) => canonicalStillPresent ? ` - ${k}` : ` - ${k} (cdkd bootstrap --destroy --region ${markerRegionOfKey(k)})`).join("\n");
1152
+ const remedy = canonicalStillPresent ? `Run 'cdkd bootstrap --destroy --region ${region}' WITHOUT --include-state-bucket ONCE: it tears down '${resolvedMarkerKey}' and then prints the exact --region spelling for each marker still standing. Follow those, then re-run with --include-state-bucket. (No per-marker spelling is given here because '${resolvedMarkerKey}' still exists and would be resolved first — and simply repeating this same command would not clear the rest, since once it is gone this region's canonical key no longer resolves anything.)` : "Run the command shown against each marker above, then re-run with --include-state-bucket.";
1153
+ throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region '${region}' has ${markerSiblings.sameRegionKeys.length} further bootstrap marker(s) under a different spelling of its name, which this teardown does not delete:\n${listing}\n${remedy}\nDestroy each one first — deleting the state bucket now would remove those markers while the asset storage they name survives, with no record of its names.`, "STATE_BUCKET_HOLDS_SIBLING_MARKERS");
1154
+ }
1070
1155
  }
1071
1156
  const planLines = [];
1072
1157
  if (marker) {
1073
1158
  planLines.push(`Asset bucket: s3://${marker.assetBucket} (region ${region}, all contents)`);
1074
1159
  planLines.push(`Container-asset ECR repository: ${marker.containerRepo} (region ${region}, all images)`);
1075
- planLines.push(`Bootstrap marker: s3://${bucketName}/${markerKey} (deleted last)`);
1160
+ planLines.push(`Bootstrap marker: s3://${bucketName}/${resolvedMarkerKey} (deleted last)`);
1076
1161
  }
1077
1162
  if (options.includeStateBucket) planLines.push(`State bucket: s3://${bucketName} (ALL contents, including all versions)`);
1078
1163
  if (!await promptBootstrapDestroyConfirm({
@@ -1085,8 +1170,10 @@ async function bootstrapDestroyCommand(options) {
1085
1170
  if (marker) {
1086
1171
  await emptyAndDeleteBucket(awsClients.s3, marker.assetBucket, accountId, "Asset bucket", logger);
1087
1172
  await deleteContainerRepo(marker.containerRepo, region, options.profile, logger);
1088
- await stateBackend.deleteRawObjects([markerKey]);
1089
- logger.info(`✓ Deleted bootstrap marker (${markerKey})`);
1173
+ await stateBackend.deleteRawObjects([resolvedMarkerKey]);
1174
+ logger.info(`✓ Deleted bootstrap marker (${resolvedMarkerKey})`);
1175
+ for (const siblingKey of markerSiblings.sameRegionKeys) logger.warn(`A further bootstrap marker for this region remains at '${siblingKey}' (this region was bootstrapped under more than one spelling of its name). It names asset storage that was NOT destroyed by this run — re-run 'cdkd bootstrap --destroy --region ${markerRegionOfKey(siblingKey)}' to tear that one down too.`);
1176
+ if (!markerListingSucceeded) logger.warn(`The bootstrap-marker listing could not be made, so a marker under another spelling of this region may still be live. Check 's3://${bucketName}/${BOOTSTRAP_MARKER_PREFIX}'.`);
1090
1177
  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.`);
1091
1178
  }
1092
1179
  if (options.includeStateBucket) {
@@ -63078,7 +63165,10 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
63078
63165
  const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, {
63079
63166
  desiredFromAwsReadback: true,
63080
63167
  maskSecrets: createSecretMasker(secrets)
63081
- }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)) } });
63168
+ }), outcome.logicalId, { logger: {
63169
+ debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)),
63170
+ warn: (msg) => logger.warn(maskSecretsInText(msg, secrets))
63171
+ } });
63082
63172
  totalSucceeded++;
63083
63173
  const revertPartial = updatePartialReason(updateResult);
63084
63174
  if (revertPartial !== void 0) logger.warn(` ✓ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, ${maskSecretsInText(updatePartialMessage(revertPartial), secrets)}`);
@@ -69861,6 +69951,107 @@ const KEY_TERMINATORS = "[^\\s\"'?]";
69861
69951
  */
69862
69952
  const GC_EXTRA_URL_SUFFIXES = [];
69863
69953
  /**
69954
+ * Every AWS partition's URL suffix. The asset-reference matchers match the
69955
+ * suffix against this CLOSED SET instead of hardcoding one literal
69956
+ * (issue #1781).
69957
+ *
69958
+ * Before this list, all three matchers spelled `amazonaws\.com` inline, so a
69959
+ * state file written in `aws-cn` / `us-iso*` / `us-isob*` (recording
69960
+ * `amazonaws.com.cn` / `c2s.ic.gov` / `sc2s.sgov.gov` hosts) matched none of
69961
+ * them and its assets read as UNREFERENCED.
69962
+ *
69963
+ * **What that actually deleted, measured rather than assumed.** The blast
69964
+ * radius is narrower than "every live asset" but still a silent, irreversible
69965
+ * delete:
69966
+ *
69967
+ * - **S3 was already rescued**, by accident. {@link CONTENT_HASH_KEY_RE}
69968
+ * collects `<sha256>.<ext>` tokens out of ANY string regardless of host, and
69969
+ * cdkd's file assets are content-addressed with an extension — so a
69970
+ * non-commercial `https://…amazonaws.com.cn/<sha256>.zip` still had its key
69971
+ * protected by the name-independent pass. Measured on a real `--dry-run`
69972
+ * against a seeded `cn-north-1` state file: 71 of 72 objects, including the
69973
+ * referenced one, were protected with the matchers still broken.
69974
+ * - **ECR was NOT rescued, and is where the bug actually deleted.** cdkd's
69975
+ * image tags are bare 64-hex with no `.<ext>` tail, so the content-hash pass
69976
+ * cannot see them, and digests are `sha256:<hex>` — also no tail. With the
69977
+ * ECR matcher missing the suffix, a referenced non-commercial image is
69978
+ * selected for deletion. That is the case the live test reproduced.
69979
+ *
69980
+ * Either way the failure direction is the opposite of the #1758 / #1745 sites,
69981
+ * which merely emit a host that does not resolve.
69982
+ *
69983
+ * Two properties of this list are load-bearing:
69984
+ *
69985
+ * - It is the union over ALL partitions, not
69986
+ * `derivePartitionAndUrlSuffix(region).urlSuffix` for the caller's region.
69987
+ * The scan reads EVERY state file in the state bucket, written by any cdkd
69988
+ * binary for any region, so it must match every partition at once; a single
69989
+ * derived literal would still delete a `cn-north-1` stack's images during a
69990
+ * `us-east-1` gc run.
69991
+ * - It is a SUPERSET of the arms {@link derivePartitionAndUrlSuffix} knows
69992
+ * (issue #1764). A suffix missing HERE deletes live assets, which is the
69993
+ * irreversible direction, so this list must lead that table rather than
69994
+ * follow it.
69995
+ *
69996
+ * The superset property is now MECHANICAL rather than hand-maintained (issue
69997
+ * #1785): the list is built FROM {@link PARTITION_TABLE} plus
69998
+ * {@link GC_EXTRA_URL_SUFFIXES}, so a partition arm added to that table
69999
+ * propagates here in the same commit instead of silently outrunning gc. It
70000
+ * used to be a hand-written literal, and the test meant to fence it iterated a
70001
+ * hand-written region list — so a new arm in the derive table redded nothing.
70002
+ *
70003
+ * It is a closed SET rather than a `[^/\s]+` wildcard so a look-alike host
70004
+ * (`https://<assetBucket>.s3.<region>.example.com/<key>`) is not treated as a
70005
+ * cdkd asset reference. That direction only ever over-PROTECTS, but it would
70006
+ * also let any string embedding the bucket name pin an object forever, which
70007
+ * quietly turns gc into a no-op.
70008
+ *
70009
+ * NOTE this is a weaker check than `src/utils/ecr-uri.ts`, which CAPTURES the
70010
+ * suffix and validates it against the region the host names. That is the right
70011
+ * shape there (one host, one caller-known region) and the wrong shape here:
70012
+ * the region in a scanned string is not necessarily the region whose partition
70013
+ * the suffix must belong to, and pairing them would re-inherit #1764's missing
70014
+ * arms on the irreversible side.
70015
+ */
70016
+ const AWS_URL_SUFFIXES = [.../* @__PURE__ */ new Set([...PARTITION_TABLE.map((row) => row.urlSuffix), ...GC_EXTRA_URL_SUFFIXES])];
70017
+ /**
70018
+ * Spell an ASCII literal so it matches case-INSENSITIVELY inside a
70019
+ * case-SENSITIVE `RegExp`: `s3` → `[sS]3`, `amazonaws.com` →
70020
+ * `[aA][mM][aA][zZ][oO][nN][aA][wW][sS]\.[cC][oO][mM]`. EVERY character goes
70021
+ * through {@link escapeRegExp} first and the ASCII letters are folded after,
70022
+ * so the result is exactly as strict as an escaped literal outside the letters
70023
+ * it folds.
70024
+ *
70025
+ * This exists because gc must fold only PART of a pattern (issue #1847). Host
70026
+ * names are case-insensitive, so
70027
+ * `https://<assetBucket>.s3.<region>.AMAZONAWS.COM/<key>` in a state file names
70028
+ * the same object as the lower-cased spelling — and a reference gc misses reads
70029
+ * as UNREFERENCED, so the live object is DELETED. The obvious fix, the `i` flag
70030
+ * the ECR matcher carries, is per-REGEX and therefore all-or-nothing: it cannot
70031
+ * fold the host while leaving a KEY or a path-segment bucket exact, and those
70032
+ * segments are genuinely case-sensitive at S3. Which segment gets folded is
70033
+ * decided per shape at the matchers themselves — see the block above
70034
+ * `s3UriRe` for the authority / path-segment / DNS-label split.
70035
+ *
70036
+ * Two further properties are load-bearing:
70037
+ *
70038
+ * - Folding by character class rather than by flag leaves
70039
+ * {@link URL_SUFFIX_ALTERNATION} — SHARED with the ECR matcher — spelled
70040
+ * exactly as that matcher's own review settled it, so the S3 side cannot
70041
+ * quietly redefine the constant the ECR side reads (issues #1792 / #1793).
70042
+ * - A two-character class is not `/k/i`. Under a `u` flag the `i` flag
70043
+ * canonicalizes U+212A KELVIN SIGN onto ASCII `k` — the hazard
70044
+ * `src/utils/ecr-uri.ts` guards its region / suffix checks against —
70045
+ * whereas `[kK]` matches those two code points and nothing else, with or
70046
+ * without `u`. (Measured, and pinned by `tests/unit/cli/gc.test.ts`:
70047
+ * `/k/iu` matches U+212A, `/[kK]/u` does not.) The matchers below carry no
70048
+ * `u` flag today, so this is about what adding one later could silently
70049
+ * widen: with classes, nothing.
70050
+ */
70051
+ function caseFoldLiteral(literal) {
70052
+ return escapeRegExp(literal).replace(/[A-Za-z]/g, (ch) => `[${ch.toLowerCase()}${ch.toUpperCase()}]`);
70053
+ }
70054
+ /**
69864
70055
  * `(?:amazonaws\.com\.cn|amazonaws\.com|...)` — {@link AWS_URL_SUFFIXES} as a
69865
70056
  * non-capturing alternation, LONGEST FIRST so `amazonaws.com.cn` is tried
69866
70057
  * before its own `amazonaws.com` prefix instead of relying on backtracking.
@@ -69870,8 +70061,48 @@ const GC_EXTRA_URL_SUFFIXES = [];
69870
70061
  * the ECR digest), and this alternation sits BEFORE those groups in all three
69871
70062
  * patterns. Making it capturing would shift every index by one and silently
69872
70063
  * collect the suffix as if it were a key.
70064
+ *
70065
+ * `spellSuffix` decides how each suffix is written out, so the SET, the
70066
+ * longest-first order and the non-capturing property keep exactly one
70067
+ * definition while the two consumers differ in case handling — see
70068
+ * {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED}.
70069
+ */
70070
+ function buildUrlSuffixAlternation(spellSuffix) {
70071
+ return `(?:${[...AWS_URL_SUFFIXES].sort((a, b) => b.length - a.length).map(spellSuffix).join("|")})`;
70072
+ }
70073
+ /**
70074
+ * The case-SENSITIVE spelling, consumed by {@link ECR_REGISTRY_HOST}.
70075
+ *
70076
+ * Folding THIS constant would be inert for its only consumer — the ECR matcher
70077
+ * carries the `i` flag, so it already matches an upper-cased suffix either way.
70078
+ * The constraint is the other direction: this is the spelling the ECR side's
70079
+ * own review settled on (issues #1792 / #1793), and the S3 matchers must widen
70080
+ * through their own {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED} copy rather than
70081
+ * by editing the shared one, so an S3-side change can never reach the ECR
70082
+ * grammar.
70083
+ */
70084
+ const URL_SUFFIX_ALTERNATION = buildUrlSuffixAlternation(escapeRegExp);
70085
+ /**
70086
+ * The case-FOLDED spelling, consumed by the three S3 matchers (issue #1847).
70087
+ * Same suffix set, same order, letters folded per {@link caseFoldLiteral} — so
70088
+ * an upper-cased or mixed-case host suffix collects the key. Still a CLOSED
70089
+ * set: folding widens the SPELLING of each suffix, never which suffixes count.
69873
70090
  */
69874
- const URL_SUFFIX_ALTERNATION = `(?:${[...[.../* @__PURE__ */ new Set([...PARTITION_TABLE.map((row) => row.urlSuffix), ...GC_EXTRA_URL_SUFFIXES])]].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|")})`;
70091
+ const URL_SUFFIX_ALTERNATION_CASE_FOLDED = buildUrlSuffixAlternation(caseFoldLiteral);
70092
+ /**
70093
+ * `[sS]3`, case-folded (issue #1847). One constant for BOTH places the literal
70094
+ * `s3` appears: the `s3://` URI SCHEME and the `s3` HOST LABEL of the two HTTPS
70095
+ * shapes. The two roles differ but the spelling and the fold argument are the
70096
+ * same, and a single constant is what stops the three matchers drifting into
70097
+ * two different spellings of it.
70098
+ */
70099
+ const S3_SEGMENT_CASE_FOLDED = caseFoldLiteral("s3");
70100
+ /**
70101
+ * `[hH][tT][tT][pP][sS]` — the URI scheme, case-folded (issue #1847). A scheme
70102
+ * is case-insensitive by RFC 3986 §3.1, and folding a fixed literal that is
70103
+ * neither the bucket name nor the key can only ever over-PROTECT.
70104
+ */
70105
+ const HTTPS_SCHEME_CASE_FOLDED = caseFoldLiteral("https");
69875
70106
  /**
69876
70107
  * The HOST half of an ECR registry, built from the ONE authoritative FORM TABLE
69877
70108
  * in `src/utils/ecr-uri.ts` (issue #1793) instead of spelled a second time here.
@@ -69964,16 +70195,21 @@ function tryDecodeBase64Text(value) {
69964
70195
  * account's or region's URI can only over-protect (keep more), never
69965
70196
  * delete more.
69966
70197
  *
69967
- * `<urlSuffix>` in the two S3 shapes is {@link URL_SUFFIX_ALTERNATION}, i.e.
69968
- * EVERY partition's suffix at once — see that constant for why a per-region
69969
- * derived literal is the wrong shape here (issue #1781).
70198
+ * `<urlSuffix>` in the two S3 shapes is
70199
+ * {@link URL_SUFFIX_ALTERNATION_CASE_FOLDED}, i.e. EVERY partition's suffix at
70200
+ * once see {@link AWS_URL_SUFFIXES} for why a per-region derived literal is
70201
+ * the wrong shape here (issue #1781). Every S3 HOST segment is matched
70202
+ * case-insensitively, including the bucket label of the virtual-hosted shape,
70203
+ * while the KEY and the bucket in the other two shapes stay EXACT (issue
70204
+ * #1847); the per-shape reasoning is in the block above `s3UriRe`.
69970
70205
  */
69971
70206
  function buildReferenceExtractors(marker) {
69972
- const bucket = escapeRegExp(marker.assetBucket);
70207
+ const bucketExact = escapeRegExp(marker.assetBucket);
70208
+ const bucketFolded = caseFoldLiteral(marker.assetBucket);
69973
70209
  const repo = escapeRegExp(marker.containerRepo);
69974
- const s3UriRe = new RegExp(`s3://${bucket}/(${KEY_TERMINATORS}+)`, "g");
69975
- const virtualHostedRe = new RegExp(`https://${bucket}\\.s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/(${KEY_TERMINATORS}+)`, "g");
69976
- const pathStyleRe = new RegExp(`https://s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/${bucket}/(${KEY_TERMINATORS}+)`, "g");
70210
+ const s3UriRe = new RegExp(`${S3_SEGMENT_CASE_FOLDED}://${bucketExact}/(${KEY_TERMINATORS}+)`, "g");
70211
+ const virtualHostedRe = new RegExp(`${HTTPS_SCHEME_CASE_FOLDED}://${bucketFolded}\\.${S3_SEGMENT_CASE_FOLDED}[^/\\s]*\\.${URL_SUFFIX_ALTERNATION_CASE_FOLDED}/(${KEY_TERMINATORS}+)`, "g");
70212
+ const pathStyleRe = new RegExp(`${HTTPS_SCHEME_CASE_FOLDED}://${S3_SEGMENT_CASE_FOLDED}[^/\\s]*\\.${URL_SUFFIX_ALTERNATION_CASE_FOLDED}/${bucketExact}/(${KEY_TERMINATORS}+)`, "g");
69977
70213
  const ecrRe = new RegExp(`${ECR_REGISTRY_HOST}/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "gi");
69978
70214
  const extractOnce = (value, refs) => {
69979
70215
  for (const re of [
@@ -70242,9 +70478,10 @@ async function gcCommand(options) {
70242
70478
  logger.debug("Options:", options);
70243
70479
  await applyRoleArnIfSet({
70244
70480
  roleArn: options.roleArn,
70245
- region: options.region
70481
+ region: canonicalizeRegion(options.region)
70246
70482
  });
70247
- const region = options.region || process.env["AWS_REGION"] || "us-east-1";
70483
+ const rawRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
70484
+ const region = canonicalizeRegion(rawRegion);
70248
70485
  const awsClients = new AwsClients({
70249
70486
  region,
70250
70487
  ...options.profile && { profile: options.profile }
@@ -70269,9 +70506,18 @@ async function gcCommand(options) {
70269
70506
  });
70270
70507
  try {
70271
70508
  const markerKey = getBootstrapMarkerKey(region);
70509
+ const rawMarkerKey = getBootstrapMarkerKey(rawRegion);
70272
70510
  let markerBody;
70511
+ let resolvedMarkerKey = markerKey;
70273
70512
  try {
70274
70513
  markerBody = await stateBackend.getRawObject(markerKey);
70514
+ if (markerBody === null && rawMarkerKey !== markerKey) {
70515
+ markerBody = await stateBackend.getRawObject(rawMarkerKey);
70516
+ if (markerBody !== null) {
70517
+ resolvedMarkerKey = rawMarkerKey;
70518
+ logger.debug(`Bootstrap marker found at the un-folded key '${rawMarkerKey}' (none at '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
70519
+ }
70520
+ }
70275
70521
  } catch (error) {
70276
70522
  if (error.name === "NoSuchBucket") {
70277
70523
  logger.info(`State bucket '${bucketName}' does not exist — this account was never bootstrapped; nothing to garbage-collect.`);
@@ -70280,10 +70526,11 @@ async function gcCommand(options) {
70280
70526
  throw error;
70281
70527
  }
70282
70528
  if (markerBody === null) {
70283
- logger.info(`No bootstrap marker for region '${region}' (${markerKey}) — the region is not opted in to cdkd asset storage; nothing to garbage-collect. (CDK bootstrap storage is 'cdk gc' territory.)`);
70529
+ const probed = rawMarkerKey === markerKey ? markerKey : `${markerKey}, ${rawMarkerKey}`;
70530
+ logger.info(`No bootstrap marker for region '${region}' (${probed}) — the region is not opted in to cdkd asset storage; nothing to garbage-collect. (CDK bootstrap storage is 'cdk gc' territory.)`);
70284
70531
  return;
70285
70532
  }
70286
- const marker = parseBootstrapMarker(markerBody, markerKey);
70533
+ const marker = parseBootstrapMarker(markerBody, resolvedMarkerKey);
70287
70534
  const lockKeys = await listAllLockKeys(stateBackend);
70288
70535
  if (lockKeys.length > 0) {
70289
70536
  const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX)} [${k}]`).join("\n");
@@ -82114,7 +82361,7 @@ function createMigrateCommand() {
82114
82361
  */
82115
82362
  function buildProgram() {
82116
82363
  const program = new Command();
82117
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.5");
82364
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.7");
82118
82365
  program.hook("preAction", (_thisCommand, actionCommand) => {
82119
82366
  const { profile } = actionCommand.optsWithGlobals();
82120
82367
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;