@go-to-k/cdkd 0.284.10 → 0.284.12

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.
@@ -11095,6 +11095,159 @@ function maskSecretsInText(text, secrets) {
11095
11095
  return text.replace(regex, "***");
11096
11096
  }
11097
11097
  /**
11098
+ * How deep {@link maskSecretsInError} follows a `cause` chain. A CYCLE is
11099
+ * already handled by the visited-set, so this bounds only a pathologically long
11100
+ * chain; the same bounded-walk shape `extractDeploymentEventError` (depth 10)
11101
+ * and the retry classifiers (depth 5) use. A link BEYOND the cap keeps its
11102
+ * original, UNMASKED message, and the last cloned link points at it.
11103
+ */
11104
+ const ERROR_CAUSE_MASK_MAX_DEPTH = 20;
11105
+ /**
11106
+ * The `cause` chain of `root`, root first, stopping at the first non-`Error`
11107
+ * link, at a link already visited (so a cycle terminates instead of hanging),
11108
+ * or at {@link ERROR_CAUSE_MASK_MAX_DEPTH}.
11109
+ */
11110
+ function errorCauseChain(root) {
11111
+ const chain = [];
11112
+ const seen = /* @__PURE__ */ new Set();
11113
+ let current = root;
11114
+ while (current instanceof Error && !seen.has(current) && chain.length < ERROR_CAUSE_MASK_MAX_DEPTH) {
11115
+ seen.add(current);
11116
+ chain.push(current);
11117
+ current = current.cause;
11118
+ }
11119
+ return chain;
11120
+ }
11121
+ /**
11122
+ * Return `error` with {@link maskSecretsInText} applied to the `message` AND the
11123
+ * `stack` of every link in its `cause` chain, with everything else about each
11124
+ * link preserved (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)
11125
+ * review).
11126
+ *
11127
+ * Two bounds on that "every link", both stated here because the PUBLIC contract
11128
+ * is what a caller reads and neither is visible from the call site:
11129
+ * - The walk stops at {@link ERROR_CAUSE_MASK_MAX_DEPTH}. A chain longer than
11130
+ * that keeps its remaining links' ORIGINAL, UNMASKED messages, and the last
11131
+ * cloned link points straight at them.
11132
+ * - A `cause` that is not an `Error` (a string, a plain object) is carried
11133
+ * through VERBATIM and is never masked — the walk has nothing to clone. No
11134
+ * cdkd or AWS SDK site constructs one today, so this is a documented residual
11135
+ * rather than a reachable leak, but a caller attaching arbitrary data as a
11136
+ * `cause` must mask it itself. `src/cli/index.ts`'s `console.error` renders
11137
+ * such a cause via `util.inspect`, so it WOULD reach the terminal.
11138
+ *
11139
+ * **Why an error and not just its text.** `formatError` (`src/utils/error-handler.ts`)
11140
+ * renders a `CdkdError`'s CAUSE as `Caused by: <cause.message>`, and `handleError`
11141
+ * logs that at `error` level for any failure that escapes a command — so a raw
11142
+ * provider error attached as a `ProvisioningError`'s cause reaches the terminal
11143
+ * verbatim, at DEFAULT verbosity, even when every log site that INTERPOLATED
11144
+ * the message masked it. Masking the string at each log site cannot close that:
11145
+ * the sink reads the error OBJECT.
11146
+ *
11147
+ * `formatError` is not the only such sink, and the second one is what makes the
11148
+ * CHAIN argument below concrete rather than hypothetical: `src/cli/index.ts`'s
11149
+ * top-level `main().catch(...)` does `console.error('Fatal error:', error)`,
11150
+ * which renders the whole object through `util.inspect` — every `[cause]` link
11151
+ * AND every link's `stack`. Measured: an outer `Error('top')` wrapping
11152
+ * `Error("Value 'hunter2' failed")` prints as
11153
+ * `Error: top ... { [cause]: Error: Value 'hunter2' failed ... }`. So a
11154
+ * multi-level sink exists TODAY, and it is why `stack` is masked below rather
11155
+ * than merely preserved.
11156
+ *
11157
+ * **Why the whole CHAIN and not just the top link.** Masking only `error.message`
11158
+ * looks sufficient because `formatError` renders one level — and it is not, in
11159
+ * two ways that a top-level-only fix gets exactly backwards. A provider that
11160
+ * wraps an AWS failure in a generic sentence (`new Error('the call failed',
11161
+ * { cause: awsError })`) leaves the plaintext ONE link down, where the
11162
+ * identity-return below then reports "nothing to mask" and hands back an object
11163
+ * still carrying it — the function's own contract says the returned error is
11164
+ * safe to render, and every later reader believes it. And `formatError`
11165
+ * rendering a single level is an implementation detail: one edit there (walking
11166
+ * the chain is the obvious improvement) re-opens the hole with nothing failing.
11167
+ * So the invariant is about the OBJECT, not about today's renderer.
11168
+ *
11169
+ * **Why a clone rather than assigning to `error.message`.** The argument is an
11170
+ * error cdkd did not create — usually the AWS SDK's — and mutating a caller's
11171
+ * object is visible to every other holder of it, including a retry loop that
11172
+ * may still classify it. Each link's clone copies the prototype and EVERY own
11173
+ * property descriptor, symbols included, so the three things that read a cause
11174
+ * chain keep working: `isMarkedNonRetryable` (a non-enumerable `Symbol.for`
11175
+ * marker), `extractDeploymentEventError` / `isThrottlingError` /
11176
+ * `isTransientServerError` (`$metadata`, `Code`, `name`), and the chain itself.
11177
+ * `Object.assign` would have dropped the marker, which is why the descriptors
11178
+ * form is used.
11179
+ *
11180
+ * `message`, `cause` and `stack` are the three descriptors deliberately NOT
11181
+ * copied through: each is re-defined per link, and copying a NON-CONFIGURABLE
11182
+ * original would make that re-definition throw. `cause` is rewired to the CLONE
11183
+ * of whatever it pointed at, in a second pass over the already-built clone map —
11184
+ * which is what makes a cyclic chain terminate rather than recurse. A `cause`
11185
+ * that is not an `Error` (a string, a plain object) keeps its original
11186
+ * descriptor verbatim.
11187
+ *
11188
+ * **Why `stack` is re-defined as DATA rather than copied.** V8 installs `stack`
11189
+ * as an own ACCESSOR whose getter reads a slot the engine attaches to an error
11190
+ * IT created, so copying that descriptor onto an `Object.create` clone yields a
11191
+ * getter with nothing behind it and `clone.stack` reads `undefined` (measured).
11192
+ * That is not a leak today — the clone is only ever reached as a `cause`, and
11193
+ * `handleError` prints the TOP-level error's stack — but this function is
11194
+ * exported and generic, so a future top-level caller would get back an error
11195
+ * with no trace at all. The clone therefore carries a masked COPY of the
11196
+ * original's stack text, which both preserves the trace and closes the sink the
11197
+ * copy would otherwise open: a stack's first line embeds the message, so an
11198
+ * unmasked stack re-exposes exactly the plaintext the `message` mask removed —
11199
+ * and `util.inspect` prints it. An original with no readable string `stack`
11200
+ * (not an engine-created error) simply gets no own `stack`, as before.
11201
+ *
11202
+ * Returns the ORIGINAL object by identity when NOTHING ANYWHERE IN THE CHAIN
11203
+ * changed, so a non-secret failure keeps referential equality and the common
11204
+ * path allocates nothing.
11205
+ */
11206
+ function maskSecretsInError(error, secrets) {
11207
+ if (secrets.size === 0 || !(error instanceof Error)) return error;
11208
+ const chain = errorCauseChain(error);
11209
+ const maskedMessages = chain.map((link) => maskSecretsInText(link.message, secrets));
11210
+ if (maskedMessages.every((masked, i) => masked === chain[i].message)) return error;
11211
+ const clones = /* @__PURE__ */ new Map();
11212
+ for (const [i, original] of chain.entries()) {
11213
+ const descriptors = {};
11214
+ for (const key of Reflect.ownKeys(original)) {
11215
+ if (key === "message" || key === "cause" || key === "stack") continue;
11216
+ const descriptor = Object.getOwnPropertyDescriptor(original, key);
11217
+ if (descriptor) descriptors[key] = descriptor;
11218
+ }
11219
+ const clone = Object.create(Object.getPrototypeOf(original), descriptors);
11220
+ Object.defineProperty(clone, "message", {
11221
+ value: maskedMessages[i],
11222
+ writable: true,
11223
+ enumerable: false,
11224
+ configurable: true
11225
+ });
11226
+ const originalStack = original.stack;
11227
+ if (typeof originalStack === "string") Object.defineProperty(clone, "stack", {
11228
+ value: maskSecretsInText(originalStack, secrets),
11229
+ writable: true,
11230
+ enumerable: false,
11231
+ configurable: true
11232
+ });
11233
+ clones.set(original, clone);
11234
+ }
11235
+ for (const original of chain) {
11236
+ const causeDescriptor = Object.getOwnPropertyDescriptor(original, "cause");
11237
+ if (!causeDescriptor) continue;
11238
+ const clone = clones.get(original);
11239
+ const causeValue = original.cause;
11240
+ const replacement = causeValue instanceof Error ? clones.get(causeValue) : void 0;
11241
+ Object.defineProperty(clone, "cause", replacement ? {
11242
+ value: replacement,
11243
+ writable: true,
11244
+ enumerable: causeDescriptor.enumerable === true,
11245
+ configurable: true
11246
+ } : causeDescriptor);
11247
+ }
11248
+ return clones.get(error);
11249
+ }
11250
+ /**
11098
11251
  * Bind a {@link RecordedSecretValues} bag into a {@link SecretMasker} for a
11099
11252
  * caller to hand to a provider.
11100
11253
  *
@@ -16053,7 +16206,7 @@ var CloudControlProvider = class {
16053
16206
  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);
16054
16207
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
16055
16208
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
16056
- const { ASGProvider } = await import("./asg-provider-ChVCjyK2.js").then((n) => n.n);
16209
+ const { ASGProvider } = await import("./asg-provider-CkIFzX7p.js").then((n) => n.n);
16057
16210
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
16058
16211
  }
16059
16212
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -16741,6 +16894,33 @@ const CR_NO_PROPERTIES_SKIP_REASON = "no properties in state — Delete handler
16741
16894
  */
16742
16895
  const CR_NO_SERVICE_TOKEN_SKIP_REASON = "no ServiceToken in state — Delete handler not invoked";
