@go-to-k/cdkd 0.255.0 → 0.257.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.
@@ -8216,128 +8216,452 @@ function normalizeAwsTagsToCfn(tags) {
8216
8216
  }
8217
8217
 
8218
8218
  //#endregion
8219
- //#region src/provisioning/providers/wafv2-provider.ts
8219
+ //#region src/deployment/retryable-errors.ts
8220
8220
  /**
8221
- * Translate the empty-string placeholder `readCurrentState` emits for an
8222
- * absent `Description` to `undefined` before shipping it to AWS.
8221
+ * Patterns that mark an AWS error as a transient/retryable failure.
8222
+ * Each entry is a substring match against the error message; all of these
8223
+ * are situations where the same call typically succeeds after a short delay
8224
+ * because of eventual consistency or just-created-dependency propagation.
8225
+ */
8226
+ const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
8227
+ "cannot be assumed",
8228
+ "Firehose is unable to assume role",
8229
+ "is unable to assume provided role",
8230
+ "role defined for the function",
8231
+ "not authorized to perform",
8232
+ "execution role",
8233
+ "trust policy",
8234
+ "Role validation failed",
8235
+ "does not have required permissions",
8236
+ "Trusted Entity",
8237
+ "currently in the following state: Pending",
8238
+ "has dependencies and cannot be deleted",
8239
+ "can't be deleted since it has",
8240
+ "DependencyViolation",
8241
+ "does not exist",
8242
+ "Schema is currently being altered",
8243
+ "Invalid principal in policy",
8244
+ "Policy Error: PrincipalNotFound",
8245
+ "Invalid value for the parameter Policy",
8246
+ "required permissions for: ENHANCED_MONITORING",
8247
+ "Caught ServiceAccessDeniedException",
8248
+ "permissions required to assume the role",
8249
+ "authorized to assume the provided role",
8250
+ "conflicting conditional operation",
8251
+ "scheduled for deletion",
8252
+ "Cannot access stream",
8253
+ "Please ensure the role can perform",
8254
+ "KMS key is invalid for CreateGrant",
8255
+ "Policy contains a statement with one or more invalid principals",
8256
+ "Invalid IAM Instance Profile",
8257
+ "Invalid InstanceProfile",
8258
+ "Failed to authorize instance profile",
8259
+ "Could not deliver test message",
8260
+ "wait 60 seconds",
8261
+ "concurrent update operation",
8262
+ "because it is in use",
8263
+ "Rate exceeded"
8264
+ ];
8265
+ /**
8266
+ * HTTP status codes that always indicate a transient failure worth retrying.
8267
+ * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
8268
+ */
8269
+ const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
8270
+ /**
8271
+ * AWS SDK v3 canonical throttling error names. Mirrors
8272
+ * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
8273
+ * error (or wrapped cause) whose `name` is one of these is a transient rate-
8274
+ * limit rejection worth retrying with backoff. Detecting by NAME is more
8275
+ * robust than by HTTP status because most AWS throttles surface as HTTP 400
8276
+ * (not 429) with the throttling signal carried only in the error code / name
8277
+ * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
8278
+ */
8279
+ const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
8280
+ "BandwidthLimitExceeded",
8281
+ "EC2ThrottledException",
8282
+ "LimitExceededException",
8283
+ "PriorRequestNotComplete",
8284
+ "ProvisionedThroughputExceededException",
8285
+ "RequestLimitExceeded",
8286
+ "RequestThrottled",
8287
+ "RequestThrottledException",
8288
+ "SlowDown",
8289
+ "ThrottledException",
8290
+ "Throttling",
8291
+ "ThrottlingException",
8292
+ "TooManyRequestsException",
8293
+ "TransactionInProgressException"
8294
+ ]);
8295
+ /**
8296
+ * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
8297
+ * signal — either an AWS SDK v3 throttling error `name`
8298
+ * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
8299
+ * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
8223
8300
  *
8224
- * `readCurrentState` always-emits `Description: ''` on a WebACL with no
8225
- * description (the comparator's top-level walk is state-keys-only, so
8226
- * the placeholder is required to detect a console-side description add).
8227
- * AWS WAFv2 `CreateWebACL` / `UpdateWebACL` reject `Description: ''`
8228
- * with `Member must have length greater than or equal to 1` (min 1 / max
8229
- * 256). On `cdkd drift --revert` the placeholder would round-trip
8230
- * through `update()` and surface as a hard AWS rejection, so we sanitize
8231
- * the wire-layer payload while keeping the read-side placeholder
8232
- * intact. This is the Class 2 pattern from
8233
- * `docs/provider-development.md § 3b`.
8301
+ * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
8302
+ * typically one cause-link deep; the bounded walk also tolerates SDK errors
8303
+ * that nest a `$response`/cause without exploding on a cyclic chain.
8304
+ *
8305
+ * BOTH signals are checked at EVERY depth. An earlier version checked the name
8306
+ * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
8307
+ * links deep was missed. This is the single shared cause-walk: the read-only
8308
+ * import tag walk (`src/provisioning/import-tag-walk.ts`) composes its own
8309
+ * classifier on top of this function rather than re-implementing the traversal,
8310
+ * so the two cannot drift apart again.
8234
8311
  */
