@go-to-k/cdkd 0.284.82 → 0.284.83

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,5 +1,5 @@
1
1
  import { d as generateResourceName, f as generateResourceNameWithFallback, g as withStackName, m as looksLikeCdkdGeneratedName, n as getLogger, o as getLiveRenderer, p as getCurrentStackName, u as applyDefaultNameForFallback } from "./logger-C9-E73FI.js";
2
- import { t as getCdkdVersion } from "./version-D_b3uJus.js";
2
+ import { t as getCdkdVersion } from "./version-DI03miJ9.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -3868,6 +3868,168 @@ async function expectedOwnerParam(client) {
3868
3868
  return owner ? { ExpectedBucketOwner: owner } : {};
3869
3869
  }
3870
3870
 
3871
+ //#endregion
3872
+ //#region src/state/s3-noncurrent-version-purge.ts
3873
+ /**
3874
+ * Parenthetical used when a caller names nothing.
3875
+ *
3876
+ * True of ANY object this function is pointed at, which is the bar for a
3877
+ * default here: a caller that forgets to describe its object must still emit a
3878
+ * warning that is correct, just less specific. It deliberately does not guess
3879
+ * at content.
3880
+ */
3881
+ const DEFAULT_OBJECT_DESCRIPTION = "the body of an object cdkd has just reported as removed";
3882
+ /**
3883
+ * `objectDescription` for the custom-resource response sidecar.
3884
+ *
3885
+ * A SHARED CONSTANT rather than the same literal at both sites, for the reason
3886
+ * this whole module is shared: the provider's own `cleanupResponseObject` and
3887
+ * `cdkd gc`'s sweep of the abandoned placeholders delete the SAME object, so a
3888
+ * reader must not be able to tell from the warning which of the two produced
3889
+ * it. Two literals are how one of them drifts — and it is not hypothetical:
3890
+ * review probed it by editing `gc.ts`'s string alone and the suite stayed
3891
+ * green, because nothing tied the two together. One binding cannot drift, and
3892
+ * needs no test to say so.
3893
+ */
3894
+ const CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION = "a custom-resource response object, which is the handler's full cfn-response body including `Data`";
3895
+ /**
3896
+ * `DeleteObjects` is capped at 1000 entries per call.
3897
+ *
3898
+ * DEFENCE IN DEPTH, and unreachable today: `stale` is accumulated from a
3899
+ * SINGLE `ListObjectVersions` page, whose `Versions` + `DeleteMarkers` are
3900
+ * capped at 1000 COMBINED by `MaxKeys`, so the chunking below never takes its
3901
+ * second iteration. It is kept because the invariant it guards ("never hand
3902
+ * DeleteObjects more than 1000") is one a future change accumulating across
3903
+ * pages would silently break.
3904
+ *
3905
+ * Mutation coverage of this constant is ASYMMETRIC, which is worth stating
3906
+ * because the obvious summary is wrong in one direction: RAISING it is green
3907
+ * (nothing ever reaches the second chunk, so a bigger ceiling changes
3908
+ * nothing), while LOWERING it to 500 is RED — the multi-page fixture's
3909
+ * thousand-entry pages then split and the asserted batch shape changes. So the
3910
+ * value is fenced from below and not from above.
3911
+ */
3912
+ const DELETE_BATCH_SIZE = 1e3;
3913
+ /** How many failing keys the warning names before it truncates. */
3914
+ const MAX_NAMED_FAILURES = 5;
3915
+ /**
3916
+ * Label for a `DeleteObjects` error entry that carries no `Key`.
3917
+ *
3918
+ * S3 always populates it in practice; the point is that an unnameable failure
3919
+ * must still COUNT, because the alternative measured here was `failed.size`
3920
+ * reaching 0 and the whole warning disappearing.
3921
+ *
3922
+ * Each keyless entry gets its OWN slot (`<unknown key #1>`, `#2`, ...) rather
3923
+ * than sharing one. Collapsing them was defended as "the honest reading", but
3924
+ * it is honest about NAMING and not about COUNTING: N keyless failures then
3925
+ * reported `1 key(s)`, which is the same prefixes-not-keys under-count this
3926
+ * change was raised to fix, arriving through the branch that fixed it. One
3927
+ * slot per failure can over-count if S3 ever returns two entries for one
3928
+ * object, which is the direction that errs toward reporting too much.
3929
+ *
3930
+ * The slot name is SYNTHETIC and its uniqueness is not enforced: a real key
3931
+ * literally called `<unknown key #1>` would merge with the first keyless
3932
+ * entry and under-count by one. Unreachable here — every caller passes
3933
+ * `custom-resource-responses/<requestId>.json` — and stated rather than left
3934
+ * implied, because "the name cannot collide" is the kind of unstated
3935
+ * invariant this module exists to stop asserting.
3936
+ */
3937
+ const UNKNOWN_KEY_PREFIX = "<unknown key #";
3938
+ /** Reason recorded when a page says it is truncated but names no next key. */
3939
+ const TRUNCATED_NO_MARKER = "listing reported IsTruncated with no NextKeyMarker; the walk stopped early and versions may remain";
3940
+ /** Record a per-key failure reason without losing an earlier one. */
3941
+ function recordFailure(failed, key, reason) {
3942
+ const existing = failed.get(key);
3943
+ if (existing) existing.push(reason);
3944
+ else failed.set(key, [reason]);
3945
+ }
3946
+ const describe$1 = (error) => error instanceof Error ? error.message : String(error);
3947
+ async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {}) {
3948
+ if (keys.length === 0) return;
3949
+ const logger = options.logger ?? getLogger().child("s3-version-purge");
3950
+ const requestFields = options.requestFields ?? {};
3951
+ const wanted = new Set(keys);
3952
+ const prefixes = options.listPrefix !== void 0 ? [options.listPrefix] : keys;
3953
+ const failed = /* @__PURE__ */ new Map();
3954
+ const unknown = { n: 0 };
3955
+ for (const prefix of prefixes) try {
3956
+ await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown);
3957
+ } catch (error) {
3958
+ const affected = options.listPrefix !== void 0 ? keys : [prefix];
3959
+ for (const key of affected) recordFailure(failed, key, describe$1(error));
3960
+ }
3961
+ if (failed.size > 0) {
3962
+ const named = [...failed.entries()].slice(0, MAX_NAMED_FAILURES).map(([key, reasons]) => `${key} (${reasons.join("; ")})`);
3963
+ const elided = failed.size - named.length;
3964
+ logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${bucket}. Their previous versions survive and remain readable via GetObject with a VersionId (${options.objectDescription ?? DEFAULT_OBJECT_DESCRIPTION}). Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Failures: ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more)` : ""));
3965
+ }
3966
+ }
3967
+ /**
3968
+ * Paginate `ListObjectVersions` under one prefix and delete every returned
3969
+ * entry that is in `wanted` and is not the current version.
3970
+ *
3971
+ * Throws only when the LISTING fails; per-key delete failures are recorded in
3972
+ * `failed` and do not stop the walk.
3973
+ *
3974
+ * Safe on an UNVERSIONED bucket: S3 answers there with the single live object
3975
+ * carrying `VersionId: 'null'` and `IsLatest: true`, which the `IsLatest`
3976
+ * filter drops — so nothing is deleted and nothing throws. A `'null'` version
3977
+ * id is NOT filtered out on its own, because a bucket whose versioning was
3978
+ * SUSPENDED can carry a genuine noncurrent `'null'` version holding the body.
3979
+ */
3980
+ async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown) {
3981
+ let keyMarker;
3982
+ let versionIdMarker;
3983
+ do {
3984
+ const resp = await s3Client.send(new ListObjectVersionsCommand({
3985
+ Bucket: bucket,
3986
+ ...requestFields,
3987
+ Prefix: prefix,
3988
+ ...keyMarker !== void 0 && { KeyMarker: keyMarker },
3989
+ ...versionIdMarker !== void 0 && { VersionIdMarker: versionIdMarker }
3990
+ }));
3991
+ const stale = [];
3992
+ for (const entry of [...resp.Versions ?? [], ...resp.DeleteMarkers ?? []]) {
3993
+ if (entry.Key === void 0 || !wanted.has(entry.Key)) continue;
3994
+ if (entry.IsLatest === void 0) recordFailure(failed, entry.Key, `version ${entry.VersionId ?? "<unknown>"}: listing omitted IsLatest, so the entry was left alone rather than risk deleting a current version`);
3995
+ if (entry.IsLatest !== false) continue;
3996
+ if (!entry.VersionId) continue;
3997
+ stale.push({
3998
+ Key: entry.Key,
3999
+ VersionId: entry.VersionId
4000
+ });
4001
+ }
4002
+ for (let i = 0; i < stale.length; i += DELETE_BATCH_SIZE) {
4003
+ const batch = stale.slice(i, i + DELETE_BATCH_SIZE);
4004
+ try {
4005
+ const deleted = await s3Client.send(new DeleteObjectsCommand({
4006
+ Bucket: bucket,
4007
+ ...requestFields,
4008
+ Delete: {
4009
+ Objects: batch,
4010
+ Quiet: true
4011
+ }
4012
+ }));
4013
+ for (const err of deleted.Errors ?? []) {
4014
+ const reason = `version ${err.VersionId ?? "<unknown>"}: ${err.Code ?? "Error"}` + (err.Message ? ` - ${err.Message}` : "");
4015
+ if (err.Key !== void 0) recordFailure(failed, err.Key, reason);
4016
+ else {
4017
+ unknown.n += 1;
4018
+ recordFailure(failed, `${UNKNOWN_KEY_PREFIX}${unknown.n}>`, reason);
4019
+ }
4020
+ }
4021
+ } catch (error) {
4022
+ for (const object of batch) recordFailure(failed, object.Key, describe$1(error));
4023
+ }
4024
+ }
4025
+ if (resp.IsTruncated === true && resp.NextKeyMarker === void 0) {
4026
+ for (const key of wanted) if (key.startsWith(prefix)) recordFailure(failed, key, TRUNCATED_NO_MARKER);
4027
+ }
4028
+ keyMarker = resp.IsTruncated === true ? resp.NextKeyMarker : void 0;
4029
+ versionIdMarker = keyMarker !== void 0 ? resp.NextVersionIdMarker : void 0;
4030
+ } while (keyMarker !== void 0);
4031
+ }
4032
+
3871
4033
  //#endregion
3872
4034
  //#region src/cli/upload-cfn-template.ts
3873
4035
  /**
@@ -3900,8 +4062,9 @@ const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
3900
4062
  /**
3901
4063
  * Upload a CFn template body to the cdkd state bucket and return both a
3902
4064
  * virtual-hosted-style HTTPS URL CloudFormation can fetch via
3903
- * `TemplateURL` and a `cleanup` callback that deletes the object (and
3904
- * destroys the S3 client).
4065
+ * `TemplateURL` and a `cleanup` callback that deletes the object, PURGES its
4066
+ * noncurrent versions (the state bucket is versioned — see the callback), and
4067
+ * destroys the S3 client.
3905
4068
  *
3906
4069
  * The state bucket's actual region is resolved via `GetBucketLocation`
3907
4070
  * (cached per-process) so the upload client and the URL match the
@@ -3953,7 +4116,17 @@ async function uploadCfnTemplate(args) {
3953
4116
  ...await expectedOwnerParam(s3)
3954
4117
  }));
3955
4118
  } finally {
3956
- s3.destroy();
4119
+ try {
4120
+ await purgeNoncurrentKeyVersions(s3, bucket, [key], {
4121
+ requestFields: await expectedOwnerParam(s3),
4122
+ logger: getLogger(),
4123
+ objectDescription: "the transient CloudFormation template body uploaded for this command"
4124
+ });
4125
+ } catch (purgeError) {
4126
+ getLogger().warn(`Could not purge noncurrent versions of the transient template s3://${bucket}/${key}; its previous versions survive and remain readable via GetObject with a VersionId. Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key by hand. Underlying error: ${purgeError instanceof Error ? purgeError.message : String(purgeError)}`);
4127
+ } finally {
4128
+ s3.destroy();
4129
+ }
3957
4130
  }
