@go-to-k/cdkd 0.281.24 → 0.281.26

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.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-BkXNtI7L.js";
2
+ import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-XC8HOR51.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-CxK7d3pH.js";
4
+ import { t as ASGProvider } from "./asg-provider-II3TmZ4t.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -5172,10 +5172,81 @@ function foldToleratedAliases(bag, aliases) {
5172
5172
  }
5173
5173
  return out ?? bag;
5174
5174
  }
5175
+ /** The transition date member, held in a CONST so the fold writes it COMPUTED. */
5176
+ const TRANSITION_DATE_KEY = "TransitionDate";
5175
5177
  /** The lifecycle `Transitions[]` aliases the applier accepts (issue #1748). */
5176
5178
  const TRANSITION_ALIASES = [["TransitionInDays", "Days"], ["TransitionDate", "Date"]];
5177
5179
  /** The lifecycle `NoncurrentVersionTransitions[]` alias (issue #1748). */
5178
5180
  const NONCURRENT_VERSION_TRANSITION_ALIASES = [["TransitionInDays", "NoncurrentDays"]];
5181
+ /** The refusal path for the notification EventBridge boolean (issue #1759). */
5182
+ const EVENTBRIDGE_CONFIG_PATH = "AWS::S3::Bucket NotificationConfiguration.EventBridgeConfiguration";
5183
+ /**
5184
+ * The block's own key, held in a CONST so the fold below writes it as a
5185
+ * COMPUTED key.
5186
+ *
5187
+ * Load-bearing, not style: `gen-nested-key-coverage`'s write-evidence walk reads
5188
+ * a bag-derived spread-and-patch literal that NAMES a member
5189
+ * (`{ ...config, EventBridgeConfiguration: … }`) as the #1475 whole-blob
5190
+ * HAND-OFF and wildcard-credits everything beneath that path as DELIVERED — so
5191
+ * a DIFF-side fold spelled that way would switch the write pass off for the
5192
+ * whole notification subtree, and the critic's only visible complaint would be
5193
+ * that some allow-list entries went "stale". Same reason
5194
+ * {@link canonicalizeBlock} exists.
5195
+ */
5196
+ const EVENTBRIDGE_CONFIG_KEY = "EventBridgeConfiguration";
5197
+ /**
5198
+ * What a DECLARED `EventBridgeConfiguration` block with no `EventBridgeEnabled`
5199
+ * key means. CFn marks the member required, so the block's presence is the
5200
+ * affirmative declaration; this is also the pre-#1759 behavior for that shape.
5201
+ */
5202
+ const EVENTBRIDGE_DEFAULT_ENABLED = true;
5203
+ /**
5204
+ * The boolean the wire derives from a USABLE `EventBridgeConfiguration` block
5205
+ * (issue #1759) — the ONE definition `applyNotificationConfiguration` and
5206
+ * {@link effectiveNotificationConfiguration} share, so the value that goes on
5207
+ * the wire and the value recorded into state cannot drift apart.
5208
+ *
5209
+ * Callers MUST have cleared `configBooleanRefusal` first; the `??` is only
5210
+ * correct once every declared-but-unusable value has been refused.
5211
+ */
5212
+ function eventBridgeEnabledOf(eb) {
5213
+ return coerceCfnBoolean(isPlainObject$1(eb) ? eb["EventBridgeEnabled"] : void 0) ?? EVENTBRIDGE_DEFAULT_ENABLED;
5214
+ }
5215
+ /** The notification families whose empty declaration is never SENT (issue #1759). */
5216
+ const NOTIFICATION_LIST_KEYS = [
5217
+ "TopicConfigurations",
5218
+ "QueueConfigurations",
5219
+ "LambdaConfigurations"
5220
+ ];
5221
+ /**
5222
+ * Drop a declared-but-EMPTY collection the applier never sends (issue #1759).
5223
+ *
5224
+ * MEASURED before choosing this answer, because the sibling shape answers the
5225
+ * opposite way: the lifecycle / CORS empty-collection skip RECORDS its
5226
+ * placeholder (issue #1718) precisely because `readLifecycle` / `readCors`
5227
+ * always emit `{Rules: []}` / `{CorsRules: []}` for an unconfigured bucket, so
5228
+ * the declared empty collection already equals the readback. `readNotification`
5229
+ * does NOT: each family is emitted only under
5230
+ * `resp.<Family> && resp.<Family>.length > 0`, so an unconfigured bucket comes
5231
+ * back with the key ABSENT. A record keeping `[]` therefore describes a key set
5232
+ * AWS can never report, and the honest answer here is the DROP.
5233
+ *
5234
+ * Scoped to a genuinely EMPTY ARRAY. A non-array declaration is skipped by the
5235
+ * applier's `Array.isArray(...)` gate too, but it is MALFORMED: dropping it
5236
+ * would make the desired side compare equal and stop the difference being
5237
+ * reported, which is the concealment `.claude/rules/providers.md` finding 3
5238
+ * refuses. It is left visible instead.
5239
+ */
5240
+ function dropEmptyNotificationFamilies(config) {
5241
+ let out;
5242
+ for (const listKey of NOTIFICATION_LIST_KEYS) {
5243
+ const list = config[listKey];
5244
+ if (!Array.isArray(list) || list.length > 0) continue;
5245
+ out ??= { ...config };
5246
+ delete out[listKey];
5247
+ }
5248
+ return out ?? config;
5249
+ }
5179
5250
  /** The `NotificationConfiguration` ARN aliases, per family (issue #1748). */
