@go-to-k/cdkd 0.9.0 → 0.11.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.
Binary file
package/dist/index.js CHANGED
@@ -783,10 +783,73 @@ Caused by: ${error.cause.message}`;
783
783
  }
784
784
  return String(error);
785
785
  }
786
+ function normalizeAwsError(err, context = {}) {
787
+ if (!(err instanceof Error)) {
788
+ return new Error(String(err));
789
+ }
790
+ const isUnknown = err.name === "Unknown" || err.message === "UnknownError";
791
+ if (!isUnknown)
792
+ return err;
793
+ const meta = err.$metadata;
794
+ const status = meta?.httpStatusCode;
795
+ const bucket = context.bucket ?? "<unknown bucket>";
796
+ const operation = context.operation ?? "operation";
797
+ switch (status) {
798
+ case 301: {
799
+ const responseHeaders = err.$response?.headers;
800
+ const region = responseHeaders?.["x-amz-bucket-region"] ?? responseHeaders?.["X-Amz-Bucket-Region"];
801
+ const where = region ? ` (in ${region})` : "";
802
+ return new Error(
803
+ `Bucket '${bucket}'${where} is in a different region than the client. cdkd resolves this automatically; if you see this message, please report it.`
804
+ );
805
+ }
806
+ case 403:
807
+ return new Error(
808
+ `Access denied to bucket '${bucket}'. Verify credentials and bucket policy.`
809
+ );
810
+ case 404:
811
+ return new Error(`Bucket '${bucket}' does not exist.`);
812
+ default: {
813
+ const statusStr = status !== void 0 ? `HTTP ${status}` : "unknown HTTP status";
814
+ return new Error(
815
+ `S3 error during ${operation} on '${bucket}' (${statusStr}). See CloudTrail for details.`
816
+ );
817
+ }
818
+ }
819
+ }
786
820
 
787
821
  // src/index.ts
788
822
  init_aws_clients();
789
823
 
824
+ // src/utils/aws-region-resolver.ts
825
+ import { GetBucketLocationCommand, S3Client as S3Client2 } from "@aws-sdk/client-s3";
826
+ var cache = /* @__PURE__ */ new Map();
827
+ async function resolveBucketRegion(bucketName, opts = {}) {
828
+ const cached = cache.get(bucketName);
829
+ if (cached)
830
+ return cached;
831
+ const promise = (async () => {
832
+ const client = new S3Client2({
833
+ region: "us-east-1",
834
+ ...opts.profile && { profile: opts.profile },
835
+ ...opts.credentials && { credentials: opts.credentials }
836
+ });
837
+ try {
838
+ const response = await client.send(new GetBucketLocationCommand({ Bucket: bucketName }));
839
+ return response.LocationConstraint || "us-east-1";
840
+ } catch {
841
+ return opts.fallbackRegion ?? "us-east-1";
842
+ } finally {
843
+ client.destroy();
844
+ }
845
+ })();
846
+ cache.set(bucketName, promise);
847
+ return promise;
848
+ }
849
+ function clearBucketRegionCache() {
850
+ cache.clear();
851
+ }
852
+
790
853
  // src/synthesis/synthesizer.ts
791
854
  import { existsSync as existsSync3, mkdirSync, statSync } from "node:fs";
792
855
  import { resolve as resolve3 } from "node:path";
@@ -2083,7 +2146,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
2083
2146
  // src/assets/file-asset-publisher.ts
2084
2147
  import { createReadStream, statSync as statSync2 } from "node:fs";
2085
2148
  import { join as join4, basename } from "node:path";
2086
- import { S3Client as S3Client2, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
2149
+ import { S3Client as S3Client3, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
2087
2150
  var FileAssetPublisher = class {
2088
2151
  logger = getLogger().child("FileAssetPublisher");
2089
2152
  /**
@@ -2104,7 +2167,7 @@ var FileAssetPublisher = class {
2104
2167
  this.logger.debug(
2105
2168
  `Publishing file asset ${asset.displayName || assetHash} \u2192 s3://${bucketName}/${objectKey}`