8235
- function sanitizeDescription(value) {
8236
- if (value === void 0 || value === null) return void 0;
8237
- if (typeof value === "string" && value.length === 0) return void 0;
8238
- return value;
8312
+ function isThrottlingError(error) {
8313
+ let current = error;
8314
+ for (let depth = 0; depth < 5 && current != null; depth++) {
8315
+ const name = current.name;
8316
+ if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
8317
+ const status = current.$metadata?.httpStatusCode;
8318
+ if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
8319
+ current = current.cause;
8320
+ }
8321
+ return false;
8239
8322
  }
8240
8323
  /**
8241
- * Parse WAFv2 WebACL ARN to extract Id, Name, and Scope.
8242
- *
8243
- * ARN format:
8244
- * arn:aws:wafv2:{region}:{account}:regional/webacl/{name}/{id}
8245
- * arn:aws:wafv2:{region}:{account}:global/webacl/{name}/{id}
8324
+ * Determine whether an AWS error should be retried.
8246
8325
  *
8247
- * A short / malformed ARN yields `undefined` for `name` / `id` (the path
8248
- * segments simply are not there) callers must guard. `scope` is always
8249
- * defined (anything not `global` maps to `REGIONAL`).
8326
+ * Checks (in order):
8327
+ * 1. Rate-limit signal on the error or any wrapped cause throttling error
8328
+ * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
8329
+ * 429, so the name check carries most of the weight). See
8330
+ * {@link isThrottlingError}.
8331
+ * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
8250
8332
  */
8251
- function parseWebACLArn(arn) {
8252
- const segments = arn.split(":").slice(5).join(":").split("/");
8253
- const scopeRaw = segments[0];
8254
- const name = segments[2];
8255
- return {
8256
- id: segments[3],
8257
- name,
8258
- scope: scopeRaw === "global" ? "CLOUDFRONT" : "REGIONAL"
8259
- };
8333
+ function isRetryableTransientError(error, message) {
8334
+ if (isThrottlingError(error)) return true;
8335
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
8260
8336
  }
8337
+
8338
+ //#endregion
8339
+ //#region src/deployment/retry.ts
8261
8340
  /**
8262
- * AWS WAFv2 WebACL Provider
8341
+ * Retry helper for resource provisioning operations that hit transient
8342
+ * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
8343
+ * dependency violations, etc.).
8263
8344
  *
8264
- * Implements resource provisioning for AWS::WAFv2::WebACL using the WAFv2 SDK.
8265
- * WHY: WAFv2 CreateWebACL is synchronous - the CC API adds unnecessary polling
8266
- * overhead for an operation that completes immediately.
8267
- * This SDK provider eliminates that polling and returns instantly.
8345
+ * Extracted from DeployEngine so the backoff schedule can be unit-tested
8346
+ * in isolation. The retryable-error classifier itself lives in
8347
+ * `./retryable-errors.ts`.
8268
8348
  */
8269
- var WAFv2WebACLProvider = class {
8270
- wafv2Client;
8271
- providerRegion = process.env["AWS_REGION"];
8272
- logger = getLogger().child("WAFv2WebACLProvider");
8273
- handledProperties = /* @__PURE__ */ new Map([["AWS::WAFv2::WebACL", /* @__PURE__ */ new Set([
8274
- "Name",
8275
- "Scope",
8276
- "Tags",
8277
- "DefaultAction",
8278
- "Description",
8279
- "Rules",
8280
- "VisibilityConfig",
8281
- "CustomResponseBodies",
8282
- "CaptchaConfig",
8283
- "ChallengeConfig",
8284
- "TokenDomains",
8285
- "AssociationConfig"
8286
- ])]]);
8287
- getClient() {
8288
- if (!this.wafv2Client) this.wafv2Client = new WAFV2Client(this.providerRegion ? { region: this.providerRegion } : {});
8289
- return this.wafv2Client;
8290
- }
8291
- /**
8292
- * Create a WAFv2 WebACL
8293
- */
8294
- async create(logicalId, resourceType, properties) {
8295
- this.logger.debug(`Creating WAFv2 WebACL ${logicalId}`);
8296
- const name = properties["Name"] || generateResourceName(logicalId, { maxLength: 128 });
8297
- const scope = properties["Scope"] || "REGIONAL";
8298
- try {
8299
- const tags = [];
8300
- if (properties["Tags"]) {
8301
- const tagList = properties["Tags"];
8302
- for (const tag of tagList) tags.push({
8303
- Key: tag.Key,
8304
- Value: tag.Value
8305
- });
8306
- }
8307
- const summary = (await this.getClient().send(new CreateWebACLCommand({
8308
- Name: name,
8309
- Scope: scope,
8310
- DefaultAction: properties["DefaultAction"],
8311
- Description: sanitizeDescription(properties["Description"]),
8312
- Rules: properties["Rules"] || [],
8313
- VisibilityConfig: properties["VisibilityConfig"],
8314
- ...tags.length > 0 && { Tags: tags },
8315
- CustomResponseBodies: properties["CustomResponseBodies"],
8316
- CaptchaConfig: properties["CaptchaConfig"],
8317
- ChallengeConfig: properties["ChallengeConfig"],
8318
- TokenDomains: properties["TokenDomains"],
8319
- AssociationConfig: properties["AssociationConfig"]
8320
- }))).Summary;
8321
- if (!summary?.ARN || !summary?.Id) throw new Error("CreateWebACL did not return Summary with ARN and Id");
8322
- this.logger.debug(`Successfully created WAFv2 WebACL ${logicalId}: ${summary.ARN}`);
8323
- return {
8324
- physicalId: summary.ARN,
8325
- attributes: {
8326
- Arn: summary.ARN,
8327
- Id: summary.Id,
8328
- LabelNamespace: summary["LabelNamespace"]
8329
- }
8330
- };
8331
- } catch (error) {
8332
- if (error instanceof ProvisioningError) throw error;
8333
- const cause = error instanceof Error ? error : void 0;
8334
- throw new ProvisioningError(`Failed to create WAFv2 WebACL ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
8349
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8350
+ /**
8351
+ * Run `operation`, retrying transient failures with exponential backoff
8352
+ * capped at `maxDelayMs`.
8353
+ *
8354
+ * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
8355
+ * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
8356
+ *
8357
+ * Non-retryable errors are rethrown immediately. The transient-error
8358
+ * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
8359
+ */
8360
+ async function withRetry(operation, logicalId, opts = {}) {
8361
+ const maxRetries = opts.maxRetries ?? 8;
8362
+ const initialDelayMs = opts.initialDelayMs ?? 1e3;
8363
+ const maxDelayMs = opts.maxDelayMs ?? 8e3;
8364
+ const sleep = opts.sleep ?? defaultSleep;
8365
+ let lastError;
8366
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
8367
+ return await operation();
8368
+ } catch (error) {
8369
+ lastError = error;
8370
+ const message = error instanceof Error ? error.message : String(error);
8371
+ if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
8372
+ const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
8373
+ opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
8374
+ for (let waited = 0; waited < delay; waited += 1e3) {
8375
+ if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
8376
+ await sleep(Math.min(1e3, delay - waited));
8335
8377
  }
8336
8378
  }
8337
- /**
8338
- * Update a WAFv2 WebACL
8339
- *
8340
- * Name and Scope are immutable - changes to those require replacement.
8379
+ throw lastError;
8380
+ }
8381
+
8382
+ //#endregion
8383
+ //#region src/provisioning/import-tag-walk.ts
8384
+ /**
8385
+ * Shared, throttle-tolerant `aws:cdk:path` tag walk for `ResourceProvider.import`.
8386
+ *
8387
+ * Providers that adopt a resource without an explicit name property fall back
8388
+ * to step 3 of the lookup order documented in `./import-helpers.ts`: enumerate
8389
+ * the service's `List*`/`Describe*` pages, then issue ONE per-candidate read
8390
+ * (`DescribeX` / `ListTagsForResource`) to obtain the tag set — the list
8391
+ * summaries usually do not carry tags. That is an inherent **N+1** read
8392
+ * pattern: an account with many resources of the type produces one API call
8393
+ * per candidate in a tight loop, which is exactly the shape AWS rate-limits.
8394
+ *
8395
+ * Every provider previously hand-rolled this loop with NO backoff, so a single
8396
+ * throttled `Describe*` aborted the whole `cdkd import` run. This helper
8397
+ * centralises the loop and wraps BOTH the list page fetch and the per-candidate
8398
+ * describe in the deploy engine's `withRetry` with exponential backoff.
8399
+ *
8400
+ * ## Why a narrower classifier than `isRetryableTransientError`
8401
+ *
8402
+ * The deploy engine's classifier is tuned for the WRITE path: it also treats
8403
+ * eventual-consistency phrasings like `does not exist` / `not authorized to
8404
+ * perform` as transient, because a just-created dependency legitimately needs a
8405
+ * moment to propagate. On a read-only import walk those messages mean the
8406
+ * opposite — the candidate really is gone, or the caller's credentials really
8407
+ * lack the permission — and retrying them burns the full backoff budget per
8408
+ * candidate before surfacing the true error.
8409
+ *
8410
+ * So the walk retries throttling ONLY, reusing the deploy engine's throttle
8411
+ * tables verbatim ({@link THROTTLING_ERROR_NAMES} /
8412
+ * {@link RETRYABLE_HTTP_STATUS_CODES}) rather than maintaining a second copy.
8413
+ *
8414
+ * ## Batching
8415
+ *
8416
+ * Batched tag reads are deliberately NOT modelled here: the services this
8417
+ * helper currently serves (EMR `DescribeCluster`, DocDB
8418
+ * `ListTagsForResource`) expose only single-resource reads. A service that
8419
+ * genuinely offers a batch API (e.g. CodeCommit `BatchGetRepositories`) can
8420
+ * satisfy several candidates from one call inside its own `describe` callback,
8421
+ * or bypass the helper entirely.
8422
+ */
8423
+ /** Max number of retries after the first attempt, per API call in the walk. */
8424
+ const DEFAULT_MAX_RETRIES = 5;
8425
+ /** Initial backoff; each retry doubles it up to {@link DEFAULT_MAX_DELAY_MS}. */
8426
+ const DEFAULT_INITIAL_DELAY_MS = 500;
8427
+ /** Cap for the per-retry delay (0.5s -> 1s -> 2s -> 4s -> 5s, ~12.5s total). */
8428
+ const DEFAULT_MAX_DELAY_MS = 5e3;
8429
+ /**
8430
+ * Wall-clock ceiling for the WHOLE walk (all pages + all candidates), not just
8431
+ * one call. Backoff is per-call, so without this a sustained throttle against a
8432
+ * large account degrades into `(pages + candidates) x ~12.5s` of near-silent
8433
+ * retrying — ~42 minutes for 200 candidates, with no way to tell a slow walk
8434
+ * from a hung one. 10 minutes is generous for a healthy walk (a 200-candidate
8435
+ * DocDB account completes in seconds when AWS is not throttling) and bounds the
8436
+ * pathological case to something a user will wait through.
8437
+ */
8438
+ const DEFAULT_MAX_WALK_MS = 10 * 6e4;
8439
+ /**
8440
+ * Ceiling on pages fetched. A service that returns a non-advancing pagination
8441
+ * token (a bug, or a marker cdkd echoes back wrongly) would otherwise spin
8442
+ * forever. This is now the shared path every migrated provider runs on, so the
8443
+ * guard belongs here rather than in each caller.
8444
+ */
8445
+ const DEFAULT_MAX_PAGES = 1e3;
8446
+ /**
8447
+ * Whether an error hit during the read-only tag walk is a rate-limit rejection
8448
+ * worth backing off on.
8449
+ *
8450
+ * Delegates the error + `.cause` chain traversal to the deploy engine's
8451
+ * {@link isThrottlingError} (throttling error names + retryable HTTP statuses,
8452
+ * at every cause depth) and adds the canonical `Rate exceeded` message, which
8453
+ * several services return with an HTTP 400 and a service-specific error name.
8454
+ *
8455
+ * This is deliberately NARROWER than `isRetryableTransientError`: that
8456
+ * classifier also treats write-path eventual-consistency phrasings (`does not
8457
+ * exist`, `not authorized to perform`) as transient, because a just-created
8458
+ * dependency legitimately needs a moment to propagate. On a read-only walk
8459
+ * those mean the candidate really is gone or the credentials really lack the
8460
+ * permission, and retrying them burns the full backoff budget per candidate
8461
+ * before surfacing the true error.
8462
+ *
8463
+ * Exported for direct unit testing and for providers whose tag walk cannot use
8464
+ * {@link importTagWalk} verbatim.
8465
+ */
8466
+ function isThrottlingLikeError(error, message) {
8467
+ return isThrottlingError(error) || message.includes("Rate exceeded");
8468
+ }
8469
+ /**
8470
+ * Module-level test hooks for the walk's backoff.
8471
+ *
8472
+ * Providers do not expose a retry option on `import()`, so their wiring tests
8473
+ * cannot inject `retry.sleep` per call — without this hook every throttle-path
8474
+ * wiring test pays a real 0.5s+ backoff wait. Tests set `sleep` to a resolved
8475
+ * no-op in `beforeEach` (and clear it in `afterEach`); a per-call
8476
+ * `retry.sleep` still wins when supplied. Never set this in production code.
8477
+ */
8478
+ const importTagWalkTestHooks = {};
8479
+ /** Thrown when the walk exceeds its wall-clock budget or page ceiling. */
8480
+ var ImportTagWalkLimitError = class extends Error {
8481
+ constructor(message) {
8482
+ super(message);
8483
+ this.name = "ImportTagWalkLimitError";
8484
+ }
8485
+ };
8486
+ /**
8487
+ * Paginate `listPage`, `describe` each candidate, and return the first one
8488
+ * whose tags carry the requested `aws:cdk:path`. Returns `null` when no
8489
+ * candidate matches (or when `cdkPath` is empty).
8490
+ *
8491
+ * Both callbacks are individually retried with exponential backoff on
8492
+ * throttling errors, so one rate-limited call mid-walk no longer aborts the
8493
+ * whole import.
8494
+ */
8495
+ async function importTagWalk(options) {
8496
+ const { cdkPath, listPage, describe, tagsOf } = options;
8497
+ if (!cdkPath) return null;
8498
+ const logicalId = options.logicalId ?? "import";
8499
+ const logger = options.retry?.logger ?? getLogger();
8500
+ const maxWalkMs = options.retry?.maxWalkMs ?? DEFAULT_MAX_WALK_MS;
8501
+ const maxPages = options.retry?.maxPages ?? DEFAULT_MAX_PAGES;
8502
+ const sleep = options.retry?.sleep ?? importTagWalkTestHooks.sleep;
8503
+ const retryOpts = {
8504
+ maxRetries: options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
8505
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS,
8506
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
8507
+ isRetryable: (message, error) => isThrottlingLikeError(error, message),
8508
+ logger,
8509
+ ...options.retry?.isInterrupted && { isInterrupted: options.retry.isInterrupted },
8510
+ ...options.retry?.onInterrupted && { onInterrupted: options.retry.onInterrupted },
8511
+ ...sleep && { sleep }
8512
+ };
8513
+ const startedAt = Date.now();
8514
+ const assertWithinBudget = (stage) => {
8515
+ const elapsed = Date.now() - startedAt;
8516
+ if (elapsed >= maxWalkMs) throw new ImportTagWalkLimitError(`Timed out looking up ${logicalId} by its aws:cdk:path tag after ${Math.round(elapsed / 1e3)}s (limit ${Math.round(maxWalkMs / 1e3)}s, while ${stage}). AWS is likely throttling the lookup; retry, or pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
8517
+ };
8518
+ let marker;
8519
+ let pages = 0;
8520
+ do {
8521
+ assertWithinBudget("listing candidates");
8522
+ if (++pages > maxPages) throw new ImportTagWalkLimitError(`Gave up looking up ${logicalId} by its aws:cdk:path tag after ${maxPages} pages (the service may be returning a non-advancing pagination token). Pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
8523
+ const currentMarker = marker;
8524
+ const page = await withRetry(() => listPage(currentMarker), logicalId, retryOpts);
8525
+ for (const summary of page.items ?? []) {
8526
+ assertWithinBudget("reading candidate tags");
8527
+ const detail = await withRetry(() => describe(summary), logicalId, retryOpts);
8528
+ if (detail === void 0) {
8529
+ logger.debug(` ↷ Skipped an ${logicalId} import candidate: its detail lookup returned no result`);
8530
+ continue;
8531
+ }
8532
+ if (matchesCdkPath(tagsOf(detail, summary), cdkPath)) return {
8533
+ summary,
8534
+ detail
8535
+ };
8536
+ }
8537
+ marker = page.nextMarker;
8538
+ } while (marker);
8539
+ return null;
8540
+ }
8541
+
8542
+ //#endregion
8543
+ //#region src/provisioning/providers/wafv2-provider.ts
8544
+ /**
8545
+ * Translate the empty-string placeholder `readCurrentState` emits for an
8546
+ * absent `Description` to `undefined` before shipping it to AWS.
8547
+ *
8548
+ * `readCurrentState` always-emits `Description: ''` on a WebACL with no
8549
+ * description (the comparator's top-level walk is state-keys-only, so
8550
+ * the placeholder is required to detect a console-side description add).
8551
+ * AWS WAFv2 `CreateWebACL` / `UpdateWebACL` reject `Description: ''`
8552
+ * with `Member must have length greater than or equal to 1` (min 1 / max
8553
+ * 256). On `cdkd drift --revert` the placeholder would round-trip
8554
+ * through `update()` and surface as a hard AWS rejection, so we sanitize
8555
+ * the wire-layer payload while keeping the read-side placeholder
8556
+ * intact. This is the Class 2 pattern from
8557
+ * `docs/provider-development.md § 3b`.
8558
+ */
8559
+ function sanitizeDescription(value) {
8560
+ if (value === void 0 || value === null) return void 0;
8561
+ if (typeof value === "string" && value.length === 0) return void 0;
8562
+ return value;
8563
+ }
8564
+ /**
8565
+ * Parse WAFv2 WebACL ARN to extract Id, Name, and Scope.
8566
+ *
8567
+ * ARN format:
8568
+ * arn:aws:wafv2:{region}:{account}:regional/webacl/{name}/{id}
8569
+ * arn:aws:wafv2:{region}:{account}:global/webacl/{name}/{id}
8570
+ *
8571
+ * A short / malformed ARN yields `undefined` for `name` / `id` (the path
8572
+ * segments simply are not there) — callers must guard. `scope` is always
8573
+ * defined (anything not `global` maps to `REGIONAL`).
8574
+ */
8575
+ function parseWebACLArn(arn) {
8576
+ const segments = arn.split(":").slice(5).join(":").split("/");
8577
+ const scopeRaw = segments[0];
8578
+ const name = segments[2];
8579
+ return {
8580
+ id: segments[3],
8581
+ name,
8582
+ scope: scopeRaw === "global" ? "CLOUDFRONT" : "REGIONAL"
8583
+ };
8584
+ }
8585
+ /**
8586
+ * AWS WAFv2 WebACL Provider
8587
+ *
8588
+ * Implements resource provisioning for AWS::WAFv2::WebACL using the WAFv2 SDK.
8589
+ * WHY: WAFv2 CreateWebACL is synchronous - the CC API adds unnecessary polling
8590
+ * overhead for an operation that completes immediately.
8591
+ * This SDK provider eliminates that polling and returns instantly.
8592
+ */
8593
+ var WAFv2WebACLProvider = class {
8594
+ wafv2Client;
8595
+ providerRegion = process.env["AWS_REGION"];
8596
+ logger = getLogger().child("WAFv2WebACLProvider");
8597
+ handledProperties = /* @__PURE__ */ new Map([["AWS::WAFv2::WebACL", /* @__PURE__ */ new Set([
8598
+ "Name",
8599
+ "Scope",
8600
+ "Tags",
8601
+ "DefaultAction",
8602
+ "Description",
8603
+ "Rules",
8604
+ "VisibilityConfig",
8605
+ "CustomResponseBodies",
8606
+ "CaptchaConfig",
8607
+ "ChallengeConfig",
8608
+ "TokenDomains",
8609
+ "AssociationConfig"
8610
+ ])]]);
8611
+ getClient() {
8612
+ if (!this.wafv2Client) this.wafv2Client = new WAFV2Client(this.providerRegion ? { region: this.providerRegion } : {});
8613
+ return this.wafv2Client;
8614
+ }
8615
+ /**
8616
+ * Create a WAFv2 WebACL
8617
+ */
8618
+ async create(logicalId, resourceType, properties) {
8619
+ this.logger.debug(`Creating WAFv2 WebACL ${logicalId}`);
8620
+ const name = properties["Name"] || generateResourceName(logicalId, { maxLength: 128 });
8621
+ const scope = properties["Scope"] || "REGIONAL";
8622
+ try {
8623
+ const tags = [];
8624
+ if (properties["Tags"]) {
8625
+ const tagList = properties["Tags"];
8626
+ for (const tag of tagList) tags.push({
8627
+ Key: tag.Key,
8628
+ Value: tag.Value
8629
+ });
8630
+ }
8631
+ const summary = (await this.getClient().send(new CreateWebACLCommand({
8632
+ Name: name,
8633
+ Scope: scope,
8634
+ DefaultAction: properties["DefaultAction"],
8635
+ Description: sanitizeDescription(properties["Description"]),
8636
+ Rules: properties["Rules"] || [],
8637
+ VisibilityConfig: properties["VisibilityConfig"],
8638
+ ...tags.length > 0 && { Tags: tags },
8639
+ CustomResponseBodies: properties["CustomResponseBodies"],
8640
+ CaptchaConfig: properties["CaptchaConfig"],
8641
+ ChallengeConfig: properties["ChallengeConfig"],
8642
+ TokenDomains: properties["TokenDomains"],
8643
+ AssociationConfig: properties["AssociationConfig"]
8644
+ }))).Summary;
8645
+ if (!summary?.ARN || !summary?.Id) throw new Error("CreateWebACL did not return Summary with ARN and Id");
8646
+ this.logger.debug(`Successfully created WAFv2 WebACL ${logicalId}: ${summary.ARN}`);
8647
+ return {
8648
+ physicalId: summary.ARN,
8649
+ attributes: {
8650
+ Arn: summary.ARN,
8651
+ Id: summary.Id,
8652
+ LabelNamespace: summary["LabelNamespace"]
8653
+ }
8654
+ };
8655
+ } catch (error) {
8656
+ if (error instanceof ProvisioningError) throw error;
8657
+ const cause = error instanceof Error ? error : void 0;
8658
+ throw new ProvisioningError(`Failed to create WAFv2 WebACL ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
8659
+ }
8660
+ }
8661
+ /**
8662
+ * Update a WAFv2 WebACL
8663
+ *
8664
+ * Name and Scope are immutable - changes to those require replacement.
8341
8665
  * UpdateWebACL requires LockToken obtained from GetWebACL.
8342
8666
  */
8343
8667
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
@@ -8529,25 +8853,31 @@ var WAFv2WebACLProvider = class {
8529
8853
  if (err instanceof WAFNonexistentItemException) return null;
8530
8854
  throw err;
8531
8855
  }
8532
- if (!input.cdkPath) return null;
8533
8856
  const scope = input.properties["Scope"] || "REGIONAL";
8534
- let nextMarker;
8535
- do {
8536
- const list = await this.getClient().send(new ListWebACLsCommand({
8537
- Scope: scope,
8538
- ...nextMarker && { NextMarker: nextMarker }
8539
- }));
8540
- for (const item of list.WebACLs ?? []) {
8541
- if (!item.ARN) continue;
8542
- const tagList = (await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: item.ARN }))).TagInfoForResource?.TagList;
8543
- if (matchesCdkPath(tagList, input.cdkPath)) return {
8544
- physicalId: item.ARN,
8545
- attributes: {}
8857
+ const match = await importTagWalk({
8858
+ cdkPath: input.cdkPath,
8859
+ logicalId: input.logicalId,
8860
+ listPage: async (marker) => {
8861
+ const list = await this.getClient().send(new ListWebACLsCommand({
8862
+ Scope: scope,
8863
+ ...marker && { NextMarker: marker }
8864
+ }));
8865
+ return {
8866
+ items: list.WebACLs,
8867
+ nextMarker: list.NextMarker
8546
8868
  };
8547
- }
8548
- nextMarker = list.NextMarker;
8549
- } while (nextMarker);
8550
- return null;
8869
+ },
8870
+ describe: async (item) => {
8871
+ if (!item.ARN) return void 0;
8872
+ return await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: item.ARN }));
8873
+ },
8874
+ tagsOf: (tagsResp) => tagsResp.TagInfoForResource?.TagList
8875
+ });
8876
+ if (!match) return null;
8877
+ return {
8878
+ physicalId: match.summary.ARN,
8879
+ attributes: {}
8880
+ };
8551
8881
  }