5180
5251
  const NOTIFICATION_ARN_ALIASES = [
5181
5252
  ["TopicConfigurations", [["Topic", "TopicArn"]]],
@@ -5248,6 +5319,439 @@ function effectiveNotificationConfiguration(config) {
5248
5319
  if (!isPlainObject$1(config)) return config;
5249
5320
  let out = config;
5250
5321
  for (const [listKey, aliases] of NOTIFICATION_ARN_ALIASES) out = canonicalizeItemList(out, listKey, (item) => foldNotificationEvents(foldToleratedAliases(item, aliases)));
5322
+ out = dropEmptyNotificationFamilies(out);
5323
+ return foldEventBridgeConfiguration(out);
5324
+ }
5325
+ /**
5326
+ * Normalize `EventBridgeConfiguration` onto the shape `readNotification` emits
5327
+ * (issue #1759, items 2 and 3 of the same block).
5328
+ *
5329
+ * CFn is stringly typed, so `EventBridgeEnabled: 'true'` is a legitimate
5330
+ * declaration; the wire turns the value into presence/absence and
5331
+ * `readNotification` emits the BOOLEAN (`{EventBridgeEnabled: <bool>}`, the
5332
+ * #1430 shape). A record holding the STRING can therefore never match the
5333
+ * readback. This is #1633's lossless-coercion arm and it meets the #1643 bar —
5334
+ * what AWS reports back is exactly the boolean cdkd derived at send time. A
5335
+ * declared `null` folds to `false` for the same reason: the applier reads it as
5336
+ * "block omitted" and sends nothing.
5337
+ *
5338
+ * Keyed off the DECLARED shape and off the wire's own predicate, never off a
5339
+ * refusal: a value `configBooleanRefusal` rejects is left COMPLETELY alone (no
5340
+ * patch, and the key not dropped), because the wire SKIPS the whole Put for it
5341
+ * and AWS keeps the previous configuration. Folding it would collapse the
5342
+ * desired side onto the retained value, the provider would never be called, and
5343
+ * the skip warning would stop — the "mirror what the wire does" rule.
5344
+ *
5345
+ * MEASURED AND DELIBERATELY NOT FOLDED (1) — the per-item `Id`, which issue
5346
+ * #1755 asked to be measured rather than assumed. The applier sends
5347
+ * `Id: t['Id']`, so a template declaring none sends none, AWS GENERATES one, and
5348
+ * `readNotification` emits it (`if (t.Id !== undefined) e['Id'] = t.Id`). That is
5349
+ * an AWS-COMPUTED value, not a spelling: it belongs in `observedProperties`,
5350
+ * which `readCurrentState` already populates, and `.claude/rules/providers.md` is
5351
+ * explicit that a computed value in `properties` makes the DESIRED baseline drift
5352
+ * from the template and disables the #1160 absent-field removal derivation. There
5353
+ * is also nothing to fold TOWARDS — the value does not exist at send time, which
5354
+ * is the #1643 "knowable at send time" test failing. So: no fold, by measurement
5355
+ * rather than by omission.
5356
+ *
5357
+ * MEASURED AND DELIBERATELY NOT FOLDED (2): `readNotification` emits
5358
+ * `EventBridgeConfiguration` UNCONDITIONALLY, so a notification configuration
5359
+ * that declares no EventBridge block at all is one key short of the readback.
5360
+ * That is NOT this class — nothing was declared, so there is no spelling to
5361
+ * normalize, and supplying the key would record one the template does not have
5362
+ * on EVERY notification-configured bucket (the shape #1723 records as the
5363
+ * reason its own arm was left unanswered). `readNotification`'s own comment
5364
+ * already scopes that difference: it is a ONE-TIME, self-clearing drift on the
5365
+ * `properties`-fallback baseline, deliberately accepted by #1430, not the
5366
+ * permanent phantom drift this fold exists to remove.
5367
+ *
5368
+ * @returns `config` BY IDENTITY when nothing is declared, when the declared
5369
+ * block is already the emitted shape, or when it is unfoldable.
5370
+ */
5371
+ function foldEventBridgeConfiguration(config) {
5372
+ const eb = config[EVENTBRIDGE_CONFIG_KEY];
5373
+ if (eb === void 0) return config;
5374
+ if (eb !== null && configBooleanRefusal(eb, "EventBridgeEnabled", EVENTBRIDGE_CONFIG_PATH)) return config;
5375
+ const enabled = eb === null ? false : eventBridgeEnabledOf(eb);
5376
+ const declared = isPlainObject$1(eb) ? eb["EventBridgeEnabled"] : void 0;
5377
+ if (isPlainObject$1(eb) && Object.keys(eb).length === 1 && Object.is(declared, enabled)) return config;
5378
+ return {
5379
+ ...config,
5380
+ [EVENTBRIDGE_CONFIG_KEY]: { EventBridgeEnabled: enabled }
5381
+ };
5382
+ }
5383
+ /** The refusal paths the lifecycle applier and its fold SHARE (issue #1755). */
5384
+ const LIFECYCLE_RULE_PATH = "AWS::S3::Bucket LifecycleConfiguration.Rules[]";
5385
+ const LIFECYCLE_FILTER_PATH = `${LIFECYCLE_RULE_PATH}.Filter`;
5386
+ const LIFECYCLE_TAG_FILTERS_PATH = `${LIFECYCLE_RULE_PATH}.TagFilters`;
5387
+ /** What `readConfigString(rule, 'Status', ...)` substitutes for an ABSENT key. */
5388
+ const LIFECYCLE_DEFAULT_STATUS = "Enabled";
5389
+ /** The rule's status member, held in a CONST so the fold writes it COMPUTED. */
5390
+ const LIFECYCLE_STATUS_KEY = "Status";
5391
+ /**
5392
+ * Gather a rule's full location scope — module-level so the applier and the
5393
+ * fold run the SAME function rather than two copies that can disagree about
5394
+ * which key wins (`.claude/rules/providers.md`: share ONE helper with the
5395
+ * provisioning path, or state and template end up narrowed to different keys
5396
+ * and the fix becomes the bug).
5397
+ *
5398
+ * `TagFilters` lives at the RULE level in the CFn schema and CDK synthesizes it
5399
+ * there; the `Filter`-side reads are cdkd's accommodation for SDK-shaped /
5400
+ * imported input.
5401
+ */
5402
+ function lifecycleRuleScope(rule) {
5403
+ const filter = rule["Filter"];
5404
+ return {
5405
+ prefix: filter?.["Prefix"] ?? rule["Prefix"],
5406
+ tagFilters: filter?.["TagFilters"] ?? rule["TagFilters"],
5407
+ sizeGt: filter?.["ObjectSizeGreaterThan"] ?? rule["ObjectSizeGreaterThan"],
5408
+ sizeLt: filter?.["ObjectSizeLessThan"] ?? rule["ObjectSizeLessThan"]
5409
+ };
5410
+ }
5411
+ /**
5412
+ * Can this rule stay in the deprecated V1 (top-level `Prefix`) form? See
5413
+ * {@link lifecycleUsesFilterForm} — S3 forbids MIXING the two forms in one
5414
+ * configuration, so the answer is decided across the whole rule list.
5415
+ */
5416
+ function lifecycleRuleIsPlainPrefixOnly(rule) {
5417
+ if (rule["Filter"] != null) return false;
5418
+ const s = lifecycleRuleScope(rule);
5419
+ return s.prefix !== void 0 && (s.tagFilters == null || s.tagFilters.length === 0) && s.sizeGt === void 0 && s.sizeLt === void 0;
5420
+ }
5421
+ /** V1 vs V2 is decided ONCE for the whole configuration — see the applier's note. */
5422
+ function lifecycleUsesFilterForm(rules) {
5423
+ return !rules.every(lifecycleRuleIsPlainPrefixOnly);
5424
+ }
5425
+ /** Discards the message; the fold only needs the PREDICATE's answer. */
5426
+ const IGNORE_REFUSAL_MESSAGE = () => {};
5427
+ /**
5428
+ * Would `applyLifecycleConfiguration` REFUSE this rule, and therefore skip the
5429
+ * whole Put? (issue #1755)
5430
+ *
5431
+ * Runs the applier's own three guards rather than restating them — the
5432
+ * "do not HAND-ROLL the mirror, run the wire's own predicate" rule, whose
5433
+ * cautionary tale is the S3 schedule fold, where an approximation disagreed
5434
+ * with the wire on three of nine container shapes.
5435
+ */
5436
+ function lifecycleRuleRefused(rule) {
5437
+ const rawFilter = rule["Filter"];
5438
+ if (rawFilter != null && requireConfigObject(rawFilter, LIFECYCLE_FILTER_PATH, { onUnusable: IGNORE_REFUSAL_MESSAGE }) === void 0) return true;
5439
+ const rawTagFilters = rawFilter?.["TagFilters"] ?? rule["TagFilters"];
5440
+ if (rawTagFilters != null && requireConfigArray(rawTagFilters, LIFECYCLE_TAG_FILTERS_PATH, { onUnusable: IGNORE_REFUSAL_MESSAGE }) === void 0) return true;
5441
+ return configStringRefusal(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH) !== void 0;
5442
+ }
5443
+ /**
5444
+ * Deep, key-ORDER-insensitive equality — the identity test for a fold that
5445
+ * rebuilds nested objects (`Filter`) even when nothing about them changed.
5446
+ *
5447
+ * `Object.is` alone would report every folded rule as changed and rewrite the
5448
+ * state record of every lifecycle-configured bucket; `JSON.stringify` would
5449
+ * report a pure reordering as a change, which is not one worth recording.
5450
+ */
5451
+ function deepSameValue(a, b) {
5452
+ if (Object.is(a, b)) return true;
5453
+ if (Array.isArray(a) || Array.isArray(b)) {
5454
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
5455
+ return a.every((element, index) => deepSameValue(element, b[index]));
5456
+ }
5457
+ if (isPlainObject$1(a) && isPlainObject$1(b)) {
5458
+ const keysA = Object.keys(a);
5459
+ if (keysA.length !== Object.keys(b).length) return false;
5460
+ return keysA.every((key) => key in b && deepSameValue(a[key], b[key]));
5461
+ }
5462
+ return false;
5463
+ }
5464
+ /**
5465
+ * The ISO string `readLifecycle` emits for a date the applier sends as
5466
+ * `new Date(value)`.
5467
+ *
5468
+ * DETERMINISM IS A PRECONDITION, not a nicety (PR review). `new Date(str)`
5469
+ * resolves an offset-less string in the HOST's local zone — `new Date('0')` is
5470
+ * `1999-12-31T15:00:00Z` under JST and `2000-01-01T00:00:00Z` under UTC, and
5471
+ * `'2030-01-01T00:00:00'` (ISO, no offset) is local by spec. The APPLIER shares
5472
+ * that dependence, so the wire and the readback still agree on any one machine —
5473
+ * but this feeds `canonicalizeDesiredProperties`, a DIFF-side pure function that
5474
+ * runs wherever the CLI runs. A CI runner in UTC and a laptop in JST would
5475
+ * canonicalize the SAME template to different ISO strings and manufacture a diff
5476
+ * neither machine can clear, which is strictly worse than the phantom drift the
5477
+ * fold exists to remove. So only the forms whose meaning is fixed by the ISO
5478
+ * 8601 spec are folded: a bare `YYYY-MM-DD` (spec-UTC), a date-time carrying
5479
+ * `Z` or a `±HH:MM` offset, and an epoch NUMBER. Everything else — `'Jan 1
5480
+ * 2030'`, `'0'`, an offset-less date-time — is left ALONE, which keeps the
5481
+ * record machine-independent at the cost of not normalizing a shape almost no
5482
+ * template uses.
5483
+ *
5484
+ * @returns `undefined` for a value that cannot become a valid `Date`, or whose
5485
+ * `Date` would depend on the host timezone — in both cases there is no
5486
+ * effective value this can claim AWS will report, so the fold leaves the rule
5487
+ * alone.
5488
+ */
5489
+ const TIMEZONE_INDEPENDENT_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?$/;
5490
+ function cfnDateToIso(value) {
5491
+ if (typeof value === "string" && !TIMEZONE_INDEPENDENT_DATE.test(value)) return void 0;
5492
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
5493
+ const parsed = new Date(value);
5494
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
5495
+ }
5496
+ /** The legacy SINGULAR action objects the CFn schema still declares (issue #1754). */
5497
+ const LEGACY_SINGULAR_TRANSITIONS = [["Transitions", "Transition"], ["NoncurrentVersionTransitions", "NoncurrentVersionTransition"]];
5498
+ /**
5499
+ * Merge the legacy singular `Transition` / `NoncurrentVersionTransition` into
5500
+ * the plural array, exactly as the wire does (issue #1754).
5501
+ *
5502
+ * The CFn `Rule` definition still declares BOTH spellings and
5503
+ * `applyLifecycleConfiguration` folds the singular into the plural array via
5504
+ * {@link mergeLegacySingular}, so `readLifecycle` can only ever emit the plural.
5505
+ * A rule declaring the singular therefore recorded a key AWS can never
5506
+ * report — the #1686 never-emitted-KEY class through the SHAPE — with no
5507
+ * warning anywhere, because nothing is malformed and nothing is substituted.
5508
+ *
5509
+ * It shares `mergeLegacySingular` itself rather than re-deriving the collision
5510
+ * rule, per the share-ONE-helper rule. What it does NOT do is pass a no-op
5511
+ * callback and fold regardless: on a `StorageClass` COLLISION the wire DROPS the
5512
+ * singular and WARNS, so a colliding rule is left COMPLETELY alone (both keys
5513
+ * kept). The NON-colliding case, which is the population this exists for, has
5514
+ * no fault to report and folds.
5515
+ *
5516
+ * Be precise about WHICH signal that preserves, because the obvious answer is
5517
+ * wrong (PR review). It is NOT the deploy-time warning: the record and the
5518
+ * canonicalized template are folded by the same function either way, so they
5519
+ * compare equal after the first deploy and the Put stops re-firing whichever
5520
+ * choice is made. What leaving it alone preserves is the `cdkd drift` report —
5521
+ * state keeps a `Transition` key `readLifecycle` can never emit, so drift
5522
+ * re-reports the rule until the template drops one of the two declarations.
5523
+ * Folding it would make state match the readback exactly and render a
5524
+ * template fault S3 itself rejects completely invisible.
5525
+ *
5526
+ * @returns `rule` BY IDENTITY when no legacy singular is declared.
5527
+ */
5528
+ function foldLegacySingularTransitions(rule) {
5529
+ let out;
5530
+ for (const [pluralKey, singularKey] of LEGACY_SINGULAR_TRANSITIONS) {
5531
+ const singular = rule[singularKey];
5532
+ const hasSingular = isPlainObject$1(singular);
5533
+ const plural = rule[pluralKey];
5534
+ const needsFilter = Array.isArray(plural) && plural.some((element) => !isPlainObject$1(element));
5535
+ if (!hasSingular && !needsFilter) continue;
5536
+ let collided = false;
5537
+ const merged = mergeLegacySingular(plural, hasSingular ? singular : void 0, () => {
5538
+ collided = true;
5539
+ });
5540
+ out ??= { ...rule };
5541
+ if (collided) {
5542
+ if (needsFilter) out[pluralKey] = merged;
5543
+ continue;
5544
+ }
5545
+ out[pluralKey] = merged;
5546
+ if (hasSingular) delete out[singularKey];
5547
+ }
5548
+ return out ?? rule;
5549
+ }
5550
+ /**
5551
+ * Emit a transition's `TransitionDate` as the ISO string the readback emits
5552
+ * (PR review of issues #1754 / #1755).
5553
+ *
5554
+ * The alias fold above only RENAMES `Date` -> `TransitionDate`, but the applier
5555
+ * sends `new Date(t['TransitionDate'] ?? t['Date'])` and `readLifecycle` emits
5556
+ * `t.Date.toISOString()` — so a hand-written / imported `TransitionDate:
5557
+ * '2030-01-01'` recorded a value the readback can never match, with no warning
5558
+ * and nothing malformed. Exactly the class {@link foldLifecycleExpiration}
5559
+ * closes for the RULE-level `ExpirationDate` one member over; found by review
5560
+ * because the fold and the readback were compared per member rather than
5561
+ * per item.
5562
+ *
5563
+ * `NoncurrentVersionTransitions` needs no twin of this: `toSdkNvt` has no date
5564
+ * member at all.
5565
+ *
5566
+ * @returns the item BY IDENTITY when no date is declared, or when it is one the
5567
+ * applier would put on the wire as an Invalid Date (nothing to record).
5568
+ */
5569
+ function foldTransitionDate(item) {
5570
+ const declared = item[TRANSITION_DATE_KEY];
5571
+ if (!declared) return item;
5572
+ const iso = cfnDateToIso(declared);
5573
+ if (iso === void 0 || iso === declared) return item;
5574
+ const out = { ...item };
5575
+ out[TRANSITION_DATE_KEY] = iso;
5576
+ return out;
5577
+ }
5578
+ /**
5579
+ * Drop a declared-but-EMPTY transition list (issue #1755).
5580
+ *
5581
+ * The applier sets `sdkRule.Transitions` / `.NoncurrentVersionTransitions` only
5582
+ * when the merged array is non-empty, and `readLifecycle` emits each only under
5583
+ * `r.<List> && r.<List>.length > 0` — so an empty declaration is one key the
5584
+ * readback can never carry. Measured on the reverse-mapper, not assumed: the
5585
+ * lifecycle block's OWN empty-collection placeholder answers the opposite way
5586
+ * (issue #1718) because `readLifecycle` DOES emit `{Rules: []}`.
5587
+ */
5588
+ function dropEmptyTransitionLists(rule) {
5589
+ let out;
5590
+ for (const [pluralKey] of LEGACY_SINGULAR_TRANSITIONS) {
5591
+ const list = rule[pluralKey];
5592
+ if (!Array.isArray(list) || list.length > 0) continue;
5593
+ out ??= { ...rule };
5594
+ delete out[pluralKey];
5595
+ }
5596
+ return out ?? rule;
5597
+ }
5598
+ /**
5599
+ * Reshape the rule's expiration onto the CFn spellings `readLifecycle` emits
5600
+ * (issue #1755 item 1).
5601
+ *
5602
+ * The CFn `Rule` definition declares `ExpirationDate` / `ExpirationInDays` and
5603
+ * the RULE-level `ExpiredObjectDeleteMarker`, and has NO `Expiration` member;
5604
+ * the applier additionally accepts a cdkd-only `Expiration` OBJECT as a third
5605
+ * source. The readback used to emit only the SDK-shaped `Expiration` object, so
5606
+ * state and the readback carried different key sets for essentially every
5607
+ * lifecycle-configured bucket CDK creates. The fix is split, per the issue: the
5608
+ * READBACK now emits the CFn scalars (recording the SDK shape instead would put
5609
+ * a spelling in state that no CFn template can declare), and this is the desired
5610
+ * side folding onto the same choice.
5611
+ *
5612
+ * The rule is left COMPLETELY alone in three cases, each because the wire does
5613
+ * something this fold must not conceal:
5614
+ *
5615
+ * - a rule-level `ExpiredObjectDeleteMarker` the wire cannot read
5616
+ * (`coerceCfnBoolean` answers `undefined`): the wire silently ignores it, and
5617
+ * nothing warns, so the difference is the user's only signal;
5618
+ * - a date the wire would send as an Invalid Date, or one whose `Date` depends
5619
+ * on the host timezone (see {@link cfnDateToIso});
5620
+ * - a rule-level `ExpiredObjectDeleteMarker: true` ALONGSIDE a `Days` / `Date`:
5621
+ * S3 forbids the combination, so the applier sends the `Days` / `Date` and
5622
+ * WARNS that the marker was not applied. Folding the marker away on both sides
5623
+ * would stop that difference being reported, so `cdkd drift` re-reporting it
5624
+ * until the template drops one of the two is the correct standing signal.
5625
+ *
5626
+ * @returns `rule` BY IDENTITY when nothing expiration-shaped is declared.
5627
+ */
5628
+ function foldLifecycleExpiration(rule) {
5629
+ const declaredMarker = rule["ExpiredObjectDeleteMarker"];
5630
+ const ruleLevelMarker = coerceCfnBoolean(declaredMarker);
5631
+ if (declaredMarker !== void 0 && ruleLevelMarker === void 0) return rule;
5632
+ if (rule["ExpirationInDays"] === void 0 && rule["ExpirationDate"] === void 0 && rule["Expiration"] === void 0 && declaredMarker === void 0) return rule;
5633
+ const expiration = rule["ExpirationInDays"] || rule["ExpirationDate"] || rule["Expiration"];
5634
+ let days;
5635
+ let date;
5636
+ let marker;
5637
+ if (typeof expiration === "number") days = expiration;
5638
+ else if (typeof expiration === "string") {
5639
+ const iso = cfnDateToIso(expiration);
5640
+ if (iso === void 0) return rule;
5641
+ date = iso;
5642
+ } else if (isPlainObject$1(expiration)) {
5643
+ if (expiration["Days"] !== void 0) days = expiration["Days"];
5644
+ if (expiration["Date"]) {
5645
+ const iso = cfnDateToIso(expiration["Date"]);
5646
+ if (iso === void 0) return rule;
5647
+ date = iso;
5648
+ }
5649
+ const nested = expiration["ExpiredObjectDeleteMarker"];
5650
+ if (nested !== void 0) {
5651
+ if (typeof nested !== "boolean") return rule;
5652
+ marker = nested;
5653
+ }
5654
+ } else if (expiration) return rule;
5655
+ if (ruleLevelMarker === true) {
5656
+ if (days !== void 0 || date !== void 0) return rule;
5657
+ days = void 0;
5658
+ date = void 0;
5659
+ marker = true;
5660
+ }
5661
+ const out = { ...rule };
5662
+ delete out["ExpirationInDays"];
5663
+ delete out["ExpirationDate"];
5664
+ delete out["Expiration"];
5665
+ delete out["ExpiredObjectDeleteMarker"];
5666
+ if (days !== void 0) out["ExpirationInDays"] = days;
5667
+ if (date !== void 0) out["ExpirationDate"] = date;
5668
+ if (marker !== void 0) out["ExpiredObjectDeleteMarker"] = marker;
5669
+ return out;
5670
+ }
5671
+ /**
5672
+ * Fold the legacy scalar `NoncurrentVersionExpirationInDays` onto the OBJECT
5673
+ * form `readLifecycle` emits (issue #1755 item 2).
5674
+ *
5675
+ * CFn declares BOTH spellings here — unlike the expiration sibling above — so
5676
+ * the readback has to pick one and the desired side has to fold to the same
5677
+ * choice. The OBJECT wins because it is the only one that can express
5678
+ * `NewerNoncurrentVersions`, so folding towards it is lossless in both
5679
+ * directions; and it is also what the wire already prefers when both are
5680
+ * declared, which is why a rule carrying both simply DROPS the scalar here.
5681
+ *
5682
+ * `coerceCfnNumber`, not `typeof === 'number'`: it is the predicate the wire
5683
+ * runs, and the wire sends the COERCED number, so a hand-written `"365"` is
5684
+ * recorded as `365` — exactly what AWS reports back.
5685
+ */
5686
+ function foldNoncurrentVersionExpiration(rule) {
5687
+ const legacy = rule["NoncurrentVersionExpirationInDays"];
5688
+ if (legacy === void 0) return rule;
5689
+ const declaredObject = rule["NoncurrentVersionExpiration"];
5690
+ if (declaredObject !== void 0 && !isPlainObject$1(declaredObject)) return rule;
5691
+ const out = { ...rule };
5692
+ delete out["NoncurrentVersionExpirationInDays"];
5693
+ if (isPlainObject$1(declaredObject)) return out;
5694
+ const days = coerceCfnNumber(legacy);
5695
+ if (days === void 0) return rule;
5696
+ out["NoncurrentVersionExpiration"] = { NoncurrentDays: days };
5697
+ return out;
5698
+ }
5699
+ /**
5700
+ * Rebuild the rule's location scope in the shape `readLifecycle` emits for the
5701
+ * `Filter` the applier actually sends (issue #1755 item 3).
5702
+ *
5703
+ * This is not a spelling at all but a defaulted-but-SENT member (#1718's
5704
+ * class): a rule with no declared scope is SENT `Filter: {Prefix: ''}` in a V2
5705
+ * configuration, AWS holds it, the readback reports it, and the record was one
5706
+ * key short. The fold is wider than the issue's headline because the same round
5707
+ * trip moves three more shapes — a bare top-level `Prefix` becomes
5708
+ * `Filter: {Prefix}`, rule-level `TagFilters` become `Filter: {TagFilters}`, and
5709
+ * rule-level `ObjectSize*` move under `Filter` — all of which the readback emits
5710
+ * only under `Filter`.
5711
+ *
5712
+ * `useFilterForm` is decided ACROSS ALL RULES, which is why this takes it as a
5713
+ * parameter instead of being a pure per-rule function: in a V1 configuration
5714
+ * (every rule scoped by exactly one top-level `Prefix`) nothing moves at all.
5715
+ *
5716
+ * WHY THIS RECORDS `Filter` WHEN THE EXPIRATION SIBLING REFUSED THE SDK SHAPE,
5717
+ * since the two answer the same objection opposite ways one member apart (PR
5718
+ * review — without this, the next author "fixes" one to match the other). The
5719
+ * CFn `Rule` definition declares no `Filter` member either, so by the
5720
+ * `Expiration` argument this too should have been fixed on the READBACK side.
5721
+ * It is not, for a reason that does not apply there: the rule-level CFn scalars
5722
+ * can express every `Expiration` the SDK returns, and so the reverse map is
5723
+ * LOSSLESS — whereas the V2 `Filter` carries a distinction with no rule-level
5724
+ * spelling at all. A scope-less rule is SENT `Filter: {Prefix: ''}`, and the
5725
+ * only rule-level rendering of that is a top-level `Prefix: ''`, which is the V1
5726
+ * form — a different configuration that S3 forbids mixing with V2. Reverse-
5727
+ * mapping would therefore destroy the V1/V2 distinction state needs to
5728
+ * reproduce the call. `readLifecycle` has emitted `Filter` since issue #1388, so
5729
+ * both sides already meet there; this fold makes the desired side agree rather
5730
+ * than moving the meeting point.
5731
+ */
5732
+ function foldLifecycleScope(rule, useFilterForm) {
5733
+ const { prefix, tagFilters, sizeGt, sizeLt } = lifecycleRuleScope(rule);
5734
+ const hasTags = tagFilters != null && tagFilters.length > 0;
5735
+ const componentCount = (hasTags ? 1 : 0) + (prefix !== void 0 ? 1 : 0) + (sizeGt !== void 0 ? 1 : 0) + (sizeLt !== void 0 ? 1 : 0);
5736
+ let filter;
5737
+ if (componentCount > 1) {
5738
+ filter = {};
5739
+ if (prefix !== void 0) filter["Prefix"] = prefix;
5740
+ if (hasTags) filter["TagFilters"] = tagFilters;
5741
+ if (sizeGt !== void 0) filter["ObjectSizeGreaterThan"] = sizeGt;
5742
+ if (sizeLt !== void 0) filter["ObjectSizeLessThan"] = sizeLt;
5743
+ } else if (hasTags) filter = { TagFilters: tagFilters };
5744
+ else if (sizeGt !== void 0) filter = { ObjectSizeGreaterThan: sizeGt };
5745
+ else if (sizeLt !== void 0) filter = { ObjectSizeLessThan: sizeLt };
5746
+ else if (useFilterForm) filter = { Prefix: prefix ?? "" };
5747
+ const out = { ...rule };
5748
+ delete out["Filter"];
5749
+ delete out["Prefix"];
5750
+ delete out["TagFilters"];
5751
+ delete out["ObjectSizeGreaterThan"];
5752
+ delete out["ObjectSizeLessThan"];
5753
+ if (filter !== void 0) out["Filter"] = filter;
5754
+ else if (prefix !== void 0) out["Prefix"] = prefix;
5251
5755
  return out;
5252
5756
  }
5253
5757
  /**
@@ -5268,29 +5772,65 @@ function effectiveNotificationConfiguration(config) {
5268
5772
  * the same two item shapes, so fixing only the reported one would have left its
5269
5773
  * siblings broken — the "diff the WHOLE blob, not the reported key" rule.
5270
5774
  *
5271
- * Two things deliberately NOT folded here, tracked rather than left unstated:
5272
- * the legacy SINGULAR `Transition` / `NoncurrentVersionTransition` objects,
5273
- * which `mergeLegacySingular` folds into the plural on the wire so the readback
5274
- * can never emit them either — a MERGE with a collision arm that WARNS, not an
5275
- * alias rename, so it needs its own decision (issue #1754); and the RULE-level
5276
- * divergences the round-trip fence measured beside these
5277
- * (`ExpirationInDays` / `NoncurrentVersionExpirationInDays` recorded against an
5278
- * `Expiration` / `NoncurrentVersionExpiration` readback, and the sent-but-
5279
- * unrecorded `Filter: {Prefix: ''}`), which are reshapes belonging on the
5280
- * READBACK side and are wider than this class (issue #1755). So a rule using
5281
- * these aliases converges on the aliases and still differs on those keys until
5282
- * #1755 lands — which is the honest state, not a claim this makes lifecycle
5283
- * whole.
5284
- *
5285
- * @returns `config` BY IDENTITY when no tolerated spelling is declared.
5775
+ * Issues #1754 / #1755 finished the block, so the "still differs on those keys"
5776
+ * caveat this doc carried is gone: the legacy SINGULAR `Transition` /
5777
+ * `NoncurrentVersionTransition` objects are MERGED here exactly as the wire
5778
+ * merges them ({@link foldLegacySingularTransitions}), the rule-level
5779
+ * `Expiration` / `NoncurrentVersionExpiration` reshapes are folded onto the
5780
+ * spellings the readback emits ({@link foldLifecycleExpiration} /
5781
+ * {@link foldNoncurrentVersionExpiration}), and the sent-but-unrecorded
5782
+ * `Filter` is supplied from the rule's full gathered SCOPE
5783
+ * ({@link foldLifecycleScope}).
5784
+ *
5785
+ * @returns `config` BY IDENTITY when the declared bag IS what goes on the wire.
5286
5786
  */
5287
5787
  function effectiveLifecycleConfiguration(config) {
5288
5788
  if (!isPlainObject$1(config)) return config;
5289
- return canonicalizeItemList(config, "Rules", (rule) => {
5290
- let out = canonicalizeItemList(rule, "Transitions", (t) => foldToleratedAliases(t, TRANSITION_ALIASES));
5291
- out = canonicalizeItemList(out, "NoncurrentVersionTransitions", (nvt) => foldToleratedAliases(nvt, NONCURRENT_VERSION_TRANSITION_ALIASES));
5292
- return out;
5293
- });
5789
+ const rules = config["Rules"];
5790
+ if (!Array.isArray(rules)) return config;
5791
+ const objectRules = rules.filter(isPlainObject$1);
5792
+ if (objectRules.length !== rules.length || objectRules.length === 0) return config;
5793
+ if (objectRules.some(lifecycleRuleRefused)) return config;
5794
+ const useFilterForm = lifecycleUsesFilterForm(objectRules);
5795
+ return canonicalizeItemList(config, "Rules", (rule) => effectiveLifecycleRule(rule, useFilterForm));
5796
+ }
5797
+ /**
5798
+ * The one rule fold both sides share — the CFn-shaped bag `readLifecycle` emits
5799
+ * for the SDK rule `applyLifecycleConfiguration` builds from this rule.
5800
+ *
5801
+ * Ordered the way the wire is: the legacy singular is merged into the plural
5802
+ * BEFORE the alias fold runs, because the singular can carry the SDK day/date
5803
+ * spellings too.
5804
+ *
5805
+ * MEASURED against the REAL synthesized `s3-lifecycle` fixture templates (every
5806
+ * bucket, both phases): the recorded rule and the emitted one carry identical
5807
+ * KEYS and identical VALUES, and differ only in key ORDER — the record keeps the
5808
+ * template's, the readback uses the reverse-mapper's. That is pre-existing and
5809
+ * unchanged here (the record was always template-ordered), and it does not reach
5810
+ * `cdkd drift`, whose baseline is `observedProperties` — captured from this very
5811
+ * reverse-mapper, so both sides carry its order. It is stated because a reader
5812
+ * comparing the two by `JSON.stringify` will see a difference and should know it
5813
+ * was measured rather than missed.
5814
+ *
5815
+ * @returns `rule` BY IDENTITY when the declared rule already IS that bag, so an
5816
+ * ordinary CFn-spelled template records byte-for-byte as before. The compare
5817
+ * is deep and key-ORDER-insensitive for the same reason: a reordering is not a
5818
+ * difference worth rewriting a state record for.
5819
+ */
5820
+ function effectiveLifecycleRule(rule, useFilterForm) {
5821
+ let out = foldLegacySingularTransitions(rule);
5822
+ out = canonicalizeItemList(out, "Transitions", (t) => foldTransitionDate(foldToleratedAliases(t, TRANSITION_ALIASES)));
5823
+ out = canonicalizeItemList(out, "NoncurrentVersionTransitions", (nvt) => foldToleratedAliases(nvt, NONCURRENT_VERSION_TRANSITION_ALIASES));
5824
+ out = dropEmptyTransitionLists(out);
5825
+ out = foldLifecycleExpiration(out);
5826
+ out = foldNoncurrentVersionExpiration(out);
5827
+ out = foldLifecycleScope(out, useFilterForm);
5828
+ if (out[LIFECYCLE_STATUS_KEY] === void 0) {
5829
+ const withStatus = { ...out };
5830
+ withStatus[LIFECYCLE_STATUS_KEY] = LIFECYCLE_DEFAULT_STATUS;
5831
+ out = withStatus;
5832
+ }
5833
+ return deepSameValue(rule, out) ? rule : out;
5294
5834
  }
5295
5835
  /**
5296
5836
  * Apply a WHOLE-BLOCK fold to one top-level property on BOTH sides of a diff —
@@ -5649,32 +6189,18 @@ var S3BucketProvider = class S3BucketProvider {
5649
6189
  async applyLifecycleConfiguration(bucketName, lifecycleConfig, onUnusable) {
5650
6190
  for (const rule of lifecycleConfig.Rules) {
5651
6191
  const rawFilter = rule["Filter"];
5652
- if (rawFilter != null && requireConfigObject(rawFilter, "AWS::S3::Bucket LifecycleConfiguration.Rules[].Filter", { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
6192
+ if (rawFilter != null && requireConfigObject(rawFilter, LIFECYCLE_FILTER_PATH, { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
5653
6193
  const rawTagFilters = rawFilter?.["TagFilters"] ?? rule["TagFilters"];
5654
6194
  if (rawTagFilters != null) {
5655
- if (requireConfigArray(rawTagFilters, "AWS::S3::Bucket LifecycleConfiguration.Rules[].TagFilters", { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
6195
+ if (requireConfigArray(rawTagFilters, LIFECYCLE_TAG_FILTERS_PATH, { ...onUnusable ? { onUnusable } : {} }) === void 0) return false;
5656
6196
  }
5657
- if (this.skipOnUnusableConfigString(rule, "Status", "Enabled", "AWS::S3::Bucket LifecycleConfiguration.Rules[]", "Leaving the whole lifecycle configuration unapplied here", onUnusable)) return false;
6197
+ if (this.skipOnUnusableConfigString(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH, "Leaving the whole lifecycle configuration unapplied here", onUnusable)) return false;
5658
6198
  }
5659
- const gatherScope = (rule) => {
5660
- const filter = rule["Filter"];
5661
- return {
5662
- prefix: filter?.["Prefix"] ?? rule["Prefix"],
5663
- tagFilters: filter?.["TagFilters"] ?? rule["TagFilters"],
5664
- sizeGt: filter?.["ObjectSizeGreaterThan"] ?? rule["ObjectSizeGreaterThan"],
5665
- sizeLt: filter?.["ObjectSizeLessThan"] ?? rule["ObjectSizeLessThan"]
5666
- };
5667
- };
5668
- const isPlainPrefixOnly = (rule) => {
5669
- if (rule["Filter"] != null) return false;
5670
- const s = gatherScope(rule);
5671
- return s.prefix !== void 0 && (s.tagFilters == null || s.tagFilters.length === 0) && s.sizeGt === void 0 && s.sizeLt === void 0;
5672
- };
5673
- const useFilterForm = !lifecycleConfig.Rules.every(isPlainPrefixOnly);
6199
+ const useFilterForm = lifecycleUsesFilterForm(lifecycleConfig.Rules);
5674
6200
  const rules = lifecycleConfig.Rules.map((rule) => {
5675
6201
  const sdkRule = {
5676
6202
  ID: rule["Id"],
5677
- Status: readConfigString(rule, "Status", "Enabled", "AWS::S3::Bucket LifecycleConfiguration.Rules[]"),
6203
+ Status: readConfigString(rule, "Status", LIFECYCLE_DEFAULT_STATUS, LIFECYCLE_RULE_PATH),
5678
6204
  Prefix: useFilterForm ? void 0 : rule["Prefix"]
5679
6205
  };
5680
6206
  const expiration = rule["ExpirationInDays"] || rule["ExpirationDate"] || rule["Expiration"];
@@ -5720,7 +6246,7 @@ var S3BucketProvider = class S3BucketProvider {
5720
6246
  if (allTransitions.length > 0) sdkRule.Transitions = allTransitions.map(toSdkTransition);
5721
6247
  const abort = isPlainObject$1(rule["AbortIncompleteMultipartUpload"]) ? rule["AbortIncompleteMultipartUpload"] : void 0;
5722
6248
  if (abort) sdkRule.AbortIncompleteMultipartUpload = { DaysAfterInitiation: abort["DaysAfterInitiation"] };
5723
- const { prefix, tagFilters, sizeGt, sizeLt } = gatherScope(rule);
6249
+ const { prefix, tagFilters, sizeGt, sizeLt } = lifecycleRuleScope(rule);
5724
6250
  const hasTags = tagFilters != null && tagFilters.length > 0;
5725
6251
  if ((hasTags ? 1 : 0) + (prefix !== void 0 ? 1 : 0) + (sizeGt !== void 0 ? 1 : 0) + (sizeLt !== void 0 ? 1 : 0) > 1) sdkRule.Filter = { And: {
5726
6252
  Prefix: prefix,
@@ -5906,7 +6432,7 @@ var S3BucketProvider = class S3BucketProvider {
5906
6432
  * SDK uses the same structure (PutBucketNotificationConfiguration replaces
5907
6433
  * the entire notification configuration in one call).
5908
6434
  */
5909
- async applyNotificationConfiguration(bucketName, notifConfig) {
6435
+ async applyNotificationConfiguration(bucketName, notifConfig, onUnusable) {
5910
6436
  const cfg = {};
5911
6437
  if (notifConfig) {
5912
6438
  const topics = notifConfig["TopicConfigurations"];
@@ -5931,13 +6457,22 @@ var S3BucketProvider = class S3BucketProvider {
5931
6457
  Filter: this.cfnNotifFilterToSdk(l["Filter"])
5932
6458
  }));
5933
6459
  const eb = notifConfig["EventBridgeConfiguration"];
5934
- if (eb !== void 0 && (!isPlainObject$1(eb) || coerceCfnBoolean(eb["EventBridgeEnabled"]) !== false)) cfg.EventBridgeConfiguration = {};
6460
+ if (eb != null) {
6461
+ const ebRefusal = configBooleanRefusal(eb, "EventBridgeEnabled", EVENTBRIDGE_CONFIG_PATH);
6462
+ if (ebRefusal !== void 0) {
6463
+ if (!onUnusable) throw new Error(`${ebRefusal}. Omit the block entirely to leave EventBridge delivery disabled`);
6464
+ onUnusable(`${ebRefusal}. Leaving the whole notification configuration unapplied here: PutBucketNotificationConfiguration is a full replace, and treating an unreadable value as ${EVENTBRIDGE_DEFAULT_ENABLED} would ENABLE EventBridge delivery the template may have been disabling; the same value is REFUSED on a template-path create`);
6465
+ return false;
6466
+ }
6467
+ if (eventBridgeEnabledOf(eb)) cfg.EventBridgeConfiguration = {};
6468
+ }
5935
6469
  }
5936
6470
  await this.s3Client.send(new PutBucketNotificationConfigurationCommand({
5937
6471
  Bucket: bucketName,
5938
6472
  NotificationConfiguration: cfg
5939
6473
  }));
5940
6474
  this.logger.debug(`Applied notification configuration to bucket ${bucketName}`);
6475
+ return true;
5941
6476
  }