3958
4131
  };
3959
4132
  return {
@@ -7600,145 +7773,6 @@ async function rebuildClientForBucketRegion(client, bucket, opts = {}) {
7600
7773
  return replacement;
7601
7774
  }
7602
7775
 
7603
- //#endregion
7604
- //#region src/state/s3-noncurrent-version-purge.ts
7605
- /**
7606
- * `DeleteObjects` is capped at 1000 entries per call.
7607
- *
7608
- * DEFENCE IN DEPTH, and unreachable today: `stale` is accumulated from a
7609
- * SINGLE `ListObjectVersions` page, whose `Versions` + `DeleteMarkers` are
7610
- * capped at 1000 COMBINED by `MaxKeys`, so the chunking below never takes its
7611
- * second iteration. It is kept because the invariant it guards ("never hand
7612
- * DeleteObjects more than 1000") is one a future change accumulating across
7613
- * pages would silently break.
7614
- *
7615
- * Mutation coverage of this constant is ASYMMETRIC, which is worth stating
7616
- * because the obvious summary is wrong in one direction: RAISING it is green
7617
- * (nothing ever reaches the second chunk, so a bigger ceiling changes
7618
- * nothing), while LOWERING it to 500 is RED — the multi-page fixture's
7619
- * thousand-entry pages then split and the asserted batch shape changes. So the
7620
- * value is fenced from below and not from above.
7621
- */
7622
- const DELETE_BATCH_SIZE = 1e3;
7623
- /** How many failing keys the warning names before it truncates. */
7624
- const MAX_NAMED_FAILURES = 5;
7625
- /**
7626
- * Label for a `DeleteObjects` error entry that carries no `Key`.
7627
- *
7628
- * S3 always populates it in practice; the point is that an unnameable failure
7629
- * must still COUNT, because the alternative measured here was `failed.size`
7630
- * reaching 0 and the whole warning disappearing.
7631
- *
7632
- * Each keyless entry gets its OWN slot (`<unknown key #1>`, `#2`, ...) rather
7633
- * than sharing one. Collapsing them was defended as "the honest reading", but
7634
- * it is honest about NAMING and not about COUNTING: N keyless failures then
7635
- * reported `1 key(s)`, which is the same prefixes-not-keys under-count this
7636
- * change was raised to fix, arriving through the branch that fixed it. One
7637
- * slot per failure can over-count if S3 ever returns two entries for one
7638
- * object, which is the direction that errs toward reporting too much.
7639
- *
7640
- * The slot name is SYNTHETIC and its uniqueness is not enforced: a real key
7641
- * literally called `<unknown key #1>` would merge with the first keyless
7642
- * entry and under-count by one. Unreachable here — every caller passes
7643
- * `custom-resource-responses/<requestId>.json` — and stated rather than left
7644
- * implied, because "the name cannot collide" is the kind of unstated
7645
- * invariant this module exists to stop asserting.
7646
- */
7647
- const UNKNOWN_KEY_PREFIX = "<unknown key #";
7648
- /** Reason recorded when a page says it is truncated but names no next key. */
7649
- const TRUNCATED_NO_MARKER = "listing reported IsTruncated with no NextKeyMarker; the walk stopped early and versions may remain";
7650
- /** Record a per-key failure reason without losing an earlier one. */
7651
- function recordFailure(failed, key, reason) {
7652
- const existing = failed.get(key);
7653
- if (existing) existing.push(reason);
7654
- else failed.set(key, [reason]);
7655
- }
7656
- const describe$1 = (error) => error instanceof Error ? error.message : String(error);
7657
- async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {}) {
7658
- if (keys.length === 0) return;
7659
- const logger = options.logger ?? getLogger().child("s3-version-purge");
7660
- const requestFields = options.requestFields ?? {};
7661
- const wanted = new Set(keys);
7662
- const prefixes = options.listPrefix !== void 0 ? [options.listPrefix] : keys;
7663
- const failed = /* @__PURE__ */ new Map();
7664
- const unknown = { n: 0 };
7665
- for (const prefix of prefixes) try {
7666
- await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown);
7667
- } catch (error) {
7668
- const affected = options.listPrefix !== void 0 ? keys : [prefix];
7669
- for (const key of affected) recordFailure(failed, key, describe$1(error));
7670
- }
7671
- if (failed.size > 0) {
7672
- const named = [...failed.entries()].slice(0, MAX_NAMED_FAILURES).map(([key, reasons]) => `${key} (${reasons.join("; ")})`);
7673
- const elided = failed.size - named.length;
7674
- logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${bucket}. Their previous versions survive and remain readable via GetObject with a VersionId (for a custom-resource response object that is the handler's full response body, including \`Data\`). Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Failures: ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more)` : ""));
7675
- }
7676
- }
7677
- /**
7678
- * Paginate `ListObjectVersions` under one prefix and delete every returned
7679
- * entry that is in `wanted` and is not the current version.
7680
- *
7681
- * Throws only when the LISTING fails; per-key delete failures are recorded in
7682
- * `failed` and do not stop the walk.
7683
- *
7684
- * Safe on an UNVERSIONED bucket: S3 answers there with the single live object
7685
- * carrying `VersionId: 'null'` and `IsLatest: true`, which the `IsLatest`
7686
- * filter drops — so nothing is deleted and nothing throws. A `'null'` version
7687
- * id is NOT filtered out on its own, because a bucket whose versioning was
7688
- * SUSPENDED can carry a genuine noncurrent `'null'` version holding the body.
7689
- */
7690
- async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown) {
7691
- let keyMarker;
7692
- let versionIdMarker;
7693
- do {
7694
- const resp = await s3Client.send(new ListObjectVersionsCommand({
7695
- Bucket: bucket,
7696
- ...requestFields,
7697
- Prefix: prefix,
7698
- ...keyMarker !== void 0 && { KeyMarker: keyMarker },
7699
- ...versionIdMarker !== void 0 && { VersionIdMarker: versionIdMarker }
7700
- }));
7701
- const stale = [];
7702
- for (const entry of [...resp.Versions ?? [], ...resp.DeleteMarkers ?? []]) {
7703
- if (entry.Key === void 0 || !wanted.has(entry.Key)) continue;
7704
- if (entry.IsLatest !== false) continue;
7705
- if (!entry.VersionId) continue;
7706
- stale.push({
7707
- Key: entry.Key,
7708
- VersionId: entry.VersionId
7709
- });
7710
- }
7711
- for (let i = 0; i < stale.length; i += DELETE_BATCH_SIZE) {
7712
- const batch = stale.slice(i, i + DELETE_BATCH_SIZE);
7713
- try {
7714
- const deleted = await s3Client.send(new DeleteObjectsCommand({
7715
- Bucket: bucket,
7716
- ...requestFields,
7717
- Delete: {
7718
- Objects: batch,
7719
- Quiet: true
7720
- }
7721
- }));
7722
- for (const err of deleted.Errors ?? []) {
7723
- const reason = `version ${err.VersionId ?? "<unknown>"}: ${err.Code ?? "Error"}` + (err.Message ? ` - ${err.Message}` : "");
7724
- if (err.Key !== void 0) recordFailure(failed, err.Key, reason);
7725
- else {
7726
- unknown.n += 1;
7727
- recordFailure(failed, `${UNKNOWN_KEY_PREFIX}${unknown.n}>`, reason);
7728
- }
7729
- }
7730
- } catch (error) {
7731
- for (const object of batch) recordFailure(failed, object.Key, describe$1(error));
7732
- }
7733
- }
7734
- if (resp.IsTruncated === true && resp.NextKeyMarker === void 0) {
7735
- for (const key of wanted) if (key.startsWith(prefix)) recordFailure(failed, key, TRUNCATED_NO_MARKER);
7736
- }
7737
- keyMarker = resp.IsTruncated === true ? resp.NextKeyMarker : void 0;
7738
- versionIdMarker = keyMarker !== void 0 ? resp.NextVersionIdMarker : void 0;
7739
- } while (keyMarker !== void 0);
7740
- }
7741
-
7742
7776
  //#endregion