2106
2169
  );
2107
- const client = new S3Client2({
2170
+ const client = new S3Client3({
2108
2171
  region: destRegion
2109
2172
  });
2110
2173
  try {
@@ -2653,6 +2716,7 @@ var AssetPublisher = class {
2653
2716
 
2654
2717
  // src/state/s3-state-backend.ts
2655
2718
  import {
2719
+ S3Client as S3Client4,
2656
2720
  GetObjectCommand,
2657
2721
  PutObjectCommand as PutObjectCommand2,
2658
2722
  DeleteObjectCommand,
@@ -2670,11 +2734,14 @@ var STATE_SCHEMA_VERSION_CURRENT = 2;
2670
2734
  var LEGACY_KEY_DEPTH = 2;
2671
2735
  var NEW_KEY_DEPTH = 3;
2672
2736
  var S3StateBackend = class {
2673
- constructor(s3Client, config) {
2737
+ constructor(s3Client, config, clientOpts = {}) {
2674
2738
  this.s3Client = s3Client;
2675
2739
  this.config = config;
2740
+ this.clientOpts = clientOpts;
2676
2741
  }
2677
2742
  logger = getLogger().child("S3StateBackend");
2743
+ clientResolved = false;
2744
+ resolveInFlight = null;
2678
2745
  /**
2679
2746
  * Get the new (region-scoped) S3 key for a stack's state file.
2680
2747
  */
@@ -2688,13 +2755,73 @@ var S3StateBackend = class {
2688
2755
  getLegacyStateKey(stackName) {
2689
2756
  return `${this.config.prefix}/${stackName}/state.json`;
2690
2757
  }
2758
+ /**
2759
+ * Resolve the state bucket's actual region and, if it differs from the
2760
+ * client's currently-configured region, replace the S3Client with one
2761
+ * pointed at the bucket's region.
2762
+ *
2763
+ * This is idempotent: subsequent calls return immediately. Concurrent
2764
+ * callers (e.g. when several public methods race during a parallel deploy)
2765
+ * share a single in-flight resolution promise so we never issue more than
2766
+ * one `GetBucketLocation` per backend.
2767
+ *
2768
+ * Errors from `GetBucketLocation` are deliberately swallowed by
2769
+ * `resolveBucketRegion` — the resolver returns `fallbackRegion` so the
2770
+ * caller can surface the more actionable downstream error (e.g. the
2771
+ * `HeadBucket` 404 routed via `normalizeAwsError`).
2772
+ */
2773
+ async ensureClientForBucket() {
2774
+ if (this.clientResolved)
2775
+ return;
2776
+ if (this.resolveInFlight)
2777
+ return this.resolveInFlight;
2778
+ this.resolveInFlight = (async () => {
2779
+ try {
2780
+ const currentRegion = await this.s3Client.config.region();
2781
+ const fallbackRegion = typeof currentRegion === "string" ? currentRegion : void 0;
2782
+ const bucketRegion = await resolveBucketRegion(this.config.bucket, {
2783
+ ...this.clientOpts.profile && { profile: this.clientOpts.profile },
2784
+ ...this.clientOpts.credentials && { credentials: this.clientOpts.credentials },
2785
+ ...fallbackRegion && { fallbackRegion }
2786
+ });
2787
+ if (bucketRegion !== currentRegion) {
2788
+ this.logger.debug(
2789
+ `State bucket '${this.config.bucket}' is in '${bucketRegion}' (client was '${currentRegion}'); rebuilding S3 client.`
2790
+ );
2791
+ const oldClient = this.s3Client;
2792
+ this.s3Client = new S3Client4({
2793
+ region: bucketRegion,
2794
+ ...this.clientOpts.profile && { profile: this.clientOpts.profile },
2795
+ ...this.clientOpts.credentials && { credentials: this.clientOpts.credentials },
2796
+ // Suppress "Are you using a Stream of unknown length" warning,
2797
+ // matching the suppression in AwsClients.
2798
+ logger: { debug: () => {
2799
+ }, info: () => {
2800
+ }, warn: () => {
2801
+ }, error: () => {
2802
+ } }
2803
+ });
2804
+ oldClient.destroy();
2805
+ }
2806
+ this.clientResolved = true;
2807
+ } finally {
2808
+ this.resolveInFlight = null;
2809
+ }
2810
+ })();
2811
+ return this.resolveInFlight;
2812
+ }
2691
2813
  /**
2692
2814
  * Verify that the configured state bucket exists.
2693
2815
  *
2694
2816
  * Called early in deploy/destroy to fail fast before expensive work
2695
2817
  * (asset publishing, Docker builds) runs against a missing bucket.
2818
+ *
2819
+ * Errors are routed through {@link normalizeAwsError} so the AWS SDK v3
2820
+ * synthetic `UnknownError` (e.g. cross-region HEAD) becomes a concrete
2821
+ * "Bucket does not exist" / "Access denied" / "different region" message.
2696
2822
  */
2697
2823
  async verifyBucketExists() {
2824
+ await this.ensureClientForBucket();
2698
2825
  try {
2699
2826
  await this.s3Client.send(new HeadBucketCommand({ Bucket: this.config.bucket }));
2700
2827
  } catch (error) {
@@ -2704,9 +2831,13 @@ var S3StateBackend = class {
2704
2831
  `State bucket '${this.config.bucket}' does not exist. Run 'cdkd bootstrap' to create it, or specify an existing bucket via --state-bucket, CDKD_STATE_BUCKET, or cdk.json context.cdkd.stateBucket.`
2705
2832
  );
2706
2833
  }
2834
+ const normalized = normalizeAwsError(error, {
2835
+ bucket: this.config.bucket,
2836
+ operation: "HeadBucket"
2837
+ });
2707
2838
  throw new StateError(
2708
- `Failed to verify state bucket '${this.config.bucket}': ${error instanceof Error ? error.message : String(error)}`,
2709
- error instanceof Error ? error : void 0
2839
+ `Failed to verify state bucket '${this.config.bucket}': ${normalized.message}`,
2840
+ normalized
2710
2841
  );
2711
2842
  }
2712
2843
  }
@@ -2719,6 +2850,7 @@ var S3StateBackend = class {
2719
2850
  * state without forcing a write-through migration first.
2720
2851
  */
2721
2852
  async stateExists(stackName, region) {
2853
+ await this.ensureClientForBucket();
2722
2854
  const newKey = this.getStateKey(stackName, region);
2723
2855
  if (await this.headObject(newKey)) {
2724
2856
  return true;
@@ -2741,6 +2873,7 @@ var S3StateBackend = class {
2741
2873
  * preserve the quotes — they are required for `IfMatch` conditions.
2742
2874
  */
2743
2875
  async getState(stackName, region) {
2876
+ await this.ensureClientForBucket();
2744
2877
  const newKey = this.getStateKey(stackName, region);
2745
2878
  try {
2746
2879
  this.logger.debug(`Getting state for stack: ${stackName} (${region})`);
@@ -2800,6 +2933,7 @@ var S3StateBackend = class {
2800
2933
  * @returns New ETag (with quotes, e.g., `"abc123"`)
2801
2934
  */
2802
2935
  async saveState(stackName, region, state, options = {}) {
2936
+ await this.ensureClientForBucket();
2803
2937
  const newKey = this.getStateKey(stackName, region);
2804
2938
  const { expectedEtag, migrateLegacy } = options;
2805
2939
  const body = {
@@ -2855,9 +2989,13 @@ var S3StateBackend = class {
2855
2989
  `State has been modified by another process. Expected ETag: ${expectedEtag}, but state has changed.`
2856
2990
  );
2857
2991
  }
2992
+ const normalized = normalizeAwsError(error, {
2993
+ bucket: this.config.bucket,
2994
+ operation: "PutObject"
2995
+ });
2858
2996
  throw new StateError(
2859
- `Failed to save state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
2860
- error instanceof Error ? error : void 0
2997
+ `Failed to save state for stack '${stackName}' (${region}): ${normalized.message}`,
2998
+ normalized
2861
2999
  );
2862
3000
  }
2863
3001
  }
@@ -2869,6 +3007,7 @@ var S3StateBackend = class {
2869
3007
  * field is left alone.
2870
3008
  */
2871
3009
  async deleteState(stackName, region) {
3010
+ await this.ensureClientForBucket();
2872
3011
  try {
2873
3012
  this.logger.debug(`Deleting state: ${stackName} (${region})`);
2874
3013
  await this.s3Client.send(
@@ -2888,9 +3027,13 @@ var S3StateBackend = class {
2888
3027
  }
2889
3028
  this.logger.debug(`State deleted: ${stackName} (${region})`);
2890
3029
  } catch (error) {
3030
+ const normalized = normalizeAwsError(error, {
3031
+ bucket: this.config.bucket,
3032
+ operation: "DeleteObject"
3033
+ });
2891
3034
  throw new StateError(
2892
- `Failed to delete state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
2893
- error instanceof Error ? error : void 0
3035
+ `Failed to delete state for stack '${stackName}' (${region}): ${normalized.message}`,
3036
+ normalized
2894
3037
  );
2895
3038
  }
2896
3039
  }
@@ -2909,6 +3052,7 @@ var S3StateBackend = class {
2909
3052
  * shows up exactly once.
2910
3053
  */
2911
3054
  async listStacks() {
3055
+ await this.ensureClientForBucket();
2912
3056
  try {
2913
3057
  this.logger.debug("Listing all stacks");
2914
3058
  const prefix = `${this.config.prefix}/`;
@@ -2959,10 +3103,11 @@ var S3StateBackend = class {
2959
3103
  this.logger.debug(`Found ${refs.length} stack(s) across regions`);
2960
3104
  return refs;
2961
3105
  } catch (error) {
2962
- throw new StateError(
2963
- `Failed to list stacks: ${error instanceof Error ? error.message : String(error)}`,
2964
- error instanceof Error ? error : void 0
2965
- );
3106
+ const normalized = normalizeAwsError(error, {
3107
+ bucket: this.config.bucket,
3108
+ operation: "ListObjectsV2"
3109
+ });
3110
+ throw new StateError(`Failed to list stacks: ${normalized.message}`, normalized);
2966
3111
  }
2967
3112
  }
2968
3113
  /**
@@ -6260,7 +6405,7 @@ Error: ${err.message || "Unknown error"}`,
6260
6405
  import { InvokeCommand } from "@aws-sdk/client-lambda";
6261
6406
  import { PublishCommand } from "@aws-sdk/client-sns";
6262
6407
  import {
6263
- S3Client as S3Client5,
6408
+ S3Client as S3Client6,
6264
6409
  PutObjectCommand as PutObjectCommand4,
6265
6410
  GetObjectCommand as GetObjectCommand3,
6266
6411
  DeleteObjectCommand as DeleteObjectCommand3
@@ -6329,7 +6474,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
6329
6474
  setResponseBucket(bucket, bucketRegion) {
6330
6475
  this.responseBucket = bucket;
6331
6476
  if (bucketRegion) {
6332
- this.s3Client = new S3Client5(bucketRegion ? { region: bucketRegion } : {});
6477
+ this.s3Client = new S3Client6(bucketRegion ? { region: bucketRegion } : {});
6333
6478
  }
6334
6479
  }
6335
6480
  /**
@@ -8844,11 +8989,14 @@ export {
8844
8989
  SynthesisError,
8845
8990
  Synthesizer,
8846
8991
  TemplateParser,
8992
+ clearBucketRegionCache,
8847
8993
  formatError,
8848
8994
  getAwsClients,
8849
8995
  getLogger,
8850
8996
  isCdkdError,
8997
+ normalizeAwsError,
8851
8998
  resetAwsClients,
8999
+ resolveBucketRegion,
8852
9000
  setAwsClients,
8853
9001
  setLogger
8854
9002
  };