@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.
@@ -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 { DNS_APEX } from "@fjall/util";
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
- * Relative record label for `domain` within `zoneName` (the staticSite
12
- * `recordLabelFor` convention): the zone apex maps to the canonical apex
13
- * label, sub-names drop the zone suffix.
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 recordLabelWithin(domain, zoneName) {
16
- if (domain === zoneName) {
17
- return DNS_APEX;
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 suffix = `.${zoneName}`;
20
- return domain.endsWith(suffix) ? domain.slice(0, -suffix.length) : domain;
21
- }
22
- /** Lowercase and strip the trailing dot — DNS names are case-insensitive. */
23
- function normaliseDnsName(name) {
24
- return name.toLowerCase().replace(/\.$/, "");
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 = new Set();
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
- if (isCompute(props.loadBalancer) && isEcsCompute(props.loadBalancer)) {
136
- const lb = props.loadBalancer.getLoadBalancer();
137
- if (!lb) {
138
- throw new Error("Compute resource does not have a load balancer. " +
139
- "Ensure ECS compute has loadBalancer enabled for CDN origin.");
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
- const resolvedProps = Cdn.resolveProps(scope, id, props, domain);
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 → ALB origin
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
- const loadBalancer = resource.getLoadBalancer();
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
+ }