@fjall/components-infrastructure 37.0.0 → 38.0.0

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.
Files changed (37) hide show
  1. package/dist/lib/app.d.ts +28 -0
  2. package/dist/lib/app.js +34 -2
  3. package/dist/lib/config/aws/disasterRecovery.d.ts +1 -2
  4. package/dist/lib/config/aws/disasterRecovery.js +52 -32
  5. package/dist/lib/patterns/aws/cdn.d.ts +19 -11
  6. package/dist/lib/patterns/aws/cdn.js +53 -25
  7. package/dist/lib/patterns/aws/domain.js +3 -3
  8. package/dist/lib/patterns/aws/externalRecordsPattern.d.ts +9 -6
  9. package/dist/lib/patterns/aws/externalRecordsPattern.js +22 -46
  10. package/dist/lib/patterns/aws/interfaces/domain.d.ts +8 -5
  11. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +9 -10
  12. package/dist/lib/patterns/aws/payload.d.ts +5 -0
  13. package/dist/lib/patterns/aws/payload.js +23 -11
  14. package/dist/lib/patterns/aws/targets/targetResolution.d.ts +4 -4
  15. package/dist/lib/patterns/aws/targets/targetResolution.js +4 -4
  16. package/dist/lib/resources/aws/backup/backupVault.d.ts +17 -2
  17. package/dist/lib/resources/aws/backup/backupVault.js +37 -6
  18. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +42 -18
  19. package/dist/lib/resources/aws/cdn/cloudFront.js +39 -51
  20. package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.d.ts +90 -0
  21. package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.js +213 -0
  22. package/dist/lib/resources/aws/cdn/index.d.ts +1 -0
  23. package/dist/lib/resources/aws/cdn/index.js +1 -0
  24. package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +15 -8
  25. package/dist/lib/resources/aws/logging/accessLogBucket.js +16 -9
  26. package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +14 -0
  27. package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +90 -0
  28. package/dist/lib/resources/aws/messaging/eventTargets.d.ts +4 -3
  29. package/dist/lib/resources/aws/messaging/eventTargets.js +71 -31
  30. package/dist/lib/resources/aws/messaging/queuePolicyQuota.d.ts +25 -0
  31. package/dist/lib/resources/aws/messaging/queuePolicyQuota.js +190 -0
  32. package/dist/lib/resources/aws/messaging/schedule.d.ts +5 -5
  33. package/dist/lib/resources/aws/messaging/sqs.js +4 -0
  34. package/dist/lib/resources/aws/messaging/subscription.d.ts +4 -3
  35. package/dist/lib/utils/cdkContext.d.ts +14 -0
  36. package/dist/lib/utils/cdkContext.js +38 -1
  37. package/package.json +3 -3
@@ -18,6 +18,7 @@ import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
18
18
  import { type Duration } from "aws-cdk-lib";
19
19
  import { type Construct } from "constructs";
20
20
  import type { HstsConfig } from "../../../resources/aws/cdn/responseHeadersPolicy.js";
21
+ import type { OpenNextBucketKey } from "@fjall/util/patterns";
21
22
  import { type RelationalDatabase, type DynamoDBDatabase, type ProxyConfig, type ReadReplicaConfig, type CredentialsConfig, type EncryptionConfig, type AuroraEncryptionConfig, type AuroraWriterConfig, type AuroraReadersConfig, type DatabaseInsightsConfig } from "../database.js";
22
23
  import { type LambdaCompute } from "../compute.js";
23
24
  import { type Storage } from "../storage.js";
@@ -210,22 +211,20 @@ export interface PatternStorageBucketConfig {
210
211
  versioned?: boolean;
211
212
  }
212
213
  /**
213
- * Full storage configuration for patterns.
214
- * Allows per-bucket configuration for each S3 bucket in the pattern.
214
+ * Full storage configuration for patterns: one optional entry per bucket
215
+ * the OpenNext construct builds — `assets` (static files), `cache` (the ISR
216
+ * cache) and `media` (uploads) — keyed by `OPENNEXT_BUCKET_KEYS` in
217
+ * `@fjall/util/patterns`, the vocabulary the generator's config schema is
218
+ * pinned to as well.
215
219
  *
216
220
  * @example
217
221
  * storage: {
218
222
  * media: { versioned: true } // force on below the resilient tier
219
223
  * }
220
224
  */
