@fjall/components-infrastructure 2.34.0 → 3.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 (62) hide show
  1. package/dist/lib/app.d.ts +19 -2
  2. package/dist/lib/app.js +46 -3
  3. package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
  4. package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
  5. package/dist/lib/patterns/aws/cdn.js +25 -10
  6. package/dist/lib/patterns/aws/computeEcs.js +6 -1
  7. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -3
  8. package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
  9. package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
  10. package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
  11. package/dist/lib/patterns/aws/devSubstrate.js +18 -8
  12. package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
  13. package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
  14. package/dist/lib/patterns/aws/domain.d.ts +3 -2
  15. package/dist/lib/patterns/aws/domain.js +3 -2
  16. package/dist/lib/patterns/aws/domainValidation.js +71 -46
  17. package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
  18. package/dist/lib/patterns/aws/index.d.ts +1 -3
  19. package/dist/lib/patterns/aws/index.js +2 -3
  20. package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
  21. package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
  22. package/dist/lib/patterns/aws/interfaces/index.js +3 -0
  23. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
  24. package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
  25. package/dist/lib/patterns/aws/patternDomain.js +198 -0
  26. package/dist/lib/patterns/aws/payload.js +23 -30
  27. package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
  28. package/dist/lib/patterns/aws/staticSite.js +28 -33
  29. package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
  30. package/dist/lib/resources/aws/base/awsStack.js +20 -17
  31. package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
  32. package/dist/lib/resources/aws/compute/ecs.js +1 -1
  33. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
  34. package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
  35. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
  36. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
  37. package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
  38. package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
  39. package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
  40. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
  41. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
  42. package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
  43. package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
  44. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
  45. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
  46. package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
  47. package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
  48. package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
  49. package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
  50. package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
  51. package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
  52. package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
  53. package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
  54. package/dist/lib/utils/costAllocationTags.d.ts +9 -0
  55. package/dist/lib/utils/costAllocationTags.js +6 -1
  56. package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
  57. package/dist/lib/utils/dnsRecordRegistry.js +64 -0
  58. package/dist/lib/utils/domainTypes.d.ts +43 -36
  59. package/dist/lib/utils/domainTypes.js +65 -2
  60. package/dist/lib/utils/managedDomainContext.d.ts +37 -0
  61. package/dist/lib/utils/managedDomainContext.js +111 -0
  62. package/package.json +4 -4
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Explicit zone identity + CloudFront certificate resolution shared by the
3
+ * CloudFront-fronted patterns (staticsite, payload).
4
+ *
5
+ * Design 2026-07-17 domain-management gold-plating — decisions D2 (synth
6
+ * side) and D3, findings H2 and H3:
7
+ *
8
+ * - Nothing here guesses a hosted zone. The old `extractRootDomain` label
9
+ * split resolved every multi-label public suffix wrongly (e.g.
10
+ * 'littlebearearthworks.com.au' → zone 'com.au') and mis-placed delegated
11
+ * sub-zone records; it is deleted. Zone identity now arrives explicitly, in
12
+ * priority order: a CLI-injected {@link ManagedDomainBinding} (literal
13
+ * values — crosses accounts and regions), the legacy
14
+ * {@link ManagedDomainExports} form (Fn.importValue — same-account,
15
+ * same-region only), or the pattern's own `zoneName`/`hostedZoneId` props.
16
+ * No identity at all is a synth-time error naming the cures.
17
+ * - Every path validates the requested domain sits within the declared zone
18
+ * (dot-boundary `isWithinZone`), so a mismatch throws at synth instead of
19
+ * silently synthesising a record for the wrong FQDN.
20
+ * - CloudFront accepts viewer certificates from us-east-1 only. A binding
21
+ * carries the domain stack's us-east-1 certificate ARN as an inspectable
22
+ * literal (asserted here at synth); the BYO and legacy-exports paths
23
+ * provision an app-owned DNS-validated certificate — in-stack when the app
24
+ * itself deploys to us-east-1, otherwise in the app-owned
25
+ * `${app}UsEast1Certificates` stack consumed through CDK cross-region
26
+ * references.
27
+ */
28
+ import { Fn, Stack, Token } from "aws-cdk-lib";
29
+ import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
30
+ import { HostedZone } from "aws-cdk-lib/aws-route53";
31
+ import { DomainCertificate } from "../../resources/aws/networking/index.js";
32
+ import { isManagedDomainBinding, isWithinZone } from "../../utils/domainTypes.js";
33
+ import { readInjectedManagedDomainBinding } from "../../utils/managedDomainContext.js";
34
+ const US_EAST_1 = "us-east-1";
35
+ /**
36
+ * Resolve the hosted zone for a pattern domain from explicit identity only
37
+ * (H2). Throws when no identity is given or when the domain falls outside the
38
+ * declared zone.
39
+ */
40
+ /**
41
+ * Effective managed-domain identity for a request (D2): an EXPLICIT
42
+ * `managedDomain` prop wins over the CLI-injected context binding (explicit
43
+ * beats injected), and the context read sits ahead of the BYO
44
+ * `zoneName`/`hostedZoneId` chain. Bare-CDK synth carries no context entry
45
+ * and falls straight through to the explicit props.
46
+ */
47
+ function resolveEffectiveManagedDomain(request) {
48
+ return (request.identity.managedDomain ??
49
+ readInjectedManagedDomainBinding(request.scope.node, request.domain, request.context));
50
+ }
51
+ export function resolvePatternZone(request) {
52
+ const { scope, idPrefix, context, domain, identity } = request;
53
+ const managed = resolveEffectiveManagedDomain(request);
54
+ if (managed !== undefined) {
55
+ assertDomainWithinZone(context, domain, managed.zoneName);
56
+ const hostedZoneId = isManagedDomainBinding(managed)
57
+ ? // CLI-injected literal (D2) — carries no Fn.importValue constraint.
58
+ managed.hostedZoneId
59
+ : // Legacy export-name fallback — same-account, same-region only.
60
+ Fn.importValue(managed.hostedZoneIdExport);
61
+ return {
62
+ hostedZone: HostedZone.fromHostedZoneAttributes(scope, `${idPrefix}ManagedHostedZone`, { hostedZoneId, zoneName: managed.zoneName }),
63
+ zoneName: managed.zoneName
64
+ };
65
+ }
66
+ if (identity.zoneName !== undefined) {
67
+ assertDomainWithinZone(context, domain, identity.zoneName);
68
+ const hostedZone = identity.hostedZoneId !== undefined
69
+ ? HostedZone.fromHostedZoneAttributes(scope, `${idPrefix}HostedZone`, {
70
+ hostedZoneId: identity.hostedZoneId,
71
+ zoneName: identity.zoneName
72
+ })
73
+ : // The lookup targets the EXPLICIT zone name — never a value derived
74
+ // from the domain's labels (H2).
75
+ HostedZone.fromLookup(scope, `${idPrefix}HostedZone`, {
76
+ domainName: identity.zoneName
77
+ });
78
+ return { hostedZone, zoneName: identity.zoneName };
79
+ }
80
+ if (identity.hostedZoneId !== undefined) {
81
+ throw new Error(`${context}: 'hostedZoneId' ('${identity.hostedZoneId}') was set ` +
82
+ "without 'zoneName' (got zoneName undefined). A zone ID alone cannot " +
83
+ "name the zone, so records and certificates cannot be validated " +
84
+ `against it. Set 'zoneName' to the hosted zone that contains '${domain}'.`);
85
+ }
86
+ throw new Error(`${context}: domain '${domain}' has no zone identity (got neither ` +
87
+ "'managedDomain' nor 'zoneName'). Fjall does not guess hosted zones " +
88
+ "from domain labels — the last-two-labels guess resolved '.com.au'-style " +
89
+ "domains to their public suffix and mis-placed delegated sub-zones. " +
90
+ "Either bind a managed domain via the CLI ('fjall domain create' or " +
91
+ "'fjall domain import' — the deploy injects the binding), set " +
92
+ "'zoneName' (plus 'hostedZoneId' to skip the runtime lookup) to the " +
93
+ `hosted zone that contains '${domain}', or drop 'domain'.`);
94
+ }
95
+ /**
96
+ * Resolve the CloudFront viewer certificate for a pattern domain (H3/D3).
97
+ *
98
+ * - Managed binding: consume the injected us-east-1 ARN as a literal,
99
+ * asserting its region component at synth (literal ARNs are inspectable).
100
+ * - Legacy exports in a us-east-1 or region-unresolved app: keep the
101
+ * same-region Fn.importValue path unchanged (the D2 bare-CDK fallback).
102
+ * - Otherwise (BYO zone identity, or legacy exports in a non-us-east-1 app):
103
+ * provision an app-owned DNS-validated certificate — beside the consumer
104
+ * when the app already deploys to us-east-1, else in the dedicated
105
+ * `${app}UsEast1Certificates` stack, consumed through CDK cross-region
106
+ * references. In-app certificates are consumed in-app, so the
107
+ * `<domain>-certificate-arn` export is always suppressed
108
+ * (exportCertificateArn: false, §12-I) to avoid colliding with a
109
+ * managed-domain/apex stack that owns the domain's exports.
110
+ */
111
+ export function resolvePatternCloudFrontCertificate(request, zone) {
112
+ const { scope, app, idPrefix, context, domain, identity } = request;
113
+ const managed = resolveEffectiveManagedDomain(request);
114
+ if (managed !== undefined && isManagedDomainBinding(managed)) {
115
+ const arn = managed.usEast1CertificateArn;
116
+ if (arn === undefined) {
117
+ throw new Error(`${context}: managed domain binding for zone '${managed.zoneName}' ` +
118
+ "carries no usEast1CertificateArn (CloudFront accepts viewer " +
119
+ "certificates from us-east-1 only). Declare a CloudFront " +
120
+ `certificate for '${domain}' on the domain stack and redeploy it ` +
121
+ "so the binding carries the ARN, or drop 'managedDomain' and set " +
122
+ "'zoneName' to provision an app-owned us-east-1 certificate.");
123
+ }
124
+ assertUsEast1CertificateArn(context, arn);
125
+ return Certificate.fromCertificateArn(scope, `${idPrefix}ManagedCertificate`, arn);
126
+ }
127
+ const region = resolvedRegionOf(scope);
128
+ if (managed !== undefined && (region === undefined || region === US_EAST_1)) {
129
+ // Legacy export-name form: Fn.importValue resolves same-account,
130
+ // same-region only, so the imported certificate satisfies CloudFront
131
+ // exactly when the app itself deploys to us-east-1. A region-unresolved
132
+ // bare-CDK synth keeps the import unchanged (the documented D2 fallback);
133
+ // a resolved non-us-east-1 region falls through to app-owned
134
+ // provisioning instead of importing a certificate CloudFront must
135
+ // reject (H3).
136
+ return Certificate.fromCertificateArn(scope, `${idPrefix}ManagedCertificate`, Fn.importValue(managed.certificateArnExport));
137
+ }
138
+ if (region === US_EAST_1) {
139
+ // Single-stack layout: the pattern's own stack already deploys to the
140
+ // only region CloudFront accepts, so the certificate lives beside its
141
+ // consumer.
142
+ return new DomainCertificate(scope, `${idPrefix}Certificate`, {
143
+ domainName: domain,
144
+ hostedZone: zone.hostedZone,
145
+ exportCertificateArn: false
146
+ }).certificate;
147
+ }
148
+ if (region === undefined) {
149
+ throw new Error(`${context}: cannot place the CloudFront certificate for '${domain}' — ` +
150
+ "the app's region is unresolved at synth (got a region token), so " +
151
+ "whether the app already deploys to us-east-1 is unknowable. Deploy " +
152
+ "through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT and " +
153
+ "CDK_DEFAULT_REGION before synthesising.");
154
+ }
155
+ // Cross-region layout (D3 BYO path): the certificate is provisioned in the
156
+ // app-owned us-east-1 stack and the distribution consumes the ARN through
157
+ // CDK cross-region references. The zone reference is re-created inside the
158
+ // certificate stack: literal attributes travel as-is, while the legacy
159
+ // exports form must switch from Fn.importValue (unresolvable outside the
160
+ // exporting region) to a lookup on its declared zone name.
161
+ const certStack = app.getUsEast1CertificateStack().getStack();
162
+ const certZone = identity.hostedZoneId !== undefined
163
+ ? HostedZone.fromHostedZoneAttributes(certStack, `${idPrefix}CertificateZone`, { hostedZoneId: identity.hostedZoneId, zoneName: zone.zoneName })
164
+ : HostedZone.fromLookup(certStack, `${idPrefix}CertificateZone`, {
165
+ domainName: zone.zoneName
166
+ });
167
+ return new DomainCertificate(certStack, `${idPrefix}Certificate`, {
168
+ domainName: domain,
169
+ hostedZone: certZone,
170
+ exportCertificateArn: false
171
+ }).certificate;
172
+ }
173
+ function assertDomainWithinZone(context, domain, zoneName) {
174
+ if (!isWithinZone(domain, zoneName)) {
175
+ throw new Error(`${context}: domain '${domain}' is not within zone '${zoneName}' ` +
176
+ "(dot-boundary match). A relative record for it would silently " +
177
+ `synthesise as '${domain}.${zoneName}' — the wrong FQDN. Point ` +
178
+ `'domain' at a name under '${zoneName}', or set the zone identity ` +
179
+ `to the hosted zone that actually contains '${domain}'.`);
180
+ }
181
+ }
182
+ function assertUsEast1CertificateArn(context, arn) {
183
+ // Binding values are literals by contract (D2), so the ARN's region
184
+ // component is inspectable at synth — CloudFront rejects any other region
185
+ // at deploy with InvalidViewerCertificate, far too late.
186
+ const region = Token.isUnresolved(arn) ? undefined : arn.split(":")[3];
187
+ if (region !== US_EAST_1) {
188
+ throw new Error(`${context}: managed domain binding's usEast1CertificateArn must be a ` +
189
+ `literal us-east-1 ACM certificate ARN (got '${arn}'). CloudFront ` +
190
+ "accepts viewer certificates from us-east-1 only — redeploy the " +
191
+ "domain stack so its CloudFront certificate is provisioned in " +
192
+ "us-east-1, or correct the injected binding.");
193
+ }
194
+ }
195
+ function resolvedRegionOf(scope) {
196
+ const region = Stack.of(scope).region;
197
+ return Token.isUnresolved(region) ? undefined : region;
198
+ }
@@ -22,24 +22,23 @@
22
22
  * payload.getDatabase().grantConnect(otherLambda);
