@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
|
@@ -1,105 +1,13 @@
|
|
|
1
1
|
import { CaaTag } from "aws-cdk-lib/aws-route53";
|
|
2
2
|
import { ARecord, AaaaRecord, AliasRecord, CnameRecord, MxRecord, TxtRecord, NsRecord, SrvRecord, CaaRecord } from "../../resources/aws/networking/dnsRecord/index.js";
|
|
3
3
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
4
|
-
import { ALIAS_CDK_PREFIX } from "../../utils/domainTypes.js";
|
|
5
4
|
import { DNS_APEX } from "@fjall/util";
|
|
5
|
+
// H13: the legacy BIND-era `composeDnsRecords` (untyped `DnsRecordInput` with
|
|
6
|
+
// ALIAS sentinels in `value`) was deleted in Phase 2. The typed composer below
|
|
7
|
+
// deliberately preserves its construct-id formula — see the eject-contract
|
|
8
|
+
// note on `composeTypedDnsRecords`.
|
|
6
9
|
/**
|
|
7
|
-
*
|
|
8
|
-
* the `DnsRecordInput` shape from the BIND-primary era where ALIAS sentinels
|
|
9
|
-
* live in `value`. Typed targets are not supported here; use
|
|
10
|
-
* `composeTypedDnsRecords` instead.
|
|
11
|
-
*/
|
|
12
|
-
export function composeDnsRecords(scope, zone, zoneName, records) {
|
|
13
|
-
const safeZone = toPascalCase(getSafeZoneName(zoneName));
|
|
14
|
-
records.forEach((record, index) => {
|
|
15
|
-
// Skip alias sentinels — they are resolved via the typed path in Phase 1+.
|
|
16
|
-
if (record.value.startsWith(ALIAS_CDK_PREFIX)) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const safeName = toPascalCase(record.name === DNS_APEX ? "Apex" : record.name);
|
|
20
|
-
const constructId = `${safeZone}${safeName}${record.type}Record${index}`;
|
|
21
|
-
const common = {
|
|
22
|
-
zone,
|
|
23
|
-
zoneName,
|
|
24
|
-
recordName: record.name,
|
|
25
|
-
ttl: record.ttl
|
|
26
|
-
};
|
|
27
|
-
switch (record.type) {
|
|
28
|
-
case "A":
|
|
29
|
-
new ARecord(scope, constructId, {
|
|
30
|
-
...common,
|
|
31
|
-
values: [record.value]
|
|
32
|
-
});
|
|
33
|
-
break;
|
|
34
|
-
case "AAAA":
|
|
35
|
-
new AaaaRecord(scope, constructId, {
|
|
36
|
-
...common,
|
|
37
|
-
values: [record.value]
|
|
38
|
-
});
|
|
39
|
-
break;
|
|
40
|
-
case "CNAME":
|
|
41
|
-
new CnameRecord(scope, constructId, {
|
|
42
|
-
...common,
|
|
43
|
-
domainName: record.value
|
|
44
|
-
});
|
|
45
|
-
break;
|
|
46
|
-
case "MX":
|
|
47
|
-
new MxRecord(scope, constructId, {
|
|
48
|
-
...common,
|
|
49
|
-
values: [
|
|
50
|
-
{
|
|
51
|
-
hostName: record.value,
|
|
52
|
-
priority: record.priority ?? 10
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
});
|
|
56
|
-
break;
|
|
57
|
-
case "TXT":
|
|
58
|
-
new TxtRecord(scope, constructId, {
|
|
59
|
-
...common,
|
|
60
|
-
values: [record.value]
|
|
61
|
-
});
|
|
62
|
-
break;
|
|
63
|
-
case "NS":
|
|
64
|
-
new NsRecord(scope, constructId, {
|
|
65
|
-
...common,
|
|
66
|
-
values: [record.value]
|
|
67
|
-
});
|
|
68
|
-
break;
|
|
69
|
-
case "SRV":
|
|
70
|
-
new SrvRecord(scope, constructId, {
|
|
71
|
-
...common,
|
|
72
|
-
values: [
|
|
73
|
-
{
|
|
74
|
-
hostName: record.value,
|
|
75
|
-
priority: record.priority ?? 10,
|
|
76
|
-
weight: record.weight ?? 0,
|
|
77
|
-
port: record.port ?? 443
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
});
|
|
81
|
-
break;
|
|
82
|
-
case "CAA":
|
|
83
|
-
new CaaRecord(scope, constructId, {
|
|
84
|
-
...common,
|
|
85
|
-
values: [
|
|
86
|
-
{
|
|
87
|
-
flag: 0,
|
|
88
|
-
tag: record.value.includes("issuewild")
|
|
89
|
-
? CaaTag.ISSUEWILD
|
|
90
|
-
: record.value.includes("iodef")
|
|
91
|
-
? CaaTag.IODEF
|
|
92
|
-
: CaaTag.ISSUE,
|
|
93
|
-
value: record.value
|
|
94
|
-
}
|
|
95
|
-
]
|
|
96
|
-
});
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Typed composer consumed by the new `Domain` construct. Accepts the
|
|
10
|
+
* Typed composer consumed by the `Domain` construct. Accepts the
|
|
103
11
|
* discriminated `DnsRecord` union (`StandardRecord | AliasRecord`).
|
|
104
12
|
*
|
|
105
13
|
* `AliasRecord` entries MUST carry a `target` produced by `fjallApp()`,
|
|
@@ -7,8 +7,9 @@ import type { DomainProps, ManualRecord } from "./interfaces/domain.js";
|
|
|
7
7
|
* User-facing Route53-oriented domain construct. Dispatches on
|
|
8
8
|
* `props.registrar` to one of three per-registrar patterns:
|
|
9
9
|
*
|
|
10
|
-
* - `route53` → `composeApexDomain` (apex HZ +
|
|
11
|
-
* - `external-delegated` → `composeDelegatedDomain` (sub-HZ
|
|
10
|
+
* - `route53` → `composeApexDomain` (apex HZ + org-gated DelegationRole)
|
|
11
|
+
* - `external-delegated` → `composeDelegatedDomain` (sub-HZ; D8
|
|
12
|
+
* child-writes NS delegation via `parentDelegationRoleArn`)
|
|
12
13
|
* - `external-records` → `composeExternalRecords` (no HZ, manual records)
|
|
13
14
|
*
|
|
14
15
|
* Applies gold-plating tags (`fjall:description`, three
|
|
@@ -11,8 +11,9 @@ import { applyCostAllocationTags } from "../../utils/costAllocationTags.js";
|
|
|
11
11
|
* User-facing Route53-oriented domain construct. Dispatches on
|
|
12
12
|
* `props.registrar` to one of three per-registrar patterns:
|
|
13
13
|
*
|
|
14
|
-
* - `route53` → `composeApexDomain` (apex HZ +
|
|
15
|
-
* - `external-delegated` → `composeDelegatedDomain` (sub-HZ
|
|
14
|
+
* - `route53` → `composeApexDomain` (apex HZ + org-gated DelegationRole)
|
|
15
|
+
* - `external-delegated` → `composeDelegatedDomain` (sub-HZ; D8
|
|
16
|
+
* child-writes NS delegation via `parentDelegationRoleArn`)
|
|
16
17
|
* - `external-records` → `composeExternalRecords` (no HZ, manual records)
|
|
17
18
|
*
|
|
18
19
|
* Applies gold-plating tags (`fjall:description`, three
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Annotations } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, Token } from "aws-cdk-lib";
|
|
2
2
|
import { DNS_APEX } from "@fjall/util";
|
|
3
|
+
import { isWithinZone, resolveRecordFqdn } from "../../utils/domainTypes.js";
|
|
3
4
|
const VALID_REGISTRARS = [
|
|
4
5
|
"route53",
|
|
5
6
|
"external-delegated",
|
|
6
7
|
"external-records"
|
|
7
8
|
];
|
|
9
|
+
const VALID_PHASES = ["zone", "full"];
|
|
8
10
|
/**
|
|
9
11
|
* Synchronous synth-time validator for `DomainProps`. Throws on hard errors
|
|
10
12
|
* (CDK catches and surfaces the stack trace pointing at the `new Domain(...)`
|
|
@@ -18,7 +20,7 @@ export function validateDomainProps(scope, props) {
|
|
|
18
20
|
const effectiveZone = resolveEffectiveZoneName(props);
|
|
19
21
|
validateRecords(scope, props, effectiveZone);
|
|
20
22
|
validateCertificates(props, effectiveZone);
|
|
21
|
-
|
|
23
|
+
validateDelegationConfig(props);
|
|
22
24
|
}
|
|
23
25
|
function validateRegistrar(props) {
|
|
24
26
|
const registrar = props.registrar;
|
|
@@ -26,11 +28,15 @@ function validateRegistrar(props) {
|
|
|
26
28
|
!VALID_REGISTRARS.includes(registrar)) {
|
|
27
29
|
throw new Error(`Domain registrar must be one of ${VALID_REGISTRARS.join(", ")}; received: ${String(registrar)}`);
|
|
28
30
|
}
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new Error("Domain: 'delegations'
|
|
31
|
+
// D8/D12 removed-surface defence — the parent-writes `delegations[]` prop
|
|
32
|
+
// no longer exists on any registrar. TS already rejects it; this catches JS
|
|
33
|
+
// callers and stale generated code, naming the replacement.
|
|
34
|
+
if (props.delegations !== undefined) {
|
|
35
|
+
throw new Error("Domain: 'delegations' was removed (D8 — one delegation direction, child " +
|
|
36
|
+
"writes NS). Cure: declare the delegated child as its own Domain with " +
|
|
37
|
+
"registrar: 'external-delegated' and set 'parentDelegationRoleArn' " +
|
|
38
|
+
"(the CLI injects it via ManagedDomainBinding.delegationRoleArn); the " +
|
|
39
|
+
"child then UPSERTs its NS records into this zone.");
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
function resolveEffectiveZoneName(props) {
|
|
@@ -47,10 +53,9 @@ function validateRecords(scope, props, effectiveZone) {
|
|
|
47
53
|
const records = props.records ?? [];
|
|
48
54
|
const seen = new Map();
|
|
49
55
|
for (const record of records) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
56
|
+
// Rejects wrong-zone absolute names and ambiguous bare-zone-suffixed
|
|
57
|
+
// names itself; a resolved name is therefore always within the zone.
|
|
58
|
+
const fqdn = resolveRecordFqdn(record.name, effectiveZone);
|
|
54
59
|
if ("target" in record) {
|
|
55
60
|
if (record.name === DNS_APEX && props.registrar === "external-records") {
|
|
56
61
|
throw new Error("Apex A records with Fjall targets require registrar: 'route53' or 'external-delegated' because RFC 1035 forbids CNAME at apex. Use a subdomain or change registrar.");
|
|
@@ -89,19 +94,61 @@ function validateCertificates(props, effectiveZone) {
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
/**
|
|
98
|
+
* D8 delegation-consistency checks for the child-writes shape.
|
|
99
|
+
*
|
|
100
|
+
* `parentDelegationRoleArn` and `phase` belong to the delegated topology
|
|
101
|
+
* (`external-delegated`) only; on any other registrar their presence is a
|
|
102
|
+
* hard error rather than a silent no-op. On the delegated topology the role
|
|
103
|
+
* ARN must be a LITERAL string (VD2-a): an unresolved token means the caller
|
|
104
|
+
* reached for `Fn.importValue`, which resolves same-account only and is
|
|
105
|
+
* exactly the trap that sank the legacy `DomainDelegation` pattern.
|
|
106
|
+
*/
|
|
107
|
+
function validateDelegationConfig(props) {
|
|
108
|
+
const arn = props
|
|
109
|
+
.parentDelegationRoleArn;
|
|
110
|
+
const phase = props.phase;
|
|
111
|
+
if (props.registrar !== "external-delegated") {
|
|
112
|
+
if (arn !== undefined) {
|
|
113
|
+
throw new Error(`Domain: 'parentDelegationRoleArn' is only supported with registrar: ` +
|
|
114
|
+
`'external-delegated' (received registrar: '${props.registrar}'). ` +
|
|
115
|
+
`Cure: the delegated child zone declares it — move the prop onto ` +
|
|
116
|
+
`the child Domain, or drop it.`);
|
|
117
|
+
}
|
|
118
|
+
if (phase !== undefined) {
|
|
119
|
+
throw new Error(`Domain: 'phase' is only supported with registrar: ` +
|
|
120
|
+
`'external-delegated' (received registrar: '${props.registrar}'). ` +
|
|
121
|
+
`Cure: drop the prop — only delegated child zones need the ` +
|
|
122
|
+
`two-step zone→full deploy gate.`);
|
|
123
|
+
}
|
|
94
124
|
return;
|
|
95
125
|
}
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
126
|
+
if (phase !== undefined &&
|
|
127
|
+
!VALID_PHASES.includes(phase)) {
|
|
128
|
+
throw new Error(`Domain: 'phase' must be one of ${VALID_PHASES.map((p) => `'${p}'`).join(", ")}; ` +
|
|
129
|
+
`received: '${String(phase)}'. Cure: use 'zone' for step 1 of the ` +
|
|
130
|
+
`two-step delegated deploy, 'full' (or omit) for the complete build.`);
|
|
131
|
+
}
|
|
132
|
+
if (arn === undefined) {
|
|
99
133
|
return;
|
|
100
134
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
if (typeof arn !== "string") {
|
|
136
|
+
throw new Error(`Domain: 'parentDelegationRoleArn' must be a string; received: ` +
|
|
137
|
+
`${typeof arn}. Cure: pass the parent DelegationRole ARN as a ` +
|
|
138
|
+
`literal string.`);
|
|
139
|
+
}
|
|
140
|
+
if (Token.isUnresolved(arn)) {
|
|
141
|
+
throw new Error("Domain: 'parentDelegationRoleArn' is an unresolved token (e.g. " +
|
|
142
|
+
"Fn.importValue) — cross-account delegation requires a literal role " +
|
|
143
|
+
"ARN because Fn::ImportValue resolves same-account only (VD2-a). " +
|
|
144
|
+
"Cure: deploy via the Fjall CLI, which injects the literal ARN from " +
|
|
145
|
+
"ManagedDomainBinding.delegationRoleArn, or paste the parent zone's " +
|
|
146
|
+
"DelegationRoleArn output value directly.");
|
|
147
|
+
}
|
|
148
|
+
if (!/^arn:[^:]+:iam::\d{12}:role\/.+$/.test(arn)) {
|
|
149
|
+
throw new Error(`Domain: 'parentDelegationRoleArn' does not look like an IAM role ARN; ` +
|
|
150
|
+
`received: '${arn}'. Cure: pass the parent zone's DelegationRoleArn ` +
|
|
151
|
+
`output (arn:<partition>:iam::<account>:role/<name>).`);
|
|
105
152
|
}
|
|
106
153
|
}
|
|
107
154
|
function normaliseCertificate(cert) {
|
|
@@ -113,33 +160,11 @@ function normaliseCertificate(cert) {
|
|
|
113
160
|
subjectAlternativeNames: cert.subjectAlternativeNames
|
|
114
161
|
};
|
|
115
162
|
}
|
|
116
|
-
function recordFqdn(recordName, zoneName) {
|
|
117
|
-
if (recordName === DNS_APEX) {
|
|
118
|
-
return zoneName;
|
|
119
|
-
}
|
|
120
|
-
// A user may already provide a FQDN (e.g. "api.example.com") or a single
|
|
121
|
-
// label ("api"). If the name contains a dot, treat it as an FQDN — the
|
|
122
|
-
// zone-boundary check then decides whether it is within the managed zone.
|
|
123
|
-
// A bare label is always appended to the zone.
|
|
124
|
-
if (recordName.includes(".")) {
|
|
125
|
-
return recordName;
|
|
126
|
-
}
|
|
127
|
-
return `${recordName}.${zoneName}`;
|
|
128
|
-
}
|
|
129
163
|
function assertWithinZone(candidate, zoneName) {
|
|
130
164
|
if (!isWithinZone(candidate, zoneName)) {
|
|
131
|
-
throw new Error(`'${candidate}' falls outside managed zone '${zoneName}'. Use a separate Domain construct or a
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Dot-boundary suffix matching. Rejects pathological cases where a user-supplied
|
|
136
|
-
* zone appears as a suffix of another zone (e.g. 'evilexample.com' against
|
|
137
|
-
* zone 'example.com' — `endsWith('example.com')` returns true without the
|
|
138
|
-
* leading dot).
|
|
139
|
-
*/
|
|
140
|
-
function isWithinZone(candidate, zoneName) {
|
|
141
|
-
if (candidate === zoneName) {
|
|
142
|
-
return true;
|
|
165
|
+
throw new Error(`'${candidate}' falls outside managed zone '${zoneName}'. Use a separate Domain construct or a delegated child Domain (registrar: 'external-delegated').`);
|
|
143
166
|
}
|
|
144
|
-
return candidate.endsWith(`.${zoneName}`);
|
|
145
167
|
}
|
|
168
|
+
// isWithinZone (dot-boundary suffix matching) and resolveRecordFqdn (the
|
|
169
|
+
// canonical BIND-semantics record-name resolver) live in
|
|
170
|
+
// lib/utils/domainTypes.ts — shared with the resources-layer dnsRecord family.
|
|
@@ -2,7 +2,7 @@ import { CfnOutput } from "aws-cdk-lib";
|
|
|
2
2
|
import { Certificate as AcmCertificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { resolveTargetToDnsName } from "./targets/targetResolution.js";
|
|
4
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveRecordFqdn } from "../../utils/domainTypes.js";
|
|
6
6
|
const MANUAL_RECORD_TTL_SECONDS = 300;
|
|
7
7
|
/**
|
|
8
8
|
* Composition for `registrar: "external-records"`. No Route53 hosted zone is
|
|
@@ -60,7 +60,7 @@ export function composeExternalRecords(scope, props) {
|
|
|
60
60
|
// entries.
|
|
61
61
|
const records = props.records ?? [];
|
|
62
62
|
records.forEach((record, index) => {
|
|
63
|
-
const fqdn =
|
|
63
|
+
const fqdn = resolveRecordFqdn(record.name, props.zoneName);
|
|
64
64
|
if ("target" in record) {
|
|
65
65
|
const value = resolveTargetToDnsName(record.target);
|
|
66
66
|
const manual = {
|
|
@@ -98,15 +98,6 @@ function normaliseCertificate(cert) {
|
|
|
98
98
|
transparencyLogging: cert.transparencyLogging
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
function toFqdn(recordName, zoneName) {
|
|
102
|
-
if (recordName === DNS_APEX) {
|
|
103
|
-
return zoneName;
|
|
104
|
-
}
|
|
105
|
-
if (recordName === zoneName || recordName.endsWith(`.${zoneName}`)) {
|
|
106
|
-
return recordName;
|
|
107
|
-
}
|
|
108
|
-
return `${recordName}.${zoneName}`;
|
|
109
|
-
}
|
|
110
101
|
function narrowManualType(type) {
|
|
111
102
|
switch (type) {
|
|
112
103
|
case "A":
|
|
@@ -7,11 +7,9 @@ export * from "./fivetranProxy.js";
|
|
|
7
7
|
export * from "./database.js";
|
|
8
8
|
export * from "../../resources/aws/compute/ec2.js";
|
|
9
9
|
export * from "../../resources/aws/networking/hostedZone.js";
|
|
10
|
-
export * from "./domainFactory.js";
|
|
11
|
-
export * from "./domainDelegation.js";
|
|
12
10
|
export { Domain } from "./domain.js";
|
|
13
11
|
export * from "./targets/index.js";
|
|
14
|
-
export { getDomainExportNames, type
|
|
12
|
+
export { getDomainExportNames, type ManagedDomainExports, type DomainDeployPhase, type FjallTarget, type StandardRecord, type DnsRecord, type Certificate, type DomainCommonProps, type Route53ApexProps, type ExternalDelegatedProps, type ExternalRecordsProps, type DomainProps, type ManualRecord } from "./interfaces/domain.js";
|
|
15
13
|
export { MIGRATION_SNAPSHOT_NAME_PREFIX, EXPECTED_SCHEMA_VERSION_ENV, EXPECTED_SCHEMA_VERSION_TOOL_ENV } from "./interfaces/database.js";
|
|
16
14
|
export * from "./compute.js";
|
|
17
15
|
export * from "./storage.js";
|
|
@@ -9,9 +9,8 @@ export * from "./database.js";
|
|
|
9
9
|
// Resource-layer re-exports — consumed by patterns that compose these primitives
|
|
10
10
|
export * from "../../resources/aws/compute/ec2.js";
|
|
11
11
|
export * from "../../resources/aws/networking/hostedZone.js";
|
|
12
|
-
// Domain patterns
|
|
13
|
-
|
|
14
|
-
export * from "./domainDelegation.js";
|
|
12
|
+
// Domain patterns (H13: the legacy DomainFactory/ApexDomain/DomainDelegation
|
|
13
|
+
// layer was deleted in Phase 2 — `Domain` is the only construct surface).
|
|
15
14
|
export { Domain } from "./domain.js";
|
|
16
15
|
export * from "./targets/index.js";
|
|
17
16
|
// Explicit exports from the interfaces barrel — avoids collision between the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { DomainDeployPhase } from "../../../utils/domainTypes.js";
|
|
2
|
+
export type { ManagedDomainExports, DomainDeployPhase } from "../../../utils/domainTypes.js";
|
|
2
3
|
export { getDomainExportNames } from "../../../utils/domainTypes.js";
|
|
3
4
|
/**
|
|
4
5
|
* Typed marker for the target of an `AliasRecord`. The helpers in
|
|
@@ -39,16 +40,10 @@ export type Certificate = string | {
|
|
|
39
40
|
readonly subjectAlternativeNames?: string[];
|
|
40
41
|
readonly transparencyLogging?: boolean;
|
|
41
42
|
};
|
|
42
|
-
export interface SubdomainDelegation {
|
|
43
|
-
readonly subdomain: string;
|
|
44
|
-
readonly toAccount: string;
|
|
45
|
-
readonly auto?: boolean;
|
|
46
|
-
}
|
|
47
43
|
export interface DomainCommonProps {
|
|
48
44
|
readonly zoneName: string;
|
|
49
45
|
readonly records?: DnsRecord[];
|
|
50
46
|
readonly certificates?: Certificate[];
|
|
51
|
-
readonly delegations?: SubdomainDelegation[];
|
|
52
47
|
readonly tags?: Record<string, string>;
|
|
53
48
|
readonly description?: string;
|
|
54
49
|
readonly costAllocationEnvironment?: string;
|
|
@@ -60,8 +55,27 @@ export interface Route53ApexProps extends DomainCommonProps {
|
|
|
60
55
|
export interface ExternalDelegatedProps extends DomainCommonProps {
|
|
61
56
|
readonly registrar: "external-delegated";
|
|
62
57
|
readonly delegatedSubdomain: string;
|
|
58
|
+
/**
|
|
59
|
+
* D8 — child-writes delegation. LITERAL ARN of the parent zone's
|
|
60
|
+
* DelegationRole (VD2-a: never `Fn.importValue`, which resolves
|
|
61
|
+
* same-account only). When present, the child zone UPSERTs its NS records
|
|
62
|
+
* into the parent zone via `CrossAccountZoneDelegationRecord`. The CLI
|
|
63
|
+
* injects this from `ManagedDomainBinding.delegationRoleArn` at deploy
|
|
64
|
+
* time; omit it for an externally-owned parent and add the NS records at
|
|
65
|
+
* the registrar instead (a synth warning reminds you the zone will not
|
|
66
|
+
* resolve until delegated).
|
|
67
|
+
*/
|
|
68
|
+
readonly parentDelegationRoleArn?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Two-step deploy gate (R2 cert-hang guard, generalised from
|
|
71
|
+
* `DevSubstrate.phase`). `"zone"` synthesises the child zone, delegation
|
|
72
|
+
* record and user records only; `"full"` (default) additionally issues the
|
|
73
|
+
* certificates — deploy that only after the delegated NS has propagated,
|
|
74
|
+
* else ACM DNS-validation hangs until CloudFormation rolls back.
|
|
75
|
+
*/
|
|
76
|
+
readonly phase?: DomainDeployPhase;
|
|
63
77
|
}
|
|
64
|
-
export interface ExternalRecordsProps extends
|
|
78
|
+
export interface ExternalRecordsProps extends DomainCommonProps {
|
|
65
79
|
readonly registrar: "external-records";
|
|
66
80
|
}
|
|
67
81
|
export type DomainProps = Route53ApexProps | ExternalDelegatedProps | ExternalRecordsProps;
|
|
@@ -14,4 +14,3 @@ export { type ConnectorType, type ConnectionAccess, type MessagingAccess, type C
|
|
|
14
14
|
export { type PatternType, type IPattern, type IPayload, type IPayloadProps, type IPatternProps, type AnyPattern, type PayloadDatabaseConfig, type PayloadComputeConfig, type PayloadCdnConfig, isPayloadPattern, isPattern } from "./pattern.js";
|
|
15
15
|
export { type OrganisationType, type IOrganisation, type IPlatform, type IAccount, type AnyOrganisation, isOrganisation, isPlatform, isAccount, isOrganisationResource } from "./organisation.js";
|
|
16
16
|
export { type IVpcPeer } from "./vpcPeer.js";
|
|
17
|
-
export { type DnsRecordInput, type DelegationInput, type CertificateInput, type DomainApexProps, type DomainDelegatedProps, type IDomainProps } from "./domain.js";
|
|
@@ -22,3 +22,6 @@ export { CONNECTION_TYPE, isConnector, isConnectable, isConnectionConfig, isConn
|
|
|
22
22
|
export { isPayloadPattern, isPattern } from "./pattern.js";
|
|
23
23
|
// Organisation interfaces
|
|
24
24
|
export { isOrganisation, isPlatform, isAccount, isOrganisationResource } from "./organisation.js";
|
|
25
|
+
// Domain interfaces — the legacy `IDomainProps` family was deleted with the
|
|
26
|
+
// H13 BIND-era layer; consumers use the `DomainProps` union exported from
|
|
27
|
+
// the patterns barrel.
|
|
@@ -23,7 +23,7 @@ import { type Storage } from "../storage.js";
|
|
|
23
23
|
import { type QueueMessaging } from "../messaging.js";
|
|
24
24
|
import { type Cdn, type SmartCdnBehaviour, type StaticSiteRouting } from "../cdn.js";
|
|
25
25
|
import { type LambdaFunction } from "../../../resources/aws/compute/index.js";
|
|
26
|
-
import type { ManagedDomainExports } from "../../../utils/domainTypes.js";
|
|
26
|
+
import type { ManagedDomainBinding, ManagedDomainExports } from "../../../utils/domainTypes.js";
|
|
27
27
|
import type { PatternType } from "@fjall/util";
|
|
28
28
|
export type { ProxyConfig, ReadReplicaConfig, CredentialsConfig, EncryptionConfig, AuroraEncryptionConfig, AuroraWriterConfig, AuroraReadersConfig, DatabaseInsightsConfig };
|
|
29
29
|
/**
|
|
@@ -287,11 +287,13 @@ export interface PayloadCdnConfig {
|
|
|
287
287
|
* }));
|
|
288
288
|
*
|
|
289
289
|
* @example
|
|
290
|
-
* // With custom domain (auto-creates certificate + DNS
|
|
290
|
+
* // With custom domain (auto-creates certificate + DNS; the zone identity
|
|
291
|
+
* // is explicit — a CLI-injected managedDomain binding, or zoneName)
|
|
291
292
|
* app.addPattern(PatternFactory.build("PayloadApp", {
|
|
292
293
|
* type: "payload",
|
|
293
294
|
* name: "my-cms",
|
|
294
|
-
* domain: "cms.example.com"
|
|
295
|
+
* domain: "cms.example.com",
|
|
296
|
+
* zoneName: "example.com"
|
|
295
297
|
* }));
|
|
296
298
|
*
|
|
297
299
|
* @example
|
|
@@ -316,18 +318,42 @@ export interface IPayloadProps {
|
|
|
316
318
|
/**
|
|
317
319
|
* Custom domain for the Payload application.
|
|
318
320
|
* When provided, automatically:
|
|
319
|
-
* -
|
|
320
|
-
*
|
|
321
|
+
* - Resolves the Route53 hosted zone from the explicit identity
|
|
322
|
+
* (`managedDomain` or `zoneName`/`hostedZoneId`) — never guessed from
|
|
323
|
+
* the domain's labels
|
|
324
|
+
* - Provides a us-east-1 ACM certificate for CloudFront: the managed
|
|
325
|
+
* binding's literal ARN, or an app-owned DNS-validated certificate
|
|
326
|
+
* (in-stack for us-east-1 apps, otherwise in the app's dedicated
|
|
327
|
+
* us-east-1 certificate stack consumed via cross-region references)
|
|
321
328
|
* - Configures CloudFront with the certificate
|
|
322
329
|
* - Creates an A record (alias) pointing to CloudFront
|
|
323
330
|
*
|
|
324
|
-
* Requires:
|
|
331
|
+
* Requires: `managedDomain` or `zoneName` naming the zone that contains
|
|
332
|
+
* the domain.
|
|
325
333
|
*
|
|
326
334
|
* @example "cms.example.com"
|
|
327
335
|
*/
|
|
328
336
|
domain?: string;
|
|
329
|
-
/**
|
|
330
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Zone and certificate identity from a managed domain stack. The
|
|
339
|
+
* CLI-injected `ManagedDomainBinding` carries literal values (hosted-zone
|
|
340
|
+
* ID + us-east-1 certificate ARN) and crosses accounts and regions; the
|
|
341
|
+
* legacy `ManagedDomainExports` form imports by export name (same account
|
|
342
|
+
* and region only). Takes priority over `zoneName`.
|
|
343
|
+
*/
|
|
344
|
+
managedDomain?: ManagedDomainExports | ManagedDomainBinding;
|
|
345
|
+
/**
|
|
346
|
+
* Explicit hosted-zone name containing `domain` (BYO zones). Fjall never
|
|
347
|
+
* derives the zone from the domain's labels — set this when not using a
|
|
348
|
+
* managed domain. `domain` must sit within this zone (validated at synth).
|
|
349
|
+
* @example "example.com.au"
|
|
350
|
+
*/
|
|
351
|
+
zoneName?: string;
|
|
352
|
+
/**
|
|
353
|
+
* Literal hosted-zone ID for `zoneName`. Optional — when set, the zone is
|
|
354
|
+
* referenced directly instead of via a runtime `HostedZone.fromLookup`.
|
|
355
|
+
*/
|
|
356
|
+
hostedZoneId?: string;
|
|
331
357
|
/** Database configuration */
|
|
332
358
|
database?: PayloadDatabaseConfig;
|
|
333
359
|
/** Compute (Lambda) configuration */
|
|
@@ -413,6 +439,7 @@ export interface StaticSiteCdnConfig {
|
|
|
413
439
|
* routing: "multipage",
|
|
414
440
|
* security: { headers: true },
|
|
415
441
|
* domain: "lbexc.com",
|
|
442
|
+
* zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
|
|
416
443
|
* forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
|
|
417
444
|
* }));
|
|
418
445
|
*/
|
|
@@ -431,8 +458,26 @@ export interface IStaticSiteProps {
|
|
|
431
458
|
security?: StaticSiteSecurityConfig;
|
|
432
459
|
/** Custom domain (same machinery as payload). Required when `forms` is set. */
|
|
433
460
|
domain?: string;
|
|
434
|
-
/**
|
|
435
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Zone and certificate identity from a managed domain stack. The
|
|
463
|
+
* CLI-injected `ManagedDomainBinding` carries literal values (hosted-zone
|
|
464
|
+
* ID + us-east-1 certificate ARN) and crosses accounts and regions; the
|
|
465
|
+
* legacy `ManagedDomainExports` form imports by export name (same account
|
|
466
|
+
* and region only). Takes priority over `zoneName`.
|
|
467
|
+
*/
|
|
468
|
+
managedDomain?: ManagedDomainExports | ManagedDomainBinding;
|
|
469
|
+
/**
|
|
470
|
+
* Explicit hosted-zone name containing `domain` (BYO zones). Fjall never
|
|
471
|
+
* derives the zone from the domain's labels — set this when not using a
|
|
472
|
+
* managed domain. `domain` must sit within this zone (validated at synth).
|
|
473
|
+
* @example "example.com.au"
|
|
474
|
+
*/
|
|
475
|
+
zoneName?: string;
|
|
476
|
+
/**
|
|
477
|
+
* Literal hosted-zone ID for `zoneName`. Optional — when set, the zone is
|
|
478
|
+
* referenced directly instead of via a runtime `HostedZone.fromLookup`.
|
|
479
|
+
*/
|
|
480
|
+
hostedZoneId?: string;
|
|
436
481
|
/** Optional contact-form endpoint (Lambda → SES). Requires `domain`. */
|
|
437
482
|
forms?: StaticSiteFormsConfig;
|
|
438
483
|
/** CDN configuration - for advanced per-path overrides. */
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { type Construct } from "constructs";
|
|
29
|
+
import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
30
|
+
import { type IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
31
|
+
import type App from "../../app.js";
|
|
32
|
+
import { type ManagedDomainBinding, type ManagedDomainExports } from "../../utils/domainTypes.js";
|
|
33
|
+
/**
|
|
34
|
+
* The explicit zone-identity surface shared by `IStaticSiteProps` and
|
|
35
|
+
* `IPayloadProps` — resolution priority is `managedDomain` (binding, then
|
|
36
|
+
* legacy exports) over `zoneName`/`hostedZoneId`.
|
|
37
|
+
*/
|
|
38
|
+
export interface PatternZoneIdentity {
|
|
39
|
+
readonly managedDomain?: ManagedDomainExports | ManagedDomainBinding;
|
|
40
|
+
readonly zoneName?: string;
|
|
41
|
+
readonly hostedZoneId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One pattern's domain-resolution request. `scope` is the pattern construct
|
|
45
|
+
* itself; `idPrefix` is its PascalCase name (construct-ID prefix, unique per
|
|
46
|
+
* scope); `context` prefixes every error, e.g. `Static site 'marketing'`.
|
|
47
|
+
*/
|
|
48
|
+
export interface PatternDomainRequest {
|
|
49
|
+
readonly scope: Construct;
|
|
50
|
+
readonly app: App;
|
|
51
|
+
readonly idPrefix: string;
|
|
52
|
+
readonly context: string;
|
|
53
|
+
readonly domain: string;
|
|
54
|
+
readonly identity: PatternZoneIdentity;
|
|
55
|
+
}
|
|
56
|
+
export interface ResolvedPatternZone {
|
|
57
|
+
readonly hostedZone: IHostedZone;
|
|
58
|
+
readonly zoneName: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function resolvePatternZone(request: PatternDomainRequest): ResolvedPatternZone;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the CloudFront viewer certificate for a pattern domain (H3/D3).
|
|
63
|
+
*
|
|
64
|
+
* - Managed binding: consume the injected us-east-1 ARN as a literal,
|
|
65
|
+
* asserting its region component at synth (literal ARNs are inspectable).
|
|
66
|
+
* - Legacy exports in a us-east-1 or region-unresolved app: keep the
|
|
67
|
+
* same-region Fn.importValue path unchanged (the D2 bare-CDK fallback).
|
|
68
|
+
* - Otherwise (BYO zone identity, or legacy exports in a non-us-east-1 app):
|
|
69
|
+
* provision an app-owned DNS-validated certificate — beside the consumer
|
|
70
|
+
* when the app already deploys to us-east-1, else in the dedicated
|
|
71
|
+
* `${app}UsEast1Certificates` stack, consumed through CDK cross-region
|
|
72
|
+
* references. In-app certificates are consumed in-app, so the
|
|
73
|
+
* `<domain>-certificate-arn` export is always suppressed
|
|
74
|
+
* (exportCertificateArn: false, §12-I) to avoid colliding with a
|
|
75
|
+
* managed-domain/apex stack that owns the domain's exports.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolvePatternCloudFrontCertificate(request: PatternDomainRequest, zone: ResolvedPatternZone): ICertificate;
|