@fjall/components-infrastructure 23.0.0 → 25.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.
@@ -55,6 +55,21 @@ export interface AccountProps extends StackProps {
55
55
  */
56
56
  machineTrustMode?: MachineTrustMode;
57
57
  }
58
+ /**
59
+ * `fjall:description` values for the governance tiers — the same user-facing
60
+ * gold-plating the domain-side constructs carry, so a governance stack's
61
+ * resources explain themselves in the console exactly like a zone or
62
+ * certificate does. Keyed by `organisationType` so each tier self-describes
63
+ * from ONE stamp in this base class.
64
+ *
65
+ * CHARSET CONTRACT: every value must stay inside the AWS tag-value pattern
66
+ * `^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$` — no commas. IAM roles and nested-stack
67
+ * resources (AWS::CloudFormation::Stack) validate tag values against it, so
68
+ * one comma aborts the whole governance deploy at changeset creation ("Tag
69
+ * [fjall:description] contained invalid characters"). Exported so the
70
+ * governanceTagging suite pins the contract for every tier.
71
+ */
72
+ export declare const GOVERNANCE_TIER_DESCRIPTIONS: Record<OrganisationType, string>;
58
73
  export declare class Account extends Stack {
59
74
  readonly organisationType: OrganisationType;
60
75
  protected readonly resolvedRegion: string;
@@ -22,9 +22,16 @@ import { InspectorEnablement } from "../../config/aws/inspectorEnablement.js";
22
22
  * resources explain themselves in the console exactly like a zone or
23
23
  * certificate does. Keyed by `organisationType` so each tier self-describes
24
24
  * from ONE stamp in this base class.
25
+ *
26
+ * CHARSET CONTRACT: every value must stay inside the AWS tag-value pattern
27
+ * `^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$` — no commas. IAM roles and nested-stack
28
+ * resources (AWS::CloudFormation::Stack) validate tag values against it, so
29
+ * one comma aborts the whole governance deploy at changeset creation ("Tag
30
+ * [fjall:description] contained invalid characters"). Exported so the
31
+ * governanceTagging suite pins the contract for every tier.
25
32
  */
26
- const GOVERNANCE_TIER_DESCRIPTIONS = {
27
- organisation: "Fjall-managed organisation governance: org root, member accounts and organisation-wide policy",
33
+ export const GOVERNANCE_TIER_DESCRIPTIONS = {
34
+ organisation: "Fjall-managed organisation governance: org root and member accounts under organisation-wide policy",
28
35
  platform: "Fjall-managed platform governance: shared platform services and CI trust",
29
36
  account: "Fjall-managed account governance: account security and operations baseline"
30
37
  };
@@ -6,7 +6,7 @@ import { type ICachePolicy, type IResponseHeadersPolicy } from "aws-cdk-lib/aws-
6
6
  import type App from "../../app.js";
7
7
  import { CloudFrontDistribution, type CachePolicyPreset, type AccessGateConfig } from "../../resources/aws/cdn/index.js";
8
8
  import { type ICdn } from "./interfaces/cdn.js";
9
- import { type StaticSiteRouting } from "@fjall/util";
9
+ import { type StaticSiteRouting, type CloudFrontPriceClass } from "@fjall/util";
10
10
  import { type ManagedDomainBinding, type ManagedDomainExports } from "../../utils/domainTypes.js";
11
11
  import { type Storage } from "./storage.js";
12
12
  import { type AnyCompute } from "./compute.js";
@@ -83,7 +83,7 @@ interface BaseCdnProps {
83
83
  comment?: string;
84
84
  enableLogging?: boolean;
85
85
  logBucket?: IBucket;
86
- priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
86
+ priceClass?: CloudFrontPriceClass;
87
87
  forwardHostHeader?: boolean;
88
88
  accessGate?: false | AccessGateConfig;
89
89
  }
@@ -23,8 +23,9 @@ import { type Storage } from "../storage.js";
23
23
  import { type QueueMessaging } from "../messaging.js";
24
24
  import { type Cdn, type SmartCdnBehaviour, type StaticSiteRouting } from "../cdn.js";
25
25
  import { type LambdaFunction } from "../../../resources/aws/compute/index.js";
26
+ import type { AccessGateConfig, SecurityHeadersPolicy } from "../../../resources/aws/cdn/index.js";
26
27
  import type { ManagedDomainBinding, ManagedDomainExports } from "../../../utils/domainTypes.js";
27
- import type { PatternType } from "@fjall/util";
28
+ import type { PatternType, CloudFrontPriceClass, STATIC_SITE_CONFIG_KEYS, STATIC_SITE_CDN_CONFIG_KEYS } from "@fjall/util";
28
29
  export type { ProxyConfig, ReadReplicaConfig, CredentialsConfig, EncryptionConfig, AuroraEncryptionConfig, AuroraWriterConfig, AuroraReadersConfig, DatabaseInsightsConfig };
29
30
  /**
30
31
  * Full database configuration for patterns.
@@ -422,6 +423,23 @@ export interface StaticSiteFormsConfig {
422
423
  export interface StaticSiteCdnConfig {
423
424
  /** Additional CDN behaviours (per-path overrides). */
424
425
  behaviours?: SmartCdnBehaviour[];
426
+ /**
427
+ * CloudFront price class — which edge locations serve the site. Default
428
+ * `"PriceClass_100"` (NA + Europe only). At small-site traffic the price
429
+ * difference between classes is ~zero, so treat this as a latency knob:
430
+ * an audience outside NA/Europe (e.g. Australia) wants `"PriceClass_All"`,
431
+ * or every request crosses an ocean to the nearest POP.
432
+ */
433
+ priceClass?: CloudFrontPriceClass;
434
+ /**
435
+ * Invalidate the distribution (`/*`) when a deploy uploads new assets.
436
+ * Default `true`: without it CloudFront serves stale edge copies until the
437
+ * cache policy's TTL (up to 24h) expires, which a non-expert cannot tell
438
+ * apart from a failed deploy. AWS grants 1,000 free invalidation paths per
439
+ * month and `/*` counts as one path per deploy. Set `false` to keep pure
440
+ * TTL semantics (e.g. very frequent deploys where a stale window is fine).
441
+ */
442
+ invalidateOnDeploy?: boolean;
425
443
  }
426
444
  /**
427
445
  * Static-site pattern props.
@@ -485,7 +503,30 @@ export interface IStaticSiteProps {
485
503
  forms?: StaticSiteFormsConfig;
486
504
  /** CDN configuration - for advanced per-path overrides. */
487
505
  cdn?: StaticSiteCdnConfig;
506
+ /**
507
+ * Gate every request behind HTTP Basic auth at the edge (a CloudFront
508
+ * viewer function; the gate runs BEFORE the routing rewrite). This is an
509
+ * obscurity gate for staging sign-off — keeping a not-yet-launched site
510
+ * out of casual view and search indexes — NOT a security boundary: the
511
+ * credentials sit in plain text here and in the distribution's function
512
+ * code, and the origin objects are unchanged. Remove it at launch.
513
+ */
514
+ accessGate?: AccessGateConfig;
488
515
  }
516
+ /**
517
+ * Field-parity witnesses (compile-time, zero runtime cost).
518
+ *
519
+ * `STATIC_SITE_CONFIG_KEYS` in `@fjall/util` is the manifest every
520
+ * field-enumerating surface is held to (generator schema, AST parser,
521
+ * emitter — see its JSDoc). These aliases fail to compile the moment
522
+ * `IStaticSiteProps` or `StaticSiteCdnConfig` gains or loses a key the
523
+ * manifest does not list, so the interface cannot drift from the manifest —
524
+ * and via the generator's parity tests, from the other surfaces.
525
+ */
526
+ type MutuallyAssignable<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false;
527
+ type AssertTrue<T extends true> = T;
528
+ export type StaticSitePropsParityWitness = AssertTrue<MutuallyAssignable<keyof IStaticSiteProps, (typeof STATIC_SITE_CONFIG_KEYS)[number]>>;
529
+ export type StaticSiteCdnConfigParityWitness = AssertTrue<MutuallyAssignable<keyof StaticSiteCdnConfig, (typeof STATIC_SITE_CDN_CONFIG_KEYS)[number]>>;
489
530
  /**
490
531
  * Union of all pattern props.
491
532
  * Extend this when adding new patterns (e.g., INextjsProps, IRemixProps).
@@ -554,7 +595,10 @@ export interface IPayload extends IPattern {
554
595
  * Provides access to the underlying resources for escape hatches.
555
596
  *
556
597
  * @example
557
- * site.getBucket().getBucket().addLifecycleRule({ ... });
598
+ * // Expire old objects: the concrete S3 Bucket is two getters away.
599
+ * site.getBucket().getBucket().addLifecycleRule({
600
+ * expiration: Duration.days(90)
601
+ * });
558
602
  * site.getCdn().getDistribution().addBehavior("/custom/*", customOrigin);
559
603
  */
560
604
  export interface IStaticSite extends IPattern {
@@ -565,6 +609,11 @@ export interface IStaticSite extends IPattern {
565
609
  getCdn(): Cdn;
566
610
  /** Get the contact-form Lambda (undefined when `forms` is not configured) */
567
611
  getFormsFunction(): LambdaFunction | undefined;
612
+ /**
613
+ * Get the security-headers ResponseHeadersPolicy (undefined unless
614
+ * `security.headers` is set) — e.g. to attach it to extra behaviours.
615
+ */
616
+ getResponseHeadersPolicy(): SecurityHeadersPolicy | undefined;
568
617
  }
569
618
  /**
570
619
  * Union type representing any pattern interface.
@@ -6,7 +6,8 @@
6
6
  * optional contact-form endpoint (Lambda Function URL → SES, CloudFront-fronted).
7
7
  *
8
8
  * Resources created:
9
- * - Private S3 bucket + BucketDeployment (asset upload)
9
+ * - Private S3 bucket + BucketDeployment (asset upload + post-upload
10
+ * invalidation of the distribution, unless `cdn.invalidateOnDeploy: false`)
10
11
  * - CloudFront distribution (OAC, routing-mode viewer function, security-headers policy)
11
12
  * - ACM certificate + Route53 alias record (when `domain` is set)
12
13
  * - Contact-form Lambda + Function URL (when `forms` is set)
@@ -29,6 +30,7 @@ import type App from "../../app.js";
29
30
  import { type IStaticSiteProps, type IStaticSite } from "./interfaces/pattern.js";
30
31
  import { type Storage } from "./storage.js";
31
32
  import { type Cdn } from "./cdn.js";
33
+ import { SecurityHeadersPolicy } from "../../resources/aws/cdn/index.js";
32
34
  import { LambdaFunction } from "../../resources/aws/compute/index.js";
33
35
  /**
34
36
  * Static-site pattern implementation.
@@ -46,7 +48,8 @@ export declare class StaticSite extends Construct implements IStaticSite {
46
48
  constructor(scope: Construct, id: string, app: App, props: IStaticSiteProps);
47
49
  private registerManifest;
48
50
  private validateProps;
49
- private createBucketAndDeployment;
51
+ private createBucket;
52
+ private deploySiteAssets;
50
53
  private createFormsEndpoint;
51
54
  private createCdn;
52
55
  private buildBehaviours;
@@ -57,4 +60,5 @@ export declare class StaticSite extends Construct implements IStaticSite {
57
60
  getBucket(): Storage;
58
61
  getCdn(): Cdn;
59
62
  getFormsFunction(): LambdaFunction | undefined;
63
+ getResponseHeadersPolicy(): SecurityHeadersPolicy | undefined;
60
64
  }
@@ -6,7 +6,8 @@
6
6
  * optional contact-form endpoint (Lambda Function URL → SES, CloudFront-fronted).
7
7
  *
8
8
  * Resources created:
9
- * - Private S3 bucket + BucketDeployment (asset upload)
9
+ * - Private S3 bucket + BucketDeployment (asset upload + post-upload
10
+ * invalidation of the distribution, unless `cdn.invalidateOnDeploy: false`)
10
11
  * - CloudFront distribution (OAC, routing-mode viewer function, security-headers policy)
11
12
  * - ACM certificate + Route53 alias record (when `domain` is set)
12
13
  * - Contact-form Lambda + Function URL (when `forms` is set)
@@ -75,11 +76,15 @@ export class StaticSite extends Construct {
75
76
  this.pascalName = toPascalCase(props.name);
76
77
  this.registerManifest();
77
78
  this.validateProps();
78
- this.createBucketAndDeployment();
79
+ this.createBucket();
79
80
  // Forms must exist before the CDN — createCdn wires the /api/contact*
80
81
  // behaviour to the forms Function URL host (§15).
81
82
  this.createFormsEndpoint();
82
83
  this.createCdn();
84
+ // Assets deploy AFTER the CDN: the deployment invalidates the
85
+ // distribution post-upload, and the distribution needs the bucket first
86
+ // (it is the origin) — so the order is bucket → CDN → deployment.
87
+ this.deploySiteAssets();
83
88
  this.createDnsRecord();
84
89
  this.exportPatternOutputs();
85
90
  }
@@ -109,16 +114,18 @@ export class StaticSite extends Construct {
109
114
  "and may be any address.");
110
115
  }
111
116
  }
112
- createBucketAndDeployment() {
113
- const deploymentSource = resolve(this.props.source, this.props.build.outputDir);
117
+ createBucket() {
118
+ // No `deployment` here — assets deploy via deploySiteAssets() once the
119
+ // distribution exists, so the upload can invalidate it.
114
120
  this._bucket = this.app.addStorage(StorageFactory.build(`${this.pascalName}Site`, {
115
- stackPlacement: "cdn",
116
- deployment: {
117
- source: deploymentSource,
118
- prune: true
119
- }
121
+ stackPlacement: "cdn"
120
122
  }));
121
123
  }
124
+ deploySiteAssets() {
125
+ const deploymentSource = resolve(this.props.source, this.props.build.outputDir);
126
+ const invalidate = this.props.cdn?.invalidateOnDeploy ?? true;
127
+ this._bucket.deployAssets({ source: deploymentSource, prune: true }, invalidate ? this._cdn.getDistribution() : undefined);
128
+ }
122
129
  createFormsEndpoint() {
123
130
  const forms = this.props.forms;
124
131
  if (!forms)
@@ -189,9 +196,15 @@ export class StaticSite extends Construct {
189
196
  // routing: "directory" — the default cannot protect it.
190
197
  routing: this.props.routing ?? "multipage",
191
198
  cachePolicy: "CACHING_OPTIMIZED",
192
- priceClass: "PriceClass_100",
199
+ // Default NA+Europe-only: the cheapest class, and right for most
200
+ // sites. An audience elsewhere (e.g. Australia) should set
201
+ // cdn.priceClass — see StaticSiteCdnConfig.
202
+ priceClass: this.props.cdn?.priceClass ?? "PriceClass_100",
193
203
  domainNames,
194
204
  certificate,
205
+ ...(this.props.accessGate !== undefined && {
206
+ accessGate: this.props.accessGate
207
+ }),
195
208
  ...(this._headersPolicy && {
196
209
  responseHeadersPolicy: this._headersPolicy.getPolicy()
197
210
  }),
@@ -304,4 +317,7 @@ export class StaticSite extends Construct {
304
317
  getFormsFunction() {
305
318
  return this._formsFunction;
306
319
  }
320
+ getResponseHeadersPolicy() {
321
+ return this._headersPolicy;
322
+ }
307
323
  }
@@ -1,8 +1,9 @@
1
1
  import { Construct } from "constructs";
2
- import { type IBucket, type EventType, type IBucketNotificationDestination, type NotificationKeyFilter } from "aws-cdk-lib/aws-s3";
2
+ import { type EventType, type IBucketNotificationDestination, type NotificationKeyFilter } from "aws-cdk-lib/aws-s3";
3
+ import { type IDistribution } from "aws-cdk-lib/aws-cloudfront";
3
4
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
4
5
  import type App from "../../app.js";
5
- import { BucketDeployment, type ResourcePolicyStatement, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
6
+ import { BucketDeployment, S3Bucket, type ResourcePolicyStatement, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
6
7
  import { type BackupTier } from "../../utils/backupTierMapping.js";
7
8
  import { type IStorage } from "./interfaces/storage.js";
8
9
  import { type IStorageConnector } from "./interfaces/connector.js";
@@ -68,11 +69,35 @@ export declare function validateStorageProps(props: S3Props): void;
68
69
  export declare class Storage extends Construct implements IStorage, IStorageConnector {
69
70
  readonly connectorType: "storage";
70
71
  private readonly bucket;
71
- private readonly bucketDeployment?;
72
+ private bucketDeployment?;
72
73
  constructor(scope: Construct, id: string, props: S3Props);
74
+ /**
75
+ * Upload assets to the bucket AFTER construction — for composition orders
76
+ * where the deployment must reference a construct that does not exist yet
77
+ * when the bucket is built. The static-site pattern is the canonical case:
78
+ * the CDN needs the bucket as its origin, and the deployment needs the
79
+ * distribution for its post-upload invalidation, so the pattern builds
80
+ * bucket → CDN → deployment. Hand-composed sites hit the same ordering and
81
+ * can call this directly with `cdn.getDistribution()`.
82
+ *
83
+ * A bucket deploys one asset set: calling this when a deployment already
84
+ * exists (constructor `deployment` config, or a prior call) throws rather
85
+ * than stacking a second BucketDeployment whose prune semantics would
86
+ * fight the first.
87
+ *
88
+ * @param distribution When set, the deployment invalidates it (`/*`) after
89
+ * upload, so the new assets are served immediately instead of after the
90
+ * cache TTL (up to 24h) expires.
91
+ */
92
+ deployAssets(config: S3DeploymentConfig, distribution?: IDistribution): BucketDeployment;
73
93
  private createDeployment;
74
94
  private addOutputs;
75
- getBucket(): IBucket;
95
+ /**
96
+ * The concrete bucket (narrowed from `IStorage`'s `IBucket`), so
97
+ * mutating escape hatches — `addLifecycleRule`, `addCorsRule` — are
98
+ * reachable without a cast.
99
+ */
100
+ getBucket(): S3Bucket;
76
101
  getBucketName(): string;
77
102
  getBucketArn(): string;
78
103
  getBucketDomainName(): string;
@@ -88,11 +88,39 @@ export class Storage extends Construct {
88
88
  ...(removalPolicy !== undefined && { removalPolicy })
89
89
  });
90
90
  if (props.deployment) {
91
- this.bucketDeployment = this.createDeployment(id, props.deployment);
91
+ this.bucketDeployment = this.createDeployment(props.deployment);
92
92
  }
93
93
  this.addOutputs(id);
94
94
  }
