@fjall/components-infrastructure 32.0.0 → 33.1.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 (88) hide show
  1. package/README.md +15 -76
  2. package/dist/lib/app.d.ts +1 -1
  3. package/dist/lib/config/audit.js +2 -1
  4. package/dist/lib/config/aws/identityCentreConfig.d.ts +1 -1
  5. package/dist/lib/config/aws/identityCentreConfig.js +1 -1
  6. package/dist/lib/config/aws/scpPreset.js +1 -2
  7. package/dist/lib/lambda-assets/cert-generator/asset/index.js +126 -0
  8. package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +1 -1
  9. package/dist/lib/patterns/aws/buildkite/buildkite.js +1 -1
  10. package/dist/lib/patterns/aws/buildkite/factory.d.ts +2 -2
  11. package/dist/lib/patterns/aws/buildkite/pins.d.ts +2 -2
  12. package/dist/lib/patterns/aws/buildkite/pins.js +3 -4
  13. package/dist/lib/patterns/aws/buildkite/schema.d.ts +1 -1
  14. package/dist/lib/patterns/aws/buildkite/schema.js +1 -1
  15. package/dist/lib/patterns/aws/buildkite/userData.d.ts +1 -1
  16. package/dist/lib/patterns/aws/buildkite/userData.js +1 -1
  17. package/dist/lib/patterns/aws/buildkite.d.ts +1 -1
  18. package/dist/lib/patterns/aws/buildkite.js +1 -1
  19. package/dist/lib/patterns/aws/cdn.d.ts +14 -0
  20. package/dist/lib/patterns/aws/cdn.js +31 -3
  21. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +8 -0
  22. package/dist/lib/patterns/aws/clickhouseDatabase.js +19 -3
  23. package/dist/lib/patterns/aws/clickhouseTls/types.d.ts +1 -1
  24. package/dist/lib/patterns/aws/compute.js +4 -2
  25. package/dist/lib/patterns/aws/computeEcs.d.ts +7 -0
  26. package/dist/lib/patterns/aws/computeEcs.js +26 -3
  27. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +30 -7
  28. package/dist/lib/patterns/aws/computeLambda.d.ts +12 -0
  29. package/dist/lib/patterns/aws/computeLambda.js +2 -0
  30. package/dist/lib/patterns/aws/computePropApplicability.js +2 -0
  31. package/dist/lib/patterns/aws/database.d.ts +14 -0
  32. package/dist/lib/patterns/aws/database.js +16 -6
  33. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +6 -6
  34. package/dist/lib/patterns/aws/messaging.js +4 -1
  35. package/dist/lib/patterns/aws/patternDomain.d.ts +1 -1
  36. package/dist/lib/patterns/aws/patternDomain.js +1 -1
  37. package/dist/lib/patterns/aws/payload.js +7 -5
  38. package/dist/lib/patterns/aws/staticSite.d.ts +6 -6
  39. package/dist/lib/patterns/aws/staticSite.js +9 -6
  40. package/dist/lib/patterns/aws/storage.d.ts +8 -0
  41. package/dist/lib/patterns/aws/storage.js +1 -0
  42. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +2 -0
  43. package/dist/lib/resources/aws/cdn/cloudFront.js +1 -0
  44. package/dist/lib/resources/aws/cdn/responseHeadersPolicy.d.ts +1 -1
  45. package/dist/lib/resources/aws/cdn/responseHeadersPolicy.js +1 -1
  46. package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +10 -0
  47. package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +7 -1
  48. package/dist/lib/resources/aws/compute/ec2.d.ts +2 -2
  49. package/dist/lib/resources/aws/compute/ec2.js +1 -1
  50. package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
  51. package/dist/lib/resources/aws/compute/ecs.js +51 -6
  52. package/dist/lib/resources/aws/compute/ecsContext.d.ts +10 -1
  53. package/dist/lib/resources/aws/compute/ecsNetworking.js +3 -1
  54. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +4 -1
  55. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +31 -8
  56. package/dist/lib/resources/aws/compute/lambda.d.ts +18 -1
  57. package/dist/lib/resources/aws/compute/lambda.js +21 -3
  58. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +5 -5
  59. package/dist/lib/resources/aws/database/clickhouseConstants.js +5 -5
  60. package/dist/lib/resources/aws/database/dynamodb.d.ts +19 -0
  61. package/dist/lib/resources/aws/database/dynamodb.js +13 -2
  62. package/dist/lib/resources/aws/database/rdsAurora.d.ts +11 -0
  63. package/dist/lib/resources/aws/database/rdsAurora.js +24 -3
  64. package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +11 -1
  65. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +1 -0
  66. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +0 -2
  67. package/dist/lib/resources/aws/database/rdsDefaults.js +0 -2
  68. package/dist/lib/resources/aws/database/rdsInstance.d.ts +11 -0
  69. package/dist/lib/resources/aws/database/rdsInstance.js +19 -4
  70. package/dist/lib/resources/aws/iam/identityCenter/user.d.ts +1 -1
  71. package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
  72. package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +22 -0
  73. package/dist/lib/resources/aws/logging/accessLogBucket.js +41 -0
  74. package/dist/lib/resources/aws/logging/index.d.ts +1 -0
  75. package/dist/lib/resources/aws/logging/index.js +1 -0
  76. package/dist/lib/resources/aws/messaging/sns.d.ts +14 -5
  77. package/dist/lib/resources/aws/messaging/sns.js +7 -1
  78. package/dist/lib/resources/aws/secrets/index.d.ts +1 -0
  79. package/dist/lib/resources/aws/secrets/index.js +1 -0
  80. package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +47 -0
  81. package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +114 -0
  82. package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +2 -3
  83. package/dist/lib/resources/aws/storage/bucketDeployment.js +2 -3
  84. package/dist/lib/resources/aws/storage/s3.d.ts +13 -1
  85. package/dist/lib/resources/aws/storage/s3.js +6 -3
  86. package/dist/lib/utils/backupTierMapping.d.ts +10 -2
  87. package/dist/lib/utils/backupTierMapping.js +3 -1
  88. package/package.json +5 -4
