@go-to-k/cdkd 0.223.0 → 0.223.2

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
@@ -1553,7 +1553,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1553
1553
  const FLUSH_EVENT_THRESHOLD = 50;
1554
1554
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1555
1555
  function getCdkdVersion() {
1556
- return "0.223.0";
1556
+ return "0.223.2";
1557
1557
  }
1558
1558
  /**
1559
1559
  * Generate a time-sortable unique run id, e.g.
@@ -5850,6 +5850,76 @@ var S3BucketProvider = class {
5850
5850
  //#endregion
5851
5851
  //#region src/provisioning/providers/s3-bucket-policy-provider.ts
5852
5852
  /**
5853
+ * Matches a CloudFront Origin Access Identity (OAI) principal ARN, e.g.
5854
+ * `arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1UREC9EUJDVG5`.
5855
+ * The capture group is the OAI id, which resolves to the OAI's S3 canonical
5856
+ * user id via `GetCloudFrontOriginAccessIdentity`.
5857
+ */
5858
+ const OAI_USER_ARN_RE = /^arn:aws[a-z-]*:iam::cloudfront:user\/CloudFront Origin Access Identity ([A-Z0-9]+)$/;
5859
+ /**
5860
+ * Matches a bare IAM principal unique id (e.g. `AIDAIBJOSOJSBZ753XCAW`). S3
5861
+ * returns an OAI grant's principal in this transient form immediately after
5862
+ * `PutBucketPolicy` (before it settles to the friendly cloudfront-user ARN).
5863
+ * It carries no recoverable link back to the OAI, so it can only be
5864
+ * canonicalized by matching against the template's `{ CanonicalUser }` form.
5865
+ */
5866
+ const IAM_UNIQUE_ID_RE = /^A[A-Z0-9]{15,}$/;
5867
+ /**
5868
+ * Process-lifetime cache of OAI id -> S3 canonical user id (or `null` when the
5869
+ * lookup failed / the OAI is gone), so a `cdkd drift` run with many OAI-granting
5870
+ * bucket policies issues at most one `GetCloudFrontOriginAccessIdentity` per OAI.
5871
+ */
5872
+ const oaiCanonicalUserIdCache = /* @__PURE__ */ new Map();
5873
+ /**
5874
+ * Build an `oaiId -> S3CanonicalUserId` map from the same-stack sibling
5875
+ * `AWS::CloudFront::CloudFrontOriginAccessIdentity` resources in the read
5876
+ * context. The OAI's `S3CanonicalUserId` is a readOnly attribute cdkd already
5877
+ * resolved at deploy time (the bucket-policy grant `Fn::GetAtt`s it), so this
5878
+ * lets the bucket-policy drift read reconcile the OAI principal forms with zero
5879
+ * extra AWS calls (and no `cloudfront:GetCloudFrontOriginAccessIdentity` IAM
5880
+ * grant) whenever the OAI lives in the same stack. The OAI resource's
5881
+ * `physicalId` IS the OAI id embedded in the cloudfront-user ARN.
5882
+ */
5883
+ function buildSiblingOaiCanonicalMap(context) {
5884
+ const map = /* @__PURE__ */ new Map();
5885
+ for (const sib of Object.values(context?.siblings ?? {})) {
5886
+ if (sib.resourceType !== "AWS::CloudFront::CloudFrontOriginAccessIdentity") continue;
5887
+ const canonical = sib.attributes?.["S3CanonicalUserId"];
5888
+ if (sib.physicalId && typeof canonical === "string") map.set(sib.physicalId, canonical);
5889
+ }
5890
+ return map;
5891
+ }
5892
+ /**
5893
+ * Pull the `Statement` array out of a parsed policy document, tolerating both
5894
+ * the single-object and array shapes (and a non-object input). Returns the live
5895
+ * statement objects so callers can mutate their `Principal` in place.
5896
+ */
5897
+ function extractStatements(policyDoc) {
5898
+ if (!policyDoc || typeof policyDoc !== "object") return [];
5899
+ const stmt = policyDoc["Statement"];
5900
+ return (Array.isArray(stmt) ? stmt : stmt ? [stmt] : []).filter((s) => !!s && typeof s === "object" && !Array.isArray(s));
5901
+ }
5902
+ /**
5903
+ * Find the `CanonicalUser` principal of the template statement that matches a
5904
+ * given AWS-side statement by Effect / Action / Resource, used to canonicalize
5905
+ * the unresolvable transient `{ AWS: <IAM-unique-id> }` OAI form. Returns
5906
+ * `undefined` when no single matching template statement carries one.
5907
+ */
5908
+ function findTemplateCanonicalUser(awsStmt, templateStmts) {
5909
+ const key = (s) => JSON.stringify([
5910
+ s["Effect"],
5911
+ s["Action"],
5912
+ s["Resource"]
5913
+ ]);
5914
+ const want = key(awsStmt);
5915
+ const matches = templateStmts.filter((t) => key(t) === want);
5916
+ if (matches.length !== 1) return void 0;
5917
+ const principal = matches[0]["Principal"];
5918
+ if (!principal || typeof principal !== "object" || Array.isArray(principal)) return void 0;
5919
+ const canonical = principal["CanonicalUser"];
5920
+ return typeof canonical === "string" ? canonical : void 0;
5921
+ }
5922
+ /**
5853
5923
  * AWS S3 Bucket Policy Provider
5854
5924
  *
5855
5925
  * Implements resource provisioning for AWS::S3::BucketPolicy using the S3 SDK.
@@ -5953,7 +6023,7 @@ var S3BucketPolicyProvider = class {
5953
6023
  * Returns `undefined` when the bucket is gone (`NoSuchBucket`) or when
5954
6024
  * no policy is currently attached (`NoSuchBucketPolicy`).
5955
6025
  */