95
- createDeployment(id, config) {
95
+ /**
96
+ * Upload assets to the bucket AFTER construction — for composition orders
97
+ * where the deployment must reference a construct that does not exist yet
98
+ * when the bucket is built. The static-site pattern is the canonical case:
99
+ * the CDN needs the bucket as its origin, and the deployment needs the
100
+ * distribution for its post-upload invalidation, so the pattern builds
101
+ * bucket → CDN → deployment. Hand-composed sites hit the same ordering and
102
+ * can call this directly with `cdn.getDistribution()`.
103
+ *
104
+ * A bucket deploys one asset set: calling this when a deployment already
105
+ * exists (constructor `deployment` config, or a prior call) throws rather
106
+ * than stacking a second BucketDeployment whose prune semantics would
107
+ * fight the first.
108
+ *
109
+ * @param distribution When set, the deployment invalidates it (`/*`) after
110
+ * upload, so the new assets are served immediately instead of after the
111
+ * cache TTL (up to 24h) expires.
112
+ */
113
+ deployAssets(config, distribution) {
114
+ if (this.bucketDeployment !== undefined) {
115
+ throw new Error(`Storage '${this.node.id}' already has a bucket deployment — a ` +
116
+ "bucket deploys one asset set. Pass everything in one " +
117
+ "deployment config instead of calling deployAssets twice (or " +
118
+ "combining it with the constructor's `deployment`).");
119
+ }
120
+ this.bucketDeployment = this.createDeployment(config, distribution);
121
+ return this.bucketDeployment;
122
+ }
123
+ createDeployment(config, distribution) {
96
124
  const cacheControlHeaders = [];
97
125
  if (config.cacheControl?.maxAge !== undefined) {
98
126
  cacheControlHeaders.push(CacheControl.maxAge(Duration.seconds(config.cacheControl.maxAge)));
@@ -100,12 +128,16 @@ export class Storage extends Construct {
100
128
  if (config.cacheControl?.immutable) {
101
129
  cacheControlHeaders.push(CacheControl.immutable());
102
130
  }
103
- return new BucketDeployment(this, `${id}Deployment`, {
131
+ return new BucketDeployment(this, `${this.node.id}Deployment`, {
104
132
  sources: [Source.asset(config.source)],
105
133
  destinationBucket: this.bucket,
106
134
  prune: config.prune ?? true,
107
135
  ...(cacheControlHeaders.length > 0 && {
108
136
  cacheControl: cacheControlHeaders
137
+ }),
138
+ ...(distribution !== undefined && {
139
+ distribution,
140
+ distributionPaths: ["/*"]
109
141
  })
110
142
  });
111
143
  }
@@ -124,6 +156,11 @@ export class Storage extends Construct {
124
156
  description: `S3 Bucket Name for ${id}`
125
157
  });
126
158
  }
159
+ /**
160
+ * The concrete bucket (narrowed from `IStorage`'s `IBucket`), so
161
+ * mutating escape hatches — `addLifecycleRule`, `addCorsRule` — are
162
+ * reachable without a cast.
163
+ */
127
164
  getBucket() {
128
165
  return this.bucket;
129
166
  }
@@ -1,4 +1,5 @@
1
1
  import { Construct } from "constructs";
2
+ import { type CloudFrontPriceClass } from "@fjall/util";
2
3
  import { Distribution, type ICachePolicy, type IResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";
3
4
  import { type IBucket } from "aws-cdk-lib/aws-s3";
4
5
  import { type IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
@@ -59,7 +60,7 @@ export interface CloudFrontDistributionProps {
59
60
  comment?: string;
60
61
  enableLogging?: boolean;
61
62
  logBucket?: IBucket;
62
- priceClass?: "PriceClass_100" | "PriceClass_200" | "PriceClass_All";
63
+ priceClass?: CloudFrontPriceClass;
63
64
  /** Adds a CloudFront Function to copy the viewer Host header into X-Forwarded-Host.
64
65
  * Required when the origin is a Lambda Function URL or ALB behind CloudFront,
65
66
  * because AllViewerExceptHostHeader replaces Host with the origin domain. */
@@ -6,17 +6,8 @@
6
6
  import { appendFileSync, existsSync, mkdirSync } from "fs";
7
7
  import { join } from "path";
8
8
  import { fjallLogDir } from "@fjall/util";
9
- import { FileRotator, LOG_ROTATION_DEFAULTS } from "@fjall/util/logRotation";
9
+ import { FileRotator, resolveLogRotationBudget } from "@fjall/util/logRotation";
10
10
  const LOG_FILENAME = "infrastructure.jsonl";
11
- /**
12
- * Shared with the CLI's own logs, so this file cannot outgrow the budget
13
- * the rest of Fjall's logging keeps to. Without it this log had no bound of
14
- * any kind: it is appended to on every CDK synth, and one developer machine
15
- * reached 267MB of it inside a single release cycle.
16
- */
17
- const rotator = new FileRotator({
18
- maxFiles: LOG_ROTATION_DEFAULTS.MAX_FILES
19
- });
20
11
  /**
21
12
  * Write a log entry to the infrastructure JSONL file
22
13
  */
@@ -36,7 +27,14 @@ function writeToLog(level, message) {
36
27
  source: "cdk-subprocess"
37
28
  });
38
29
  const logPath = join(logDir, LOG_FILENAME);
39
- rotator.rotateIfNeeded(logPath, LOG_ROTATION_DEFAULTS.MAX_FILE_SIZE);
30
+ // Budget shared with the CLI's own logs — same defaults, same
31
+ // FJALL_LOG_MAX_SIZE / FJALL_LOG_MAX_FILES overrides — so this file
32
+ // cannot outgrow what the rest of Fjall's logging keeps to. It reached
33
+ // 267MB on one developer machine when it had no bound. Resolved per
34
+ // write for the same reason as the log dir above: CDK imports this
35
+ // module long before a command's environment is readable.
36
+ const { maxFileSize, maxFiles } = resolveLogRotationBudget();
37
+ new FileRotator({ maxFiles }).rotateIfNeeded(logPath, maxFileSize);
40
38
  appendFileSync(logPath, entry + "\n");
41
39
  }
42
40
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "23.0.0",
3
+ "version": "25.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -80,9 +80,10 @@
80
80
  },
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-organizations": "^3.1098.0",
83
- "@fjall/generator": "^23.0.0",
84
- "@fjall/util": "^23.0.0",
85
- "constructs": "^10.7.2"
83
+ "@fjall/generator": "^25.0.0",
84
+ "@fjall/util": "^25.0.0",
85
+ "constructs": "^10.7.2",
86
+ "zod": "^4.4.3"
86
87
  },
87
88
  "overrides": {
88
89
  "@smithy/core": "2.5.5"