@go-to-k/cdkd 0.280.33 → 0.280.35

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 requireConfigObject, $t as AssemblyReader, A as green, At as runDockerForeground, B as disableInstanceApiTermination, Bt as resolveCaptureObservedState, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackTerminationProtectionError, Ct as getBootstrapMarkerKey, D as bold, Dt as buildDockerImage, E as formatResourceLine, Et as validateContainerRepoName, F as clearOnUpdateRemoval, Ft as synthesisStatusMessage, G as WAFv2WebACLProvider, Gt as stateBucketExistenceConfirmed, H as IntrinsicFunctionResolver, Ht as resolveStateBucketWithDefault, I as ProviderRegistry, It as getDefaultStateBucketName, J as assertRegionMatch, Jt as CFN_TEMPLATE_URL_LIMIT, K as normalizeAwsTagsToCfn, Kt as warnDeprecatedNoPrefixCliFlag, L as findActionableSilentDrops, Lt as getLegacyStateBucketName, M as yellow, Mt as AssetManifestLoader, N as IAMRoleProvider, Nt as getDockerImageBySourceHash, O as cyan, On as normalizeAwsError, Ot as formatDockerLoginError, P as collectInlinePolicyNamesManagedBySiblings, Pt as Synthesizer, Q as requireConfigArray, Qt as expectedOwnerParam, R as CloudControlProvider, Rt as resolveApp, S as extractDeploymentEventError, Sn as StackHasActiveImportsError, St as ensureAssetStorage, T as renderStatefulReason, Tt as validateAssetBucketName, U as cfnRefValueFromPhysicalId, Ut as resolveStateBucketWithDefaultAndSource, V as isTerminationProtectionPropagationError, Vt as resolveSkipPrefix, W as refStateLookupFromResource, Wt as resolveUseCdkBootstrapAssets, X as readConfigString, Xt as findLargeInlineResources, Y as configStringRefusal, Yt as MIGRATE_TMP_PREFIX, Z as replayWarn, Zt as uploadCfnTemplate, _ as createPreDeleteFinalSnapshot, _n as NestedStackChildDirectDestroyError, _t as createAssetRedirectResolver, a as DeploymentEventsStore, an as resetAwsClients, at as isRetryableTransientError, b as unsupportedFinalSnapshotError, bn as ResourceTimeoutError, bt as AssetModeResolver, c as replayFailedOperations, cn as CdkdError, ct as TemplateParser, d as IMPLICIT_DELETE_DEPENDENCIES, dn as DeployCancelledError, dt as rebuildClientForBucketRegion, en as processStackMessages, et as requireConfigString, f as computeImplicitDeleteEdges, fn as LocalInvokeBuildError$1, ft as shouldRetainResource, g as ccRoutedFinalSnapshotError, gn as MissingCdkCliError, gt as buildAssetRedirectMap, h as buildFinalSnapshotIdentifier, ht as WorkGraph, i as DeploymentEventsReader, in as getAwsClients, it as withRetry, j as red, jt as runDockerStreaming, k as gray, kn as withErrorHandling, kt as getDockerCmd, l as replayRollback, lt as LockManager, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalStartServiceError, mt as stringifyValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveBucketRegion, nt as DiffCalculator, o as planFailedOps, on as setAwsClients, ot as isThrottlingError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalMigrateError, pt as AssetPublisher, q as resolveExplicitPhysicalId, qt as CFN_TEMPLATE_BODY_LIMIT, r as DeployEngine, rn as AwsClients, rt as describeTypeWithThrottleRetry, s as planRollback, st as DagBuilder, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as applyRoleArnIfSet, u as withResourceDeadline, ut as S3StateBackend, v as isFinalSnapshotError, vn as PartialFailureError, vt as loadPublishableAssetManifest, w as isStatefulRecreateTargetSync, wt as parseBootstrapMarker, x as makeCanonicalizePropertiesFn, xn as ResourceUpdateNotSupportedError, xt as BOOTSTRAP_MARKER_PREFIX, y as refusesFinalSnapshot, yn as ProvisioningError, yt as rewriteTemplateAssetReferences, z as slowCcOperationTimeoutMs, zt as resolveAutoAssetStorage } from "./deploy-engine-Dn60Zkq5.js";
2
+ import { $ as requireConfigObject, $t as AssemblyReader, A as green, At as runDockerForeground, B as disableInstanceApiTermination, Bt as resolveCaptureObservedState, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackTerminationProtectionError, Ct as getBootstrapMarkerKey, D as bold, Dt as buildDockerImage, E as formatResourceLine, Et as validateContainerRepoName, F as clearOnUpdateRemoval, Ft as synthesisStatusMessage, G as WAFv2WebACLProvider, Gt as stateBucketExistenceConfirmed, H as IntrinsicFunctionResolver, Ht as resolveStateBucketWithDefault, I as ProviderRegistry, It as getDefaultStateBucketName, J as assertRegionMatch, Jt as CFN_TEMPLATE_URL_LIMIT, K as normalizeAwsTagsToCfn, Kt as warnDeprecatedNoPrefixCliFlag, L as findActionableSilentDrops, Lt as getLegacyStateBucketName, M as yellow, Mt as AssetManifestLoader, N as IAMRoleProvider, Nt as getDockerImageBySourceHash, O as cyan, On as normalizeAwsError, Ot as formatDockerLoginError, P as collectInlinePolicyNamesManagedBySiblings, Pt as Synthesizer, Q as requireConfigArray, Qt as expectedOwnerParam, R as CloudControlProvider, Rt as resolveApp, S as extractDeploymentEventError, Sn as StackHasActiveImportsError, St as ensureAssetStorage, T as renderStatefulReason, Tt as validateAssetBucketName, U as cfnRefValueFromPhysicalId, Ut as resolveStateBucketWithDefaultAndSource, V as isTerminationProtectionPropagationError, Vt as resolveSkipPrefix, W as refStateLookupFromResource, Wt as resolveUseCdkBootstrapAssets, X as readConfigString, Xt as findLargeInlineResources, Y as configStringRefusal, Yt as MIGRATE_TMP_PREFIX, Z as replayWarn, Zt as uploadCfnTemplate, _ as createPreDeleteFinalSnapshot, _n as NestedStackChildDirectDestroyError, _t as createAssetRedirectResolver, a as DeploymentEventsStore, an as resetAwsClients, at as isRetryableTransientError, b as unsupportedFinalSnapshotError, bn as ResourceTimeoutError, bt as AssetModeResolver, c as replayFailedOperations, cn as CdkdError, ct as TemplateParser, d as IMPLICIT_DELETE_DEPENDENCIES, dn as DeployCancelledError, dt as rebuildClientForBucketRegion, en as processStackMessages, et as requireConfigString, f as computeImplicitDeleteEdges, fn as LocalInvokeBuildError$1, ft as shouldRetainResource, g as ccRoutedFinalSnapshotError, gn as MissingCdkCliError, gt as buildAssetRedirectMap, h as buildFinalSnapshotIdentifier, ht as WorkGraph, i as DeploymentEventsReader, in as getAwsClients, it as withRetry, j as red, jt as runDockerStreaming, k as gray, kn as withErrorHandling, kt as getDockerCmd, l as replayRollback, lt as LockManager, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalStartServiceError, mt as stringifyValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveBucketRegion, nt as DiffCalculator, o as planFailedOps, on as setAwsClients, ot as isThrottlingError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalMigrateError, pt as AssetPublisher, q as resolveExplicitPhysicalId, qt as CFN_TEMPLATE_BODY_LIMIT, r as DeployEngine, rn as AwsClients, rt as describeTypeWithThrottleRetry, s as planRollback, st as DagBuilder, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as applyRoleArnIfSet, u as withResourceDeadline, ut as S3StateBackend, v as isFinalSnapshotError, vn as PartialFailureError, vt as loadPublishableAssetManifest, w as isStatefulRecreateTargetSync, wt as parseBootstrapMarker, x as makeCanonicalizePropertiesFn, xn as ResourceUpdateNotSupportedError, xt as BOOTSTRAP_MARKER_PREFIX, y as refusesFinalSnapshot, yn as ProvisioningError, yt as rewriteTemplateAssetReferences, z as slowCcOperationTimeoutMs, zt as resolveAutoAssetStorage } from "./deploy-engine-DjdMx6fC.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-DrM2HEGB.js";
4
+ import { t as ASGProvider } from "./asg-provider-CL2zlhEi.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";
@@ -18008,6 +18008,95 @@ var EventBridgeBusProvider = class {
18008
18008
  }
