@go-to-k/cdkd 0.284.22 → 0.284.24

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.
@@ -16860,7 +16860,7 @@ var CloudControlProvider = class {
16860
16860
  if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
16861
16861
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
16862
16862
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
16863
- const { ASGProvider } = await import("./asg-provider-DLU0EEhO.js").then((n) => n.n);
16863
+ const { ASGProvider } = await import("./asg-provider-Bh0odlSk.js").then((n) => n.n);
16864
16864
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
16865
16865
  }
16866
16866
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -17575,6 +17575,47 @@ const CR_NO_SERVICE_TOKEN_SKIP_REASON = "no ServiceToken in state — Delete han
17575
17575
  */
17576
17576
  const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did not complete — resource unproven";
17577
17577
  /**
17578
+ * Fourth sibling of the three above, for the arm where the handler RAN, was
17579
+ * reached, and answered `Status: 'FAILED'` (issue
17580
+ * [#2054](https://github.com/go-to-k/cdkd/issues/2054)).
17581
+ *
17582
+ * The terminal FAILED arm used to warn and fall through to `return undefined`,
17583
+ * which `deleteSkipReason` reads as DELETED — so cdkd dropped the state
17584
+ * record, printed the row as deleted and exited 0 over a resource the handler
17585
+ * had EXPLICITLY said it did not delete. It is the same silent-orphan class
17586
+ * {@link CR_DELETE_INVOKE_FAILED_SKIP_REASON} removed from the throw arm,
17587
+ * reached through the handler's RESPONSE instead.
17588
+ *
17589
+ * **Unconditional, with no already-gone classifier.** A handler that reports
17590
+ * FAILED because the thing it manages was already absent is a real and common
17591
+ * shape, and today's leniency lets those destroys finish green. Classifying
17592
+ * the reason to keep them green was rejected: the reason is free text a user's
17593
+ * handler writes, so any classifier is a guess, and a wrong guess
17594
+ * re-introduces exactly the orphan this arm exists to stop.
17595
+ *
17596
+ * This is therefore a COMPATIBILITY BREAK: a destroy whose delete handler
17597
+ * reports FAILED now exits 2 with the record kept, where it used to exit 0
17598
+ * with the record dropped.
17599
+ *
17600
+ * **The two callers have DIFFERENT escape hatches, and only one of them is a
17601
+ * flag.** On `cdkd deploy` the skip is forced back to exit 0 by
17602
+ * `--allow-unaddressed` (issue #1960, the flag that settled the analogous
17603
+ * exit-code question). `cdkd destroy` has no such flag — a skip raises
17604
+ * `PartialFailureError` unconditionally (`src/cli/commands/destroy.ts`) — so
17605
+ * there the remedy is the one that command's own summary names: confirm the
17606
+ * resource is gone, then drop the record with `cdkd state orphan <stack>`.
17607
+ * Messages must not offer the flag on the destroy path, which is the path this
17608
+ * arm is mostly reached from.
17609
+ *
17610
+ * **Fixed wording, no interpolation**, for the reason spelled out on
17611
+ * {@link CR_DELETE_INVOKE_FAILED_SKIP_REASON}: the handler's own `Reason` is a
17612
+ * user-authored string, it goes out on the `logger.warn` beside this, and a
17613
+ * `Reason` carrying `does not exist` / `not found` would make the deploy-side
17614
+ * replacement sites classify the skip as "already gone" and drop the record
17615
+ * one layer further out.
17616
+ */
17617
+ const CR_DELETE_HANDLER_FAILED_SKIP_REASON = "Delete handler reported FAILED — resource unproven";
17618
+ /**
17578
17619
  * The deploy-side caveat both skip warnings in this file carry (issue
17579
17620
  * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
17580
17621
  *
@@ -17585,6 +17626,29 @@ const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did n
17585
17626
  * torn down by hand. Mirrors the caveat `compositeIdFormatMessage` already
17586
17627
  * carries for the composite-id family.
17587
17628
  */
17629
+ /**
17630
+ * The bound BOTH delete-path skips in this file have to state (found in review
17631
+ * of issue [#2054](https://github.com/go-to-k/cdkd/issues/2054)).
17632
+ *
17633
+ * A skip KEEPS the state record, and the natural thing to promise is that a
17634
+ * re-run retries the handler. **On `cdkd destroy` that promise is false**, and
17635
+ * it is false in the direction that matters. `destroy-runner.ts` walks every
17636
+ * reverse-DAG level regardless of skips, so the SAME run that skipped the
17637
+ * custom resource goes on to delete its backing Lambda. The next
17638
+ * `cdkd destroy` therefore reaches the issue-#804 pre-check above, finds the
17639
+ * function gone, and treats the resource as already deleted — dropping the
17640
+ * record and exiting 0 over a resource the handler explicitly refused to
17641
+ * remove, which is the very silent orphan #2054 removed one run earlier.
17642
+ *
17643
+ * Closing it properly means making that pre-check answer `'skipped'` when the
17644
+ * teardown was never PROVEN, which needs a durable "a prior run skipped this"
17645
+ * signal. Every candidate is outside this file: a `ResourceState` field (a
17646
+ * state-schema bump), or a `DeleteContext` flag threaded from
17647
+ * `destroy-runner.ts`. So the record is described here as what it actually is
17648
+ * — a POINTER to something that has to be torn down by hand — rather than as a
17649
+ * retry that will not happen.
17650
+ */
17651
+ const CR_SKIP_NOT_A_RETRY_CAVEAT = "NOTE this record is a POINTER, not a retry: the same destroy run deletes the backing Lambda, so the next 'cdkd destroy' finds the handler gone and DROPS this record (issue 804 pre-check). Tear the resource down by hand, then clear the stack's records with 'cdkd state orphan <stack>' — that command drops EVERY record for the stack, not just this one.";
17588
17652
  const DEPLOY_SKIP_CAVEAT = "NOTE this arm is ALSO reached from cdkd deploy. Since issue 1762 the DELETE of a resource removed from the template behaves like destroy — the record is KEPT and the next deploy re-attempts it — but a REPLACEMENT / rollback delete FAILS the resource instead (https://github.com/go-to-k/cdkd/issues/1762), leaving the old one untracked; there, tear the resource down by hand.";
17589
17653
  /**
17590
17654
  * Type guard to validate Lambda response payload structure
@@ -17898,25 +17962,50 @@ const CR_LOG_TAIL_BOILERPLATE = /^(START|END|REPORT|XRAY|INIT_START|INIT_REPORT|
17898
17962
  */
17899
17963
  const SNS_SERVICE_TOKEN_ARN_RE = /^arn:aws[a-z0-9-]*:sns:/;
17900
17964
  /**
17965
+ * Account segment {@link syntheticStackId} falls back to when STS could not
17966
+ * answer (`AwsAccountInfo.fabricated`, issue
17967
+ * [#1730](https://github.com/go-to-k/cdkd/issues/1730)).
17968
+ *
17969
+ * The Cloud Control enrichment sites answer a fabricated account by OMITTING
17970
+ * the value they would have built. That is not available here — `StackId` is a
17971
+ * REQUIRED member of the custom-resource request payload — so the choice is
17972
+ * between two wrong strings, and the honest one is the one a handler cannot
17973
+ * mistake for real. `getAccountInfo`'s own fallback id (`123456789012`) is
17974
+ * shaped exactly like a live account; the all-zero id is not a valid AWS
17975
+ * account and reads as the placeholder it is.
17976
+ */
17977
+ const SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT = "000000000000";
17978
+ /**
17901
17979
  * The synthetic `StackId` handed to a custom-resource handler in place of the
17902
17980
  * CloudFormation stack ARN cdkd does not have.
17903
17981
  *
17904
- * **Its `arn:aws:` prefix is deliberately NOT partition-derived** (issue
17905
- * #1815, which fixed the SNS routing predicate above). Every segment of this
17906
- * value is fabricated — the region is a fixed `us-east-1` rather than the
17907
- * deploy region, and the account is the all-zero placeholder so it addresses
17908
- * nothing and cannot be made to. Deriving ONLY the partition would produce a
17909
- * strictly LESS coherent ARN (`arn:aws-cn:cloudformation:us-east-1:0000...`,
17910
- * a China partition carrying a commercial region) while fixing nothing a
17911
- * handler could rely on. The coherent fix is to synthesize the real
17912
- * partition / region / account together, which changes what every handler
17913
- * observes and belongs in its own change.
17982
+ * Partition / region / account are synthesized TOGETHER from the real deploy
17983
+ * context (issue [#1866](https://github.com/go-to-k/cdkd/issues/1866)). Every
17984
+ * segment used to be fabricated — `arn:aws:cloudformation:us-east-1:0000...`
17985
+ * regardless of where the deploy actually ranand CloudFormation-authored
17986
+ * handlers DO read `event.StackId`: to re-derive the region / account they are
17987
+ * running in, to build ARNs, to name log streams, to correlate a response.
17988
+ * Each of those read a coherent-looking ARN and got an answer that addresses
17989
+ * nothing.
17990
+ *
17991
+ * Deriving only ONE segment is worse than deriving none, which is why issue
17992
+ * #1815 deliberately left the hardcoded `arn:aws:` prefix alone rather than
17993
+ * partition-deriving it in isolation: `arn:aws-cn:cloudformation:us-east-1:…`
17994
+ * is a China partition carrying a commercial region, strictly LESS coherent
17995
+ * than a uniformly-commercial fabrication. So this takes the whole
17996
+ * {@link AwsAccountInfo} — where `partition` is already derived FROM `region`
17997
+ * — rather than any one field.
17998
+ *
17999
+ * The stack-NAME segment stays synthetic (`cdkd-<logicalId>`): cdkd has no
18000
+ * CloudFormation stack, so there is no real value to put there.
17914
18001
  *
17915
18002
  * Factored into one place so the rationale cannot go stale against two other
17916
- * copies: the create / update / delete request builders all use it.
18003
+ * copies: the create / update / delete request builders all use it, through
18004
+ * {@link CustomResourceProvider.resolveSyntheticStackId}.
17917
18005
  */