16743
16896
  /**
16897
+ * Third sibling of the two above, for the arm where cdkd HAD everything it
16898
+ * needed and the Delete request still could not be completed — a permanent
16899
+ * `lambda:InvokeFunction` denial, an exhausted readiness waiter, a response
16900
+ * that never arrived.
16901
+ *
16902
+ * That arm used to swallow the error and return `undefined`, which
16903
+ * `deleteSkipReason` reads as DELETED: `cdkd destroy` printed `✓ … deleted`,
16904
+ * dropped the state record and exited 0 over a handler that never received a
16905
+ * `Delete` — silently orphaning everything that handler manages. It is the
16906
+ * same silent-orphan class issue
16907
+ * [#1752](https://github.com/go-to-k/cdkd/issues/1752) removed from the two
16908
+ * arms above, reached through the catch rather than through a guard.
16909
+ *
16910
+ * **Fixed wording, no interpolation.** The underlying AWS message goes out on
16911
+ * the `logger.warn` beside it and NOT into the reason, because a `reason` is
16912
+ * rendered into the `Error` the deploy-side replacement sites throw, whose
16913
+ * catch classifies an already-deleted resource by SUBSTRING — an AWS message
16914
+ * carrying `does not exist` / `not found` would make a skip read as "already
16915
+ * gone" and drop the record again, one layer further out. Same rule the
16916
+ * `sns-subscription` abort follows.
16917
+ *
16918
+ * The premise is "the resource was NOT destroyed", not "no AWS call was
16919
+ * issued": the handler may have run and failed, or run and had its response
16920
+ * lost. Both leave the resource unproven, which is what a skip asserts.
16921
+ */
16922
+ const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did not complete — resource unproven";
16923
+ /**
16744
16924
  * The deploy-side caveat both skip warnings in this file carry (issue
16745
16925
  * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
16746
16926
  *
@@ -16795,12 +16975,115 @@ function decodeInvokeLogTail(logResult) {
16795
16975
  }
16796
16976
  }
16797
16977
  /**
16978
+ * Recover the backing function's own STATUS fields from an
16979
+ * `@smithy/util-waiter` failure message, or `undefined` when they are not
16980
+ * there (issue #2033).
16981
+ *
16982
+ * The message is `JSON.stringify(result)` and `result.reason` is, for both
16983
+ * Lambda readiness waiters, the ENTIRE `GetFunction` response. Only these
16984
+ * AWS-authored status fields are lifted out of it — never the whole payload,
16985
+ * which carries `Configuration.Environment.Variables` into a durable store.
16986
+ *
16987
+ * Best-effort by construction: a non-JSON message, a different waiter shape, or
16988
+ * a payload with no `Configuration` all yield `undefined`, and the caller falls
16989
+ * back to a fixed sentence.
16990
+ */
16991
+ function extractWaiterFunctionStatus(message) {
16992
+ let parsed;
16993
+ try {
16994
+ parsed = JSON.parse(message);
16995
+ } catch {
16996
+ return;
16997
+ }
16998
+ const config = (parsed?.reason)?.Configuration;
16999
+ if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
17000
+ const fields = config;
17001
+ const parts = [];
17002
+ for (const key of [
17003
+ "State",
17004
+ "StateReasonCode",
17005
+ "StateReason",
17006
+ "LastUpdateStatus",
17007
+ "LastUpdateStatusReasonCode",
17008
+ "LastUpdateStatusReason"
17009
+ ]) {
17010
+ const value = fields[key];
17011
+ if (typeof value === "string" && value !== "") parts.push(`${key}=${value}`);
17012
+ }
17013
+ return parts.length > 0 ? parts.join(", ") : void 0;
17014
+ }
17015
+ /**
17016
+ * Render a Lambda readiness-waiter failure for a message that is persisted
17017
+ * (issue #2033) — see `waitForBackingLambdaReady` for the whole argument.
17018
+ *
17019
+ * TIMEOUT / ABORT keep the waiter's own message: its `observedResponses` keys
17020
+ * are status lines `@smithy/util-waiter` builds itself (`403: <AWS message>`),
17021
+ * which is exactly the diagnostic a stalled waiter needs and carries no
17022
+ * response body. Every other state serialized the full `GetFunction` response,
17023
+ * so that arm reports the error NAME plus the function's own status fields.
17024
+ */
17025
+ function describeWaiterFailure(error) {
17026
+ const name = error instanceof Error ? error.name : "Error";
17027
+ const message = error instanceof Error ? error.message : String(error);
17028
+ if (name === "TimeoutError" || name === "AbortError") return message;
17029
+ return `${name} (${extractWaiterFunctionStatus(message) ?? "no function status reported"}). The waiter's raw payload is withheld because it embeds the whole GetFunction response, environment variables included; run \`aws lambda get-function\` for the detail.`;
17030
+ }
17031
+ /**
16798
17032
  * IAM-authorization-propagation signals in a custom resource FAILED reason that
16799
17033
  * indicate the backing Lambda's freshly-attached execution-role policy has not
16800
17034
  * yet taken effect for its assumed-role session (so a recycle + retry will
16801
17035
  * succeed once IAM settles). Lowercase substrings. Intentionally narrow — these
16802
17036
  * are the IAM-permission-not-yet-effective phrases only, NOT generic transient
16803
17037
  * errors (throttling / timeouts), which must not trigger a CR re-invoke.
17038
+ *
17039
+ * **This set is deliberately NARROWER than
17040
+ * `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` (`src/deployment/retryable-errors.ts`)
17041
+ * and stays that way** (issue
17042
+ * [#2033](https://github.com/go-to-k/cdkd/issues/2033), which asked whether the
17043
+ * narrowing was still intended or a list that had stopped tracking its
17044
+ * counterpart).
17045
+ *
17046
+ * "Narrower", not "a subset" — an earlier revision of this comment said SUBSET
17047
+ * and that was FALSE of the list beside it. Three of these six entries appear
17048
+ * in no form in the shared list (`no identity-based policy allows`, both
17049
+ * `not in the state functionActive` spellings), and a fourth appears there only
17050
+ * ANCHORED: the shared list carries `Firehose is unable to assume role` /
17051
+ * `is unable to assume provided role` / `is unable to assume the role` and
17052
+ * deliberately refuses the bare `is unable to assume` this list uses, so that a
17053
+ * permanent `... is unable to assume role X because of an explicit deny` cannot
17054
+ * burn a retry budget. The bare spelling is right HERE — the text is the
17055
+ * handler's own reason about the race cdkd created — and wrong for AWS-authored
17056
+ * text about a call cdkd made, which is why
17057
+ * {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS} does not re-export it.
17058
+ *
17059
+ * The narrowing is intended, because the two lists are consumed under different
17060
+ * COSTS and classify text with different AUTHORS:
17061
+ *
17062
+ * - This set is matched against the HANDLER's own FAILED `Reason` (and, since
17063
+ * #1674, against arbitrary handler stdout in the log tail). A match here buys
17064
+ * a re-INVOKE, which re-runs the user's `Create` — a non-idempotent handler
17065
+ * repeats partial work, and a Provider-framework `onEvent` can create a
17066
+ * SECOND physical resource and orphan the first (the accepted cost stated on
17067
+ * {@link CR_TRANSIENT_AUTHZ_LOG_SIGNALS}). So the phrases must name the race
17068
+ * cdkd ITSELF created — the backing function's freshly-attached execution
17069
+ * role — and nothing else. Most of the superset's entries describe a
17070
+ * DOWNSTREAM call the handler made (`Invalid principal in policy`,
17071
+ * `Cannot access stream`, `KMS key is invalid for CreateGrant`,
17072
+ * `Invalid InstanceProfile`, …); a re-invoke is not the remedy for those, so
17073
+ * each one would buy a recycle plus an identical re-failure. The three the
17074
+ * issue named specifically — `role defined for the function`, `trust policy`,
17075
+ * `Invalid principal in policy` — are exactly that shape when they arrive in
17076
+ * a handler-authored reason, and the first two are ALREADY covered here in
17077
+ * the spelling that matters (`cannot be assumed` / `is unable to assume` are
17078
+ * what Lambda emits for an unassumable execution role).
17079
+ * - The shared list is matched against text AWS wrote about a call CDKD made.
17080
+ * It is used, in full, by
17081
+ * {@link CustomResourceProvider.isTransientAuthzThrow} for a THROWN error
17082
+ * from one of the provider's OWN SDK calls — see that method for why the
17083
+ * wider list is correct there and costs nothing extra.
17084
+ *
17085
+ * So the answer to "should these converge" is no; what was genuinely missing was
17086
+ * the second consumer, not a wider first one.
16804
17087
  */
