@fjall/components-infrastructure 14.1.0 → 14.3.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/patterns/aws/apexDomainPattern.js +1 -1
- package/dist/lib/patterns/aws/cdn.d.ts +83 -4
- package/dist/lib/patterns/aws/cdn.js +279 -69
- package/dist/lib/patterns/aws/cdnAppOrigin.d.ts +83 -0
- package/dist/lib/patterns/aws/cdnAppOrigin.js +252 -0
- package/dist/lib/patterns/aws/computeEcs.d.ts +9 -1
- package/dist/lib/patterns/aws/computeEcs.js +10 -0
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +1 -1
- package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +13 -4
- package/dist/lib/patterns/aws/dnsRecordComposer.js +88 -6
- package/dist/lib/patterns/aws/domainCertificateComposer.js +66 -1
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +7 -0
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +32 -0
- package/dist/lib/patterns/aws/patternDomain.d.ts +9 -0
- package/dist/lib/patterns/aws/patternDomain.js +60 -27
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +37 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +96 -4
- package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
- package/dist/lib/resources/aws/compute/ecs.js +27 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +2 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.js +117 -61
- package/dist/lib/resources/aws/compute/ingressProfile.d.ts +203 -0
- package/dist/lib/resources/aws/compute/ingressProfile.js +215 -0
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +10 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +18 -9
- package/dist/lib/utils/dnsRecordRegistry.d.ts +3 -5
- package/dist/lib/utils/dnsRecordRegistry.js +11 -14
- package/dist/lib/utils/domainTypes.d.ts +11 -0
- package/dist/lib/utils/domainTypes.js +17 -0
- package/dist/lib/utils/managedDomainContext.d.ts +58 -1
- package/dist/lib/utils/managedDomainContext.js +96 -0
- package/package.json +3 -3
|
@@ -44,7 +44,7 @@ export function composeApexDomain(scope, props) {
|
|
|
44
44
|
costAllocationDomain: props.zoneName
|
|
45
45
|
});
|
|
46
46
|
if (props.records && props.records.length > 0) {
|
|
47
|
-
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
|
|
47
|
+
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records, props.recordIds);
|
|
48
48
|
}
|
|
49
49
|
return {
|
|
50
50
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
@@ -130,11 +130,36 @@ export interface HttpCdnProps extends BaseCdnProps {
|
|
|
130
130
|
protocolPolicy?: "HTTP_ONLY" | "HTTPS_ONLY" | "MATCH_VIEWER";
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Smart origin CDN props - auto-detect origin type from Fjall resource
|
|
133
|
+
* Smart origin CDN props - auto-detect origin type from Fjall resource.
|
|
134
|
+
*
|
|
135
|
+
* ECS compute origins (design 2026-08-18 cdn-app-origin, D3) resolve a
|
|
136
|
+
* TLS-valid ORIGIN HOSTNAME from the compute's own ingress profile instead
|
|
137
|
+
* of the raw ELB hostname (which no ACM certificate ever covers): a routing
|
|
138
|
+
* host when the compute declares exactly one, else `origin.<domain>`,
|
|
139
|
+
* derived. Certificate coverage and listener forwarding are validated at
|
|
140
|
+
* synth; when the Cdn owns the hostname it mints the alias record in this
|
|
141
|
+
* stack, record-before-distribution ordered.
|
|
134
142
|
*/
|
|
135
143
|
export interface SmartCdnProps extends BaseCdnProps {
|
|
136
144
|
originType: "auto";
|
|
137
145
|
origin: Storage | AnyCompute | string;
|
|
146
|
+
/**
|
|
147
|
+
* Origin hostname override for the DEFAULT origin's ECS compute (a
|
|
148
|
+
* behaviour's ECS origin takes the same override on the behaviour
|
|
149
|
+
* entry). A literal hostname: a compute-declared routing host (the
|
|
150
|
+
* compute keeps owning its record), or a free name inside the cluster's
|
|
151
|
+
* zone (the Cdn mints its record). Refused for `redirectHosts` entries —
|
|
152
|
+
* they 301, never serve. Default: auto-resolution (sole routing host,
|
|
153
|
+
* else `origin.<domain>`).
|
|
154
|
+
*/
|
|
155
|
+
originHostname?: string;
|
|
156
|
+
/**
|
|
157
|
+
* Whether the Cdn mints the origin alias record when it owns the DEFAULT
|
|
158
|
+
* origin's hostname (behaviour entries carry their own). "none" = the
|
|
159
|
+
* record is managed elsewhere (BYO or a migration state). Refused when
|
|
160
|
+
* the hostname is a routing host — the compute owns that record (P1).
|
|
161
|
+
*/
|
|
162
|
+
originRecord?: "alias" | "none";
|
|
138
163
|
}
|
|
139
164
|
export type ICdnProps = S3CdnProps | AlbCdnProps | HttpCdnProps | SmartCdnProps;
|
|
140
165
|
/**
|
|
@@ -143,15 +168,53 @@ export type ICdnProps = S3CdnProps | AlbCdnProps | HttpCdnProps | SmartCdnProps;
|
|
|
143
168
|
*/
|
|
144
169
|
export interface SmartCdnBehaviour {
|
|
145
170
|
pathPattern: string;
|
|
146
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Origin for this path pattern. Omit to route the pattern to the
|
|
173
|
+
* distribution's DEFAULT origin — the common shape where behaviours exist
|
|
174
|
+
* to vary caching or allowed methods per path, not to change origins. An
|
|
175
|
+
* origin-less behaviour is the default behaviour varied per path, so its
|
|
176
|
+
* omitted `cachePolicy`/`allowedMethods` inherit the distribution's
|
|
177
|
+
* `cachePolicy`/`defaultAllowedMethods` (a behaviour with its own origin
|
|
178
|
+
* is an independent config and keeps the resource-layer fallbacks).
|
|
179
|
+
*/
|
|
180
|
+
origin?: Storage | AnyCompute | string;
|
|
147
181
|
cachePolicy?: CachePolicyPreset | ICachePolicy;
|
|
148
182
|
allowedMethods?: "GET_HEAD" | "GET_HEAD_OPTIONS" | "ALL";
|
|
183
|
+
/**
|
|
184
|
+
* Origin hostname override for THIS behaviour's ECS compute origin — the
|
|
185
|
+
* behaviour-level counterpart of the distribution's `originHostname`
|
|
186
|
+
* (same semantics; see {@link SmartCdnProps.originHostname}). Overrides
|
|
187
|
+
* bind per compute: two entries naming the same compute must agree.
|
|
188
|
+
*/
|
|
189
|
+
originHostname?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Record-minting override for THIS behaviour's ECS compute origin — the
|
|
192
|
+
* behaviour-level counterpart of {@link SmartCdnProps.originRecord}.
|
|
193
|
+
*/
|
|
194
|
+
originRecord?: "alias" | "none";
|
|
149
195
|
}
|
|
150
196
|
/**
|
|
151
197
|
* CDN wrapper class that extends CloudFrontDistribution with Fjall patterns.
|
|
152
198
|
*/
|
|
153
199
|
export declare class Cdn extends CloudFrontDistribution implements ICdn {
|
|
154
200
|
constructor(scope: Construct, id: string, props: ICdnProps);
|
|
201
|
+
private static createEcsOriginResolver;
|
|
202
|
+
/**
|
|
203
|
+
* Assemble the per-compute `originHostname`/`originRecord` overrides the
|
|
204
|
+
* resolver binds by compute identity: the distribution-level props apply
|
|
205
|
+
* to the DEFAULT origin's compute, each behaviour's own overrides apply
|
|
206
|
+
* to that behaviour's compute. Two entries naming the same compute must
|
|
207
|
+
* agree — one alias record cannot carry two spellings.
|
|
208
|
+
*/
|
|
209
|
+
private static collectEcsOriginOverrides;
|
|
210
|
+
/**
|
|
211
|
+
* Mint the Cdn-owned origin alias records (P1) after the distribution
|
|
212
|
+
* exists, using the compute profile's own zone instance so the DNS claim
|
|
213
|
+
* registry sees the same zone identity as the cluster's records. The
|
|
214
|
+
* distribution depends on each record: the hostname must resolve before a
|
|
215
|
+
* distribution update referencing it propagates.
|
|
216
|
+
*/
|
|
217
|
+
private createOriginRecords;
|
|
155
218
|
/**
|
|
156
219
|
* Resolve a `domainConfig` through the shared pattern-domain helpers
|
|
157
220
|
* (design C1): zone identity via `resolvePatternZone` (explicit
|
|
@@ -171,11 +234,27 @@ export declare class Cdn extends CloudFrontDistribution implements ICdn {
|
|
|
171
234
|
*/
|
|
172
235
|
private createDomainRecord;
|
|
173
236
|
/**
|
|
174
|
-
* Resolve ICdnProps to
|
|
237
|
+
* Resolve ICdnProps to the distribution props PLUS the Cdn-owned origin
|
|
238
|
+
* record plans the resolution produced — one resolution pass, one result.
|
|
175
239
|
*/
|
|
176
240
|
private static resolveProps;
|
|
177
241
|
/**
|
|
178
|
-
* Transform smart behaviours to CdnBehaviour objects.
|
|
242
|
+
* Transform smart behaviours to CdnBehaviour objects. A behaviour with no
|
|
243
|
+
* origin routes to the distribution's default origin — the same resolved
|
|
244
|
+
* config, so the resource layer memoizes them onto one distribution
|
|
245
|
+
* origin entry.
|
|
246
|
+
*
|
|
247
|
+
* Knob inheritance is asymmetric BY CONTRACT. An origin-less behaviour IS
|
|
248
|
+
* the default behaviour varied per path (the shape
|
|
249
|
+
* `SmartCdnBehaviour.origin` advertises), so its omitted
|
|
250
|
+
* `cachePolicy`/`allowedMethods` inherit the DISTRIBUTION's defaults —
|
|
251
|
+
* otherwise a caching-only behaviour on an `allowedMethods: "ALL"` API
|
|
252
|
+
* distribution would snap its path back to the resource layer's GET/HEAD
|
|
253
|
+
* fallback and 403 every POST at the edge. A behaviour WITH its own
|
|
254
|
+
* origin keeps the legacy resource-layer fallbacks (GET_HEAD /
|
|
255
|
+
* CACHING_OPTIMIZED): those deployed behaviours are byte-frozen, and an
|
|
256
|
+
* independent origin is an independent config, not a variation of the
|
|
257
|
+
* default behaviour.
|
|
179
258
|
*/
|
|
180
259
|
private static resolveBehaviours;
|
|
181
260
|
/**
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { Fn, Token } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
|
-
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
3
|
+
import { CloudFrontTarget, LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
4
4
|
import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
|
|
5
5
|
import { AliasRecord } from "../../resources/aws/networking/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getSafeZoneName, toPascalCase } from "../../utils/capitaliseString.js";
|
|
7
|
+
import { recordLabelWithin } from "../../utils/domainTypes.js";
|
|
7
8
|
import { resolvePatternZone, resolvePatternCloudFrontCertificate } from "./patternDomain.js";
|
|
8
9
|
import { isStorage } from "./storage.js";
|
|
10
|
+
import { CdnEcsOriginResolver, cdnOriginRefusal, internalAlbMessage, normaliseDnsName } from "./cdnAppOrigin.js";
|
|
9
11
|
import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* The distribution's own alias names, normalised — consumed by the
|
|
14
|
+
* origin-loop guard and by the ECS origin resolver (auto-resolution excludes
|
|
15
|
+
* them from routed-host candidates, and a resolved origin equal to one is
|
|
16
|
+
* refused as a loop).
|
|
14
17
|
*/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
function collectAliasNames(props) {
|
|
19
|
+
const aliasNames = new Set();
|
|
20
|
+
if (props.domainConfig !== undefined) {
|
|
21
|
+
aliasNames.add(normaliseDnsName(props.domainConfig.domainName));
|
|
18
22
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
for (const name of props.domainNames ?? []) {
|
|
24
|
+
if (!Token.isUnresolved(name)) {
|
|
25
|
+
aliasNames.add(normaliseDnsName(name));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return aliasNames;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Validates CDN props — synth-time hard errors for shapes CloudFormation
|
|
@@ -69,15 +73,7 @@ function validateCdnProps(props) {
|
|
|
69
73
|
// every request CloudFront → DNS → CloudFront until the request-depth
|
|
70
74
|
// limit 508s. Synth can see this shape whenever the origin hostname is a
|
|
71
75
|
// literal, so refuse it with the cure.
|
|
72
|
-
const aliasNames =
|
|
73
|
-
if (props.domainConfig !== undefined) {
|
|
74
|
-
aliasNames.add(normaliseDnsName(props.domainConfig.domainName));
|
|
75
|
-
}
|
|
76
|
-
for (const name of props.domainNames ?? []) {
|
|
77
|
-
if (!Token.isUnresolved(name)) {
|
|
78
|
-
aliasNames.add(normaliseDnsName(name));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
76
|
+
const aliasNames = collectAliasNames(props);
|
|
81
77
|
const defaultOriginHostname = props.originType === "http"
|
|
82
78
|
? props.domainName
|
|
83
79
|
: props.originType === "auto" && typeof props.origin === "string"
|
|
@@ -96,7 +92,7 @@ function validateCdnProps(props) {
|
|
|
96
92
|
for (const originHostname of literalOriginHostnames) {
|
|
97
93
|
if (!Token.isUnresolved(originHostname) &&
|
|
98
94
|
aliasNames.has(normaliseDnsName(originHostname))) {
|
|
99
|
-
throw
|
|
95
|
+
throw cdnOriginRefusal("C4", `CDN origin '${originHostname}' is also one of the distribution's own ` +
|
|
100
96
|
"domain names — once DNS points that name at the distribution, every " +
|
|
101
97
|
"request loops CloudFront → CloudFront. Point the origin at a " +
|
|
102
98
|
"dedicated origin hostname instead (e.g. an ECS service " +
|
|
@@ -130,18 +126,64 @@ function validateCdnProps(props) {
|
|
|
130
126
|
"'certificateArn', or configure the pattern's 'domain' with a zone " +
|
|
131
127
|
"identity so it provisions one.");
|
|
132
128
|
}
|
|
133
|
-
// Validate ALB origin
|
|
129
|
+
// Validate ALB origin (design 2026-08-18 cdn-app-origin, D4): CloudFront
|
|
130
|
+
// validates the origin certificate against the raw `*.elb.amazonaws.com`
|
|
131
|
+
// hostname, which no ACM certificate ever covers — an effective-HTTPS alb
|
|
132
|
+
// origin can never complete a TLS handshake. E9, with the cure conditional
|
|
133
|
+
// on the listener protocol: HTTP_ONLY only works against a port-80
|
|
134
|
+
// listener, and a cert-bearing cluster has exactly one listener, on 443.
|
|
134
135
|
if (props.originType === "alb") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
const computeRef = isCompute(props.loadBalancer) && isEcsCompute(props.loadBalancer)
|
|
137
|
+
? props.loadBalancer
|
|
138
|
+
: undefined;
|
|
139
|
+
if (computeRef !== undefined && !computeRef.getLoadBalancer()) {
|
|
140
|
+
throw new Error("Compute resource does not have a load balancer. " +
|
|
141
|
+
"Ensure ECS compute has loadBalancer enabled for CDN origin.");
|
|
142
|
+
}
|
|
143
|
+
const profile = computeRef?.getIngressProfile();
|
|
144
|
+
if (profile?.internal) {
|
|
145
|
+
// E2 — same defect as the construct-origin lane, same shared message.
|
|
146
|
+
throw cdnOriginRefusal("E2", internalAlbMessage("CDN alb origin: the load balancer", "Make the load balancer internet-facing."));
|
|
147
|
+
}
|
|
148
|
+
const effectiveProtocol = props.protocolPolicy ?? "HTTPS_ONLY";
|
|
149
|
+
if (effectiveProtocol !== "HTTP_ONLY") {
|
|
150
|
+
// E9 — effective HTTPS to the raw ELB hostname.
|
|
151
|
+
const cure = profile === undefined
|
|
152
|
+
? 'If the ALB has an HTTP (port-80) listener, set protocolPolicy: "HTTP_ONLY"; ' +
|
|
153
|
+
'otherwise use originType "auto" with the compute as origin, ' +
|
|
154
|
+
"which resolves a certificate-covered origin hostname."
|
|
155
|
+
: profile.listenerPort === 80
|
|
156
|
+
? 'Set protocolPolicy: "HTTP_ONLY" (the cluster\'s listener serves HTTP), ' +
|
|
157
|
+
'or use originType "auto" with the compute as origin.'
|
|
158
|
+
: 'Use originType "auto" with the compute as origin — the ' +
|
|
159
|
+
"cluster's only listener is HTTPS (443), so the sole working " +
|
|
160
|
+
"path is a certificate-covered origin hostname.";
|
|
161
|
+
throw cdnOriginRefusal("E9", `CDN alb origin with protocolPolicy '${effectiveProtocol}' can ` +
|
|
162
|
+
"never complete a TLS handshake: CloudFront validates the origin " +
|
|
163
|
+
"certificate against the raw ELB hostname " +
|
|
164
|
+
"(*.elb.amazonaws.com), which no ACM certificate covers — every " +
|
|
165
|
+
`origin fetch fails with a 502. ${cure}`);
|
|
166
|
+
}
|
|
167
|
+
if (profile !== undefined && profile.listenerPort === 443) {
|
|
168
|
+
// E9b (HTTP_ONLY variant) — nothing listens on 80: a connect-timeout
|
|
169
|
+
// 504 instead of a TLS 502, equally broken.
|
|
170
|
+
throw cdnOriginRefusal("E9b", 'CDN alb origin with protocolPolicy "HTTP_ONLY": the cluster\'s ' +
|
|
171
|
+
"only listener is HTTPS (443) — a certificate resolved, so " +
|
|
172
|
+
"nothing listens on port 80 and every origin fetch times out. " +
|
|
173
|
+
'Use originType "auto" with the compute as origin instead.');
|
|
141
174
|
}
|
|
142
175
|
}
|
|
143
176
|
// Validate smart origin
|
|
144
177
|
if (props.originType === "auto") {
|
|
178
|
+
if (!(isCompute(props.origin) && isEcsCompute(props.origin)) &&
|
|
179
|
+
(props.originHostname !== undefined || props.originRecord !== undefined)) {
|
|
180
|
+
throw new Error("CDN 'originHostname'/'originRecord' configure the distribution's " +
|
|
181
|
+
"DEFAULT origin, and only when it is an ECS compute — they " +
|
|
182
|
+
"resolve the origin hostname against that compute's ingress " +
|
|
183
|
+
"profile. Drop them for storage, Lambda, and literal-hostname " +
|
|
184
|
+
"default origins; a behaviour's ECS origin takes the same " +
|
|
185
|
+
"overrides on the behaviour entry itself.");
|
|
186
|
+
}
|
|
145
187
|
if (isCompute(props.origin)) {
|
|
146
188
|
if (isEcsCompute(props.origin)) {
|
|
147
189
|
if (!props.origin.getLoadBalancer()) {
|
|
@@ -175,11 +217,133 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
175
217
|
const domain = props.domainConfig !== undefined && props.app !== undefined
|
|
176
218
|
? Cdn.resolveDomain(scope, id, props.domainConfig, props.app)
|
|
177
219
|
: undefined;
|
|
178
|
-
|
|
179
|
-
|
|
220
|
+
// ECS construct-reference origins resolve through one memoized resolver
|
|
221
|
+
// (design 2026-08-18 cdn-app-origin, D3): the default origin and every
|
|
222
|
+
// behaviour referencing the same compute share one resolution and at
|
|
223
|
+
// most one origin record. The record plans ride the resolution RESULT —
|
|
224
|
+
// they exist only once every origin has resolved, so returning them from
|
|
225
|
+
// resolveProps makes the ordering a property of the data flow rather
|
|
226
|
+
// than a call-sequence convention on the resolver instance.
|
|
227
|
+
const resolved = Cdn.resolveProps(scope, id, props, domain);
|
|
228
|
+
super(scope, id, resolved.distributionProps);
|
|
180
229
|
if (domain !== undefined && props.domainConfig !== undefined) {
|
|
181
230
|
this.createDomainRecord(id, props.domainConfig, domain);
|
|
182
231
|
}
|
|
232
|
+
this.createOriginRecords(resolved.recordPlans);
|
|
233
|
+
}
|
|
234
|
+
static createEcsOriginResolver(id, props) {
|
|
235
|
+
return new CdnEcsOriginResolver({
|
|
236
|
+
cdnId: id,
|
|
237
|
+
aliasNames: collectAliasNames(props),
|
|
238
|
+
overrides: Cdn.collectEcsOriginOverrides(id, props)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Assemble the per-compute `originHostname`/`originRecord` overrides the
|
|
243
|
+
* resolver binds by compute identity: the distribution-level props apply
|
|
244
|
+
* to the DEFAULT origin's compute, each behaviour's own overrides apply
|
|
245
|
+
* to that behaviour's compute. Two entries naming the same compute must
|
|
246
|
+
* agree — one alias record cannot carry two spellings.
|
|
247
|
+
*/
|
|
248
|
+
static collectEcsOriginOverrides(id, props) {
|
|
249
|
+
const overrides = new Map();
|
|
250
|
+
const claim = (compute, override, site) => {
|
|
251
|
+
const existing = overrides.get(compute) ?? {};
|
|
252
|
+
for (const key of ["originHostname", "originRecord"]) {
|
|
253
|
+
const incoming = override[key];
|
|
254
|
+
if (incoming === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
const held = existing[key];
|
|
257
|
+
if (held !== undefined && held !== incoming) {
|
|
258
|
+
throw new Error(`CDN '${id}': conflicting ${key} overrides for the same ECS ` +
|
|
259
|
+
`compute origin ('${held}' vs '${incoming}' from ${site}) — ` +
|
|
260
|
+
"overrides bind per compute, so every entry naming it must " +
|
|
261
|
+
"agree. Keep one spelling.");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
overrides.set(compute, { ...existing, ...override });
|
|
265
|
+
};
|
|
266
|
+
if (props.originType === "auto") {
|
|
267
|
+
const override = {
|
|
268
|
+
...(props.originHostname !== undefined && {
|
|
269
|
+
originHostname: props.originHostname
|
|
270
|
+
}),
|
|
271
|
+
...(props.originRecord !== undefined && {
|
|
272
|
+
originRecord: props.originRecord
|
|
273
|
+
})
|
|
274
|
+
};
|
|
275
|
+
if ((override.originHostname !== undefined ||
|
|
276
|
+
override.originRecord !== undefined) &&
|
|
277
|
+
isCompute(props.origin) &&
|
|
278
|
+
isEcsCompute(props.origin)) {
|
|
279
|
+
claim(props.origin, override, "the distribution props");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const behaviour of props.behaviours ?? []) {
|
|
283
|
+
if (behaviour.originHostname === undefined &&
|
|
284
|
+
behaviour.originRecord === undefined) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (behaviour.origin === undefined) {
|
|
288
|
+
throw new Error(`CDN '${id}': behaviour '${behaviour.pathPattern}' sets ` +
|
|
289
|
+
"'originHostname'/'originRecord' but no 'origin' — an " +
|
|
290
|
+
"origin-less behaviour routes to the distribution's default " +
|
|
291
|
+
"origin, whose overrides live on the distribution props. Move " +
|
|
292
|
+
"the override there, or give the behaviour its own origin.");
|
|
293
|
+
}
|
|
294
|
+
if (typeof behaviour.origin === "string" ||
|
|
295
|
+
!isCompute(behaviour.origin) ||
|
|
296
|
+
!isEcsCompute(behaviour.origin)) {
|
|
297
|
+
throw new Error(`CDN '${id}': behaviour '${behaviour.pathPattern}' sets ` +
|
|
298
|
+
"'originHostname'/'originRecord', but its origin is not an ECS " +
|
|
299
|
+
"compute — they configure how the origin hostname resolves " +
|
|
300
|
+
"against the compute's ingress profile. Drop them for storage, " +
|
|
301
|
+
"Lambda, and literal-hostname origins.");
|
|
302
|
+
}
|
|
303
|
+
claim(behaviour.origin, {
|
|
304
|
+
...(behaviour.originHostname !== undefined && {
|
|
305
|
+
originHostname: behaviour.originHostname
|
|
306
|
+
}),
|
|
307
|
+
...(behaviour.originRecord !== undefined && {
|
|
308
|
+
originRecord: behaviour.originRecord
|
|
309
|
+
})
|
|
310
|
+
}, `behaviour '${behaviour.pathPattern}'`);
|
|
311
|
+
}
|
|
312
|
+
return overrides;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Mint the Cdn-owned origin alias records (P1) after the distribution
|
|
316
|
+
* exists, using the compute profile's own zone instance so the DNS claim
|
|
317
|
+
* registry sees the same zone identity as the cluster's records. The
|
|
318
|
+
* distribution depends on each record: the hostname must resolve before a
|
|
319
|
+
* distribution update referencing it propagates.
|
|
320
|
+
*/
|
|
321
|
+
createOriginRecords(plans) {
|
|
322
|
+
// Plans are memoized per compute, so two entries with one hostname mean
|
|
323
|
+
// two DIFFERENT computes resolved the same name — an inherently invalid
|
|
324
|
+
// shape (one alias record cannot target two load balancers). Refuse it
|
|
325
|
+
// with the cure instead of letting CDK's duplicate-construct-id throw.
|
|
326
|
+
const seenHostnames = new Set();
|
|
327
|
+
for (const plan of plans) {
|
|
328
|
+
const key = normaliseDnsName(plan.hostname);
|
|
329
|
+
if (seenHostnames.has(key)) {
|
|
330
|
+
throw new Error(`CDN '${this.node.id}': two ECS compute origins resolved the ` +
|
|
331
|
+
`same origin hostname '${plan.hostname}' — one alias record ` +
|
|
332
|
+
"cannot target two load balancers. Give one of them a " +
|
|
333
|
+
"distinct originHostname.");
|
|
334
|
+
}
|
|
335
|
+
seenHostnames.add(key);
|
|
336
|
+
}
|
|
337
|
+
for (const plan of plans) {
|
|
338
|
+
const safeHost = toPascalCase(getSafeZoneName(plan.hostname));
|
|
339
|
+
const record = new AliasRecord(this, `${safeHost}OriginRecord`, {
|
|
340
|
+
zone: plan.hostedZone,
|
|
341
|
+
zoneName: plan.zoneName,
|
|
342
|
+
recordName: plan.recordName,
|
|
343
|
+
target: new LoadBalancerTarget(plan.loadBalancer)
|
|
344
|
+
});
|
|
345
|
+
this.getDistribution().node.addDependency(record);
|
|
346
|
+
}
|
|
183
347
|
}
|
|
184
348
|
/**
|
|
185
349
|
* Resolve a `domainConfig` through the shared pattern-domain helpers
|
|
@@ -229,16 +393,35 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
229
393
|
});
|
|
230
394
|
}
|
|
231
395
|
/**
|
|
232
|
-
* Resolve ICdnProps to
|
|
396
|
+
* Resolve ICdnProps to the distribution props PLUS the Cdn-owned origin
|
|
397
|
+
* record plans the resolution produced — one resolution pass, one result.
|
|
233
398
|
*/
|
|
234
399
|
static resolveProps(scope, id, props, domain) {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
400
|
+
const ecsOriginResolver = Cdn.createEcsOriginResolver(id, props);
|
|
401
|
+
const defaultOrigin = Cdn.resolveDefaultOrigin(props, ecsOriginResolver);
|
|
402
|
+
const behaviours = Cdn.resolveBehaviours(props.behaviours, defaultOrigin, ecsOriginResolver, {
|
|
403
|
+
cachePolicy: props.cachePolicy,
|
|
404
|
+
allowedMethods: props.defaultAllowedMethods
|
|
405
|
+
});
|
|
237
406
|
const appName = props.appName;
|
|
238
407
|
const domainNames = props.domainNames ??
|
|
239
408
|
(props.domainConfig !== undefined
|
|
240
409
|
? [props.domainConfig.domainName]
|
|
241
410
|
: undefined);
|
|
411
|
+
// ANY origin resolved through the ECS profile lane serves a hostname
|
|
412
|
+
// that differs from the viewer-facing aliases, so host fidelity via
|
|
413
|
+
// x-forwarded-host is the sensible default (design D3 step 9) — for a
|
|
414
|
+
// behaviour's ECS origin exactly as for the default origin, since the
|
|
415
|
+
// one viewer-request function rides every behaviour. Explicit
|
|
416
|
+
// `forwardHostHeader: false` wins.
|
|
417
|
+
const hasEcsOrigin = (props.originType === "auto" &&
|
|
418
|
+
isCompute(props.origin) &&
|
|
419
|
+
isEcsCompute(props.origin)) ||
|
|
420
|
+
(props.behaviours ?? []).some((behaviour) => behaviour.origin !== undefined &&
|
|
421
|
+
typeof behaviour.origin !== "string" &&
|
|
422
|
+
isCompute(behaviour.origin) &&
|
|
423
|
+
isEcsCompute(behaviour.origin));
|
|
424
|
+
const impliedForwardHostHeader = hasEcsOrigin && (domainNames?.length ?? 0) > 0 ? true : undefined;
|
|
242
425
|
const certificate = props.certificate ??
|
|
243
426
|
(props.certificateArn
|
|
244
427
|
? Certificate.fromCertificateArn(scope, `${id}Certificate`, props.certificateArn)
|
|
@@ -253,40 +436,65 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
253
436
|
}
|
|
254
437
|
: {};
|
|
255
438
|
return {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
439
|
+
distributionProps: {
|
|
440
|
+
appName,
|
|
441
|
+
defaultOrigin,
|
|
442
|
+
defaultCachePolicy: props.cachePolicy,
|
|
443
|
+
defaultAllowedMethods: props.defaultAllowedMethods,
|
|
444
|
+
behaviours,
|
|
445
|
+
domainNames,
|
|
446
|
+
certificate,
|
|
447
|
+
comment: props.comment,
|
|
448
|
+
enableLogging: props.enableLogging,
|
|
449
|
+
logBucket: props.logBucket,
|
|
450
|
+
priceClass: props.priceClass,
|
|
451
|
+
forwardHostHeader: props.forwardHostHeader ?? impliedForwardHostHeader,
|
|
452
|
+
accessGate: props.accessGate,
|
|
453
|
+
...s3Routing
|
|
454
|
+
},
|
|
455
|
+
recordPlans: ecsOriginResolver.getRecordPlans()
|
|
270
456
|
};
|
|
271
457
|
}
|
|
272
458
|
/**
|
|
273
|
-
* Transform smart behaviours to CdnBehaviour objects.
|
|
459
|
+
* Transform smart behaviours to CdnBehaviour objects. A behaviour with no
|
|
460
|
+
* origin routes to the distribution's default origin — the same resolved
|
|
461
|
+
* config, so the resource layer memoizes them onto one distribution
|
|
462
|
+
* origin entry.
|
|
463
|
+
*
|
|
464
|
+
* Knob inheritance is asymmetric BY CONTRACT. An origin-less behaviour IS
|
|
465
|
+
* the default behaviour varied per path (the shape
|
|
466
|
+
* `SmartCdnBehaviour.origin` advertises), so its omitted
|
|
467
|
+
* `cachePolicy`/`allowedMethods` inherit the DISTRIBUTION's defaults —
|
|
468
|
+
* otherwise a caching-only behaviour on an `allowedMethods: "ALL"` API
|
|
469
|
+
* distribution would snap its path back to the resource layer's GET/HEAD
|
|
470
|
+
* fallback and 403 every POST at the edge. A behaviour WITH its own
|
|
471
|
+
* origin keeps the legacy resource-layer fallbacks (GET_HEAD /
|
|
472
|
+
* CACHING_OPTIMIZED): those deployed behaviours are byte-frozen, and an
|
|
473
|
+
* independent origin is an independent config, not a variation of the
|
|
474
|
+
* default behaviour.
|
|
274
475
|
*/
|
|
275
|
-
static resolveBehaviours(behaviours) {
|
|
476
|
+
static resolveBehaviours(behaviours, defaultOrigin, ecsOriginResolver, distributionDefaults) {
|
|
276
477
|
if (!behaviours || behaviours.length === 0) {
|
|
277
478
|
return undefined;
|
|
278
479
|
}
|
|
279
|
-
return behaviours.map((behaviour) =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
480
|
+
return behaviours.map((behaviour) => behaviour.origin === undefined
|
|
481
|
+
? {
|
|
482
|
+
pathPattern: behaviour.pathPattern,
|
|
483
|
+
origin: defaultOrigin,
|
|
484
|
+
cachePolicy: behaviour.cachePolicy ?? distributionDefaults.cachePolicy,
|
|
485
|
+
allowedMethods: behaviour.allowedMethods ?? distributionDefaults.allowedMethods
|
|
486
|
+
}
|
|
487
|
+
: {
|
|
488
|
+
pathPattern: behaviour.pathPattern,
|
|
489
|
+
origin: Cdn.detectOriginFromResource(behaviour.origin, ecsOriginResolver),
|
|
490
|
+
cachePolicy: behaviour.cachePolicy,
|
|
491
|
+
allowedMethods: behaviour.allowedMethods
|
|
492
|
+
});
|
|
285
493
|
}
|
|
286
494
|
/**
|
|
287
495
|
* Resolve the default origin from ICdnProps.
|
|
288
496
|
*/
|
|
289
|
-
static resolveDefaultOrigin(props) {
|
|
497
|
+
static resolveDefaultOrigin(props, ecsOriginResolver) {
|
|
290
498
|
switch (props.originType) {
|
|
291
499
|
case "s3": {
|
|
292
500
|
const bucket = isStorage(props.bucket)
|
|
@@ -332,7 +540,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
332
540
|
protocolPolicy: props.protocolPolicy
|
|
333
541
|
};
|
|
334
542
|
case "auto":
|
|
335
|
-
return Cdn.detectOriginFromResource(props.origin);
|
|
543
|
+
return Cdn.detectOriginFromResource(props.origin, ecsOriginResolver);
|
|
336
544
|
default: {
|
|
337
545
|
const _exhaustive = props;
|
|
338
546
|
throw new Error(`Unsupported CDN origin type: ${props.originType}`);
|
|
@@ -342,7 +550,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
342
550
|
/**
|
|
343
551
|
* Auto-detect origin configuration from a Fjall resource.
|
|
344
552
|
*/
|
|
345
|
-
static detectOriginFromResource(resource) {
|
|
553
|
+
static detectOriginFromResource(resource, ecsOriginResolver) {
|
|
346
554
|
// String → HTTP origin
|
|
347
555
|
if (typeof resource === "string") {
|
|
348
556
|
return {
|
|
@@ -357,15 +565,13 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
357
565
|
bucket: resource.getBucket()
|
|
358
566
|
};
|
|
359
567
|
}
|
|
360
|
-
// ECS Compute →
|
|
568
|
+
// ECS Compute → HTTP origin on a resolved, certificate-covered origin
|
|
569
|
+
// hostname (design 2026-08-18 cdn-app-origin, D3). The former raw-ELB
|
|
570
|
+
// alb-origin emission was TLS-broken by construction: the resource
|
|
571
|
+
// layer's HTTPS_ONLY default validated the certificate against
|
|
572
|
+
// *.elb.amazonaws.com, which no ACM certificate covers.
|
|
361
573
|
if (isEcsCompute(resource)) {
|
|
362
|
-
|
|
363
|
-
if (loadBalancer) {
|
|
364
|
-
return {
|
|
365
|
-
type: "alb",
|
|
366
|
-
loadBalancer
|
|
367
|
-
};
|
|
368
|
-
}
|
|
574
|
+
return ecsOriginResolver.resolve(resource).originConfig;
|
|
369
575
|
}
|
|
370
576
|
// Lambda Compute → HTTP origin (function URL)
|
|
371
577
|
if (isLambdaCompute(resource)) {
|
|
@@ -382,6 +588,10 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
382
588
|
domainName
|
|
383
589
|
};
|
|
384
590
|
}
|
|
591
|
+
// Targeted error (design D3): a behaviour origin used to fall through
|
|
592
|
+
// to the generic detect throw for this shape.
|
|
593
|
+
throw new Error("Lambda compute must have a function URL for CDN origin. " +
|
|
594
|
+
"Enable functionUrl in your Lambda compute configuration.");
|
|
385
595
|
}
|
|
386
596
|
throw new Error(`Unable to detect CDN origin from resource: ${typeof resource}. ` +
|
|
387
597
|
"Provide explicit origin configuration using originType.");
|