5942
6477
  /**
5943
6478
  * Convert CFn notification Filter ({ S3Key: { Rules: [{ Name, Value }] } })
@@ -6951,9 +7486,11 @@ var S3BucketProvider = class S3BucketProvider {
6951
7486
  await this.applyLoggingConfiguration(bucketName, void 0);
6952
7487
  });
6953
7488
  await this.diffSubConfig(bucketName, previousProperties["NotificationConfiguration"], properties["NotificationConfiguration"], async (cfg) => {
6954
- await this.applyNotificationConfiguration(bucketName, cfg);
6955
- recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(cfg));
6956
- }, async () => this.applyNotificationConfiguration(bucketName, void 0));
7489
+ if (!await this.applyNotificationConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("NotificationConfiguration");
7490
+ else recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(cfg));
7491
+ }, async () => {
7492
+ await this.applyNotificationConfiguration(bucketName, void 0);
7493
+ });
6957
7494
  await this.diffSubConfig(bucketName, previousProperties["ReplicationConfiguration"], properties["ReplicationConfiguration"], async (cfg) => {
6958
7495
  if (!await this.applyReplicationConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("ReplicationConfiguration");
6959
7496
  }, async () => {
@@ -7067,10 +7604,8 @@ var S3BucketProvider = class S3BucketProvider {
7067
7604
  };
7068
7605
  const replayOnUnusable = replayWarn(this.logger, context).onUnusable;
7069
7606
  const notifConfig = properties["NotificationConfiguration"];
7070
- if (notifConfig) {
7071
- await this.applyNotificationConfiguration(bucketName, notifConfig);
7072
- recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(notifConfig));
7073
- }
7607
+ if (notifConfig) if (!await this.applyNotificationConfiguration(bucketName, notifConfig, replayOnUnusable)) overrides.set("NotificationConfiguration", void 0);
7608
+ else recordEffectiveFold("NotificationConfiguration", effectiveNotificationConfiguration(notifConfig));
7074
7609
  const corsConfig = properties["CorsConfiguration"];
7075
7610
  if (corsConfig?.CorsRules && Array.isArray(corsConfig.CorsRules) && corsConfig.CorsRules.length > 0) await this.applyCorsConfiguration(bucketName, corsConfig);
7076
7611
  else if (corsConfig != null) this.announceCreateEmptyCollectionSkip("CorsConfiguration", "CorsConfiguration.CorsRules");
@@ -7421,11 +7956,9 @@ var S3BucketProvider = class S3BucketProvider {
7421
7956
  if (r.Status !== void 0) out["Status"] = r.Status;
7422
7957
  if (r.Prefix !== void 0) out["Prefix"] = r.Prefix;
7423
7958
  if (r.Expiration) {
7424
- const exp = {};
7425
- if (r.Expiration.Days !== void 0) exp["Days"] = r.Expiration.Days;
7426
- if (r.Expiration.Date !== void 0) exp["Date"] = r.Expiration.Date.toISOString();
7427
- if (r.Expiration.ExpiredObjectDeleteMarker !== void 0) exp["ExpiredObjectDeleteMarker"] = r.Expiration.ExpiredObjectDeleteMarker;
7428
- out["Expiration"] = exp;
7959
+ if (r.Expiration.Days !== void 0) out["ExpirationInDays"] = r.Expiration.Days;
7960
+ if (r.Expiration.Date !== void 0) out["ExpirationDate"] = r.Expiration.Date.toISOString();
7961
+ if (r.Expiration.ExpiredObjectDeleteMarker !== void 0) out["ExpiredObjectDeleteMarker"] = r.Expiration.ExpiredObjectDeleteMarker;
7429
7962
  }
7430
7963
  if (r.Transitions && r.Transitions.length > 0) out["Transitions"] = r.Transitions.map((t) => {
7431
7964
  const item = {};
@@ -62978,6 +63511,50 @@ function escapeRegExp(value) {
62978
63511
  */