16805
17088
  const CR_TRANSIENT_AUTHZ_SIGNALS = [
16806
17089
  "not authorized to perform",
@@ -16811,6 +17094,27 @@ const CR_TRANSIENT_AUTHZ_SIGNALS = [
16811
17094
  "is unable to assume"
16812
17095
  ];
16813
17096
  /**
17097
+ * The CR-specific spellings {@link CustomResourceProvider.isTransientAuthzThrow}
17098
+ * adds ON TOP of `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — i.e. exactly the
17099
+ * phrases the shared list does not carry in any form (issue #2033).
17100
+ *
17101
+ * Deliberately NOT `CR_TRANSIENT_AUTHZ_SIGNALS` itself, which is what the first
17102
+ * cut of the fix used. Three of that list's entries are already covered by the
17103
+ * shared list (`not authorized to perform` / `cannot be assumed`, plus the three
17104
+ * ANCHORED `unable to assume` spellings), so re-uniting the whole thing bought
17105
+ * nothing except the bare, UN-anchored `is unable to assume` — which the shared
17106
+ * list refuses on purpose so a permanent explicit-deny cannot spend a 47.75s
17107
+ * budget before failing. This list is the difference, and only the difference.
17108
+ *
17109
+ * Lower-cased substrings, matched against a lower-cased message (the shared list
17110
+ * is mixed-case and matched verbatim by `isIamPropagationError`).
17111
+ *
17112
+ * `is not in the state functionactive` from the sibling list is omitted as a
17113
+ * pure superstring of the entry below it: any message matching it matches this
17114
+ * one too.
17115
+ */
17116
+ const CR_THROWN_AUTHZ_EXTRA_SIGNALS = ["no identity-based policy allows", "not in the state functionactive"];
17117
+ /**
16814
17118
  * The same IAM-authorization-propagation signals, matched against the backing
16815
17119
  * function's INVOCATION LOG TAIL rather than the FAILED reason (issue #1674).
16816
17120
  *
@@ -16871,6 +17175,37 @@ const CR_TRANSIENT_AUTHZ_LOG_SIGNALS = [
16871
17175
  * at 4 KB regardless, so this only trims the extreme case.
16872
17176
  */
16873
17177
  const CR_LOG_TAIL_WARN_MAX_CHARS = 2e3;
17178
+ /** Default for `CDKD_CR_AUTHZ_MAX_RETRIES` — see `transientAuthzMaxRetries`. */
17179
+ const CR_AUTHZ_MAX_RETRIES_DEFAULT = 2;
17180
+ /**
17181
+ * Hard ceiling for `CDKD_CR_AUTHZ_MAX_RETRIES`.
17182
+ *
17183
+ * The knob's units are RE-INVOCATIONS OF THE USER'S HANDLER, each one also
17184
+ * paying a `recycleBackingFunctionExecEnv` (an `UpdateFunctionConfiguration`
17185
+ * plus a 120s waiter). Ten of those is already far past the point where an
17186
+ * IAM-propagation race would have settled, so anything above it is a typo or a
17187
+ * misunderstanding rather than a preference — and left unclamped a `1e9`
17188
+ * passes the finite / `>= 0` gate and re-invokes until the deploy engine's
17189
+ * per-resource deadline fires an hour later.
17190
+ */
17191
+ const CR_AUTHZ_MAX_RETRIES_CEILING = 10;
17192
+ /**
17193
+ * Bound for the `.cause` walks in this file, matching the depth
17194
+ * `isMarkedNonRetryable` / `isThrottlingError` use in
17195
+ * `src/deployment/retryable-errors.ts`. Bounded rather than unbounded so a
17196
+ * cyclic chain cannot hang the classifier.
17197
+ */
17198
+ const CR_ERROR_CAUSE_MAX_DEPTH = 5;
17199
+ /**
17200
+ * Sleep seam for this provider's hand-rolled waits (the pre-delivery retry
17201
+ * backoff and the S3 response poll).
17202
+ *
17203
+ * Mutable module state ONLY so tests can run a 47.75s retry schedule without
17204
+ * spending 47.75s; production never reassigns it. Mirrors the
17205
+ * `deleteTableRetryDelays.sleep` seam the DynamoDB providers use, and the
17206
+ * `sleep` option `withRetry` already exposes for the same reason.
17207
+ */
17208
+ const customResourceRetryDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
16874
17209
  /**
16875
17210
  * Lines Lambda emits for EVERY invocation regardless of what the handler logged.
16876
17211
  * A tail consisting only of these carries no diagnostic value, and it is the
@@ -17010,6 +17345,24 @@ var CustomResourceProvider = class CustomResourceProvider {
17010
17345
  * exponential backoff for async patterns (CDK Provider framework with
17011
17346
  * isCompleteHandler), so an outer retry adds nothing but the multi-
17012
17347
  * key bug.
17348
+ *
17349
+ * **Opting out of the outer loop is a promise to retry HERE, and that
17350
+ * promise is kept PER CALL — not per attempt.** Issue
17351
+ * [#2033](https://github.com/go-to-k/cdkd/issues/2033) found the claim
17352
+ * backed for exactly one error SHAPE: the internal loop keyed only on the
17353
+ * handler's RETURNED `cfnResponse.Status === 'FAILED'`, and its body had no
17354
+ * `try` / `catch` at any point, so a THROWN error from any AWS SDK call the
17355
+ * provider itself makes left `create()` directly and was single-shot — while
17356
+ * every other resource type got 26 retries over 47.75s for the identical
17357
+ * wording. The per-call decision, in one place:
17358
+ *
17359
+ * | call | retried on a throw? | why |
17360
+ * |---|---|---|
17361
+ * | S3 `PutObject` (response-key placeholder) | YES, own `withRetry` (the standard dense propagation schedule, 47.75s) — and its exhausted throw is `markNonRetryable`d so the loop below cannot spend a SECOND budget on it | idempotent PUT of an empty object at a key cdkd just minted; touches no response-URL lifecycle, so a replay is free |
17362
+ * | Lambda `Invoke` / SNS `Publish` | YES, but only PRE-DELIVERY, on {@link CustomResourceProvider.preDeliveryAuthzMaxRetries} — its OWN budget, the same dense 47.75s schedule every other resource type gets | a replay re-delivers the request, which is the hazard this flag exists for, so the PRE-delivery fence is what makes the budget affordable: nothing has been delivered, so a replay re-invokes NOTHING — see {@link CustomResourceProvider.isTransientAuthzThrow} |
17363
+ * | `waitUntilFunctionActiveV2` / `waitUntilFunctionUpdatedV2` | ALREADY, by the SDK waiter — and their wrapped failure is `markNonRetryable`d, so the loop below does not replay it either | measured against `@aws-sdk/client-lambda`: the generated `checkState` catches EVERY exception and returns `RETRY`, so a mid-propagation 403 on `lambda:GetFunction` is polled out to `maxWaitTime` (600s). Wrapping them again would only stack a second budget on top — and `@smithy/util-waiter` serializes its `observedResponses` into the TIMEOUT message, whose keys read `403: User: … is not authorized to perform: lambda:GetFunction …`, so a classifier reading that message would have replayed a PERMANENT denial for 3 x 600s |
17364
+ * | `GetFunction` (delete-path backing-Lambda probe) | NO, deliberately | it already fails OPEN — anything but a definitive `ResourceNotFoundException` falls through to the normal invoke path, whose waiters cover the same propagation window one call later. Retrying would only delay that fall-through by up to 47.75s on a genuine permission denial |
17365
+ * | anything AFTER delivery (`pollS3Response`, the `FunctionError` throw, `cleanupResponseObject`) | NO, deliberately | the handler is running and will PUT to the URL of THIS attempt; a replay strands it at a key nobody polls, which is precisely the bug `disableOuterRetry` prevents |
17013
17366
  */
17014
17367
  disableOuterRetry = true;
17015
17368
  /** Max time to wait for synchronous S3 response after Lambda invocation (30 seconds) */
@@ -17023,8 +17376,13 @@ var CustomResourceProvider = class CustomResourceProvider {
17023
17376
  /** Max poll interval for async polling with exponential backoff (30 seconds) */
17024
17377
  MAX_POLL_INTERVAL_MS = 3e4;
17025
17378
  /**
17026
- * How many extra times to re-invoke a custom resource whose handler returned
17027
- * FAILED with a *transient IAM-authorization* reason (e.g. the CDK Provider
17379
+ * How many extra times to RE-INVOKE a custom resource whose handler returned
17380
+ * FAILED with a *transient IAM-authorization* reason.
17381
+ *
17382
+ * TWO error shapes, TWO budgets, deliberately (issue #2033) — this one and
17383
+ * {@link CustomResourceProvider.preDeliveryAuthzMaxRetries}. This budget
17384
+ * governs the shape where the handler ALREADY RAN: it returned FAILED with a
17385
+ * transient-authz reason (e.g. the CDK Provider
17028
17386
  * framework's `lambda:GetFunction` / "not in the state functionActive" 403
17029
17387
  * when the framework role's freshly-attached inline policy has not yet
17030
17388
  * propagated to the assumed-role session). cdkd's fast SDK path invokes the
@@ -17036,16 +17394,67 @@ var CustomResourceProvider = class CustomResourceProvider {
17036
17394
  * `disableOuterRetry` to avoid stranding a pre-signed response URL — so we
17037
17395
  * retry HERE instead, deriving a fresh response URL + RequestId per attempt
17038
17396
  * and recycling the backing function's execution environment between tries).
17039
- * Override via `CDKD_CR_AUTHZ_MAX_RETRIES`. `0` disables the RETRY only —
17397
+ *
17398
+ * **It is SMALL (2) because every retry it authorises RE-RUNS THE USER'S
17399
+ * HANDLER**, and that is the whole reason the second shape does not share it:
17400
+ * a pre-delivery throw invokes the handler ZERO times, so the argument that
17401
+ * bounds this number does not apply there at all. Sharing it was measured
17402
+ * wrong — 2 retries on the dense schedule is 250ms + 500ms of coverage, i.e.
17403
+ * 0.75s against an IAM-propagation window this repo has measured at 7-12s, so
17404
+ * issue #2033's own scenario still failed with the fix in place.
17405
+ *
17406
+ * Override via `CDKD_CR_AUTHZ_MAX_RETRIES` (clamped to
17407
+ * {@link CR_AUTHZ_MAX_RETRIES_CEILING}). `0` disables the RE-INVOKE only —
17040
17408
  * the issue-#1674 log-tail scan and the reason annotation it produces still
17041
17409
  * run, because they describe the failure rather than react to it.
17410
+ *
17411
+ * It does NOT disable either of the two retries that cannot reach the
17412
+ * handler: the response-placeholder `PutObject` retry in
17413
+ * `generateResponseURL`, and the pre-delivery arm above. Neither is what this
17414
+ * knob exists to bound — it bounds how many times a user's handler may be
17415
+ * re-run — and a user turning off re-invokes should not thereby lose the
17416
+ * propagation coverage every other resource type gets for free.
17042
17417
  */
17043
17418
  transientAuthzMaxRetries = (() => {
17044
17419
  const raw = process.env["CDKD_CR_AUTHZ_MAX_RETRIES"];
17045
- if (raw === void 0 || raw === "") return 2;
17420
+ if (raw === void 0 || raw === "") return CR_AUTHZ_MAX_RETRIES_DEFAULT;
17046
17421
  const n = Number(raw);
17047
- return Number.isFinite(n) && n >= 0 ? n : 2;
17422
+ if (!Number.isFinite(n) || n < 0) return CR_AUTHZ_MAX_RETRIES_DEFAULT;
17423
+ const clamped = Math.min(Math.floor(n), CR_AUTHZ_MAX_RETRIES_CEILING);
17424
+ if (clamped !== n) this.logger.warn(`CDKD_CR_AUTHZ_MAX_RETRIES=${raw} is out of range; using ${clamped} (whole numbers, at most ${CR_AUTHZ_MAX_RETRIES_CEILING} — each retry re-invokes the custom resource handler).`);
17425
+ return clamped;
17048
17426
  })();
17427
+ /**
17428
+ * Budget for the PRE-DELIVERY thrown arm — a transient IAM-authorization
17429
+ * error thrown by the `Invoke` / `Publish` itself BEFORE the request reached
17430
+ * the handler (issue #2033). The reported shape is an `AccessDeniedException`
17431
+ * on `lambda:InvokeFunction` while the DEPLOYING principal's own
17432
+ * freshly-attached policy is still propagating.
17433
+ *
17434
+ * It is {@link IAM_PROPAGATION_MAX_RETRIES} — the same 26 retries over 47.75s
17435
+ * that `withRetry` gives every other resource type for the identical wording,
17436
+ * on the same dense schedule ({@link IAM_PROPAGATION_INITIAL_DELAY_MS}
17437
+ * doubling to {@link IAM_PROPAGATION_MAX_DELAY_MS}). The measured window this
17438
+ * has to cover is 7-12s; the FAILED-response budget's 0.75s does not.
17439
+ *
17440
+ * **Why it can afford that while its sibling cannot**: it fires only when
17441
+ * `delivered === false`, so the handler has been invoked ZERO times and a
17442
+ * replay re-runs NOTHING — no partial work repeated, no second physical
17443
+ * resource from a Provider-framework `onEvent`, no stranded response URL. The
17444
+ * only cost of a retry here is one `PutObject` + one presign, and the
17445
+ * abandoned placeholder is swept before the next attempt. So the constraint
17446
+ * that keeps `transientAuthzMaxRetries` at 2 is simply absent, and matching
17447
+ * every other resource type is the correct answer instead.
17448
+ *
17449
+ * A thrown retry also skips the exec-env recycle: the denial is on CDKD's own
17450
+ * principal, not on the backing function's role, so there is no warm
17451
+ * container holding stale credentials to invalidate.
17452
+ *
17453
+ * Deliberately NOT overridable by an env var. It bounds no handler
17454
+ * invocation, so there is nothing for a user to trade off — the same reason
17455
+ * the placeholder `PutObject`'s `withRetry` takes no knob either.
17456
+ */
17457
+ preDeliveryAuthzMaxRetries = 26;
17049
17458
  constructor(config) {
17050
17459
  const awsClients = getAwsClients();
17051
17460
  this.lambdaClient = awsClients.lambda;
@@ -17252,7 +17661,11 @@ var CustomResourceProvider = class CustomResourceProvider {
17252
17661
  if (cfnResponse.Status === "FAILED") this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}`);
17253
17662
  else this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
17254
17663
  } catch (error) {
17255
- this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}`);
17664
+ 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}`);
17665
+ return {
17666
+ outcome: "skipped",
17667
+ reason: CR_DELETE_INVOKE_FAILED_SKIP_REASON
17668
+ };
17256
17669
  }
17257
17670
  }
17258
17671
  /**
@@ -17309,31 +17722,71 @@ var CustomResourceProvider = class CustomResourceProvider {
17309
17722
  * `prepareInvocation()`) and recycling the backing function's execution
17310
17723
  * environment between tries so its next cold start re-assumes the role.
17311
17724
  *
17725
+ * **The loop covers TWO error shapes** (issue #2033). The original one is the
17726
+ * handler's RETURNED `Status: 'FAILED'`. The second is an error THROWN by one
17727
+ * of the provider's OWN SDK calls before the request reached the handler —
17728
+ * which used to leave `create()` directly, because the loop body had no
17729
+ * `try` / `catch` at any point, making every such call single-shot while every
17730
+ * other resource type got the outer `withRetry`'s 26 attempts for the identical
17731
+ * wording. The `delivered` flag below is what keeps the second arm honest: it
17732
+ * flips the moment `Invoke` / `Publish` RETURNS, and a throw after that point
17733
+ * is rethrown untouched no matter how transient it reads, because the handler
17734
+ * is running and will PUT to THIS attempt's response URL. See
17735
+ * {@link CustomResourceProvider.disableOuterRetry} for the whole per-call
17736
+ * table, and {@link CustomResourceProvider.isTransientAuthzThrow} for why a
17737
+ * PRE-delivery throw is safe to replay at all.
17738
+ *
17312
17739
  * `buildRequest` is called once per attempt with the fresh invocation so the
17313
17740
  * CFn request body always carries the matching ResponseURL / RequestId.
17314
17741
  * Returns the final response; the caller decides what a terminal FAILED means
17315
17742
  * (create/update throw, delete warns-and-continues).
17316
17743
  */
17317
17744
  async invokeCustomResourceWithRetry(serviceToken, logicalId, operation, buildRequest) {
17318
- for (let attempt = 0;; attempt++) {
17319
- const invocation = await this.prepareInvocation();
17320
- const request = buildRequest(invocation);
17321
- this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
17322
- const { response: cfnResponse, logResult } = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation);
17323
- const reasonIsAuthz = cfnResponse.Status === "FAILED" && this.isTransientAuthzFailure(cfnResponse.Reason);
17324
- const logTail = cfnResponse.Status === "FAILED" && !reasonIsAuthz ? decodeInvokeLogTail(logResult) : void 0;
17325
- const logAuthzMatch = logTail === void 0 ? void 0 : this.findTransientAuthzLogLine(logTail);
17326
- if (cfnResponse.Status === "FAILED" && attempt < this.transientAuthzMaxRetries && (reasonIsAuthz || logAuthzMatch !== void 0)) {
17327
- this.logger.warn(`Custom resource ${operation} for ${logicalId} returned a transient IAM-authorization FAILED (attempt ${attempt + 1}/${this.transientAuthzMaxRetries + 1}): ${this.truncateReason(cfnResponse.Reason)}. ` + (logAuthzMatch === void 0 ? "" : `The handler's reason carried no authorization wording; the denial was found in the backing function's log: ${this.truncateReason(logAuthzMatch.line)}. `) + `Recycling the backing function's execution environment and retrying so its next cold start picks up the propagated policy.`);
17328
- await this.recycleBackingFunctionExecEnv(serviceToken, logicalId);
17329
- continue;
17745
+ const watch = this.startInterruptWatch(logicalId);
17746
+ try {
17747
+ let preDeliveryRetries = 0;
17748
+ let failedResponseRetries = 0;
17749
+ for (let attempt = 0;; attempt++) {
17750
+ let delivered = false;
17751
+ let cfnResponse;
17752
+ let logResult;
17753
+ let invocation;
17754
+ try {
17755
+ invocation = await this.prepareInvocation(logicalId, watch);
17756
+ const request = buildRequest(invocation);
17757
+ this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
17758
+ const sent = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation, () => {
17759
+ delivered = true;
17760
+ });
17761
+ cfnResponse = sent.response;
17762
+ logResult = sent.logResult;
17763
+ } catch (error) {
17764
+ if (delivered || preDeliveryRetries >= this.preDeliveryAuthzMaxRetries || !this.isTransientAuthzThrow(error)) throw error;
17765
+ const delayMs = Math.min(250 * Math.pow(2, preDeliveryRetries), IAM_PROPAGATION_MAX_DELAY_MS);
17766
+ this.logger.warn(`Custom resource ${operation} for ${logicalId} hit a transient IAM-authorization error before the request was delivered (attempt ${attempt + 1}/${this.preDeliveryAuthzMaxRetries + 1}): ${this.truncateReason(error instanceof Error ? error.message : String(error))}. Retrying in ${delayMs / 1e3}s with a fresh response URL and RequestId.`);
17767
+ if (invocation !== void 0) await this.cleanupResponseObject(invocation.responseKey);
17768
+ preDeliveryRetries += 1;
17769
+ await this.sleepInterruptibly(delayMs, watch);
17770
+ continue;
17771
+ }
17772
+ const reasonIsAuthz = cfnResponse.Status === "FAILED" && this.isTransientAuthzFailure(cfnResponse.Reason);
17773
+ const logTail = cfnResponse.Status === "FAILED" && !reasonIsAuthz ? decodeInvokeLogTail(logResult) : void 0;
17774
+ const logAuthzMatch = logTail === void 0 ? void 0 : this.findTransientAuthzLogLine(logTail);
17775
+ if (cfnResponse.Status === "FAILED" && failedResponseRetries < this.transientAuthzMaxRetries && (reasonIsAuthz || logAuthzMatch !== void 0)) {
17776
+ failedResponseRetries += 1;
17777
+ this.logger.warn(`Custom resource ${operation} for ${logicalId} returned a transient IAM-authorization FAILED (attempt ${attempt + 1}/${this.transientAuthzMaxRetries + 1}): ${this.truncateReason(cfnResponse.Reason)}. ` + (logAuthzMatch === void 0 ? "" : `The handler's reason carried no authorization wording; the denial was found in the backing function's log: ${this.truncateReason(logAuthzMatch.line)}. `) + `Recycling the backing function's execution environment and retrying so its next cold start picks up the propagated policy.`);
17778
+ await this.recycleBackingFunctionExecEnv(serviceToken, logicalId);
17779
+ continue;
17780
+ }
17781
+ if (cfnResponse.Status === "FAILED" && logAuthzMatch !== void 0) return {
17782
+ ...cfnResponse,
17783
+ Reason: `${cfnResponse.Reason ?? "Unknown reason"} [cdkd: the reason carried no authorization wording, but the backing function's invocation log matched the IAM-authorization signal "${logAuthzMatch.signal}" — see the cdkd warning for the log line, or the function's CloudWatch log group]`
17784
+ };
17785
+ if (logTail !== void 0 && hasHandlerLogOutput(logTail)) this.logger.warn(`Custom resource ${operation} for ${logicalId} failed and cdkd could not classify the reason. Log tail from the DISPATCH invocation (for the CDK Provider framework's async pattern the failure may have occurred in a later execution, whose log this is not):\n` + this.truncateReason(logTail, CR_LOG_TAIL_WARN_MAX_CHARS));
17786
+ return cfnResponse;
17330
17787
  }
17331
- if (cfnResponse.Status === "FAILED" && logAuthzMatch !== void 0) return {
17332
- ...cfnResponse,
17333
- Reason: `${cfnResponse.Reason ?? "Unknown reason"} [cdkd: the reason carried no authorization wording, but the backing function's invocation log matched the IAM-authorization signal "${logAuthzMatch.signal}" — see the cdkd warning for the log line, or the function's CloudWatch log group]`
17334
- };
17335
- if (logTail !== void 0 && hasHandlerLogOutput(logTail)) this.logger.warn(`Custom resource ${operation} for ${logicalId} failed and cdkd could not classify the reason. Log tail from the DISPATCH invocation (for the CDK Provider framework's async pattern the failure may have occurred in a later execution, whose log this is not):\n` + this.truncateReason(logTail, CR_LOG_TAIL_WARN_MAX_CHARS));
17336
- return cfnResponse;
17788
+ } finally {
17789
+ watch.dispose();
17337
17790
  }
