@fjall/components-infrastructure 37.0.0 → 39.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 (72) hide show
  1. package/dist/lib/app.d.ts +52 -12
  2. package/dist/lib/app.js +60 -24
  3. package/dist/lib/config/aws/configRecorder.js +3 -0
  4. package/dist/lib/config/aws/disasterRecovery.d.ts +1 -2
  5. package/dist/lib/config/aws/disasterRecovery.js +167 -45
  6. package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -2
  7. package/dist/lib/patterns/aws/cdn.d.ts +19 -11
  8. package/dist/lib/patterns/aws/cdn.js +53 -25
  9. package/dist/lib/patterns/aws/clickhouseDatabase.js +8 -4
  10. package/dist/lib/patterns/aws/computeEcs.js +17 -1
  11. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +30 -4
  12. package/dist/lib/patterns/aws/computePropApplicability.js +1 -0
  13. package/dist/lib/patterns/aws/domain.js +3 -3
  14. package/dist/lib/patterns/aws/externalRecordsPattern.d.ts +9 -6
  15. package/dist/lib/patterns/aws/externalRecordsPattern.js +22 -46
  16. package/dist/lib/patterns/aws/interfaces/domain.d.ts +8 -5
  17. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +9 -10
  18. package/dist/lib/patterns/aws/messaging.d.ts +10 -0
  19. package/dist/lib/patterns/aws/messaging.js +10 -6
  20. package/dist/lib/patterns/aws/payload.d.ts +5 -0
  21. package/dist/lib/patterns/aws/payload.js +23 -11
  22. package/dist/lib/patterns/aws/storage.d.ts +41 -0
  23. package/dist/lib/patterns/aws/storage.js +23 -8
  24. package/dist/lib/patterns/aws/targets/targetResolution.d.ts +4 -4
  25. package/dist/lib/patterns/aws/targets/targetResolution.js +4 -4
  26. package/dist/lib/resources/aws/backup/backupVault.d.ts +27 -2
  27. package/dist/lib/resources/aws/backup/backupVault.js +49 -6
  28. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +42 -18
  29. package/dist/lib/resources/aws/cdn/cloudFront.js +39 -51
  30. package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.d.ts +90 -0
  31. package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.js +213 -0
  32. package/dist/lib/resources/aws/cdn/index.d.ts +1 -0
  33. package/dist/lib/resources/aws/cdn/index.js +1 -0
  34. package/dist/lib/resources/aws/compute/ecs.d.ts +7 -4
  35. package/dist/lib/resources/aws/compute/ecs.js +27 -21
  36. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +3 -0
  37. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +11 -0
  38. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +8 -0
  39. package/dist/lib/resources/aws/compute/ecsValidation.js +8 -0
  40. package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +8 -0
  41. package/dist/lib/resources/aws/compute/persistentDataVolume.js +2 -0
  42. package/dist/lib/resources/aws/database/dynamodb.js +2 -1
  43. package/dist/lib/resources/aws/database/rdsAurora.js +6 -1
  44. package/dist/lib/resources/aws/database/rdsInstance.js +6 -1
  45. package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +15 -8
  46. package/dist/lib/resources/aws/logging/accessLogBucket.js +16 -9
  47. package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
  48. package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +16 -0
  49. package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +91 -0
  50. package/dist/lib/resources/aws/messaging/eventBridgeRule.js +36 -5
  51. package/dist/lib/resources/aws/messaging/eventTargets.d.ts +4 -3
  52. package/dist/lib/resources/aws/messaging/eventTargets.js +71 -31
  53. package/dist/lib/resources/aws/messaging/queuePolicyQuota.d.ts +25 -0
  54. package/dist/lib/resources/aws/messaging/queuePolicyQuota.js +190 -0
  55. package/dist/lib/resources/aws/messaging/schedule.d.ts +5 -5
  56. package/dist/lib/resources/aws/messaging/sqs.d.ts +36 -1
  57. package/dist/lib/resources/aws/messaging/sqs.js +40 -2
  58. package/dist/lib/resources/aws/messaging/subscription.d.ts +4 -3
  59. package/dist/lib/resources/aws/networking/vpc.js +12 -6
  60. package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +26 -1
  61. package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +56 -1
  62. package/dist/lib/resources/aws/storage/s3.d.ts +21 -1
  63. package/dist/lib/resources/aws/storage/s3.js +89 -3
  64. package/dist/lib/utils/backupTierMapping.d.ts +23 -2
  65. package/dist/lib/utils/backupTierMapping.js +25 -0
  66. package/dist/lib/utils/cdkContext.d.ts +14 -0
  67. package/dist/lib/utils/cdkContext.js +38 -1
  68. package/dist/lib/utils/exportNaming.d.ts +30 -27
  69. package/dist/lib/utils/exportNaming.js +32 -28
  70. package/dist/lib/utils/standardTagsAspect.d.ts +8 -18
  71. package/dist/lib/utils/standardTagsAspect.js +8 -34
  72. package/package.json +4 -4