62979
63512
  const KEY_TERMINATORS = "[^\\s\"'?]";
62980
63513
  /**
63514
+ * `(?:amazonaws\.com\.cn|amazonaws\.com|...)` — {@link AWS_URL_SUFFIXES} as a
63515
+ * non-capturing alternation, LONGEST FIRST so `amazonaws.com.cn` is tried
63516
+ * before its own `amazonaws.com` prefix instead of relying on backtracking.
63517
+ *
63518
+ * The `(?:` is load-bearing, not stylistic: every matcher below reads its
63519
+ * results by GROUP INDEX (`match[1]` is the S3 key / the ECR tag, `match[2]`
63520
+ * the ECR digest), and this alternation sits BEFORE those groups in all three
63521
+ * patterns. Making it capturing would shift every index by one and silently
63522
+ * collect the suffix as if it were a key.
63523
+ */
63524
+ const URL_SUFFIX_ALTERNATION = `(?:${[...[
63525
+ "amazonaws.com",
63526
+ "amazonaws.com.cn",
63527
+ "amazonaws.eu",
63528
+ "c2s.ic.gov",
63529
+ "sc2s.sgov.gov",
63530
+ "cloud.adc-e.uk",
63531
+ "csp.hci.ic.gov"
63532
+ ]].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|")})`;
63533
+ /**
63534
+ * The HOST half of an ECR registry, as an alternation over the forms AWS
63535
+ * serves: `<acct>.dkr.ecr.<region>.<urlSuffix>`, its FIPS sibling
63536
+ * `<acct>.dkr.ecr-fips.<region>.<urlSuffix>`, and the short-form alias
63537
+ * `<acct>.dkr-ecr.<region>.on.aws`.
63538
+ *
63539
+ * Kept DELIBERATELY separate from the S3 matchers' shared
63540
+ * {@link URL_SUFFIX_ALTERNATION} rather than folded into it: `on.aws` is an
63541
+ * ECR-only endpoint, so adding it to the shared suffix set would also make
63542
+ * `https://<assetBucket>.s3.<region>.on.aws/<key>` a recognized S3 reference.
63543
+ * Widening either matcher only ever over-PROTECTS, but the two services' host
63544
+ * grammars genuinely differ and conflating them is how the next widening goes
63545
+ * wrong.
63546
+ *
63547
+ * Account and region are matched loosely on purpose (see the extractor doc):
63548
+ * collecting a reference from another account's or region's URI can only KEEP
63549
+ * more, never delete more — which is also why the two extra forms are worth
63550
+ * carrying even though cdkd's own publisher writes the plain one. Issue #1785
63551
+ * tracks deriving these host forms from ONE place shared with
63552
+ * `src/utils/ecr-uri.ts` instead of two hand-kept copies.
63553
+ *
63554
+ * Every group is non-capturing, for the {@link URL_SUFFIX_ALTERNATION} reason.
63555
+ */
63556
+ const ECR_REGISTRY_HOST = `\\d{12}\\.(?:dkr\\.ecr(?:-fips)?\\.[a-z0-9-]+\\.${URL_SUFFIX_ALTERNATION}|dkr-ecr\\.[a-z0-9-]+\\.on\\.aws)`;
63557
+ /**
62981
63558
  * cdkd's publishers write content-addressed keys (`<sha256>.<ext>`). A
62982
63559
  * second, name-independent pass collects every such token from every
62983
63560
  * scanned string — belt-and-braces against URL captures that ran through
@@ -63015,21 +63592,26 @@ function tryDecodeBase64Text(value) {
63015
63592
  * ApiGateway BodyS3Location / SFN DefinitionS3Location, ...) — handled
63016
63593
  * in the walk itself; every sibling string value is collected.
63017
63594
  * - `s3://<assetBucket>/<key>` URIs.
63018
- * - `https://<assetBucket>.s3[.<region>].amazonaws.com/<key>`
63595
+ * - `https://<assetBucket>.s3[.<region>].<urlSuffix>/<key>`
63019
63596
  * (virtual-hosted style, region / dualstack variants included).
63020
- * - `https://s3[.<region>].amazonaws.com/<assetBucket>/<key>` (path style).
63021
- * - `<acct>.dkr.ecr.<region>.amazonaws.com/<containerRepo>:<tag>` and/or
63022
- * `...@sha256:<digest>` image URIs. Account / region are matched
63023
- * loosely on purpose: collecting a reference from another account's or
63024
- * region's URI can only over-protect (keep more), never delete more.
63597
+ * - `https://s3[.<region>].<urlSuffix>/<assetBucket>/<key>` (path style).
63598
+ * - `<ecrHost>/<containerRepo>:<tag>` and/or `...@sha256:<digest>` image
63599
+ * URIs, where `<ecrHost>` is {@link ECR_REGISTRY_HOST}. Account / region
63600
+ * are matched loosely on purpose: collecting a reference from another
63601
+ * account's or region's URI can only over-protect (keep more), never
63602
+ * delete more.
63603
+ *
63604
+ * `<urlSuffix>` in the two S3 shapes is {@link URL_SUFFIX_ALTERNATION}, i.e.
63605
+ * EVERY partition's suffix at once — see that constant for why a per-region
63606
+ * derived literal is the wrong shape here (issue #1781).
63025
63607
  */