17338
17791
  }
17339
17792
  /**
@@ -17355,6 +17808,63 @@ var CustomResourceProvider = class CustomResourceProvider {
17355
17808
  return CR_TRANSIENT_AUTHZ_SIGNALS.some((p) => lower.includes(p));
17356
17809
  }
17357
17810
  /**
17811
+ * Classify an error THROWN by one of the provider's own SDK calls as a
17812
+ * transient IAM-authorization race worth replaying (issue #2033).
17813
+ *
17814
+ * The list is cdkd's shared `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — the
17815
+ * same one every other resource type is classified with, via `withRetry` —
17816
+ * plus {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS}, the two CR-specific spellings
17817
+ * the shared list does not carry in any form. That is deliberately WIDER than
17818
+ * the FAILED-reason classifier, and the asymmetry is the whole point: this
17819
+ * text was written by AWS about a call CDKD made, whereas a FAILED reason was
17820
+ * written by the user's handler about a call IT made. See
17821
+ * {@link CR_TRANSIENT_AUTHZ_SIGNALS} for the full argument, including why the
17822
+ * bare `is unable to assume` spelling stays on that side of the line.
17823
+ *
17824
+ * **Two REFUSALS come before any pattern**, and each closes a way for a
17825
+ * single call to be given two retry budgets:
17826
+ *
17827
+ * - `isMarkedNonRetryable` — cdkd's own declaration that this raising cannot
17828
+ * succeed on a replay. Both of the provider's already-retried calls stamp
17829
+ * it: the placeholder `PutObject` after its `withRetry` is exhausted, and
17830
+ * `waitForBackingLambdaReady` after the SDK waiter has polled for its full
17831
+ * 600s. The marker rather than the wording is what makes the second one
17832
+ * sound: `@smithy/util-waiter` serializes `observedResponses` into its
17833
+ * TIMEOUT message and those keys read
17834
+ * `403: User: … is not authorized to perform: lambda:GetFunction …`, so a
17835
+ * PERMANENT denial matched every pattern here and a message-shaped fence
17836
+ * would be one AWS wording change from failing open. It also refuses a
17837
+ * cdkd-authored REFUSAL whose text happens to carry an authz phrase.
17838
+ * - the `.cause` chain is WALKED (bounded), matching `isThrottlingError` /
17839
+ * `isMarkedNonRetryable`. cdkd wraps SDK errors routinely, and issue #2040
17840
+ * documents the drop-`cause` class in this very directory; a top-level-only
17841
+ * read would silently un-retry a wrapped propagation denial.
17842
+ *
17843
+ * **Every pattern in that union is an AUTHORIZATION or REQUEST-VALIDATION
17844
+ * rejection, and that is what makes replaying an `Invoke` safe here.** Such a
17845
+ * rejection is decided at the API front door, before any execution environment
17846
+ * is engaged, so the handler provably did not run and a replay cannot
17847
+ * re-deliver work — the hazard `disableOuterRetry` exists for. It is also why
17848
+ * this classifier deliberately does NOT reach for the broader
17849
+ * `isRetryableTransientError`: a throttle, an HTTP 5xx or a socket timeout can
17850
+ * each arrive AFTER the request was accepted, so replaying one could invoke a
17851
+ * non-idempotent handler twice. Those classes stay single-shot on this path by
17852
+ * design, and the caller's `delivered` flag is the second, independent fence.
17853
+ */
17854
+ isTransientAuthzThrow(error) {
17855
+ if (isMarkedNonRetryable(error)) return false;
17856
+ let current = error;
17857
+ for (let depth = 0; depth < CR_ERROR_CAUSE_MAX_DEPTH && current != null; depth++) {
17858
+ const message = current instanceof Error ? current.message : typeof current === "string" ? current : "";
17859
+ if (message !== "") {
17860
+ const lower = message.toLowerCase();
17861
+ if (isIamPropagationError(message) || CR_THROWN_AUTHZ_EXTRA_SIGNALS.some((p) => lower.includes(p))) return true;
17862
+ }
17863
+ current = current.cause;
17864
+ }
17865
+ return false;
17866
+ }
17867
+ /**
17358
17868
  * Find the IAM-authorization denial inside a backing function's invocation
17359
17869
  * log tail, for the case the FAILED reason itself carries none (issue #1674).
17360
17870
  *
@@ -17440,15 +17950,25 @@ var CustomResourceProvider = class CustomResourceProvider {
17440
17950
  * erased from the FAILED reason (issue #1674). Returned UNDECODED so the
17441
17951
  * happy path pays nothing — only a FAILED whose reason missed decodes it.
17442
17952
  * Absent on the SNS path: there is no Lambda invoke to attach a log to.
17953
+ *
17954
+ * `onDelivered` is invoked exactly once, the moment the `Invoke` / `Publish`
17955
+ * RETURNS — i.e. at the point after which a replay would re-deliver the
17956
+ * request to the handler and strand this attempt's pre-signed response URL.
17957
+ * The caller's retry-on-throw arm is fenced on it (issue #2033). Deliberately
17958
+ * called AFTER the send rather than before: a rejection at the API front door
17959
+ * (the IAM-propagation class this arm exists for) means the handler never ran,
17960
+ * and treating that as delivered would leave the reported failure single-shot.
17443
17961
  */
