@go-to-k/cdkd 0.9.0 → 0.10.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
@@ -885,6 +885,40 @@ function withErrorHandling(fn) {
885
885
  }
886
886
  };
887
887
  }
888
+ function normalizeAwsError(err, context = {}) {
889
+ if (!(err instanceof Error)) {
890
+ return new Error(String(err));
891
+ }
892
+ const isUnknown = err.name === "Unknown" || err.message === "UnknownError";
893
+ if (!isUnknown)
894
+ return err;
895
+ const meta = err.$metadata;
896
+ const status = meta?.httpStatusCode;
897
+ const bucket = context.bucket ?? "<unknown bucket>";
898
+ const operation = context.operation ?? "operation";
899
+ switch (status) {
900
+ case 301: {
901
+ const responseHeaders = err.$response?.headers;
902
+ const region = responseHeaders?.["x-amz-bucket-region"] ?? responseHeaders?.["X-Amz-Bucket-Region"];
903
+ const where = region ? ` (in ${region})` : "";
904
+ return new Error(
905
+ `Bucket '${bucket}'${where} is in a different region than the client. cdkd resolves this automatically; if you see this message, please report it.`
906
+ );
907
+ }
908
+ case 403:
909
+ return new Error(
910
+ `Access denied to bucket '${bucket}'. Verify credentials and bucket policy.`
911
+ );
912
+ case 404:
913
+ return new Error(`Bucket '${bucket}' does not exist.`);
914
+ default: {
915
+ const statusStr = status !== void 0 ? `HTTP ${status}` : "unknown HTTP status";
916
+ return new Error(
917
+ `S3 error during ${operation} on '${bucket}' (${statusStr}). See CloudTrail for details.`
918
+ );
919
+ }
920
+ }
921
+ }
888
922
 
889
923
  // src/cli/commands/bootstrap.ts
890
924
  init_aws_clients();
@@ -995,7 +1029,7 @@ async function bootstrapCommand(options) {
995
1029
  if (err.name === "NotFound" || err.name === "NoSuchBucket") {
996
1030
  logger.debug(`Bucket ${bucketName} does not exist, will create`);
997
1031
  } else {
998
- throw error;
1032
+ throw normalizeAwsError(error, { bucket: bucketName, operation: "HeadBucket" });
999
1033
  }
1000
1034
  }
