@fjall/components-infrastructure 14.1.0 → 14.2.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/cdn.d.ts +55 -1
- package/dist/lib/patterns/aws/cdn.js +216 -48
- package/dist/lib/patterns/aws/cdnAppOrigin.d.ts +74 -0
- package/dist/lib/patterns/aws/cdnAppOrigin.js +251 -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/domainCertificateComposer.js +69 -0
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +7 -0
- 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 +76 -15
- package/dist/lib/resources/aws/compute/ingressProfile.d.ts +174 -0
- package/dist/lib/resources/aws/compute/ingressProfile.js +195 -0
- package/dist/lib/utils/managedDomainContext.d.ts +26 -1
- package/dist/lib/utils/managedDomainContext.js +69 -0
- package/package.json +3 -3
|
@@ -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
|
/**
|
|
@@ -146,12 +171,41 @@ export interface SmartCdnBehaviour {
|
|
|
146
171
|
origin: Storage | AnyCompute | string;
|
|
147
172
|
cachePolicy?: CachePolicyPreset | ICachePolicy;
|
|
148
173
|
allowedMethods?: "GET_HEAD" | "GET_HEAD_OPTIONS" | "ALL";
|
|
174
|
+
/**
|
|
175
|
+
* Origin hostname override for THIS behaviour's ECS compute origin — the
|
|
176
|
+
* behaviour-level counterpart of the distribution's `originHostname`
|
|
177
|
+
* (same semantics; see {@link SmartCdnProps.originHostname}). Overrides
|
|
178
|
+
* bind per compute: two entries naming the same compute must agree.
|
|
179
|
+
*/
|
|
180
|
+
originHostname?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Record-minting override for THIS behaviour's ECS compute origin — the
|
|
183
|
+
* behaviour-level counterpart of {@link SmartCdnProps.originRecord}.
|
|
184
|
+
*/
|
|
185
|
+
originRecord?: "alias" | "none";
|
|
149
186
|
}
|
|
150
187
|
/**
|
|
151
188
|
* CDN wrapper class that extends CloudFrontDistribution with Fjall patterns.
|
|
152
189
|
*/
|
|
153
190
|
export declare class Cdn extends CloudFrontDistribution implements ICdn {
|
|
154
191
|
constructor(scope: Construct, id: string, props: ICdnProps);
|
|
192
|
+
private static createEcsOriginResolver;
|
|
193
|
+
/**
|
|
194
|
+
* Assemble the per-compute `originHostname`/`originRecord` overrides the
|
|
195
|
+
* resolver binds by compute identity: the distribution-level props apply
|
|
196
|
+
* to the DEFAULT origin's compute, each behaviour's own overrides apply
|
|
197
|
+
* to that behaviour's compute. Two entries naming the same compute must
|
|
198
|
+
* agree — one alias record cannot carry two spellings.
|
|
199
|
+
*/
|
|
200
|
+
private static collectEcsOriginOverrides;
|
|
201
|
+
/**
|
|
202
|
+
* Mint the Cdn-owned origin alias records (P1) after the distribution
|
|
203
|
+
* exists, using the compute profile's own zone instance so the DNS claim
|
|
204
|
+
* registry sees the same zone identity as the cluster's records. The
|
|
205
|
+
* distribution depends on each record: the hostname must resolve before a
|
|
206
|
+
* distribution update referencing it propagates.
|
|
207
|
+
*/
|
|
208
|
+
private createOriginRecords;
|
|
155
209
|
/**
|
|
156
210
|
* Resolve a `domainConfig` through the shared pattern-domain helpers
|
|
157
211
|
* (design C1): zone identity via `resolvePatternZone` (explicit
|
|
@@ -1,27 +1,30 @@
|
|
|
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
7
|
import { resolvePatternZone, resolvePatternCloudFrontCertificate } from "./patternDomain.js";
|
|
8
8
|
import { isStorage } from "./storage.js";
|
|
9
|
+
import { CdnEcsOriginResolver, recordLabelWithin, normaliseDnsName } from "./cdnAppOrigin.js";
|
|
9
10
|
import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* The distribution's own alias names, normalised — consumed by the
|
|
13
|
+
* origin-loop guard and by the ECS origin resolver (auto-resolution excludes
|
|
14
|
+
* them from routed-host candidates, and a resolved origin equal to one is
|
|
15
|
+
* refused as a loop).
|
|
14
16
|
*/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
function collectAliasNames(props) {
|
|
18
|
+
const aliasNames = new Set();
|
|
19
|
+
if (props.domainConfig !== undefined) {
|
|
20
|
+
aliasNames.add(normaliseDnsName(props.domainConfig.domainName));
|
|
18
21
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
22
|
+
for (const name of props.domainNames ?? []) {
|
|
23
|
+
if (!Token.isUnresolved(name)) {
|
|
24
|
+
aliasNames.add(normaliseDnsName(name));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return aliasNames;
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* Validates CDN props — synth-time hard errors for shapes CloudFormation
|
|
@@ -69,15 +72,7 @@ function validateCdnProps(props) {
|
|
|
69
72
|
// every request CloudFront → DNS → CloudFront until the request-depth
|
|
70
73
|
// limit 508s. Synth can see this shape whenever the origin hostname is a
|
|
71
74
|
// 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
|
-
}
|
|
75
|
+
const aliasNames = collectAliasNames(props);
|
|
81
76
|
const defaultOriginHostname = props.originType === "http"
|
|
82
77
|
? props.domainName
|
|
83
78
|
: props.originType === "auto" && typeof props.origin === "string"
|
|
@@ -130,18 +125,66 @@ function validateCdnProps(props) {
|
|
|
130
125
|
"'certificateArn', or configure the pattern's 'domain' with a zone " +
|
|
131
126
|
"identity so it provisions one.");
|
|
132
127
|
}
|
|
133
|
-
// Validate ALB origin
|
|
128
|
+
// Validate ALB origin (design 2026-08-18 cdn-app-origin, D4): CloudFront
|
|
129
|
+
// validates the origin certificate against the raw `*.elb.amazonaws.com`
|
|
130
|
+
// hostname, which no ACM certificate ever covers — an effective-HTTPS alb
|
|
131
|
+
// origin can never complete a TLS handshake. E9, with the cure conditional
|
|
132
|
+
// on the listener protocol: HTTP_ONLY only works against a port-80
|
|
133
|
+
// listener, and a cert-bearing cluster has exactly one listener, on 443.
|
|
134
134
|
if (props.originType === "alb") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
const computeRef = isCompute(props.loadBalancer) && isEcsCompute(props.loadBalancer)
|
|
136
|
+
? props.loadBalancer
|
|
137
|
+
: undefined;
|
|
138
|
+
if (computeRef !== undefined && !computeRef.getLoadBalancer()) {
|
|
139
|
+
throw new Error("Compute resource does not have a load balancer. " +
|
|
140
|
+
"Ensure ECS compute has loadBalancer enabled for CDN origin.");
|
|
141
|
+
}
|
|
142
|
+
const profile = computeRef?.getIngressProfile();
|
|
143
|
+
if (profile?.internal) {
|
|
144
|
+
// E2 — same defect as the construct-origin lane: no public path in.
|
|
145
|
+
throw new Error("CDN alb origin: the load balancer is internal — CloudFront " +
|
|
146
|
+
"reaches origins over the public internet and cannot address an " +
|
|
147
|
+
"internal ALB. Make the load balancer internet-facing.");
|
|
148
|
+
}
|
|
149
|
+
const effectiveProtocol = props.protocolPolicy ?? "HTTPS_ONLY";
|
|
150
|
+
if (effectiveProtocol !== "HTTP_ONLY") {
|
|
151
|
+
// E9 — effective HTTPS to the raw ELB hostname.
|
|
152
|
+
const cure = profile === undefined
|
|
153
|
+
? 'If the ALB has an HTTP (port-80) listener, set protocolPolicy: "HTTP_ONLY"; ' +
|
|
154
|
+
'otherwise use originType "auto" with the compute as origin, ' +
|
|
155
|
+
"which resolves a certificate-covered origin hostname."
|
|
156
|
+
: profile.listenerPort === 80
|
|
157
|
+
? 'Set protocolPolicy: "HTTP_ONLY" (the cluster\'s listener serves HTTP), ' +
|
|
158
|
+
'or use originType "auto" with the compute as origin.'
|
|
159
|
+
: 'Use originType "auto" with the compute as origin — the ' +
|
|
160
|
+
"cluster's only listener is HTTPS (443), so the sole working " +
|
|
161
|
+
"path is a certificate-covered origin hostname.";
|
|
162
|
+
throw new Error(`CDN alb origin with protocolPolicy '${effectiveProtocol}' can ` +
|
|
163
|
+
"never complete a TLS handshake: CloudFront validates the origin " +
|
|
164
|
+
"certificate against the raw ELB hostname " +
|
|
165
|
+
"(*.elb.amazonaws.com), which no ACM certificate covers — every " +
|
|
166
|
+
`origin fetch fails with a 502. ${cure}`);
|
|
167
|
+
}
|
|
168
|
+
if (profile !== undefined && profile.listenerPort === 443) {
|
|
169
|
+
// E9 (HTTP_ONLY variant) — nothing listens on 80: a connect-timeout
|
|
170
|
+
// 504 instead of a TLS 502, equally broken.
|
|
171
|
+
throw new Error('CDN alb origin with protocolPolicy "HTTP_ONLY": the cluster\'s ' +
|
|
172
|
+
"only listener is HTTPS (443) — a certificate resolved, so " +
|
|
173
|
+
"nothing listens on port 80 and every origin fetch times out. " +
|
|
174
|
+
'Use originType "auto" with the compute as origin instead.');
|
|
141
175
|
}
|
|
142
176
|
}
|
|
143
177
|
// Validate smart origin
|
|
144
178
|
if (props.originType === "auto") {
|
|
179
|
+
if (!(isCompute(props.origin) && isEcsCompute(props.origin)) &&
|
|
180
|
+
(props.originHostname !== undefined || props.originRecord !== undefined)) {
|
|
181
|
+
throw new Error("CDN 'originHostname'/'originRecord' configure the distribution's " +
|
|
182
|
+
"DEFAULT origin, and only when it is an ECS compute — they " +
|
|
183
|
+
"resolve the origin hostname against that compute's ingress " +
|
|
184
|
+
"profile. Drop them for storage, Lambda, and literal-hostname " +
|
|
185
|
+
"default origins; a behaviour's ECS origin takes the same " +
|
|
186
|
+
"overrides on the behaviour entry itself.");
|
|
187
|
+
}
|
|
145
188
|
if (isCompute(props.origin)) {
|
|
146
189
|
if (isEcsCompute(props.origin)) {
|
|
147
190
|
if (!props.origin.getLoadBalancer()) {
|
|
@@ -175,11 +218,124 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
175
218
|
const domain = props.domainConfig !== undefined && props.app !== undefined
|
|
176
219
|
? Cdn.resolveDomain(scope, id, props.domainConfig, props.app)
|
|
177
220
|
: undefined;
|
|
178
|
-
|
|
221
|
+
// ECS construct-reference origins resolve through one memoized resolver
|
|
222
|
+
// (design 2026-08-18 cdn-app-origin, D3): the default origin and every
|
|
223
|
+
// behaviour referencing the same compute share one resolution and at
|
|
224
|
+
// most one origin record.
|
|
225
|
+
const ecsOriginResolver = Cdn.createEcsOriginResolver(id, props);
|
|
226
|
+
const resolvedProps = Cdn.resolveProps(scope, id, props, domain, ecsOriginResolver);
|
|
179
227
|
super(scope, id, resolvedProps);
|
|
180
228
|
if (domain !== undefined && props.domainConfig !== undefined) {
|
|
181
229
|
this.createDomainRecord(id, props.domainConfig, domain);
|
|
182
230
|
}
|
|
231
|
+
this.createOriginRecords(ecsOriginResolver.getRecordPlans());
|
|
232
|
+
}
|
|
233
|
+
static createEcsOriginResolver(id, props) {
|
|
234
|
+
return new CdnEcsOriginResolver({
|
|
235
|
+
cdnId: id,
|
|
236
|
+
aliasNames: collectAliasNames(props),
|
|
237
|
+
overrides: Cdn.collectEcsOriginOverrides(id, props)
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Assemble the per-compute `originHostname`/`originRecord` overrides the
|
|
242
|
+
* resolver binds by compute identity: the distribution-level props apply
|
|
243
|
+
* to the DEFAULT origin's compute, each behaviour's own overrides apply
|
|
244
|
+
* to that behaviour's compute. Two entries naming the same compute must
|
|
245
|
+
* agree — one alias record cannot carry two spellings.
|
|
246
|
+
*/
|
|
247
|
+
static collectEcsOriginOverrides(id, props) {
|
|
248
|
+
const overrides = new Map();
|
|
249
|
+
const claim = (compute, override, site) => {
|
|
250
|
+
const existing = overrides.get(compute) ?? {};
|
|
251
|
+
for (const key of ["originHostname", "originRecord"]) {
|
|
252
|
+
const incoming = override[key];
|
|
253
|
+
if (incoming === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
const held = existing[key];
|
|
256
|
+
if (held !== undefined && held !== incoming) {
|
|
257
|
+
throw new Error(`CDN '${id}': conflicting ${key} overrides for the same ECS ` +
|
|
258
|
+
`compute origin ('${held}' vs '${incoming}' from ${site}) — ` +
|
|
259
|
+
"overrides bind per compute, so every entry naming it must " +
|
|
260
|
+
"agree. Keep one spelling.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
overrides.set(compute, { ...existing, ...override });
|
|
264
|
+
};
|
|
265
|
+
if (props.originType === "auto") {
|
|
266
|
+
const override = {
|
|
267
|
+
...(props.originHostname !== undefined && {
|
|
268
|
+
originHostname: props.originHostname
|
|
269
|
+
}),
|
|
270
|
+
...(props.originRecord !== undefined && {
|
|
271
|
+
originRecord: props.originRecord
|
|
272
|
+
})
|
|
273
|
+
};
|
|
274
|
+
if ((override.originHostname !== undefined ||
|
|
275
|
+
override.originRecord !== undefined) &&
|
|
276
|
+
isCompute(props.origin) &&
|
|
277
|
+
isEcsCompute(props.origin)) {
|
|
278
|
+
claim(props.origin, override, "the distribution props");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const behaviour of props.behaviours ?? []) {
|
|
282
|
+
if (behaviour.originHostname === undefined &&
|
|
283
|
+
behaviour.originRecord === undefined) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (typeof behaviour.origin === "string" ||
|
|
287
|
+
!isCompute(behaviour.origin) ||
|
|
288
|
+
!isEcsCompute(behaviour.origin)) {
|
|
289
|
+
throw new Error(`CDN '${id}': behaviour '${behaviour.pathPattern}' sets ` +
|
|
290
|
+
"'originHostname'/'originRecord', but its origin is not an ECS " +
|
|
291
|
+
"compute — they configure how the origin hostname resolves " +
|
|
292
|
+
"against the compute's ingress profile. Drop them for storage, " +
|
|
293
|
+
"Lambda, and literal-hostname origins.");
|
|
294
|
+
}
|
|
295
|
+
claim(behaviour.origin, {
|
|
296
|
+
...(behaviour.originHostname !== undefined && {
|
|
297
|
+
originHostname: behaviour.originHostname
|
|
298
|
+
}),
|
|
299
|
+
...(behaviour.originRecord !== undefined && {
|
|
300
|
+
originRecord: behaviour.originRecord
|
|
301
|
+
})
|
|
302
|
+
}, `behaviour '${behaviour.pathPattern}'`);
|
|
303
|
+
}
|
|
304
|
+
return overrides;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Mint the Cdn-owned origin alias records (P1) after the distribution
|
|
308
|
+
* exists, using the compute profile's own zone instance so the DNS claim
|
|
309
|
+
* registry sees the same zone identity as the cluster's records. The
|
|
310
|
+
* distribution depends on each record: the hostname must resolve before a
|
|
311
|
+
* distribution update referencing it propagates.
|
|
312
|
+
*/
|
|
313
|
+
createOriginRecords(plans) {
|
|
314
|
+
// Plans are memoized per compute, so two entries with one hostname mean
|
|
315
|
+
// two DIFFERENT computes resolved the same name — an inherently invalid
|
|
316
|
+
// shape (one alias record cannot target two load balancers). Refuse it
|
|
317
|
+
// with the cure instead of letting CDK's duplicate-construct-id throw.
|
|
318
|
+
const byHostname = new Map();
|
|
319
|
+
for (const plan of plans) {
|
|
320
|
+
const key = normaliseDnsName(plan.hostname);
|
|
321
|
+
if (byHostname.has(key)) {
|
|
322
|
+
throw new Error(`CDN '${this.node.id}': two ECS compute origins resolved the ` +
|
|
323
|
+
`same origin hostname '${plan.hostname}' — one alias record ` +
|
|
324
|
+
"cannot target two load balancers. Give one of them a " +
|
|
325
|
+
"distinct originHostname.");
|
|
326
|
+
}
|
|
327
|
+
byHostname.set(key, plan);
|
|
328
|
+
}
|
|
329
|
+
for (const plan of plans) {
|
|
330
|
+
const safeHost = toPascalCase(getSafeZoneName(plan.hostname));
|
|
331
|
+
const record = new AliasRecord(this, `${safeHost}OriginRecord`, {
|
|
332
|
+
zone: plan.hostedZone,
|
|
333
|
+
zoneName: plan.zoneName,
|
|
334
|
+
recordName: plan.recordName,
|
|
335
|
+
target: new LoadBalancerTarget(plan.loadBalancer)
|
|
336
|
+
});
|
|
337
|
+
this.getDistribution().node.addDependency(record);
|
|
338
|
+
}
|
|
183
339
|
}
|
|
184
340
|
/**
|
|
185
341
|
* Resolve a `domainConfig` through the shared pattern-domain helpers
|
|
@@ -231,14 +387,24 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
231
387
|
/**
|
|
232
388
|
* Resolve ICdnProps to CloudFrontDistributionProps.
|
|
233
389
|
*/
|
|
234
|
-
static resolveProps(scope, id, props, domain) {
|
|
235
|
-
const defaultOrigin = Cdn.resolveDefaultOrigin(props);
|
|
236
|
-
const behaviours = Cdn.resolveBehaviours(props.behaviours);
|
|
390
|
+
static resolveProps(scope, id, props, domain, ecsOriginResolver) {
|
|
391
|
+
const defaultOrigin = Cdn.resolveDefaultOrigin(props, ecsOriginResolver);
|
|
392
|
+
const behaviours = Cdn.resolveBehaviours(props.behaviours, ecsOriginResolver);
|
|
237
393
|
const appName = props.appName;
|
|
238
394
|
const domainNames = props.domainNames ??
|
|
239
395
|
(props.domainConfig !== undefined
|
|
240
396
|
? [props.domainConfig.domainName]
|
|
241
397
|
: undefined);
|
|
398
|
+
// A default origin resolved through the ECS profile lane serves a
|
|
399
|
+
// hostname that differs from the viewer-facing aliases, so host
|
|
400
|
+
// fidelity via x-forwarded-host is the sensible default (design D3
|
|
401
|
+
// step 9). Explicit `forwardHostHeader: false` wins.
|
|
402
|
+
const impliedForwardHostHeader = props.originType === "auto" &&
|
|
403
|
+
isCompute(props.origin) &&
|
|
404
|
+
isEcsCompute(props.origin) &&
|
|
405
|
+
(domainNames?.length ?? 0) > 0
|
|
406
|
+
? true
|
|
407
|
+
: undefined;
|
|
242
408
|
const certificate = props.certificate ??
|
|
243
409
|
(props.certificateArn
|
|
244
410
|
? Certificate.fromCertificateArn(scope, `${id}Certificate`, props.certificateArn)
|
|
@@ -264,7 +430,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
264
430
|
enableLogging: props.enableLogging,
|
|
265
431
|
logBucket: props.logBucket,
|
|
266
432
|
priceClass: props.priceClass,
|
|
267
|
-
forwardHostHeader: props.forwardHostHeader,
|
|
433
|
+
forwardHostHeader: props.forwardHostHeader ?? impliedForwardHostHeader,
|
|
268
434
|
accessGate: props.accessGate,
|
|
269
435
|
...s3Routing
|
|
270
436
|
};
|
|
@@ -272,13 +438,13 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
272
438
|
/**
|
|
273
439
|
* Transform smart behaviours to CdnBehaviour objects.
|
|
274
440
|
*/
|
|
275
|
-
static resolveBehaviours(behaviours) {
|
|
441
|
+
static resolveBehaviours(behaviours, ecsOriginResolver) {
|
|
276
442
|
if (!behaviours || behaviours.length === 0) {
|
|
277
443
|
return undefined;
|
|
278
444
|
}
|
|
279
445
|
return behaviours.map((behaviour) => ({
|
|
280
446
|
pathPattern: behaviour.pathPattern,
|
|
281
|
-
origin: Cdn.detectOriginFromResource(behaviour.origin),
|
|
447
|
+
origin: Cdn.detectOriginFromResource(behaviour.origin, ecsOriginResolver),
|
|
282
448
|
cachePolicy: behaviour.cachePolicy,
|
|
283
449
|
allowedMethods: behaviour.allowedMethods
|
|
284
450
|
}));
|
|
@@ -286,7 +452,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
286
452
|
/**
|
|
287
453
|
* Resolve the default origin from ICdnProps.
|
|
288
454
|
*/
|
|
289
|
-
static resolveDefaultOrigin(props) {
|
|
455
|
+
static resolveDefaultOrigin(props, ecsOriginResolver) {
|
|
290
456
|
switch (props.originType) {
|
|
291
457
|
case "s3": {
|
|
292
458
|
const bucket = isStorage(props.bucket)
|
|
@@ -332,7 +498,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
332
498
|
protocolPolicy: props.protocolPolicy
|
|
333
499
|
};
|
|
334
500
|
case "auto":
|
|
335
|
-
return Cdn.detectOriginFromResource(props.origin);
|
|
501
|
+
return Cdn.detectOriginFromResource(props.origin, ecsOriginResolver);
|
|
336
502
|
default: {
|
|
337
503
|
const _exhaustive = props;
|
|
338
504
|
throw new Error(`Unsupported CDN origin type: ${props.originType}`);
|
|
@@ -342,7 +508,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
342
508
|
/**
|
|
343
509
|
* Auto-detect origin configuration from a Fjall resource.
|
|
344
510
|
*/
|
|
345
|
-
static detectOriginFromResource(resource) {
|
|
511
|
+
static detectOriginFromResource(resource, ecsOriginResolver) {
|
|
346
512
|
// String → HTTP origin
|
|
347
513
|
if (typeof resource === "string") {
|
|
348
514
|
return {
|
|
@@ -357,15 +523,13 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
357
523
|
bucket: resource.getBucket()
|
|
358
524
|
};
|
|
359
525
|
}
|
|
360
|
-
// ECS Compute →
|
|
526
|
+
// ECS Compute → HTTP origin on a resolved, certificate-covered origin
|
|
527
|
+
// hostname (design 2026-08-18 cdn-app-origin, D3). The former raw-ELB
|
|
528
|
+
// alb-origin emission was TLS-broken by construction: the resource
|
|
529
|
+
// layer's HTTPS_ONLY default validated the certificate against
|
|
530
|
+
// *.elb.amazonaws.com, which no ACM certificate covers.
|
|
361
531
|
if (isEcsCompute(resource)) {
|
|
362
|
-
|
|
363
|
-
if (loadBalancer) {
|
|
364
|
-
return {
|
|
365
|
-
type: "alb",
|
|
366
|
-
loadBalancer
|
|
367
|
-
};
|
|
368
|
-
}
|
|
532
|
+
return ecsOriginResolver.resolve(resource).originConfig;
|
|
369
533
|
}
|
|
370
534
|
// Lambda Compute → HTTP origin (function URL)
|
|
371
535
|
if (isLambdaCompute(resource)) {
|
|
@@ -382,6 +546,10 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
382
546
|
domainName
|
|
383
547
|
};
|
|
384
548
|
}
|
|
549
|
+
// Targeted error (design D3): a behaviour origin used to fall through
|
|
550
|
+
// to the generic detect throw for this shape.
|
|
551
|
+
throw new Error("Lambda compute must have a function URL for CDN origin. " +
|
|
552
|
+
"Enable functionUrl in your Lambda compute configuration.");
|
|
385
553
|
}
|
|
386
554
|
throw new Error(`Unable to detect CDN origin from resource: ${typeof resource}. ` +
|
|
387
555
|
"Provide explicit origin configuration using originType.");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECS construct-reference origin resolution for the `Cdn` pattern (design
|
|
3
|
+
* 2026-08-18 cdn-app-origin, D3). Given a fronted ECS compute, resolve a
|
|
4
|
+
* TLS-valid ORIGIN HOSTNAME from the compute's own ingress profile, decide
|
|
5
|
+
* who owns its DNS record (P1: the construct that declares a hostname owns
|
|
6
|
+
* its record — a `routing[].host` hostname is the compute's; a derived or
|
|
7
|
+
* free hostname is the Cdn's), and validate certificate coverage and
|
|
8
|
+
* listener forwarding at synth — fail closed where decidable, warn where
|
|
9
|
+
* opaque or partial (P3).
|
|
10
|
+
*
|
|
11
|
+
* The resolver is memoized per compute within one `Cdn`: the default origin
|
|
12
|
+
* and every behaviour referencing the same compute share one resolution and
|
|
13
|
+
* at most one record.
|
|
14
|
+
*/
|
|
15
|
+
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
16
|
+
import type { IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
17
|
+
import type { CdnOriginConfig } from "../../resources/aws/cdn/index.js";
|
|
18
|
+
import type { IEcsCompute } from "./interfaces/compute.js";
|
|
19
|
+
import { normaliseDnsName } from "../../resources/aws/compute/ingressProfile.js";
|
|
20
|
+
export { normaliseDnsName };
|
|
21
|
+
/**
|
|
22
|
+
* Relative record label for `domain` within `zoneName` (the staticSite
|
|
23
|
+
* `recordLabelFor` convention): the zone apex maps to the canonical apex
|
|
24
|
+
* label, sub-names drop the zone suffix.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordLabelWithin(domain: string, zoneName: string): string;
|
|
27
|
+
/** A Cdn-owned origin alias record to mint after the distribution exists. */
|
|
28
|
+
export interface EcsOriginRecordPlan {
|
|
29
|
+
hostname: string;
|
|
30
|
+
hostedZone: IHostedZone;
|
|
31
|
+
zoneName: string;
|
|
32
|
+
recordName: string;
|
|
33
|
+
loadBalancer: IApplicationLoadBalancer;
|
|
34
|
+
}
|
|
35
|
+
export interface ResolvedEcsOrigin {
|
|
36
|
+
hostname: string;
|
|
37
|
+
originConfig: CdnOriginConfig;
|
|
38
|
+
/** Present when the Cdn owns the record and `originRecord` is not "none". */
|
|
39
|
+
recordPlan?: EcsOriginRecordPlan;
|
|
40
|
+
/** True when the hostname is a compute-declared routing host (P1). */
|
|
41
|
+
computeOwnsRecord: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Explicit per-compute origin overrides: the distribution-level
|
|
45
|
+
* `originHostname`/`originRecord` for the default origin's compute, and each
|
|
46
|
+
* behaviour's own `originHostname`/`originRecord` for its compute — so every
|
|
47
|
+
* lane that can throw E3 can also cure it (design D3: behaviour origins get
|
|
48
|
+
* the same targeted validation AND the same override surface).
|
|
49
|
+
*/
|
|
50
|
+
export interface EcsOriginOverride {
|
|
51
|
+
originHostname?: string;
|
|
52
|
+
originRecord?: "alias" | "none";
|
|
53
|
+
}
|
|
54
|
+
export interface CdnEcsOriginResolverOptions {
|
|
55
|
+
cdnId: string;
|
|
56
|
+
/** The distribution's own alias names, normalised — the origin-loop guard. */
|
|
57
|
+
aliasNames: ReadonlySet<string>;
|
|
58
|
+
/** Per-compute explicit overrides — assembled by the Cdn from the
|
|
59
|
+
* distribution props (default origin) and each behaviour entry. */
|
|
60
|
+
overrides?: ReadonlyMap<IEcsCompute, EcsOriginOverride>;
|
|
61
|
+
}
|
|
62
|
+
export declare class CdnEcsOriginResolver {
|
|
63
|
+
private readonly options;
|
|
64
|
+
private readonly resolutions;
|
|
65
|
+
private warnedUnknownCoverage;
|
|
66
|
+
constructor(options: CdnEcsOriginResolverOptions);
|
|
67
|
+
resolve(compute: IEcsCompute): ResolvedEcsOrigin;
|
|
68
|
+
/** Every Cdn-owned record the resolutions so far require, one per compute. */
|
|
69
|
+
getRecordPlans(): EcsOriginRecordPlan[];
|
|
70
|
+
private resolveFresh;
|
|
71
|
+
private explicitHostnameFor;
|
|
72
|
+
private effectiveOriginRecord;
|
|
73
|
+
private selectHostname;
|
|
74
|
+
}
|