@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.
Files changed (62) hide show
  1. package/dist/lib/app.d.ts +19 -2
  2. package/dist/lib/app.js +46 -3
  3. package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
  4. package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
  5. package/dist/lib/patterns/aws/cdn.js +25 -10
  6. package/dist/lib/patterns/aws/computeEcs.js +6 -1
  7. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -3
  8. package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
  9. package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
  10. package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
  11. package/dist/lib/patterns/aws/devSubstrate.js +18 -8
  12. package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
  13. package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
  14. package/dist/lib/patterns/aws/domain.d.ts +3 -2
  15. package/dist/lib/patterns/aws/domain.js +3 -2
  16. package/dist/lib/patterns/aws/domainValidation.js +71 -46
  17. package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
  18. package/dist/lib/patterns/aws/index.d.ts +1 -3
  19. package/dist/lib/patterns/aws/index.js +2 -3
  20. package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
  21. package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
  22. package/dist/lib/patterns/aws/interfaces/index.js +3 -0
  23. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
  24. package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
  25. package/dist/lib/patterns/aws/patternDomain.js +198 -0
  26. package/dist/lib/patterns/aws/payload.js +23 -30
  27. package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
  28. package/dist/lib/patterns/aws/staticSite.js +28 -33
  29. package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
  30. package/dist/lib/resources/aws/base/awsStack.js +20 -17
  31. package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
  32. package/dist/lib/resources/aws/compute/ecs.js +1 -1
  33. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
  34. package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
  35. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
  36. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
  37. package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
  38. package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
  39. package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
  40. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
  41. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
  42. package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
  43. package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
  44. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
  45. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
  46. package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
  47. package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
  48. package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
  49. package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
  50. package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
  51. package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
  52. package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
  53. package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
  54. package/dist/lib/utils/costAllocationTags.d.ts +9 -0
  55. package/dist/lib/utils/costAllocationTags.js +6 -1
  56. package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
  57. package/dist/lib/utils/dnsRecordRegistry.js +64 -0
  58. package/dist/lib/utils/domainTypes.d.ts +43 -36
  59. package/dist/lib/utils/domainTypes.js +65 -2
  60. package/dist/lib/utils/managedDomainContext.d.ts +37 -0
  61. package/dist/lib/utils/managedDomainContext.js +111 -0
  62. 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
- * Legacy composer consumed by `DomainFactory` (deleted in Phase 2). Accepts
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 + delegations)
11
- * - `external-delegated` → `composeDelegatedDomain` (sub-HZ only)
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 + delegations)
15
- * - `external-delegated` → `composeDelegatedDomain` (sub-HZ only)
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
- validateDelegations(props);
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
- // Runtime defence — TS forbids `delegations` on external-records, but a JS
30
- // caller could still pass it. Surface the mistake loudly.
31
- if (registrar === "external-records" &&
32
- props.delegations !== undefined) {
33
- throw new Error("Domain: 'delegations' is not supported with registrar: 'external-records' there is no Fjall-managed hosted zone to cascade from.");
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 (D8one 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
- const fqdn = recordFqdn(record.name, effectiveZone);
51
- if (!isWithinZone(fqdn, effectiveZone)) {
52
- throw new Error(`Record '${fqdn}' falls outside managed zone '${effectiveZone}'. Use a separate Domain construct or a SubdomainDelegation.`);
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
- function validateDelegations(props) {
93
- if (props.registrar === "external-records") {
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
- const delegations = props
97
- .delegations;
98
- if (!delegations) {
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
- for (const delegation of delegations) {
102
- if (delegation.auto === true && !delegation.toAccount) {
103
- throw new Error(`Delegation '${delegation.subdomain}' has auto: true but no 'toAccount' set. Provide the child Fjall account name.`);
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 SubdomainDelegation.`);
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 { DNS_APEX } from "@fjall/util";
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 = toFqdn(record.name, props.zoneName);
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 DnsRecordInput, type DelegationInput, type CertificateInput, type DomainApexProps, type DomainDelegatedProps, type IDomainProps, type ManagedDomainExports, type FjallTarget, type StandardRecord, type DnsRecord, type Certificate, type SubdomainDelegation, type DomainCommonProps, type Route53ApexProps, type ExternalDelegatedProps, type ExternalRecordsProps, type DomainProps, type ManualRecord } from "./interfaces/domain.js";
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
- export * from "./domainFactory.js";
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
- export type { DnsRecordInput, DelegationInput, CertificateInput, DomainApexProps, DomainDelegatedProps, IDomainProps, ManagedDomainExports } from "../../../utils/domainTypes.js";
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 Omit<DomainCommonProps, "delegations"> {
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
- * - Looks up the Route53 hosted zone
320
- * - Creates an ACM certificate with DNS validation (in us-east-1)
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: A Route53 hosted zone for the domain must exist.
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
- /** Import zone and cert from a managed domain stack instead of creating inline */
330
- managedDomain?: ManagedDomainExports;
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
- /** Import zone and cert from a managed domain stack instead of creating inline */
435
- managedDomain?: ManagedDomainExports;
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;