18009
18009
  };
18010
18010
 
18011
+ //#endregion
18012
+ //#region src/utils/ip-protocol.ts
18013
+ /**
18014
+ * EC2 `IpProtocol` value canonicalization (issue
18015
+ * [#1643](https://github.com/go-to-k/cdkd/issues/1643)).
18016
+ *
18017
+ * EC2 owns the spelling of a security-group rule's protocol: it accepts an
18018
+ * IANA protocol NUMBER or a protocol NAME, rewrites the four numbers it has a
18019
+ * name for before storing the rule, and lower-cases a name it is given. So the
18020
+ * value cdkd SENDS and the value AWS REPORTS are routinely two spellings of
18021
+ * ONE protocol.
18022
+ *
18023
+ * This lives in `src/utils/` rather than beside either consumer because it has
18024
+ * TWO, in different layers: `src/analyzer/drift-protocol-normalize.ts` (the
18025
+ * drift comparator) and `src/provisioning/providers/ec2-provider.ts` (rule
18026
+ * IDENTITY — the standalone-rule lookup, the inline-rule reconcile, and the
18027
+ * write-side revoke/authorize diff). Hosting it in the analyzer made the
18028
+ * provider carry the tree's only `src/provisioning/**` -> `src/analyzer/**`
18029
+ * import, which is both a layering break and a real hazard: an edit made for
18030
+ * the drift comparator would silently change which AWS rule a revoke matches.
18031
+ *
18032
+ * ## The rename set is MEASURED, not guessed
18033
+ *
18034
+ * Probed against real AWS (us-east-1, 2026-08-12) by authorizing one rule per
18035
+ * protocol on a scratch security group and reading `IpProtocol` back, for
18036
+ * ingress AND egress:
18037
+ *
18038
+ * - RENAMED: `1` -> `icmp`, `6` -> `tcp`, `17` -> `udp`, `58` -> `icmpv6`.
18039
+ * - NOT renamed: `-1`, `0`, `2`, `4`, `27`, `41`, `47`, `50`, `51`, `88`,
18040
+ * `89`, `94`, `103`, `112`, `132`, `255` — all read back as the number.
18041
+ *
18042
+ * So the rename set is exactly the four protocols EC2 has a NAME for — a
18043
+ * CLOSED set, not the open-ended IANA list the issue worried about. `-1` is
18044
+ * unaffected, which is why #1633's live test came back clean.
18045
+ *
18046
+ * Case is also AWS's to decide: `TCP` / `Tcp` / `tcp` authorized on the same
18047
+ * port collapsed into ONE `tcp` permission in the readback, so a name is
18048
+ * matched case-insensitively and canonicalized to lower case. Only the four
18049
+ * KNOWN names are lowered — an unrecognized string is left exactly as-is
18050
+ * rather than folded, so this can never make two genuinely different values
18051
+ * compare equal.
18052
+ *
18053
+ * Known bound: only the CANONICAL decimal spelling is recognized. A
18054
+ * zero-padded `'06'` or a `'0x6'` misses the table and is returned verbatim,
18055
+ * so such a template still phantom-drifts. Neither form has been observed in
18056
+ * a CDK-synthesized template, and accepting them would mean parsing rather
18057
+ * than table lookup — which is what keeps the "closed, measured set" property
18058
+ * that makes this safe.
18059
+ */
18060
+ /**
18061
+ * The protocol numbers AWS rewrites to a name, measured (see the module
18062
+ * docstring). Keyed by the number's STRING form, since that is the shape every
18063
+ * input is normalized to first.
18064
+ */
18065
+ const PROTOCOL_NUMBER_TO_NAME = /* @__PURE__ */ new Map([
18066
+ ["1", "icmp"],
18067
+ ["6", "tcp"],
18068
+ ["17", "udp"],
18069
+ ["58", "icmpv6"]
18070
+ ]);
18071
+ /** The four names EC2 canonicalizes to, for the case-insensitive match. */
18072
+ const CANONICAL_PROTOCOL_NAMES = new Set(PROTOCOL_NUMBER_TO_NAME.values());
18073
+ /**
18074
+ * Canonicalize ONE `IpProtocol` value the way EC2 does.
18075
+ *
18076
+ * Returns the input unchanged for any shape that is not a protocol value — an
18077
+ * unresolved intrinsic object (`{Ref: ...}`), `null`, a boolean.
18078
+ *
18079
+ * EVERY number is stringified, not only an integer one: an unquoted YAML
18080
+ * `IpProtocol: 47` parses to a number while AWS always reads back a string, and
18081
+ * `ec2-provider.ts`'s rule-identity key has stringified every number since
18082
+ * #1633 specifically so a legacy record holding the NUMBER `-1` still matches
18083
+ * AWS's `'-1'`. Narrowing that to integers would have silently regressed a
18084
+ * malformed `6.5` from `'6.5'` to the number `6.5`, and collapsed `NaN` /
18085
+ * `Infinity` onto one JSON `null` bucket — the exact merging that key exists
18086
+ * to avoid. The rename lookup is a table hit, so a non-integer simply misses it.
18087
+ */
18088
+ function canonicalizeIpProtocolValue(value) {
18089
+ let text;
18090
+ if (typeof value === "string") text = value;
18091
+ else if (typeof value === "number") text = String(value);
18092
+ else return value;
18093
+ const renamed = PROTOCOL_NUMBER_TO_NAME.get(text);
18094
+ if (renamed !== void 0) return renamed;
18095
+ const lowered = text.toLowerCase();
18096
+ if (CANONICAL_PROTOCOL_NAMES.has(lowered)) return lowered;
18097
+ return text;
18098
+ }
18099
+
18011
18100
  //#endregion
