@fjall/components-infrastructure 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/patterns/aws/apexDomainPattern.js +12 -24
- package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +6 -2
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +28 -27
- package/dist/lib/patterns/aws/domainCertificateComposer.d.ts +48 -0
- package/dist/lib/patterns/aws/domainCertificateComposer.js +179 -0
- package/dist/lib/patterns/aws/domainValidation.js +95 -7
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +33 -0
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.d.ts +10 -0
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +27 -1
- package/dist/lib/utils/domainTypes.d.ts +5 -11
- package/dist/lib/utils/domainTypes.js +7 -4
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
2
|
-
import {
|
|
2
|
+
import { composeDomainCertificates } from "./domainCertificateComposer.js";
|
|
3
3
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
4
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
5
5
|
/**
|
|
@@ -27,19 +27,17 @@ export function composeApexDomain(scope, props) {
|
|
|
27
27
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
28
28
|
costAllocationDomain: props.zoneName
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
certificates.set(normalised.domainName, dc.certificate);
|
|
30
|
+
// Regional entries mint beside the zone as before; cloudFront entries
|
|
31
|
+
// mint in us-east-1 (in-stack here, or in the domain-paired
|
|
32
|
+
// UsEast1Certificates stack) and publish the zone-level export (D3).
|
|
33
|
+
const certificates = composeDomainCertificates(scope, {
|
|
34
|
+
certificates: props.certificates,
|
|
35
|
+
effectiveZoneName: props.zoneName,
|
|
36
|
+
safeZone,
|
|
37
|
+
hostedZone: hostedZoneConstruct.hostedZone,
|
|
38
|
+
hostedZoneId: props.hostedZoneId,
|
|
39
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
40
|
+
costAllocationDomain: props.zoneName
|
|
43
41
|
});
|
|
44
42
|
if (props.records && props.records.length > 0) {
|
|
45
43
|
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
|
|
@@ -55,13 +53,3 @@ export function composeApexDomain(scope, props) {
|
|
|
55
53
|
manualRecords: []
|
|
56
54
|
};
|
|
57
55
|
}
|
|
58
|
-
function normaliseCertificate(cert) {
|
|
59
|
-
if (typeof cert === "string") {
|
|
60
|
-
return { domainName: cert };
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
domainName: cert.domainName,
|
|
64
|
-
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
65
|
-
transparencyLogging: cert.transparencyLogging
|
|
66
|
-
};
|
|
67
|
-
}
|
|
@@ -10,8 +10,12 @@ export interface DelegatedDomainPatternResult {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
13
|
-
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}`
|
|
14
|
-
*
|
|
13
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` —
|
|
14
|
+
* or ADOPTS an existing one when `hostedZoneId` + `adoptedNameServers` are
|
|
15
|
+
* set (both-or-neither, validated): the zone is imported instead of created
|
|
16
|
+
* and the delegation UPSERT carries the adopted NS literals, since an
|
|
17
|
+
* imported zone exposes no `hostedZoneNameServers` attribute. When
|
|
18
|
+
* `parentDelegationRoleArn` is present, the child NS are UPSERTed into the
|
|
15
19
|
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
16
20
|
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
17
21
|
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
@@ -2,14 +2,18 @@ import { Annotations } from "aws-cdk-lib";
|
|
|
2
2
|
import { Role } from "../../resources/aws/iam/index.js";
|
|
3
3
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
4
4
|
import { CrossAccountDelegationRecord } from "../../resources/aws/networking/crossAccountDelegationRecord.js";
|
|
5
|
-
import {
|
|
5
|
+
import { composeDomainCertificates } from "./domainCertificateComposer.js";
|
|
6
6
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
7
7
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
8
8
|
import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
|
|
9
9
|
/**
|
|
10
10
|
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
11
|
-
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}`
|
|
12
|
-
*
|
|
11
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` —
|
|
12
|
+
* or ADOPTS an existing one when `hostedZoneId` + `adoptedNameServers` are
|
|
13
|
+
* set (both-or-neither, validated): the zone is imported instead of created
|
|
14
|
+
* and the delegation UPSERT carries the adopted NS literals, since an
|
|
15
|
+
* imported zone exposes no `hostedZoneNameServers` attribute. When
|
|
16
|
+
* `parentDelegationRoleArn` is present, the child NS are UPSERTed into the
|
|
13
17
|
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
14
18
|
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
15
19
|
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
@@ -26,6 +30,9 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
26
30
|
const safeZone = toPascalCase(getSafeZoneName(effectiveZone));
|
|
27
31
|
const hostedZoneConstruct = new HostedZone(scope, `${safeZone}HostedZone`, {
|
|
28
32
|
zoneName: effectiveZone,
|
|
33
|
+
// Adoption path: an existing child zone is imported by id rather than
|
|
34
|
+
// created (both-or-neither with adoptedNameServers, validated upstream).
|
|
35
|
+
hostedZoneId: props.hostedZoneId,
|
|
29
36
|
// The PARENT account owns the delegation role — this is the child.
|
|
30
37
|
createDelegationRole: false,
|
|
31
38
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
@@ -44,6 +51,9 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
44
51
|
delegatedZone: hostedZoneConstruct.hostedZone,
|
|
45
52
|
delegatedZoneName: effectiveZone,
|
|
46
53
|
parentHostedZoneName: props.zoneName,
|
|
54
|
+
// Adoption: the imported zone has no NS attribute — the UPSERT uses
|
|
55
|
+
// the user-declared literals (undefined on the create path).
|
|
56
|
+
adoptedNameServers: props.adoptedNameServers,
|
|
47
57
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
48
58
|
costAllocationDomain: props.zoneName
|
|
49
59
|
});
|
|
@@ -55,7 +65,10 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
55
65
|
`the parent DelegationRole ARN from the managed-domain binding) or ` +
|
|
56
66
|
`add the zone's NS records at the parent registrar manually.`);
|
|
57
67
|
}
|
|
58
|
-
|
|
68
|
+
// Created zones surface their synth-time NS tokens; adopted zones surface
|
|
69
|
+
// the declared literals (authoritative — the same values the delegation
|
|
70
|
+
// record UPSERTs into the parent).
|
|
71
|
+
const nameServers = hostedZoneConstruct.nameServers ?? props.adoptedNameServers ?? [];
|
|
59
72
|
const records = props.records ?? [];
|
|
60
73
|
if (records.length > 0) {
|
|
61
74
|
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, records);
|
|
@@ -70,19 +83,17 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
70
83
|
manualRecords: []
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
certificates.set(normalised.domainName, dc.certificate);
|
|
86
|
+
// Regional entries mint beside the zone as before; cloudFront entries
|
|
87
|
+
// mint in us-east-1 (in-stack here, or in the domain-paired
|
|
88
|
+
// UsEast1Certificates stack) and publish the zone-level export (D3).
|
|
89
|
+
const certificates = composeDomainCertificates(scope, {
|
|
90
|
+
certificates: props.certificates,
|
|
91
|
+
effectiveZoneName: effectiveZone,
|
|
92
|
+
safeZone,
|
|
93
|
+
hostedZone: hostedZoneConstruct.hostedZone,
|
|
94
|
+
hostedZoneId: props.hostedZoneId,
|
|
95
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
96
|
+
costAllocationDomain: props.zoneName
|
|
86
97
|
});
|
|
87
98
|
return {
|
|
88
99
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
@@ -91,13 +102,3 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
91
102
|
manualRecords: []
|
|
92
103
|
};
|
|
93
104
|
}
|
|
94
|
-
function normaliseCertificate(cert) {
|
|
95
|
-
if (typeof cert === "string") {
|
|
96
|
-
return { domainName: cert };
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
domainName: cert.domainName,
|
|
100
|
-
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
101
|
-
transparencyLogging: cert.transparencyLogging
|
|
102
|
-
};
|
|
103
|
-
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Construct } from "constructs";
|
|
2
|
+
import { type IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
|
+
import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
+
import type { Certificate } from "./interfaces/domain.js";
|
|
5
|
+
export interface DomainCertificateComposition {
|
|
6
|
+
readonly certificates: readonly Certificate[] | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Zone the export names derive from — the apex `zoneName`, or the
|
|
9
|
+
* delegated child's `${delegatedSubdomain}.${zoneName}` (mirrors the
|
|
10
|
+
* Domain construct's `resolveEffectiveZoneName`).
|
|
11
|
+
*/
|
|
12
|
+
readonly effectiveZoneName: string;
|
|
13
|
+
/** PascalCase construct-ID prefix shared with the calling composer. */
|
|
14
|
+
readonly safeZone: string;
|
|
15
|
+
readonly hostedZone: IHostedZone;
|
|
16
|
+
/**
|
|
17
|
+
* Literal zone id when the zone is ADOPTED — the paired us-east-1 stack
|
|
18
|
+
* re-imports the zone by attributes instead of a runtime lookup (a created
|
|
19
|
+
* zone's id is a cross-stack token the paired stack must not consume).
|
|
20
|
+
*/
|
|
21
|
+
readonly hostedZoneId: string | undefined;
|
|
22
|
+
readonly costAllocationEnvironment: string | undefined;
|
|
23
|
+
readonly costAllocationDomain: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Certificate composition shared by `composeApexDomain` and
|
|
27
|
+
* `composeDelegatedDomain`.
|
|
28
|
+
*
|
|
29
|
+
* Regional entries mint a `DomainCertificate` beside the zone exactly as
|
|
30
|
+
* before (byte-stable construct IDs `${safeZone}${safeCertName}Cert${index}`,
|
|
31
|
+
* per-domain `<domain>-certificate-arn` export). A `cloudFront: true` entry
|
|
32
|
+
* (D3) instead mints in us-east-1 — the only region CloudFront accepts
|
|
33
|
+
* viewer certificates from — and publishes the zone-level
|
|
34
|
+
* `<zone>-us-east-1-certificate-arn` export in place of the per-domain one:
|
|
35
|
+
*
|
|
36
|
+
* - domain stack already in us-east-1 (the fjall.io case): certificate and
|
|
37
|
+
* export stay IN-STACK, no extra stack;
|
|
38
|
+
* - any other resolved region: certificate and export live in a
|
|
39
|
+
* domain-paired `<StackName>UsEast1Certificates` stack. NO CDK
|
|
40
|
+
* cross-region references — the ARN is consumed as a LITERAL via D2
|
|
41
|
+
* DescribeStacks (`ManagedDomainBinding.usEast1CertificateArn`), so
|
|
42
|
+
* neither stack synthesises the export writer/reader machinery.
|
|
43
|
+
*
|
|
44
|
+
* An unresolved region or account is a synth-time error naming the cures:
|
|
45
|
+
* where the certificate must live is unknowable, and guessing would strand
|
|
46
|
+
* the export CloudFront consumers (patternDomain) fail without.
|
|
47
|
+
*/
|
|
48
|
+
export declare function composeDomainCertificates(scope: Construct, composition: DomainCertificateComposition): Map<string, ICertificate>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { CfnOutput, Stack, Stage, Token } from "aws-cdk-lib";
|
|
2
|
+
import { HostedZone as AWSHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
|
+
import { getDomainExportNames, getDomainUsEast1CertificatesStackName } from "@fjall/util";
|
|
4
|
+
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
5
|
+
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
6
|
+
const US_EAST_1 = "us-east-1";
|
|
7
|
+
/**
|
|
8
|
+
* Certificate composition shared by `composeApexDomain` and
|
|
9
|
+
* `composeDelegatedDomain`.
|
|
10
|
+
*
|
|
11
|
+
* Regional entries mint a `DomainCertificate` beside the zone exactly as
|
|
12
|
+
* before (byte-stable construct IDs `${safeZone}${safeCertName}Cert${index}`,
|
|
13
|
+
* per-domain `<domain>-certificate-arn` export). A `cloudFront: true` entry
|
|
14
|
+
* (D3) instead mints in us-east-1 — the only region CloudFront accepts
|
|
15
|
+
* viewer certificates from — and publishes the zone-level
|
|
16
|
+
* `<zone>-us-east-1-certificate-arn` export in place of the per-domain one:
|
|
17
|
+
*
|
|
18
|
+
* - domain stack already in us-east-1 (the fjall.io case): certificate and
|
|
19
|
+
* export stay IN-STACK, no extra stack;
|
|
20
|
+
* - any other resolved region: certificate and export live in a
|
|
21
|
+
* domain-paired `<StackName>UsEast1Certificates` stack. NO CDK
|
|
22
|
+
* cross-region references — the ARN is consumed as a LITERAL via D2
|
|
23
|
+
* DescribeStacks (`ManagedDomainBinding.usEast1CertificateArn`), so
|
|
24
|
+
* neither stack synthesises the export writer/reader machinery.
|
|
25
|
+
*
|
|
26
|
+
* An unresolved region or account is a synth-time error naming the cures:
|
|
27
|
+
* where the certificate must live is unknowable, and guessing would strand
|
|
28
|
+
* the export CloudFront consumers (patternDomain) fail without.
|
|
29
|
+
*/
|
|
30
|
+
export function composeDomainCertificates(scope, composition) {
|
|
31
|
+
const certificates = new Map();
|
|
32
|
+
(composition.certificates ?? []).forEach((cert, index) => {
|
|
33
|
+
const normalised = normaliseCertificate(cert);
|
|
34
|
+
const safeCertName = toPascalCase(normalised.domainName.split(".").join(""));
|
|
35
|
+
const certId = `${composition.safeZone}${safeCertName}Cert${index}`;
|
|
36
|
+
if (normalised.cloudFront !== true) {
|
|
37
|
+
const dc = new DomainCertificate(scope, certId, {
|
|
38
|
+
domainName: normalised.domainName,
|
|
39
|
+
subjectAlternativeNames: normalised.subjectAlternativeNames,
|
|
40
|
+
transparencyLogging: normalised.transparencyLogging,
|
|
41
|
+
hostedZone: composition.hostedZone,
|
|
42
|
+
costAllocationEnvironment: composition.costAllocationEnvironment,
|
|
43
|
+
costAllocationDomain: composition.costAllocationDomain
|
|
44
|
+
});
|
|
45
|
+
certificates.set(normalised.domainName, dc.certificate);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
certificates.set(normalised.domainName, mintCloudFrontCertificate(scope, composition, normalised, certId));
|
|
49
|
+
});
|
|
50
|
+
return certificates;
|
|
51
|
+
}
|
|
52
|
+
function mintCloudFrontCertificate(scope, composition, cert, certId) {
|
|
53
|
+
const domainStack = Stack.of(scope);
|
|
54
|
+
const region = Token.isUnresolved(domainStack.region)
|
|
55
|
+
? undefined
|
|
56
|
+
: domainStack.region;
|
|
57
|
+
if (region === undefined) {
|
|
58
|
+
throw new Error(`Domain '${composition.effectiveZoneName}': cannot place the ` +
|
|
59
|
+
`CloudFront certificate for '${cert.domainName}' — the domain ` +
|
|
60
|
+
"stack's region is unresolved at synth (got a region token), so " +
|
|
61
|
+
"whether it already deploys to us-east-1 is unknowable. Deploy " +
|
|
62
|
+
"through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT and " +
|
|
63
|
+
"CDK_DEFAULT_REGION before synthesising.");
|
|
64
|
+
}
|
|
65
|
+
if (region === US_EAST_1) {
|
|
66
|
+
// In-stack placement (the fjall.io case): the domain stack's home region
|
|
67
|
+
// already satisfies CloudFront, so no paired stack is minted and the
|
|
68
|
+
// zone-level export publishes beside the zone.
|
|
69
|
+
const dc = new DomainCertificate(scope, certId, {
|
|
70
|
+
domainName: cert.domainName,
|
|
71
|
+
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
72
|
+
transparencyLogging: cert.transparencyLogging,
|
|
73
|
+
hostedZone: composition.hostedZone,
|
|
74
|
+
costAllocationEnvironment: composition.costAllocationEnvironment,
|
|
75
|
+
costAllocationDomain: composition.costAllocationDomain,
|
|
76
|
+
// The zone-level us-east-1 export below replaces the per-domain
|
|
77
|
+
// regional export: CloudFront consumers resolve the ARN through
|
|
78
|
+
// ManagedDomainBinding.usEast1CertificateArn, and a viewer
|
|
79
|
+
// certificate has no regional (ALB/ECS) consumers to export for.
|
|
80
|
+
exportCertificateArn: false
|
|
81
|
+
});
|
|
82
|
+
emitUsEast1Export(scope, composition, dc);
|
|
83
|
+
return dc.certificate;
|
|
84
|
+
}
|
|
85
|
+
const pairedStack = resolvePairedUsEast1Stack(domainStack, composition, cert);
|
|
86
|
+
// Re-create the zone reference INSIDE the paired stack: an adopted zone's
|
|
87
|
+
// literal id travels as-is, while a created zone's id is a cross-stack
|
|
88
|
+
// token the paired stack must not consume (that would demand
|
|
89
|
+
// crossRegionReferences) — it re-resolves by lookup on the declared zone
|
|
90
|
+
// name instead (same split as patternDomain's BYO path).
|
|
91
|
+
const certZone = composition.hostedZoneId !== undefined
|
|
92
|
+
? AWSHostedZone.fromHostedZoneAttributes(pairedStack, `${composition.safeZone}UsEast1CertificateZone`, {
|
|
93
|
+
hostedZoneId: composition.hostedZoneId,
|
|
94
|
+
zoneName: composition.effectiveZoneName
|
|
95
|
+
})
|
|
96
|
+
: AWSHostedZone.fromLookup(pairedStack, `${composition.safeZone}UsEast1CertificateZone`, { domainName: composition.effectiveZoneName });
|
|
97
|
+
const dc = new DomainCertificate(pairedStack, certId, {
|
|
98
|
+
domainName: cert.domainName,
|
|
99
|
+
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
100
|
+
transparencyLogging: cert.transparencyLogging,
|
|
101
|
+
hostedZone: certZone,
|
|
102
|
+
costAllocationEnvironment: composition.costAllocationEnvironment,
|
|
103
|
+
costAllocationDomain: composition.costAllocationDomain,
|
|
104
|
+
exportCertificateArn: false
|
|
105
|
+
});
|
|
106
|
+
emitUsEast1Export(pairedStack, composition, dc);
|
|
107
|
+
return dc.certificate;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The zone-level `<zone>-us-east-1-certificate-arn` export
|
|
111
|
+
* (`getDomainExportNames(...).usEast1CertificateArn` — the SSOT
|
|
112
|
+
* `DomainService.resolveDomainForApp` reads into
|
|
113
|
+
* `ManagedDomainBinding.usEast1CertificateArn` for CloudFront consumers).
|
|
114
|
+
* Scope is the stack that owns the certificate: the domain stack in-region,
|
|
115
|
+
* the paired stack otherwise.
|
|
116
|
+
*/
|
|
117
|
+
function emitUsEast1Export(scope, composition, dc) {
|
|
118
|
+
const exports = getDomainExportNames(composition.effectiveZoneName);
|
|
119
|
+
new CfnOutput(scope, `${composition.safeZone}UsEast1CertificateArn`, {
|
|
120
|
+
key: `${composition.safeZone}UsEast1CertificateArn`,
|
|
121
|
+
value: dc.certificateArn,
|
|
122
|
+
exportName: exports.usEast1CertificateArn
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The domain-paired us-east-1 certificate stack,
|
|
127
|
+
* `<StackName>UsEast1Certificates` — the name contract
|
|
128
|
+
* `DomainService.resolveDomainForApp` derives when the domain stack itself
|
|
129
|
+
* is not us-east-1. Env-pinned to the domain stack's concrete account:
|
|
130
|
+
* without one the paired stack cannot be addressed, so an unresolved
|
|
131
|
+
* account fails synth with the cures.
|
|
132
|
+
*/
|
|
133
|
+
function resolvePairedUsEast1Stack(domainStack, composition, cert) {
|
|
134
|
+
if (Token.isUnresolved(domainStack.account)) {
|
|
135
|
+
throw new Error(`Domain '${composition.effectiveZoneName}': the CloudFront ` +
|
|
136
|
+
`certificate for '${cert.domainName}' needs the paired us-east-1 ` +
|
|
137
|
+
"certificate stack, which needs a concrete account (got an " +
|
|
138
|
+
"unresolved account token). Deploy through the Fjall CLI, or " +
|
|
139
|
+
"export CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION before " +
|
|
140
|
+
"synthesising.");
|
|
141
|
+
}
|
|
142
|
+
const container = Stage.of(domainStack);
|
|
143
|
+
if (container === undefined) {
|
|
144
|
+
throw new Error(`Domain '${composition.effectiveZoneName}': the paired us-east-1 ` +
|
|
145
|
+
"certificate stack must be created beside the domain stack, but " +
|
|
146
|
+
"the domain stack has no enclosing App/Stage. Synthesise the " +
|
|
147
|
+
"domain under a CDK App (the Fjall CLI always does).");
|
|
148
|
+
}
|
|
149
|
+
// SSOT name contract with the read side (DomainService.resolveDomainForApp
|
|
150
|
+
// describes this stack with a us-east-1 CloudFormation client).
|
|
151
|
+
const pairedId = getDomainUsEast1CertificatesStackName(domainStack.stackName);
|
|
152
|
+
const existing = container.node.tryFindChild(pairedId);
|
|
153
|
+
if (existing !== undefined) {
|
|
154
|
+
if (!Stack.isStack(existing)) {
|
|
155
|
+
throw new Error(`Domain '${composition.effectiveZoneName}': construct '${pairedId}' ` +
|
|
156
|
+
"already exists beside the domain stack but is not a Stack — the " +
|
|
157
|
+
"paired us-east-1 certificate stack name is reserved by the D3 " +
|
|
158
|
+
"contract. Rename the colliding construct.");
|
|
159
|
+
}
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
// Plain env-pinned stack, deliberately WITHOUT crossRegionReferences: the
|
|
163
|
+
// ARN leaves this stack as a literal through DescribeStacks, never as a
|
|
164
|
+
// CDK token, so no writer/reader machinery may synthesise.
|
|
165
|
+
return new Stack(container, pairedId, {
|
|
166
|
+
env: { account: domainStack.account, region: US_EAST_1 }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function normaliseCertificate(cert) {
|
|
170
|
+
if (typeof cert === "string") {
|
|
171
|
+
return { domainName: cert };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
domainName: cert.domainName,
|
|
175
|
+
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
176
|
+
transparencyLogging: cert.transparencyLogging,
|
|
177
|
+
cloudFront: cert.cloudFront
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -86,29 +86,69 @@ function validateRecords(scope, props, effectiveZone) {
|
|
|
86
86
|
}
|
|
87
87
|
function validateCertificates(props, effectiveZone) {
|
|
88
88
|
const certificates = props.certificates ?? [];
|
|
89
|
+
let cloudFrontCount = 0;
|
|
89
90
|
for (const cert of certificates) {
|
|
90
91
|
const resolved = normaliseCertificate(cert);
|
|
91
92
|
assertWithinZone(resolved.domainName, effectiveZone);
|
|
92
93
|
for (const san of resolved.subjectAlternativeNames ?? []) {
|
|
93
94
|
assertWithinZone(san, effectiveZone);
|
|
94
95
|
}
|
|
96
|
+
// D3 CloudFront designation consistency. TS already narrows the prop to
|
|
97
|
+
// boolean; the runtime check catches JS callers and stale generated code.
|
|
98
|
+
if (resolved.cloudFront !== undefined &&
|
|
99
|
+
typeof resolved.cloudFront !== "boolean") {
|
|
100
|
+
throw new Error(`Domain: certificate '${resolved.domainName}' has a non-boolean ` +
|
|
101
|
+
`'cloudFront' (received: ${JSON.stringify(resolved.cloudFront)}). ` +
|
|
102
|
+
`Cure: set cloudFront: true for a CloudFront viewer certificate, ` +
|
|
103
|
+
`or drop the prop.`);
|
|
104
|
+
}
|
|
105
|
+
if (resolved.cloudFront === true) {
|
|
106
|
+
cloudFrontCount += 1;
|
|
107
|
+
if (props.registrar === "external-records") {
|
|
108
|
+
throw new Error(`Domain: certificate '${resolved.domainName}' sets cloudFront: ` +
|
|
109
|
+
`true, which registrar 'external-records' does not support — ` +
|
|
110
|
+
`there is no Fjall-managed hosted zone to DNS-validate the ` +
|
|
111
|
+
`us-east-1 certificate against. Cure: manage the zone with ` +
|
|
112
|
+
`registrar 'route53' or 'external-delegated', or drop ` +
|
|
113
|
+
`cloudFront and provision an app-owned us-east-1 certificate.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (cloudFrontCount > 1) {
|
|
118
|
+
throw new Error(`Domain: at most one certificate may set cloudFront: true (received ` +
|
|
119
|
+
`${cloudFrontCount}) — the zone-level ` +
|
|
120
|
+
`'<zone>-us-east-1-certificate-arn' export is singular. Cure: keep ` +
|
|
121
|
+
`one CloudFront certificate and move the other names into its ` +
|
|
122
|
+
`subjectAlternativeNames.`);
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
125
|
/**
|
|
98
126
|
* D8 delegation-consistency checks for the child-writes shape.
|
|
99
127
|
*
|
|
100
|
-
* `parentDelegationRoleArn` and `
|
|
101
|
-
* (`external-delegated`) only; on any other registrar
|
|
102
|
-
* hard error rather than a silent no-op. On the
|
|
103
|
-
* ARN must be a LITERAL string (VD2-a): an
|
|
104
|
-
* reached for `Fn.importValue`, which
|
|
105
|
-
* exactly the trap that sank the legacy
|
|
128
|
+
* `parentDelegationRoleArn`, `phase` and `adoptedNameServers` belong to the
|
|
129
|
+
* delegated topology (`external-delegated`) only; on any other registrar
|
|
130
|
+
* their presence is a hard error rather than a silent no-op. On the
|
|
131
|
+
* delegated topology the role ARN must be a LITERAL string (VD2-a): an
|
|
132
|
+
* unresolved token means the caller reached for `Fn.importValue`, which
|
|
133
|
+
* resolves same-account only and is exactly the trap that sank the legacy
|
|
134
|
+
* `DomainDelegation` pattern. Adoption (`hostedZoneId` +
|
|
135
|
+
* `adoptedNameServers`) is both-or-neither: an imported zone exposes no
|
|
136
|
+
* NS attribute, so a one-sided declaration either cannot delegate or
|
|
137
|
+
* cannot name the zone it delegates to.
|
|
106
138
|
*/
|
|
107
139
|
function validateDelegationConfig(props) {
|
|
108
140
|
const arn = props
|
|
109
141
|
.parentDelegationRoleArn;
|
|
110
142
|
const phase = props.phase;
|
|
111
143
|
if (props.registrar !== "external-delegated") {
|
|
144
|
+
const adoptedNameServers = props
|
|
145
|
+
.adoptedNameServers;
|
|
146
|
+
if (adoptedNameServers !== undefined) {
|
|
147
|
+
throw new Error(`Domain: 'adoptedNameServers' is only supported with registrar: ` +
|
|
148
|
+
`'external-delegated' (received registrar: '${props.registrar}'). ` +
|
|
149
|
+
`Cure: adoption of an existing child zone is declared on the ` +
|
|
150
|
+
`delegated child Domain — move the prop there, or drop it.`);
|
|
151
|
+
}
|
|
112
152
|
if (arn !== undefined) {
|
|
113
153
|
throw new Error(`Domain: 'parentDelegationRoleArn' is only supported with registrar: ` +
|
|
114
154
|
`'external-delegated' (received registrar: '${props.registrar}'). ` +
|
|
@@ -129,6 +169,7 @@ function validateDelegationConfig(props) {
|
|
|
129
169
|
`received: '${String(phase)}'. Cure: use 'zone' for step 1 of the ` +
|
|
130
170
|
`two-step delegated deploy, 'full' (or omit) for the complete build.`);
|
|
131
171
|
}
|
|
172
|
+
validateAdoptionConfig(props);
|
|
132
173
|
if (arn === undefined) {
|
|
133
174
|
return;
|
|
134
175
|
}
|
|
@@ -151,13 +192,60 @@ function validateDelegationConfig(props) {
|
|
|
151
192
|
`output (arn:<partition>:iam::<account>:role/<name>).`);
|
|
152
193
|
}
|
|
153
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Child-zone ADOPTION consistency (external-delegated only). `hostedZoneId`
|
|
197
|
+
* and `adoptedNameServers` are both-or-neither, and the name servers must be
|
|
198
|
+
* a non-empty array of literal hostnames: an imported zone exposes no
|
|
199
|
+
* `hostedZoneNameServers` attribute, so these literals are the ONLY source
|
|
200
|
+
* the `Custom::CrossAccountZoneDelegation` UPSERT has for the parent NS row
|
|
201
|
+
* — a token or empty set would delegate the zone to nowhere.
|
|
202
|
+
*/
|
|
203
|
+
function validateAdoptionConfig(props) {
|
|
204
|
+
const hostedZoneId = props.hostedZoneId;
|
|
205
|
+
const adoptedNameServers = props
|
|
206
|
+
.adoptedNameServers;
|
|
207
|
+
if (hostedZoneId === undefined && adoptedNameServers === undefined) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (adoptedNameServers === undefined) {
|
|
211
|
+
throw new Error(`Domain: adopting an existing child zone requires 'adoptedNameServers' ` +
|
|
212
|
+
`alongside 'hostedZoneId' (both-or-neither). An imported zone ` +
|
|
213
|
+
`exposes no name-server attribute at synth, so the parent NS UPSERT ` +
|
|
214
|
+
`needs the live values. Cure: read them once ` +
|
|
215
|
+
`(aws route53 get-hosted-zone --id ${String(hostedZoneId)}) and ` +
|
|
216
|
+
`paste the DelegationSet.NameServers literals.`);
|
|
217
|
+
}
|
|
218
|
+
if (hostedZoneId === undefined) {
|
|
219
|
+
throw new Error(`Domain: 'adoptedNameServers' requires 'hostedZoneId' (both-or-neither) ` +
|
|
220
|
+
`— the literals describe an EXISTING zone to adopt. Cure: set the ` +
|
|
221
|
+
`adopted zone's hostedZoneId, or drop 'adoptedNameServers' to create ` +
|
|
222
|
+
`a fresh child zone.`);
|
|
223
|
+
}
|
|
224
|
+
if (typeof hostedZoneId !== "string" || hostedZoneId.length === 0) {
|
|
225
|
+
throw new Error(`Domain: 'hostedZoneId' must be a non-empty string; received: ` +
|
|
226
|
+
`${JSON.stringify(hostedZoneId)}.`);
|
|
227
|
+
}
|
|
228
|
+
if (!Array.isArray(adoptedNameServers) || adoptedNameServers.length === 0) {
|
|
229
|
+
throw new Error(`Domain: 'adoptedNameServers' must be a non-empty array of NS ` +
|
|
230
|
+
`hostnames; received: ${JSON.stringify(adoptedNameServers)}. Cure: ` +
|
|
231
|
+
`paste the adopted zone's DelegationSet.NameServers.`);
|
|
232
|
+
}
|
|
233
|
+
for (const ns of adoptedNameServers) {
|
|
234
|
+
if (typeof ns !== "string" || ns.length === 0 || Token.isUnresolved(ns)) {
|
|
235
|
+
throw new Error(`Domain: every 'adoptedNameServers' entry must be a literal NS ` +
|
|
236
|
+
`hostname (no CDK tokens) — the delegation UPSERT writes them ` +
|
|
237
|
+
`verbatim into the parent zone. Received: ${JSON.stringify(ns)}.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
154
241
|
function normaliseCertificate(cert) {
|
|
155
242
|
if (typeof cert === "string") {
|
|
156
243
|
return { domainName: cert };
|
|
157
244
|
}
|
|
158
245
|
return {
|
|
159
246
|
domainName: cert.domainName,
|
|
160
|
-
subjectAlternativeNames: cert.subjectAlternativeNames
|
|
247
|
+
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
248
|
+
cloudFront: cert.cloudFront
|
|
161
249
|
};
|
|
162
250
|
}
|
|
163
251
|
function assertWithinZone(candidate, zoneName) {
|
|
@@ -39,6 +39,22 @@ export type Certificate = string | {
|
|
|
39
39
|
readonly domainName: string;
|
|
40
40
|
readonly subjectAlternativeNames?: string[];
|
|
41
41
|
readonly transparencyLogging?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* D3 — CloudFront viewer certificate. CloudFront accepts certificates
|
|
44
|
+
* from us-east-1 only, so a `cloudFront: true` entry mints its
|
|
45
|
+
* `DomainCertificate` there — in-stack when the domain stack itself
|
|
46
|
+
* resolves to us-east-1, otherwise in a domain-paired
|
|
47
|
+
* `<StackName>UsEast1Certificates` stack — and publishes the
|
|
48
|
+
* zone-level `<zone>-us-east-1-certificate-arn` export in place of
|
|
49
|
+
* the per-domain regional export. The ARN reaches CloudFront
|
|
50
|
+
* consumers as a LITERAL through
|
|
51
|
+
* `ManagedDomainBinding.usEast1CertificateArn` (D2 DescribeStacks),
|
|
52
|
+
* never via CDK cross-region references. At most one certificate per
|
|
53
|
+
* Domain may set it (the zone-level export is singular — put extra
|
|
54
|
+
* names in `subjectAlternativeNames`); unsupported on registrar
|
|
55
|
+
* `"external-records"`, which has no hosted zone to validate against.
|
|
56
|
+
*/
|
|
57
|
+
readonly cloudFront?: boolean;
|
|
42
58
|
};
|
|
43
59
|
export interface DomainCommonProps {
|
|
44
60
|
readonly zoneName: string;
|
|
@@ -55,6 +71,23 @@ export interface Route53ApexProps extends DomainCommonProps {
|
|
|
55
71
|
export interface ExternalDelegatedProps extends DomainCommonProps {
|
|
56
72
|
readonly registrar: "external-delegated";
|
|
57
73
|
readonly delegatedSubdomain: string;
|
|
74
|
+
/**
|
|
75
|
+
* Child-zone ADOPTION: hosted zone id of an EXISTING zone for
|
|
76
|
+
* `{delegatedSubdomain}.{zoneName}`. When present the pattern IMPORTS the
|
|
77
|
+
* zone instead of creating one. Requires `adoptedNameServers`
|
|
78
|
+
* (both-or-neither — validated): an imported zone exposes no
|
|
79
|
+
* `hostedZoneNameServers` attribute at synth, so the parent NS UPSERT
|
|
80
|
+
* needs the live values as literals.
|
|
81
|
+
*/
|
|
82
|
+
readonly hostedZoneId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Literal NS hostnames of the adopted zone (from a live
|
|
85
|
+
* `get-hosted-zone` read). Carried verbatim into the
|
|
86
|
+
* `Custom::CrossAccountZoneDelegation` UPSERT — the delegation record in
|
|
87
|
+
* the parent zone must keep pointing at the adopted zone's real name
|
|
88
|
+
* servers. Only valid together with `hostedZoneId`.
|
|
89
|
+
*/
|
|
90
|
+
readonly adoptedNameServers?: string[];
|
|
58
91
|
/**
|
|
59
92
|
* D8 — child-writes delegation. LITERAL ARN of the parent zone's
|
|
60
93
|
* DelegationRole (VD2-a: never `Fn.importValue`, which resolves
|
|
@@ -6,6 +6,16 @@ export interface CrossAccountDelegationRecordProps {
|
|
|
6
6
|
readonly delegatedZone: IHostedZone;
|
|
7
7
|
readonly delegatedZoneName: string;
|
|
8
8
|
readonly parentHostedZoneName: string;
|
|
9
|
+
/**
|
|
10
|
+
* Literal NS hostnames for an ADOPTED (imported) delegated zone. An
|
|
11
|
+
* imported `IHostedZone` exposes no `hostedZoneNameServers` attribute —
|
|
12
|
+
* CDK cannot know a live zone's NS at synth — so the custom resource
|
|
13
|
+
* would UPSERT an empty parent NS row. When set, a facade over the
|
|
14
|
+
* imported zone supplies these literals instead (see
|
|
15
|
+
* `withLiteralNameServers`). Omit for created zones, whose NS arrive as
|
|
16
|
+
* a CloudFormation attribute token.
|
|
17
|
+
*/
|
|
18
|
+
readonly adoptedNameServers?: string[];
|
|
9
19
|
readonly description?: string;
|
|
10
20
|
readonly costAllocationEnvironment?: string;
|
|
11
21
|
readonly costAllocationDomain?: string;
|
|
@@ -2,6 +2,29 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { Tags } from "aws-cdk-lib";
|
|
3
3
|
import { CrossAccountZoneDelegationRecord as CdkCrossAccountZoneDelegationRecord } from "aws-cdk-lib/aws-route53";
|
|
4
4
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
5
|
+
/**
|
|
6
|
+
* CDK facade seam for zone adoption. `CrossAccountZoneDelegationRecord`
|
|
7
|
+
* reads exactly `zoneName` and `hostedZoneNameServers` off its
|
|
8
|
+
* `delegatedZone`; the facade delegates every access to the imported zone
|
|
9
|
+
* and overrides only `hostedZoneNameServers` with the user-declared
|
|
10
|
+
* literals. Methods are bound to the underlying zone so `this`-dependent
|
|
11
|
+
* internals behave identically. Pinned by the adopted-case synth snapshot
|
|
12
|
+
* in crossAccountDelegationRecord.test.ts — a CDK upgrade that starts
|
|
13
|
+
* reading more through this seam trips the pin.
|
|
14
|
+
*/
|
|
15
|
+
function withLiteralNameServers(zone, nameServers) {
|
|
16
|
+
return new Proxy(zone, {
|
|
17
|
+
get(target, property) {
|
|
18
|
+
if (property === "hostedZoneNameServers") {
|
|
19
|
+
return [...nameServers];
|
|
20
|
+
}
|
|
21
|
+
const value = Reflect.get(target, property);
|
|
22
|
+
return typeof value === "function"
|
|
23
|
+
? value.bind(target)
|
|
24
|
+
: value;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
5
28
|
// CDK's CrossAccountZoneDelegationRecord is a custom-resource Lambda: user-level tags
|
|
6
29
|
// on the record itself may not reach AWS. We tag the wrapping Fjall Construct for
|
|
7
30
|
// assertion purposes; create-path tags still propagate to child resources (Lambda,
|
|
@@ -14,9 +37,12 @@ export class CrossAccountDelegationRecord extends Construct {
|
|
|
14
37
|
this.description =
|
|
15
38
|
props.description ??
|
|
16
39
|
`Fjall-managed cross-account delegation for ${props.delegatedZoneName} in parent ${props.parentHostedZoneName}`;
|
|
40
|
+
const delegatedZone = props.adoptedNameServers !== undefined
|
|
41
|
+
? withLiteralNameServers(props.delegatedZone, props.adoptedNameServers)
|
|
42
|
+
: props.delegatedZone;
|
|
17
43
|
this.record = new CdkCrossAccountZoneDelegationRecord(this, "Record", {
|
|
18
44
|
delegationRole: props.delegationRole,
|
|
19
|
-
delegatedZone
|
|
45
|
+
delegatedZone,
|
|
20
46
|
parentHostedZoneName: props.parentHostedZoneName
|
|
21
47
|
});
|
|
22
48
|
Tags.of(this).add("fjall:description", this.description);
|
|
@@ -29,15 +29,9 @@ export declare function resolveRecordFqdn(recordName: string, zoneName: string):
|
|
|
29
29
|
/**
|
|
30
30
|
* Two-step deploy phase for constructs whose certificates must not be issued
|
|
31
31
|
* before their zone's NS delegation has propagated (design R2 cert-hang
|
|
32
|
-
* guard)
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* guard) plus its omitted-phase default — homed in `@fjall/util`
|
|
33
|
+
* (`infra/domainExports.ts`, the lowest common layer) and re-exported here
|
|
34
|
+
* so construct-side consumers keep their import path. deploy-core's
|
|
35
|
+
* `orchestration/domain/types.ts` re-exports the same declaration.
|
|
36
36
|
*/
|
|
37
|
-
export type DomainDeployPhase
|
|
38
|
-
/**
|
|
39
|
-
* Default deploy phase when `phase` is omitted — the complete build. The
|
|
40
|
-
* two-step guard (R2) sets `"zone"` explicitly for step 1, so the safe
|
|
41
|
-
* default for a single-shot deploy is everything.
|
|
42
|
-
*/
|
|
43
|
-
export declare const DOMAIN_DEPLOY_DEFAULT_PHASE: DomainDeployPhase;
|
|
37
|
+
export { DOMAIN_DEPLOY_DEFAULT_PHASE, type DomainDeployPhase } from "@fjall/util";
|
|
@@ -58,8 +58,11 @@ export function resolveRecordFqdn(recordName, zoneName) {
|
|
|
58
58
|
return `${recordName}.${zoneName}`;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Two-step deploy phase for constructs whose certificates must not be issued
|
|
62
|
+
* before their zone's NS delegation has propagated (design R2 cert-hang
|
|
63
|
+
* guard) plus its omitted-phase default — homed in `@fjall/util`
|
|
64
|
+
* (`infra/domainExports.ts`, the lowest common layer) and re-exported here
|
|
65
|
+
* so construct-side consumers keep their import path. deploy-core's
|
|
66
|
+
* `orchestration/domain/types.ts` re-exports the same declaration.
|
|
64
67
|
*/
|
|
65
|
-
export
|
|
68
|
+
export { DOMAIN_DEPLOY_DEFAULT_PHASE } from "@fjall/util";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
70
|
-
"@fjall/generator": "^3.
|
|
71
|
-
"@fjall/util": "^3.
|
|
70
|
+
"@fjall/generator": "^3.6.0",
|
|
71
|
+
"@fjall/util": "^3.6.0",
|
|
72
72
|
"constructs": "^10.6.0"
|
|
73
73
|
},
|
|
74
74
|
"overrides": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=18.0.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "10366db602d7e80669e22825a52071a47e05c3b7"
|
|
86
86
|
}
|