17444
- async sendRequest(serviceToken, request, responseKey, logicalId, operation) {
17962
+ async sendRequest(serviceToken, request, responseKey, logicalId, operation, onDelivered) {
17445
17963
  if (this.isSnsServiceToken(serviceToken)) {
17446
17964
  this.logger.debug(`ServiceToken is SNS topic, publishing to: ${serviceToken}`);
17447
17965
  await this.publishToSns(serviceToken, request);
17966
+ onDelivered();
17448
17967
  return { response: await this.pollS3Response(responseKey, logicalId, operation) };
17449
17968
  }
17450
17969
  await this.waitForBackingLambdaReady(serviceToken, logicalId);
17451
17970
  const invokeResponse = await this.invokeLambda(serviceToken, request);
17971
+ onDelivered();
17452
17972
  return {
17453
17973
  response: await this.getCustomResourceResponse(invokeResponse, responseKey, logicalId, operation),
17454
17974
  ...invokeResponse.LogResult === void 0 ? {} : { logResult: invokeResponse.LogResult }
@@ -17481,6 +18001,37 @@ var CustomResourceProvider = class CustomResourceProvider {
17481
18001
  * deploy engine's per-resource `--resource-timeout` (default 30 min)
17482
18002
  * still bounds the outer Custom Resource provisioning attempt, so
17483
18003
  * this waiter cap is layered defense, not the only timeout.
18004
+ *
18005
+ * **The wrapped failure is `markNonRetryable`d, and its message is NOT the
18006
+ * waiter's** (issue #2033). Both halves are about `@smithy/util-waiter`'s
18007
+ * `checkExceptions`, which serializes its whole result into the `Error`
18008
+ * message:
18009
+ *
18010
+ * - On TIMEOUT that includes `observedResponses`, whose keys read
18011
+ * `403: User: … is not authorized to perform: lambda:GetFunction on
18012
+ * resource: …`. The waiter's generated `checkState` catches EVERY
18013
+ * exception and returns `RETRY`, so reaching here means a 600s budget was
18014
+ * already spent — yet the message matched
18015
+ * {@link CustomResourceProvider.isTransientAuthzThrow} exactly, so a
18016
+ * PERMANENT `lambda:GetFunction` denial was replayed for 3 x 600s instead
18017
+ * of 10 minutes, blowing any `--resource-timeout
18018
+ * AWS::CloudFormation::CustomResource=15m`. The marker is a property of
18019
+ * the error object, so unlike a wording test it cannot be defeated by AWS
18020
+ * rephrasing the denial.
18021
+ * - On the non-TIMEOUT arm (`State: Failed`, i.e. an ENI / VPC failure) the
18022
+ * serialized result carries `reason` and `final`, which for this waiter
18023
+ * are the ENTIRE `GetFunction` response — `Configuration.Environment.
18024
+ * Variables` included. That message reached `ProvisioningError.message`
18025
+ * and `extractDeploymentEventError` persisted it to
18026
+ * `deployments/{runId}.jsonl`, a durable store that outlives
18027
+ * `cdkd destroy` and is contractually "error + metadata only, never
18028
+ * resource properties, because they may contain secrets"
18029
+ * (`docs/deployment-events.md`). So this arm interpolates the error NAME
18030
+ * plus a fixed sentence, and recovers only the function's own
18031
+ * `State` / `StateReason` / `StateReasonCode` — AWS-authored status
18032
+ * fields — from the serialized payload. The TIMEOUT / ABORT arms keep
18033
+ * their message: `observedResponses` keys are status lines built by
18034
+ * `createMessageFromResponse`, never a response body.
17484
18035
  */
17485
18036
  async waitForBackingLambdaReady(serviceToken, logicalId) {
17486
18037
  try {
@@ -17497,7 +18048,7 @@ var CustomResourceProvider = class CustomResourceProvider {
17497
18048
  maxDelay: 5
17498
18049
  }, { FunctionName: serviceToken });
17499
18050
  } catch (error) {
17500
- throw new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${error instanceof Error ? error.message : String(error)}`);
18051
+ throw markNonRetryable(new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${describeWaiterFailure(error)}`, { cause: error }));
17501
18052
  }
17502
18053
  }
17503
18054
  /**
@@ -17593,28 +18144,38 @@ var CustomResourceProvider = class CustomResourceProvider {
17593
18144
  * Centralising this in one helper makes that invariant impossible to
17594
18145
  * violate at the call sites.
17595
18146
  */
17596
- async prepareInvocation() {
18147
+ async prepareInvocation(logicalId, watch) {
17597
18148
  const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
17598
18149
  const responseKey = this.getResponseKey(requestId);
17599
18150
  return {
17600
18151
  requestId,
17601
18152
  responseKey,
17602
- responseURL: await this.generateResponseURL(responseKey)
18153
+ responseURL: await this.generateResponseURL(responseKey, logicalId, watch)
17603
18154
  };
17604
18155
  }
17605
18156
  /**
17606
18157
  * Generate a pre-signed S3 PUT URL for Lambda to send its response
17607
18158
  */
17608
- async generateResponseURL(responseKey) {
18159
+ async generateResponseURL(responseKey, logicalId, watch) {
17609
18160
  if (!this.responseBucket) return "https://localhost/cfn-response-not-configured";
17610
18161
  await this.ensureResponseClient();
17611
- await this.s3Client.send(new PutObjectCommand({
17612
- Bucket: this.responseBucket,
17613
- Key: responseKey,
17614
- Body: "",
17615
- ContentLength: 0,
17616
- ContentType: "application/json"
17617
- }));
18162
+ const bucket = this.responseBucket;
18163
+ try {
18164
+ await withRetry(() => this.s3Client.send(new PutObjectCommand({
18165
+ Bucket: bucket,
18166
+ Key: responseKey,
18167
+ Body: "",
18168
+ ContentLength: 0,
18169
+ ContentType: "application/json"
18170
+ })), `${logicalId} (custom-resource response placeholder)`, {
18171
+ logger: this.logger,
18172
+ isInterrupted: watch.isInterrupted,
18173
+ onInterrupted: watch.onInterrupted,
18174
+ sleep: customResourceRetryDelays.sleep
18175
+ });
18176
+ } catch (error) {
18177
+ throw markNonRetryable(error instanceof Error ? error : new Error(String(error)));
18178
+ }
17618
18179
  const command = new PutObjectCommand({
17619
18180
  Bucket: this.responseBucket,
17620
18181
  Key: responseKey
@@ -17725,7 +18286,50 @@ var CustomResourceProvider = class CustomResourceProvider {
17725
18286
  return result;
17726
18287
  }
17727
18288
  sleep(ms) {
17728
- return new Promise((resolve) => setTimeout(resolve, ms));
18289
+ return customResourceRetryDelays.sleep(ms);
18290
+ }
18291
+ /**
18292
+ * Install a SIGINT watch for one custom-resource invocation (issue #2033).
18293
+ *
18294
+ * `docs/provider-development.md` requires a new `withRetry` to thread
18295
+ * `isInterrupted` / `onInterrupted`, and a hand-rolled backoff to be
18296
+ * interruptible for the same reason: without it Ctrl-C is dead for the whole
18297
+ * 47.75s schedule. The provider already had the shape — `pollS3Response`
18298
+ * installs its own handler — so this is that pattern, lifted so the two new
18299
+ * wait sites share one flag and one disposal.
18300
+ *
18301
+ * The caller MUST `dispose()` in a `finally`; a leaked listener would
18302
+ * accumulate one per resource across a deploy.
18303
+ */
18304
+ startInterruptWatch(logicalId) {
18305
+ let interrupted = false;
18306
+ const handler = () => {
18307
+ interrupted = true;
18308
+ };
18309
+ process.on("SIGINT", handler);
18310
+ return {
18311
+ isInterrupted: () => interrupted,
18312
+ onInterrupted: () => /* @__PURE__ */ new Error(`Custom resource ${logicalId} interrupted by user`),
18313
+ dispose: () => {
18314
+ process.removeListener("SIGINT", handler);
18315
+ }
18316
+ };
18317
+ }
18318
+ /**
18319
+ * `sleep` that checks the interrupt watch at most a second apart, mirroring
18320
+ * `withRetry`'s own once-per-second probe. Throws the watch's error when the
18321
+ * user has hit Ctrl-C, so the retry loop unwinds instead of sitting out the
18322
+ * remaining backoff.
18323
+ */
18324
+ async sleepInterruptibly(ms, watch) {
18325
+ let remaining = ms;
18326
+ while (remaining > 0) {
18327
+ if (watch.isInterrupted()) throw watch.onInterrupted();
18328
+ const chunk = Math.min(1e3, remaining);
18329
+ await this.sleep(chunk);
18330
+ remaining -= chunk;
18331
+ }
18332
+ if (watch.isInterrupted()) throw watch.onInterrupted();
17729
18333
  }
17730
18334
  /**
17731
18335
  * Adopt an existing custom resource into cdkd state.
@@ -22426,6 +23030,22 @@ function computeImplicitDeleteEdges(resources) {
22426
23030
  return edges;
22427
23031
  }
22428
23032
 
23033
+ //#endregion
23034
+ //#region src/deployment/masking-retry-logger.ts
23035
+ /**
23036
+ * Bind `logger` to `secrets`, masking every line the retry loop emits.
23037
+ *
23038
+ * No-op when the caller resolved no secret ({@link maskSecretsInText} returns
23039
+ * the text unchanged for an empty bag), so a non-secret resource's output is
23040
+ * byte-identical to threading the raw logger.
23041
+ */
23042
+ function maskingRetryLogger(logger, secrets) {
23043
+ return {
23044
+ debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)),
23045
+ warn: (msg) => logger.warn(maskSecretsInText(msg, secrets))
23046
+ };
23047
+ }
23048
+
22429
23049
  //#endregion
22430
23050
  //#region src/deployment/resource-deadline.ts
22431
23051
  /**
@@ -22705,6 +23325,38 @@ function replayingStateCreateContext(secrets) {
22705
23325
  };
22706
23326
  }
22707
23327
  /**
23328
+ * The {@link DeploymentEventError} a failed replay records, with this op's
23329
+ * re-resolved secrets masked out of its message (issue
23330
+ * [#2031](https://github.com/go-to-k/cdkd/issues/2031) acceptance item 2).
23331
+ *
23332
+ * `extractDeploymentEventError` copies `err.message` VERBATIM, and the events
23333
+ * store is a DURABLE sink — `deployments/{runId}.jsonl` in S3 outlives the
23334
+ * terminal the `logger.warn` beside it scrolls past, and `cdkd events` replays
23335
+ * it later. The standalone `cdkd rollback` command wires
23336
+ * `recordEvent: (e) => eventRecorder.record(e)` (`src/cli/commands/rollback.ts`)
23337
+ * with NO masking of its own, so without this the plaintext the terminal line
23338
+ * masks is persisted one statement later.
23339
+ *
23340
+ * The in-process caller (`DeployEngine.rollbackExecutorContext`) routes through
23341
+ * `maskSecretsInEvent`, but that masks with the DEPLOY's `perResourceSecrets`
23342
+ * for the resource — a different bag from the one this replay re-resolved from
23343
+ * the JOURNAL, which can name a different secret version or a reference the
23344
+ * deploy never resolved. Masking here is what makes both callers equal, and
23345
+ * double-masking is a no-op (the mask is not a key of either bag).
23346
+ *
23347
+ * `name` / `awsErrorCode` / `requestId` are deliberately left alone: they are
23348
+ * AWS-authored identifiers, not message text, and #2038 traced all three as
23349
+ * non-sensitive.
23350
+ */
23351
+ function maskedRollbackEventError(error, secrets) {
23352
+ const extracted = extractDeploymentEventError(error);
23353
+ if (secrets.size === 0) return extracted;
23354
+ return {
23355
+ ...extracted,
23356
+ message: maskSecretsInText(extracted.message, secrets)
23357
+ };
23358
+ }
23359
+ /**
22708
23360
  * Which provisioning layer a delete must be judged against: the CURRENT
22709
23361
  * state record wins (it is what state says AWS holds right now), with the
22710
23362
  * journaled op's routing as the legacy-state fallback. Shared by both
@@ -23063,10 +23715,10 @@ function redactRollbackRecord(record, secrets, journaledProps) {
23063
23715
  properties: redactSecretsForState(record.properties, secrets, journaledProps, STATE_DERIVED_RULES)
23064
23716
  }, secrets);
23065
23717
  }
23066
- async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
23718
+ async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted, secrets) {
23067
23719
  if (provider.disableOuterRetry) return await provider.update(...args);
23068
23720
  return await withRetry(() => provider.update(...args), logicalId, {
23069
- logger,
23721
+ logger: maskingRetryLogger(logger, secrets),
23070
23722
  ...isInterrupted && {
23071
23723
  isInterrupted,
23072
23724
  onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while retrying a resource update")
@@ -23121,6 +23773,21 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23121
23773
  const action = classifyRollbackOp(op, stateResources, orphanLogicalIds);
23122
23774
  const { logger } = ctx;
23123
23775
  /**
23776
+ * This op's `plaintext -> {{resolve:...}}expression` bag, filled by
23777
+ * {@link resolveReplayProps} on whichever arm runs (issues
23778
+ * [#2038](https://github.com/go-to-k/cdkd/issues/2038) /
23779
+ * [#2031](https://github.com/go-to-k/cdkd/issues/2031)).
23780
+ *
23781
+ * HOISTED above the `try` rather than declared per arm, which is what the
23782
+ * two arms used to do: the shared catch below logs the thrown AWS message and
23783
+ * persists it to the events store, and it cannot see a binding scoped to the
23784
+ * arm that threw. Exactly one arm runs per call, so a single per-op bag is
23785
+ * equivalent to the per-arm ones for every existing reader (`secrets.size`
23786
+ * stays 0 on the arms that resolve nothing, so `redactRollbackRecord` and the
23787
+ * maskers keep their identity behavior).
23788
+ */
23789
+ const secrets = /* @__PURE__ */ new Map();
23790
+ /**
23124
23791
  * The route a CREATE-rollback arm resolved for this op (issue #1366) —
23125
23792
  * hoisted so the shared catch's ROLLBACK_RESOURCE_FAILED reports the route
23126
23793
  * the delete was going to take, which is the one a refusal is about. Stays
@@ -23244,7 +23911,6 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23244
23911
  case "reverse-replacement": {
23245
23912
  const current = stateResources[op.logicalId];
23246
23913
  const prev = op.previousState;
23247
- const secrets = /* @__PURE__ */ new Map();
23248
23914
  const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
23249
23915
  logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
23250
23916
  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.`);
@@ -23261,7 +23927,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23261
23927
  try {
23262
23928
  createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
23263
23929
  ...RECREATE_RETRY_SCHEDULE,
23264
- logger,
23930
+ logger: maskingRetryLogger(logger, secrets),
23265
23931
  ...isInterrupted && {
23266
23932
  isInterrupted,
23267
23933
  onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while waiting out the name cooldown")
@@ -23284,7 +23950,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23284
23950
  try {
23285
23951
  createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
23286
23952
  ...RECREATE_RETRY_SCHEDULE,
23287
- logger,
23953
+ logger: maskingRetryLogger(logger, secrets),
23288
23954
  ...isInterrupted && {
23289
23955
  isInterrupted,
23290
23956
  onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while waiting for the old name to release")
@@ -23292,7 +23958,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23292
23958
  isRetryable: isRecreateRetryableError
23293
23959
  });
23294
23960
  } catch (recreateError) {
23295
- throw new Error(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'.`);
23961
+ throw new Error(maskSecretsInText(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'.`, secrets));
23296
23962
  }
23297
23963
  }
23298
23964
  const adoptedLiveNewResource = !deletedNewFirst && createResult.physicalId === current.physicalId;
@@ -23315,7 +23981,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23315
23981
  ...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
23316
23982
  }), op.logicalId, current.physicalId, "while deleting the new resource after re-creating the old one");