18012
18101
  //#region src/provisioning/providers/ec2-provider.ts
18013
18102
  /**
@@ -18113,6 +18202,49 @@ function narrowIngressIpProtocol(properties, onUnusable) {
18113
18202
  };
18114
18203
  }
18115
18204
  /**
18205
+ * Fold a rule bag's `IpProtocol` to the spelling AWS will hold (issue #1648).
18206
+ *
18207
+ * DIFF-side only — see the call site in `canonicalizeDesiredProperties` for why
18208
+ * this is not done inside `narrowIngressIpProtocol`, which also feeds the wire.
18209
+ * Returns the original object when nothing changes, so an ordinary rule
18210
+ * compares byte-for-byte as before.
18211
+ */
18212
+ function canonicalizeSgRuleProtocol(rule) {
18213
+ if (!("IpProtocol" in rule)) return rule;
18214
+ const next = canonicalizeIpProtocolValue(rule["IpProtocol"]);
18215
+ if (next === rule["IpProtocol"]) return rule;
18216
+ return {
18217
+ ...rule,
18218
+ IpProtocol: next
18219
+ };
18220
+ }
18221
+ /**
18222
+ * The `AWS::EC2::SecurityGroup` inline-rule twin of
18223
+ * {@link canonicalizeSgRuleProtocol}: fold every element of the two rule lists.
18224
+ * Identity-returns when nothing changes, and leaves a non-array list or a
18225
+ * non-object element alone so a malformed template still reaches AWS's own
18226
+ * validation rather than being reshaped here.
18227
+ */
18228
+ function canonicalizeSgInlineRuleProtocols(properties) {
18229
+ let out = properties;
18230
+ for (const key of ["SecurityGroupIngress", "SecurityGroupEgress"]) {
18231
+ const list = out[key];
18232
+ if (!Array.isArray(list)) continue;
18233
+ let changed = false;
18234
+ const mapped = list.map((el) => {
18235
+ if (el === null || typeof el !== "object" || Array.isArray(el)) return el;
18236
+ const next = canonicalizeSgRuleProtocol(el);
18237
+ if (next !== el) changed = true;
18238
+ return next;
18239
+ });
18240
+ if (changed) out = {
18241
+ ...out,
18242
+ [key]: mapped
18243
+ };
18244
+ }
18245
+ return out;
18246
+ }
18247
+ /**
18116
18248
  * AWS EC2 Networking Provider
18117
18249
  *
18118
18250
  * Implements resource provisioning for EC2 networking resources:
@@ -19906,7 +20038,7 @@ var EC2Provider = class {
19906
20038
  const prefixKey = direction === "egress" ? rule["DestinationPrefixListId"] : rule["SourcePrefixListId"];
19907
20039
  const peerOwner = direction === "ingress" ? rule["SourceSecurityGroupOwnerId"] : void 0;
19908
20040
  return JSON.stringify({
19909
- p: rule["IpProtocol"] ?? "-1",
20041
+ p: sgProtocolKey(rule["IpProtocol"]),
19910
20042
  f: rule["FromPort"] ?? null,
19911
20043
  t: rule["ToPort"] ?? null,
19912
20044
  c4: rule["CidrIp"] ?? null,
@@ -20293,7 +20425,11 @@ var EC2Provider = class {
20293
20425
  * `AllocationId`, `ConnectivityType`, `PrivateIpAddress`.
20294
20426
  * - **AWS::EC2::RouteTable**: `DescribeRouteTables` for `VpcId`.
20295
20427
  * - **AWS::EC2::SecurityGroup**: `DescribeSecurityGroups` for
20296
- * `GroupName`, `GroupDescription`, `VpcId`, plus `SecurityGroupIngress`
20428
+ * `GroupName`, `GroupDescription`, `VpcId`, `Tags` (issue #1649 — the
20429
+ * same call returns them in the CFn `[{Key,Value}]` shape, so they are
20430
+ * read rather than declared unreadable the way every sibling standalone
20431
+ * type declares them; reserved `aws:`-prefixed entries are stripped and
20432
+ * the key is omitted entirely when AWS reports none), plus `SecurityGroupIngress`
20297
20433
  * and `SecurityGroupEgress` reverse-mapped from AWS's normalized
20298
20434
  * `IpPermissions[]` / `IpPermissionsEgress[]` form. Each AWS
20299
20435
  * `IpPermission` is flattened into one CFn rule per `IpRanges` /
@@ -20445,8 +20581,9 @@ var EC2Provider = class {
20445
20581
  canonicalizeDesiredProperties(resourceType, properties) {
20446
20582
  if (resourceType === "AWS::EC2::SecurityGroupIngress") {
20447
20583
  const { narrowed } = narrowIngressIpProtocol(properties, () => {});
20448
- return narrowed;
20584
+ return canonicalizeSgRuleProtocol(narrowed);
20449
20585
  }
20586
+ if (resourceType === "AWS::EC2::SecurityGroup") return canonicalizeSgInlineRuleProtocols(properties);
20450
20587
  if (resourceType !== "AWS::EC2::Route") return properties;
20451
20588
  const { declared, narrowed } = narrowRouteDestinations(properties);
20452
20589
  return declared.length > 1 ? narrowed : properties;
@@ -20516,6 +20653,8 @@ var EC2Provider = class {
20516
20653
  if (sg.GroupName !== void 0) result["GroupName"] = sg.GroupName;
20517
20654
  if (sg.Description !== void 0) result["GroupDescription"] = sg.Description;
20518
20655
  if (sg.VpcId !== void 0) result["VpcId"] = sg.VpcId;
20656
+ const tags = normalizeAwsTagsToCfn(sg.Tags);
20657
+ if (tags.length > 0) result["Tags"] = tags;
20519
20658
  const stateIngress = Array.isArray(properties?.["SecurityGroupIngress"]) ? properties["SecurityGroupIngress"] : void 0;
20520
20659
  result["SecurityGroupIngress"] = reconcileSgRules(flattenIpPermissions(sg.IpPermissions ?? [], "ingress"), stateIngress, "ingress");
20521
20660
  const stateEgress = Array.isArray(properties?.["SecurityGroupEgress"]) ? properties["SecurityGroupEgress"] : void 0;
@@ -20725,7 +20864,8 @@ var EC2Provider = class {
20725
20864
  const toPort = parts[3] === "-1" ? void 0 : parseInt(parts[3], 10);
20726
20865
  const sg = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [groupId] }))).SecurityGroups?.[0];
20727
20866
  if (!sg) return void 0;
20728
- const candidates = (sg.IpPermissions ?? []).filter((p) => (p.IpProtocol ?? "-1") === protocol && (p.FromPort ?? void 0) === fromPort && (p.ToPort ?? void 0) === toPort);
20867
+ const wantedProtocol = sgProtocolKey(protocol);
20868
+ const candidates = (sg.IpPermissions ?? []).filter((p) => sgProtocolKey(p.IpProtocol) === wantedProtocol && (p.FromPort ?? void 0) === fromPort && (p.ToPort ?? void 0) === toPort);
20729
20869
  if (candidates.length === 0) return void 0;
20730
20870
  const flat = flattenIpPermissions(candidates, "ingress");
20731
20871
  if (properties && Object.keys(properties).length > 0) {
@@ -20946,8 +21086,7 @@ function flattenIpPermissions(perms, direction) {
20946
21086
  * non-match honest instead of manufacturing a fake identity.
20947
21087
  */