5956
- async readCurrentState(physicalId, _logicalId, _resourceType) {
6026
+ async readCurrentState(physicalId, _logicalId, _resourceType, properties, context) {
5957
6027
  let policyJson;
5958
6028
  try {
5959
6029
  policyJson = (await this.s3Client.send(new GetBucketPolicyCommand({ Bucket: physicalId }))).Policy;
@@ -5964,14 +6034,90 @@ var S3BucketPolicyProvider = class {
5964
6034
  }
5965
6035
  if (!policyJson) return void 0;
5966
6036
  const result = { Bucket: physicalId };
6037
+ let policyDoc;
5967
6038
  try {
5968
- result["PolicyDocument"] = JSON.parse(policyJson);
6039
+ policyDoc = JSON.parse(policyJson);
5969
6040
  } catch {
5970
6041
  result["PolicyDocument"] = policyJson;
6042
+ return result;
5971
6043
  }
6044
+ await this.normalizeOaiPrincipals(policyDoc, properties, context);
6045
+ result["PolicyDocument"] = policyDoc;
5972
6046
  return result;
5973
6047
  }
5974
6048
  /**
6049
+ * Normalize CloudFront OAI grant principals in a (parsed) bucket policy to
6050
+ * the `{ CanonicalUser: <id> }` form, in place (issue #872):
6051
+ * - `{ AWS: <oai-user-arn> }` -> map the OAI id in the ARN to its
6052
+ * `S3CanonicalUserId`, preferring the sibling OAI resource's already-read
6053
+ * state attributes (zero AWS call — the OAI's `S3CanonicalUserId` is a
6054
+ * readOnly attribute cdkd already resolved at deploy time) and falling
6055
+ * back to `GetCloudFrontOriginAccessIdentity` only when the OAI is not a
6056
+ * same-stack sibling (e.g. an imported / external OAI). This is the SAFE
6057
+ * path: a genuinely-different OAI resolves to a different canonical id, so
6058
+ * real drift is still detected.
6059
+ * - `{ AWS: <bare-IAM-unique-id> }` -> the transient post-PutBucketPolicy
6060
+ * rendering, which carries no recoverable link to the OAI. Canonicalize
6061
+ * it only by matching the corresponding template statement (same Effect /
6062
+ * Action / Resource) carrying a `{ CanonicalUser }` principal. A user
6063
+ * cannot author a bare IAM unique id as a bucket-policy principal, so
6064
+ * adopting the template form here only fires for AWS's transient
6065
+ * rendering. NOTE: once the principal settles to the ARN form (seconds
6066
+ * to minutes), a genuine out-of-band OAI repoint IS detected via the ARN
6067
+ * path above; the only masking window is a repoint observed during the
6068
+ * transient bare-id phase, which the next drift run (ARN form) reports.
6069
+ * Statements whose principal is not a single-string `AWS` OAI form (e.g. a
6070
+ * wildcard, a service principal, a normal role ARN, or an `AWS` ARRAY of
6071
+ * principals) are left untouched — skipping is safe (it can only leave a
6072
+ * residual false positive, never hide real drift). CDK's single-OAI grant
6073
+ * emits the single-string form.
6074
+ */
6075
+ async normalizeOaiPrincipals(policyDoc, templateProps, context) {
6076
+ const stmts = extractStatements(policyDoc);
6077
+ if (stmts.length === 0) return;
6078
+ const templateStmts = extractStatements(templateProps?.["PolicyDocument"]);
6079
+ const siblingCanonicalById = buildSiblingOaiCanonicalMap(context);
6080
+ for (const stmt of stmts) {
6081
+ const principal = stmt["Principal"];
6082
+ if (!principal || typeof principal !== "object" || Array.isArray(principal)) continue;
6083
+ const awsPrincipal = principal["AWS"];
6084
+ if (typeof awsPrincipal !== "string") continue;
6085
+ const arnMatch = OAI_USER_ARN_RE.exec(awsPrincipal);
6086
+ if (arnMatch) {
6087
+ const oaiId = arnMatch[1];
6088
+ const canonical = siblingCanonicalById.get(oaiId) ?? await this.resolveOaiCanonicalUserId(oaiId);
6089
+ if (canonical) stmt["Principal"] = { CanonicalUser: canonical };
6090
+ continue;
6091
+ }
6092
+ if (IAM_UNIQUE_ID_RE.test(awsPrincipal)) {
6093
+ const tmplCanonical = findTemplateCanonicalUser(stmt, templateStmts);
6094
+ if (tmplCanonical) stmt["Principal"] = { CanonicalUser: tmplCanonical };
6095
+ }
6096
+ }
6097
+ }
6098
+ /**
6099
+ * Resolve a CloudFront OAI id to its S3 canonical user id via
6100
+ * `GetCloudFrontOriginAccessIdentity`, cached for the process lifetime. Used
6101
+ * only as a FALLBACK when the OAI is not a same-stack sibling (its
6102
+ * `S3CanonicalUserId` is otherwise read straight from sibling state — see
6103
+ * {@link buildSiblingOaiCanonicalMap}). Best-effort: returns `null` (and
6104
+ * caches it) on any failure so a missing OAI / missing permission leaves the
6105
+ * principal unchanged rather than failing the drift read.
6106
+ */
6107
+ async resolveOaiCanonicalUserId(oaiId) {
6108
+ const cached = oaiCanonicalUserIdCache.get(oaiId);
6109
+ if (cached !== void 0) return cached;
6110
+ let canonical = null;
6111
+ try {
6112
+ canonical = (await getAwsClients().cloudFront.send(new GetCloudFrontOriginAccessIdentityCommand({ Id: oaiId }))).CloudFrontOriginAccessIdentity?.S3CanonicalUserId ?? null;
6113
+ } catch (err) {
6114
+ this.logger.debug(`Could not resolve CloudFront OAI ${oaiId} canonical user id for bucket-policy drift normalization: ${err instanceof Error ? err.message : String(err)} — leaving the principal unchanged.`);
6115
+ canonical = null;
6116
+ }
6117
+ oaiCanonicalUserIdCache.set(oaiId, canonical);
6118
+ return canonical;
6119
+ }
6120
+ /**
5975
6121
  * Adopt an existing S3 bucket policy into cdkd state.
5976
6122
  *
5977
6123
  * The operational identifier for an `S3::BucketPolicy` is the **bucket
@@ -18214,15 +18360,25 @@ var CloudFrontOAIProvider = class {
18214
18360
  //#endregion
18215
18361
  //#region src/provisioning/providers/cloudfront-distribution-provider.ts
18216
18362
  /**
18217
- * Fields in the DistributionConfig that follow the { Quantity, Items } pattern.
18218
- * The CDK template may provide just Items (an array); we wrap it with Quantity.
18363
+ * Top-level `DistributionConfig` fields that are a BARE ARRAY in the CFn
18364
+ * template but a `{ Quantity, Items }` wrapper in the SDK shape — so
18365
+ * `convertToSdkFormat` wraps them and `convertToCfnFormat` unwraps them.
18366
+ *
18367
+ * `OriginGroups` is deliberately NOT here (issue #873): unlike `Origins` /
18368
+ * `CacheBehaviors` / `Aliases` / `CustomErrorResponses` (bare lists in CFn), the
18369
+ * CFn `DistributionConfig.OriginGroups` property is ITSELF a `{ Quantity, Items }`
18370
+ * object, and so are its nested `Members` and `FailoverCriteria.StatusCodes`.
18371
+ * CDK synthesizes that full `{ Quantity, Items }` shape and `GetDistributionConfig`
18372
+ * returns the same, so OriginGroups must pass through UNTOUCHED in both
18373
+ * directions — unwrapping it (it was incorrectly listed here before) turned the
18374
+ * read-back OriginGroups into a bare array that no longer matched the template's
18375
+ * `{ Quantity, Items }`, firing phantom drift.
18219
18376
  */
18220
18377
  const QUANTITY_ITEM_FIELDS = [
18221
18378
  "Origins",
18222
18379
  "CacheBehaviors",
18223
18380
  "CustomErrorResponses",
18224
- "Aliases",
18225
- "OriginGroups"
18381
+ "Aliases"
18226
18382
  ];
18227
18383
  /**
18228
18384
  * Nested fields inside each CacheBehavior / DefaultCacheBehavior that use
@@ -18443,27 +18599,18 @@ var CloudFrontDistributionProvider = class {
18443
18599
  * logging bucket to its `<bucket>.s3.amazonaws.com` regional domain on
18444
18600
  * read, which never matches the bare bucket domain a template may
18445
18601
  * carry; treat it as drift-unknown to avoid a guaranteed mismatch.
18446
- * - `DistributionConfig.OriginGroups`: each origin group carries its own
18447
- * inner `{ Quantity, Items }` wrappers (`Members`,
18448
- * `FailoverCriteria.StatusCodes`) that `convertToCfnFormat` does not yet
18449
- * unwrap symmetrically (only the top-level OriginGroups list is unwrapped;
18450
- * there is no `revertOriginGroup` pass, and `convertToSdkFormat` likewise
18451
- * does not descend into them). Against the observed baseline both sides go
18452
- * through the same `readCurrentState` so this never false-positives on a
18453
- * normal deploy, but against the `properties`-fallback baseline (older
18454
- * state without `observedProperties`) the raw `{ Quantity, Items }` shape
18455
- * would diff the bare-array template form. Suppress it until a dedicated
18456
- * fix lands a `revertOriginGroup` + a real OriginGroups integ fixture
18457
- * (there is no OriginGroups fixture to verify the inner shape today). See
18458
- * the PR #871 review thread.
18602
+ *
18603
+ * NOTE: `DistributionConfig.OriginGroups` is NO LONGER suppressed (issue
18604
+ * #873). The earlier suppression worked around `convertToCfnFormat`
18605
+ * incorrectly unwrapping the top-level OriginGroups `{ Quantity, Items }`
18606
+ * wrapper; the real fix (dropping OriginGroups from `QUANTITY_ITEM_FIELDS` so
18607
+ * it passes through untouched in both directions) makes OriginGroups compare
18608
+ * equal, so it is now drift-checked normally and a real OriginGroups change
18609
+ * IS reported.
18459
18610
  */
18460
18611
  getDriftUnknownPaths(resourceType) {
18461
18612
  if (resourceType !== "AWS::CloudFront::Distribution") return [];
18462
- return [
18463
- "DistributionConfig.CallerReference",
18464
- "DistributionConfig.Logging.Bucket",
18465
- "DistributionConfig.OriginGroups"
18466
- ];
18613
+ return ["DistributionConfig.CallerReference", "DistributionConfig.Logging.Bucket"];
18467
18614
  }
18468
18615
  /**
18469
18616
  * Get resource attribute (for Fn::GetAtt resolution)
@@ -38414,7 +38561,9 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
38414
38561
  if (lid === excludedLogicalId) continue;
38415
38562
  siblings[lid] = {
38416
38563
  resourceType: res.resourceType,
38417
- properties: res.properties ?? {}
38564
+ physicalId: res.physicalId,
38565
+ properties: res.properties ?? {},
38566
+ attributes: res.attributes ?? {}
38418
38567
  };
38419
38568
  }
38420
38569
  return { siblings };
@@ -54379,7 +54528,7 @@ function reorderArgs(argv) {
54379
54528
  async function main() {
54380
54529
  installPipeCloseHandler();
54381
54530
  const program = new Command();
54382
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.223.0");
54531
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.223.2");
54383
54532
  program.addCommand(createBootstrapCommand());
54384
54533
  program.addCommand(createSynthCommand());
54385
54534
  program.addCommand(createListCommand());