23
23
  */
24
24
  import { Construct } from "constructs";
25
- import { CfnOutput, Fn, Stack } from "aws-cdk-lib";
25
+ import { CfnOutput, Stack } from "aws-cdk-lib";
26
26
  import { Secret } from "../../resources/aws/secrets/index.js";
27
27
  import { Code, Runtime, Architecture, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
28
- import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
29
- import { HostedZone, ARecord, RecordTarget } from "aws-cdk-lib/aws-route53";
28
+ import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
29
+ import { ARecord, RecordTarget } from "aws-cdk-lib/aws-route53";
30
30
  import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
31
31
  import { DatabaseFactory } from "./database.js";
32
32
  import { ComputeFactory, InvokeMode } from "./compute.js";
33
33
  import { StorageFactory } from "./storage.js";
34
34
  import { MessagingFactory } from "./messaging.js";
35
35
  import { CdnFactory } from "./cdn.js";
36
+ import { resolvePatternZone, resolvePatternCloudFrontCertificate } from "./patternDomain.js";
36
37
  import { FjallLogger } from "../../utils/validationLogger.js";
37
38
  import { toPascalCase, toValidDatabaseName } from "../../utils/capitaliseString.js";
38
39
  const PAYLOAD_DEFAULTS = {
39
40
  /** Path to OpenNext output relative to app folder (CDK runs from fjall/<appname>) */
40
41
  SOURCE: "../..",
41
- /** CloudFront requires us-east-1 for certain resources; used as region fallback */
42
- FALLBACK_REGION: "us-east-1",
43
42
  DATABASE: {
44
43
  TYPE: "Instance",
45
44
  /** AWS default; 7 days covers most accidental data loss recovery scenarios */
@@ -78,14 +77,6 @@ const PAYLOAD_DEFAULTS = {
78
77
  ADMIN: "/admin/*"
79
78
  }
80
79
  };
81
- /**
82
- * Extract root domain from a full domain name.
83
- * e.g., "cms.example.com" -> "example.com"
84
- */
85
- function extractRootDomain(domain) {
86
- const parts = domain.split(".");
87
- return parts.length > 2 ? parts.slice(-2).join(".") : domain;
88
- }
89
80
  /**
90
81
  * Safely get the underlying Lambda function from a compute resource.
91
82
  * Throws a clear error if the function is not available.
@@ -488,23 +479,25 @@ export class Payload extends Construct {
488
479
  if (!domain) {
489
480
  throw new Error("Domain is required for simple domain resolution");
490
481
  }
491
- // Managed domain: import zone and cert from domain stack
492
- if (this.props.managedDomain) {
493
- const managed = this.props.managedDomain;
494
- const hostedZone = HostedZone.fromHostedZoneAttributes(this, `${this.pascalName}ManagedHostedZone`, {
495
- hostedZoneId: Fn.importValue(managed.hostedZoneIdExport),
496
- zoneName: managed.zoneName
497
- });
498
- const certificate = Certificate.fromCertificateArn(this, `${this.pascalName}ManagedCertificate`, Fn.importValue(managed.certificateArnExport));
499
- return { domainNames: [domain], certificate, hostedZone };
500
- }
501
- const rootDomain = extractRootDomain(domain);
502
- const hostedZone = HostedZone.fromLookup(this, `${this.pascalName}HostedZone`, { domainName: rootDomain });
503
- const certificate = new Certificate(this, `${this.pascalName}Certificate`, {
504
- domainName: domain,
505
- validation: CertificateValidation.fromDns(hostedZone)
506
- });
507
- return { domainNames: [domain], certificate, hostedZone };
482
+ // Zone identity is explicit only, and the domain is validated against the
483
+ // declared zone; the CloudFront certificate is the binding's literal
484
+ // us-east-1 ARN or an app-owned us-east-1 certificate (design H2 + H3/D3
485
+ // see patternDomain.ts).
486
+ const request = {
487
+ scope: this,
488
+ app: this.app,
489
+ idPrefix: this.pascalName,
490
+ context: `Payload '${this.props.name}'`,
491
+ domain,
492
+ identity: {
493
+ managedDomain: this.props.managedDomain,
494
+ zoneName: this.props.zoneName,
495
+ hostedZoneId: this.props.hostedZoneId
496
+ }
497
+ };
498
+ const zone = resolvePatternZone(request);
499
+ const certificate = resolvePatternCloudFrontCertificate(request, zone);
500
+ return { domainNames: [domain], certificate, hostedZone: zone.hostedZone };
508
501
  }
509
502
  resolveAdvancedDomain() {
510
503
  const cdnConfig = this.props.cdn;
@@ -20,6 +20,7 @@
20
20
  * routing: "multipage",
21
21
  * security: { headers: true },
22
22
  * domain: "lbexc.com",
23
+ * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
23
24
  * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
24
25
  * }));
25
26
  */
@@ -20,6 +20,7 @@
20
20
  * routing: "multipage",
21
21
  * security: { headers: true },
22
22
  * domain: "lbexc.com",
23
+ * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
23
24
  * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
24
25
  * }));