23317
23983
  } catch (deleteError) {
23318
- logger.warn(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state.`);
23984
+ logger.warn(maskSecretsInText(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state.`, secrets));
23319
23985
  result.warnings++;
23320
23986
  }
23321
23987
  logger.info(adoptedLiveNewResource ? ` Rollback: ${op.logicalId} adopted the live resource (${createResult.physicalId}) — replacement NOT fully reversed (name-idempotent Create API)` : ` Rollback: ${op.logicalId} replacement reversed (old resource re-created as ${createResult.physicalId})`);
@@ -23347,7 +24013,6 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23347
24013
  resourceType: op.resourceType,
23348
24014
  provisionedBy: op.provisionedBy
23349
24015
  });
23350
- const secrets = /* @__PURE__ */ new Map();
23351
24016
  const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
23352
24017
  const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
23353
24018
  const revertResult = await updateWithRollbackRetry(provider, [
@@ -23357,10 +24022,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23357
24022
  desiredProps ?? {},
23358
24023
  currentProps ?? {},
23359
24024
  { maskSecrets: createSecretMasker(secrets) }
23360
- ], op.logicalId, logger, isInterrupted);
24025
+ ], op.logicalId, logger, isInterrupted, secrets);
23361
24026
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
23362
24027
  const rollbackPartial = updatePartialReason(revertResult);