1001
1035
  if (bucketExists) {
@@ -3197,6 +3231,7 @@ var AssetPublisher = class {
3197
3231
 
3198
3232
  // src/state/s3-state-backend.ts
3199
3233
  import {
3234
+ S3Client as S3Client4,
3200
3235
  GetObjectCommand,
3201
3236
  PutObjectCommand as PutObjectCommand2,
3202
3237
  DeleteObjectCommand,
@@ -3210,15 +3245,44 @@ import {
3210
3245
  var STATE_SCHEMA_VERSION_LEGACY = 1;
3211
3246
  var STATE_SCHEMA_VERSION_CURRENT = 2;
3212
3247
 
3248
+ // src/utils/aws-region-resolver.ts
3249
+ import { GetBucketLocationCommand, S3Client as S3Client3 } from "@aws-sdk/client-s3";
3250
+ var cache = /* @__PURE__ */ new Map();
3251
+ async function resolveBucketRegion(bucketName, opts = {}) {
3252
+ const cached = cache.get(bucketName);
3253
+ if (cached)
3254
+ return cached;
3255
+ const promise = (async () => {
3256
+ const client = new S3Client3({
3257
+ region: "us-east-1",
3258
+ ...opts.profile && { profile: opts.profile },
3259
+ ...opts.credentials && { credentials: opts.credentials }
3260
+ });
3261
+ try {
3262
+ const response = await client.send(new GetBucketLocationCommand({ Bucket: bucketName }));
3263
+ return response.LocationConstraint || "us-east-1";
3264
+ } catch {
3265
+ return opts.fallbackRegion ?? "us-east-1";
3266
+ } finally {
3267
+ client.destroy();
3268
+ }
3269
+ })();
3270
+ cache.set(bucketName, promise);
3271
+ return promise;
3272
+ }
3273
+
3213
3274
  // src/state/s3-state-backend.ts
3214
3275
  var LEGACY_KEY_DEPTH = 2;
3215
3276
  var NEW_KEY_DEPTH = 3;
3216
3277
  var S3StateBackend = class {
3217
- constructor(s3Client, config) {
3278
+ constructor(s3Client, config, clientOpts = {}) {
3218
3279
  this.s3Client = s3Client;
3219
3280
  this.config = config;
3281
+ this.clientOpts = clientOpts;
3220
3282
  }
3221
3283
  logger = getLogger().child("S3StateBackend");
3284
+ clientResolved = false;
3285
+ resolveInFlight = null;
3222
3286
  /**
3223
3287
  * Get the new (region-scoped) S3 key for a stack's state file.
3224
3288
  */
@@ -3232,13 +3296,73 @@ var S3StateBackend = class {
3232
3296
  getLegacyStateKey(stackName) {
3233
3297
  return `${this.config.prefix}/${stackName}/state.json`;
3234
3298
  }
3299
+ /**
3300
+ * Resolve the state bucket's actual region and, if it differs from the
3301
+ * client's currently-configured region, replace the S3Client with one
3302
+ * pointed at the bucket's region.
3303
+ *
3304
+ * This is idempotent: subsequent calls return immediately. Concurrent
3305
+ * callers (e.g. when several public methods race during a parallel deploy)
3306
+ * share a single in-flight resolution promise so we never issue more than
3307
+ * one `GetBucketLocation` per backend.
3308
+ *
3309
+ * Errors from `GetBucketLocation` are deliberately swallowed by
3310
+ * `resolveBucketRegion` — the resolver returns `fallbackRegion` so the
3311
+ * caller can surface the more actionable downstream error (e.g. the
3312
+ * `HeadBucket` 404 routed via `normalizeAwsError`).
3313
+ */
3314
+ async ensureClientForBucket() {
3315
+ if (this.clientResolved)
3316
+ return;
3317
+ if (this.resolveInFlight)
3318
+ return this.resolveInFlight;
3319
+ this.resolveInFlight = (async () => {
3320
+ try {
3321
+ const currentRegion = await this.s3Client.config.region();
3322
+ const fallbackRegion = typeof currentRegion === "string" ? currentRegion : void 0;
3323
+ const bucketRegion = await resolveBucketRegion(this.config.bucket, {
3324
+ ...this.clientOpts.profile && { profile: this.clientOpts.profile },
3325
+ ...this.clientOpts.credentials && { credentials: this.clientOpts.credentials },
3326
+ ...fallbackRegion && { fallbackRegion }
3327
+ });
3328
+ if (bucketRegion !== currentRegion) {
3329
+ this.logger.debug(
3330
+ `State bucket '${this.config.bucket}' is in '${bucketRegion}' (client was '${currentRegion}'); rebuilding S3 client.`
3331
+ );
3332
+ const oldClient = this.s3Client;
3333
+ this.s3Client = new S3Client4({
3334
+ region: bucketRegion,
3335
+ ...this.clientOpts.profile && { profile: this.clientOpts.profile },
3336
+ ...this.clientOpts.credentials && { credentials: this.clientOpts.credentials },
3337
+ // Suppress "Are you using a Stream of unknown length" warning,
3338
+ // matching the suppression in AwsClients.
3339
+ logger: { debug: () => {
3340
+ }, info: () => {
3341
+ }, warn: () => {
3342
+ }, error: () => {
3343
+ } }
3344
+ });
3345
+ oldClient.destroy();
3346
+ }
3347
+ this.clientResolved = true;
3348
+ } finally {
3349
+ this.resolveInFlight = null;
3350
+ }
3351
+ })();
3352
+ return this.resolveInFlight;
3353
+ }
3235
3354
  /**
3236
3355
  * Verify that the configured state bucket exists.
3237
3356
  *
3238
3357
  * Called early in deploy/destroy to fail fast before expensive work
3239
3358
  * (asset publishing, Docker builds) runs against a missing bucket.
3359
+ *
3360
+ * Errors are routed through {@link normalizeAwsError} so the AWS SDK v3
3361
+ * synthetic `UnknownError` (e.g. cross-region HEAD) becomes a concrete
3362
+ * "Bucket does not exist" / "Access denied" / "different region" message.
3240
3363
  */
3241
3364
  async verifyBucketExists() {
3365
+ await this.ensureClientForBucket();
3242
3366
  try {
3243
3367
  await this.s3Client.send(new HeadBucketCommand2({ Bucket: this.config.bucket }));
3244
3368
  } catch (error) {
@@ -3248,9 +3372,13 @@ var S3StateBackend = class {
3248
3372
  `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.`
3249
3373
  );
3250
3374
  }
3375
+ const normalized = normalizeAwsError(error, {
3376
+ bucket: this.config.bucket,
3377
+ operation: "HeadBucket"
3378
+ });
3251
3379
  throw new StateError(
3252
- `Failed to verify state bucket '${this.config.bucket}': ${error instanceof Error ? error.message : String(error)}`,
3253
- error instanceof Error ? error : void 0
3380
+ `Failed to verify state bucket '${this.config.bucket}': ${normalized.message}`,
3381
+ normalized
3254
3382
  );
3255
3383
  }
3256
3384
  }
@@ -3263,6 +3391,7 @@ var S3StateBackend = class {
3263
3391
  * state without forcing a write-through migration first.
3264
3392
  */
3265
3393
  async stateExists(stackName, region) {
3394
+ await this.ensureClientForBucket();
3266
3395
  const newKey = this.getStateKey(stackName, region);
3267
3396
  if (await this.headObject(newKey)) {
3268
3397
  return true;
@@ -3285,6 +3414,7 @@ var S3StateBackend = class {
3285
3414
  * preserve the quotes — they are required for `IfMatch` conditions.
3286
3415
  */
3287
3416
  async getState(stackName, region) {
3417
+ await this.ensureClientForBucket();
3288
3418
  const newKey = this.getStateKey(stackName, region);
3289
3419
  try {
3290
3420
  this.logger.debug(`Getting state for stack: ${stackName} (${region})`);
@@ -3344,6 +3474,7 @@ var S3StateBackend = class {
3344
3474
  * @returns New ETag (with quotes, e.g., `"abc123"`)
3345
3475
  */
3346
3476
  async saveState(stackName, region, state, options = {}) {
3477
+ await this.ensureClientForBucket();
3347
3478
  const newKey = this.getStateKey(stackName, region);
3348
3479
  const { expectedEtag, migrateLegacy } = options;
3349
3480
  const body = {
@@ -3399,9 +3530,13 @@ var S3StateBackend = class {
3399
3530
  `State has been modified by another process. Expected ETag: ${expectedEtag}, but state has changed.`
3400
3531
  );
3401
3532
  }
3533
+ const normalized = normalizeAwsError(error, {
3534
+ bucket: this.config.bucket,
3535
+ operation: "PutObject"
3536
+ });
3402
3537
  throw new StateError(
3403
- `Failed to save state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3404
- error instanceof Error ? error : void 0
3538
+ `Failed to save state for stack '${stackName}' (${region}): ${normalized.message}`,
3539
+ normalized
3405
3540
  );
3406
3541
  }
3407
3542
  }
@@ -3413,6 +3548,7 @@ var S3StateBackend = class {
3413
3548
  * field is left alone.
3414
3549
  */
3415
3550
  async deleteState(stackName, region) {
3551
+ await this.ensureClientForBucket();
3416
3552
  try {
3417
3553
  this.logger.debug(`Deleting state: ${stackName} (${region})`);
3418
3554
  await this.s3Client.send(
@@ -3432,9 +3568,13 @@ var S3StateBackend = class {
3432
3568
  }
3433
3569
  this.logger.debug(`State deleted: ${stackName} (${region})`);
3434
3570
  } catch (error) {
3571
+ const normalized = normalizeAwsError(error, {
3572
+ bucket: this.config.bucket,
3573
+ operation: "DeleteObject"
3574
+ });
3435
3575
  throw new StateError(
3436
- `Failed to delete state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3437
- error instanceof Error ? error : void 0
3576
+ `Failed to delete state for stack '${stackName}' (${region}): ${normalized.message}`,
3577
+ normalized
3438
3578
  );
3439
3579
  }
3440
3580
  }
@@ -3453,6 +3593,7 @@ var S3StateBackend = class {
3453
3593
  * shows up exactly once.
3454
3594
  */
3455
3595
  async listStacks() {
3596
+ await this.ensureClientForBucket();
3456
3597
  try {
3457
3598
  this.logger.debug("Listing all stacks");
3458
3599
  const prefix = `${this.config.prefix}/`;
@@ -3503,10 +3644,11 @@ var S3StateBackend = class {
3503
3644
  this.logger.debug(`Found ${refs.length} stack(s) across regions`);
3504
3645
  return refs;
3505
3646
  } catch (error) {
3506
- throw new StateError(
3507
- `Failed to list stacks: ${error instanceof Error ? error.message : String(error)}`,
3508
- error instanceof Error ? error : void 0
3509
- );
3647
+ const normalized = normalizeAwsError(error, {
3648
+ bucket: this.config.bucket,
3649
+ operation: "ListObjectsV2"
3650
+ });
3651
+ throw new StateError(`Failed to list stacks: ${normalized.message}`, normalized);
3510
3652
  }
3511
3653
  }
3512
3654
  /**
@@ -6804,7 +6946,7 @@ Error: ${err.message || "Unknown error"}`,
6804
6946
  import { InvokeCommand } from "@aws-sdk/client-lambda";
6805
6947
  import { PublishCommand } from "@aws-sdk/client-sns";
6806
6948
  import {
6807
- S3Client as S3Client5,
6949
+ S3Client as S3Client6,
6808
6950
  PutObjectCommand as PutObjectCommand4,
6809
6951
  GetObjectCommand as GetObjectCommand3,
6810
6952
  DeleteObjectCommand as DeleteObjectCommand3
@@ -6873,7 +7015,7 @@ var CustomResourceProvider = class _CustomResourceProvider {
6873
7015
  setResponseBucket(bucket, bucketRegion) {
6874
7016
  this.responseBucket = bucket;
6875
7017
  if (bucketRegion) {
6876
- this.s3Client = new S3Client5(bucketRegion ? { region: bucketRegion } : {});
7018
+ this.s3Client = new S3Client6(bucketRegion ? { region: bucketRegion } : {});
6877
7019
  }
6878
7020
  }
6879
7021
  /**
@@ -25798,7 +25940,7 @@ var CodeBuildProvider = class {
25798
25940
  const tags = properties["Tags"];
25799
25941
  const envVars = environment?.["EnvironmentVariables"];
25800
25942
  const cfnCache = properties["Cache"];
25801
- const cache = cfnCache ? {
25943
+ const cache2 = cfnCache ? {
25802
25944
  type: cfnCache["Type"],
25803
25945
  location: cfnCache["Location"],
25804
25946
  modes: cfnCache["Modes"]
@@ -25885,7 +26027,7 @@ var CodeBuildProvider = class {
25885
26027
  timeoutInMinutes: properties["TimeoutInMinutes"],
25886
26028
  queuedTimeoutInMinutes: properties["QueuedTimeoutInMinutes"],
25887
26029
  encryptionKey: properties["EncryptionKey"],
25888
- cache,
26030
+ cache: cache2,
25889
26031
  vpcConfig,
25890
26032
  logsConfig,
25891
26033
  concurrentBuildLimit: properties["ConcurrentBuildLimit"],
@@ -28429,10 +28571,17 @@ async function deployCommand(stacks, options) {
28429
28571
  ...options.profile && { profile: options.profile }
28430
28572
  });
28431
28573
  setAwsClients(awsClients);
28432
- const preflightStateBackend = new S3StateBackend(awsClients.s3, {
28433
- bucket: stateBucket,
28434
- prefix: options.statePrefix
28435
- });
28574
+ const preflightStateBackend = new S3StateBackend(
28575
+ awsClients.s3,
28576
+ {
28577
+ bucket: stateBucket,
28578
+ prefix: options.statePrefix
28579
+ },
28580
+ {
28581
+ region,
28582
+ ...options.profile && { profile: options.profile }
28583
+ }
28584
+ );
28436
28585
  await preflightStateBackend.verifyBucketExists();
28437
28586
  let deployInterrupted = false;
28438
28587
  const topLevelSigintHandler = () => {
@@ -28583,7 +28732,10 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
28583
28732
  region: baseRegion,
28584
28733
  ...options.profile && { profile: options.profile }
28585
28734
  });
28586
- const stackStateBackend = new S3StateBackend(stateS3Client.s3, stateConfig);
28735
+ const stackStateBackend = new S3StateBackend(stateS3Client.s3, stateConfig, {
28736
+ region: baseRegion,
28737
+ ...options.profile && { profile: options.profile }
28738
+ });
28587
28739
  const stackLockManager = new LockManager(stateS3Client.s3, stateConfig);
28588
28740
  const stackProviderRegistry = new ProviderRegistry();
28589
28741
  registerAllProviders(stackProviderRegistry);
@@ -28764,7 +28916,10 @@ async function diffCommand(stacks, options) {
28764
28916
  bucket: stateBucket,
28765
28917
  prefix: options.statePrefix
28766
28918
  };
28767
- const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
28919
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
28920
+ region,
28921
+ ...options.profile && { profile: options.profile }
28922
+ });
28768
28923
  const diffCalculator = new DiffCalculator();
28769
28924
  const intrinsicResolver = new IntrinsicFunctionResolver(region);
28770
28925
  for (const stackInfo of targetStacks) {
@@ -28884,7 +29039,10 @@ async function destroyCommand(stackArgs, options) {
28884
29039
  bucket: stateBucket,
28885
29040
  prefix: options.statePrefix
28886
29041
  };
28887
- const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
29042
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
29043
+ ...options.region && { region: options.region },
29044
+ ...options.profile && { profile: options.profile }
29045
+ });
28888
29046
  await stateBackend.verifyBucketExists();
28889
29047
  const lockManager = new LockManager(awsClients.s3, stateConfig);
28890
29048
  const dagBuilder = new DagBuilder();
@@ -29333,7 +29491,10 @@ async function setupStateBackend(options) {
29333
29491
  const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
29334
29492
  const prefix = options.statePrefix;
29335
29493
  const stateConfig = { bucket, prefix };
29336
- const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
29494
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
29495
+ region,
29496
+ ...options.profile && { profile: options.profile }
29497
+ });
29337
29498
  const lockManager = new LockManager(awsClients.s3, stateConfig);
29338
29499
  await stateBackend.verifyBucketExists();
29339
29500
  return {
@@ -29737,7 +29898,7 @@ function reorderArgs(argv) {
29737
29898
  }
29738
29899
  async function main() {
29739
29900
  const program = new Command10();
29740
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.9.0");
29901
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.10.0");
29741
29902
  program.addCommand(createBootstrapCommand());
29742
29903
  program.addCommand(createSynthCommand());
29743
29904
  program.addCommand(createListCommand());