8552
8882
  };
8553
8883
 
@@ -8917,8 +9247,37 @@ function collectReferencedParameterNames(template) {
8917
9247
  var IntrinsicFunctionResolver = class {
8918
9248
  logger = getLogger().child("IntrinsicFunctionResolver");
8919
9249
  resolverRegion;
8920
- constructor(region) {
9250
+ strictGetAtt;
9251
+ /**
9252
+ * Number of unknown-attribute resolutions that fell back to the physical
9253
+ * ID (the warn path) since construction / the last
9254
+ * {@link resetPhysicalIdFallbackCount}. Counting semantics (issue #1111
9255
+ * item 3): the deploy engine resets this at the start of each `deploy()`
9256
+ * run AND again right before provisioning on the change path — the diff
9257
+ * phase resolves through this same counted resolver, so without the
9258
+ * second reset a fallback site on a to-be-updated resource would count
9259
+ * once during diff and again during provisioning (~2x distinct sites in
9260
+ * the summary). Each distinct fallback site therefore counts once per
9261
+ * run: the surfaced summary covers provisioning + output resolution on
9262
+ * the change path, diff + output resolution on the no-change path, and
9263
+ * diff only under --dry-run. Instance-scoped, so parallel stacks (each
9264
+ * with their own engine + resolver) never share a counter; for the same
9265
+ * reason a nested-stack CHILD engine's fallbacks are counted by the
9266
+ * child's own resolver and are NOT aggregated into the parent stack's
9267
+ * deploy summary.
9268
+ */
9269
+ physicalIdFallbackCount = 0;
9270
+ constructor(region, options) {
8921
9271
  this.resolverRegion = region || process.env["AWS_REGION"] || "us-east-1";
9272
+ this.strictGetAtt = options?.strictGetAtt ?? false;
9273
+ }
9274
+ /** Unknown-attribute physicalId fallbacks recorded since the last reset. */
9275
+ getPhysicalIdFallbackCount() {
9276
+ return this.physicalIdFallbackCount;
9277
+ }
9278
+ /** Reset the per-run fallback counter (called at the start of each deploy). */
9279
+ resetPhysicalIdFallbackCount() {
9280
+ this.physicalIdFallbackCount = 0;
8922
9281
  }
8923
9282
  /**
8924
9283
  * Resolve parameter values from template Parameters section
@@ -9210,19 +9569,19 @@ var IntrinsicFunctionResolver = class {
9210
9569
  if (resourceType === "AWS::DynamoDB::Table" || resourceType === "AWS::DynamoDB::GlobalTable") switch (attributeName) {
9211
9570
  case "Arn": return `arn:${partition}:dynamodb:${region}:${accountId}:table/${physicalId}`;
9212
9571
  case "StreamArn": return;
9213
- default: return physicalId;
9572
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9214
9573
  }
9215
9574
  if (resourceType === "AWS::S3::Bucket") switch (attributeName) {
9216
9575
  case "Arn": return `arn:${partition}:s3:::${physicalId}`;
9217
9576
  case "DomainName": return `${physicalId}.s3.amazonaws.com`;
9218
9577
  case "RegionalDomainName": return `${physicalId}.s3.${region}.amazonaws.com`;
9219
9578
  case "WebsiteURL": return `http://${physicalId}.s3-website-${region}.amazonaws.com`;
9220
- default: return physicalId;
9579
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9221
9580
  }
9222
9581
  if (resourceType === "AWS::IAM::Role") switch (attributeName) {
9223
9582
  case "Arn": return `arn:${partition}:iam::${accountId}:role/${physicalId}`;
9224
9583
  case "RoleId": return;
9225
- default: return physicalId;
9584
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9226
9585
  }
9227
9586
  if (resourceType === "AWS::EC2::VPC") switch (attributeName) {
9228
9587
  case "VpcId": return physicalId;
@@ -9252,37 +9611,38 @@ var IntrinsicFunctionResolver = class {
9252
9611
  return [];
9253
9612
  }
9254
9613
  case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
9255
- default: return physicalId;
9614
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9256
9615
  }
9257
9616
  if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
9258
9617
  case "Arn": return `arn:${partition}:iam::${accountId}:policy/${physicalId}`;
9259
9618
  case "PolicyId": return;
9260
- default: return physicalId;
9619
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9261
9620
  }
9262
9621
  if (resourceType === "AWS::IAM::User") switch (attributeName) {
9263
9622
  case "Arn": return `arn:${partition}:iam::${accountId}:user/${physicalId}`;
9264
- default: return physicalId;
9623
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9265
9624
  }
9266
9625
  if (resourceType === "AWS::IAM::Group") switch (attributeName) {
9267
9626
  case "Arn": return `arn:${partition}:iam::${accountId}:group/${physicalId}`;
9268
- default: return physicalId;
9627
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9269
9628
  }
9270
9629
  if (resourceType === "AWS::IAM::InstanceProfile") switch (attributeName) {
9271
9630
  case "Arn": return `arn:${partition}:iam::${accountId}:instance-profile/${physicalId}`;
9272
- default: return physicalId;
9631
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9273
9632
  }
9274
9633
  if (resourceType === "AWS::KMS::Key") switch (attributeName) {
9275
9634
  case "Arn": return `arn:${partition}:kms:${region}:${accountId}:key/${physicalId}`;
9276
9635
  case "KeyId": return physicalId;
9277
- default: return physicalId;
9636
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9278
9637
  }
9279
9638
  if (resourceType === "AWS::Cognito::UserPool") switch (attributeName) {
9280
9639
  case "Arn": return `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${physicalId}`;
9281
- default: return physicalId;
9640
+ case "UserPoolId": return physicalId;
9641
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9282
9642
  }
9283
9643
  if (resourceType === "AWS::Kinesis::Stream") switch (attributeName) {
9284
9644
  case "Arn": return `arn:${partition}:kinesis:${region}:${accountId}:stream/${physicalId}`;
9285
- default: return physicalId;
9645
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9286
9646
  }
9287
9647
  if (resourceType === "AWS::Events::Rule") switch (attributeName) {
9288
9648
  case "Arn": {
@@ -9292,36 +9652,36 @@ var IntrinsicFunctionResolver = class {
9292
9652
  const busName = bus.startsWith("arn:") ? bus.split("/").pop() || "" : bus;
9293
9653
  return busName ? `arn:${partition}:events:${region}:${accountId}:rule/${busName}/${physicalId}` : `arn:${partition}:events:${region}:${accountId}:rule/${physicalId}`;
9294
9654
  }
9295
- default: return physicalId;
9655
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9296
9656
  }
9297
9657
  if (resourceType === "AWS::Events::EventBus") switch (attributeName) {
9298
9658
  case "Arn": return `arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`;
9299
9659
  case "Name": return physicalId;
9300
- default: return physicalId;
9660
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9301
9661
  }
9302
9662
  if (resourceType === "AWS::EFS::FileSystem") switch (attributeName) {
9303
9663
  case "Arn": return `arn:${partition}:elasticfilesystem:${region}:${accountId}:file-system/${physicalId}`;
9304
9664
  case "FileSystemId": return physicalId;
9305
- default: return physicalId;
9665
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9306
9666
  }
9307
9667
  if (resourceType === "AWS::KinesisFirehose::DeliveryStream") switch (attributeName) {
9308
9668
  case "Arn": return `arn:${partition}:firehose:${region}:${accountId}:deliverystream/${physicalId}`;
9309
- default: return physicalId;
9669
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9310
9670
  }
9311
9671
  if (resourceType === "AWS::CodeBuild::Project") switch (attributeName) {
9312
9672
  case "Arn": return `arn:${partition}:codebuild:${region}:${accountId}:project/${physicalId}`;
9313
- default: return physicalId;
9673
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9314
9674
  }
9315
9675
  if (resourceType === "AWS::CloudTrail::Trail") switch (attributeName) {
9316
9676
  case "Arn":
9317
9677
  if (physicalId.startsWith("arn:")) return physicalId;
9318
9678
  return `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${physicalId}`;
9319
- default: return physicalId;
9679
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9320
9680
  }
9321
9681
  if (resourceType === "AWS::AppSync::GraphQLApi") switch (attributeName) {
9322
9682
  case "Arn": return `arn:${partition}:appsync:${region}:${accountId}:apis/${physicalId}`;
9323
9683
  case "ApiId": return physicalId;
9324
- default: return physicalId;
9684
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9325
9685
  }
9326
9686
  if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
9327
9687
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
@@ -9333,38 +9693,38 @@ var IntrinsicFunctionResolver = class {
9333
9693
  this.logger.warn(`Failed to fetch HostedZoneId for namespace ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
9334
9694
  return;
9335
9695
  }
9336
- default: return physicalId;
9696
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9337
9697
  }
9338
9698
  if (resourceType === "AWS::ServiceDiscovery::Service") switch (attributeName) {
9339
9699
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:service/${physicalId}`;
9340
9700
  case "Id": return physicalId;
9341
- default: return physicalId;
9701
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9342
9702
  }
9343
9703
  if (resourceType === "AWS::CloudWatch::Alarm") switch (attributeName) {
9344
9704
  case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
9345
- default: return physicalId;
9705
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9346
9706
  }
9347
9707
  if (resourceType === "AWS::CloudWatch::CompositeAlarm") switch (attributeName) {
9348
9708
  case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
9349
- default: return physicalId;
9709
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9350
9710
  }
9351
9711
  if (resourceType === "AWS::RDS::DBInstance" || resourceType === "AWS::DocDB::DBInstance" || resourceType === "AWS::Neptune::DBInstance") switch (attributeName) {
9352
9712
  case "DBInstanceArn":
9353
9713
  case "Arn": return `arn:${partition}:rds:${region}:${accountId}:db:${physicalId}`;
9354
- default: return physicalId;
9714
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9355
9715
  }
9356
9716
  if (resourceType === "AWS::RDS::DBCluster" || resourceType === "AWS::DocDB::DBCluster" || resourceType === "AWS::Neptune::DBCluster") switch (attributeName) {
9357
9717
  case "DBClusterArn":
9358
9718
  case "Arn": return `arn:${partition}:rds:${region}:${accountId}:cluster:${physicalId}`;
9359
- default: return physicalId;
9719
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9360
9720
  }
9361
9721
  if (resourceType === "AWS::S3Express::DirectoryBucket") switch (attributeName) {
9362
9722
  case "Arn": return `arn:${partition}:s3express:${region}:${accountId}:bucket/${physicalId}`;
9363
- default: return physicalId;
9723
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9364
9724
  }
9365
9725
  if (resourceType === "AWS::Lambda::Function") switch (attributeName) {
9366
9726
  case "Arn": return `arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`;
9367
- default: return physicalId;
9727
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9368
9728
  }
9369
9729
  if (resourceType === "AWS::SQS::Queue") {
9370
9730
  let queueName = physicalId;
@@ -9376,25 +9736,26 @@ var IntrinsicFunctionResolver = class {
9376
9736
  case "Arn": return `arn:${partition}:sqs:${region}:${accountId}:${queueName}`;
9377
9737
  case "QueueUrl": return physicalId;
9378
9738
  case "QueueName": return queueName;
9379
- default: return physicalId;
9739
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9380
9740
  }
9381
9741
  }
9382
9742
  if (resourceType === "AWS::SNS::Topic") switch (attributeName) {
9383
9743
  case "TopicArn": return `arn:${partition}:sns:${region}:${accountId}:${physicalId}`;
9384
- default: return physicalId;
9744
+ case "TopicName": return physicalId;
9745
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9385
9746
  }
9386
9747
  if (resourceType === "AWS::Logs::LogGroup") switch (attributeName) {
9387
9748
  case "Arn": return `arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}:*`;
9388
- default: return physicalId;
9749
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9389
9750
  }
9390
9751
  if (resourceType === "AWS::ECR::Repository") switch (attributeName) {
9391
9752
  case "Arn": return `arn:${partition}:ecr:${region}:${accountId}:repository/${physicalId}`;
9392
9753
  case "RepositoryUri": return `${accountId}.dkr.ecr.${region}.amazonaws.com/${physicalId}`;
9393
- default: return physicalId;
9754
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9394
9755
  }
9395
9756
  if (resourceType === "AWS::ECS::Cluster") switch (attributeName) {
9396
9757
  case "Arn": return `arn:${partition}:ecs:${region}:${accountId}:cluster/${physicalId}`;
9397
- default: return physicalId;
9758
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9398
9759
  }
9399
9760
  if (resourceType === "AWS::ECS::Service") switch (attributeName) {
9400
9761
  case "Name": {
@@ -9407,18 +9768,30 @@ var IntrinsicFunctionResolver = class {
9407
9768
  if (pipeIdx >= 0) return physicalId.substring(pipeIdx + 1);
9408
9769
  return physicalId;
9409
9770
  }
9410
- default: return physicalId;
9771
+ case "ServiceArn": {
9772
+ const pipeIdx = physicalId.indexOf("|");
9773
+ if (pipeIdx < 0) return physicalId;
9774
+ const left = physicalId.substring(0, pipeIdx);
9775
+ if (left.includes(":service/")) return left;
9776
+ const clusterIdx = left.indexOf(":cluster/");
9777
+ if (clusterIdx < 0) return physicalId;
9778
+ const clusterName = left.substring(clusterIdx + 9);
9779
+ const serviceName = physicalId.substring(pipeIdx + 1);
9780
+ return `${left.substring(0, clusterIdx)}:service/${clusterName}/${serviceName}`;
9781
+ }
9782
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9411
9783
  }
9412
9784
  if (resourceType === "AWS::EC2::SecurityGroup") switch (attributeName) {
9413
9785
  case "GroupId": return physicalId;
9414
9786
  case "VpcId": return;
9415
- default: return physicalId;
9787
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9416
9788
  }
9417
9789
  if (resourceType === "AWS::EC2::Subnet") switch (attributeName) {
9418
9790
  case "SubnetId": return physicalId;
9419
- default: return physicalId;
9791
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9420
9792
  }
9421
9793
  if (resourceType === "AWS::EC2::Instance") switch (attributeName) {
9794
+ case "InstanceId": return physicalId;
9422
9795
  case "PrivateIp":
9423
9796
  case "PublicIp":
9424
9797
  case "PrivateDnsName":
@@ -9457,7 +9830,7 @@ var IntrinsicFunctionResolver = class {
9457
9830
  }
9458
9831
  return physicalId;
9459
9832
  }
9460
- default: return physicalId;
9833
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9461
9834
  }
9462
9835
  if (resourceType === "AWS::EC2::LaunchTemplate") {
9463
9836
  if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
@@ -9470,11 +9843,44 @@ var IntrinsicFunctionResolver = class {
9470
9843
  }
9471
9844
  return attributeName === "LatestVersionNumber" ? "$Latest" : "$Default";
9472
9845
  }
9473
- return physicalId;
9846
+ if (attributeName === "LaunchTemplateId") return physicalId;
9847
+ return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9474
9848
  }
9849
+ return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9850
+ }
9851
+ /**
9852
+ * Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
9853
+ *
9854
+ * Applied by BOTH the final unknown-type fallback of
9855
+ * {@link constructAttribute} and every per-type handler's
9856
+ * `default:`-for-an-unknown-attribute branch:
9857
+ *
9858
+ * - An attribute name ending in `Arn` whose fallback value is not
9859
+ * ARN-shaped (or ending in `Url` whose fallback is not an http(s) URL)
9860
+ * cannot be what CloudFormation would return — hard-fail. The #1103
9861
+ * incident shipped four resource NAMES into stack Outputs where ARNs
9862
+ * were requested, with a green deploy. A physicalId that already IS an
9863
+ * ARN / URL passes the shape check and remains a valid fallback.
9864
+ * - Under `--strict-getatt`, EVERY unknown-attribute fallback (any
9865
+ * suffix) is a hard error.
9866
+ * - Otherwise: `Alias` / `Endpoint` and every other suffix keep the
9867
+ * warn-and-return behavior (an alias or endpoint is
9868
+ * shape-indistinguishable from a plain name, so a hard-fail there
9869
+ * would risk failing correct deploys — false positives are
9870
+ * unacceptable). Each warn-and-return increments the per-run fallback
9871
+ * counter surfaced in the deploy summary.
9872
+ *
9873
+ * A `return physicalId` that is the CORRECT value for a KNOWN attribute
9874
+ * (e.g. `AWS::KMS::Key.KeyId`, `AWS::SNS::Topic.TopicName`) must NOT
9875
+ * route through this helper — those are explicit `case`s in the
9876
+ * per-type handlers.
9877
+ */
9878
+ guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId) {
9475
9879
  const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
9476
9880
  const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
9477
9881
  if (expectsArnShape || expectsUrlShape) throw new Error(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${physicalId}" is not ${expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)"}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${attributeName}.`);
9882
+ if (this.strictGetAtt) throw new Error(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${physicalId}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${attributeName}.`);
9883
+ this.physicalIdFallbackCount++;
9478
9884
  this.logger.warn(`Unknown attribute ${attributeName} for resource type ${resourceType}, returning physical ID`);
9479
9885
  return physicalId;
9480
9886
  }
@@ -10878,7 +11284,7 @@ var CloudControlProvider = class {
10878
11284
  this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
10879
11285
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
10880
11286
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
10881
- const { ASGProvider } = await import("./asg-provider-BPtdM67j.js").then((n) => n.n);
11287
+ const { ASGProvider } = await import("./asg-provider-6IZKZOKp.js").then((n) => n.n);
10882
11288
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
10883
11289
  return;
10884
11290
  }
@@ -14538,456 +14944,143 @@ var ProviderRegistry = class {
14538
14944
  /**
14539
14945
  * Get the Cloud Control provider instance (for resource state lookup)
14540
14946
  */
14541
- getCloudControlProvider() {
14542
- return this.cloudControlProvider;
14543
- }
14544
- /**
14545
- * Get all registered resource types (excluding Cloud Control)
14546
- */
14547
- getRegisteredTypes() {
14548
- return Array.from(this.providers.keys());
14549
- }
14550
- /**
14551
- * Get provider type for a resource type
14552
- *
14553
- * @returns 'sdk' | 'cloud-control' | null
14554
- */
14555
- getProviderType(resourceType) {
14556
- if (this.providers.has(resourceType)) return "sdk";
14557
- if (CloudControlProvider.isSupportedResourceType(resourceType)) return "cloud-control";
14558
- if (this.allowedUnsupportedTypes.has(resourceType)) return "cloud-control";
14559
- return null;
14560
- }
14561
- /**
14562
- * Validate that all resource types have available providers
14563
- *
14564
- * This should be called before deployment starts to ensure all resources can be provisioned.
14565
- *
14566
- * @param resourceTypes Set of resource types to validate
14567
- * @throws Error if any resource type doesn't have a provider
14568
- */
14569
- validateResourceTypes(resourceTypes) {
14570
- const unsupportedTypes = [];
14571
- for (const resourceType of resourceTypes) if (!this.hasProvider(resourceType)) unsupportedTypes.push(resourceType);
14572
- if (unsupportedTypes.length > 0) {
14573
- const details = unsupportedTypes.map((type) => {
14574
- return ` - ${type}\n ${isNonProvisionable(type) ? "AWS reports this type as NON_PROVISIONABLE (Cloud Control API cannot manage it) and cdkd has no SDK provider for it." : "cdkd does not currently support this type — no SDK provider is registered, and the type is either on cdkd's Cloud Control blocklist (pending a dedicated SDK provider) or is not an AWS:: namespace."}\n Request support: ${unsupportedTypeIssueUrl(type)}`;
14575
- }).join("\n");
14576
- throw new Error(`The following resource types are not supported by cdkd:\n` + details + `\n\nTo attempt deployment anyway (Cloud Control will likely fail for NON_PROVISIONABLE types), re-run with: --allow-unsupported-types ${unsupportedTypes.join(",")}`);
14577
- }
14578
- this.logger.debug(`Validated ${resourceTypes.size} resource types: all have available providers`);
14579
- }
14580
- /**
14581
- * Walk every resource in the template and identify top-level CFn
14582
- * properties cdkd's SDK provider would silently drop on write. As of
14583
- * issue [#614](https://github.com/go-to-k/cdkd/issues/614), silent drops
14584
- * auto-route the resource through Cloud Control API by default (see
14585
- * {@link getProviderFor}) — the method emits info-level routing decisions
14586
- * for each silent-drop resource, plus warn-level lines for resources
14587
- * where the user explicitly opted into the silent drop via
14588
- * `--allow-unsupported-properties`. The ONE remaining throw path is the
14589
- * CC-route viability guard: when the auto-route target cannot manage the
14590
- * type (`NON_PROVISIONABLE` or a provider-level `disableCcApiFallback`
14591
- * opt-out — e.g. AWS::FSx::FileSystem's Windows/ONTAP/OpenZFS blocks),
14592
- * this rejects pre-flight with a clear per-property error instead of
14593
- * letting provisioning fail opaquely.
14594
- *
14595
- * Must be called AFTER {@link validateResourceTypes} — type-level errors
14596
- * are still hard rejects. For a type allowed via `--allow-unsupported-types`,
14597
- * the property check is a no-op (`findSilentDropProperties` returns `[]`
14598
- * for non-Tier-1 / unknown types).
14599
- *
14600
- * @see findAutoRouteHits for the pure-functional pre-deploy plan-builder
14601
- * that returns the same information without logging.
14947
+ getCloudControlProvider() {
14948
+ return this.cloudControlProvider;
14949
+ }
14950
+ /**
14951
+ * Get all registered resource types (excluding Cloud Control)
14602
14952
  */
14603
- validateResourceProperties(resources) {
14604
- this.reportSilentDropDecisions(resources);
14953
+ getRegisteredTypes() {
14954
+ return Array.from(this.providers.keys());
14605
14955
  }
14606
14956
  /**
14607
- * Info-log every silent-drop routing decision (auto-route via CC API) and
14608
- * warn-log every silent drop the user explicitly opted into via
14609
- * `--allow-unsupported-properties` (forced SDK path, the property will
14610
- * be dropped). Does not mutate state; throws ONLY for the CC-route
14611
- * viability guard (un-allowed silent drops on a type Cloud Control cannot
14612
- * manage — see {@link buildUnroutableSilentDropMessage}).
14613
- *
14614
- * Issue [#614](https://github.com/go-to-k/cdkd/issues/614). Replaces the
14615
- * pre-v0.16x throw path: silent drops are now a routing signal, not an
14616
- * error (except the viability guard above).
14957
+ * Get provider type for a resource type
14617
14958
  *
14618
- * When the optional `provisionedBy` (from existing state) is `'cc-api'`,
14619
- * the auto-route info line is demoted to `debug` — the resource has been
14620
- * on CC for at least one prior deploy, so the routing decision is
14621
- * **continuation of sticky state, not a fresh auto-route**. Surfacing the
14622
- * info line every deploy would be repetitive noise. The warn line for
14623
- * explicit `--allow-unsupported-properties` overrides is NOT demoted —
14624
- * that override is an active user choice for THIS deploy and should
14625
- * surface every time.
14959
+ * @returns 'sdk' | 'cloud-control' | null
14626
14960
  */
14627
- reportSilentDropDecisions(resources) {
14628
- for (const { logicalId, resourceType, properties, provisionedBy } of resources) {
14629
- const drops = findSilentDropProperties(resourceType, properties);
14630
- if (drops.length === 0) continue;
14631
- const overridden = [];
14632
- const autoRouted = [];
14633
- for (const { property } of drops) {
14634
- const allowKey = `${resourceType}:${property}`;
14635
- if (this.allowedUnsupportedProperties.has(allowKey)) overridden.push(property);
14636
- else autoRouted.push(property);
14637
- }
14638
- if (autoRouted.length > 0) {
14639
- const provider = this.providers.get(resourceType);
14640
- if (isNonProvisionable(resourceType) || provider?.disableCcApiFallback === true) throw new Error(`${logicalId}: ${this.buildUnroutableSilentDropMessage(resourceType, drops.filter((d) => autoRouted.includes(d.property)))}`);
14641
- const message = `${logicalId} (${resourceType}): routing via Cloud Control API (cdkd's SDK Provider does not yet wire ${autoRouted.join(", ")} — CC API will forward the full property map. Override via --allow-unsupported-properties ${autoRouted.map((p) => `${resourceType}:${p}`).join(",")}.)`;
14642
- if (provisionedBy === "cc-api") this.logger.debug(message);
14643
- else this.logger.info(message);
14644
- }
14645
- if (overridden.length > 0) {
14646
- const propList = overridden.join(", ");
14647
- this.logger.warn(`${logicalId} (${resourceType}): ${propList} will be silently dropped (--allow-unsupported-properties override accepted). Remove the override to route this resource via Cloud Control API instead.`);
14648
- }
14649
- }
14961
+ getProviderType(resourceType) {
14962
+ if (this.providers.has(resourceType)) return "sdk";
14963
+ if (CloudControlProvider.isSupportedResourceType(resourceType)) return "cloud-control";
14964
+ if (this.allowedUnsupportedTypes.has(resourceType)) return "cloud-control";
14965
+ return null;
14650
14966
  }
14651
14967
  /**
14652
- * Pure-functional discovery of every resource whose template uses one or
14653
- * more silent-drop properties that are NOT in the
14654
- * `--allow-unsupported-properties` allow set — i.e. every resource that
14655
- * {@link getProviderFor} would auto-route via Cloud Control. Returned
14656
- * entries carry the silent-drop property names so plan / diff renderers
14657
- * can show `[via CC API: RuntimeManagementConfig]`.
14968
+ * Validate that all resource types have available providers
14658
14969
  *
14659
- * Does NOT log or throw. Use {@link reportSilentDropDecisions} for the
14660
- * side-effecting info / warn surface.
14970
+ * This should be called before deployment starts to ensure all resources can be provisioned.
14971
+ *
14972
+ * @param resourceTypes Set of resource types to validate
14973
+ * @throws Error if any resource type doesn't have a provider
14661
14974
  */
14662
- findAutoRouteHits(resources) {
14663
- const hits = [];
14664
- for (const { logicalId, resourceType, properties } of resources) {
14665
- const actionable = findActionableSilentDrops(resourceType, properties, this.allowedUnsupportedProperties);
14666
- if (actionable.length === 0) continue;
14667
- hits.push({
14668
- logicalId,
14669
- resourceType,
14670
- properties: actionable.map((d) => d.property)
14671
- });
14672
- }
14673
- return hits;
14674
- }
14675
- };
14676
- function isCustomResource(resourceType) {
14677
- return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
14678
- }
14679
-
14680
- //#endregion
14681
- //#region src/deployment/retryable-errors.ts
14682
- /**
14683
- * Patterns that mark an AWS error as a transient/retryable failure.
14684
- * Each entry is a substring match against the error message; all of these
14685
- * are situations where the same call typically succeeds after a short delay
14686
- * because of eventual consistency or just-created-dependency propagation.
14687
- */
14688
- const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
14689
- "cannot be assumed",
14690
- "Firehose is unable to assume role",
14691
- "is unable to assume provided role",
14692
- "role defined for the function",
14693
- "not authorized to perform",
14694
- "execution role",
14695
- "trust policy",
14696
- "Role validation failed",
14697
- "does not have required permissions",
14698
- "Trusted Entity",
14699
- "currently in the following state: Pending",
14700
- "has dependencies and cannot be deleted",
14701
- "can't be deleted since it has",
14702
- "DependencyViolation",
14703
- "does not exist",
14704
- "Schema is currently being altered",
14705
- "Invalid principal in policy",
14706
- "Policy Error: PrincipalNotFound",
14707
- "Invalid value for the parameter Policy",
14708
- "required permissions for: ENHANCED_MONITORING",
14709
- "Caught ServiceAccessDeniedException",
14710
- "permissions required to assume the role",
14711
- "authorized to assume the provided role",
14712
- "conflicting conditional operation",
14713
- "scheduled for deletion",
14714
- "Cannot access stream",
14715
- "Please ensure the role can perform",
14716
- "KMS key is invalid for CreateGrant",
14717
- "Policy contains a statement with one or more invalid principals",
14718
- "Invalid IAM Instance Profile",
14719
- "Invalid InstanceProfile",
14720
- "Failed to authorize instance profile",
14721
- "Could not deliver test message",
14722
- "wait 60 seconds",
14723
- "concurrent update operation",
14724
- "because it is in use",
14725
- "Rate exceeded"
14726
- ];
14727
- /**
14728
- * HTTP status codes that always indicate a transient failure worth retrying.
14729
- * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
14730
- */
14731
- const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
14732
- /**
14733
- * AWS SDK v3 canonical throttling error names. Mirrors
14734
- * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
14735
- * error (or wrapped cause) whose `name` is one of these is a transient rate-
14736
- * limit rejection worth retrying with backoff. Detecting by NAME is more
14737
- * robust than by HTTP status because most AWS throttles surface as HTTP 400
14738
- * (not 429) with the throttling signal carried only in the error code / name
14739
- * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
14740
- */
14741
- const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
14742
- "BandwidthLimitExceeded",
14743
- "EC2ThrottledException",
14744
- "LimitExceededException",
14745
- "PriorRequestNotComplete",
14746
- "ProvisionedThroughputExceededException",
14747
- "RequestLimitExceeded",
14748
- "RequestThrottled",
14749
- "RequestThrottledException",
14750
- "SlowDown",
14751
- "ThrottledException",
14752
- "Throttling",
14753
- "ThrottlingException",
14754
- "TooManyRequestsException",
14755
- "TransactionInProgressException"
14756
- ]);
14757
- /**
14758
- * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
14759
- * signal — either an AWS SDK v3 throttling error `name`
14760
- * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
14761
- * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
14762
- *
14763
- * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
14764
- * typically one cause-link deep; the bounded walk also tolerates SDK errors
14765
- * that nest a `$response`/cause without exploding on a cyclic chain.
14766
- *
14767
- * BOTH signals are checked at EVERY depth. An earlier version checked the name
14768
- * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
14769
- * links deep was missed. This is the single shared cause-walk: the read-only
14770
- * import tag walk (`src/provisioning/import-tag-walk.ts`) composes its own
14771
- * classifier on top of this function rather than re-implementing the traversal,
14772
- * so the two cannot drift apart again.
14773
- */
14774
- function isThrottlingError(error) {
14775
- let current = error;
14776
- for (let depth = 0; depth < 5 && current != null; depth++) {
14777
- const name = current.name;
14778
- if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
14779
- const status = current.$metadata?.httpStatusCode;
14780
- if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
14781
- current = current.cause;
14782
- }
14783
- return false;
14784
- }
14785
- /**
14786
- * Determine whether an AWS error should be retried.
14787
- *
14788
- * Checks (in order):
14789
- * 1. Rate-limit signal on the error or any wrapped cause — throttling error
14790
- * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
14791
- * 429, so the name check carries most of the weight). See
14792
- * {@link isThrottlingError}.
14793
- * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
14794
- */
14795
- function isRetryableTransientError(error, message) {
14796
- if (isThrottlingError(error)) return true;
14797
- return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
14798
- }
14799
-
14800
- //#endregion
14801
- //#region src/deployment/retry.ts
14802
- /**
14803
- * Retry helper for resource provisioning operations that hit transient
14804
- * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
14805
- * dependency violations, etc.).
14806
- *
14807
- * Extracted from DeployEngine so the backoff schedule can be unit-tested
14808
- * in isolation. The retryable-error classifier itself lives in
14809
- * `./retryable-errors.ts`.
14810
- */
14811
- const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14812
- /**
14813
- * Run `operation`, retrying transient failures with exponential backoff
14814
- * capped at `maxDelayMs`.
14815
- *
14816
- * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
14817
- * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
14818
- *
14819
- * Non-retryable errors are rethrown immediately. The transient-error
14820
- * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
14821
- */
14822
- async function withRetry(operation, logicalId, opts = {}) {
14823
- const maxRetries = opts.maxRetries ?? 8;
14824
- const initialDelayMs = opts.initialDelayMs ?? 1e3;
14825
- const maxDelayMs = opts.maxDelayMs ?? 8e3;
14826
- const sleep = opts.sleep ?? defaultSleep;
14827
- let lastError;
14828
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
14829
- return await operation();
14830
- } catch (error) {
14831
- lastError = error;
14832
- const message = error instanceof Error ? error.message : String(error);
14833
- if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
14834
- const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
14835
- opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
14836
- for (let waited = 0; waited < delay; waited += 1e3) {
14837
- if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
14838
- await sleep(Math.min(1e3, delay - waited));
14975
+ validateResourceTypes(resourceTypes) {
14976
+ const unsupportedTypes = [];
14977
+ for (const resourceType of resourceTypes) if (!this.hasProvider(resourceType)) unsupportedTypes.push(resourceType);
14978
+ if (unsupportedTypes.length > 0) {
14979
+ const details = unsupportedTypes.map((type) => {
14980
+ return ` - ${type}\n ${isNonProvisionable(type) ? "AWS reports this type as NON_PROVISIONABLE (Cloud Control API cannot manage it) and cdkd has no SDK provider for it." : "cdkd does not currently support this type — no SDK provider is registered, and the type is either on cdkd's Cloud Control blocklist (pending a dedicated SDK provider) or is not an AWS:: namespace."}\n Request support: ${unsupportedTypeIssueUrl(type)}`;
14981
+ }).join("\n");
14982
+ throw new Error(`The following resource types are not supported by cdkd:\n` + details + `\n\nTo attempt deployment anyway (Cloud Control will likely fail for NON_PROVISIONABLE types), re-run with: --allow-unsupported-types ${unsupportedTypes.join(",")}`);
14839
14983
  }
14984
+ this.logger.debug(`Validated ${resourceTypes.size} resource types: all have available providers`);
14840
14985
  }
14841
- throw lastError;
14842
- }
14843
-
14844
- //#endregion
14845
- //#region src/provisioning/import-tag-walk.ts
14846
- /**
14847
- * Shared, throttle-tolerant `aws:cdk:path` tag walk for `ResourceProvider.import`.
14848
- *
14849
- * Providers that adopt a resource without an explicit name property fall back
14850
- * to step 3 of the lookup order documented in `./import-helpers.ts`: enumerate
14851
- * the service's `List*`/`Describe*` pages, then issue ONE per-candidate read
14852
- * (`DescribeX` / `ListTagsForResource`) to obtain the tag set — the list
14853
- * summaries usually do not carry tags. That is an inherent **N+1** read
14854
- * pattern: an account with many resources of the type produces one API call
14855
- * per candidate in a tight loop, which is exactly the shape AWS rate-limits.
14856
- *
14857
- * Every provider previously hand-rolled this loop with NO backoff, so a single
14858
- * throttled `Describe*` aborted the whole `cdkd import` run. This helper
14859
- * centralises the loop and wraps BOTH the list page fetch and the per-candidate
14860
- * describe in the deploy engine's `withRetry` with exponential backoff.
14861
- *
14862
- * ## Why a narrower classifier than `isRetryableTransientError`
14863
- *
14864
- * The deploy engine's classifier is tuned for the WRITE path: it also treats
14865
- * eventual-consistency phrasings like `does not exist` / `not authorized to
14866
- * perform` as transient, because a just-created dependency legitimately needs a
14867
- * moment to propagate. On a read-only import walk those messages mean the
14868
- * opposite — the candidate really is gone, or the caller's credentials really
14869
- * lack the permission — and retrying them burns the full backoff budget per
14870
- * candidate before surfacing the true error.
14871
- *
14872
- * So the walk retries throttling ONLY, reusing the deploy engine's throttle
14873
- * tables verbatim ({@link THROTTLING_ERROR_NAMES} /
14874
- * {@link RETRYABLE_HTTP_STATUS_CODES}) rather than maintaining a second copy.
14875
- *
14876
- * ## Batching
14877
- *
14878
- * Batched tag reads are deliberately NOT modelled here: the services this
14879
- * helper currently serves (EMR `DescribeCluster`, DocDB
14880
- * `ListTagsForResource`) expose only single-resource reads. A service that
14881
- * genuinely offers a batch API (e.g. CodeCommit `BatchGetRepositories`) can
14882
- * satisfy several candidates from one call inside its own `describe` callback,
14883
- * or bypass the helper entirely.
14884
- */
14885
- /** Max number of retries after the first attempt, per API call in the walk. */
14886
- const DEFAULT_MAX_RETRIES = 5;
14887
- /** Initial backoff; each retry doubles it up to {@link DEFAULT_MAX_DELAY_MS}. */
14888
- const DEFAULT_INITIAL_DELAY_MS = 500;
14889
- /** Cap for the per-retry delay (0.5s -> 1s -> 2s -> 4s -> 5s, ~12.5s total). */
14890
- const DEFAULT_MAX_DELAY_MS = 5e3;
14891
- /**
14892
- * Wall-clock ceiling for the WHOLE walk (all pages + all candidates), not just
14893
- * one call. Backoff is per-call, so without this a sustained throttle against a
14894
- * large account degrades into `(pages + candidates) x ~12.5s` of near-silent
14895
- * retrying — ~42 minutes for 200 candidates, with no way to tell a slow walk
14896
- * from a hung one. 10 minutes is generous for a healthy walk (a 200-candidate
14897
- * DocDB account completes in seconds when AWS is not throttling) and bounds the
14898
- * pathological case to something a user will wait through.
14899
- */
14900
- const DEFAULT_MAX_WALK_MS = 10 * 6e4;
14901
- /**
14902
- * Ceiling on pages fetched. A service that returns a non-advancing pagination
14903
- * token (a bug, or a marker cdkd echoes back wrongly) would otherwise spin
14904
- * forever. This is now the shared path every migrated provider runs on, so the
14905
- * guard belongs here rather than in each caller.
14906
- */
14907
- const DEFAULT_MAX_PAGES = 1e3;
14908
- /**
14909
- * Whether an error hit during the read-only tag walk is a rate-limit rejection
14910
- * worth backing off on.
14911
- *
14912
- * Delegates the error + `.cause` chain traversal to the deploy engine's
14913
- * {@link isThrottlingError} (throttling error names + retryable HTTP statuses,
14914
- * at every cause depth) and adds the canonical `Rate exceeded` message, which
14915
- * several services return with an HTTP 400 and a service-specific error name.
14916
- *
14917
- * This is deliberately NARROWER than `isRetryableTransientError`: that
14918
- * classifier also treats write-path eventual-consistency phrasings (`does not
14919
- * exist`, `not authorized to perform`) as transient, because a just-created
14920
- * dependency legitimately needs a moment to propagate. On a read-only walk
14921
- * those mean the candidate really is gone or the credentials really lack the
14922
- * permission, and retrying them burns the full backoff budget per candidate
14923
- * before surfacing the true error.
14924
- *
14925
- * Exported for direct unit testing and for providers whose tag walk cannot use
14926
- * {@link importTagWalk} verbatim.
14927
- */
14928
- function isThrottlingLikeError(error, message) {
14929
- return isThrottlingError(error) || message.includes("Rate exceeded");
14930
- }
14931
- /** Thrown when the walk exceeds its wall-clock budget or page ceiling. */
14932
- var ImportTagWalkLimitError = class extends Error {
14933
- constructor(message) {
14934
- super(message);
14935
- this.name = "ImportTagWalkLimitError";
14986
+ /**
14987
+ * Walk every resource in the template and identify top-level CFn
14988
+ * properties cdkd's SDK provider would silently drop on write. As of
14989
+ * issue [#614](https://github.com/go-to-k/cdkd/issues/614), silent drops
14990
+ * auto-route the resource through Cloud Control API by default (see
14991
+ * {@link getProviderFor}) — the method emits info-level routing decisions
14992
+ * for each silent-drop resource, plus warn-level lines for resources
14993
+ * where the user explicitly opted into the silent drop via
14994
+ * `--allow-unsupported-properties`. The ONE remaining throw path is the
14995
+ * CC-route viability guard: when the auto-route target cannot manage the
14996
+ * type (`NON_PROVISIONABLE` or a provider-level `disableCcApiFallback`
14997
+ * opt-out e.g. AWS::FSx::FileSystem's Windows/ONTAP/OpenZFS blocks),
14998
+ * this rejects pre-flight with a clear per-property error instead of
14999
+ * letting provisioning fail opaquely.
15000
+ *
15001
+ * Must be called AFTER {@link validateResourceTypes} — type-level errors
15002
+ * are still hard rejects. For a type allowed via `--allow-unsupported-types`,
15003
+ * the property check is a no-op (`findSilentDropProperties` returns `[]`
15004
+ * for non-Tier-1 / unknown types).
15005
+ *
15006
+ * @see findAutoRouteHits for the pure-functional pre-deploy plan-builder
15007
+ * that returns the same information without logging.
15008
+ */
15009
+ validateResourceProperties(resources) {
15010
+ this.reportSilentDropDecisions(resources);
14936
15011
  }
14937
- };
14938
- /**
14939
- * Paginate `listPage`, `describe` each candidate, and return the first one
14940
- * whose tags carry the requested `aws:cdk:path`. Returns `null` when no
14941
- * candidate matches (or when `cdkPath` is empty).
14942
- *
14943
- * Both callbacks are individually retried with exponential backoff on
14944
- * throttling errors, so one rate-limited call mid-walk no longer aborts the
14945
- * whole import.
14946
- */
14947
- async function importTagWalk(options) {
14948
- const { cdkPath, listPage, describe, tagsOf } = options;
14949
- if (!cdkPath) return null;
14950
- const logicalId = options.logicalId ?? "import";
14951
- const logger = options.retry?.logger ?? getLogger();
14952
- const maxWalkMs = options.retry?.maxWalkMs ?? DEFAULT_MAX_WALK_MS;
14953
- const maxPages = options.retry?.maxPages ?? DEFAULT_MAX_PAGES;
14954
- const retryOpts = {
14955
- maxRetries: options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
14956
- initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS,
14957
- maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
14958
- isRetryable: (message, error) => isThrottlingLikeError(error, message),
14959
- logger,
14960
- ...options.retry?.isInterrupted && { isInterrupted: options.retry.isInterrupted },
14961
- ...options.retry?.onInterrupted && { onInterrupted: options.retry.onInterrupted },
14962
- ...options.retry?.sleep && { sleep: options.retry.sleep }
14963
- };
14964
- const startedAt = Date.now();
14965
- const assertWithinBudget = (stage) => {
14966
- const elapsed = Date.now() - startedAt;
14967
- if (elapsed >= maxWalkMs) throw new ImportTagWalkLimitError(`Timed out looking up ${logicalId} by its aws:cdk:path tag after ${Math.round(elapsed / 1e3)}s (limit ${Math.round(maxWalkMs / 1e3)}s, while ${stage}). AWS is likely throttling the lookup; retry, or pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
14968
- };
14969
- let marker;
14970
- let pages = 0;
14971
- do {
14972
- assertWithinBudget("listing candidates");
14973
- if (++pages > maxPages) throw new ImportTagWalkLimitError(`Gave up looking up ${logicalId} by its aws:cdk:path tag after ${maxPages} pages (the service may be returning a non-advancing pagination token). Pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
14974
- const currentMarker = marker;
14975
- const page = await withRetry(() => listPage(currentMarker), logicalId, retryOpts);
14976
- for (const summary of page.items ?? []) {
14977
- assertWithinBudget("reading candidate tags");
14978
- const detail = await withRetry(() => describe(summary), logicalId, retryOpts);
14979
- if (detail === void 0) {
14980
- logger.debug(` ↷ Skipped an ${logicalId} import candidate: its detail lookup returned no result`);
14981
- continue;
15012
+ /**
15013
+ * Info-log every silent-drop routing decision (auto-route via CC API) and
15014
+ * warn-log every silent drop the user explicitly opted into via
15015
+ * `--allow-unsupported-properties` (forced SDK path, the property will
15016
+ * be dropped). Does not mutate state; throws ONLY for the CC-route
15017
+ * viability guard (un-allowed silent drops on a type Cloud Control cannot
15018
+ * manage see {@link buildUnroutableSilentDropMessage}).
15019
+ *
15020
+ * Issue [#614](https://github.com/go-to-k/cdkd/issues/614). Replaces the
15021
+ * pre-v0.16x throw path: silent drops are now a routing signal, not an
15022
+ * error (except the viability guard above).
15023
+ *
15024
+ * When the optional `provisionedBy` (from existing state) is `'cc-api'`,
15025
+ * the auto-route info line is demoted to `debug` — the resource has been
15026
+ * on CC for at least one prior deploy, so the routing decision is
15027
+ * **continuation of sticky state, not a fresh auto-route**. Surfacing the
15028
+ * info line every deploy would be repetitive noise. The warn line for
15029
+ * explicit `--allow-unsupported-properties` overrides is NOT demoted —
15030
+ * that override is an active user choice for THIS deploy and should
15031
+ * surface every time.
15032
+ */
15033
+ reportSilentDropDecisions(resources) {
15034
+ for (const { logicalId, resourceType, properties, provisionedBy } of resources) {
15035
+ const drops = findSilentDropProperties(resourceType, properties);
15036
+ if (drops.length === 0) continue;
15037
+ const overridden = [];
15038
+ const autoRouted = [];
15039
+ for (const { property } of drops) {
15040
+ const allowKey = `${resourceType}:${property}`;
15041
+ if (this.allowedUnsupportedProperties.has(allowKey)) overridden.push(property);
15042
+ else autoRouted.push(property);
15043
+ }
15044
+ if (autoRouted.length > 0) {
15045
+ const provider = this.providers.get(resourceType);
15046
+ if (isNonProvisionable(resourceType) || provider?.disableCcApiFallback === true) throw new Error(`${logicalId}: ${this.buildUnroutableSilentDropMessage(resourceType, drops.filter((d) => autoRouted.includes(d.property)))}`);
15047
+ const message = `${logicalId} (${resourceType}): routing via Cloud Control API (cdkd's SDK Provider does not yet wire ${autoRouted.join(", ")} — CC API will forward the full property map. Override via --allow-unsupported-properties ${autoRouted.map((p) => `${resourceType}:${p}`).join(",")}.)`;
15048
+ if (provisionedBy === "cc-api") this.logger.debug(message);
15049
+ else this.logger.info(message);
15050
+ }
15051
+ if (overridden.length > 0) {
15052
+ const propList = overridden.join(", ");
15053
+ this.logger.warn(`${logicalId} (${resourceType}): ${propList} will be silently dropped (--allow-unsupported-properties override accepted). Remove the override to route this resource via Cloud Control API instead.`);
14982
15054
  }
14983
- if (matchesCdkPath(tagsOf(detail, summary), cdkPath)) return {
14984
- summary,
14985
- detail
14986
- };
14987
15055
  }
14988
- marker = page.nextMarker;
14989
- } while (marker);
14990
- return null;
15056
+ }
15057
+ /**
15058
+ * Pure-functional discovery of every resource whose template uses one or
15059
+ * more silent-drop properties that are NOT in the
15060
+ * `--allow-unsupported-properties` allow set — i.e. every resource that
15061
+ * {@link getProviderFor} would auto-route via Cloud Control. Returned
15062
+ * entries carry the silent-drop property names so plan / diff renderers
15063
+ * can show `[via CC API: RuntimeManagementConfig]`.
15064
+ *
15065
+ * Does NOT log or throw. Use {@link reportSilentDropDecisions} for the
15066
+ * side-effecting info / warn surface.
15067
+ */
15068
+ findAutoRouteHits(resources) {
15069
+ const hits = [];
15070
+ for (const { logicalId, resourceType, properties } of resources) {
15071
+ const actionable = findActionableSilentDrops(resourceType, properties, this.allowedUnsupportedProperties);
15072
+ if (actionable.length === 0) continue;
15073
+ hits.push({
15074
+ logicalId,
15075
+ resourceType,
15076
+ properties: actionable.map((d) => d.property)
15077
+ });
15078
+ }
15079
+ return hits;
15080
+ }
15081
+ };
15082
+ function isCustomResource(resourceType) {
15083
+ return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
14991
15084
  }
14992
15085
 
14993
15086
  //#endregion
@@ -16280,7 +16373,7 @@ var DeployEngine = class {
16280
16373
  this.options = options;
16281
16374
  this.stackRegion = stackRegion;
16282
16375
  this.exportIndexStore = exportIndexStore;
16283
- this.resolver = new IntrinsicFunctionResolver(stackRegion);
16376
+ this.resolver = new IntrinsicFunctionResolver(stackRegion, { strictGetAtt: options.strictGetAtt ?? false });
16284
16377
  this.options.concurrency = options.concurrency ?? 10;
16285
16378
  this.options.dryRun = options.dryRun ?? false;
16286
16379
  this.options.lockTimeout = options.lockTimeout ?? 300 * 1e3;
@@ -16295,6 +16388,7 @@ var DeployEngine = class {
16295
16388
  async deploy(stackName, template) {
16296
16389
  this.recordedImports = [];
16297
16390
  this.recordedOutputReads = [];
16391
+ this.resolver.resetPhysicalIdFallbackCount();
16298
16392
  return withStackName(stackName, () => this.doDeploy(stackName, template));
16299
16393
  }
16300
16394
  /**
@@ -16611,7 +16705,8 @@ var DeployEngine = class {
16611
16705
  deleted: 0,
16612
16706
  unchanged: Object.keys(currentState.resources).length,
16613
16707
  durationMs: Date.now() - startTime,
16614
- outputs: this.buildDisplayOutputs(template, persistedOutputs)
16708
+ outputs: this.buildDisplayOutputs(template, persistedOutputs),
16709
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16615
16710
  };
16616
16711
  }
16617
16712
  const createChanges = this.diffCalculator.filterByType(changes, "CREATE");
@@ -16626,9 +16721,11 @@ var DeployEngine = class {
16626
16721
  updated: updateChanges.length,
16627
16722
  deleted: deleteChanges.length,
16628
16723
  unchanged: this.diffCalculator.filterByType(changes, "NO_CHANGE").length,
16629
- durationMs: Date.now() - startTime
16724
+ durationMs: Date.now() - startTime,
16725
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16630
16726
  };
16631
16727
  }
16728
+ this.resolver.resetPhysicalIdFallbackCount();
16632
16729
  const progress = {
16633
16730
  current: 0,
16634
16731
  total: createChanges.length + updateChanges.length + deleteChanges.length
@@ -16647,7 +16744,8 @@ var DeployEngine = class {
16647
16744
  deleted: actualCounts.deleted,
16648
16745
  unchanged: unchangedCount,
16649
16746
  durationMs,
16650
- outputs: this.buildDisplayOutputs(template, newState.outputs ?? {})
16747
+ outputs: this.buildDisplayOutputs(template, newState.outputs ?? {}),
16748
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16651
16749
  };
16652
16750
  } finally {
16653
16751
  renderer.stop();
@@ -16861,7 +16959,13 @@ var DeployEngine = class {
16861
16959
  }
16862
16960
  throw error;
16863
16961
  }
16864
- const outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
16962
+ let outputs;
16963
+ try {
16964
+ outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
16965
+ } catch (outputError) {
16966
+ await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
16967
+ throw outputError;
16968
+ }
16865
16969
  return {
16866
16970
  state: {
16867
16971
  version: 8,
@@ -16877,6 +16981,58 @@ var DeployEngine = class {
16877
16981
  };
16878
16982
  }
16879
16983
  /**
16984
+ * Persist state after provisioning fully succeeded but output resolution
16985
+ * threw (only reachable under `--strict-getatt`, whose promotion fires
16986
+ * AFTER the rollback catch block). The persisted shape mirrors the
16987
+ * success-path state EXCEPT for outputs:
16988
+ *
16989
+ * - `resources`: this run's provisioning result (`newResources`) — every
16990
+ * create/update/delete landed in AWS, so state must record it.
16991
+ * - `imports` / `outputReads`: this run's `recordedImports` /
16992
+ * `recordedOutputReads` (the provisioning that produced them succeeded;
16993
+ * dropping them would desync the strong-reference records from AWS —
16994
+ * matters on the update-deploy path where the pre-deploy snapshot may
16995
+ * be stale).
16996
+ * - `outputs`: the PREVIOUSLY persisted map — resolveOutputs threw before
16997
+ * producing a new one, mirroring what a resource-failure persist keeps.
16998
+ * The exports index is deliberately NOT updated (it stays consistent
16999
+ * with the old outputs that remain in state).
17000
+ *
17001
+ * ETag handling mirrors the post-rollback save: expected-ETag first (or
17002
+ * unconditional when `pendingMigration` — same as the per-resource save),
17003
+ * then a fresh-ETag retry, then warn. Best-effort: the deploy error being
17004
+ * rethrown is the primary signal; a failed save only warns.
17005
+ */
17006
+ async persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration) {
17007
+ const buildState = () => ({
17008
+ version: 8,
17009
+ region: this.stackRegion,
17010
+ stackName: currentState.stackName,
17011
+ resources: newResources,
17012
+ outputs: currentState.outputs,
17013
+ ...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
17014
+ ...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
17015
+ lastModified: Date.now()
17016
+ });
17017
+ try {
17018
+ const expectedEtag = pendingMigration ? void 0 : currentEtag;
17019
+ await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(buildState()), {
17020
+ ...expectedEtag !== void 0 && { expectedEtag },
17021
+ migrateLegacy: pendingMigration
17022
+ });
17023
+ this.logger.debug("State saved after output resolution failure");
17024
+ } catch (saveError) {
17025
+ this.logger.debug(`Retrying state save after output resolution failure (ETag mismatch): ${saveError instanceof Error ? saveError.message : String(saveError)}`);
17026
+ try {
17027
+ const freshEtag = (await this.stateBackend.getState(stackName, this.stackRegion))?.etag;
17028
+ await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(buildState()), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
17029
+ this.logger.debug("State saved after output resolution failure (retry succeeded)");
17030
+ } catch (retryError) {
17031
+ this.logger.warn(`Failed to save state after output resolution failure: ${retryError instanceof Error ? retryError.message : String(retryError)} — resources were provisioned but not recorded; run deploy again to reconcile.`);
17032
+ }
17033
+ }
17034
+ }
17035
+ /**
16880
17036
  * Perform best-effort rollback of completed operations respecting dependencies
16881
17037
  *
16882
17038
  * - CREATE → delete the newly created resource (in reverse dependency order)
@@ -17624,6 +17780,7 @@ var DeployEngine = class {
17624
17780
  if (typeof exportName === "string") outputs[exportName] = value;
17625
17781
  }
17626
17782
  } catch (error) {
17783
+ if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`);
17627
17784
  this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
17628
17785
  outputs[outputKey] = void 0;
17629
17786
  }
@@ -17642,5 +17799,5 @@ var DeployEngine = class {
17642
17799
  };
17643
17800
 
17644
17801
  //#endregion
17645
- export { BOOTSTRAP_MARKER_PREFIX as $, StateError as $t, refStateLookupFromResource as A, AssemblyReader as At, TemplateParser as B, DependencyError as Bt, ProviderRegistry as C, warnDeprecatedNoPrefixCliFlag as Ct, isTerminationProtectionPropagationError as D, findLargeInlineResources as Dt, disableInstanceApiTermination as E, MIGRATE_TMP_PREFIX as Et, resolveExplicitPhysicalId as F, resetAwsClients as Ft, AssetPublisher as G, MissingCdkCliError as Gt, S3StateBackend as H, LocalMigrateError as Ht, assertRegionMatch as I, setAwsClients as It, buildAssetRedirectMap as J, ProvisioningError as Jt, stringifyValue as K, NestedStackChildDirectDestroyError as Kt, applyRoleArnIfSet as L, AssetError as Lt, CDK_PATH_TAG as M, resolveBucketRegion as Mt, matchesCdkPath as N, AwsClients as Nt, IntrinsicFunctionResolver as O, uploadCfnTemplate as Ot, normalizeAwsTagsToCfn as P, getAwsClients as Pt, AssetModeResolver as Q, StackTerminationProtectionError as Qt, DiffCalculator as R, CdkdError as Rt, isRetryableTransientError as S, resolveUseCdkBootstrapAssets as St, CloudControlProvider as T, CFN_TEMPLATE_URL_LIMIT as Tt, rebuildClientForBucketRegion as U, LocalStartServiceError as Ut, LockManager as V, LocalInvokeBuildError as Vt, shouldRetainResource as W, LockError as Wt, loadPublishableAssetManifest as X, ResourceUpdateNotSupportedError as Xt, createAssetRedirectResolver as Y, ResourceTimeoutError as Yt, rewriteTemplateAssetReferences as Z, StackHasActiveImportsError as Zt, yellow as _, resolveAutoAssetStorage as _t, IMPLICIT_DELETE_DEPENDENCIES as a, __exportAll as an, buildDockerImage as at, importTagWalk as b, resolveStateBucketWithDefault as bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as c, runDockerForeground as ct, formatResourceLine as d, getDockerImageBySourceHash as dt, SynthesisError as en, ensureAssetStorage as et, bold as f, Synthesizer as ft, red as g, resolveApp as gt, green as h, getLegacyStateBucketName as ht, withResourceDeadline as i, withErrorHandling as in, validateContainerRepoName as it, WAFv2WebACLProvider as j, clearBucketRegionCache as jt, cfnRefValueFromPhysicalId as k, expectedOwnerParam as kt, isStatefulRecreateTargetSync as l, runDockerStreaming as lt, gray as m, getDefaultStateBucketName as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as nn, parseBootstrapMarker as nt, computeImplicitDeleteEdges as o, formatDockerLoginError as ot, cyan as p, synthesisStatusMessage as pt, WorkGraph as q, PartialFailureError as qt, DeployEngine as r, normalizeAwsError as rn, validateAssetBucketName as rt, extractDeploymentEventError as s, getDockerCmd as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as tn, getBootstrapMarkerKey as tt, renderStatefulReason as u, AssetManifestLoader as ut, IAMRoleProvider as v, resolveCaptureObservedState as vt, findActionableSilentDrops as w, CFN_TEMPLATE_BODY_LIMIT as wt, withRetry as x, resolveStateBucketWithDefaultAndSource as xt, collectInlinePolicyNamesManagedBySiblings as y, resolveSkipPrefix as yt, DagBuilder as z, ConfigError as zt };
17646
- //# sourceMappingURL=deploy-engine-CFUNbwD1.js.map
17802
+ export { BOOTSTRAP_MARKER_PREFIX as $, StateError as $t, withRetry as A, AssemblyReader as At, TemplateParser as B, DependencyError as Bt, disableInstanceApiTermination as C, warnDeprecatedNoPrefixCliFlag as Ct, refStateLookupFromResource as D, findLargeInlineResources as Dt, cfnRefValueFromPhysicalId as E, MIGRATE_TMP_PREFIX as Et, resolveExplicitPhysicalId as F, resetAwsClients as Ft, AssetPublisher as G, MissingCdkCliError as Gt, S3StateBackend as H, LocalMigrateError as Ht, assertRegionMatch as I, setAwsClients as It, buildAssetRedirectMap as J, ProvisioningError as Jt, stringifyValue as K, NestedStackChildDirectDestroyError as Kt, applyRoleArnIfSet as L, AssetError as Lt, CDK_PATH_TAG as M, resolveBucketRegion as Mt, matchesCdkPath as N, AwsClients as Nt, WAFv2WebACLProvider as O, uploadCfnTemplate as Ot, normalizeAwsTagsToCfn as P, getAwsClients as Pt, AssetModeResolver as Q, StackTerminationProtectionError as Qt, DiffCalculator as R, CdkdError as Rt, CloudControlProvider as S, resolveUseCdkBootstrapAssets as St, IntrinsicFunctionResolver as T, CFN_TEMPLATE_URL_LIMIT as Tt, rebuildClientForBucketRegion as U, LocalStartServiceError as Ut, LockManager as V, LocalInvokeBuildError as Vt, shouldRetainResource as W, LockError as Wt, loadPublishableAssetManifest as X, ResourceUpdateNotSupportedError as Xt, createAssetRedirectResolver as Y, ResourceTimeoutError as Yt, rewriteTemplateAssetReferences as Z, StackHasActiveImportsError as Zt, yellow as _, resolveAutoAssetStorage as _t, IMPLICIT_DELETE_DEPENDENCIES as a, __exportAll as an, buildDockerImage as at, ProviderRegistry as b, resolveStateBucketWithDefault as bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as c, runDockerForeground as ct, formatResourceLine as d, getDockerImageBySourceHash as dt, SynthesisError as en, ensureAssetStorage as et, bold as f, Synthesizer as ft, red as g, resolveApp as gt, green as h, getLegacyStateBucketName as ht, withResourceDeadline as i, withErrorHandling as in, validateContainerRepoName as it, isRetryableTransientError as j, clearBucketRegionCache as jt, importTagWalk as k, expectedOwnerParam as kt, isStatefulRecreateTargetSync as l, runDockerStreaming as lt, gray as m, getDefaultStateBucketName as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as nn, parseBootstrapMarker as nt, computeImplicitDeleteEdges as o, formatDockerLoginError as ot, cyan as p, synthesisStatusMessage as pt, WorkGraph as q, PartialFailureError as qt, DeployEngine as r, normalizeAwsError as rn, validateAssetBucketName as rt, extractDeploymentEventError as s, getDockerCmd as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as tn, getBootstrapMarkerKey as tt, renderStatefulReason as u, AssetManifestLoader as ut, IAMRoleProvider as v, resolveCaptureObservedState as vt, isTerminationProtectionPropagationError as w, CFN_TEMPLATE_BODY_LIMIT as wt, findActionableSilentDrops as x, resolveStateBucketWithDefaultAndSource as xt, collectInlinePolicyNamesManagedBySiblings as y, resolveSkipPrefix as yt, DagBuilder as z, ConfigError as zt };
17803
+ //# sourceMappingURL=deploy-engine-DOPBIRKR.js.map