23363
- if (rollbackPartial !== void 0) logger.warn(` Rollback: ${op.logicalId} restored, ${updatePartialMessage(rollbackPartial)}`);
24028
+ if (rollbackPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} restored, ${updatePartialMessage(rollbackPartial)}`, secrets));
23364
24029
  else logger.info(` Rollback: ${op.logicalId} restored successfully`);
23365
24030
  await afterOp?.(op.logicalId);
23366
24031
  ctx.recordEvent?.({
@@ -23370,13 +24035,13 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23370
24035
  logicalId: op.logicalId,
23371
24036
  resourceType: op.resourceType,
23372
24037
  ...op.provisionedBy && { provisionedBy: op.provisionedBy },
23373
- ...rollbackPartial !== void 0 && { reason: rollbackPartial }
24038
+ ...rollbackPartial !== void 0 && { reason: maskSecretsInText(rollbackPartial, secrets) }
23374
24039
  });
23375
24040
  return;
23376
24041
  }
23377
24042
  }
23378
24043
  } catch (rollbackError) {
23379
- logger.warn(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
24044
+ logger.warn(maskSecretsInText(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, secrets));
23380
24045
  logger.warn(" Continuing with remaining rollback operations...");
23381
24046
  result.failures++;
23382
24047
  const failedRoute = createRollbackRoute ?? op.provisionedBy;
@@ -23387,7 +24052,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
23387
24052
  logicalId: op.logicalId,
23388
24053
  resourceType: op.resourceType,
23389
24054
  ...failedRoute && { provisionedBy: failedRoute },
23390
- error: extractDeploymentEventError(rollbackError)
24055
+ error: maskedRollbackEventError(rollbackError, secrets)
23391
24056
  });
23392
24057
  }
23393
24058
  }
@@ -23424,6 +24089,15 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
23424
24089
  }
23425
24090
  const op = failedOps[i];
23426
24091
  const action = classifyFailedOp(op, stateResources);
24092
+ /**
24093
+ * This op's re-resolved secret bag — the twin of `replaySingle`'s, and
24094
+ * hoisted above this iteration's `try` for the same reason (issues #2038 /
24095
+ * #2031): the shared catch below logs the thrown AWS message and persists it
24096
+ * to the events store, and could not see a binding scoped to the arm that
24097
+ * threw. Re-created per ITERATION, so one op's secrets can never mask
24098
+ * another's text.
24099
+ */
24100
+ const secrets = /* @__PURE__ */ new Map();
23427
24101
  let createRollbackRoute;
23428
24102
  try {
23429
24103
  switch (action) {
@@ -23491,7 +24165,6 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
23491
24165
  resourceType: op.resourceType,
23492
24166
  provisionedBy: op.provisionedBy ?? current.provisionedBy
23493
24167
  });
23494
- const secrets = /* @__PURE__ */ new Map();
23495
24168
  const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
23496
24169
  const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
23497
24170
  const revertFailedResult = await updateWithRollbackRetry(provider, [
@@ -23501,10 +24174,10 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
23501
24174
  desiredProps ?? {},
23502
24175
  attemptedProps ?? {},
23503
24176
  { maskSecrets: createSecretMasker(secrets) }
23504
- ], op.logicalId, logger, options.isInterrupted);
24177
+ ], op.logicalId, logger, options.isInterrupted, secrets);
23505
24178
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
23506
24179
  const revertFailedPartial = updatePartialReason(revertFailedResult);
23507
- if (revertFailedPartial !== void 0) logger.warn(` Rollback: ${op.logicalId} reverted, ${updatePartialMessage(revertFailedPartial)}`);
24180
+ if (revertFailedPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} reverted, ${updatePartialMessage(revertFailedPartial)}`, secrets));
23508
24181
  else logger.info(` Rollback: ${op.logicalId} reverted successfully`);
23509
24182
  await options.afterOp?.(op.logicalId);
23510
24183
  ctx.recordEvent?.({
@@ -23514,13 +24187,13 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
23514
24187
  logicalId: op.logicalId,
23515
24188
  resourceType: op.resourceType,
23516
24189
  ...op.provisionedBy && { provisionedBy: op.provisionedBy },
23517
- ...revertFailedPartial !== void 0 && { reason: revertFailedPartial }
24190
+ ...revertFailedPartial !== void 0 && { reason: maskSecretsInText(revertFailedPartial, secrets) }
23518
24191
  });
23519
24192
  break;
23520
24193
  }
23521
24194
  }
23522
24195
  } catch (revertError) {
23523
- logger.warn(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}`);
24196
+ logger.warn(maskSecretsInText(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}`, secrets));
23524
24197
  result.failures++;
23525
24198
  pending.add(op);
23526
24199
  const failedRoute = createRollbackRoute ?? op.provisionedBy;
@@ -23531,7 +24204,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
23531
24204
  logicalId: op.logicalId,
23532
24205
  resourceType: op.resourceType,
23533
24206
  ...failedRoute && { provisionedBy: failedRoute },
23534
- error: extractDeploymentEventError(revertError)
24207
+ error: maskedRollbackEventError(revertError, secrets)
23535
24208
  });
23536
24209
  }
23537
24210
  }
@@ -23631,7 +24304,7 @@ const FLUSH_INTERVAL_MS = 2e3;
23631
24304
  const FLUSH_EVENT_THRESHOLD = 50;
23632
24305
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
23633
24306
  function getCdkdVersion() {
23634
- return "0.284.10";
24307
+ return "0.284.12";
23635
24308
  }
23636
24309
  /**
23637
24310
  * Generate a time-sortable unique run id, e.g.
@@ -24096,6 +24769,14 @@ var DeploymentEventsReader = class {
24096
24769
  //#endregion
24097
24770
  //#region src/deployment/deploy-engine.ts
24098
24771
  /**
24772
+ * The bag a resource with no recorded secret masks against (issue #2038).
24773
+ * Shared so the "no entry" path allocates nothing and — more usefully — so
24774
+ * every masking site takes the SAME branch: `maskSecretsInText` /
24775
+ * `maskSecretsInError` both return their input unchanged for an empty bag, so
24776
+ * an absent entry and an empty one cannot behave differently. Never written to.
24777
+ */
24778
+ const EMPTY_SECRETS = /* @__PURE__ */ new Map();
24779
+ /**
24099
24780
  * Default per-resource warn threshold: warn the user when a single
24100
24781
  * resource has been in flight for 5 minutes. Most CC API resources
24101
24782
  * complete in under a minute; 5m is the agreed elbow.
@@ -25299,7 +25980,7 @@ var DeployEngine = class {
25299
25980
  } catch (error) {
25300
25981
  renderer.removeTask(logicalId);
25301
25982
  const message = error instanceof Error ? error.message : String(error);
25302
- this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
25983
+ this.logger.error(this.maskForResource(logicalId, `Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`));
25303
25984
  this.recordEvent({
25304
25985
  eventType: "RESOURCE_FAILED",
25305
25986
  stackName,
@@ -25310,7 +25991,7 @@ var DeployEngine = class {
25310
25991
  durationMs: Date.now() - resourceStartedAt,
25311
25992
  error: extractDeploymentEventError(error)
25312
25993
  });
25313
- throw new ProvisioningError(`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`, resourceType, logicalId, stateResources[logicalId]?.physicalId, error instanceof Error ? error : void 0);
25994
+ throw new ProvisioningError(`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`, resourceType, logicalId, stateResources[logicalId]?.physicalId, error instanceof Error ? maskSecretsInError(error, this.perResourceSecrets.get(logicalId) ?? EMPTY_SECRETS) : void 0);
25314
25995
  } finally {
25315
25996
  renderer.removeTask(logicalId);
25316
25997
  }
@@ -25399,7 +26080,8 @@ var DeployEngine = class {
25399
26080
  * #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
25400
26081
  * the two --replace escape hatches cannot drift apart.
25401
26082
  */
25402
- async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, createContext, updateReplacePolicy) {
26083
+ async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
26084
+ const createContext = { maskSecrets: createSecretMasker(secrets) };
25403
26085
  const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
25404
26086
  let deleteResult;
25405
26087
  try {
@@ -25409,7 +26091,7 @@ var DeployEngine = class {
25409
26091
  ...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
25410
26092
  });
25411
26093
  } catch (deleteError) {
25412
- throw new Error(`Failed to delete old resource ${logicalId} (${currentResource.physicalId}) during the --replace delete-first fallback: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
26094
+ throw new Error(maskSecretsInText(`Failed to delete old resource ${logicalId} (${currentResource.physicalId}) during the --replace delete-first fallback: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, secrets));
25413
26095
  }
25414
26096
  const replaceSkipReason = deleteSkipReason(deleteResult);
25415
26097
  if (replaceSkipReason !== void 0) throw new Error(deleteSkippedMessage(logicalId, currentResource.physicalId, replaceSkipReason, "during the --replace delete-first fallback"));
@@ -25420,13 +26102,13 @@ var DeployEngine = class {
25420
26102
  maxRetries: 8,
25421
26103
  initialDelayMs: 2e3,
25422
26104
  maxDelayMs: 1e4,
25423
- logger: this.logger,
26105
+ logger: this.maskingRetryLoggerFor(secrets),
25424
26106
  isInterrupted: () => this.interrupted,
25425
26107
  onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
25426
26108
  isRetryable: isRecreateRetryableError
25427
26109
  });
25428
26110
  } catch (recreateError) {
25429
- throw new Error(`Failed to re-create ${logicalId} after the --replace delete-first fallback already deleted the old resource (${currentResource.physicalId}): ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. Re-run the deploy to create it fresh.`);
26111
+ throw new Error(maskSecretsInText(`Failed to re-create ${logicalId} after the --replace delete-first fallback already deleted the old resource (${currentResource.physicalId}): ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. Re-run the deploy to create it fresh.`, secrets));
25430
26112
  }
25431
26113
  }