@@ -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}`
@@ -47,7 +47,35 @@ export interface S3Props {
47
47
  * that versions after all is not stamped.
48
48
  */
49
49
  readonly versioningExemption?: S3VersioningExemption;
50
+ /**
51
+ * `"AES256"` is SSE-S3: AWS's own key, free, and real encryption at rest.
52
+ * `"KMS"` is SSE-KMS with a key of YOUR OWN, so it requires `kmsKeyArn` —
53
+ * it is refused without one rather than falling back.
54
+ *
55
+ * The fallback is what this refuses. Asking CloudFormation for KMS
56
+ * encryption with no key makes CDK mint an `AWS::KMS::Key` of its own,
57
+ * `DeletionPolicy: Retain`, with no alias and no tags — an orphan every
58
+ * Fjall teardown sweep is blind to, because a sweep finds keys by the
59
+ * alias tag `CustomerManagedKey` stamps and that key carries none.
60
+ *
61
+ * Nor does the bucket quietly join the stack's shared key, the way an
62
+ * `"SSE_KMS"` queue does. A queue holds messages for at most fourteen
63
+ * days; a bucket holds objects forever, and an object is readable only
64
+ * with the key it was written under — so moving a live bucket's key
65
+ * revokes read access to everything already in it, at the same deploy
66
+ * that rewrites the reader's `kms:Decrypt` grant onto the new key. The
67
+ * choice of key for a bucket is the operator's, always.
68
+ */
50
69
  readonly encryption?: "AES256" | "KMS";
70
+ /**
71
+ * The customer managed key this bucket's objects are encrypted with.
72
+ * Implies `encryption: "KMS"` when no encryption is stated, and is refused
73
+ * beside `"AES256"` rather than silently ignored.
74
+ *
75
+ * An ARN rather than an `IKey` because this is the plan vocabulary the
76
+ * scaffold and the codemod write; a key you hold in `infrastructure.ts`
77
+ * passes as `key.keyArn`.
78
+ */
51
79
  readonly kmsKeyArn?: string;
52
80
  /**
53
81
  * S3 Bucket Keys for KMS-encrypted buckets — defaults to true when
@@ -94,6 +122,19 @@ export interface StorageBuildProps extends S3Props {
94
122
  export type StorageFactoryFn = ((app: App, scope: Construct) => Storage) & {
95
123
  stackPlacement?: "storage" | "cdn" | "compute";
96
124
  };
125
+ /**
126
+ * Refuse the two `encryption`/`kmsKeyArn` pairings a bucket cannot honour.
127
+ *
128
+ * Both were warnings until 38.x, and both warnings said something untrue.
129
+ * `"KMS"` with no key did not fall back to "the default AWS managed key": it
130
+ * made CDK mint a retained, alias-less, tag-less key of its own beside a
131
+ * bucket that may be DESTROY — the orphan class this refusal exists for. And
132
+ * a key beside `"AES256"` was not "ignored": CDK refuses that pair itself,
133
+ * one frame later, in construct vocabulary the operator did not type.
134
+ *
135
+ * A key with NO stated encryption is neither, and is deliberately allowed —
136
+ * CDK infers KMS from the key, which is what the caller asked for.
137
+ */
97
138
  export declare function validateStorageProps(props: S3Props): void;
98
139
  export declare class Storage extends Construct implements IStorage, IStorageConnector {
99
140
  readonly connectorType: "storage";
@@ -4,7 +4,6 @@ import { Source, CacheControl } from "aws-cdk-lib/aws-s3-deployment";
4
4
  import { Key } from "aws-cdk-lib/aws-kms";
5
5
  import { CfnOutput, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
6
6
  import { BucketDeployment, S3Bucket } from "../../resources/aws/storage/index.js";
7
- import { FjallLogger } from "../../utils/validationLogger.js";
8
7
  export { isStorage } from "./interfaces/storage.js";
9
8
  function toBucketEncryption(encryption) {
10
9
  if (!encryption)
@@ -50,14 +49,30 @@ function resolveStorageRemovalPolicy(props) {
50
49
  }
51
50
  return props.retain ? RemovalPolicy.RETAIN : undefined;
52
51
  }
52
+ /**
53
+ * Refuse the two `encryption`/`kmsKeyArn` pairings a bucket cannot honour.
54
+ *
55
+ * Both were warnings until 38.x, and both warnings said something untrue.
56
+ * `"KMS"` with no key did not fall back to "the default AWS managed key": it
57
+ * made CDK mint a retained, alias-less, tag-less key of its own beside a
58
+ * bucket that may be DESTROY — the orphan class this refusal exists for. And
59
+ * a key beside `"AES256"` was not "ignored": CDK refuses that pair itself,
60
+ * one frame later, in construct vocabulary the operator did not type.
61
+ *
62
+ * A key with NO stated encryption is neither, and is deliberately allowed —
63
+ * CDK infers KMS from the key, which is what the caller asked for.
64
+ */
53
65
  export function validateStorageProps(props) {
54
- if (props.encryption === "KMS" && !props.kmsKeyArn) {
55
- FjallLogger.warn("'encryption' is set to 'KMS' but 'kmsKeyArn' is not provided. " +
56
- "The bucket will use the default AWS managed key.");
57
- }
58
- if (props.kmsKeyArn && props.encryption !== "KMS") {
59
- FjallLogger.warn("'kmsKeyArn' is provided but 'encryption' is not set to 'KMS'. " +
60
- "The KMS key will be ignored. Set encryption: 'KMS' to use the key.");
66
+ if (props.encryption === "KMS" && props.kmsKeyArn === undefined) {
67
+ throw new Error(`encryption: "KMS" needs the ARN of the key to encrypt with. Pass ` +
68
+ `'kmsKeyArn', or use encryption: "AES256" for S3-managed encryption ` +
69
+ `at rest, which costs nothing and needs no key.`);
70
+ }
71
+ if (props.kmsKeyArn !== undefined && props.encryption === "AES256") {
72
+ throw new Error(`'kmsKeyArn' encrypts with a key of your own and encryption: "AES256" ` +
73
+ `encrypts with S3's, so the pair has no meaning. Set ` +
74
+ `encryption: "KMS" to use the key, or drop 'kmsKeyArn' to keep ` +
75
+ `"AES256".`);
61
76
  }
