@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.
- package/dist/lib/app.d.ts +19 -2
- package/dist/lib/app.js +46 -3
- package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
- package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
- package/dist/lib/patterns/aws/cdn.js +25 -10
- package/dist/lib/patterns/aws/computeEcs.js +6 -1
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -3
- package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
- package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
- package/dist/lib/patterns/aws/devSubstrate.js +18 -8
- package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
- package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
- package/dist/lib/patterns/aws/domain.d.ts +3 -2
- package/dist/lib/patterns/aws/domain.js +3 -2
- package/dist/lib/patterns/aws/domainValidation.js +71 -46
- package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
- package/dist/lib/patterns/aws/index.d.ts +1 -3
- package/dist/lib/patterns/aws/index.js +2 -3
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
- package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
- package/dist/lib/patterns/aws/interfaces/index.js +3 -0
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
- package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
- package/dist/lib/patterns/aws/patternDomain.js +198 -0
- package/dist/lib/patterns/aws/payload.js +23 -30
- package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
- package/dist/lib/patterns/aws/staticSite.js +28 -33
- package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
- package/dist/lib/resources/aws/base/awsStack.js +20 -17
- package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecs.js +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
- package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
- package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
- package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
- package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
- package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
- package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
- package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
- package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
- package/dist/lib/utils/costAllocationTags.d.ts +9 -0
- package/dist/lib/utils/costAllocationTags.js +6 -1
- package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
- package/dist/lib/utils/dnsRecordRegistry.js +64 -0
- package/dist/lib/utils/domainTypes.d.ts +43 -36
- package/dist/lib/utils/domainTypes.js +65 -2
- package/dist/lib/utils/managedDomainContext.d.ts +37 -0
- package/dist/lib/utils/managedDomainContext.js +111 -0
- 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,
|
|
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
|
|
29
|
-
import {
|
|
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
|
-
//
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
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
|
*/
|
|
@@ -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 {
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// explicit
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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";
|