17918
- function syntheticStackId(logicalId) {
17919
- return `arn:aws:cloudformation:us-east-1:000000000000:stack/cdkd-${logicalId}/cdkd`;
18006
+ function syntheticStackId(logicalId, accountInfo) {
18007
+ const account = accountInfo.fabricated ? SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT : accountInfo.accountId;
18008
+ return `arn:${accountInfo.partition}:cloudformation:${accountInfo.region}:${account}:stack/cdkd-${logicalId}/cdkd`;
17920
18009
  }
17921
18010
  /**
17922
18011
  * `true` when the tail contains at least one line the HANDLER produced.
@@ -18109,11 +18198,29 @@ var CustomResourceProvider = class CustomResourceProvider {
18109
18198
  * the placeholder `PutObject`'s `withRetry` takes no knob either.
18110
18199
  */
18111
18200
  preDeliveryAuthzMaxRetries = 26;
18201
+ /**
18202
+ * The region the client bag this provider was built from was EXPLICITLY
18203
+ * configured with, or `undefined` (issue #1866).
18204
+ *
18205
+ * Captured in the constructor, beside the clients, rather than read per call:
18206
+ * `cdkd deploy` builds a region-configured `AwsClients` and a fresh
18207
+ * `ProviderRegistry` per stack, and with `--stack-concurrency` (default 4) it
18208
+ * swaps the process-global bag while other stacks are mid-flight — so a
18209
+ * call-time read can hand a SIBLING stack's region. Pairing it with the
18210
+ * clients keeps the two consistent by construction.
18211
+ *
18212
+ * `AwsClients.configuredRegion` is deliberately the only region a client bag
18213
+ * will answer (see its own note on why `client.config.region()` is unsound),
18214
+ * and `undefined` means no region was pinned anywhere — which
18215
+ * {@link getAccountInfo} then resolves from `AWS_REGION` itself.
18216
+ */
18217
+ configuredRegion;
18112
18218
  constructor(config) {
18113
18219
  const awsClients = getAwsClients();
18114
18220
  this.lambdaClient = awsClients.lambda;
18115
18221
  this.snsClient = awsClients.sns;
18116
18222
  this.s3Client = awsClients.s3;
18223
+ this.configuredRegion = awsClients.configuredRegion;
18117
18224
  this.responseBucket = config?.responseBucket;
18118
18225
  this.responsePrefix = config?.responsePrefix ?? "custom-resource-responses";
18119
18226
  this.asyncResponseTimeoutMs = config?.asyncResponseTimeoutMs ?? CustomResourceProvider.DEFAULT_ASYNC_RESPONSE_TIMEOUT_MS;
@@ -18211,6 +18318,22 @@ var CustomResourceProvider = class CustomResourceProvider {
18211
18318
  return this.responseClientResolveInFlight;
18212
18319
  }
18213
18320
  /**
18321
+ * Resolve {@link syntheticStackId} against this deploy's REAL account /
18322
+ * region / partition (issue #1866).
18323
+ *
18324
+ * `getAccountInfo` never throws — it answers a `fabricated` account when STS
18325
+ * cannot, which {@link SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT} handles — so
18326
+ * this cannot turn a working deploy into a failing one on the credential
18327
+ * path. It is resolved ONCE per `create` / `update` / `delete` rather than
18328
+ * per invocation attempt: the value does not vary between attempts, and the
18329
+ * request builder the retry loop re-runs is synchronous.
18330
+ */
18331
+ async resolveSyntheticStackId(logicalId) {
18332
+ const accountInfo = await getAccountInfo(this.configuredRegion);
18333
+ if (accountInfo.fabricated) this.logger.warn(`Custom resource ${logicalId}: STS did not report this deploy's account id, so the synthetic StackId handed to the handler carries the placeholder account ${SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT}. A handler that parses StackId to re-derive the account it is running in will not get a usable one — fix the credentials (or set AWS_ACCOUNT_ID) and re-run.`);
18334
+ return syntheticStackId(logicalId, accountInfo);
18335
+ }
18336
+ /**
18214
18337
  * Create a custom resource by invoking its Lambda handler
18215
18338
  */
18216
18339
  async create(logicalId, resourceType, properties) {
@@ -18225,7 +18348,7 @@ var CustomResourceProvider = class CustomResourceProvider {
18225
18348
  ResponseURL: invocation.responseURL,
18226
18349
  ResourceType: resourceType,
18227
18350
  LogicalResourceId: logicalId,
18228
- StackId: syntheticStackId(logicalId),
18351
+ StackId: invocation.stackId,
18229
18352
  ResourceProperties: this.stringifyProperties(properties)
18230
18353
  }));
18231
18354
  if (cfnResponse.Status === "FAILED") throw new Error(`Custom resource handler returned FAILED: ${cfnResponse.Reason || "Unknown reason"}`);
@@ -18257,7 +18380,7 @@ var CustomResourceProvider = class CustomResourceProvider {
18257
18380
  ResourceType: resourceType,
18258
18381
  LogicalResourceId: logicalId,
18259
18382
  PhysicalResourceId: physicalId,
18260
- StackId: syntheticStackId(logicalId),
18383
+ StackId: invocation.stackId,
18261
18384
  ResourceProperties: this.stringifyProperties(properties),
18262
18385
  OldResourceProperties: this.stringifyProperties(previousProperties)
18263
18386
  }));
@@ -18298,7 +18421,7 @@ var CustomResourceProvider = class CustomResourceProvider {
18298
18421
  }
18299
18422
  if (typeof serviceToken !== "string") throw new ProvisioningError(`Custom Resource ${logicalId}: ServiceToken is not a resolved string ARN (got ${typeof serviceToken}). This usually indicates state was written by a pre-fix cdkd import; re-run \`cdkd import\` or \`cdkd state orphan <stack>\` to recover.`, resourceType, logicalId, physicalId);
18300
18423
  if (!this.isSnsServiceToken(serviceToken) && await this.isBackingLambdaGone(serviceToken)) {
18301
- this.logger.warn(`Backing Lambda for custom resource ${logicalId} no longer exists (${serviceToken}); treating the custom resource as already deleted`);
18424
+ this.logger.warn(`Backing Lambda for custom resource ${logicalId} no longer exists (${serviceToken}); treating the custom resource as already deleted and DROPPING its state record. The handler can never run again, so if its teardown was never PROVEN — e.g. an earlier run reported this resource as skipped (issue 2054) — whatever it manages is still LIVE and is now untracked by cdkd. Check for leftovers before treating the stack as gone.`);
18302
18425
  return;
18303
18426
  }
18304
18427
  try {
@@ -18309,13 +18432,19 @@ var CustomResourceProvider = class CustomResourceProvider {
18309
18432
  ResourceType: resourceType,
18310
18433
  LogicalResourceId: logicalId,
18311
18434
  PhysicalResourceId: physicalId,
18312
- StackId: syntheticStackId(logicalId),
18435
+ StackId: invocation.stackId,
18313
18436
  ResourceProperties: this.stringifyProperties(properties)
18314
18437
  }));