221
- export interface PayloadStorageConfig {
222
- /** Assets bucket (static files) */
223
- assets?: PatternStorageBucketConfig;
224
- /** Cache bucket (ISR cache) */
225
- cache?: PatternStorageBucketConfig;
226
- /** Media bucket (uploads) */
227
- media?: PatternStorageBucketConfig;
228
- }
225
+ export type PayloadStorageConfig = {
226
+ [Bucket in OpenNextBucketKey]?: PatternStorageBucketConfig;
227
+ };
229
228
  /**
230
229
  * SQS queue configuration for pattern messaging.
231
230
  */
@@ -58,6 +58,11 @@ export declare class Payload extends Construct implements IPayload {
58
58
  private buildBaseDatabaseProps;
59
59
  private createAuroraDatabase;
60
60
  private createInstanceDatabase;
61
+ /**
62
+ * Pinned to `OPENNEXT_BUCKET_KEYS` in both directions: a key the vocabulary
63
+ * gains fails here until a builder exists, and a builder for a key it does
64
+ * not carry is an excess property. Built in the vocabulary's order.
65
+ */
61
66
  private createStorageBuckets;
62
67
  private createAssetsBucket;
63
68
  private createCacheBucket;
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { Construct } from "constructs";
25
25
  import { CfnOutput, Stack } from "aws-cdk-lib";
26
+ import { OPENNEXT_BUCKET_KEYS, patternBucketConstructId } from "@fjall/util/patterns";
26
27
  import { Secret } from "../../resources/aws/secrets/index.js";
27
28
  import { Code, Runtime, Architecture, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
28
29
  import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
@@ -209,10 +210,25 @@ export class Payload extends Construct {
209
210
  };
210
211
  return this.app.addDatabase(DatabaseFactory.build(this.props.name, instanceProps));
211
212
  }
213
+ /**
214
+ * Pinned to `OPENNEXT_BUCKET_KEYS` in both directions: a key the vocabulary
215
+ * gains fails here until a builder exists, and a builder for a key it does
216
+ * not carry is an excess property. Built in the vocabulary's order.
217
+ */
212
218
  createStorageBuckets() {
213
- this._assetsBucket = this.createAssetsBucket();
214
- this._cacheBucket = this.createCacheBucket();
215
- this._mediaBucket = this.createMediaBucket();
219
+ const builders = {
220
+ assets: () => {
221
+ this._assetsBucket = this.createAssetsBucket();
222
+ },
223
+ cache: () => {
224
+ this._cacheBucket = this.createCacheBucket();
225
+ },
226
+ media: () => {
227
+ this._mediaBucket = this.createMediaBucket();
228
+ }
229
+ };
230
+ for (const bucket of OPENNEXT_BUCKET_KEYS)
231
+ builders[bucket]();
216
232
  }
217
233
  createAssetsBucket() {
218
234
  const assetsConfig = this.props.storage?.assets ?? {};
@@ -230,7 +246,7 @@ export class Payload extends Construct {
230
246
  }
231
247
  }
232
248
  };
233
- return this.app.addStorage(StorageFactory.build(`${this.props.name}-assets`, assetsProps));
249
+ return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "assets"), assetsProps));
234
250
  }
235
251
  createCacheBucket() {
236
252
  const cacheConfig = this.props.storage?.cache ?? {};
@@ -244,7 +260,7 @@ export class Payload extends Construct {
244
260
  prune: false // ISR adds files at runtime
245
261
  }
246
262
  };
247
- return this.app.addStorage(StorageFactory.build(`${this.props.name}-cache`, cacheProps));
263
+ return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "cache"), cacheProps));
248
264
  }
249
265
  createMediaBucket() {
250
266
  const mediaConfig = this.props.storage?.media ?? {};
@@ -254,7 +270,7 @@ export class Payload extends Construct {
254
270
  versioned: mediaConfig.versioned
255
271
  })
256
272
  };
257
- return this.app.addStorage(StorageFactory.build(`${this.props.name}-media`, mediaProps));
273
+ return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "media"), mediaProps));
258
274
  }