63026
63608
  function buildReferenceExtractors(marker) {
63027
63609
  const bucket = escapeRegExp(marker.assetBucket);
63028
63610
  const repo = escapeRegExp(marker.containerRepo);
63029
63611
  const s3UriRe = new RegExp(`s3://${bucket}/(${KEY_TERMINATORS}+)`, "g");
63030
- const virtualHostedRe = new RegExp(`https://${bucket}\\.s3[^/\\s]*\\.amazonaws\\.com/(${KEY_TERMINATORS}+)`, "g");
63031
- const pathStyleRe = new RegExp(`https://s3[^/\\s]*\\.amazonaws\\.com/${bucket}/(${KEY_TERMINATORS}+)`, "g");
63032
- const ecrRe = new RegExp(`\\d{12}\\.dkr\\.ecr\\.[a-z0-9-]+\\.amazonaws\\.com/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "g");
63612
+ const virtualHostedRe = new RegExp(`https://${bucket}\\.s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/(${KEY_TERMINATORS}+)`, "g");
63613
+ const pathStyleRe = new RegExp(`https://s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/${bucket}/(${KEY_TERMINATORS}+)`, "g");
63614
+ const ecrRe = new RegExp(`${ECR_REGISTRY_HOST}/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "g");
63033
63615
  const extractOnce = (value, refs) => {
63034
63616
  for (const re of [
63035
63617
  s3UriRe,
@@ -74854,7 +75436,7 @@ function createMigrateCommand() {
74854
75436
  */
74855
75437
  function buildProgram() {
74856
75438
  const program = new Command();
74857
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.24");
75439
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.26");
74858
75440
  program.hook("preAction", (_thisCommand, actionCommand) => {
74859
75441
  const { profile } = actionCommand.optsWithGlobals();
74860
75442
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;