25
26
  */
@@ -29,15 +30,14 @@ import { dirname, resolve } from "node:path";
29
30
  import { fileURLToPath } from "node:url";
30
31
  import { Code, Runtime, Architecture, FunctionUrlAuthType, HttpMethod } from "aws-cdk-lib/aws-lambda";
31
32
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
32
- import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
33
- import { HostedZone } from "aws-cdk-lib/aws-route53";
34
33
  import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
35
34
  import { DNS_APEX, defaultFormsCorsOrigin, defaultFormsFromAddress, isAddressAtDomain } from "@fjall/util";
36
35
  import { StorageFactory } from "./storage.js";
37
36
  import { CdnFactory } from "./cdn.js";
38
37
  import { SecurityHeadersPolicy } from "../../resources/aws/cdn/index.js";
39
38
  import { LambdaFunction } from "../../resources/aws/compute/index.js";
40
- import { DomainCertificate, AliasRecord } from "../../resources/aws/networking/index.js";
39
+ import { AliasRecord } from "../../resources/aws/networking/index.js";
40
+ import { resolvePatternZone, resolvePatternCloudFrontCertificate } from "./patternDomain.js";
41
41
  import { toPascalCase } from "../../utils/capitaliseString.js";
42
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
43
43
  const FORMS_ASSET_DIR = resolve(__dirname, "../../lambda-assets/static-site-forms/asset");