62
77
  }
63
78
  export class Storage extends Construct {
@@ -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,18 +2,43 @@ 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;
15
30
  readonly vaultArn: CfnOutput;
16
31
  readonly vaultName: CfnOutput;
32
+ /** The key the vault encrypts recovery points with. */
33
+ readonly encryptionKey: IKey;
34
+ /**
35
+ * Whether this stack created the key. A re-owned vault names a live key the
36
+ * stack does not manage, and `addToResourcePolicy` on an imported key is a
37
+ * no-op — so a caller that needs a statement on the key policy (a
38
+ * cross-account copy destination does) must check this and say so when it
39
+ * cannot apply one.
40
+ */
41
+ readonly ownsEncryptionKey: boolean;
17
42
  constructor(scope: Construct, id: string, props: BackupVaultProps);
18
43
  static build(id: string, props: BackupVaultProps): (scope: Construct) => BackupVault;
19
44
  }
@@ -1,31 +1,74 @@
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;
8
20
  vaultName;
21
+ /** The key the vault encrypts recovery points with. */
22
+ encryptionKey;
23
+ /**
24
+ * Whether this stack created the key. A re-owned vault names a live key the
25
+ * stack does not manage, and `addToResourcePolicy` on an imported key is a
26
+ * no-op — so a caller that needs a statement on the key policy (a
27
+ * cross-account copy destination does) must check this and say so when it
28
+ * cannot apply one.
29
+ */
30
+ ownsEncryptionKey;
9
31
  constructor(scope, id, props) {
10
32
  super(scope, id);
11
33
  // RETAIN default: a DESTROY-policy vault is silently deleted with the stack.
12
34
  // Callers opt into DESTROY explicitly for ephemeral/dev vaults.
13
35
  const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN;
14
- const encryptionKey = props.encryptionKey ||
15
- new CustomerManagedKey(this, `${props.vaultName}Key`, {
36
+ const aliasName = backupVaultKeyAliasName(props.vaultName);
37
+ let encryptionKey;
38
+ if (props.encryptionKey !== undefined) {
39
+ encryptionKey = props.encryptionKey;
40
+ new Alias(this, `${props.vaultName}KeyAlias`, {
41
+ aliasName,
42
+ targetKey: encryptionKey
43
+ });
44
+ }
45
+ else {
46
+ encryptionKey = new CustomerManagedKey(this, `${props.vaultName}Key`, {
16
47
  description: `Encryption key for backup vault ${props.vaultName}`,
17
- aliasName: `cmk/backupVault/${props.vaultName}`,
48
+ aliasName,
18
49
  // Recovery points in a retained vault outlive the stack.
19
50
  protects: "outlives-stack",
20
51
  removalPolicy
21
- });
52
+ }).key;
53
+ }
54
+ this.encryptionKey = encryptionKey;
55
+ this.ownsEncryptionKey = props.encryptionKey === undefined;
22
56
  this.vault = new Vault(this, `${props.vaultName}Vault`, {
23
57
  backupVaultName: props.vaultName,
24
- encryptionKey: encryptionKey.key,
58
+ encryptionKey,
25
59
  accessPolicy: props.accessPolicy,
26
60
  lockConfiguration: props.lockConfiguration,
27
61
  removalPolicy
28
62
  });
63
+ if (props.importIdentityOnly === true) {
64
+ const cfnVault = this.vault.node.defaultChild;
65
+ if (!(cfnVault instanceof CfnBackupVault)) {
66
+ throw new Error(`Backup vault "${props.vaultName}": expected an AWS::Backup::BackupVault default child to strip for import`);
67
+ }
68
+ for (const property of MUTABLE_VAULT_PROPERTIES) {
69
+ cfnVault.addPropertyDeletionOverride(property);
70
+ }
71
+ }
29
72
  this.vaultArn = new CfnOutput(this, `${props.vaultName}VaultArn`, {
30
73
  key: `${props.vaultName}VaultArn`,
31
74
  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