259
275
  createTagCache() {
260
276
  const tagCacheProps = {
@@ -561,11 +577,7 @@ export class Payload extends Construct {
561
577
  key: `${this.pascalName}PatternResources`,
562
578
  value: JSON.stringify({
563
579
  databases: [this.props.name, `${this.props.name}-tag-cache`],
564
- storage: [
565
- `${this.props.name}-assets`,
566
- `${this.props.name}-cache`,
567
- `${this.props.name}-media`
568
- ],
580
+ storage: OPENNEXT_BUCKET_KEYS.map((bucket) => patternBucketConstructId(this.props.name, bucket)),
569
581
  cdn: this.props.name
570
582
  }),
571
583
  description: `Related resource names for pattern ${this.props.name}`
@@ -64,9 +64,9 @@ export declare function resolveBucketTarget(appName: string, bucketName: string)
64
64
  export declare function resolveCustomTarget(dnsName: string, hostedZoneId: string): IAliasRecordTarget;
65
65
  /**
66
66
  * Resolve a `FjallTarget` to its raw DNS-name string. The return value is a
67
- * CloudFormation token for the ECS/CDN/bucket variants — Phase 2 resolves the
68
- * concrete host via `describeStackResources` after deploy. Used only by
69
- * `externalRecordsPattern.ts` where no Route53 resource exists and manual
70
- * record emission needs a string value.
67
+ * CloudFormation token for the ECS/CDN/bucket variants, resolved by
68
+ * CloudFormation at deploy (the concrete host lands in the stack's manual
69
+ * record `Value` output). Used only by `externalRecordsPattern.ts` where no
70
+ * Route53 resource exists and manual record emission needs a string value.
71
71
  */
72
72
  export declare function resolveTargetToDnsName(target: FjallTarget): string;
@@ -89,10 +89,10 @@ export function resolveCustomTarget(dnsName, hostedZoneId) {
89
89
  }
90
90
  /**
91
91
  * Resolve a `FjallTarget` to its raw DNS-name string. The return value is a
92
- * CloudFormation token for the ECS/CDN/bucket variants — Phase 2 resolves the
93
- * concrete host via `describeStackResources` after deploy. Used only by
94
- * `externalRecordsPattern.ts` where no Route53 resource exists and manual
95
- * record emission needs a string value.
92
+ * CloudFormation token for the ECS/CDN/bucket variants, resolved by
93
+ * CloudFormation at deploy (the concrete host lands in the stack's manual
94
+ * record `Value` output). Used only by `externalRecordsPattern.ts` where no
95
+ * Route53 resource exists and manual record emission needs a string value.
96
96
  */
97
97
  export function resolveTargetToDnsName(target) {
98
98
  switch (target.kind) {
@@ -2,13 +2,28 @@ import { Construct } from "constructs";
2
2
  import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
3
3
  import { BackupVault as Vault, type LockConfiguration } from "aws-cdk-lib/aws-backup";
4
4
  import { type PolicyDocument } from "aws-cdk-lib/aws-iam";
5
- import { CustomerManagedKey } from "../secrets/kms.js";
5
+ import { type IKey } from "aws-cdk-lib/aws-kms";
6
+ export declare function backupVaultKeyAliasName(vaultName: string): string;
6
7
  export interface BackupVaultProps {
7
8
  vaultName: string;
8
- encryptionKey?: CustomerManagedKey;
9
+ /**
10
+ * The key the vault is encrypted with when it already exists outside the
11
+ * stack (a re-owned vault's live key). `EncryptionKeyArn` is create-only,
12
+ * so a re-owned vault must always name its live key or CloudFormation
13
+ * replaces it. The wrapper aliases the key and creates none. Omit to create
14
+ * a stack-managed key.
15
+ */
16
+ encryptionKey?: IKey;
9
17
  accessPolicy?: PolicyDocument;
10
18
  lockConfiguration?: LockConfiguration;
11
19
  removalPolicy?: RemovalPolicy;
20
+ /**
21
+ * Import pass: declare identity, not intent. Strips the mutable properties
22
+ * (`BackupVaultTags`, `AccessPolicy`) from the synthesised resource so the
23
+ * deploy's `--import-existing-resources` change set imports a live mirror;
24
+ * the converge deploy re-declares them.
25
+ */
26
+ importIdentityOnly?: boolean;
12
27
  }
13
28
  export declare class BackupVault extends Construct {
14
29
  readonly vault: Vault;
@@ -1,7 +1,19 @@
1
1
  import { Construct } from "constructs";
2
2
  import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
3
- import { BackupVault as Vault } from "aws-cdk-lib/aws-backup";
3
+ import { BackupVault as Vault, CfnBackupVault } from "aws-cdk-lib/aws-backup";
4
+ import { Alias } from "aws-cdk-lib/aws-kms";
4
5
  import { CustomerManagedKey } from "../secrets/kms.js";
6
+ /**
7
+ * Vault properties CloudFormation applies as UPDATES, not identity. An import
8
+ * pass strips them so `--import-existing-resources` records a live mirror and
9
+ * the converge deploy that follows applies them as the diff (import records
10
+ * properties, it never applies them — a tag declared on the import pass is
11
+ * "already there" forever).
12
+ */
13
+ const MUTABLE_VAULT_PROPERTIES = ["BackupVaultTags", "AccessPolicy"];
14
+ export function backupVaultKeyAliasName(vaultName) {
15
+ return `cmk/backupVault/${vaultName}`;
16
+ }
5
17
  export class BackupVault extends Construct {
6
18
  vault;
7
19
  vaultArn;
@@ -11,21 +23,40 @@ export class BackupVault extends Construct {
11
23
  // RETAIN default: a DESTROY-policy vault is silently deleted with the stack.
12
24
  // Callers opt into DESTROY explicitly for ephemeral/dev vaults.
13
25
  const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN;
14
- const encryptionKey = props.encryptionKey ||
15
- new CustomerManagedKey(this, `${props.vaultName}Key`, {
26
+ const aliasName = backupVaultKeyAliasName(props.vaultName);
27
+ let encryptionKey;
28
+ if (props.encryptionKey !== undefined) {
29
+ encryptionKey = props.encryptionKey;
30
+ new Alias(this, `${props.vaultName}KeyAlias`, {
31
+ aliasName,
32
+ targetKey: encryptionKey
33
+ });
34
+ }
35
+ else {
36
+ encryptionKey = new CustomerManagedKey(this, `${props.vaultName}Key`, {
16
37
  description: `Encryption key for backup vault ${props.vaultName}`,
17
- aliasName: `cmk/backupVault/${props.vaultName}`,
38
+ aliasName,
18
39
  // Recovery points in a retained vault outlive the stack.
19
40
  protects: "outlives-stack",
20
41
  removalPolicy
21
- });
42
+ }).key;
43
+ }
22
44
  this.vault = new Vault(this, `${props.vaultName}Vault`, {
23
45
  backupVaultName: props.vaultName,
24
- encryptionKey: encryptionKey.key,
46
+ encryptionKey,
25
47
  accessPolicy: props.accessPolicy,
26
48
  lockConfiguration: props.lockConfiguration,
27
49
  removalPolicy
28
50
  });
51
+ if (props.importIdentityOnly === true) {
52
+ const cfnVault = this.vault.node.defaultChild;
53
+ if (!(cfnVault instanceof CfnBackupVault)) {
54
+ throw new Error(`Backup vault "${props.vaultName}": expected an AWS::Backup::BackupVault default child to strip for import`);
55
+ }
56
+ for (const property of MUTABLE_VAULT_PROPERTIES) {
57
+ cfnVault.addPropertyDeletionOverride(property);
58
+ }
59
+ }
29
60
  this.vaultArn = new CfnOutput(this, `${props.vaultName}VaultArn`, {
30
61
  key: `${props.vaultName}VaultArn`,
31
62
  value: this.vault.backupVaultArn,
@@ -1,5 +1,6 @@
1
1
  import { Construct } from "constructs";
2
2
  import { type CloudFrontPriceClass } from "@fjall/util";
3
+ import { CloudFrontLogDelivery, type CloudFrontLogFormat } from "./cloudFrontLogDelivery.js";
3
4
  import { Distribution, type ICachePolicy, type IResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";
4
5
  import { type IBucket } from "aws-cdk-lib/aws-s3";
5
6
  import { type IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
@@ -59,25 +60,13 @@ export interface CloudFrontDistributionProps {
59
60
  certificate?: ICertificate;
60
61
  comment?: string;
61
62
  /**
62
- * Standard (legacy) S3 access logging. It cannot deliver to a bucket in an
63
- * opt-in region (`af-south-1`, `ap-east-1`, …): CloudFormation rejects the
64
- * distribution at deploy, so a concrete opt-in delivery region is refused
65
- * at synth instead — cure with `enableLogging: false`, or a `logBucket`
66
- * whose region CDK knows to be default-enabled (see `logBucket`).
63
+ * Standard logging (v2): access logs delivered through CloudWatch Logs to
64
+ * an S3 bucket in any region, including opt-in ones. Omit for no logging.
65
+ * The distribution's own `Logging` block (legacy logging) is never
66
+ * rendered — the legacy `enableLogging`/`logBucket`/`logFilePrefix` props
67
+ * are refused at synth since 38.0.0.
67
68
  */
68
- enableLogging?: boolean;
69
- /**
70
- * Delivery bucket for standard logs; without it CDK creates one in the
71
- * distribution's own stack (and region). Its `env.region` is the delivery
72
- * region the opt-in refusal on `enableLogging` checks, so an imported
73
- * bucket must declare it: `Bucket.fromBucketAttributes(…, { region })`, or
74
- * a `Bucket` in a stack pinned to its region. `fromBucketName` and
75
- * `fromBucketArn` inherit this stack's region, and a token bucket region
76
- * falls back to it.
77
- */
78
- logBucket?: IBucket;
79
- /** Key prefix for standard logs delivered to `logBucket`. */
80
- logFilePrefix?: string;
69
+ logging?: CloudFrontLoggingProps;
81
70
  priceClass?: CloudFrontPriceClass;
82
71
  /** Adds a CloudFront Function to copy the viewer Host header into X-Forwarded-Host.
83
72
  * Required when the origin is a Lambda Function URL or ALB behind CloudFront,
@@ -121,8 +110,43 @@ export interface CloudFrontDistributionProps {
121
110
  */
122
111
  originRequestPolicy?: "all-viewer-except-host" | "none";
123
112
  }
113
+ /**
114
+ * Standard logging (v2) for a distribution. The delivery resources are
115
+ * created in `deliveryScope` — which MUST be a us-east-1 stack, the only
116
+ * region CloudWatch Logs accepts them in — and default to the distribution's
117
+ * own scope, which is right only when the distribution's stack is itself in
118
+ * us-east-1. The `Cdn` pattern resolves the scope through
119
+ * `App.getUsEast1CdnLoggingStack()`; direct callers in another region pass
120
+ * a us-east-1 stack themselves.
121
+ */
122
+ export interface CloudFrontLoggingProps {
123
+ /** Bucket the logs land in — same account as the distribution. */
124
+ bucket: IBucket;
125
+ /** Key prefix (no leading or trailing slash); defaults to `cloudfront/<id>`. */
126
+ prefix?: string;
127
+ /** CloudWatch Logs output format; defaults to `json`. */
128
+ format?: CloudFrontLogFormat;
129
+ /** Scope for the delivery resources; a us-east-1 stack. */
130
+ deliveryScope?: Construct;
131
+ }
132
+ /**
133
+ * The props CloudFront standard (legacy) logging took before 38.0.0. They
134
+ * are not members of `CloudFrontDistributionProps` any more, so a
135
+ * TypeScript caller cannot pass them; a JavaScript caller, a spread from a
136
+ * saved config or a generator template still can, and the legacy `Logging`
137
+ * block would otherwise be silently dropped. Refused at synth by name.
138
+ */
139
+ export declare const LEGACY_CLOUDFRONT_LOGGING_PROPS: readonly ["enableLogging", "logBucket", "logFilePrefix"];
140
+ /**
141
+ * `replacement` is the seam's v2 shape, named in the refusal: the resource
142
+ * requires the bucket and has no `false`, the pattern defaults the bucket
143
+ * and accepts `false`.
144
+ */
145
+ export declare function assertNoLegacyLoggingProps(context: string, props: object, replacement: string): void;
124
146
  export declare class CloudFrontDistribution extends Construct {
125
147
  readonly id: string;
148
+ /** The v2 log delivery, when `logging` was given. */
149
+ readonly logDelivery?: CloudFrontLogDelivery;
126
150
  private distribution;
127
151
  private readonly origins;
128
152
  /** Shared S3 access identities, keyed by the owning stack (see
@@ -1,8 +1,9 @@
1
1
  import { Construct } from "constructs";
2
- import { CfnOutput, Names, Stack, Token } from "aws-cdk-lib";
2
+ import { CfnOutput, Names, Stack } from "aws-cdk-lib";
3
3
  import { toPascalCase } from "../../../utils/capitaliseString.js";
4
4
  import { registerCdnAliasTarget } from "../../../utils/cdnAliasTargetRegistry.js";
5
- import { cdnDomainExportName, getRegionInfo, OPT_IN_REGION_CODES } from "@fjall/util";
5
+ import { cdnDomainExportName } from "@fjall/util";
6
+ import { CloudFrontLogDelivery, DEFAULT_CLOUDFRONT_LOG_FORMAT } from "./cloudFrontLogDelivery.js";
6
7
  import { Distribution, PriceClass, ViewerProtocolPolicy, CachePolicy, OriginRequestPolicy, AllowedMethods, OriginProtocolPolicy, Function as CloudFrontFunction, FunctionCode, FunctionRuntime, FunctionEventType, OriginAccessIdentity, S3OriginAccessControl } from "aws-cdk-lib/aws-cloudfront";
7
8
  import { S3BucketOrigin, LoadBalancerV2Origin, HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
8
9
  /**
@@ -43,56 +44,38 @@ const URI_REWRITE_FRAGMENTS = {
43
44
  " if (seg.indexOf('.') === -1) { request.uri = uri + '/index.html'; } } }"
44
45
  };
45
46
  /**
46
- * CloudFront standard (legacy) logging cannot deliver to an S3 bucket in an
47
- * opt-in region: CDK's `renderLogging` performs no region check and
48
- * CloudFormation rejects the distribution at deploy, after the rest of the
49
- * stack has been created. The delivery region is known at synth whenever the
50
- * bucket's stack is env-pinned, so the refusal lives here — the seam every
51
- * distribution crosses, whether built directly or through the `Cdn`
52
- * pattern — and names both cures.
53
- *
54
- * Logging is on exactly when `renderLogging` emits a Logging block:
55
- * `enableLogging: true`, or a `logBucket` without an explicit
56
- * `enableLogging: false` (that pair is CDK's own contradiction error and is
57
- * left to it). The delivery region is the bucket's `env.region` when it is
58
- * concrete, else the distribution stack's — the bucket CDK would create.
59
- * A token delivery region (an unpinned stack, and no region on the bucket)
60
- * cannot be checked and passes. A concrete region `AWS_REGIONS_METADATA`
61
- * does not list is refused too — every region launched since 2019 is opt-in,
62
- * so an unlisted one cannot be assumed default-enabled — naming the metadata
63
- * as the cure. The guard fires whenever the delivery region is concrete and
64
- * known to the metadata.
47
+ * The props CloudFront standard (legacy) logging took before 38.0.0. They
48
+ * are not members of `CloudFrontDistributionProps` any more, so a
49
+ * TypeScript caller cannot pass them; a JavaScript caller, a spread from a
50
+ * saved config or a generator template still can, and the legacy `Logging`
51
+ * block would otherwise be silently dropped. Refused at synth by name.
65
52
  */
66
- function validateStandardLoggingRegion(scope, id, props) {
67
- const loggingOn = props.enableLogging === true ||
68
- (props.enableLogging === undefined && props.logBucket !== undefined);
69
- if (!loggingOn)
70
- return;
71
- const bucketRegion = props.logBucket?.env.region;
72
- const region = bucketRegion !== undefined && !Token.isUnresolved(bucketRegion)
73
- ? bucketRegion
74
- : Stack.of(scope).region;
75
- if (Token.isUnresolved(region))
76
- return;
77
- if (getRegionInfo(region) === undefined) {
78
- throw new Error(`CloudFront distribution '${id}': region ${region} is not in ` +
79
- "@fjall/util's AWS_REGIONS_METADATA, so its opt-in status cannot be " +
80
- "checked — update the metadata, or set enableLogging: false on this " +
81
- "CDN.");
82
- }
83
- if (!OPT_IN_REGION_CODES.has(region))
53
+ export const LEGACY_CLOUDFRONT_LOGGING_PROPS = [
54
+ "enableLogging",
55
+ "logBucket",
56
+ "logFilePrefix"
57
+ ];
58
+ /**
59
+ * `replacement` is the seam's v2 shape, named in the refusal: the resource
60
+ * requires the bucket and has no `false`, the pattern defaults the bucket
61
+ * and accepts `false`.
62
+ */
63
+ export function assertNoLegacyLoggingProps(context, props, replacement) {
64
+ const legacy = new Set(LEGACY_CLOUDFRONT_LOGGING_PROPS);
65
+ const present = Object.entries(props)
66
+ .filter(([key, value]) => legacy.has(key) && value !== undefined)
67
+ .map(([key]) => key);
68
+ if (present.length === 0)
84
69
  return;
85
- throw new Error(`CloudFront distribution '${id}': standard (legacy) logging cannot ` +
86
- `deliver to an S3 bucket in opt-in region ${region} — logging was ` +
87
- "enabled explicitly or by the app backup tier's cloudFrontLogging " +
88
- "default. Set enableLogging: false on this CDN, or pass a logBucket " +
89
- "whose region CDK knows to be default-enabled: " +
90
- "Bucket.fromBucketAttributes(…, { region }), or a bucket in a stack " +
91
- "pinned to that region (fromBucketName and fromBucketArn inherit this " +
92
- "stack's region).");
70
+ throw new Error(`${context}: ${present.join(", ")} ${present.length === 1 ? "is" : "are"} ` +
71
+ "not supported since 38.0.0 — CloudFront standard (legacy) logging was " +
72
+ `replaced by CloudWatch-vended standard logging (v2). Use ${replacement} ` +
73
+ "instead; see docs/release-notes/38.0.0.md for the migration.");
93
74
  }
94
75
  export class CloudFrontDistribution extends Construct {
95
76
  id;
77
+ /** The v2 log delivery, when `logging` was given. */
78
+ logDelivery;
96
79
  distribution;
97
80
  origins = new Map();
98
81
  /** Shared S3 access identities, keyed by the owning stack (see
@@ -102,7 +85,7 @@ export class CloudFrontDistribution extends Construct {
102
85
  constructor(scope, id, props) {
103
86
  super(scope, id);
104
87
  this.id = id;
105
- validateStandardLoggingRegion(this, id, props);
88
+ assertNoLegacyLoggingProps(`CloudFront distribution '${id}'`, props, "logging: { bucket, prefix?, format?, deliveryScope? } (or no logging prop at all)");
106
89
  // Sanitise id for CloudFormation output keys (must be alphanumeric)
107
90
  const outputName = toPascalCase(id);
108
91
  const defaultOrigin = this.createOrigin(props.defaultOrigin);
@@ -158,11 +141,16 @@ export class CloudFrontDistribution extends Construct {
158
141
  domainNames: props.domainNames,
159
142
  certificate: props.certificate,
160
143
  comment: props.comment,
161
- enableLogging: props.enableLogging,
162
- logBucket: props.logBucket,
163
- logFilePrefix: props.logFilePrefix,
164
144
  priceClass: this.resolvePriceClass(props.priceClass)
165
145
  });
146
+ if (props.logging !== undefined) {
147
+ this.logDelivery = new CloudFrontLogDelivery(props.logging.deliveryScope ?? this, `${id}LogDelivery`, {
148
+ distribution: this.distribution,
149
+ bucket: props.logging.bucket,
150
+ prefix: props.logging.prefix ?? `cloudfront/${id}`,
151
+ format: props.logging.format ?? DEFAULT_CLOUDFRONT_LOG_FORMAT
152
+ });
153
+ }
166
154
  // The CDN-qualified export is unconditional and always unique, so an app
167
155
  // fronting several distributions stays deployable. The unqualified
168
156
  // app-level export is emitted once from `App.synth()`, and only when the
@@ -0,0 +1,90 @@
1
+ import { type IDistribution } from "aws-cdk-lib/aws-cloudfront";
2
+ import { CfnDelivery, CfnDeliveryDestination, CfnDeliverySource } from "aws-cdk-lib/aws-logs";
3
+ import { type IBucket } from "aws-cdk-lib/aws-s3";
4
+ import { Construct } from "constructs";
5
+ /**
6
+ * Output formats CloudWatch Logs writes a CloudFront access-log delivery in.
7
+ * `json` is the default; `parquet` adds CloudWatch's conversion charge on top
8
+ * of delivery. Fixed at creation — a change replaces the destination.
9
+ */
10
+ export declare const CLOUDFRONT_LOG_FORMATS: readonly ["json", "parquet", "plain", "w3c", "raw"];
11
+ export type CloudFrontLogFormat = (typeof CLOUDFRONT_LOG_FORMATS)[number];
12
+ export declare const DEFAULT_CLOUDFRONT_LOG_FORMAT: CloudFrontLogFormat;
13
+ /**
14
+ * CloudWatch Logs mints every CloudFront delivery source, destination and
15
+ * delivery in us-east-1 whatever region the bucket is in — the delivery ARNs
16
+ * read `arn:aws:logs:us-east-1:…` and the API refuses other regions.
17
+ */
18
+ export declare const CLOUDFRONT_LOG_DELIVERY_REGION = "us-east-1";
19
+ /** The service principal that writes vended logs into the bucket. */
20
+ export declare const CLOUDFRONT_LOG_DELIVERY_PRINCIPAL = "delivery.logs.amazonaws.com";
21
+ /** Objects land at `<prefix>/<yyyy>/<MM>/<dd>/<HH>/…`. Changeable in place. */
22
+ export declare const CLOUDFRONT_LOG_S3_SUFFIX_PATH = "{yyyy}/{MM}/{dd}/{HH}";
23
+ /**
24
+ * Every delivery source and destination is named under this prefix —
25
+ * `fjall-cf-<distributionId>` for the source, `fjall-cf-<distributionId>-<digest>`
26
+ * for the destination. The deploy-test residue scan attributes delivery
27
+ * resources by the prefix and the digest width and restates both; a change
28
+ * here is a change to what the scan can see.
29
+ */
30
+ export declare const CLOUDFRONT_LOG_DELIVERY_NAME_PREFIX = "fjall-cf-";
31
+ /** Hex characters of the sha256 the destination name carries. */
32
+ export declare const CLOUDFRONT_LOG_DELIVERY_DIGEST_LENGTH = 8;
33
+ export interface CloudFrontLogDeliveryProps {
34
+ /**
35
+ * The distribution whose access logs are delivered. CloudWatch Logs allows
36
+ * one delivery source per distribution account-wide, so the source is
37
+ * named by the distribution id and nothing else.
38
+ */
39
+ readonly distribution: IDistribution;
40
+ /** The bucket the logs land in; same account as the distribution. */
41
+ readonly bucket: IBucket;
42
+ /** Key prefix the objects land under — see `normaliseCloudFrontLogPrefix`. */
43
+ readonly prefix: string;
44
+ readonly format: CloudFrontLogFormat;
45
+ }
46
+ /**
47
+ * The prefix is part of the destination ARN (`arn:aws:s3:::<bucket>/<prefix>`),
48
+ * a create-only property, and it keys the destination's name, so it must be
49
+ * a literal at synth. Leading and trailing slashes are dropped (CloudWatch
50
+ * joins the suffix path with its own separator); an empty prefix is refused
51
+ * rather than silently selecting the bucket-root `AWSLogs/…` layout.
52
+ */
53
+ export declare function normaliseCloudFrontLogPrefix(context: string, prefix: string): string;
54
+ /**
55
+ * CloudFront standard logging (v2): the CloudWatch Logs delivery source
56
+ * (the distribution), delivery destination (the bucket and prefix) and the
57
+ * delivery joining them, plus the bucket-policy statement the delivery
58
+ * principal writes through. The names — `fjall-cf-<distributionId>` and
59
+ * `fjall-cf-<distributionId>-<digest>` — carry no construct path, but
60
+ * CloudFormation replaces on the LOGICAL id and creates the replacement
61
+ * before deleting the original, and CloudWatch refuses a second source for
62
+ * the same distribution. So the three logical ids are load-bearing once a
63
+ * delivery is live: a re-layout of this construct or its owner strands the
64
+ * update in rollback, and is a breaking change with a migration (delete
65
+ * the live delivery first) rather than a refactor. The delivery test pins
66
+ * the ids in both layouts.
67
+ *
68
+ * The scope MUST be a us-east-1 stack (asserted at synth). When the
69
+ * distribution and bucket live in another region's stack, their ARNs reach
70
+ * this stack through CDK cross-region references; both are stable under an
71
+ * unchanged logical id, which the export writer requires.
72
+ */
73
+ export declare class CloudFrontLogDelivery extends Construct {
74
+ readonly sourceName: string;
75
+ readonly destinationName: string;
76
+ readonly source: CfnDeliverySource;
77
+ readonly destination: CfnDeliveryDestination;
78
+ readonly delivery: CfnDelivery;
79
+ constructor(scope: Construct, id: string, props: CloudFrontLogDeliveryProps);
80
+ /**
81
+ * The bucket-policy statement the delivery principal writes through, and
82
+ * the key-policy statement when the bucket is KMS-encrypted. Both carry the
83
+ * confused-deputy conditions; the bucket statement additionally requires
84
+ * `bucket-owner-full-control`, the one ACL a BucketOwnerEnforced bucket
85
+ * accepts. Imported buckets and keys are not managed here — a synth
86
+ * warning names the statement the owner must add, because a missing grant
87
+ * fails delivery silently rather than at deploy.
88
+ */
89
+ private grantDeliveryWrite;
90
+ }