20948
21088
  function sgProtocolKey(value) {
20949
- if (typeof value === "number") return String(value);
20950
- return value ?? "-1";
21089
+ return canonicalizeIpProtocolValue(value ?? "-1");
20951
21090
  }
20952
21091
  function sgRuleKey(rule, direction) {
20953
21092
  const peerKey = direction === "egress" ? rule["DestinationSecurityGroupId"] : rule["SourceSecurityGroupId"];
@@ -52818,6 +52957,145 @@ async function canonicalizePrincipalUniqueIds(baseline, aws, resolve) {
52818
52957
  };
52819
52958
  }
52820
52959
 
52960
+ //#endregion
52961
+ //#region src/analyzer/drift-protocol-normalize.ts
52962
+ /**
52963
+ * EC2 security-group `IpProtocol` VALUE canonicalization for the drift
52964
+ * comparator (issue
52965
+ * [#1643](https://github.com/go-to-k/cdkd/issues/1643)) — a sibling of
52966
+ * `drift-normalize.ts` for a difference that is not about ORDER, in the same
52967
+ * spirit as `drift-principal-normalize.ts`.
52968
+ *
52969
+ * EC2 owns the spelling of `IpProtocol`: it accepts an IANA protocol NUMBER or
52970
+ * a protocol NAME, and rewrites a subset of the numbers to their canonical
52971
+ * name before storing the rule. cdkd records what it SENT (the template's
52972
+ * value, post-#1633), AWS holds the canonical name, and
52973
+ * `readSecurityGroupIngressCurrentState` reads the name back — so the two
52974
+ * sides are two spellings of ONE protocol and every `cdkd drift` run reports
52975
+ * the difference forever. `drift --revert` cannot clear it either: it revokes
52976
+ * and re-authorizes into the same state, which is the permanent phantom drift
52977
+ * the #1591 / #1633 fixes address for their own arms.
52978
+ *
52979
+ * Unlike the send-side narrowing #1633 does, this is a READBACK-normalization
52980
+ * concern, so it lives here and runs on BOTH comparison sides. That placement
52981
+ * is load-bearing for two reasons: the baseline may be the user's TEMPLATE
52982
+ * (a resource deployed before observed-capture), where the value is whatever
52983
+ * they wrote; and it covers the inline
52984
+ * `AWS::EC2::SecurityGroup.SecurityGroupIngress[]` / `SecurityGroupEgress[]`
52985
+ * rules, which #1633 deliberately does not touch.
52986
+ *
52987
+ * ## The rename set is MEASURED, not guessed
52988
+ *
52989
+ * Probed against real AWS (us-east-1, 2026-08-12) by authorizing one rule per
52990
+ * protocol on a scratch security group and reading `IpProtocol` back, for
52991
+ * ingress AND egress:
52992
+ *
52993
+ * - RENAMED: `1` -> `icmp`, `6` -> `tcp`, `17` -> `udp`, `58` -> `icmpv6`.
52994
+ * - NOT renamed: `-1`, `0`, `2`, `4`, `27`, `41`, `47`, `50`, `51`, `88`,
52995
+ * `89`, `94`, `103`, `112`, `132`, `255` — all read back as the number.
52996
+ *
52997
+ * So the rename set is exactly the four protocols EC2 has a NAME for, and it
52998
+ * is a CLOSED set rather than the open-ended IANA list the issue worried
52999
+ * about. `-1` is unaffected, which is why #1633's live test came back clean.
53000
+ *
53001
+ * The measured rename table, the case-folding rule and their bounds live with
53002
+ * the shared value canonicalizer in `src/utils/ip-protocol.ts` — this module
53003
+ * owns only WHERE in a resource's properties that canonicalization applies.
53004
+ *
53005
+ * **This pass matters for the `properties`-fallback baseline, not for a fresh
53006
+ * deploy.** A resource deployed by a current binary captures
53007
+ * `observedProperties` from the same `readCurrentState` the drift run uses, so
53008
+ * both sides already hold AWS's spelling and this is a no-op. The population it
53009
+ * is FOR is the one whose baseline is the user's TEMPLATE: state written before
53010
+ * observed-capture existed, or a provider that captures none. That is also why
53011
+ * the integ fixture has to strip `observedProperties` to exercise it — the
53012
+ * first version of this change asserted a fresh deploy would fail without the
53013
+ * module, and three independent reviews showed it would not.
53014
+ */
53015
+ /**
53016
+ * The resource types carrying an `IpProtocol` this pass rewrites, mapped to
53017
+ * the property path(s) it lives at. The standalone rule types carry it at the
53018
+ * top level; `AWS::EC2::SecurityGroup` carries it inside each element of its
53019
+ * two inline rule arrays.
53020
+ *
53021
+ * Deliberately a CLOSED table rather than a walk for any key named
53022
+ * `IpProtocol`: a blanket rewrite would turn an unrelated property whose value
53023
+ * happens to be `'6'` into `'tcp'`.
53024
+ */
53025
+ const IP_PROTOCOL_PATHS = /* @__PURE__ */ new Map([
53026
+ ["AWS::EC2::SecurityGroupIngress", ["IpProtocol"]],
53027
+ ["AWS::EC2::SecurityGroupEgress", ["IpProtocol"]],
53028
+ ["AWS::EC2::SecurityGroup", ["SecurityGroupIngress[].IpProtocol", "SecurityGroupEgress[].IpProtocol"]]
53029
+ ]);
53030
+ /**
53031
+ * Rewrite the `IpProtocol` at `path` inside `properties`, returning a copy
53032
+ * when anything changed and the original object otherwise.
53033
+ *
53034
+ * `path` is either a plain key (`'IpProtocol'`) or an array-crossing form
53035
+ * (`'SecurityGroupIngress[].IpProtocol'`) whose left half names an array
53036
+ * whose every element carries the key.
53037
+ */
53038
+ function rewriteAtPath(properties, path) {
53039
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) return properties;
53040
+ const bag = properties;
53041
+ const arrayMarker = path.indexOf("[].");
53042
+ if (arrayMarker === -1) {
53043
+ if (!(path in bag)) return properties;
53044
+ const next = canonicalizeIpProtocolValue(bag[path]);
53045
+ if (next === bag[path]) return properties;
53046
+ return {
53047
+ ...bag,
53048
+ [path]: next
53049
+ };
53050
+ }
53051
+ const arrayKey = path.slice(0, arrayMarker);
53052
+ const elementKey = path.slice(arrayMarker + 3);
53053
+ const list = bag[arrayKey];
53054
+ if (!Array.isArray(list)) return properties;
53055
+ let changed = false;
53056
+ const mapped = list.map((element) => {
53057
+ if (element === null || typeof element !== "object" || Array.isArray(element)) return element;
53058
+ const rule = element;
53059
+ if (!(elementKey in rule)) return element;
53060
+ const next = canonicalizeIpProtocolValue(rule[elementKey]);
53061
+ if (next === rule[elementKey]) return element;
53062
+ changed = true;
53063
+ return {
53064
+ ...rule,
53065
+ [elementKey]: next
53066
+ };
53067
+ });
53068
+ if (!changed) return properties;
53069
+ return {
53070
+ ...bag,
53071
+ [arrayKey]: mapped
53072
+ };
53073
+ }
53074
+ /**
53075
+ * Canonicalize every `IpProtocol` on BOTH drift-comparison sides for the
53076
+ * security-group resource types that carry one.
53077
+ *
53078
+ * A resource type with no `IpProtocol` is returned untouched, so the caller
53079
+ * can invoke this unconditionally.
53080
+ */
53081
+ function canonicalizeIpProtocols(baseline, aws, resourceType) {
53082
+ const paths = IP_PROTOCOL_PATHS.get(resourceType);
53083
+ if (paths === void 0) return {
53084
+ baseline,
53085
+ aws
53086
+ };
53087
+ let nextBaseline = baseline;
53088
+ let nextAws = aws;
53089
+ for (const path of paths) {
53090
+ nextBaseline = rewriteAtPath(nextBaseline, path);
53091
+ nextAws = rewriteAtPath(nextAws, path);
53092
+ }
53093
+ return {
53094
+ baseline: nextBaseline,
53095
+ aws: nextAws
53096
+ };
53097
+ }
53098
+
52821
53099
  //#endregion
52822
53100
  //#region src/analyzer/drift-cc-api-deny-list.ts
52823
53101
  /**
@@ -53234,7 +53512,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
53234
53512
  const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
53235
53513
  const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
53236
53514
  const normalized = await canonicalizePrincipalUniqueIds(baseline, aws, resolvePrincipalUniqueId);
53237
- const changes = calculateResourceDrift(normalized.baseline, normalized.aws, {
53515
+ const protocolNormalized = canonicalizeIpProtocols(normalized.baseline, normalized.aws, resource.resourceType);
53516
+ const changes = calculateResourceDrift(protocolNormalized.baseline, protocolNormalized.aws, {
53238
53517
  ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
53239
53518
  unionWalkObjects: useObserved,
53240
53519
  unorderedPaths
@@ -70984,7 +71263,7 @@ function createMigrateCommand() {
70984
71263
  */
70985
71264
  function buildProgram() {
70986
71265
  const program = new Command();
70987
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.33");
71266
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.35");
70988
71267
  program.hook("preAction", (_thisCommand, actionCommand) => {
70989
71268
  const { profile } = actionCommand.optsWithGlobals();
70990
71269
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;