25432
26114
  /**
@@ -25448,10 +26130,10 @@ var DeployEngine = class {
25448
26130
  ...parameterValues && { parameters: parameterValues },
25449
26131
  ...conditions && { conditions }
25450
26132
  }, stackName);
26133
+ if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
25451
26134
  const resolvedProps = await this.resolver.resolve(desiredProps, context);
25452
26135
  this.perResourceTemplateProps.set(logicalId, desiredProps);
25453
26136
  const createSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
25454
- if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
25455
26137
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
25456
26138
  this.attemptedResolvedProps.set(logicalId, resolvedProps);
25457
26139
  const createDecision = this.providerRegistry.getProviderFor({
@@ -25492,9 +26174,9 @@ var DeployEngine = class {
25492
26174
  ...parameterValues && { parameters: parameterValues },
25493
26175
  ...conditions && { conditions }
25494
26176
  }, stackName);
25495
- const resolvedProps = await this.resolver.resolve(desiredProps, context);
25496
26177
  const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
25497
26178
  this.perResourceSecrets.set(logicalId, updateSecrets);
26179
+ const resolvedProps = await this.resolver.resolve(desiredProps, context);
25498
26180
  this.perResourceTemplateProps.set(logicalId, desiredProps);
25499
26181
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
25500
26182
  this.attemptedResolvedProps.set(logicalId, resolvedProps);
@@ -25575,7 +26257,7 @@ var DeployEngine = class {
25575
26257
  maxRetries: 8,
25576
26258
  initialDelayMs: 2e3,
25577
26259
  maxDelayMs: 1e4,
25578
- logger: this.logger,
26260
+ logger: this.maskingRetryLoggerFor(updateSecrets),
25579
26261
  isInterrupted: () => this.interrupted,
25580
26262
  onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
25581
26263
  isRetryable: isRecreateRetryableError
@@ -25594,7 +26276,7 @@ var DeployEngine = class {
25594
26276
  if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).`, "NAMED_REPLACEMENT_COLLISION");
25595
26277
  this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
25596
26278
  deletedOldFirst = true;
25597
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
26279
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
25598
26280
  }
25599
26281
  if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
25600
26282
  const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
@@ -25602,7 +26284,7 @@ var DeployEngine = class {
25602
26284
  if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
25603
26285
  this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
25604
26286
  deletedOldFirst = true;
25605
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
26287
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
25606
26288
  }
25607
26289
  if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") this.logger.info(` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`);
25608
26290
  else {
@@ -25993,12 +26675,74 @@ var DeployEngine = class {
25993
26675
  return withRetry(operation, logicalId, {
25994
26676
  ...maxRetries !== void 0 && { maxRetries },
25995
26677
  ...initialDelayMs !== void 0 && { initialDelayMs },
25996
- logger: this.logger,
26678
+ logger: this.maskingRetryLogger(logicalId),
25997
26679
  isInterrupted: () => this.interrupted,
25998
26680
  onInterrupted: () => new InterruptedError(this.interruptCause ?? "user")
25999
26681
  });
26000
26682
  }
26001
26683
  /**
26684
+ * Mask one line of engine-authored text with a resource's OWN recorded
26685
+ * secrets (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)).
26686
+ *
26687
+ * Per-resource, never session-wide — see the `perResourceSecrets` field doc
26688
+ * for why one resource's secret must not rewrite another's literal. A
26689
+ * `logicalId` with no entry (or an empty bag) forwards verbatim, so every
26690
+ * non-secret resource is byte-identical to before.
26691
+ */
26692
+ maskForResource(logicalId, text) {
26693
+ return maskSecretsInText(text, this.perResourceSecrets.get(logicalId) ?? EMPTY_SECRETS);
26694
+ }
26695
+ /**
26696
+ * The LAZY `RetryLogger` the engine's generic `withRetry` wrapper threads
26697
+ * (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038) acceptance
26698
+ * item 1) — the same masking shape `drift.ts` and `rollback-executor.ts`
26699
+ * install, bound to the resource's OWN recorded secrets.
26700
+ *
26701
+ * `retry.ts` interpolates the AWS message verbatim into both the per-attempt
26702
+ * `debug` line and the give-up `warn` summary, and the bag this engine hands
26703
+ * a provider is RESOLVED — `perResourceSecrets` is populated immediately after
26704
+ * `resolver.resolve` and BEFORE the create / update call, so a secret is in
26705
+ * scope at every retried provider call. An AWS validation error routinely
26706
+ * quotes the offending value back, so the give-up summary could print it at
26707
+ * DEFAULT verbosity: the same hole #2038 found on the rollback path, one
26708
+ * caller over.
26709
+ *
26710
+ * The bag is resolved PER LINE rather than captured, because `withRetry`
26711
+ * (the private wrapper below) is reached from call sites that hold no bag —
26712
+ * DELETE, the observed-capture drain, the Outputs pass — and a
26713
+ * `logicalId`-keyed read is the only thing available there. The two
26714
+ * `--replace` sites do hold their caller's bag and use
26715
+ * {@link maskingRetryLoggerFor} instead; see the note on
26716
+ * {@link replaceDeleteFirstAndRecreate}'s `secrets` parameter for why binding
26717
+ * the bag beats looking it up whenever the bag is in scope.
26718
+ *
26719
+ * Do NOT restate that the engine "already masked its own error text" — an
26720
+ * earlier revision of this comment did, and it was FALSE: `provisionResource`
26721
+ * logged the raw AWS message at `error` level (a HIGHER level than this
26722
+ * summary) until #2038's review round. Only the EVENT store was masked.
26723
+ */
26724
+ maskingRetryLogger(logicalId) {
26725
+ return {
26726
+ debug: (msg) => this.logger.debug(this.maskForResource(logicalId, msg)),
26727
+ warn: (msg) => this.logger.warn(this.maskForResource(logicalId, msg))
26728
+ };
26729
+ }
26730
+ /**
26731
+ * The EAGER `RetryLogger`, bound to the bag the caller actually resolved
26732
+ * with (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)).
26733
+ *
26734
+ * Preferred over {@link maskingRetryLogger} wherever the resolution pass's
26735
+ * own `RecordedSecretValues` is in scope, for the reason
26736
+ * {@link replaceDeleteFirstAndRecreate}'s parameter list already states about
26737
+ * the masker it threads: a map looked up by logical id is a DIFFERENT thing
26738
+ * from the bag this call resolved with, and one file must not argue both
26739
+ * sides. It delegates to the shared `masking-retry-logger.ts` so the deploy
26740
+ * engine, `rollback-executor.ts` and `drift.ts` cannot drift apart.
26741
+ */
26742
+ maskingRetryLoggerFor(secrets) {
26743
+ return maskingRetryLogger(this.logger, secrets);
26744
+ }
26745
+ /**
26002
26746
  * What a failed Output resolution does, shared by both passes of
26003
26747
  * {@link resolveOutputs} so they cannot drift — the alias pass reports the
26004
26748
  * SAME failure for a name it could not resolve as the value pass does for a
@@ -26099,5 +26843,5 @@ var DeployEngine = class {
26099
26843
  };
26100
26844
 
26101
26845
  //#endregion
26102
- export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
26103
- //# sourceMappingURL=deploy-engine-DGuaeWaV.js.map
26846
+ export { isTerminationProtectionPropagationError as $, ResourceUpdateNotSupportedError as $n, validateAssetBucketName as $t, renderStatefulReason as A, derivePartitionAndUrlSuffix as An, INTRINSIC_KEYS as At, exportAliasCollisionScrubWarning as B, CdkdError as Bn, stringifyValue as Bt, isFinalSnapshotError as C, CFN_TEMPLATE_URL_LIMIT as Cn, s3BucketArn as Ct, extractDeploymentEventError as D, expectedOwnerParam as Dn, s3BucketWebsiteUrl as Dt, makeCanonicalizePropertiesFn as E, uploadCfnTemplate as En, s3BucketRegionalDomainName as Et, green as F, AwsClients as Fn, LockManager as Ft, collectInlinePolicyNamesManagedBySiblings as G, LocalMigrateError as Gn, rewriteTemplateAssetReferences as Gt, secretBearingStateKeyWarning as H, DependencyError as Hn, buildAssetRedirectMap as Ht, red as I, getAwsClients as In, S3StateBackend as It, findActionableSilentDrops as J, MissingCdkCliError as Jn, AssetModeResolver as Jt, clearOnUpdateRemoval as K, LocalStartServiceError as Kn, escapeRegExp$1 as Kt, yellow as L, resetAwsClients as Ln, rebuildClientForBucketRegion as Lt, bold as M, processStackMessages as Mn, withRetry as Mt, cyan as N, clearBucketRegionCache as Nn, DagBuilder as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, PARTITION_TABLE as On, applyRoleArnIfSet as Ot, gray as P, resolveBucketRegion as Pn, TemplateParser as Pt, disableInstanceApiTermination as Q, ResourceTimeoutError as Qn, parseBootstrapMarker as Qt, collectDeclaredOutputNames as R, setAwsClients as Rn, shouldRetainResource as Rt, createPreDeleteFinalSnapshot as S, CFN_TEMPLATE_BODY_LIMIT as Sn, scrubResourceRecord as St, unsupportedFinalSnapshotError as T, findLargeInlineResources as Tn, s3BucketDualStackDomainName as Tt, stateKeySecretExposure as U, DeployCancelledError as Un, createAssetRedirectResolver as Ut, isExportAliasCollision as V, ConfigError as Vn, WorkGraph as Vt, IAMRoleProvider as W, LocalInvokeBuildError as Wn, loadPublishableAssetManifest as Wt, CloudControlProvider as X, PartialFailureError as Xn, ensureAssetStorage as Xt, findSilentDropProperties as Y, NestedStackChildDirectDestroyError as Yn, BOOTSTRAP_MARKER_PREFIX as Yt, slowCcOperationTimeoutMs as Z, ProvisioningError as Zn, getBootstrapMarkerKey as Zt, computeImplicitDeleteEdges as _, resolveStateBucketWithDefault as _n, STATE_SOURCED_READBACK_RULES as _t, DeploymentEventsStore as a, runDockerForeground as an, isCdkdError as ar, normalizeAwsTagsToCfn as at, buildFinalSnapshotIdentifier as b, stateBucketExistenceConfirmed as bn, maskSecretsInText as bt, replayFailedOperations as c, getDockerImageBySourceHash as cn, isMarkedNonRetryable as cr, coerceCfnBoolean as ct, updatePartialReason as d, getDefaultStateBucketName as dn, markNonRetryable as dr, readConfigString as dt, validateContainerRepoName as en, StackHasActiveImportsError as er, IntrinsicFunctionResolver as et, UNSPECIFIED_SKIP_REASON as f, getLegacyStateBucketName as fn, __exportAll as fr, replayWarn as ft, IMPLICIT_DELETE_DEPENDENCIES as g, resolveSkipPrefix as gn, STATE_SOURCED_CROSS_GENERATION_RULES as gt, maskingRetryLogger as h, resolveCaptureObservedState as hn, requireConfigString as ht, DeploymentEventsReader as i, getDockerCmd as in, formatError as ir, WAFv2WebACLProvider as it, formatResourceLine as j, AssemblyReader as jn, describeTypeWithThrottleRetry as jt, isStatefulRecreateTargetSync as k, canonicalizeRegion as kn, DiffCalculator as kt, replayRollback as l, Synthesizer as ln, isRetryableTransientError as lr, configBooleanRefusal as lt, withResourceDeadline as m, resolveAutoAssetStorage as mn, requireConfigObject as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, buildDockerImage as nn, StateError as nr, getAccountInfo as nt, planFailedOps as o, runDockerStreaming as on, normalizeAwsError as or, resolveExplicitPhysicalId as ot, deleteSkipReason as p, resolveApp as pn, requireConfigArray as pt, ProviderRegistry as q, LockError as qn, stripControlChars as qt, DeployEngine as r, formatDockerLoginError as rn, SynthesisError as rr, refStateLookupFromResource as rt, planRollback as s, AssetManifestLoader as sn, withErrorHandling as sr, assertRegionMatch as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDenyExternalAccessPolicy as tn, StackTerminationProtectionError as tr, cfnRefValueFromPhysicalId as tt, updatePartialMessage as u, synthesisStatusMessage as un, isThrottlingError as ur, configStringRefusal as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, resolveStateBucketWithDefaultAndSource as vn, TEMPLATE_SOURCED_RULES as vt, refusesFinalSnapshot as w, MIGRATE_TMP_PREFIX as wn, s3BucketDomainName as wt, ccRoutedFinalSnapshotError as x, warnDeprecatedNoPrefixCliFlag as xn, redactSecretsForState as xt, PRE_DELETE_SNAPSHOT_TYPES as y, resolveUseCdkBootstrapAssets as yn, createSecretMasker as yt, collectPublishedOutputNames as z, AssetError as zn, AssetPublisher as zt };
26847
+ //# sourceMappingURL=deploy-engine-nwoIJsLn.js.map