@fjall/components-infrastructure 7.3.0 → 8.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 (44) hide show
  1. package/dist/lib/app.d.ts +12 -0
  2. package/dist/lib/app.js +25 -5
  3. package/dist/lib/config/aws/ipam.js +4 -3
  4. package/dist/lib/patterns/aws/cdn.d.ts +7 -1
  5. package/dist/lib/patterns/aws/cdn.js +9 -2
  6. package/dist/lib/patterns/aws/interfaces/domain.d.ts +6 -0
  7. package/dist/lib/patterns/aws/storage.js +3 -0
  8. package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +9 -4
  9. package/dist/lib/patterns/aws/targets/fjallTargets.js +11 -5
  10. package/dist/lib/patterns/aws/targets/targetResolution.d.ts +33 -40
  11. package/dist/lib/patterns/aws/targets/targetResolution.js +42 -52
  12. package/dist/lib/resources/aws/backup/backupVault.js +2 -0
  13. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +9 -0
  14. package/dist/lib/resources/aws/cdn/cloudFront.js +2 -1
  15. package/dist/lib/resources/aws/compute/ecs.js +4 -3
  16. package/dist/lib/resources/aws/compute/ecsNetworking.js +27 -9
  17. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -1
  18. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +6 -2
  19. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +2 -1
  20. package/dist/lib/resources/aws/database/rdsHelpers.js +12 -2
  21. package/dist/lib/resources/aws/database/rdsInstance.js +2 -1
  22. package/dist/lib/resources/aws/database/rdsProxyOutput.js +2 -1
  23. package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
  24. package/dist/lib/resources/aws/networking/ipamPool.js +2 -1
  25. package/dist/lib/resources/aws/networking/vpc.js +6 -1
  26. package/dist/lib/resources/aws/secrets/kms.d.ts +24 -7
  27. package/dist/lib/resources/aws/secrets/kms.js +27 -5
  28. package/dist/lib/resources/aws/secrets/parameter.js +3 -1
  29. package/dist/lib/resources/aws/secrets/secret.js +4 -1
  30. package/dist/lib/resources/aws/storage/ecr.d.ts +6 -0
  31. package/dist/lib/resources/aws/storage/ecr.js +8 -4
  32. package/dist/lib/resources/aws/storage/s3.d.ts +10 -0
  33. package/dist/lib/resources/aws/storage/s3.js +9 -4
  34. package/dist/lib/utils/albAliasTargetRegistry.d.ts +44 -0
  35. package/dist/lib/utils/albAliasTargetRegistry.js +69 -0
  36. package/dist/lib/utils/env.d.ts +9 -0
  37. package/dist/lib/utils/env.js +12 -0
  38. package/dist/lib/utils/exportNaming.d.ts +50 -0
  39. package/dist/lib/utils/exportNaming.js +52 -0
  40. package/dist/lib/utils/orgConfigParser.d.ts +3 -8
  41. package/dist/lib/utils/orgConfigParser.js +4 -14
  42. package/dist/lib/utils/removalPolicy.d.ts +11 -3
  43. package/dist/lib/utils/removalPolicy.js +20 -5
  44. package/package.json +5 -4