7743
7777
  //#region src/state/s3-state-backend.ts
7744
7778
  /**
@@ -8060,7 +8094,6 @@ var S3StateBackend = class {
8060
8094
  }));
8061
8095
  this.logger.debug(`Deleted legacy state for stack: ${stackName}`);
8062
8096
  }
8063
- await this.deleteRollbackJournal(stackName, region);
8064
8097
  this.logger.debug(`State deleted: ${stackName} (${region})`);
8065
8098
  } catch (error) {
8066
8099
  const normalized = normalizeAwsError(error, {
@@ -8068,6 +8101,8 @@ var S3StateBackend = class {
8068
8101
  operation: "DeleteObject"
8069
8102
  });
8070
8103
  throw new StateError(`Failed to delete state for stack '${stackName}' (${region}): ${normalized.message}`, normalized);
8104
+ } finally {
8105
+ await this.deleteRollbackJournal(stackName, region);
8071
8106
  }
8072
8107
  }
8073
8108
  /**
@@ -8314,7 +8349,8 @@ var S3StateBackend = class {
8314
8349
  logger: this.logger
8315
8350
  });
8316
8351
  } catch (error) {
8317
- this.logger.warn(`Could not purge noncurrent versions of ${keys.length} key(s) in bucket '${this.config.bucket}': the purge could not be started. Their previous versions survive and remain readable via GetObject with a VersionId. Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Underlying error: ${error instanceof Error ? error.message : String(error)}`);
8352
+ const describing = options.objectDescription ? ` (${options.objectDescription})` : "";
8353
+ this.logger.warn(`Could not purge noncurrent versions of ${keys.length} key(s) in bucket '${this.config.bucket}': the purge could not be started. Their previous versions survive and remain readable via GetObject with a VersionId${describing}. Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Underlying error: ${error instanceof Error ? error.message : String(error)}`);
8318
8354
  }
8319
8355
  }
8320
8356
  /**
@@ -8387,19 +8423,48 @@ var S3StateBackend = class {
8387
8423
  * Delete the stack's rollback journal object (idempotent). Called on the
8388
8424
  * deploy success path, after a clean rollback, and via {@link deleteState}
8389
8425
  * so `cdkd destroy` / `cdkd state destroy` sweep it too.
8426
+ *
8427
+ * TWO steps, and the second is not housekeeping (issue
8428
+ * [#2346](https://github.com/go-to-k/cdkd/issues/2346) site 4). `cdkd
8429
+ * bootstrap` turns VERSIONING ON for the state bucket, so the
8430
+ * `DeleteObject` above only writes a DELETE MARKER and leaves every prior
8431
+ * version of the journal readable through `GetObject` with a `VersionId`.
8432
+ * The journal's `failedOperations[].attemptedProperties` is the PROPERTIES
8433
+ * OF THE FAILED WRITE, verbatim — measured 2026-08-20 on
8434
+ * `CdkdDeletionPolicySnapshotHeavyExample` as four surviving versions each
8435
+ * carrying a literal `"MasterUserPassword": "Cdkdcf2f..."` after cdkd
8436
+ * reported the state deleted (recorded in the `s3_stack_prefix` comment of
8437
+ * `tests/integration/s3-versions.sh`, ~line 270). A
8438
+ * delete-only cleanup therefore reports success while the credential stays
8439
+ * retrievable by anyone holding `s3:GetObjectVersion`.
8440
+ *
8441
+ * Unlike `state.json` — whose noncurrent versions ARE the state-recovery
8442
+ * capability versioning is enabled FOR, which is why {@link deleteState}
8443
+ * deliberately does NOT purge — the journal is TRANSIENT by design: it
8444
+ * exists only between a failed / interrupted deploy and its `cdkd
8445
+ * rollback`. There is no recovery capability to weigh against the purge.
8446
+ *
8447
+ * The purge runs UNCONDITIONALLY, including on the arm where the delete
8448
+ * failed. `purgeNoncurrentKeyVersions` filters on `IsLatest`, so a key
8449
+ * whose delete failed keeps its current version intact and only its history
8450
+ * goes — the worst case is that the object survives while its old bodies do
8451
+ * not, which is the safe direction. Skipping the purge when the delete
8452
+ * threw would leave every readable version behind with no warning at all,
8453
+ * the same partial-failure gap `cdkd gc` closed with its `finally`.
8390
8454
  */