18315
- if (cfnResponse.Status === "FAILED") this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}`);
18316
- else this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
18438
+ if (cfnResponse.Status === "FAILED") {
18439
+ this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}. The handler reported that it did NOT delete, so anything this custom resource manages is LEFT IN PLACE — cdkd is KEEPING the state record and the run exits non-zero. ${CR_SKIP_NOT_A_RETRY_CAVEAT} ('cdkd deploy' also accepts --allow-unaddressed, which forces exit 0; 'cdkd destroy' has no such flag.) ${DEPLOY_SKIP_CAVEAT}`);
18440
+ return {
18441
+ outcome: "skipped",
18442
+ reason: CR_DELETE_HANDLER_FAILED_SKIP_REASON
18443
+ };
18444
+ }
18445
+ this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
18317
18446
  } catch (error) {
18318
- this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}. The Delete handler did not complete, so anything this custom resource manages may still be LIVE — cdkd is KEEPING the state record so a re-run can retry it. ${DEPLOY_SKIP_CAVEAT}`);
18447
+ this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}. The Delete handler did not complete, so anything this custom resource manages may still be LIVE — cdkd is KEEPING the state record and the run exits non-zero. ${CR_SKIP_NOT_A_RETRY_CAVEAT} ${DEPLOY_SKIP_CAVEAT}`);
18319
18448
  return {
18320
18449
  outcome: "skipped",
18321
18450
  reason: CR_DELETE_INVOKE_FAILED_SKIP_REASON
@@ -18391,13 +18520,22 @@ var CustomResourceProvider = class CustomResourceProvider {
18391
18520
  * PRE-delivery throw is safe to replay at all.
18392
18521
  *
18393
18522
  * `buildRequest` is called once per attempt with the fresh invocation so the
18394
- * CFn request body always carries the matching ResponseURL / RequestId.
18523
+ * CFn request body always carries the matching ResponseURL / RequestId. The
18524
+ * synthetic `StackId` rides the same bag although it is stable across
18525
+ * attempts (issue #1866), for an ORDERING reason rather than a freshness one:
18526
+ * resolving it needs an `await`, and every await before the SIGINT watch
18527
+ * below is installed is a window in which Ctrl-C is dead — `docs/
18528
+ * provider-development.md` requires a new wait site to be interruptible, and
18529
+ * the pre-delivery backoff this method owns is 47.75s long.
18530
+ *
18395
18531
  * Returns the final response; the caller decides what a terminal FAILED means
18396
- * (create/update throw, delete warns-and-continues).
18532
+ * (create / update throw; delete warns and returns `'skipped'` — issue
18533
+ * #2054, which replaced its warn-and-continue).
18397
18534
  */
18398
18535
  async invokeCustomResourceWithRetry(serviceToken, logicalId, operation, buildRequest) {
18399
18536
  const watch = this.startInterruptWatch(logicalId);
18400
18537
  try {
18538
+ const stackId = await this.resolveSyntheticStackId(logicalId);
18401
18539
  let preDeliveryRetries = 0;
18402
18540
  let failedResponseRetries = 0;
18403
18541
  for (let attempt = 0;; attempt++) {
@@ -18407,7 +18545,10 @@ var CustomResourceProvider = class CustomResourceProvider {
18407
18545
  let invocation;
18408
18546
  try {
18409
18547
  invocation = await this.prepareInvocation(logicalId, watch);
18410
- const request = buildRequest(invocation);
18548
+ const request = buildRequest({
18549
+ ...invocation,
18550
+ stackId
18551
+ });
18411
18552
  this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
18412
18553
  const sent = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation, () => {
18413
18554
  delivered = true;
@@ -24227,7 +24368,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
24227
24368
  eventType: "ROLLBACK_STARTED",
24228
24369
  stackName
24229
24370
  });
24230
- const resolver = new IntrinsicFunctionResolver(ctx.region);
24371
+ const resolver = new ReplayResolvers(ctx.region);
24231
24372
  const { createOps, otherOps } = partitionOps(operations);
24232
24373
  for (let i = otherOps.length - 1; i >= 0; i--) {
24233
24374
  if (options.isInterrupted?.()) {
@@ -24292,6 +24433,21 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
24292
24433
  * Cognito `client_secret`). Rollback is synth-free, so re-resolve straight from
24293
24434
  * the expression string here.
24294
24435
  *
24436
+ * BOTH SIDES OF A DIFF ARE CLASSIFIED, not just the bag that is written, and
24437
+ * that is deliberate (issue #2057 review). The `revert` / `--revert-failed`
24438
+ * arms call this twice — once for the desired bag and once for the CURRENT /
24439
+ * ATTEMPTED one, which only becomes the provider's `previousProperties`. Two
24440
+ * things make a wrong-region value there consequential rather than cosmetic:
24441
+ * a patch-based provider computes its patch previous-vs-desired, so a wrong
24442
+ * previous side can emit a wrong patch or, when both sides carry the same
24443
+ * expression and resolve to the same wrong value, silently compute a NO-OP and
24444
+ * skip the revert entirely; and every resolved plaintext lands in the SHARED
24445
+ * per-op `secrets` map, which is the redaction needle for the state record this
24446
+ * op persists, so a foreign-region plaintext mis-redacts that record. In
24447
+ * practice both bags carry the SAME expression (state redacts them identically),
24448
+ * so scoping the refusal to the written bag would buy a rare case at the cost of
24449
+ * a rule nobody could apply by reading one call site.
24450
+ *
24295
24451
  * Records each `plaintext -> expression` into `secrets` so the caller can redact
24296
24452
  * the persisted state record back to the expression — the same
24297
24453
  * resolve-for-provider + redact-for-state split the deploy engine applies at its
@@ -24305,28 +24461,365 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
24305
24461
  * `StringList` parameter is public config, stored resolved, and never appears
24306
24462
  * as an expression in the journal.
24307
24463
  */
24308
- async function resolveReplayProps(props, resolver, secrets) {
24464
+ async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId) {
24309
24465
  if (props === void 0) return void 0;
24310
- const ctx = {
24466
+ const resolverContext = {
24311
24467
  template: { Resources: {} },
24312
24468
  resources: {},
24313
24469
  recordedSecretValues: secrets
24314
24470
  };
24315
- const walk = async (v) => {
24316
- if (typeof v === "string") return v.includes("{{resolve:") ? await resolver.resolveDynamicReferences(v, ctx) : v;
24471
+ const walk = async (v, path) => {
24472
+ if (typeof v === "string") {
24473
+ if (!v.includes("{{resolve:")) return v;
24474
+ return await resolveLeafByRegion(v, path, logicalId, execCtx, resolvers, resolverContext);
24475
+ }
24317
24476
  if (Array.isArray(v)) {
24318
24477
  const out = new Array(v.length);
24319
- for (let i = 0; i < v.length; i++) out[i] = await walk(v[i]);
24478
+ for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], `${path}[${i}]`);
24320
24479
  return out;
24321
24480
  }
24322
24481
  if (v !== null && typeof v === "object") {
24323
24482
  const out = {};
24324
- for (const [k, val] of Object.entries(v)) out[k] = await walk(val);
24483
+ for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
24325
24484
  return out;
24326
24485
  }
24327
24486
  return v;
24328
24487
  };
24329
- return await walk(props);
24488
+ return await walk(props, "");
24489
+ }
24490
+ /**
24491
+ * The `{{resolve:<service>:...}}` families whose value can be a SECRET, and
24492
+ * therefore the only ones the region question below is asked about: every
24493
+ * `secretsmanager` reference by spelling, and every `ssm` one, which is secret
24494
+ * exactly when its parameter is a `SecureString` (issue #1901).
24495
+ *
24496
+ * Every OTHER service is `local` because cdkd cannot resolve it at all, NOT
24497
+ * because it is public. `ssm-secure` is the live example and is emphatically
24498
+ * not public: `resolveDynamicReferences` has no arm for it, so the literal
24499
+ * token is passed through to AWS and CloudFormation resolves it SERVER-side.
24500
+ * cdkd never holds its value, so there is no region for cdkd to get wrong —
24501
+ * which is the only reason it can be waved through here.
24502
+ */
24503
+ const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set(["secretsmanager", "ssm"]);
24504
+ /**
24505
+ * Split a `{{resolve:secretsmanager:...}}` inner body into its SECRET_ID.
24506
+ *
24507
+ * Mirrors `IntrinsicFunctionResolver.resolveSecretsManagerReference`'s own
24508
+ * split — including the END-ANCHORED whole-secret form — because a secret ID
24509
+ * may legitimately contain colons (an ARN always does), so `split(':')[1]` is
24510
+ * wrong for exactly the shape this file cares about most.
24511
+ */
24512
+ function secretsManagerSecretId(inner) {
24513
+ const afterService = inner.substring(15);
24514
+ let stringIdx = afterService.indexOf(":SecretString:");
24515
+ let binaryIdx = afterService.indexOf(":SecretBinary:");
24516
+ if (stringIdx < 0 && afterService.endsWith(":SecretString")) stringIdx = afterService.length - 13;
24517
+ if (binaryIdx < 0 && afterService.endsWith(":SecretBinary")) binaryIdx = afterService.length - 13;
24518
+ const delimiterIdx = stringIdx >= 0 && binaryIdx >= 0 ? Math.min(stringIdx, binaryIdx) : stringIdx >= 0 ? stringIdx : binaryIdx;
24519
+ return delimiterIdx >= 0 ? afterService.substring(0, delimiterIdx) : afterService;
24520
+ }
24521
+ /**
24522
+ * The parameter name an `{{resolve:ssm:...}}` reference asks for — byte-for-byte
24523
+ * what `IntrinsicFunctionResolver.resolveSSMReference` passes as `GetParameter`'s
24524
+ * `Name`, which is `parts.slice(1).join(':')` on the colon-split inner body.
24525
+ *
24526
+ * The whole remainder, deliberately, with NOTHING stripped:
24527
+ *
24528
+ * - An SSM dynamic reference CAN name a full ARN. The resolver joins the tail
24529
+ * back together, so `{{resolve:ssm:arn:aws:ssm:us-west-2:111122223333:parameter/db/pw}}`
24530
+ * reaches AWS as that ARN. A `split(':')[1]` here would yield the literal
24531
+ * `'arn'` — a parameter that does not exist — and then report the reference
24532
+ * as region-LESS and refuse it, which is the guess-in-the-other-direction the
24533
+ * `named-region` arm exists to prevent.
24534
+ * - A trailing `:<version>` / `:<label>` is part of the name AS SSM PARSES IT
24535
+ * (`GetParameter` accepts `name:3` / `name:prod`), so stripping it would name
24536
+ * a different thing in the refusal message than the one that would be read.
24537
+ */
24538
+ function ssmParameterName(inner) {
24539
+ return inner.substring(4);
24540
+ }
24541
+ /**
24542
+ * The region an ARN names, or `undefined` for anything that is not an ARN with
24543
+ * a populated region field (`arn:<partition>:<service>:<region>:...`).
24544
+ */
24545
+ function arnRegion(secretId) {
24546
+ if (!secretId.startsWith("arn:")) return void 0;
24547
+ const region = secretId.split(":")[3];
24548
+ return region ? region : void 0;
24549
+ }
24550
+ /**
24551
+ * The producer regions a stack's persisted cross-stack reads name, for
24552
+ * {@link RollbackExecutorContext.importedProducerRegions} (issue #2057).
24553
+ *
24554
+ * Both record kinds count, and for the same reason: each one is a value this
24555
+ * stack read out of ANOTHER region's state, so each one is a way a
24556
+ * foreign-region `{{resolve:...}}` expression can have reached this stack's own
24557
+ * record. `imports` is the strong `Fn::ImportValue` edge; `outputReads` is the
24558
+ * weak `Fn::GetStackOutput` one (schema v8), which is the EASIER of the two to
24559
+ * point across a region boundary because the reference carries its own
24560
+ * `Region` argument.
24561
+ *
24562
+ * Deduplicated case-insensitively, keeping each region's first-recorded
24563
+ * spelling so the refusal message echoes what the user will see in
24564
+ * `state.json`. The consumer's own region is deliberately NOT filtered here —
24565
+ * {@link classifyReplaySecretRegion} does that, because it is the one that
24566
+ * knows which region is asking.
24567
+ *
24568
+ * Exported so the two `RollbackExecutorContext` construction sites derive the
24569
+ * list identically — `cdkd rollback` from the state it loaded, and
24570
+ * `DeployEngine.rollbackExecutorContext` from `crossStackReadsForPartialSave`,
24571
+ * which unions that snapshot with the reads the failing deploy itself made.
24572
+ */
24573
+ function producerRegionsFromState(state) {
24574
+ const seen = /* @__PURE__ */ new Set();
24575
+ const regions = [];
24576
+ for (const entry of [...state.imports ?? [], ...state.outputReads ?? []]) {
24577
+ const canonical = canonicalizeRegion(entry.sourceRegion);
24578
+ if (!canonical || seen.has(canonical)) continue;
24579
+ seen.add(canonical);
24580
+ regions.push(entry.sourceRegion);
24581
+ }
24582
+ return regions;
24583
+ }
24584
+ /**
24585
+ * Decide which region must answer for a single `{{resolve:...}}` expression a
24586
+ * rollback replay is about to re-resolve — issue
24587
+ * [#2057](https://github.com/go-to-k/cdkd/issues/2057).
24588
+ *
24589
+ * WHY A REPLAY CAN BE HOLDING A FOREIGN REGION'S EXPRESSION AT ALL. Since
24590
+ * issue #1934 a cross-stack consumer re-resolves a redacted producer value in
24591
+ * the PRODUCER's region (`reresolveCrossStackValue` /
24592
+ * `resolverForProducerRegion`) — correct, because a Secrets Manager secret or
24593
+ * an SSM `SecureString` of the same NAME in two regions is two independent
24594
+ * values. The plaintext is then recorded into the CONSUMER's
24595
+ * `recordedSecretValues`, so the consumer's `state.json` (and from there the
24596
+ * rollback journal) persists the PRODUCER's spelling of the expression. That is
24597
+ * the right thing to persist, and it is region-less: the reader cannot tell
24598
+ * from the string which region produced it.
24599
+ *
24600
+ * The replay rebuilds its resolver from the CONSUMER's region alone, so
24601
+ * re-resolving that expression locally answers from a same-named secret in the
24602
+ * wrong region and writes it to a LIVE resource. Silent, and on the recovery
24603
+ * path. The rule applied here is the family's, from issue #1957: A NAMED REGION
24604
+ * BINDS; NEVER SUBSTITUTE A GUESS. The three verdicts are that one sentence:
24605
+ *
24606
+ * - **`named-region`** — the expression's SECRET_ID is an ARN, which names its
24607
+ * own region. The region is ESTABLISHED, so it binds: the caller resolves
24608
+ * through a resolver pinned to it ({@link ReplayResolvers.forRegion}) rather
24609
+ * than refusing. Refusing here would be the guess in the other direction.
24610
+ *
24611
+ * cdkd would otherwise get this wrong, which is why the arm exists at all:
24612
+ * `resolveSecretsManagerReference` builds its client from
24613
+ * `this.explicitRegion` and passes the ARN through as an opaque `SecretId`,
24614
+ * and `@aws-sdk/client-secrets-manager`'s endpoint ruleset has NO
24615
+ * ARN-derived endpoint rule (unlike, say, S3 access points), so a
24616
+ * foreign-region ARN is sent to the stack's own regional endpoint. What the
24617
+ * SERVICE then does with it is not something this repo can settle offline —
24618
+ * see the fixture note in
24619
+ * `tests/integration/rollback-cross-region-secret/README.md`. Pinning the
24620
+ * client to the ARN's region is correct either way: if Secrets Manager would
24621
+ * have refused the foreign ARN, this turns a hard failure into a correct
24622
+ * resolution; if it would have honoured it, this reaches the same value by
24623
+ * the documented route. Neither outcome is a regression.
24624
+ *
24625
+ * - **`ambiguous`** — the expression names no region (the plain name form) AND
24626
+ * this stack has a foreign producer region on record
24627
+ * ({@link RollbackExecutorContext.importedProducerRegions}). Nothing on hand
24628
+ * can establish the origin, so the replay refuses instead of guessing.
24629
+ *
24630
+ * KNOWN OVER-REFUSAL, accepted deliberately, and WIDER THAN THE SSM CASE
24631
+ * ALONE — state both, because the second one is the common shape:
24632
+ *
24633
+ * (a) Any NAME-FORM `secretsmanager` reference in a stack that has ANY
24634
+ * foreign producer region on record is refused, even when that secret is
24635
+ * the stack's own purely-local one and has nothing to do with the
24636
+ * cross-region read. The evidence is per-STACK, not per-reference, so one
24637
+ * cross-region export plus one ordinary
24638
+ * `{{resolve:secretsmanager:mysecret:SecretString:pw}}` is enough — and CDK's
24639
+ * `secretValueFromJson` emits exactly that name form, so this is the shape
24640
+ * most people will meet. It also persists: with the union the producer
24641
+ * region stays on record until the next SUCCESSFUL deploy. Per-reference
24642
+ * evidence is what would narrow it, and that needs the region recorded
24643
+ * ALONGSIDE the expression — the persisted-shape change issue #2057
24644
+ * deliberately deferred (its options 1 and 2). Until then the refusal is
24645
+ * loud, names the ARN spelling as the remedy, and is the fail-closed side
24646
+ * of a trade whose other side is a silent wrong-secret write.
24647
+ *
24648
+ * (b) An `ssm` reference is secret only when its parameter is a
24649
+ * `SecureString`, and this arm cannot tell. So a `{{resolve:ssm:/app/env}}`
24650
+ * naming a PUBLIC `String` that reached a persisted bag (issue #2036's
24651
+ * acknowledged over-redaction) is refused too. Narrowing it by
24652
+ * `isRecordedSecretExpression` was considered and REJECTED, and not because
24653
+ * the store is unreachable — it is imported by this very file. It is
24654
+ * unusable: `recordedSecretExpressions` is populated BY resolution, and in
24655
+ * the standalone `cdkd rollback` process nothing has resolved anything when
24656
+ * the first op is classified, so the store is empty and every `ssm` verdict
24657
+ * would come back "not secret" — turning the protection off for exactly the
24658
+ * SecureString case it exists for. Worse, once one op DID resolve a
24659
+ * reference the store would be warm for the next, so the verdict would
24660
+ * depend on OP ORDER. A resolve-the-type-first probe is unsound for the
24661
+ * same reason the whole issue exists: the TYPE is region-dependent (#1957),
24662
+ * so probing locally can report `String` for a name that is `SecureString`
24663
+ * in the producer's region and wave through the very write this refuses.
24664
+ * The residual is therefore a loud, actionable error on a narrow
24665
+ * intersection (an over-redacted public ssm reference AND a cross-region
24666
+ * read on record), which is the fail-closed side of the trade.
24667
+ *
24668
+ * - **`local`** — everything else, which is the overwhelmingly common case:
24669
+ * every non-secret service, every same-region ARN (the ordinary CDK
24670
+ * `secretValueFromJson` shape), and every name-form expression in a stack
24671
+ * with no foreign producer region recorded. Resolved exactly as before this
24672
+ * change.
24673
+ *
24674
+ * A same-region ARN answers `local` even when a foreign producer region IS on
24675
+ * record: the expression settles the question itself, so the weaker evidence
24676
+ * never gets consulted.
24677
+ */
24678
+ function classifyReplaySecretRegion(expression, consumerRegion, importedProducerRegions) {
24679
+ const inner = expression.startsWith("{{resolve:") ? expression.slice(10, -2) : void 0;
24680
+ if (inner === void 0) return { kind: "local" };
24681
+ const service = inner.split(":")[0];
24682
+ if (service === void 0 || !REPLAY_SECRET_SERVICES.has(service)) return { kind: "local" };
24683
+ const secretName = service === "secretsmanager" ? secretsManagerSecretId(inner) : ssmParameterName(inner);
24684
+ if (!secretName) return { kind: "local" };
24685
+ const named = arnRegion(secretName);
24686
+ if (named !== void 0) return canonicalizeRegion(named) === canonicalizeRegion(consumerRegion) ? { kind: "local" } : {
24687
+ kind: "named-region",
24688
+ secretName,
24689
+ region: named
24690
+ };
24691
+ const seen = /* @__PURE__ */ new Set();
24692
+ const foreignProducerRegions = [];
24693
+ for (const candidate of importedProducerRegions ?? []) {
24694
+ const canonical = canonicalizeRegion(candidate);
24695
+ if (!canonical || canonical === canonicalizeRegion(consumerRegion)) continue;
24696
+ if (seen.has(canonical)) continue;
24697
+ seen.add(canonical);
24698
+ foreignProducerRegions.push(candidate);
24699
+ }
24700
+ if (foreignProducerRegions.length === 0) return { kind: "local" };
24701
+ return {
24702
+ kind: "ambiguous",
24703
+ secretName,
24704
+ foreignProducerRegions
24705
+ };
24706
+ }
24707
+ /**
24708
+ * The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
24709
+ * region an ARN-named reference asks for (issue #2057).
24710
+ *
24711
+ * One instance per replay, not per op — the resolved-value cache lives on the
24712
+ * resolver INSTANCE since issue #1933, so a resolver per op would re-fetch every
24713
+ * referenced secret once per op. The pinned siblings are cached here for the
24714
+ * same reason: a 100-op replay of a bag carrying one foreign ARN must pay one
24715
+ * `GetSecretValue`, not a hundred.
24716
+ *
24717
+ * A pinned sibling is a PLAIN resolver, deliberately NOT the resolver class's
24718
+ * own `producerRegionGuest` (which the class sets on the siblings
24719
+ * `resolverForProducerRegion` builds, to stop a foreign region pinning a verdict
24720
+ * in the process-global `recordedSecretExpressions` store — the issue #1933
24721
+ * shape, where an `ssm` parameter whose TYPE differs by region has one region's
24722
+ * verdict decide the other's redaction).
24723
+ *
24724
+ * WHY A GUEST FLAG IS NOT NEEDED HERE, and the argument has to be this one
24725
+ * rather than "only `secretsmanager` routes to a sibling" (that earlier claim
24726
+ * was FALSE — `resolveSSMReference` joins its colon-split tail back together, so
24727
+ * an `ssm` reference CAN name a full ARN and CAN therefore route here):
24728
+ *
24729
+ * {@link ReplayResolvers.forRegion} is reached ONLY from a `named-region`
24730
+ * verdict, which `classifyReplaySecretRegion` returns only when the
24731
+ * SECRET_ID / parameter name starts with `arn:` and carries a region. So a
24732
+ * pinned sibling only ever resolves an expression whose KEY EMBEDS THE
24733
+ * REGION IT IS BEING RESOLVED IN.
24734
+ *
24735
+ * The store is keyed by the expression string alone, and that is exactly what
24736
+ * makes #1933 possible: two regions sharing one key. An ARN-form key cannot be
24737
+ * shared by two regions, so a verdict pinned from a sibling can never contradict
24738
+ * another region's for the same key. If a future change ever routes a
24739
+ * region-LESS expression to `forRegion`, this argument dies with it and the
24740
+ * sibling needs the guest flag.
24741
+ */
24742
+ var ReplayResolvers = class {
24743
+ /** The stack's own resolver — every `local` verdict resolves through this. */
24744
+ primary;
24745
+ pinned = /* @__PURE__ */ new Map();
24746
+ stackRegion;
24747
+ constructor(stackRegion) {
24748
+ this.stackRegion = stackRegion;
24749
+ this.primary = new IntrinsicFunctionResolver(stackRegion);
24750
+ }
24751
+ /** The resolver that must answer for `region` — `primary` when it is the stack's own. */
24752
+ forRegion(region) {
24753
+ const target = canonicalizeRegion(region);
24754
+ if (target === canonicalizeRegion(this.stackRegion)) return this.primary;
24755
+ const cached = this.pinned.get(target);
24756
+ if (cached) return cached;
24757
+ const scoped = new IntrinsicFunctionResolver(target);
24758
+ this.pinned.set(target, scoped);
24759
+ return scoped;
24760
+ }
24761
+ };
24762
+ /**
24763
+ * The refusal an `ambiguous` replay reference throws (issue #2057).
24764
+ *
24765
+ * A plain throw, like the final-snapshot refusals above and for the same
24766
+ * reason: the per-op catch in {@link replaySingle} /
24767
+ * {@link replayFailedOperations} counts it as a failure, which keeps the
24768
+ * journal segment and lets the user re-run once the reference is disambiguated.
24769
+ * Refusing is strictly better than the alternative it replaces — resolving a
24770
+ * producer-region reference against the consumer's region does not fail, it
24771
+ * succeeds with the WRONG credential and writes it to a resource that is live.
24772
+ *
24773
+ * Names the reference, the regions, and the remedy. Never the resolved value:
24774
+ * nothing here has resolved anything yet, and the expression is the same string
24775
+ * `state.json` already stores in the clear.
24776
+ */
24777
+ function regionAmbiguousReplaySecretError(logicalId, propertyPath, secretName, foreignProducerRegions, consumerRegion) {
24778
+ return new CdkdError(`Rollback of ${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`} cannot re-resolve the secret reference '${secretName}': the reference carries no region of its own, and this stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so it may have been resolved in one of those rather than in '${consumerRegion}'. A secret of the same name in two regions is two independent values, so replaying this would write the WRONG secret to a live resource. Refusing instead. Resolve the reference in its own region and set the property directly (or spell it as a full ARN, which names its region and is resolved there), then re-run 'cdkd rollback'.`, "ROLLBACK_SECRET_REGION_AMBIGUOUS");
24779
+ }
24780
+ /**
24781
+ * Re-resolve one LEAF string, sending each `{{resolve:...}}` reference in it to
24782
+ * the region {@link classifyReplaySecretRegion} says must answer (issue #2057).
24783
+ *
24784
+ * Refuses FIRST, over the whole leaf, before any reference is fetched: a leaf
24785
+ * can splice several references together, and resolving the safe ones first
24786
+ * would leave half a credential fetched (and cached, and recorded as a
24787
+ * redaction needle) for an op that is about to be refused anyway.
24788
+ *
24789
+ * Then TWO paths, and the split is deliberate rather than an optimisation:
24790
+ *
24791
+ * - With no foreign-region reference — every leaf on every existing code path
24792
+ * — the leaf goes to `resolveDynamicReferences` WHOLE, exactly as before this
24793
+ * change. That method has its own well-tested substitution semantics (it
24794
+ * collects matches from the ORIGINAL string, so a resolved plaintext that is
24795
+ * itself token-shaped is never re-resolved — issue #1917), and this change
24796
+ * does not want to relitigate any of it.
24797
+ * - With one, the leaf is rebuilt segment by segment so each reference can be
24798
+ * resolved by its OWN region's resolver. `resolveDynamicReferences` resolves
24799
+ * every token in the string it is handed with the one resolver it is called
24800
+ * on, so a mixed leaf cannot be served by a single call. Each token is
24801
+ * resolved ALONE and its result concatenated, which means no resolved value
24802
+ * is ever re-scanned for tokens either.
24803
+ *
24804
+ * `dynamicReferenceTokens` returns the tokens in order and non-overlapping, so
24805
+ * walking the leaf with a moving `indexOf` cursor reproduces their positions
24806
+ * exactly, duplicates included.
24807
+ */
24808
+ async function resolveLeafByRegion(leaf, propertyPath, logicalId, execCtx, resolvers, resolverContext) {
24809
+ const verdicts = dynamicReferenceTokens(leaf).map((token) => [token, classifyReplaySecretRegion(token, execCtx.region, execCtx.importedProducerRegions)]);
24810
+ for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousReplaySecretError(logicalId, propertyPath, verdict.secretName, verdict.foreignProducerRegions, execCtx.region);
24811
+ if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return await resolvers.primary.resolveDynamicReferences(leaf, resolverContext);
24812
+ let out = "";
24813
+ let cursor = 0;
24814
+ for (const [token, verdict] of verdicts) {
24815
+ const at = leaf.indexOf(token, cursor);
24816
+ if (at < 0) throw new CdkdError(`Rollback of ${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`} could not locate a scanned dynamic reference in the value it was scanned from. Refusing rather than resolving it in '${execCtx.region}', which would be the wrong region for a reference that names another one. This is an internal invariant failure — please report it with the resource type and property path.`, "ROLLBACK_SECRET_TOKEN_SCAN_MISMATCH");
24817
+ out += leaf.slice(cursor, at);
24818
+ const resolver = verdict.kind === "named-region" ? resolvers.forRegion(verdict.region) : resolvers.primary;
24819
+ out += await resolver.resolveDynamicReferences(token, resolverContext);
24820
+ cursor = at + token.length;
24821
+ }
24822
+ return out + leaf.slice(cursor);
24330
24823
  }
24331
24824
  /**
24332
24825
  * Redact resolved secret plaintext back out of a post-rollback state record
@@ -24650,7 +25143,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
24650
25143
  case "reverse-replacement": {
24651
25144
  const current = stateResources[op.logicalId];
24652
25145
  const prev = op.previousState;
24653
- const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
25146
+ const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
24654
25147
  logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
24655
25148
  if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
24656
25149
  const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
@@ -24742,8 +25235,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
24742
25235
  resourceType: op.resourceType,
24743
25236
  provisionedBy: op.provisionedBy
24744
25237
  });
24745
- const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
24746
- const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
25238
+ const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets, ctx, op.logicalId);
25239
+ const currentProps = await resolveReplayProps(current.properties, resolver, secrets, ctx, op.logicalId);
24747
25240
  const revertResult = await updateWithRollbackRetry(provider, [
24748
25241
  op.logicalId,
24749
25242
  current.physicalId,
@@ -24803,7 +25296,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
24803
25296
  remainingFailedOps: []
24804
25297
  };
24805
25298
  const { logger } = ctx;
24806
- const resolver = new IntrinsicFunctionResolver(ctx.region);
25299
+ const resolver = new ReplayResolvers(ctx.region);
24807
25300
  const emitEnvelope = options.emitEnvelope === true && failedOps.length > 0;
24808
25301
  if (emitEnvelope) ctx.recordEvent?.({
24809
25302
  eventType: "ROLLBACK_STARTED",
@@ -24894,8 +25387,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
24894
25387
  resourceType: op.resourceType,
24895
25388
  provisionedBy: op.provisionedBy ?? current.provisionedBy
24896
25389
  });
24897
- const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
24898
- const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
25390
+ const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId);
25391
+ const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets, ctx, op.logicalId);
24899
25392
  const revertFailedResult = await updateWithRollbackRetry(provider, [
24900
25393
  op.logicalId,
24901
25394
  current.physicalId,
@@ -25033,7 +25526,7 @@ const FLUSH_INTERVAL_MS = 2e3;
25033
25526
  const FLUSH_EVENT_THRESHOLD = 50;
25034
25527
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
25035
25528
  function getCdkdVersion() {
25036
- return "0.284.22";
25529
+ return "0.284.24";
25037
25530
  }
25038
25531
  /**
25039
25532
  * Generate a time-sortable unique run id, e.g.
@@ -25583,6 +26076,92 @@ function deepEqualValue(a, b) {
25583
26076
  }
25584
26077
  return true;
25585
26078
  }
26079
+ /**
26080
+ * The `imports` / `outputReads` records to persist on a save that is NOT the
26081
+ * final success save — the UNION of the pre-deploy snapshot and what THIS
26082
+ * session resolved (issue
26083
+ * [#2057](https://github.com/go-to-k/cdkd/issues/2057) review).
26084
+ *
26085
+ * Every non-success save used to write `currentState.imports` /
26086
+ * `currentState.outputReads` verbatim, i.e. the PRE-DEPLOY snapshot, while
26087
+ * writing the POST-deploy `newResources` beside it. So a deploy that
26088
+ * introduced a cross-stack read and then failed persisted resources built FROM
26089
+ * that read next to a record that does not mention it. Two consequences, and
26090
+ * only the first is about #2057:
26091
+ *
26092
+ * 1. A rollback journal exists only after a FAILED deploy, so
26093
+ * {@link producerRegionsFromState} saw an empty list on exactly the deploy
26094
+ * that introduces a cross-region secret read — and
26095
+ * `classifyReplaySecretRegion` answered `local`, resolving the producer's
26096
+ * region-less expression in the consumer's region. The refusal was inert
26097
+ * where it mattered most.
26098
+ * 2. INDEPENDENT PRE-EXISTING BUG. `state.imports[]` is what
26099
+ * `findActiveImportConsumers` (`src/cli/commands/destroy-runner.ts`) scans
26100
+ * to refuse destroying a producer while a consumer still imports from it,
26101
+ * and `state.outputReads[]` is what `findDownstreamConsumers`
26102
+ * (`src/cli/commands/recreate-downstream-consumers.ts`) enumerates. A
26103
+ * failed deploy therefore silently DOWNGRADED a fresh strong reference to
26104
+ * no reference: the consumer's resource is live and recorded, its import is
26105
+ * not, and `cdkd destroy` on the producer sails through the strong-ref
26106
+ * pre-flight. This exists on main today, with or without #2057.
26107
+ *
26108
+ * DIRECTION OF THE RESIDUAL, stated rather than left to be discovered: a union
26109
+ * never drops a record, so a stack that STOPS reading across a region keeps the
26110
+ * stale entry until its next SUCCESSFUL deploy, whose save replaces the list
26111
+ * wholesale (`imports: [...this.recordedImports]`). Until then a purely-local
26112
+ * rollback can be refused on the strength of a read the template no longer has.
26113
+ * That is the fail-closed side — a clear error naming the region to reconcile,
26114
+ * versus a silent wrong-secret write — and the same asymmetry already justifies
26115
+ * preserving the snapshot at all (dropping it would strip a live strong-ref
26116
+ * record on every diff-clean deploy).
26117
+ *
26118
+ * THE RULE IS "EVERY SAVE EXCEPT THE TERMINAL SUCCESS ONE", and it is stated
26119
+ * that way rather than as "every non-success save" because the latter is loose
26120
+ * in both directions: the diff-clean no-change save in `doDeploy` is a SUCCESS
26121
+ * outcome and unions anyway (nothing was re-resolved, so the union is an
26122
+ * identity there and one rule beats an exception), while
26123
+ * `persistStateAfterOutputFailure` looks like a success save — provisioning
26124
+ * was clean — and is not one.
26125
+ *
26126
+ * THE ENUMERATION IS NOT KEPT HERE, DELIBERATELY. Two prose counts in this
26127
+ * lane were measured wrong (an "ALL FIVE" that missed
26128
+ * `persistStateAfterOutputFailure`, and a "three post-rollback saves" that is
26129
+ * two), and each wrong count is worse than none: it is the sentence a reader
26130
+ * uses to conclude the rule is already applied everywhere.
26131
+ * `tests/unit/deployment/deploy-engine-cross-stack-read-writers.test.ts`
26132
+ * derives the set instead — it SCANS this file for every `imports:` /
26133
+ * `outputReads:` object key that writes a VALUE and fails on any that is not
26134
+ * the one allow-listed success-path write, with a positive control proving the
26135
+ * scan can see a violation. A save site added here fails that test rather than
26136
+ * escaping silently, so the authority on "where is this applied" is a grep the
26137
+ * test performs, not a number anybody has to maintain.
26138
+ */
26139
+ function crossStackReadsForPartialSave(previous, recordedImports, recordedOutputReads) {
26140
+ const imports = unionCrossStackReads(previous.imports, recordedImports, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.exportName}`);
26141
+ const outputReads = unionCrossStackReads(previous.outputReads, recordedOutputReads, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.outputName}`);
26142
+ return {
26143
+ ...imports.length > 0 && { imports },
26144
+ ...outputReads.length > 0 && { outputReads }
26145
+ };
26146
+ }
26147
+ /**
26148
+ * Concatenate two cross-stack-read lists, dropping a later duplicate of an
26149
+ * identity an earlier entry already carries. First-seen wins, so the PRE-DEPLOY
26150
+ * spelling of a region survives — entries are COMPARED on a canonicalized
26151
+ * region but STORED verbatim, mirroring `producerRegionsFromState`.
26152
+ */
26153
+ function unionCrossStackReads(previous, recorded, identity) {
26154
+ const seen = /* @__PURE__ */ new Set();
26155
+ const out = [];
26156
+ for (const entry of [...previous ?? [], ...recorded]) {
26157
+ if (entry === null || typeof entry !== "object") continue;
26158
+ const key = identity(entry);
26159
+ if (seen.has(key)) continue;
26160
+ seen.add(key);
26161
+ out.push(entry);
26162
+ }
26163
+ return out;
26164
+ }
25586
26165
  var DeployEngine = class {
25587
26166
  logger = getLogger().child("DeployEngine");
25588
26167
  resolver;
@@ -26128,8 +26707,7 @@ var DeployEngine = class {
26128
26707
  stackName: currentState.stackName,
26129
26708
  resources: currentState.resources,
26130
26709
  outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
26131
- ...currentState.imports && currentState.imports.length > 0 && { imports: currentState.imports },
26132
- ...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
26710
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26133
26711
  lastModified: Date.now()
26134
26712
  };
26135
26713
  const saveOptions = {};
@@ -26247,8 +26825,7 @@ var DeployEngine = class {
26247
26825
  stackName: currentState.stackName,
26248
26826
  resources: newResources,
26249
26827
  outputs: currentState.outputs,
26250
- ...currentState.imports && currentState.imports.length > 0 && { imports: currentState.imports },
26251
- ...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
26828
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26252
26829
  lastModified: Date.now()
26253
26830
  };
26254
26831
  const migrate = pendingMigration;
@@ -26378,8 +26955,7 @@ var DeployEngine = class {
26378
26955
  stackName: currentState.stackName,
26379
26956
  resources: newResources,
26380
26957
  outputs: currentState.outputs,
26381
- ...currentState.imports && currentState.imports.length > 0 && { imports: currentState.imports },
26382
- ...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
26958
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26383
26959
  lastModified: Date.now()
26384
26960
  };
26385
26961
  const migrate = pendingMigration;
@@ -26405,7 +26981,7 @@ var DeployEngine = class {
26405
26981
  this.logger.warn("Partial state has been saved. Run 'cdkd deploy' to resume, 'cdkd rollback' to revert, or destroy to clean up.");
26406
26982
  } else {
26407
26983
  await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "auto-rollback-started", initialDeploy);
26408
- autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName)).failures === 0;
26984
+ autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName, currentState)).failures === 0;
26409
26985
  }
26410
26986
  try {
26411
26987
  const postRollbackState = {
@@ -26414,8 +26990,7 @@ var DeployEngine = class {
26414
26990
  stackName: currentState.stackName,
26415
26991
  resources: newResources,
26416
26992
  outputs: currentState.outputs,
26417
- ...currentState.imports && currentState.imports.length > 0 && { imports: currentState.imports },
26418
- ...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
26993
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26419
26994
  lastModified: Date.now()
26420
26995
  };
26421
26996
  await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...currentEtag !== void 0 && { expectedEtag: currentEtag } });
@@ -26431,8 +27006,7 @@ var DeployEngine = class {
26431
27006
  stackName: currentState.stackName,
26432
27007
  resources: newResources,
26433
27008
  outputs: currentState.outputs,
26434
- ...currentState.imports && currentState.imports.length > 0 && { imports: currentState.imports },
26435
- ...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
27009
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26436
27010
  lastModified: Date.now()
26437
27011
  };
26438
27012
  await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
@@ -26497,8 +27071,7 @@ var DeployEngine = class {
26497
27071
  stackName: currentState.stackName,
26498
27072
  resources: newResources,
26499
27073
  outputs: currentState.outputs,
26500
- ...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
26501
- ...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
27074
+ ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
26502
27075
  lastModified: Date.now()
26503
27076
  });
26504
27077
  try {
@@ -26525,8 +27098,8 @@ var DeployEngine = class {
26525
27098
  * command drives identical semantics). Thin wrapper that builds the
26526
27099
  * executor context from the engine's collaborators and delegates.
26527
27100
  */
26528
- async performRollback(completedOperations, stateResources, stackName) {
26529
- const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext());
27101
+ async performRollback(completedOperations, stateResources, stackName, previousState) {
27102
+ const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext(previousState));
26530
27103
  return {
26531
27104
  failures: result.failures,
26532
27105
  warnings: result.warnings
@@ -26585,14 +27158,15 @@ var DeployEngine = class {
26585
27158
  this.logger.info(`The automatic rollback restored the pre-deploy state. The failed resource's pre-failure record was kept — if it was left partially applied, run 'cdkd rollback ${stackName} --revert-failed' to revert it.`);
26586
27159
  }
26587
27160
  /** Build the {@link RollbackExecutorContext} from the engine's fields. */
26588
- rollbackExecutorContext() {
27161
+ rollbackExecutorContext(previousState) {
26589
27162
  return {
26590
27163
  providerRegistry: this.providerRegistry,
26591
27164
  region: this.stackRegion,
26592
27165
  logger: this.logger,
26593
27166
  recordEvent: (event) => this.recordEvent(event),
26594
27167
  finalSnapshotClients: this.options.finalSnapshotClients,
26595
- skipFinalSnapshot: this.options.skipFinalSnapshot
27168
+ skipFinalSnapshot: this.options.skipFinalSnapshot,
27169
+ importedProducerRegions: producerRegionsFromState(crossStackReadsForPartialSave(previousState, this.recordedImports, this.recordedOutputReads))
26596
27170
  };
26597
27171
  }
26598
27172
  /**
@@ -27572,5 +28146,5 @@ var DeployEngine = class {
27572
28146
  };
27573
28147
 
27574
28148
  //#endregion
27575
- export { isTerminationProtectionPropagationError as $, PartialFailureError as $n, getBootstrapMarkerKey as $t, renderStatefulReason as A, expectedOwnerParam as An, applyRoleArnIfSet as At, exportAliasCollisionScrubWarning as B, resetAwsClients as Bn, shouldRetainResource as Bt, isFinalSnapshotError as C, stateBucketExistenceConfirmed as Cn, redactSecretsForState as Ct, extractDeploymentEventError as D, MIGRATE_TMP_PREFIX as Dn, s3BucketDualStackDomainName as Dt, makeCanonicalizePropertiesFn as E, CFN_TEMPLATE_URL_LIMIT as En, s3BucketDomainName as Et, green as F, processStackMessages as Fn, DagBuilder as Ft, collectInlinePolicyNamesManagedBySiblings as G, DependencyError as Gn, createAssetRedirectResolver as Gt, secretBearingStateKeyWarning as H, AssetError as Hn, stringifyValue as Ht, red as I, clearBucketRegionCache as In, TemplateParser as It, findActionableSilentDrops as J, LocalMigrateError as Jn, escapeRegExp$1 as Jt, clearOnUpdateRemoval as K, DeployCancelledError as Kn, loadPublishableAssetManifest as Kt, yellow as L, resolveBucketRegion as Ln, LockManager as Lt, bold as M, canonicalizeRegion as Mn, INTRINSIC_KEYS as Mt, cyan as N, derivePartitionAndUrlSuffix as Nn, describeTypeWithThrottleRetry as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, findLargeInlineResources as On, s3BucketRegionalDomainName as Ot, gray as P, AssemblyReader as Pn, withRetry as Pt, disableInstanceApiTermination as Q, NestedStackChildDirectDestroyError as Qn, ensureAssetStorage as Qt, collectDeclaredOutputNames as R, AwsClients as Rn, S3StateBackend as Rt, createPreDeleteFinalSnapshot as S, resolveUseCdkBootstrapAssets as Sn, maskSecretsInText as St, unsupportedFinalSnapshotError as T, CFN_TEMPLATE_BODY_LIMIT as Tn, s3BucketArn as Tt, stateKeySecretExposure as U, CdkdError as Un, WorkGraph as Ut, isExportAliasCollision as V, setAwsClients as Vn, AssetPublisher as Vt, IAMRoleProvider as W, ConfigError as Wn, buildAssetRedirectMap as Wt, CloudControlProvider as X, LockError as Xn, AssetModeResolver as Xt, findSilentDropProperties as Y, LocalStartServiceError as Yn, stripControlChars as Yt, slowCcOperationTimeoutMs as Z, MissingCdkCliError as Zn, BOOTSTRAP_MARKER_PREFIX as Zt, computeImplicitDeleteEdges as _, resolveAutoAssetStorage as _n, STATE_SOURCED_READBACK_RULES as _t, DeploymentEventsStore as a, buildDockerImage as an, StateError as ar, normalizeAwsTagsToCfn as at, buildFinalSnapshotIdentifier as b, resolveStateBucketWithDefault as bn, dynamicReferenceTokens as bt, replayFailedOperations as c, runDockerForeground as cn, isCdkdError as cr, coerceCfnBoolean as ct, updatePartialReason as d, getDockerImageBySourceHash as dn, isMarkedNonRetryable as dr, readConfigString as dt, parseBootstrapMarker as en, ProvisioningError as er, IntrinsicFunctionResolver as et, UNSPECIFIED_SKIP_REASON as f, Synthesizer as fn, isRetryableTransientError as fr, replayWarn as ft, IMPLICIT_DELETE_DEPENDENCIES as g, resolveApp as gn, STATE_SOURCED_CROSS_GENERATION_RULES as gt, maskingRetryLogger as h, getLegacyStateBucketName as hn, __exportAll as hr, requireConfigString as ht, DeploymentEventsReader as i, buildDenyExternalAccessPolicy as in, StackTerminationProtectionError as ir, WAFv2WebACLProvider as it, formatResourceLine as j, PARTITION_TABLE as jn, DiffCalculator as jt, isStatefulRecreateTargetSync as k, uploadCfnTemplate as kn, s3BucketWebsiteUrl as kt, replayRollback as l, runDockerStreaming as ln, normalizeAwsError as lr, configBooleanRefusal as lt, withResourceDeadline as m, getDefaultStateBucketName as mn, markNonRetryable as mr, requireConfigObject as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, validateAssetBucketName as nn, ResourceUpdateNotSupportedError as nr, getAccountInfo as nt, planFailedOps as o, formatDockerLoginError as on, SynthesisError as or, resolveExplicitPhysicalId as ot, deleteSkipReason as p, synthesisStatusMessage as pn, isThrottlingError as pr, requireConfigArray as pt, ProviderRegistry as q, LocalInvokeBuildError as qn, rewriteTemplateAssetReferences as qt, DeployEngine as r, validateContainerRepoName as rn, StackHasActiveImportsError as rr, refStateLookupFromResource as rt, planRollback as s, getDockerCmd as sn, formatError as sr, assertRegionMatch as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, readBootstrapMarkerBody as tn, ResourceTimeoutError as tr, cfnRefValueFromPhysicalId as tt, updatePartialMessage as u, AssetManifestLoader as un, withErrorHandling as ur, configStringRefusal as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, resolveCaptureObservedState as vn, TEMPLATE_SOURCED_RULES as vt, refusesFinalSnapshot as w, warnDeprecatedNoPrefixCliFlag as wn, scrubResourceRecord as wt, ccRoutedFinalSnapshotError as x, resolveStateBucketWithDefaultAndSource as xn, isSingleDynamicReferenceToken as xt, PRE_DELETE_SNAPSHOT_TYPES as y, resolveSkipPrefix as yn, createSecretMasker as yt, collectPublishedOutputNames as z, getAwsClients as zn, rebuildClientForBucketRegion as zt };
27576
- //# sourceMappingURL=deploy-engine-DOlhIeGv.js.map
28149
+ export { disableInstanceApiTermination as $, NestedStackChildDirectDestroyError as $n, ensureAssetStorage as $t, isStatefulRecreateTargetSync as A, uploadCfnTemplate as An, s3BucketWebsiteUrl as At, collectPublishedOutputNames as B, getAwsClients as Bn, rebuildClientForBucketRegion as Bt, createPreDeleteFinalSnapshot as C, resolveUseCdkBootstrapAssets as Cn, maskSecretsInText as Ct, makeCanonicalizePropertiesFn as D, CFN_TEMPLATE_URL_LIMIT as Dn, s3BucketDomainName as Dt, unsupportedFinalSnapshotError as E, CFN_TEMPLATE_BODY_LIMIT as En, s3BucketArn as Et, gray as F, AssemblyReader as Fn, withRetry as Ft, IAMRoleProvider as G, ConfigError as Gn, buildAssetRedirectMap as Gt, isExportAliasCollision as H, setAwsClients as Hn, AssetPublisher as Ht, green as I, processStackMessages as In, DagBuilder as It, ProviderRegistry as J, LocalInvokeBuildError as Jn, rewriteTemplateAssetReferences as Jt, collectInlinePolicyNamesManagedBySiblings as K, DependencyError as Kn, createAssetRedirectResolver as Kt, red as L, clearBucketRegionCache as Ln, TemplateParser as Lt, formatResourceLine as M, PARTITION_TABLE as Mn, DiffCalculator as Mt, bold as N, canonicalizeRegion as Nn, INTRINSIC_KEYS as Nt, extractDeploymentEventError as O, MIGRATE_TMP_PREFIX as On, s3BucketDualStackDomainName as Ot, cyan as P, derivePartitionAndUrlSuffix as Pn, describeTypeWithThrottleRetry as Pt, slowCcOperationTimeoutMs as Q, MissingCdkCliError as Qn, BOOTSTRAP_MARKER_PREFIX as Qt, yellow as R, resolveBucketRegion as Rn, LockManager as Rt, ccRoutedFinalSnapshotError as S, resolveStateBucketWithDefaultAndSource as Sn, isSingleDynamicReferenceToken as St, refusesFinalSnapshot as T, warnDeprecatedNoPrefixCliFlag as Tn, scrubResourceRecord as Tt, secretBearingStateKeyWarning as U, AssetError as Un, stringifyValue as Ut, exportAliasCollisionScrubWarning as V, resetAwsClients as Vn, shouldRetainResource as Vt, stateKeySecretExposure as W, CdkdError as Wn, WorkGraph as Wt, findSilentDropProperties as X, LocalStartServiceError as Xn, stripControlChars as Xt, findActionableSilentDrops as Y, LocalMigrateError as Yn, escapeRegExp$1 as Yt, CloudControlProvider as Z, LockError as Zn, AssetModeResolver as Zt, IMPLICIT_DELETE_DEPENDENCIES as _, resolveApp as _n, STATE_SOURCED_CROSS_GENERATION_RULES as _t, DeploymentEventsStore as a, buildDenyExternalAccessPolicy as an, StackTerminationProtectionError as ar, WAFv2WebACLProvider as at, PRE_DELETE_SNAPSHOT_TYPES as b, resolveSkipPrefix as bn, createSecretMasker as bt, producerRegionsFromState as c, getDockerCmd as cn, formatError as cr, assertRegionMatch as ct, updatePartialMessage as d, AssetManifestLoader as dn, withErrorHandling as dr, configStringRefusal as dt, getBootstrapMarkerKey as en, PartialFailureError as er, isTerminationProtectionPropagationError as et, updatePartialReason as f, getDockerImageBySourceHash as fn, isMarkedNonRetryable as fr, readConfigString as ft, maskingRetryLogger as g, getLegacyStateBucketName as gn, __exportAll as gr, requireConfigString as gt, withResourceDeadline as h, getDefaultStateBucketName as hn, markNonRetryable as hr, requireConfigObject as ht, DeploymentEventsReader as i, validateContainerRepoName as in, StackHasActiveImportsError as ir, refStateLookupFromResource as it, renderStatefulReason as j, expectedOwnerParam as jn, applyRoleArnIfSet as jt, MULTI_REGION_RECREATE_BLOCKED_TYPES as k, findLargeInlineResources as kn, s3BucketRegionalDomainName as kt, replayFailedOperations as l, runDockerForeground as ln, isCdkdError as lr, coerceCfnBoolean as lt, deleteSkipReason as m, synthesisStatusMessage as mn, isThrottlingError as mr, requireConfigArray as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, readBootstrapMarkerBody as nn, ResourceTimeoutError as nr, cfnRefValueFromPhysicalId as nt, planFailedOps as o, buildDockerImage as on, StateError as or, normalizeAwsTagsToCfn as ot, UNSPECIFIED_SKIP_REASON as p, Synthesizer as pn, isRetryableTransientError as pr, replayWarn as pt, clearOnUpdateRemoval as q, DeployCancelledError as qn, loadPublishableAssetManifest as qt, DeployEngine as r, validateAssetBucketName as rn, ResourceUpdateNotSupportedError as rr, getAccountInfo as rt, planRollback as s, formatDockerLoginError as sn, SynthesisError as sr, resolveExplicitPhysicalId as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, parseBootstrapMarker as tn, ProvisioningError as tr, IntrinsicFunctionResolver as tt, replayRollback as u, runDockerStreaming as un, normalizeAwsError as ur, configBooleanRefusal as ut, computeImplicitDeleteEdges as v, resolveAutoAssetStorage as vn, STATE_SOURCED_READBACK_RULES as vt, isFinalSnapshotError as w, stateBucketExistenceConfirmed as wn, redactSecretsForState as wt, buildFinalSnapshotIdentifier as x, resolveStateBucketWithDefault as xn, dynamicReferenceTokens as xt, ATOMIC_FINAL_SNAPSHOT_TYPES as y, resolveCaptureObservedState as yn, TEMPLATE_SOURCED_RULES as yt, collectDeclaredOutputNames as z, AwsClients as zn, S3StateBackend as zt };
28150
+ //# sourceMappingURL=deploy-engine-BJQbni1s.js.map