package/dist/lib/app.d.ts CHANGED
@@ -139,6 +139,18 @@ export declare class App extends CdkApp {
139
139
  * Retrieve default network stack - named as `${this.name}Network`
140
140
  */
141
141
  getDefaultNetworkStack(): AwsStack;
142
+ /**
143
+ * The Network stack as a dependency, or `undefined` when the app is
144
+ * networkless.
145
+ *
146
+ * `getDefaultNetworkStack()` CREATES the stack as a side effect, so naming
147
+ * it unconditionally as a dependency is what makes a networkless app
148
+ * synthesise an empty `<App>Network` — zero resources, which CloudFormation
149
+ * rejects outright ("Template format error: At least one Resources member
150
+ * must be defined"). Synth is green, `cdk deploy` is not, so nothing before
151
+ * the deploy can tell.
152
+ */
153
+ private networkDependency;
142
154
  /**
143
155
  * Retrieve default database stack - named as `${this.name}Database`
144
156
  */
package/dist/lib/app.js CHANGED
@@ -12,6 +12,7 @@ import { ServiceDiscoveryNamespace } from "./resources/aws/networking/serviceDis
12
12
  import { BuildkiteFactory } from "./patterns/aws/buildkite.js";
13
13
  import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
14
14
  import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
15
+ import { emitAppLevelAlbAliasExports } from "./utils/albAliasTargetRegistry.js";
15
16
  import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
16
17
  import { randomBytes } from "crypto";
17
18
  import { getConfig } from "./utils/getConfig.js";
@@ -203,7 +204,7 @@ export class App extends CdkApp {
203
204
  * by CDK when compute resources reference database resources.
204
205
  */
205
206
  getDefaultComputeStack() {
206
- return this.getStack(`${this.stackPrefix}Compute`, this.getDefaultNetworkStack());
207
+ return this.getStack(`${this.stackPrefix}Compute`, this.networkDependency());
207
208
  }
208
209
  /**
209
210
  * Retrieve default network stack - named as `${this.name}Network`
@@ -211,17 +212,31 @@ export class App extends CdkApp {
211
212
  getDefaultNetworkStack() {
212
213
  return this.getStack(`${this.stackPrefix}Network`);
213
214
  }
215
+ /**
216
+ * The Network stack as a dependency, or `undefined` when the app is
217
+ * networkless.
218
+ *
219
+ * `getDefaultNetworkStack()` CREATES the stack as a side effect, so naming
220
+ * it unconditionally as a dependency is what makes a networkless app
221
+ * synthesise an empty `<App>Network` — zero resources, which CloudFormation
222
+ * rejects outright ("Template format error: At least one Resources member
223
+ * must be defined"). Synth is green, `cdk deploy` is not, so nothing before
224
+ * the deploy can tell.
225
+ */
226
+ networkDependency() {
227
+ return this.networkDisabled ? undefined : this.getDefaultNetworkStack();
228
+ }
214
229
  /**
215
230
  * Retrieve default database stack - named as `${this.name}Database`
216
231
  */
217
232
  getDefaultDatabaseStack() {
218
- return this.getStack(`${this.stackPrefix}Database`, this.getDefaultNetworkStack());
233
+ return this.getStack(`${this.stackPrefix}Database`, this.networkDependency());
219
234
  }
220
235
  /**
221
236
  * Retrieve default storage stack - named as `${this.name}Storage`
222
237
  */
223
238
  getDefaultStorageStack() {
224
- return this.getStack(`${this.stackPrefix}Storage`, this.getDefaultNetworkStack());
239
+ return this.getStack(`${this.stackPrefix}Storage`, this.networkDependency());
225
240
  }
226
241
  /**
227
242
  * Retrieve default CDN stack - named as `${this.name}Cdn`
@@ -236,7 +251,9 @@ export class App extends CdkApp {
236
251
  // crossRegionReferences is enabled at creation so the Cdn stack can
237
252
  // consume a us-east-1 certificate (domain gold-plating D3, BYO path).
238
253
  // The flag is inert unless a cross-region reference is synthesised.
239
- return this.getStack(`${this.stackPrefix}Cdn`, this.networkDisabled ? undefined : this.getDefaultNetworkStack(), { crossRegionReferences: true });
254
+ return this.getStack(`${this.stackPrefix}Cdn`, this.networkDependency(), {
255
+ crossRegionReferences: true
256
+ });
240
257
  }
241
258
  /**
242
259
  * Retrieve default messaging stack - named as `${this.name}Messaging`
@@ -246,7 +263,7 @@ export class App extends CdkApp {
246
263
  * require VPC, but we maintain consistent stack dependency patterns.
247
264
  */
248
265
  getDefaultMessagingStack() {
249
- return this.getStack(`${this.stackPrefix}Messaging`, this.getDefaultNetworkStack());
266
+ return this.getStack(`${this.stackPrefix}Messaging`, this.networkDependency());
250
267
  }
251
268
  /**
252
269
  * Retrieve the app-owned us-east-1 certificate stack — named
@@ -816,6 +833,9 @@ export class App extends CdkApp {
816
833
  // attach directly to the App and never call getStack(), so without this
817
834
  // they synthesise untagged. Idempotent via the aspectApplied guard.
818
835
  this.applyTagsAspect();
836
+ // Must precede super.synth(): the tree is still mutable here, and only
837
+ // now is the ALB count per app final.
838
+ emitAppLevelAlbAliasExports(this);
819
839
  const assembly = super.synth(options);
820
840
  // After synthesis, write Fjall manifest to cdk.out
821
841
  try {
@@ -2,6 +2,7 @@ import { CfnOutput } from "aws-cdk-lib";
2
2
  import { Construct } from "constructs";
3
3
  import { Ipam as IpamClass } from "../../resources/aws/networking/ipam.js";
4
4
  import { getConfig } from "../../utils/getConfig.js";
5
+ import { IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME } from "../../utils/exportNaming.js";
5
6
  export class Ipam extends Construct {
6
7
  privateDefaultScopeId;
7
8
  constructor(scope, id, props) {
@@ -23,10 +24,10 @@ export class Ipam extends Construct {
23
24
  ]
24
25
  });
25
26
  this.privateDefaultScopeId = ipam.attrPrivateDefaultScopeId;
26
- new CfnOutput(this, "IpamPrivateDefaultScopeId", {
27
- key: "IpamPrivateDefaultScopeId",
27
+ new CfnOutput(this, IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME, {
28
+ key: IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME,
28
29
  value: ipam.attrPrivateDefaultScopeId,
29
- exportName: "IpamPrivateDefaultScopeId"
30
+ exportName: IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME
30
31
  });
31
32
  }
32
33
  }
@@ -13,6 +13,12 @@ import { type AnyCompute } from "./compute.js";
13
13
  * Common CDN props shared across all origin types.
14
14
  */
15
15
  interface BaseCdnProps {
16
+ /**
17
+ * App this CDN fronts, keying the export a `Domain` resolves with
18
+ * `fjallCdn(<appName>)`. `CdnFactory.build` fills it from the `App`, so it
19
+ * is only set by hand for a directly-instantiated `Cdn`.
20
+ */
21
+ appName?: string;
16
22
  cachePolicy?: CachePolicyPreset | ICachePolicy;
17
23
  defaultAllowedMethods?: "GET_HEAD" | "GET_HEAD_OPTIONS" | "ALL";
18
24
  behaviours?: SmartCdnBehaviour[];
@@ -151,5 +157,5 @@ export declare class CdnFactory {
151
157
  * @param props - CDN configuration properties
152
158
  * @returns Factory function that creates the CDN when invoked
153
159
  */
154
- static build(id: string, props: ICdnProps): (_app: App, scope: Construct) => Cdn;
160
+ static build(id: string, props: ICdnProps): (app: App, scope: Construct) => Cdn;
155
161
  }
@@ -82,6 +82,7 @@ export class Cdn extends CloudFrontDistribution {
82
82
  static resolveProps(scope, id, props) {
83
83
  const defaultOrigin = Cdn.resolveDefaultOrigin(props);
84
84
  const behaviours = Cdn.resolveBehaviours(props.behaviours);
85
+ const appName = props.appName;
85
86
  const certificate = props.certificate ??
86
87
  (props.certificateArn
87
88
  ? Certificate.fromCertificateArn(scope, `${id}Certificate`, props.certificateArn)
@@ -96,6 +97,7 @@ export class Cdn extends CloudFrontDistribution {
96
97
  }
97
98
  : {};
98
99
  return {
100
+ appName,
99
101
  defaultOrigin,
100
102
  defaultCachePolicy: props.cachePolicy,
101
103
  defaultAllowedMethods: props.defaultAllowedMethods,
@@ -273,8 +275,13 @@ export class CdnFactory {
273
275
  * @returns Factory function that creates the CDN when invoked
274
276
  */
275
277
  static build(id, props) {
276
- return (_app, scope) => {
277
- return new Cdn(scope, id, props);
278
+ return (app, scope) => {
279
+ // The App is the only place the app name is known here; without it the
280
+ // export falls back to the construct id and the Domain import misses.
281
+ return new Cdn(scope, id, {
282
+ ...props,
283
+ appName: props.appName ?? app.getName()
284
+ });
278
285
  };
279
286
  }
280
287
  }
@@ -11,6 +11,12 @@ export { getDomainExportNames } from "../../../utils/domainTypes.js";
11
11
  export type FjallTarget = {
12
12
  readonly kind: "ecs";
13
13
  readonly appName: string;
14
+ /**
15
+ * Names one ALB-bearing compute when the app fronts several. Absent
16
+ * means the app's sole ALB, which only exists as an export when the app
17
+ * has exactly one (lib/utils/albAliasTargetRegistry.ts).
18
+ */
19
+ readonly computeName?: string;
14
20
  } | {
15
21
  readonly kind: "cdn";
16
22
  readonly appName: string;
@@ -60,6 +60,9 @@ export class Storage extends Construct {
60
60
  : undefined;
61
61
  this.bucket = new S3Bucket(this, `${id}Bucket`, {
62
62
  bucketName: props.bucketName,
63
+ // The bucket nests one level below, so its own id is `${id}Bucket` — the
64
+ // author's handle for `fjallBucket(<name>)` is THIS construct's id.
65
+ aliasTargetName: props.bucketName ?? id,
63
66
  versioned: props.versioned,
64
67
  encryption: toBucketEncryption(props.encryption),
65
68
  encryptionKey,
@@ -10,11 +10,16 @@ export type FjallAliasTarget = FjallTarget & IAliasRecordTarget;
10
10
  /**
11
11
  * Target helper for a Fjall ECS/ALB-fronted app.
12
12
  *
13
- * Requires the app stack to publish exports `${safeApp}AlbDnsName` and
14
- * `${safeApp}AlbHostedZoneId`. Phase 2 wires these emitters; until then,
15
- * deploys fail at CloudFormation execution with "No export named found".
13
+ * With no `computeName`, requires the app stack to publish exports
14
+ * `${safeApp}AlbDnsName` and `${safeApp}AlbHostedZoneId` which `App.synth()`
15
+ * emits only for an app fronting exactly ONE ALB, since "the app's ALB" names
16
+ * nothing otherwise. Pass `computeName` for an app with several; that pair
17
+ * (`${safeApp}-${safeCompute}-Alb…`) is emitted unconditionally per compute.
18
+ *
19
+ * Naming an app that has no matching export fails at CloudFormation execution
20
+ * with "No export named … found", after synth and every unit test pass.
16
21
  */
17
- export declare function fjallApp(appName: string): FjallAliasTarget;
22
+ export declare function fjallApp(appName: string, computeName?: string): FjallAliasTarget;
18
23
  /**
19
24
  * Target helper for a Fjall CloudFront-fronted app.
20
25
  *
@@ -2,15 +2,21 @@ import { resolveBucketTarget, resolveCdnTarget, resolveCustomTarget, resolveEcsT
2
2
  /**
3
3
  * Target helper for a Fjall ECS/ALB-fronted app.
4
4
  *
5
- * Requires the app stack to publish exports `${safeApp}AlbDnsName` and
6
- * `${safeApp}AlbHostedZoneId`. Phase 2 wires these emitters; until then,
7
- * deploys fail at CloudFormation execution with "No export named found".
5
+ * With no `computeName`, requires the app stack to publish exports
6
+ * `${safeApp}AlbDnsName` and `${safeApp}AlbHostedZoneId` which `App.synth()`
7
+ * emits only for an app fronting exactly ONE ALB, since "the app's ALB" names
8
+ * nothing otherwise. Pass `computeName` for an app with several; that pair
9
+ * (`${safeApp}-${safeCompute}-Alb…`) is emitted unconditionally per compute.
10
+ *
11
+ * Naming an app that has no matching export fails at CloudFormation execution
12
+ * with "No export named … found", after synth and every unit test pass.
8
13
  */
9
- export function fjallApp(appName) {
10
- const resolved = resolveEcsTarget(appName);
14
+ export function fjallApp(appName, computeName) {
15
+ const resolved = resolveEcsTarget(appName, computeName);
11
16
  return {
12
17
  kind: "ecs",
13
18
  appName,
19
+ ...(computeName !== undefined && { computeName }),
14
20
  bind(record, zone) {
15
21
  return resolved.bind(record, zone);
16
22
  }
@@ -1,64 +1,57 @@
1
1
  import { type IAliasRecordTarget } from "aws-cdk-lib/aws-route53";
2
2
  import type { FjallTarget } from "../interfaces/domain.js";
3
3
  /**
4
- * Cross-phase export-name contract.
4
+ * Producer-consumer export-name contract for `Domain` alias targets.
5
5
  *
6
- * Phase 1 DEFINES these export names; Phase 2 is responsible for wiring the
7
- * emitting stacks (`computeEcs.ts`, `cdn.ts`, `storage.ts`) to publish each
8
- * `CfnOutput` with the matching `exportName`. Until Phase 2 lands the
9
- * producers, a deploy of any `Domain` with a typed target fails at
10
- * CloudFormation execution with "No export named … found" — the helpers
11
- * compile and synthesise, but the cross-stack import resolves at deploy.
6
+ * Each resolver below imports a value a *different* stack exported. The two
7
+ * sides agree only if they compose the same string, and CloudFormation is the
8
+ * first thing that checks: a mismatch synthesises, typechecks and unit-tests
9
+ * clean, then fails the deploy with "No export named found".
12
10
  *
13
- * Producer-consumer matrix (see Phase 1 plan §3, D6 / T0.5):
11
+ * | Producer | Consumer | Export name |
12
+ * |-------------------------------------|-----------------------|-----------------------------------------|
13
+ * | `App.synth()` (sole-ALB apps only) | `resolveEcsTarget` | `<App>AlbDnsName` |
14
+ * | `App.synth()` (sole-ALB apps only) | `resolveEcsTarget` | `<App>AlbHostedZoneId` |
15
+ * | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbDnsName` |
16
+ * | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbHostedZoneId` |
17
+ * | `cloudFront.ts` CDN stack | `resolveCdnTarget` | `<App>CdnDistributionDomainName` |
18
+ * | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket>WebsiteEndpoint` |
19
+ * | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket>WebsiteHostedZoneId` |
14
20
  *
15
- * | Producer (Phase 2) | Consumer (Phase 1) | Export name |
16
- * |-----------------------------|-------------------------|------------------------------------------|
17
- * | `computeEcs.ts` app stack | `resolveEcsTarget` | `${safeApp}AlbDnsName` |
18
- * | `computeEcs.ts` app stack | `resolveEcsTarget` | `${safeApp}AlbHostedZoneId` |
19
- * | `cdn.ts` CDN stack | `resolveCdnTarget` | `${safeApp}CdnDistributionDomainName` |
20
- * | `storage.ts` bucket stack | `resolveBucketTarget` | `${safeBucket}WebsiteEndpoint` |
21
- * | `storage.ts` bucket stack | `resolveBucketTarget` | `${safeBucket}WebsiteHostedZoneId` |
21
+ * Both sides derive the name from `@fjall/util`'s `…ExportName` helpers, which
22
+ * take the RAW app or bucket name and sanitise internally. Compose these
23
+ * strings by hand on either side and the pair silently drifts: an earlier
24
+ * contract standardised only the suffix and took a pre-sanitised token, so a
25
+ * producer keyed on its construct id emitted `MyAppCdnCdnDistributionDomainName`
26
+ * against a consumer asking for `MyAppCdnDistributionDomainName`, and a bucket
27
+ * named `www.example.com` was imported as a name CloudFormation cannot hold.
28
+ * Parity is pinned by `lib/__tests__/targetExportParity.test.ts`, which reads
29
+ * both names out of synthesised templates rather than from the helpers.
22
30
  */
23
- export declare const EXPORT_NAMES: {
24
- readonly ALB_DNS: (safeApp: string) => string;
25
- readonly ALB_HOSTED_ZONE_ID: (safeApp: string) => string;
26
- readonly CDN_DOMAIN: (safeApp: string) => string;
27
- readonly BUCKET_WEBSITE_ENDPOINT: (safeBucket: string) => string;
28
- readonly BUCKET_WEBSITE_HZ_ID: (safeBucket: string) => string;
29
- };
30
31
  /** Route53-reserved hosted-zone id for CloudFront distributions. */
31
32
  export declare const CLOUDFRONT_HOSTED_ZONE_ID = "Z2FDTNDATAQYW2";
32
33
  /**
33
34
  * Resolve a Fjall ECS/ALB-fronted app to an `IAliasRecordTarget`.
34
35
  *
35
- * Requires the app stack to publish exports:
36
- * - `${safeApp}AlbDnsName`
37
- * - `${safeApp}AlbHostedZoneId`
38
- *
39
- * Phase 2 wires the emitters; until then, deploys of typed targets fail with
40
- * "No export named … found".
36
+ * `appName` is the app's name, NOT the cluster or construct id — the producer
37
+ * in `ecsNetworking.ts` keys on the same value. `computeName` names one of
38
+ * several ALB-bearing computes; omit it for an app whose sole ALB is exported
39
+ * unqualified by `App.synth()`.
41
40
  */
42
- export declare function resolveEcsTarget(appName: string): IAliasRecordTarget;
41
+ export declare function resolveEcsTarget(appName: string, computeName?: string): IAliasRecordTarget;
43
42
  /**
44
43
  * Resolve a Fjall CloudFront-fronted app to an `IAliasRecordTarget`.
45
44
  *
46
- * Requires the CDN stack to publish export `${safeApp}CdnDistributionDomainName`.
47
- * The CloudFront hosted-zone id is the globally fixed `Z2FDTNDATAQYW2`.
48
- *
49
- * Phase 2 wires the emitter; until then, deploys of typed targets fail with
50
- * "No export named … found".
45
+ * `appName` is the app's name, NOT the CDN construct id (`fjall add cdn`
46
+ * defaults that to `<App>Cdn`). The CloudFront hosted-zone id is the globally
47
+ * fixed `Z2FDTNDATAQYW2`.
51
48
  */
52
49
  export declare function resolveCdnTarget(appName: string): IAliasRecordTarget;
53
50
  /**
54
51
  * Resolve a Fjall S3 static-site bucket to an `IAliasRecordTarget`.
55
52
  *
56
- * Requires the bucket stack to publish exports:
57
- * - `${safeBucket}WebsiteEndpoint`
58
- * - `${safeBucket}WebsiteHostedZoneId`
59
- *
60
- * Phase 2 wires the emitters; until then, deploys of typed targets fail with
61
- * "No export named … found".
53
+ * `bucketName` is the bucket's name as written, dots and all — the helper
54
+ * strips what CloudFormation will not hold, and the producer strips the same.
62
55
  */
63
56
  export declare function resolveBucketTarget(bucketName: string): IAliasRecordTarget;
64
57
  /**
@@ -1,32 +1,33 @@
1
1
  import { Fn } from "aws-cdk-lib";
2
- import { toPascalCase } from "../../../utils/capitaliseString.js";
2
+ import { albDnsExportName, albHostedZoneIdExportName, cdnDomainExportName, bucketWebsiteEndpointExportName, bucketWebsiteHostedZoneIdExportName } from "@fjall/util";
3
3
  /**
4
- * Cross-phase export-name contract.
4
+ * Producer-consumer export-name contract for `Domain` alias targets.
5
5
  *
6
- * Phase 1 DEFINES these export names; Phase 2 is responsible for wiring the
7
- * emitting stacks (`computeEcs.ts`, `cdn.ts`, `storage.ts`) to publish each
8
- * `CfnOutput` with the matching `exportName`. Until Phase 2 lands the
9
- * producers, a deploy of any `Domain` with a typed target fails at
10
- * CloudFormation execution with "No export named … found" — the helpers
11
- * compile and synthesise, but the cross-stack import resolves at deploy.
6
+ * Each resolver below imports a value a *different* stack exported. The two
7
+ * sides agree only if they compose the same string, and CloudFormation is the
8
+ * first thing that checks: a mismatch synthesises, typechecks and unit-tests
9
+ * clean, then fails the deploy with "No export named found".
12
10
  *
13
- * Producer-consumer matrix (see Phase 1 plan §3, D6 / T0.5):
11
+ * | Producer | Consumer | Export name |
12
+ * |-------------------------------------|-----------------------|-----------------------------------------|
13
+ * | `App.synth()` (sole-ALB apps only) | `resolveEcsTarget` | `<App>AlbDnsName` |
14
+ * | `App.synth()` (sole-ALB apps only) | `resolveEcsTarget` | `<App>AlbHostedZoneId` |
15
+ * | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbDnsName` |
16
+ * | `ecsNetworking.ts` app stack | `resolveEcsTarget` | `<App>-<Compute>-AlbHostedZoneId` |
17
+ * | `cloudFront.ts` CDN stack | `resolveCdnTarget` | `<App>CdnDistributionDomainName` |
18
+ * | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket>WebsiteEndpoint` |
19
+ * | `s3.ts` bucket stack | `resolveBucketTarget` | `<Bucket>WebsiteHostedZoneId` |
14
20
  *
15
- * | Producer (Phase 2) | Consumer (Phase 1) | Export name |
16
- * |-----------------------------|-------------------------|------------------------------------------|
17
- * | `computeEcs.ts` app stack | `resolveEcsTarget` | `${safeApp}AlbDnsName` |
18
- * | `computeEcs.ts` app stack | `resolveEcsTarget` | `${safeApp}AlbHostedZoneId` |
19
- * | `cdn.ts` CDN stack | `resolveCdnTarget` | `${safeApp}CdnDistributionDomainName` |
20
- * | `storage.ts` bucket stack | `resolveBucketTarget` | `${safeBucket}WebsiteEndpoint` |
21
- * | `storage.ts` bucket stack | `resolveBucketTarget` | `${safeBucket}WebsiteHostedZoneId` |
21
+ * Both sides derive the name from `@fjall/util`'s `…ExportName` helpers, which
22
+ * take the RAW app or bucket name and sanitise internally. Compose these
23
+ * strings by hand on either side and the pair silently drifts: an earlier
24
+ * contract standardised only the suffix and took a pre-sanitised token, so a
25
+ * producer keyed on its construct id emitted `MyAppCdnCdnDistributionDomainName`
26
+ * against a consumer asking for `MyAppCdnDistributionDomainName`, and a bucket
27
+ * named `www.example.com` was imported as a name CloudFormation cannot hold.
28
+ * Parity is pinned by `lib/__tests__/targetExportParity.test.ts`, which reads
29
+ * both names out of synthesised templates rather than from the helpers.
22
30
  */
23
- export const EXPORT_NAMES = {
24
- ALB_DNS: (safeApp) => `${safeApp}AlbDnsName`,
25
- ALB_HOSTED_ZONE_ID: (safeApp) => `${safeApp}AlbHostedZoneId`,
26
- CDN_DOMAIN: (safeApp) => `${safeApp}CdnDistributionDomainName`,
27
- BUCKET_WEBSITE_ENDPOINT: (safeBucket) => `${safeBucket}WebsiteEndpoint`,
28
- BUCKET_WEBSITE_HZ_ID: (safeBucket) => `${safeBucket}WebsiteHostedZoneId`
29
- };
30
31
  /** Route53-reserved hosted-zone id for CloudFront distributions. */
31
32
  export const CLOUDFRONT_HOSTED_ZONE_ID = "Z2FDTNDATAQYW2";
32
33
  /**
@@ -44,47 +45,36 @@ function buildAliasTarget(dnsName, hostedZoneId) {
44
45
  /**
45
46
  * Resolve a Fjall ECS/ALB-fronted app to an `IAliasRecordTarget`.
46
47
  *
47
- * Requires the app stack to publish exports:
48
- * - `${safeApp}AlbDnsName`
49
- * - `${safeApp}AlbHostedZoneId`
50
- *
51
- * Phase 2 wires the emitters; until then, deploys of typed targets fail with
52
- * "No export named … found".
48
+ * `appName` is the app's name, NOT the cluster or construct id — the producer
49
+ * in `ecsNetworking.ts` keys on the same value. `computeName` names one of
50
+ * several ALB-bearing computes; omit it for an app whose sole ALB is exported
51
+ * unqualified by `App.synth()`.
53
52
  */
54
- export function resolveEcsTarget(appName) {
55
- const safeApp = toPascalCase(appName);
56
- const dnsName = Fn.importValue(EXPORT_NAMES.ALB_DNS(safeApp));
57
- const hostedZoneId = Fn.importValue(EXPORT_NAMES.ALB_HOSTED_ZONE_ID(safeApp));
53
+ export function resolveEcsTarget(appName, computeName) {
54
+ const dnsName = Fn.importValue(albDnsExportName(appName, computeName));
55
+ const hostedZoneId = Fn.importValue(albHostedZoneIdExportName(appName, computeName));
58
56
  return buildAliasTarget(dnsName, hostedZoneId);
59
57
  }
60
58
  /**
61
59
  * Resolve a Fjall CloudFront-fronted app to an `IAliasRecordTarget`.
62
60
  *
63
- * Requires the CDN stack to publish export `${safeApp}CdnDistributionDomainName`.
64
- * The CloudFront hosted-zone id is the globally fixed `Z2FDTNDATAQYW2`.
65
- *
66
- * Phase 2 wires the emitter; until then, deploys of typed targets fail with
67
- * "No export named … found".
61
+ * `appName` is the app's name, NOT the CDN construct id (`fjall add cdn`
62
+ * defaults that to `<App>Cdn`). The CloudFront hosted-zone id is the globally
63
+ * fixed `Z2FDTNDATAQYW2`.
68
64
  */
69
65
  export function resolveCdnTarget(appName) {
70
- const safeApp = toPascalCase(appName);
71
- const dnsName = Fn.importValue(EXPORT_NAMES.CDN_DOMAIN(safeApp));
66
+ const dnsName = Fn.importValue(cdnDomainExportName(appName));
72
67
  return buildAliasTarget(dnsName, CLOUDFRONT_HOSTED_ZONE_ID);
73
68
  }
74
69
  /**
75
70
  * Resolve a Fjall S3 static-site bucket to an `IAliasRecordTarget`.
76
71
  *
77
- * Requires the bucket stack to publish exports:
78
- * - `${safeBucket}WebsiteEndpoint`
79
- * - `${safeBucket}WebsiteHostedZoneId`
80
- *
81
- * Phase 2 wires the emitters; until then, deploys of typed targets fail with
82
- * "No export named … found".
72
+ * `bucketName` is the bucket's name as written, dots and all — the helper
73
+ * strips what CloudFormation will not hold, and the producer strips the same.
83
74
  */
84
75
  export function resolveBucketTarget(bucketName) {
85
- const safeBucket = toPascalCase(bucketName);
86
- const dnsName = Fn.importValue(EXPORT_NAMES.BUCKET_WEBSITE_ENDPOINT(safeBucket));
87
- const hostedZoneId = Fn.importValue(EXPORT_NAMES.BUCKET_WEBSITE_HZ_ID(safeBucket));
76
+ const dnsName = Fn.importValue(bucketWebsiteEndpointExportName(bucketName));
77
+ const hostedZoneId = Fn.importValue(bucketWebsiteHostedZoneIdExportName(bucketName));
88
78
  return buildAliasTarget(dnsName, hostedZoneId);
89
79
  }
90
80
  /**
@@ -104,11 +94,11 @@ export function resolveCustomTarget(dnsName, hostedZoneId) {
104
94
  export function resolveTargetToDnsName(target) {
105
95
  switch (target.kind) {
106
96
  case "ecs":
107
- return Fn.importValue(EXPORT_NAMES.ALB_DNS(toPascalCase(target.appName)));
97
+ return Fn.importValue(albDnsExportName(target.appName, target.computeName));
108
98
  case "cdn":
109
- return Fn.importValue(EXPORT_NAMES.CDN_DOMAIN(toPascalCase(target.appName)));
99
+ return Fn.importValue(cdnDomainExportName(target.appName));
110
100
  case "bucket":
111
- return Fn.importValue(EXPORT_NAMES.BUCKET_WEBSITE_ENDPOINT(toPascalCase(target.bucketName)));
101
+ return Fn.importValue(bucketWebsiteEndpointExportName(target.bucketName));
112
102
  case "custom":
113
103
  return target.dnsName;
114
104
  default: {
@@ -15,6 +15,8 @@ export class BackupVault extends Construct {
15
15
  new CustomerManagedKey(this, `${props.vaultName}Key`, {
16
16
  description: `Encryption key for backup vault ${props.vaultName}`,
17
17
  aliasName: `cmk/backupVault/${props.vaultName}`,
18
+ // Recovery points in a retained vault outlive the stack.
19
+ protects: "outlives-stack",
18
20
  removalPolicy
19
21
  });
20
22
  this.vault = new Vault(this, `${props.vaultName}Vault`, {
@@ -39,6 +39,15 @@ export interface AccessGateConfig {
39
39
  password: string;
40
40
  }
41
41
  export interface CloudFrontDistributionProps {
42
+ /**
43
+ * App this CDN fronts. Keys the cross-stack export a `Domain` resolves via
44
+ * `fjallCdn(<appName>)`. Falls back to the construct id, which is what the
45
+ * consumer previously disagreed with: `fjall add cdn` defaults the id to
46
+ * `${toPascalCase(appName)}Cdn`, so the producer emitted
47
+ * `MyAppCdnCdnDistributionDomainName` for a consumer asking for
48
+ * `MyAppCdnDistributionDomainName`.
49
+ */
50
+ appName?: string;
42
51
  defaultOrigin: CdnOriginConfig;
43
52
  defaultCachePolicy?: CachePolicyPreset | ICachePolicy;
44
53
  defaultAllowedMethods?: "GET_HEAD" | "GET_HEAD_OPTIONS" | "ALL";
@@ -1,6 +1,7 @@
1
1
  import { Construct } from "constructs";
2
2
  import { CfnOutput } from "aws-cdk-lib";
3
3
  import { toPascalCase } from "../../../utils/capitaliseString.js";
4
+ import { cdnDomainExportName } from "@fjall/util";
4
5
  import { Distribution, PriceClass, ViewerProtocolPolicy, CachePolicy, OriginRequestPolicy, AllowedMethods, OriginProtocolPolicy, Function as CloudFrontFunction, FunctionCode, FunctionRuntime, FunctionEventType } from "aws-cdk-lib/aws-cloudfront";
5
6
  import { S3BucketOrigin, LoadBalancerV2Origin, HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
6
7
  export class CloudFrontDistribution extends Construct {
@@ -70,7 +71,7 @@ export class CloudFrontDistribution extends Construct {
70
71
  });
71
72
  new CfnOutput(this, `${outputName}DistributionDomainName`, {
72
73
  key: `${outputName}DistributionDomainName`,
73
- exportName: `${outputName}CdnDistributionDomainName`,
74
+ exportName: cdnDomainExportName(props.appName ?? id),
74
75
  value: this.distribution.distributionDomainName,
75
76
  description: `CloudFront distribution domain name for ${id}`
76
77
  });
@@ -3,6 +3,7 @@ import { Connections, Port } from "aws-cdk-lib/aws-ec2";
3
3
  import { Construct } from "constructs";
4
4
  import { CfnOutput, Aspects } from "aws-cdk-lib";
5
5
  import { processConnections } from "../../../utils/connections.js";
6
+ import { stackScopedExportName } from "../../../utils/exportNaming.js";
6
7
  import { toPascalCase } from "../../../utils/capitaliseString.js";
7
8
  import { createEcsServiceAlarms, createLogPatternAlarms } from "../monitoring/index.js";
8
9
  // Extracted modules
@@ -307,7 +308,7 @@ export default class EcsCluster extends Construct {
307
308
  const safeServiceName = toPascalCase(serviceName);
308
309
  new CfnOutput(this, `${this.outputName}${safeServiceName}DeployableService`, {
309
310
  key: `${this.outputName}${safeServiceName}DeployableService`,
310
- exportName: `${props.clusterName}${serviceName}DeployableService`,
311
+ exportName: stackScopedExportName(this, `${props.clusterName}${serviceName}DeployableService`),
311
312
  value: serviceData.service.serviceArn,
312
313
  description: `Deployable ECS Service ARN for ${serviceName} in ${props.clusterName}`
313
314
  });
@@ -331,12 +332,12 @@ export default class EcsCluster extends Construct {
331
332
  });
332
333
  new CfnOutput(this, `${this.outputName}DeployableCluster`, {
333
334
  key: `${this.outputName}DeployableCluster`,
334
- exportName: `${props.clusterName}DeployableCluster`,
335
+ exportName: stackScopedExportName(this, `${props.clusterName}DeployableCluster`),
335
336
  value: cluster.clusterArn
336
337
  });
337
338
  new CfnOutput(this, `${this.outputName}ClusterArn`, {
338
339
  key: `${this.outputName}ClusterArn`,
339
- exportName: `${props.clusterName}ClusterArn`,
340
+ exportName: stackScopedExportName(this, `${props.clusterName}ClusterArn`),
340
341
  value: cluster.clusterArn,
341
342
  description: `ECS Cluster ARN for ${props.clusterName}`
342
343
  });