@@ -48,11 +48,6 @@ const FORMS_DEFAULTS = {
48
48
  MEMORY_MB: 256,
49
49
  MAX_CONCURRENCY: 5
50
50
  };
51
- /** Extract the registrable root (e.g. "a.b.example.com" → "example.com"). */
52
- function extractRootDomain(domain) {
53
- const parts = domain.split(".");
54
- return parts.length > 2 ? parts.slice(-2).join(".") : domain;
55
- }
56
51
  /** Record label relative to the zone — DNS_APEX for the apex. */
57
52
  function recordLabelFor(domain, zoneName) {
58
53
  if (domain === zoneName)
@@ -227,43 +222,43 @@ export class StaticSite extends Construct {
227
222
  : {
228
223
  domainNames: undefined,
229
224
  certificate: undefined,
230
- hostedZone: undefined
225
+ hostedZone: undefined,
226
+ zoneName: undefined
231
227
  };
232
228
  return this._resolvedDomain;
233
229
  }
234
230
  resolveSimpleDomain(domain) {
235
- if (this.props.managedDomain) {
236
- const managed = this.props.managedDomain;
237
- const hostedZone = HostedZone.fromHostedZoneAttributes(this, `${this.pascalName}ManagedHostedZone`, {
238
- hostedZoneId: Fn.importValue(managed.hostedZoneIdExport),
239
- zoneName: managed.zoneName
240
- });
241
- const certificate = Certificate.fromCertificateArn(this, `${this.pascalName}ManagedCertificate`, Fn.importValue(managed.certificateArnExport));
242
- return { domainNames: [domain], certificate, hostedZone };
243
- }
244
- const rootDomain = extractRootDomain(domain);
245
- const hostedZone = HostedZone.fromLookup(this, `${this.pascalName}HostedZone`, { domainName: rootDomain });
246
- // Single-stack: the cert is consumed in-stack, so suppress the ARN export
247
- // to avoid a collision with a managed-domain/apex stack (§12-I).
248
- const domainCert = new DomainCertificate(this, `${this.pascalName}Certificate`, {
249
- domainName: domain,
250
- hostedZone,
251
- exportCertificateArn: false
252
- });
231
+ // Zone identity is explicit only, and the domain is validated against the
232
+ // declared zone; the CloudFront certificate is the binding's literal
233
+ // us-east-1 ARN or an app-owned us-east-1 certificate (design H2 + H3/D3
234
+ // — see patternDomain.ts).
235
+ const request = {
236
+ scope: this,
237
+ app: this.app,
238
+ idPrefix: this.pascalName,
239
+ context: `Static site '${this.props.name}'`,
240
+ domain,
241
+ identity: {
242
+ managedDomain: this.props.managedDomain,
243
+ zoneName: this.props.zoneName,
244
+ hostedZoneId: this.props.hostedZoneId
245
+ }
246
+ };
247
+ const zone = resolvePatternZone(request);
248
+ const certificate = resolvePatternCloudFrontCertificate(request, zone);
253
249
  return {
254
250
  domainNames: [domain],
255
- certificate: domainCert.certificate,
256
- hostedZone
251
+ certificate,
252
+ hostedZone: zone.hostedZone,
253
+ zoneName: zone.zoneName
257
254
  };
258
255
  }
259
256
  createDnsRecord() {
260
257
  if (!this.props.domain)
261
258
  return;
262
- const { hostedZone } = this.resolveDomainConfig();
263
- if (!hostedZone)
259
+ const { hostedZone, zoneName } = this.resolveDomainConfig();
260
+ if (!hostedZone || !zoneName)
264
261
  return;
265
- const zoneName = this.props.managedDomain?.zoneName ??
266
- extractRootDomain(this.props.domain);
267
262
  // Alias record lives in the CDN stack (where the distribution is) so there
268
263
  // is no cross-stack cycle back to this pattern node.
269
264
  const cdnStack = this.app.getDefaultCdnStack().getStack();
@@ -14,6 +14,14 @@ export interface AwsStackProps {
14
14
  readonly accountId?: string;
15
15
  readonly region?: string;
16
16
  accountName?: string;
17
+ /**
18
+ * Enable CDK native cross-region references on this stack. Required on
19
+ * BOTH the producing and consuming stack of a cross-region reference
20
+ * (e.g. a us-east-1 CloudFront certificate consumed by a Cdn stack in
21
+ * another region). Inert unless a cross-region reference is actually
22
+ * synthesised.
23
+ */
24
+ readonly crossRegionReferences?: boolean;
17
25
  }
18
26
  export declare class AwsStack implements StackBuilder {
19
27
  readonly resources: {
@@ -32,24 +32,27 @@ export class AwsStack {
32
32
  return stack;
33
33
  }
34
34
  getStackProps(props) {
35
- // If no explicit props are provided, fall back to the account/region that
36
- // CDK resolved for this run. This allows context look-ups (e.g.
37
- // Vpc.fromLookup) to work without every stack having to be created with
38
- // explicit env settings.
39
- if (!props) {
40
- const account = process.env.CDK_DEFAULT_ACCOUNT;
41
- const region = process.env.CDK_DEFAULT_REGION;
42
- if (account && region) {
43
- return { env: { account, region } };
44
- }
45
- return;
35
+ const crossRegionReferences = props?.crossRegionReferences
36
+ ? { crossRegionReferences: true }
37
+ : {};
38
+ // An explicit account/region pin wins. Otherwise fall back to the
39
+ // account/region that CDK resolved for this run, so context look-ups
40
+ // (e.g. Vpc.fromLookup) work without every stack having to be created
41
+ // with explicit env settings.
42
+ if (props?.accountId || props?.region) {
43
+ return {
44
+ env: props.accountId && props.region
45
+ ? { account: props.accountId, region: props.region }
46
+ : undefined,
47
+ ...crossRegionReferences
48
+ };
49
+ }
50
+ const account = process.env.CDK_DEFAULT_ACCOUNT;
51
+ const region = process.env.CDK_DEFAULT_REGION;
52
+ if (account && region) {
53
+ return { env: { account, region }, ...crossRegionReferences };
46
54
  }
47
- const stackProps = {
48
- env: props.accountId && props.region
49
- ? { account: props.accountId, region: props.region }
50
- : undefined
51
- };
52
- return stackProps;
55
+ return props?.crossRegionReferences ? crossRegionReferences : undefined;
53
56
  }
54
57
  isConnectable(arg) {
55
58
  return typeof arg === "object" && arg !== null && "connections" in arg;
@@ -10,7 +10,7 @@ export * from "./ecsContext.js";
10
10
  export * from "./ecsTaskDefinition.js";
11
11
  export * from "./ecsNetworking.js";
12
12
  export { CapacityProviderDependencyAspect } from "./ecsCapacityProviderAspect.js";
13
- export { validateEcsClusterProps, validateSsmPathComponent } from "./ecsValidation.js";
13
+ export { validateEcsClusterProps, validateEcsDomainConfig, validateSsmPathComponent } from "./ecsValidation.js";
14
14
  export * from "./ecsServiceFactory.js";
15
15
  /**
16
16
  * ECS Cluster supporting multiple services with a shared ALB.
@@ -18,7 +18,7 @@ export * from "./ecsContext.js";
18
18
  export * from "./ecsTaskDefinition.js";
19
19
  export * from "./ecsNetworking.js";
20
20
  export { CapacityProviderDependencyAspect } from "./ecsCapacityProviderAspect.js";
21
- export { validateEcsClusterProps, validateSsmPathComponent } from "./ecsValidation.js";
21
+ export { validateEcsClusterProps, validateEcsDomainConfig, validateSsmPathComponent } from "./ecsValidation.js";
22
22
  export * from "./ecsServiceFactory.js";
23
23
  /**
24
24
  * ECS Cluster supporting multiple services with a shared ALB.
@@ -1,7 +1,7 @@
1
1
  import { type ApplicationListener, type ApplicationLoadBalancer, type IApplicationTargetGroup } from "aws-cdk-lib/aws-elasticloadbalancingv2";
2
2
  import { type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
3
3
  import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
4
- import { ARecord, type IHostedZone } from "aws-cdk-lib/aws-route53";
4
+ import { type ARecord, type IHostedZone } from "aws-cdk-lib/aws-route53";
5
5
  import type { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
6
6
  import type { ContainerDefinition, FargateService, Ec2Service } from "aws-cdk-lib/aws-ecs";
7
7
  import { SecurityGroup } from "../networking/securityGroup.js";