@@ -171,6 +171,13 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
171
171
  * the version entry the app containers get.
172
172
  */
173
173
  private resolveSchemaGateMaterialisation;
174
+ /**
175
+ * Encryption for a log group this pattern creates beside the cluster's
176
+ * own (scheduled-task and migration-task logs): the stack's shared key
177
+ * when the app backup tier encrypts, nothing otherwise — the same key the
178
+ * cluster resolved for its service and exec-audit groups.
179
+ */
180
+ private tierLogGroupEncryption;
174
181
  private materialiseScheduledTasks;
175
182
  private buildScheduledTaskDefinition;
176
183
  /**
@@ -955,6 +955,12 @@ export class EcsCompute extends Construct {
955
955
  domainConfig: props.cluster.domainConfig,
956
956
  ...(props.cluster.securityGroup !== undefined && {
957
957
  securityGroup: props.cluster.securityGroup
958
+ }),
959
+ ...(props.cluster.loadBalancerDeletionProtection !== undefined && {
960
+ loadBalancerDeletionProtection: props.cluster.loadBalancerDeletionProtection
961
+ }),
962
+ ...(props.cluster.loadBalancerAccessLogs !== undefined && {
963
+ loadBalancerAccessLogs: props.cluster.loadBalancerAccessLogs
958
964
  })
959
965
  }
960
966
  : undefined;
@@ -970,7 +976,12 @@ export class EcsCompute extends Construct {
970
976
  applicationId: props.applicationId ??
971
977
  (App.hasInstance() ? App.getInstance().getName() : undefined),
972
978
  ...(props.taskStopWatchdog === false && { taskStopWatchdog: false }),
973
- ...(props.containerInsights === true && { containerInsights: true })
979
+ ...(props.containerInsights !== undefined && {
980
+ containerInsights: props.containerInsights
981
+ }),
982
+ ...(props.appBackupTier !== undefined && {
983
+ appBackupTier: props.appBackupTier
984
+ })
974
985
  };
975
986
  this.ecsCluster = new EcsCluster(this, `${id}Ecs`, ecsProps);
976
987
  this.connections = this.ecsCluster.connections;
@@ -1155,6 +1166,16 @@ export class EcsCompute extends Construct {
1155
1166
  defaultImageTag: CONSTRUCTS_VERSION
1156
1167
  };
1157
1168
  }
1169
+ /**
1170
+ * Encryption for a log group this pattern creates beside the cluster's
1171
+ * own (scheduled-task and migration-task logs): the stack's shared key
1172
+ * when the app backup tier encrypts, nothing otherwise — the same key the
1173
+ * cluster resolved for its service and exec-audit groups.
1174
+ */
1175
+ tierLogGroupEncryption() {
1176
+ const key = this.ecsCluster.encryptionKey;
1177
+ return key === undefined ? {} : { encryptionKey: key.forLogGroups() };
1178
+ }
1158
1179
  materialiseScheduledTasks(id, props) {
1159
1180
  const scheduledTasks = props.cluster?.scheduledTasks;
1160
1181
  if (!scheduledTasks || scheduledTasks.length === 0)
@@ -1200,7 +1221,8 @@ export class EcsCompute extends Construct {
1200
1221
  // removal default (D17).
1201
1222
  const logGroup = entry.logGroup ??
1202
1223
  new LogGroup(this, `${id}${toPascalCase(entry.name)}LogGroup`, {
1203
- retention: entry.logRetention ?? DEFAULT_LOG_RETENTION
1224
+ retention: entry.logRetention ?? DEFAULT_LOG_RETENTION,
1225
+ ...this.tierLogGroupEncryption()
1204
1226
  });
1205
1227
  const logging = new AwsLogDriver({
1206
1228
  streamPrefix: `/ecs-scheduled/${id}/${entry.name}`,
@@ -1429,7 +1451,8 @@ export class EcsCompute extends Construct {
1429
1451
  const inheritedImage = service.taskDefinition.defaultContainer?.imageName;
1430
1452
  const image = resolveMigrationImage(svcConfig, migrations, inheritedImage);
1431
1453
  const logGroup = new LogGroup(this, `${idPrefix}LogGroup`, {
1432
- retention: RetentionDays.ONE_MONTH
1454
+ retention: RetentionDays.ONE_MONTH,
1455
+ ...this.tierLogGroupEncryption()
1433
1456
  });
1434
1457
  const executionRole = new Role(this, `${idPrefix}ExecutionRole`, {
1435
1458
  assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
@@ -5,6 +5,7 @@ import { type ILogGroup, type RetentionDays } from "aws-cdk-lib/aws-logs";
5
5
  import { type PolicyDocument, type IManagedPolicy, type PolicyStatement } from "aws-cdk-lib/aws-iam";
6
6
  import type { ITopic } from "aws-cdk-lib/aws-sns";
7
7
  import { type ConnectionSpec } from "./interfaces/connector.js";
8
+ import type { BackupTier } from "../../utils/backupTierMapping.js";
8
9
  import { type RemoteConnectionSpec } from "../../resources/aws/compute/ecsRemoteConnections.js";
9
10
  import { type EcsRoutingConfig, type EcsContainerDependency } from "../../resources/aws/compute/ecsTypes.js";
10
11
  import { ScalingType, type DomainConfig, type EcsCapacityProvider, type Ec2CapacityConfig, type QueueScalingConfig } from "../../resources/aws/compute/ecs.js";
@@ -181,8 +182,8 @@ export interface EcsContainerConfig {
181
182
  * per service. Suitable for high-replica services where the init-container
182
183
  * tax dominates rollout time.
183
184
  *
184
- * @see aiDocs/decisions/2026-04-29-ecs-init-container-over-runtask.md
185
- * @see aiDocs/plans/2026-04-29-ecs-factory-hardening.md § 4.3.4 — threshold
185
+ * @see the 2026-04-29 decision: ECS init container over RunTask
186
+ * @see the 2026-04-29 ECS factory-hardening plan, § 4.3.4 — threshold
186
187
  * guidance for choosing between modes.
187
188
  * @see https://aws.amazon.com/blogs/containers/announcing-amazon-ecs-deployment-lifecycle-hooks/
188
189
  */
@@ -222,7 +223,7 @@ export type EcsMigrationsMode = "init-container" | "lifecycle-hook" | "post-depl
222
223
  * mode: "lifecycle-hook"
223
224
  * }
224
225
  *
225
- * @see aiDocs/decisions/2026-04-29-ecs-init-container-over-runtask.md
226
+ * @see the 2026-04-29 decision: ECS init container over RunTask
226
227
  */
227
228
  /**
228
229
  * Discriminated on `mode`. The init-container variant keeps its narrow surface
@@ -236,8 +237,8 @@ export type EcsMigrationsMode = "init-container" | "lifecycle-hook" | "post-depl
236
237
  * lifecycle-hook-only field — is a typecheck error. This protects against
237
238
  * the Pitfall-5 trap (silent field drift across union variants).
238
239
  *
239
- * @see aiDocs/designs/2026-05-12-migration-runner-abstraction-choice.md
240
- * @see .claude/rules/typescript-standards.md § "Pitfall 5"
240
+ * @see the 2026-05-12 migration-runner abstraction-choice design
241
+ * @see the TypeScript standards, Zod Pitfall 5 (three-way option drift)
241
242
  */
242
243
  export type EcsMigrationsConfig = EcsInitContainerMigrationsConfig | EcsLifecycleHookMigrationsConfig | EcsPostDeployMigrationsConfig;
243
244
  /**
@@ -623,6 +624,19 @@ export interface EcsClusterConfig {
623
624
  * (e.g. ClickHouseDatabase) that own their security group lifecycle.
624
625
  */
625
626
  securityGroup?: ISecurityGroup;
627
+ /**
628
+ * Deletion protection on the shared load balancer. Default: the app backup
629
+ * tier's `deletionProtection` column, applied only when the stack's
630
+ * environment resolves to production. The destroy pre-flight switches it
631
+ * off before a consented destroy.
632
+ */
633
+ loadBalancerDeletionProtection?: boolean;
634
+ /**
635
+ * Access logs from the shared load balancer into the stack's
636
+ * lifecycle-expired access-log bucket. Default: the app backup tier's
637
+ * `albAccessLogs` column.
638
+ */
639
+ loadBalancerAccessLogs?: boolean;
626
640
  /**
627
641
  * Scheduled tasks materialised into `Ec2TaskDefinition`s and registered via
628
642
  * `app.addSchedule(...)` against the existing `EcsScheduleTarget` shape
@@ -1074,9 +1088,18 @@ export interface EcsComputeProps {
1074
1088
  * `RunningTaskCount` liveness alarms (`ecsAlarms.ts` gates them on this
1075
1089
  * flag because the metric only exists in `ECS/ContainerInsights`).
1076
1090
  * Scale-to-zero services should pair it with
1077
- * `alarms: { runningTasksMinimum: 0 }` or they idle in ALARM.
1091
+ * `alarms: { runningTasksMinimum: 0 }` or they idle in ALARM. Default:
1092
+ * the app backup tier's `containerInsights` column (on at enterprise);
1093
+ * `false` is an explicit, auditable opt-out.
1078
1094
  */
1079
- containerInsights?: true;
1095
+ containerInsights?: boolean;
1080
1096
  /** Application ID for alarm tagging (used by webhook to map alarms to applications). */
1081
1097
  applicationId?: string;
1098
+ /**
1099
+ * The owning app's backup tier. Auto-derived from the App by
1100
+ * ComputeFactory; the tier-owned defaults (Container Insights, exec-session
1101
+ * and log-group encryption, load-balancer deletion protection and access
1102
+ * logs) follow it. Only specify for direct construction.
1103
+ */
1104
+ appBackupTier?: BackupTier;
1082
1105
  }
@@ -4,6 +4,7 @@ import { Connections, type IVpc, type ISecurityGroup } from "aws-cdk-lib/aws-ec2
4
4
  import { Repository } from "aws-cdk-lib/aws-ecr";
5
5
  import { type RepositoryImage } from "aws-cdk-lib/aws-ecs";
6
6
  import { Construct } from "constructs";
7
+ import type { BackupTier } from "../../utils/backupTierMapping.js";
7
8
  import { type ILambdaCompute } from "./interfaces/compute.js";
8
9
  import { type ConnectionSpec } from "./interfaces/connector.js";
9
10
  import { LambdaFunction } from "../../resources/aws/compute/lambda.js";
@@ -95,6 +96,17 @@ interface BaseLambdaProps {
95
96
  * Only specify for advanced use cases.
96
97
  */
97
98
  appName?: string;
99
+ /**
100
+ * The owning app's backup tier. Auto-derived from the App by
101
+ * ComputeFactory; the tier-owned defaults (X-Ray tracing, environment and
102
+ * log-group encryption) follow it. Only specify for direct construction.
103
+ */
104
+ appBackupTier?: BackupTier;
105
+ /**
106
+ * Active X-Ray tracing. Default: the app backup tier's `lambdaTracing`
107
+ * column (on at enterprise).
108
+ */
109
+ tracing?: boolean;
98
110
  /**
99
111
  * Resources this Lambda needs to connect to (e.g., databases, S3 buckets, SQS queues).
100
112
  * Creates security group rules for IConnectable resources and IAM grants for IAM resources.
@@ -167,6 +167,8 @@ export class LambdaCompute extends Construct {
167
167
  secretsImport: props.secretsImport,
168
168
  environment: props.environment,
169
169
  appName: props.appName,
170
+ appBackupTier: props.appBackupTier,
171
+ tracing: props.tracing,
170
172
  functionName: id
171
173
  };
172
174
  this.lambdaFunction = new LambdaFunction(scope, `${id}Lambda`, lambdaProps);
@@ -10,6 +10,8 @@ const COMPUTE_PROP_APPLICABILITY = {
10
10
  type: "all",
11
11
  vpc: "all",
12
12
  appName: ["ecs", "lambda"],
13
+ appBackupTier: ["ecs", "lambda"],
14
+ tracing: ["lambda"],
13
15
  docker: ["ecs", "lambda"],
14
16
  ecrRepository: ["ecs", "lambda"],
15
17
  cluster: ["ecs"],
@@ -5,6 +5,7 @@ import type { RdsAlarmThresholds } from "../../resources/aws/monitoring/index.js
5
5
  import type App from "../../app.js";
6
6
  import { type DynamoDBKeySchema, type DynamoDBGlobalSecondaryIndex } from "../../resources/aws/database/dynamodb.js";
7
7
  import { ClickHouseDatabase, type ClickHouseDatabaseProps, type ClickHouseAlarmThresholds } from "./clickhouseDatabase.js";
8
+ import { type BackupTier } from "../../utils/backupTierMapping.js";
8
9
  import { type Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
9
10
  import { type ITable } from "aws-cdk-lib/aws-dynamodb";
10
11
  import { type Secret, type SecretImport } from "../../resources/aws/secrets/index.js";
@@ -40,6 +41,13 @@ type BaseDatabaseProps = {
40
41
  databaseName: string;
41
42
  instanceType?: string;
42
43
  deletionProtection?: boolean;
44
+ /**
45
+ * The owning app's backup tier (`App.getApp({ backup: { tier } })`), which
46
+ * the tier-owned defaults (backup retention) follow. `DatabaseFactory`
47
+ * fills it from the App as it does `vpc` and `applicationId`; pass it only
48
+ * for direct `new RelationalDatabase(...)` construction.
49
+ */
50
+ appBackupTier?: BackupTier;
43
51
  /** SNS topic for alarm notifications. Accepts ITopic, ARN string, or "import:ExportName". */
44
52
  alertsTopic?: ITopic | string;
45
53
  /** Alarm thresholds. false to disable, undefined for defaults, object to override. */
@@ -194,6 +202,12 @@ export interface DynamoDBDatabaseProps {
194
202
  encryption?: "AWS_OWNED" | "AWS_MANAGED" | "CUSTOMER_MANAGED";
195
203
  deletionProtection?: boolean;
196
204
  removalPolicy?: "DESTROY" | "RETAIN" | "SNAPSHOT";
205
+ /**
206
+ * The owning app's backup tier, which the tier-owned defaults (encryption,
207
+ * deletion protection) follow. `DatabaseFactory` fills it from the App;
208
+ * pass it only for direct `new DynamoDBDatabase(...)` construction.
209
+ */
210
+ appBackupTier?: BackupTier;
197
211
  }
198
212
  /**
199
213
  * Relational database props union (Aurora, Instance, GlobalAurora).
@@ -130,7 +130,7 @@ function validateRelationalDatabaseProps(props) {
130
130
  // Instance-only options on Aurora/GlobalAurora
131
131
  warnIfPropertiesIgnored(props, ["readReplica", "multiAz", "allocatedStorage", "iamAuthentication"], "Instance", "Instance database");
132
132
  // Aurora-only options on Instance
133
- warnIfPropertiesIgnored(props, ["readers", "writer", "backupRetention"], ["Aurora", "GlobalAurora"], "Aurora database");
133
+ warnIfPropertiesIgnored(props, ["readers", "writer"], ["Aurora", "GlobalAurora"], "Aurora database");
134
134
  // GlobalAurora-only options on non-global databases
135
135
  warnIfPropertiesIgnored(props, ["primaryRegion", "secondaryRegions", "enableGlobalWriteForwarding"], "GlobalAurora", "GlobalAurora database");
136
136
  }
@@ -170,21 +170,27 @@ export class DatabaseFactory {
170
170
  static build(id, props) {
171
171
  return (app, scope) => {
172
172
  if (props.type === "DynamoDB") {
173
- validateDatabaseProps(props);
174
- return new DynamoDBDatabase(scope, id, props);
173
+ const dynamoProps = {
174
+ ...props,
175
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
176
+ };
177
+ validateDatabaseProps(dynamoProps);
178
+ return new DynamoDBDatabase(scope, id, dynamoProps);
175
179
  }
176
180
  if (props.type === "ClickHouse") {
177
181
  const clickhouseProps = {
178
182
  ...props,
179
183
  vpc: props.vpc ?? app.getVpc(),
180
- applicationId: props.applicationId ?? app.getName()
184
+ applicationId: props.applicationId ?? app.getName(),
185
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
181
186
  };
182
187
  return new ClickHouseDatabase(scope, id, clickhouseProps);
183
188
  }
184
189
  const databaseProps = {
185
190
  ...props,
186
191
  vpc: props.vpc ?? app.getVpc(),
187
- applicationId: props.applicationId ?? app.getName()
192
+ applicationId: props.applicationId ?? app.getName(),
193
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
188
194
  };
189
195
  validateDatabaseProps(databaseProps);
190
196
  return new RelationalDatabase(scope, id, databaseProps);
@@ -223,7 +229,8 @@ export class DynamoDBDatabase extends Construct {
223
229
  pointInTimeRecovery: props.pointInTimeRecovery,
224
230
  encryption: props.encryption,
225
231
  deletionProtection: props.deletionProtection,
226
- removalPolicy: props.removalPolicy
232
+ removalPolicy: props.removalPolicy,
233
+ appBackupTier: props.appBackupTier
227
234
  };
228
235
  this.table = new DynamoDBTable(this, id, tableProps);
229
236
  }
@@ -390,6 +397,7 @@ export class RelationalDatabase extends Construct {
390
397
  credentials: props.credentials,
391
398
  encryption: props.encryption,
392
399
  backupRetention: props.backupRetention,
400
+ appBackupTier: props.appBackupTier,
393
401
  monitoringInterval: props.monitoringInterval,
394
402
  preferredMaintenanceWindow: props.preferredMaintenanceWindow,
395
403
  port: props.port,
@@ -429,6 +437,7 @@ export class RelationalDatabase extends Construct {
429
437
  credentials: props.credentials,
430
438
  encryption: props.encryption,
431
439
  backupRetention: props.backupRetention,
440
+ appBackupTier: props.appBackupTier,
432
441
  monitoringInterval: props.monitoringInterval,
433
442
  preferredMaintenanceWindow: props.preferredMaintenanceWindow,
434
443
  port: props.port,
@@ -459,6 +468,7 @@ export class RelationalDatabase extends Construct {
459
468
  backupRetention: props.backupRetention !== undefined
460
469
  ? Duration.days(props.backupRetention)
461
470
  : undefined,
471
+ appBackupTier: props.appBackupTier,
462
472
  databaseInsights: props.databaseInsights,
463
473
  proxy: props.proxy,
464
474
  readReplica: props.readReplica,
@@ -468,16 +468,16 @@ export interface StaticSiteCdnConfig {
468
468
  * optional contact-form endpoint (Lambda Function URL → SES, CloudFront-fronted).
469
469
  *
470
470
  * @example
471
- * app.addPattern(PatternFactory.build("LbexcSite", {
471
+ * app.addPattern(PatternFactory.build("StorefrontSite", {
472
472
  * type: "staticsite",
473
- * name: "lbexc",
474
- * source: "../../lbexc-static-export",
473
+ * name: "storefront",
474
+ * source: "../../storefront-static-export",
475
475
  * build: { command: "npm run build", outputDir: "out" },
476
476
  * routing: "multipage",
477
477
  * security: { headers: true },
478
- * domain: "lbexc.com",
479
- * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
480
- * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
478
+ * domain: "storefront.example",
479
+ * zoneName: "storefront.example", // or a CLI-injected `managedDomain` binding
480
+ * forms: { to: "hello@storefront.example", corsOrigin: "https://storefront.example" }
481
481
  * }));
482
482
  */
483
483
  export interface IStaticSiteProps {
@@ -315,7 +315,10 @@ export class MessagingFactory {
315
315
  applicationId: props.applicationId ?? app.getName()
316
316
  });
317
317
  case "topic":
318
- return new TopicMessaging(scope, id, props);
318
+ return new TopicMessaging(scope, id, {
319
+ ...props,
320
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
321
+ });
319
322
  case "eventBus":
320
323
  return new EventBusMessaging(scope, id, props);
321
324
  default: {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Nothing here guesses a hosted zone. The old `extractRootDomain` label
9
9
  * split resolved every multi-label public suffix wrongly (e.g.
10
- * 'littlebearearthworks.com.au' → zone 'com.au') and mis-placed delegated
10
+ * 'example.com.au' → zone 'com.au') and mis-placed delegated
11
11
  * sub-zone records; it is deleted. Zone identity now arrives explicitly, in
12
12
  * priority order: a CLI-injected {@link ManagedDomainBinding} (literal
13
13
  * values — crosses accounts and regions), the legacy
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Nothing here guesses a hosted zone. The old `extractRootDomain` label
9
9
  * split resolved every multi-label public suffix wrongly (e.g.
10
- * 'littlebearearthworks.com.au' → zone 'com.au') and mis-placed delegated
10
+ * 'example.com.au' → zone 'com.au') and mis-placed delegated
11
11
  * sub-zone records; it is deleted. Zone identity now arrives explicitly, in
12
12
  * priority order: a CLI-injected {@link ManagedDomainBinding} (literal
13
13
  * values — crosses accounts and regions), the legacy
@@ -41,8 +41,6 @@ const PAYLOAD_DEFAULTS = {
41
41
  SOURCE: "../..",
42
42
  DATABASE: {
43
43
  TYPE: "Instance",
44
- /** AWS default; 7 days covers most accidental data loss recovery scenarios */
45
- BACKUP_RETENTION: 7,
46
44
  DELETION_PROTECTION: true
47
45
  },
48
46
  COMPUTE: {
@@ -172,7 +170,7 @@ export class Payload extends Construct {
172
170
  return {
173
171
  databaseName: dbConfig.databaseName ?? toValidDatabaseName(this.props.name),
174
172
  databaseEngine: dbConfig.databaseEngine ?? "postgresql",
175
- backupRetention: dbConfig.backupRetention ?? PAYLOAD_DEFAULTS.DATABASE.BACKUP_RETENTION,
173
+ backupRetention: dbConfig.backupRetention,
176
174
  deletionProtection: dbConfig.deletionProtection ??
177
175
  PAYLOAD_DEFAULTS.DATABASE.DELETION_PROTECTION,
178
176
  publiclyAccessible: dbConfig.publiclyAccessible,
@@ -219,8 +217,10 @@ export class Payload extends Construct {
219
217
  createAssetsBucket() {
220
218
  const assetsConfig = this.props.storage?.assets ?? {};
221
219
  const assetsProps = {
222
- // Build output, pruned on every deploy — versioning would only bill for churn.
220
+ // Build output, pruned on every deploy — versioning would only bill for
221
+ // churn; the exemption stamp tells posture the omission is by design.
223
222
  versioned: assetsConfig.versioned ?? false,
223
+ versioningExemption: "build-output",
224
224
  deployment: {
225
225
  source: `${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.ASSETS}`,
226
226
  prune: true,
@@ -235,8 +235,10 @@ export class Payload extends Construct {
235
235
  createCacheBucket() {
236
236
  const cacheConfig = this.props.storage?.cache ?? {};
237
237
  const cacheProps = {
238
- // ISR cache, regenerated at runtime — versioning would only bill for churn.
238
+ // ISR cache, regenerated at runtime — versioning would only bill for
239
+ // churn; the exemption stamp tells posture the omission is by design.
239
240
  versioned: cacheConfig.versioned ?? false,
241
+ versioningExemption: "isr-cache",
240
242
  deployment: {
241
243
  source: `${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.CACHE}`,
242
244
  prune: false // ISR adds files at runtime
@@ -13,16 +13,16 @@
13
13
  * - Contact-form Lambda + Function URL (when `forms` is set)
14
14
  *
15
15
  * @example
16
- * const site = app.addPattern(PatternFactory.build("LbexcSite", {
16
+ * const site = app.addPattern(PatternFactory.build("StorefrontSite", {
17
17
  * type: "staticsite",
18
- * name: "lbexc",
19
- * source: "../../lbexc-static-export",
18
+ * name: "storefront",
19
+ * source: "../../storefront-static-export",
20
20
  * build: { command: "npm run build", outputDir: "out" },
21
21
  * routing: "multipage",
22
22
  * security: { headers: true },
23
- * domain: "lbexc.com",
24
- * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
25
- * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
23
+ * domain: "storefront.example",
24
+ * zoneName: "storefront.example", // or a CLI-injected `managedDomain` binding
25
+ * forms: { to: "hello@storefront.example", corsOrigin: "https://storefront.example" }
26
26
  * }));
27
27
  */
28
28
  import { Construct } from "constructs";
@@ -13,16 +13,16 @@
13
13
  * - Contact-form Lambda + Function URL (when `forms` is set)
14
14
  *
15
15
  * @example
16
- * const site = app.addPattern(PatternFactory.build("LbexcSite", {
16
+ * const site = app.addPattern(PatternFactory.build("StorefrontSite", {
17
17
  * type: "staticsite",
18
- * name: "lbexc",
19
- * source: "../../lbexc-static-export",
18
+ * name: "storefront",
19
+ * source: "../../storefront-static-export",
20
20
  * build: { command: "npm run build", outputDir: "out" },
21
21
  * routing: "multipage",
22
22
  * security: { headers: true },
23
- * domain: "lbexc.com",
24
- * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
25
- * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
23
+ * domain: "storefront.example",
24
+ * zoneName: "storefront.example", // or a CLI-injected `managedDomain` binding
25
+ * forms: { to: "hello@storefront.example", corsOrigin: "https://storefront.example" }
26
26
  * }));
27
27
  */
28
28
  import { Construct } from "constructs";
@@ -207,6 +207,9 @@ export class StaticSite extends Construct {
207
207
  reservedConcurrentExecutions: forms.maxConcurrency ?? FORMS_DEFAULTS.MAX_CONCURRENCY,
208
208
  lambdaDescription: `Contact-form handler for ${this.props.name}`,
209
209
  roleDescription: `Contact-form send role for ${this.props.name}`,
210
+ // An app workload, not a framework handler: tracing and encryption
211
+ // follow the app backup tier like every factory-built Lambda.
212
+ appBackupTier: this.app.getBackupTier(),
210
213
  enableFunctionUrl: true,
211
214
  functionUrlAuthType: FunctionUrlAuthType.NONE,
212
215
  functionUrlCors: {
@@ -2,6 +2,7 @@ import { Construct } from "constructs";
2
2
  import { type EventType, type IBucketNotificationDestination, type NotificationKeyFilter } from "aws-cdk-lib/aws-s3";
3
3
  import { type IDistribution } from "aws-cdk-lib/aws-cloudfront";
4
4
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
5
+ import { type S3VersioningExemption } from "@fjall/util/aws";
5
6
  import type App from "../../app.js";
6
7
  import { BucketDeployment, S3Bucket, type ResourcePolicyStatement, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
7
8
  import { type BackupTier } from "../../utils/backupTierMapping.js";
@@ -39,6 +40,13 @@ export interface S3Props {
39
40
  * `resilient`/`enterprise`, off otherwise. An explicit value always wins.
40
41
  */
41
42
  readonly versioned?: boolean;
43
+ /**
44
+ * Why this bucket runs unversioned by design (`build-output`, `isr-cache`,
45
+ * `cold-tier`) — see `S3BucketProps.versioningExemption`. Stamped as the
46
+ * `fjall:s3:versioning-exempt` tag when versioning resolves off; a bucket
47
+ * that versions after all is not stamped.
48
+ */
49
+ readonly versioningExemption?: S3VersioningExemption;
42
50
  readonly encryption?: "AES256" | "KMS";
43
51
  readonly kmsKeyArn?: string;
44
52
  /**
@@ -78,6 +78,7 @@ export class Storage extends Construct {
78
78
  aliasTargetName: props.bucketName ?? id,
79
79
  appName: props.appName,
80
80
  versioned: props.versioned,
81
+ versioningExemption: props.versioningExemption,
81
82
  encryption: toBucketEncryption(props.encryption),
82
83
  encryptionKey,
83
84
  ...(props.bucketKeyEnabled !== undefined && {
@@ -60,6 +60,8 @@ export interface CloudFrontDistributionProps {
60
60
  comment?: string;
61
61
  enableLogging?: boolean;
62
62
  logBucket?: IBucket;
63
+ /** Key prefix for standard logs delivered to `logBucket`. */
64
+ logFilePrefix?: string;
63
65
  priceClass?: CloudFrontPriceClass;
64
66
  /** Adds a CloudFront Function to copy the viewer Host header into X-Forwarded-Host.
65
67
  * Required when the origin is a Lambda Function URL or ALB behind CloudFront,
@@ -110,6 +110,7 @@ export class CloudFrontDistribution extends Construct {
110
110
  comment: props.comment,
111
111
  enableLogging: props.enableLogging,
112
112
  logBucket: props.logBucket,
113
+ logFilePrefix: props.logFilePrefix,
113
114
  priceClass: this.resolvePriceClass(props.priceClass)
114
115
  });
115
116
  // The CDN-qualified export is unconditional and always unique, so an app
@@ -40,7 +40,7 @@ export interface SecurityHeadersPolicyProps {
40
40
  * Wrapped on the description + SOC2-defaults trigger (NOT taggability — the
41
41
  * resource is not taggable): it owns Fjall's uniform security-header defaults
42
42
  * so every consumer inherits the same posture in one edit. Per
43
- * `.claude/rules/generator-standards.md § Wrapper Routing Discipline`,
43
+ * the Wrapper Routing Discipline rule (generator standards),
44
44
  * `lib/{config,patterns}/` instantiate this class, never the raw L2.
45
45
  *
46
46
  * Defaults (all `override:true`): HSTS 730d+includeSubdomains+preload,
@@ -18,7 +18,7 @@ const DEFAULT_COMMENT = "Fjall security headers policy";
18
18
  * Wrapped on the description + SOC2-defaults trigger (NOT taggability — the
19
19
  * resource is not taggable): it owns Fjall's uniform security-header defaults
20
20
  * so every consumer inherits the same posture in one edit. Per
21
- * `.claude/rules/generator-standards.md § Wrapper Routing Discipline`,
21
+ * the Wrapper Routing Discipline rule (generator standards),
22
22
  * `lib/{config,patterns}/` instantiate this class, never the raw L2.
23
23
  *
24
24
  * Defaults (all `override:true`): HSTS 730d+includeSubdomains+preload,
@@ -7,6 +7,16 @@ export interface ApplicationLoadBalancerOptions {
7
7
  readonly internetFacing: boolean;
8
8
  readonly loadBalancerName?: string;
9
9
  readonly securityGroup?: ISecurityGroup;
10
+ /**
11
+ * `deletion_protection.enabled` on the load balancer (CDK emits the
12
+ * attribute as `false` when unset, so an unprotected template is unchanged).
13
+ */
14
+ readonly deletionProtection?: boolean;
15
+ /**
16
+ * Deliver access logs to the stack's shared access-log bucket under
17
+ * `alb/<id>/`. The bucket is created on first use.
18
+ */
19
+ readonly accessLogs?: boolean;
10
20
  }
11
21
  /**
12
22
  * Context-free Application Load Balancer factory shared by the ECS cluster path
@@ -2,6 +2,7 @@ import { Duration } from "aws-cdk-lib";
2
2
  import { ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
3
3
  import { SubnetType } from "aws-cdk-lib/aws-ec2";
4
4
  import { DEFAULT_ALB_IDLE_TIMEOUT_SECONDS } from "@fjall/util/httpKeepAlive";
5
+ import { stackAccessLogBucket } from "../logging/accessLogBucket.js";
5
6
  /**
6
7
  * Context-free Application Load Balancer factory shared by the ECS cluster path
7
8
  * and the dev substrate. Subnet placement is derived from `internetFacing`
@@ -9,7 +10,7 @@ import { DEFAULT_ALB_IDLE_TIMEOUT_SECONDS } from "@fjall/util/httpKeepAlive";
9
10
  * ECS path's existing derivation so the extraction is byte-identical.
10
11
  */
11
12
  export function createApplicationLoadBalancer(scope, id, options) {
12
- return new ApplicationLoadBalancer(scope, id, {
13
+ const loadBalancer = new ApplicationLoadBalancer(scope, id, {
13
14
  vpc: options.vpc,
14
15
  internetFacing: options.internetFacing,
15
16
  // Pinned (rather than left at the AWS default of the same value) because
@@ -23,10 +24,15 @@ export function createApplicationLoadBalancer(scope, id, options) {
23
24
  ...(options.loadBalancerName && {
24
25
  loadBalancerName: options.loadBalancerName
25
26
  }),
27
+ deletionProtection: options.deletionProtection,
26
28
  vpcSubnets: {
27
29
  subnetType: options.internetFacing
28
30
  ? SubnetType.PUBLIC
29
31
  : SubnetType.PRIVATE_WITH_EGRESS
30
32
  }
31
33
  });
34
+ if (options.accessLogs === true) {
35
+ loadBalancer.logAccessLogs(stackAccessLogBucket(scope), `alb/${id}`);
36
+ }
37
+ return loadBalancer;
32
38
  }
@@ -27,7 +27,7 @@ export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumePro
27
27
  * `AutoScalingScheduledAction.IgnoreUnmodifiedGroupSizeProperties`
28
28
  * regardless. For callers owning rollout externally.
29
29
  *
30
- * See `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
30
+ * See the ClickHouse rolling-update-window troubleshooting note for the
31
31
  * ClickHouse-specific downtime expectations.
32
32
  */
33
33
  export type Ec2InstanceUpdatePolicyConfig = {
@@ -127,7 +127,7 @@ export interface Ec2InstanceProps extends StackProps {
127
127
  * ASG `UpdatePolicy` resolution. Absent → `UpdatePolicy.instanceRefresh`
128
128
  * (0/100 with `persistentDataVolume`, 100/200 otherwise). See
129
129
  * `Ec2InstanceUpdatePolicyConfig` for the variant menu and
130
- * `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
130
+ * the ClickHouse rolling-update-window troubleshooting note for the
131
131
  * downtime-window runbook.
132
132
  */
133
133
  updatePolicy?: Ec2InstanceUpdatePolicyConfig;
@@ -17,7 +17,7 @@ import { PersistentDataVolume } from "./persistentDataVolume.js";
17
17
  * `encrypted: true` enforces EBS encryption at the wrapper layer rather than
18
18
  * relying on the AWS account-level "encryption by default" setting — the
19
19
  * propagation contract documented in
20
- * `.claude/rules/generator-standards.md § "Wrapper Routing Discipline"`.
20
+ * the Wrapper Routing Discipline rule (generator standards).
21
21
  */
22
22
  const DEFAULT_ROOT_DEVICE_NAME = "/dev/xvda";
23
23
  const DEFAULT_ROOT_VOLUME_SIZE_GIB = 30;