Skip to content

Commit 0a36ea6

Browse files
authored
Merge pull request #648 from StevenCAD/main
Add support for private custom domains
2 parents a8baa7d + 8c6ac60 commit 0a36ea6

File tree

44 files changed

+5894
-3633
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+5894
-3633
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [9.1.0] - 2026-02-10
8+
9+
### Changed
10+
- Add support for private custom domains. Thank you @StevenCAD ([648](https://github.com/amplify-education/serverless-domain-manager/pull/648))
11+
712
## [9.0.0] - 2026-02-04
813

914
### Changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ custom:
155155
| createRoute53IPv6Record | `true` | Toggles whether or not the plugin will create an AAAA Alias record in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. |
156156
| route53Profile | `(none)` | Profile to use for accessing Route53 resources when Route53 records are in a different account |
157157
| route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) |
158-
| endpointType | `EDGE` | Defines the endpoint type, accepts `REGIONAL` or `EDGE`. |
158+
| endpointType | `EDGE` | Defines the endpoint type, accepts `REGIONAL`, `EDGE`, or `PRIVATE`. Note: `PRIVATE` endpoints are only supported with REST APIs and can only be accessed from within a VPC. Private endpoints do not support HTTP APIs, WebSocket APIs, mutual TLS, or non-simple routing policies. |
159159
| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. |
160160
| tlsTruststoreUri | `undefined` | An Amazon S3 url that specifies the truststore for mutual TLS authentication, for example `s3://bucket-name/key-name`. The truststore can contain certificates from public or private certificate authorities. Be aware mutual TLS is only available for `regional` APIs. |
161161
| tlsTruststoreVersion | `undefined` | The version of the S3 object that contains your truststore. To specify a version, you must have versioning enabled for the S3 bucket. |
@@ -212,7 +212,9 @@ npm test
212212
To run integration tests, set an environment variable `TEST_DOMAIN` to the domain you will be testing for (i.e. `example.com` if creating a domain for `api.example.com`).
213213
And `ROUTE53_PROFILE` for creating route53 record in one AWS account and deploy in another. Then,
214214
```
215+
export SERVERLESS_LICENSE_KEY=<license_key>
215216
export TEST_DOMAIN=example.com
217+
export VPC_NAME=vpc_name
216218
export ROUTE53_PROFILE=default
217219
export TLS_TRUSTSTORE_URI=s3://bucket-name/key-name
218220
export TLS_TRUSTSTORE_VERSION=default

package-lock.json

Lines changed: 4994 additions & 3553 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "serverless-domain-manager",
3-
"version": "9.0.0",
3+
"version": "9.1.0",
44
"engines": {
55
"node": ">=18"
66
},
@@ -66,6 +66,7 @@
6666
"randomstring": "^1.3.0",
6767
"serverless": "4.2.5",
6868
"serverless-plugin-split-stacks": "^1.14.0",
69+
"serverless-vpc-discovery": "^6.0.0",
6970
"ts-node": "^10.9.2",
7071
"typescript": "^5.9.3"
7172
},

src/aws/acm-wrapper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class ACMWrapper {
6161
errorMessage += ` The endpoint type '${Globals.endpointTypes.edge}' is used. ` +
6262
`Make sure the needed ACM certificate exists in the '${Globals.defaultRegion}' region.`;
6363
}
64+
if (domain.endpointType === Globals.endpointTypes.private) {
65+
errorMessage += ` The endpoint type '${Globals.endpointTypes.private}' is used. ` +
66+
`Make sure the needed ACM certificate exists in the '${Globals.getRegion()}' region.`;
67+
}
6468
throw Error(errorMessage);
6569
}
6670
return certificateArn;

src/aws/api-gateway-v1-wrapper.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
GetBasePathMappingsCommandOutput,
1818
GetDomainNameCommand,
1919
GetDomainNameCommandOutput,
20+
GetDomainNamesCommand,
21+
GetDomainNamesCommandInput,
22+
GetDomainNamesCommandOutput,
2023
UpdateBasePathMappingCommand
2124
} from "@aws-sdk/client-api-gateway";
2225
import ApiGatewayMap = require("../models/api-gateway-map");
@@ -25,6 +28,7 @@ import Logging from "../logging";
2528
import { getAWSPagedResults } from "../utils";
2629

2730
class APIGatewayV1Wrapper extends APIGatewayBase {
31+
protected readonly versionPrefix = "V1";
2832
public readonly apiGateway: APIGatewayClient;
2933

3034
constructor (credentials?: any) {
@@ -54,7 +58,8 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
5458
};
5559

5660
const isEdgeType = domain.endpointType === Globals.endpointTypes.edge;
57-
if (isEdgeType) {
61+
const isPrivateType = domain.endpointType === Globals.endpointTypes.private;
62+
if (isEdgeType || isPrivateType) {
5863
params.certificateArn = domain.certificateArn;
5964
} else {
6065
params.regionalCertificateArn = domain.certificateArn;
@@ -88,11 +93,15 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
8893
* @param silent: To issue an error or not. Not by default.
8994
*/
9095
public async getCustomDomain (domain: DomainConfig, silent: boolean = true): Promise<DomainInfo> {
96+
const domainNameId = await this.resolvePrivateDomainNameId(domain, silent);
97+
if (domainNameId === null) return;
98+
9199
// Make API call
92100
try {
93101
const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send(
94102
new GetDomainNameCommand({
95-
domainName: domain.givenDomainName
103+
domainName: domain.givenDomainName,
104+
...(domainNameId && { domainNameId })
96105
})
97106
);
98107
return new DomainInfo(domainInfo);
@@ -106,11 +115,41 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
106115
}
107116
}
108117

118+
protected async fetchPrivateDomainNameId (domain: DomainConfig): Promise<string | undefined> {
119+
try {
120+
type DomainNameItem = {
121+
domainName: string;
122+
domainNameId?: string;
123+
endpointConfiguration?: { types?: string[] };
124+
};
125+
126+
const items = await getAWSPagedResults<DomainNameItem, GetDomainNamesCommandInput, GetDomainNamesCommandOutput>(
127+
this.apiGateway,
128+
"items",
129+
"position",
130+
"position",
131+
new GetDomainNamesCommand({})
132+
);
133+
134+
const matchingDomain = items.find(
135+
(item) => item.domainName === domain.givenDomainName &&
136+
item.endpointConfiguration?.types?.includes(Globals.endpointTypes.private)
137+
);
138+
139+
return matchingDomain?.domainNameId;
140+
} catch (err) {
141+
Logging.logWarning(`V1 - Unable to list domain names to find domainNameId: ${err.message}`);
142+
return undefined;
143+
}
144+
}
145+
109146
public async deleteCustomDomain (domain: DomainConfig): Promise<void> {
110147
// Make API call
111148
try {
149+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
112150
await this.apiGateway.send(new DeleteDomainNameCommand({
113-
domainName: domain.givenDomainName
151+
domainName: domain.givenDomainName,
152+
...(domainNameId && { domainNameId })
114153
}));
115154
} catch (err) {
116155
throw new Error(`V1 - Failed to delete custom domain '${domain.givenDomainName}':\n${err.message}`);
@@ -119,11 +158,13 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
119158

120159
public async createBasePathMapping (domain: DomainConfig): Promise<void> {
121160
try {
161+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
122162
await this.apiGateway.send(new CreateBasePathMappingCommand({
123163
basePath: domain.basePath,
124164
domainName: domain.givenDomainName,
125165
restApiId: domain.apiId,
126-
stage: domain.stage
166+
stage: domain.stage,
167+
...(domainNameId && { domainNameId })
127168
}));
128169
Logging.logInfo(`V1 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`);
129170
} catch (err) {
@@ -135,13 +176,15 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
135176

136177
public async getBasePathMappings (domain: DomainConfig): Promise<ApiGatewayMap[]> {
137178
try {
179+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
138180
const items = await getAWSPagedResults<BasePathMapping, GetBasePathMappingsCommandInput, GetBasePathMappingsCommandOutput>(
139181
this.apiGateway,
140182
"items",
141183
"position",
142184
"position",
143185
new GetBasePathMappingsCommand({
144-
domainName: domain.givenDomainName
186+
domainName: domain.givenDomainName,
187+
...(domainNameId && { domainNameId })
145188
})
146189
);
147190
return items.map((item) => {
@@ -159,14 +202,16 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
159202
Logging.logInfo(`V1 - Updating API mapping from '${domain.apiMapping.basePath}'
160203
to '${domain.basePath}' for '${domain.givenDomainName}'`);
161204
try {
205+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
162206
await this.apiGateway.send(new UpdateBasePathMappingCommand({
163207
basePath: domain.apiMapping.basePath,
164208
domainName: domain.givenDomainName,
165209
patchOperations: [{
166210
op: "replace",
167211
path: "/basePath",
168212
value: domain.basePath
169-
}]
213+
}],
214+
...(domainNameId && { domainNameId })
170215
}));
171216
} catch (err) {
172217
throw new Error(
@@ -177,10 +222,12 @@ class APIGatewayV1Wrapper extends APIGatewayBase {
177222

178223
public async deleteBasePathMapping (domain: DomainConfig): Promise<void> {
179224
try {
225+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
180226
await this.apiGateway.send(
181227
new DeleteBasePathMappingCommand({
182228
basePath: domain.apiMapping.basePath,
183-
domainName: domain.givenDomainName
229+
domainName: domain.givenDomainName,
230+
...(domainNameId && { domainNameId })
184231
})
185232
);
186233
Logging.logInfo(`V1 - Removed '${domain.apiMapping.basePath}' base path mapping`);

src/aws/api-gateway-v2-wrapper.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ import {
1919
GetApiMappingsCommandOutput,
2020
GetDomainNameCommand,
2121
GetDomainNameCommandOutput,
22+
GetDomainNamesCommand,
23+
GetDomainNamesCommandInput,
24+
GetDomainNamesCommandOutput,
2225
UpdateApiMappingCommand
2326
} from "@aws-sdk/client-apigatewayv2";
2427
import Logging from "../logging";
2528
import { getAWSPagedResults } from "../utils";
2629

2730
class APIGatewayV2Wrapper extends APIGatewayBase {
31+
protected readonly versionPrefix = "V2";
2832
public readonly apiGateway: ApiGatewayV2Client;
2933

3034
constructor (credentials?: any) {
@@ -59,7 +63,8 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
5963
};
6064

6165
const isEdgeType = domain.endpointType === Globals.endpointTypes.edge;
62-
if (!isEdgeType && domain.tlsTruststoreUri) {
66+
const isPrivateType = domain.endpointType === Globals.endpointTypes.private;
67+
if (!isEdgeType && !isPrivateType && domain.tlsTruststoreUri) {
6368
params.MutualTlsAuthentication = {
6469
TruststoreUri: domain.tlsTruststoreUri
6570
};
@@ -87,11 +92,15 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
8792
* @param silent: To issue an error or not. Not by default.
8893
*/
8994
public async getCustomDomain (domain: DomainConfig, silent: boolean = true): Promise<DomainInfo> {
95+
const domainNameId = await this.resolvePrivateDomainNameId(domain, silent);
96+
if (domainNameId === null) return;
97+
9098
// Make API call
9199
try {
92100
const domainInfo: GetDomainNameCommandOutput = await this.apiGateway.send(
93101
new GetDomainNameCommand({
94-
DomainName: domain.givenDomainName
102+
DomainName: domain.givenDomainName,
103+
...(domainNameId && { DomainNameId: domainNameId })
95104
})
96105
);
97106
return new DomainInfo(domainInfo);
@@ -105,16 +114,46 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
105114
}
106115
}
107116

117+
protected async fetchPrivateDomainNameId (domain: DomainConfig): Promise<string | undefined> {
118+
try {
119+
type DomainNameItem = {
120+
DomainName: string;
121+
DomainNameId?: string;
122+
DomainNameConfigurations?: Array<{ EndpointType?: string }>;
123+
};
124+
125+
const items = await getAWSPagedResults<DomainNameItem, GetDomainNamesCommandInput, GetDomainNamesCommandOutput>(
126+
this.apiGateway,
127+
"Items",
128+
"NextToken",
129+
"NextToken",
130+
new GetDomainNamesCommand({})
131+
);
132+
133+
const matchingDomain = items.find(
134+
(item) => item.DomainName === domain.givenDomainName &&
135+
item.DomainNameConfigurations?.some((config) => config.EndpointType === Globals.endpointTypes.private)
136+
);
137+
138+
return matchingDomain?.DomainNameId;
139+
} catch (err) {
140+
Logging.logWarning(`V2 - Unable to list domain names to find domainNameId: ${err.message}`);
141+
return undefined;
142+
}
143+
}
144+
108145
/**
109146
* Delete Custom Domain Name
110147
* @param domain: DomainConfig
111148
*/
112149
public async deleteCustomDomain (domain: DomainConfig): Promise<void> {
113150
// Make API call
114151
try {
152+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
115153
await this.apiGateway.send(
116154
new DeleteDomainNameCommand({
117-
DomainName: domain.givenDomainName
155+
DomainName: domain.givenDomainName,
156+
...(domainNameId && { DomainNameId: domainNameId })
118157
})
119158
);
120159
} catch (err) {
@@ -138,12 +177,14 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
138177
);
139178
}
140179
try {
180+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
141181
await this.apiGateway.send(
142182
new CreateApiMappingCommand({
143183
ApiId: domain.apiId,
144184
ApiMappingKey: domain.basePath,
145185
DomainName: domain.givenDomainName,
146-
Stage: domain.stage
186+
Stage: domain.stage,
187+
...(domainNameId && { DomainNameId: domainNameId })
147188
})
148189
);
149190
Logging.logInfo(`V2 - Created API mapping '${domain.basePath}' for '${domain.givenDomainName}'`);
@@ -160,13 +201,15 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
160201
*/
161202
public async getBasePathMappings (domain: DomainConfig): Promise<ApiGatewayMap[]> {
162203
try {
204+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
163205
const items = await getAWSPagedResults<ApiMapping, GetApiMappingsCommandInput, GetApiMappingsCommandOutput>(
164206
this.apiGateway,
165207
"Items",
166208
"NextToken",
167209
"NextToken",
168210
new GetApiMappingsCommand({
169-
DomainName: domain.givenDomainName
211+
DomainName: domain.givenDomainName,
212+
...(domainNameId && { DomainNameId: domainNameId })
170213
})
171214
);
172215
return items.map(
@@ -185,13 +228,15 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
185228
*/
186229
public async updateBasePathMapping (domain: DomainConfig): Promise<void> {
187230
try {
231+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
188232
await this.apiGateway.send(
189233
new UpdateApiMappingCommand({
190234
ApiId: domain.apiId,
191235
ApiMappingId: domain.apiMapping.apiMappingId,
192236
ApiMappingKey: domain.basePath,
193237
DomainName: domain.givenDomainName,
194-
Stage: domain.stage
238+
Stage: domain.stage,
239+
...(domainNameId && { DomainNameId: domainNameId })
195240
})
196241
);
197242
Logging.logInfo(`V2 - Updated API mapping to '${domain.basePath}' for '${domain.givenDomainName}'`);
@@ -207,9 +252,11 @@ class APIGatewayV2Wrapper extends APIGatewayBase {
207252
*/
208253
public async deleteBasePathMapping (domain: DomainConfig): Promise<void> {
209254
try {
255+
const domainNameId = await this.getDomainNameIdForPrivateDomain(domain);
210256
await this.apiGateway.send(new DeleteApiMappingCommand({
211257
ApiMappingId: domain.apiMapping.apiMappingId,
212-
DomainName: domain.givenDomainName
258+
DomainName: domain.givenDomainName,
259+
...(domainNameId && { DomainNameId: domainNameId })
213260
}));
214261
Logging.logInfo(`V2 - Removed API Mapping with id: '${domain.apiMapping.apiMappingId}'`);
215262
} catch (err) {

src/globals.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export default class Globals {
2323

2424
public static endpointTypes = {
2525
edge: "EDGE",
26-
regional: "REGIONAL"
26+
regional: "REGIONAL",
27+
private: "PRIVATE"
2728
};
2829

2930
public static apiTypes = {

0 commit comments

Comments
 (0)