8391
8455
  async deleteRollbackJournal(stackName, region) {
8392
8456
  await this.ensureClientForBucket();
8457
+ const key = this.getRollbackJournalKey(stackName, region);
8393
8458
  try {
8394
8459
  await this.s3Client.send(new DeleteObjectCommand({
8395
8460
  Bucket: this.config.bucket,
8396
8461
  ...await this.ownerParam(),
8397
- Key: this.getRollbackJournalKey(stackName, region)
8462
+ Key: key
8398
8463
  }));
8399
8464
  } catch (error) {
8400
- if (isNoSuchKey(error) || error.name === "NotFound") return;
8401
- this.logger.warn(`Failed to delete rollback journal for '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`);
8465
+ if (!isNoSuchKey(error) && error.name !== "NotFound") this.logger.warn(`Failed to delete rollback journal for '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`);
8402
8466
  }
8467
+ await this.purgeNoncurrentVersions([key], { objectDescription: "the rollback journal, whose `failedOperations[].attemptedProperties` records the properties of the failed write verbatim" });
8403
8468
  }
8404
8469
  /**
8405
8470
  * HeadObject probe — returns true on 200, false on NotFound. Other errors
@@ -20476,7 +20541,7 @@ var CloudControlProvider = class {
20476
20541
  await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
20477
20542
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
20478
20543
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
20479
- const { ASGProvider } = await import("./asg-provider-BoJz-x3Q.js").then((n) => n.n);
20544
+ const { ASGProvider } = await import("./asg-provider-B-KONBYk.js").then((n) => n.n);
20480
20545
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
20481
20546
  }
20482
20547
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -23388,7 +23453,10 @@ var CustomResourceProvider = class CustomResourceProvider {
23388
23453
  } catch (error) {
23389
23454
  this.logger.debug(`Failed to delete custom-resource response object s3://${bucket}/${responseKey}; it remains as a current object. Underlying error: ${error instanceof Error ? error.message : String(error)}`);
23390
23455
  }
23391
- await purgeNoncurrentKeyVersions(this.s3Client, bucket, [responseKey], { logger: this.logger });
23456
+ await purgeNoncurrentKeyVersions(this.s3Client, bucket, [responseKey], {
23457
+ logger: this.logger,
23458
+ objectDescription: CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION
23459
+ });
23392
23460
  }
23393
23461
  /**
23394
23462
  * Convert property values to strings for CloudFormation compatibility
@@ -32390,5 +32458,5 @@ var DeployEngine = class {
32390
32458
  };
32391
32459
 
32392
32460
  //#endregion
32393
- export { maskerOrIdentity as $, CFN_TEMPLATE_BODY_LIMIT as $n, maskSecretsInText as $t, renderStatefulReason as A, describeAwsFailure as An, ProvisioningError as Ar, requireConfigString as At, exportAliasCollisionScrubWarning as B, Synthesizer as Bn, withErrorHandling as Br, INTRINSIC_KEYS as Bt, isFinalSnapshotError as C, getBootstrapMarkerKey as Cn, LocalInvokeBuildError as Cr, coerceCfnBoolean as Ct, extractDeploymentEventError as D, validateAssetBucketName as Dn, MissingCdkCliError as Dr, replayWarn as Dt, makeCanonicalizePropertiesFn as E, readBootstrapMarkerBody as En, LockError as Er, readConfigString as Et, green as F, partitionSensitiveEnv as Fn, StateError as Fr, s3BucketDualStackDomainName as Ft, IAMRoleProvider as G, resolveAutoAssetStorage as Gn, markRedactedCause as Gr, STATE_SOURCED_CROSS_GENERATION_RULES as Gt, secretBearingStateKeyWarning as H, getDefaultStateBucketName as Hn, isRetryableTransientError as Hr, withRetry as Ht, red as I, runDockerForeground as In, SynthesisError as Ir, s3BucketRegionalDomainName as It, ProviderRegistry as J, resolveStateBucketWithDefault as Jn, createSecretMasker as Jt, collectInlinePolicyNamesManagedBySiblings as K, resolveCaptureObservedState as Kn, retryClassificationText as Kr, STATE_SOURCED_READBACK_RULES as Kt, yellow as L, runDockerStreaming as Ln, formatError as Lr, s3BucketWebsiteUrl as Lt, bold as M, dockerSpawnEnvWithSensitive as Mn, ResourceUpdateNotSupportedError as Mr, producerRegionsFromState as Mt, cyan as N, formatDockerLoginError as Nn, StackHasActiveImportsError as Nr, s3BucketArn as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, validateContainerRepoName as On, NestedStackChildDirectDestroyError as Or, requireConfigArray as Ot, gray as P, getDockerCmd as Pn, StackTerminationProtectionError as Pr, s3BucketDomainName as Pt, maskDeep as Q, warnDeprecatedNoPrefixCliFlag as Qn, maskSecretsInError as Qt, collectDeclaredOutputNames as R, AssetManifestLoader as Rn, isCdkdError as Rr, applyRoleArnIfSet as Rt, createPreDeleteFinalSnapshot as S, ensureAssetStorage as Sn, DynamicReferenceRegionAmbiguousError as Sr, assertRegionMatch as St, unsupportedFinalSnapshotError as T, parseBootstrapMarker as Tn, LocalStartServiceError as Tr, configStringRefusal as Tt, stateKeySecretExposure as U, getLegacyStateBucketName as Un, isThrottlingError as Ur, DagBuilder as Ut, isExportAliasCollision as V, synthesisStatusMessage as Vn, isMarkedNonRetryable as Vr, describeTypeWithThrottleRetry as Vt, getCurrentResourceSecrets as W, resolveApp as Wn, markNonRetryable as Wr, TemplateParser as Wt, findSilentDropProperties as X, resolveUseCdkBootstrapAssets as Xn, errorCauseChain as Xt, findActionableSilentDrops as Y, resolveStateBucketWithDefaultAndSource as Yn, dynamicReferenceTokens as Yt, createMaskedRetryLogger as Z, stateBucketExistenceConfirmed as Zn, isSingleDynamicReferenceToken as Zt, computeImplicitDeleteEdges as _, escapeRegExp$1 as _n, CdkdError as _r, parameterTypeMayLoseSecretIdentity as _t, DeploymentEventsStore as a, rebuildClientForBucketRegion as an, PARTITION_TABLE as ar, isInterruptedWaitError as at, buildFinalSnapshotIdentifier as b, BOOTSTRAP_MARKER_PREFIX as bn, DependencyError as br, normalizeAwsTagsToCfn as bt, replayFailedOperations as c, importableOutputs as cn, AssemblyReader as cr, slowCcOperationTimeoutMs as ct, updatePartialReason as d, stringifyValue as dn, resolveBucketRegion as dr, IntrinsicFunctionResolver as dt, redactSecretsForState as en, CFN_TEMPLATE_URL_LIMIT as er, CUSTOM_RESOURCE_RESPONSE_PREFIX as et, UNSPECIFIED_SKIP_REASON as f, WorkGraph as fn, AwsClients as fr, carriesDynamicReference as ft, IMPLICIT_DELETE_DEPENDENCIES as g, rewriteTemplateAssetReferences as gn, AssetError as gr, isUnboundTemplateParameter as gt, maskingRetryLogger as h, loadPublishableAssetManifest as hn, setAwsClients as hr, getAccountInfo as ht, DeploymentEventsReader as i, S3StateBackend as in, expectedOwnerParam as ir, interruptWatchListenerCount as it, formatResourceLine as j, buildDockerImage as jn, ResourceTimeoutError as jr, classifyReplaySecretRegion as jt, isStatefulRecreateTargetSync as k, buildDenyExternalAccessPolicy as kn, PartialFailureError as kr, requireConfigObject as kt, replayRollback as l, shouldRetainResource as ln, processStackMessages as lr, disableInstanceApiTermination as lt, withResourceDeadline as m, createAssetRedirectResolver as mn, resetAwsClients as mr, coerceParameterTypedValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, LockManager as nn, findLargeInlineResources as nr, beginCommandInterruptScope as nt, planFailedOps as o, exportNamesCarriedFrom as on, canonicalizeRegion as or, startInterruptWatch as ot, deleteSkipReason as p, buildAssetRedirectMap as pn, getAwsClients as pr, cfnRefValueFromPhysicalId as pt, clearOnUpdateRemoval as q, resolveSkipPrefix as qn, __exportAll as qr, TEMPLATE_SOURCED_RULES as qt, DeployEngine as r, displaySafe as rn, uploadCfnTemplate as rr, endCommandInterruptScope as rt, planRollback as s, importableOutputKeys as sn, derivePartitionAndUrlSuffix as sr, CloudControlProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, scrubResourceRecord as tn, MIGRATE_TMP_PREFIX as tr, DEFAULT_STATE_PREFIX as tt, updatePartialMessage as u, AssetPublisher as un, clearBucketRegionCache as ur, isTerminationProtectionPropagationError as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, stripControlChars as vn, ConfigError as vr, refStateLookupFromResource as vt, refusesFinalSnapshot as w, isCrossRegionRedirect as wn, LocalMigrateError as wr, configBooleanRefusal as wt, ccRoutedFinalSnapshotError as x, assertAssetBucketRegion as xn, DeployCancelledError as xr, resolveExplicitPhysicalId as xt, PRE_DELETE_SNAPSHOT_TYPES as y, AssetModeResolver as yn, CrossAccountSecretRefusalError as yr, WAFv2WebACLProvider as yt, collectPublishedOutputNames as z, getDockerImageBySourceHash as zn, normalizeAwsError as zr, DiffCalculator as zt };
32394
- //# sourceMappingURL=deploy-engine-Dh4Jid_H.js.map
32461
+ export { maskerOrIdentity as $, CFN_TEMPLATE_BODY_LIMIT as $n, maskSecretsInText as $t, renderStatefulReason as A, describeAwsFailure as An, PartialFailureError as Ar, requireConfigString as At, exportAliasCollisionScrubWarning as B, Synthesizer as Bn, normalizeAwsError as Br, INTRINSIC_KEYS as Bt, isFinalSnapshotError as C, getBootstrapMarkerKey as Cn, DynamicReferenceRegionAmbiguousError as Cr, coerceCfnBoolean as Ct, extractDeploymentEventError as D, validateAssetBucketName as Dn, LockError as Dr, replayWarn as Dt, makeCanonicalizePropertiesFn as E, readBootstrapMarkerBody as En, LocalStartServiceError as Er, readConfigString as Et, green as F, partitionSensitiveEnv as Fn, StackTerminationProtectionError as Fr, s3BucketDualStackDomainName as Ft, IAMRoleProvider as G, resolveAutoAssetStorage as Gn, markNonRetryable as Gr, STATE_SOURCED_CROSS_GENERATION_RULES as Gt, secretBearingStateKeyWarning as H, getDefaultStateBucketName as Hn, isMarkedNonRetryable as Hr, withRetry as Ht, red as I, runDockerForeground as In, StateError as Ir, s3BucketRegionalDomainName as It, ProviderRegistry as J, resolveStateBucketWithDefault as Jn, __exportAll as Jr, createSecretMasker as Jt, collectInlinePolicyNamesManagedBySiblings as K, resolveCaptureObservedState as Kn, markRedactedCause as Kr, STATE_SOURCED_READBACK_RULES as Kt, yellow as L, runDockerStreaming as Ln, SynthesisError as Lr, s3BucketWebsiteUrl as Lt, bold as M, dockerSpawnEnvWithSensitive as Mn, ResourceTimeoutError as Mr, producerRegionsFromState as Mt, cyan as N, formatDockerLoginError as Nn, ResourceUpdateNotSupportedError as Nr, s3BucketArn as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, validateContainerRepoName as On, MissingCdkCliError as Or, requireConfigArray as Ot, gray as P, getDockerCmd as Pn, StackHasActiveImportsError as Pr, s3BucketDomainName as Pt, maskDeep as Q, warnDeprecatedNoPrefixCliFlag as Qn, maskSecretsInError as Qt, collectDeclaredOutputNames as R, AssetManifestLoader as Rn, formatError as Rr, applyRoleArnIfSet as Rt, createPreDeleteFinalSnapshot as S, ensureAssetStorage as Sn, DeployCancelledError as Sr, assertRegionMatch as St, unsupportedFinalSnapshotError as T, parseBootstrapMarker as Tn, LocalMigrateError as Tr, configStringRefusal as Tt, stateKeySecretExposure as U, getLegacyStateBucketName as Un, isRetryableTransientError as Ur, DagBuilder as Ut, isExportAliasCollision as V, synthesisStatusMessage as Vn, withErrorHandling as Vr, describeTypeWithThrottleRetry as Vt, getCurrentResourceSecrets as W, resolveApp as Wn, isThrottlingError as Wr, TemplateParser as Wt, findSilentDropProperties as X, resolveUseCdkBootstrapAssets as Xn, errorCauseChain as Xt, findActionableSilentDrops as Y, resolveStateBucketWithDefaultAndSource as Yn, dynamicReferenceTokens as Yt, createMaskedRetryLogger as Z, stateBucketExistenceConfirmed as Zn, isSingleDynamicReferenceToken as Zt, computeImplicitDeleteEdges as _, escapeRegExp$1 as _n, AssetError as _r, parameterTypeMayLoseSecretIdentity as _t, DeploymentEventsStore as a, rebuildClientForBucketRegion as an, expectedOwnerParam as ar, isInterruptedWaitError as at, buildFinalSnapshotIdentifier as b, BOOTSTRAP_MARKER_PREFIX as bn, CrossAccountSecretRefusalError as br, normalizeAwsTagsToCfn as bt, replayFailedOperations as c, importableOutputs as cn, derivePartitionAndUrlSuffix as cr, slowCcOperationTimeoutMs as ct, updatePartialReason as d, stringifyValue as dn, clearBucketRegionCache as dr, IntrinsicFunctionResolver as dt, redactSecretsForState as en, CFN_TEMPLATE_URL_LIMIT as er, CUSTOM_RESOURCE_RESPONSE_PREFIX as et, UNSPECIFIED_SKIP_REASON as f, WorkGraph as fn, resolveBucketRegion as fr, carriesDynamicReference as ft, IMPLICIT_DELETE_DEPENDENCIES as g, rewriteTemplateAssetReferences as gn, setAwsClients as gr, isUnboundTemplateParameter as gt, maskingRetryLogger as h, loadPublishableAssetManifest as hn, resetAwsClients as hr, getAccountInfo as ht, DeploymentEventsReader as i, S3StateBackend as in, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as ir, interruptWatchListenerCount as it, formatResourceLine as j, buildDockerImage as jn, ProvisioningError as jr, classifyReplaySecretRegion as jt, isStatefulRecreateTargetSync as k, buildDenyExternalAccessPolicy as kn, NestedStackChildDirectDestroyError as kr, requireConfigObject as kt, replayRollback as l, shouldRetainResource as ln, AssemblyReader as lr, disableInstanceApiTermination as lt, withResourceDeadline as m, createAssetRedirectResolver as mn, getAwsClients as mr, coerceParameterTypedValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, LockManager as nn, findLargeInlineResources as nr, beginCommandInterruptScope as nt, planFailedOps as o, exportNamesCarriedFrom as on, PARTITION_TABLE as or, startInterruptWatch as ot, deleteSkipReason as p, buildAssetRedirectMap as pn, AwsClients as pr, cfnRefValueFromPhysicalId as pt, clearOnUpdateRemoval as q, resolveSkipPrefix as qn, retryClassificationText as qr, TEMPLATE_SOURCED_RULES as qt, DeployEngine as r, displaySafe as rn, uploadCfnTemplate as rr, endCommandInterruptScope as rt, planRollback as s, importableOutputKeys as sn, canonicalizeRegion as sr, CloudControlProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, scrubResourceRecord as tn, MIGRATE_TMP_PREFIX as tr, DEFAULT_STATE_PREFIX as tt, updatePartialMessage as u, AssetPublisher as un, processStackMessages as ur, isTerminationProtectionPropagationError as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, stripControlChars as vn, CdkdError as vr, refStateLookupFromResource as vt, refusesFinalSnapshot as w, isCrossRegionRedirect as wn, LocalInvokeBuildError as wr, configBooleanRefusal as wt, ccRoutedFinalSnapshotError as x, assertAssetBucketRegion as xn, DependencyError as xr, resolveExplicitPhysicalId as xt, PRE_DELETE_SNAPSHOT_TYPES as y, AssetModeResolver as yn, ConfigError as yr, WAFv2WebACLProvider as yt, collectPublishedOutputNames as z, getDockerImageBySourceHash as zn, isCdkdError as zr, DiffCalculator as zt };
32462
+ //# sourceMappingURL=deploy-engine-BES1Z20a.js.map