@stacksjs/ts-cloud 0.7.4 → 0.7.6
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/aws/index.js +259 -0
- package/dist/bin/cli.js +316 -316
- package/dist/chunk-0wxyppza.js +146 -0
- package/dist/chunk-3knnr7wh.js +601 -0
- package/dist/chunk-3rsfns7x.js +10427 -0
- package/dist/chunk-93hjhs78.js +1752 -0
- package/dist/chunk-arsh1g5h.js +1749 -0
- package/dist/chunk-b82pbxyp.js +1572 -0
- package/dist/chunk-c6rgvg1j.js +46124 -0
- package/dist/chunk-d2p5n2aq.js +23 -0
- package/dist/chunk-d7p84vz5.js +1699 -0
- package/dist/chunk-dpkk640m.js +4392 -0
- package/dist/chunk-eq08r166.js +8 -0
- package/dist/chunk-hsk6fe6x.js +371 -0
- package/dist/chunk-mj1tmhte.js +13 -0
- package/dist/chunk-p6309384.js +12828 -0
- package/dist/chunk-pegd7rmf.js +10 -0
- package/dist/chunk-pqfzdg68.js +12 -0
- package/dist/chunk-qpj3edwz.js +601 -0
- package/dist/chunk-tjjgajbh.js +1032 -0
- package/dist/chunk-tnztxpcb.js +630 -0
- package/dist/chunk-tt4kxske.js +1788 -0
- package/dist/chunk-v0bahtg2.js +4 -0
- package/dist/chunk-vd87cpvn.js +1754 -0
- package/dist/chunk-zn0nxxa8.js +1779 -0
- package/dist/deploy/index.js +87 -0
- package/dist/dns/index.js +31 -0
- package/dist/drivers/hetzner/provision.d.ts +53 -0
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +98 -0
- package/dist/drivers/shared/remote-exec.d.ts +47 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4383 -91358
- package/dist/push/index.js +509 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/terminal.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/alarms.html +3 -3
- package/dist/ui/serverless/assets.html +3 -3
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/deployments.html +3 -3
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/dist/ui/serverless/queues.html +3 -3
- package/dist/ui/serverless/scheduler.html +3 -3
- package/dist/ui/serverless/secrets.html +3 -3
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +3 -3
- package/package.json +3 -3
|
@@ -0,0 +1,1788 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Route53Client
|
|
3
|
+
} from "./chunk-tnztxpcb.js";
|
|
4
|
+
import {
|
|
5
|
+
AWSClient
|
|
6
|
+
} from "./chunk-arsh1g5h.js";
|
|
7
|
+
// src/dns/porkbun.ts
|
|
8
|
+
var PORKBUN_API_URL = "https://api.porkbun.com/api/json/v3";
|
|
9
|
+
|
|
10
|
+
class PorkbunProvider {
|
|
11
|
+
name = "porkbun";
|
|
12
|
+
apiKey;
|
|
13
|
+
secretKey;
|
|
14
|
+
constructor(apiKey, secretKey) {
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
this.secretKey = secretKey;
|
|
17
|
+
}
|
|
18
|
+
async request(endpoint, additionalBody = {}) {
|
|
19
|
+
const response = await fetch(`${PORKBUN_API_URL}${endpoint}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json"
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
apikey: this.apiKey,
|
|
26
|
+
secretapikey: this.secretKey,
|
|
27
|
+
...additionalBody
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Porkbun API error: ${response.status} ${response.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
if (data.status === "ERROR") {
|
|
35
|
+
throw new Error(`Porkbun API error: ${data.message || "Unknown error"}`);
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
getSubdomain(recordName, domain) {
|
|
40
|
+
const cleanName = recordName.replace(/\.$/, "");
|
|
41
|
+
const cleanDomain = domain.replace(/\.$/, "");
|
|
42
|
+
if (cleanName === cleanDomain) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
if (cleanName.endsWith(`.${cleanDomain}`)) {
|
|
46
|
+
return cleanName.slice(0, -(cleanDomain.length + 1));
|
|
47
|
+
}
|
|
48
|
+
return cleanName;
|
|
49
|
+
}
|
|
50
|
+
getRootDomain(domain) {
|
|
51
|
+
const parts = domain.replace(/\.$/, "").split(".");
|
|
52
|
+
if (parts.length >= 2) {
|
|
53
|
+
return parts.slice(-2).join(".");
|
|
54
|
+
}
|
|
55
|
+
return domain;
|
|
56
|
+
}
|
|
57
|
+
async createRecord(domain, record) {
|
|
58
|
+
try {
|
|
59
|
+
const rootDomain = this.getRootDomain(domain);
|
|
60
|
+
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
61
|
+
const body = {
|
|
62
|
+
type: record.type,
|
|
63
|
+
content: record.content,
|
|
64
|
+
ttl: String(record.ttl || 600)
|
|
65
|
+
};
|
|
66
|
+
if (subdomain) {
|
|
67
|
+
body.name = subdomain;
|
|
68
|
+
}
|
|
69
|
+
if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
|
|
70
|
+
body.prio = String(record.priority);
|
|
71
|
+
}
|
|
72
|
+
if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
|
|
73
|
+
body.content = `${record.weight} ${record.port} ${record.content}`;
|
|
74
|
+
}
|
|
75
|
+
const response = await this.request(`/dns/create/${rootDomain}`, body);
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
id: response.id?.toString(),
|
|
79
|
+
message: "Record created successfully"
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async upsertRecord(domain, record) {
|
|
89
|
+
try {
|
|
90
|
+
const rootDomain = this.getRootDomain(domain);
|
|
91
|
+
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
92
|
+
const existing = await this.listRecords(domain, record.type);
|
|
93
|
+
if (existing.success) {
|
|
94
|
+
const matchingRecord = existing.records.find((r) => {
|
|
95
|
+
const existingSubdomain = this.getSubdomain(r.name, rootDomain);
|
|
96
|
+
return existingSubdomain === subdomain && r.type === record.type;
|
|
97
|
+
});
|
|
98
|
+
if (matchingRecord?.id) {
|
|
99
|
+
const body = {
|
|
100
|
+
type: record.type,
|
|
101
|
+
content: record.content,
|
|
102
|
+
ttl: String(record.ttl || 600)
|
|
103
|
+
};
|
|
104
|
+
if (subdomain) {
|
|
105
|
+
body.name = subdomain;
|
|
106
|
+
}
|
|
107
|
+
if ((record.type === "MX" || record.type === "SRV") && record.priority !== undefined) {
|
|
108
|
+
body.prio = String(record.priority);
|
|
109
|
+
}
|
|
110
|
+
if (record.type === "SRV" && record.weight !== undefined && record.port !== undefined) {
|
|
111
|
+
body.content = `${record.weight} ${record.port} ${record.content}`;
|
|
112
|
+
}
|
|
113
|
+
await this.request(`/dns/edit/${rootDomain}/${matchingRecord.id}`, body);
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
id: matchingRecord.id,
|
|
117
|
+
message: "Record updated successfully"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return this.createRecord(domain, record);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return this.createRecord(domain, record);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async deleteRecord(domain, record) {
|
|
127
|
+
try {
|
|
128
|
+
const rootDomain = this.getRootDomain(domain);
|
|
129
|
+
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
130
|
+
const existing = await this.listRecords(domain, record.type);
|
|
131
|
+
if (!existing.success) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
message: "Failed to list records"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const matchingRecord = existing.records.find((r) => {
|
|
138
|
+
const existingSubdomain = this.getSubdomain(r.name, rootDomain);
|
|
139
|
+
return existingSubdomain === subdomain && r.type === record.type && r.content === record.content;
|
|
140
|
+
});
|
|
141
|
+
if (!matchingRecord?.id) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
message: "Record not found"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
await this.request(`/dns/delete/${rootDomain}/${matchingRecord.id}`);
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
message: "Record deleted successfully"
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async listRecords(domain, type) {
|
|
160
|
+
try {
|
|
161
|
+
const rootDomain = this.getRootDomain(domain);
|
|
162
|
+
let endpoint = `/dns/retrieve/${rootDomain}`;
|
|
163
|
+
if (type) {
|
|
164
|
+
endpoint = `/dns/retrieveByNameType/${rootDomain}/${type}`;
|
|
165
|
+
}
|
|
166
|
+
const response = await this.request(endpoint);
|
|
167
|
+
const records = (response.records || []).map((r) => ({
|
|
168
|
+
id: r.id,
|
|
169
|
+
name: r.name || rootDomain,
|
|
170
|
+
type: r.type,
|
|
171
|
+
content: r.content,
|
|
172
|
+
ttl: Number.parseInt(r.ttl, 10),
|
|
173
|
+
priority: r.prio ? Number.parseInt(r.prio, 10) : undefined
|
|
174
|
+
}));
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
records
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
records: [],
|
|
183
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async canManageDomain(domain) {
|
|
188
|
+
try {
|
|
189
|
+
const rootDomain = this.getRootDomain(domain);
|
|
190
|
+
await this.request(`/dns/retrieve/${rootDomain}`);
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async listDomains() {
|
|
197
|
+
try {
|
|
198
|
+
const response = await this.request("/domain/listAll");
|
|
199
|
+
return (response.domains || []).map((d) => d.domain);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async getNameServers(domain) {
|
|
205
|
+
try {
|
|
206
|
+
const rootDomain = this.getRootDomain(domain);
|
|
207
|
+
const response = await this.request(`/dns/getNS/${rootDomain}`);
|
|
208
|
+
return response.ns || [];
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async updateNameServers(domain, nameservers) {
|
|
214
|
+
try {
|
|
215
|
+
const rootDomain = this.getRootDomain(domain);
|
|
216
|
+
await this.request(`/dns/updateNS/${rootDomain}`, {
|
|
217
|
+
ns: nameservers
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// src/dns/godaddy.ts
|
|
226
|
+
var GODADDY_API_URL = "https://api.godaddy.com";
|
|
227
|
+
var GODADDY_OTE_API_URL = "https://api.ote-godaddy.com";
|
|
228
|
+
|
|
229
|
+
class GoDaddyProvider {
|
|
230
|
+
name = "godaddy";
|
|
231
|
+
apiKey;
|
|
232
|
+
apiSecret;
|
|
233
|
+
baseUrl;
|
|
234
|
+
constructor(apiKey, apiSecret, environment = "production") {
|
|
235
|
+
this.apiKey = apiKey;
|
|
236
|
+
this.apiSecret = apiSecret;
|
|
237
|
+
this.baseUrl = environment === "ote" ? GODADDY_OTE_API_URL : GODADDY_API_URL;
|
|
238
|
+
}
|
|
239
|
+
async request(method, endpoint, body) {
|
|
240
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
241
|
+
const headers = {
|
|
242
|
+
Authorization: `sso-key ${this.apiKey}:${this.apiSecret}`,
|
|
243
|
+
"Content-Type": "application/json",
|
|
244
|
+
Accept: "application/json"
|
|
245
|
+
};
|
|
246
|
+
const options = {
|
|
247
|
+
method,
|
|
248
|
+
headers
|
|
249
|
+
};
|
|
250
|
+
if (body) {
|
|
251
|
+
options.body = JSON.stringify(body);
|
|
252
|
+
}
|
|
253
|
+
const response = await fetch(url, options);
|
|
254
|
+
if (response.status === 204) {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
if (response.ok) {
|
|
258
|
+
const text = await response.text();
|
|
259
|
+
if (text) {
|
|
260
|
+
return JSON.parse(text);
|
|
261
|
+
}
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
let errorMessage = `GoDaddy API error: ${response.status} ${response.statusText}`;
|
|
265
|
+
try {
|
|
266
|
+
const errorData = await response.json();
|
|
267
|
+
if (errorData.message) {
|
|
268
|
+
errorMessage = `GoDaddy API error: ${errorData.message}`;
|
|
269
|
+
}
|
|
270
|
+
if (errorData.fields) {
|
|
271
|
+
errorMessage += ` - Fields: ${JSON.stringify(errorData.fields)}`;
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
274
|
+
throw new Error(errorMessage);
|
|
275
|
+
}
|
|
276
|
+
getSubdomain(recordName, domain) {
|
|
277
|
+
const cleanName = recordName.replace(/\.$/, "");
|
|
278
|
+
const cleanDomain = domain.replace(/\.$/, "");
|
|
279
|
+
if (cleanName === cleanDomain) {
|
|
280
|
+
return "@";
|
|
281
|
+
}
|
|
282
|
+
if (cleanName.endsWith(`.${cleanDomain}`)) {
|
|
283
|
+
return cleanName.slice(0, -(cleanDomain.length + 1));
|
|
284
|
+
}
|
|
285
|
+
return cleanName;
|
|
286
|
+
}
|
|
287
|
+
getRootDomain(domain) {
|
|
288
|
+
const parts = domain.replace(/\.$/, "").split(".");
|
|
289
|
+
if (parts.length >= 2) {
|
|
290
|
+
return parts.slice(-2).join(".");
|
|
291
|
+
}
|
|
292
|
+
return domain;
|
|
293
|
+
}
|
|
294
|
+
toGoDaddyRecord(record, domain) {
|
|
295
|
+
const rootDomain = this.getRootDomain(domain);
|
|
296
|
+
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
297
|
+
const gdRecord = {
|
|
298
|
+
type: record.type,
|
|
299
|
+
name: subdomain,
|
|
300
|
+
data: record.content,
|
|
301
|
+
ttl: record.ttl || 600
|
|
302
|
+
};
|
|
303
|
+
if (record.type === "MX" && record.priority !== undefined) {
|
|
304
|
+
gdRecord.priority = record.priority;
|
|
305
|
+
}
|
|
306
|
+
return gdRecord;
|
|
307
|
+
}
|
|
308
|
+
fromGoDaddyRecord(record, domain) {
|
|
309
|
+
const rootDomain = this.getRootDomain(domain);
|
|
310
|
+
let name = record.name;
|
|
311
|
+
if (name === "@") {
|
|
312
|
+
name = rootDomain;
|
|
313
|
+
} else if (!name.endsWith(rootDomain)) {
|
|
314
|
+
name = `${name}.${rootDomain}`;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
name,
|
|
318
|
+
type: record.type,
|
|
319
|
+
content: record.data,
|
|
320
|
+
ttl: record.ttl,
|
|
321
|
+
priority: record.priority
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async createRecord(domain, record) {
|
|
325
|
+
try {
|
|
326
|
+
const rootDomain = this.getRootDomain(domain);
|
|
327
|
+
const gdRecord = this.toGoDaddyRecord(record, domain);
|
|
328
|
+
await this.request("PATCH", `/v1/domains/${rootDomain}/records`, [gdRecord]);
|
|
329
|
+
return {
|
|
330
|
+
success: true,
|
|
331
|
+
message: "Record created successfully"
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return {
|
|
335
|
+
success: false,
|
|
336
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async upsertRecord(domain, record) {
|
|
341
|
+
try {
|
|
342
|
+
const rootDomain = this.getRootDomain(domain);
|
|
343
|
+
const gdRecord = this.toGoDaddyRecord(record, domain);
|
|
344
|
+
await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${gdRecord.name}`, [gdRecord]);
|
|
345
|
+
return {
|
|
346
|
+
success: true,
|
|
347
|
+
message: "Record upserted successfully"
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return this.createRecord(domain, record);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async deleteRecord(domain, record) {
|
|
354
|
+
try {
|
|
355
|
+
const rootDomain = this.getRootDomain(domain);
|
|
356
|
+
const subdomain = this.getSubdomain(record.name, rootDomain);
|
|
357
|
+
const existingRecords = await this.request("GET", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
|
|
358
|
+
const remainingRecords = existingRecords.filter((r) => r.data !== record.content);
|
|
359
|
+
if (remainingRecords.length === existingRecords.length) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
message: "Record not found"
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (remainingRecords.length === 0) {
|
|
366
|
+
try {
|
|
367
|
+
await this.request("DELETE", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`);
|
|
368
|
+
} catch {}
|
|
369
|
+
} else {
|
|
370
|
+
await this.request("PUT", `/v1/domains/${rootDomain}/records/${record.type}/${subdomain}`, remainingRecords);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
success: true,
|
|
374
|
+
message: "Record deleted successfully"
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async listRecords(domain, type) {
|
|
384
|
+
try {
|
|
385
|
+
const rootDomain = this.getRootDomain(domain);
|
|
386
|
+
let endpoint = `/v1/domains/${rootDomain}/records`;
|
|
387
|
+
if (type) {
|
|
388
|
+
endpoint = `/v1/domains/${rootDomain}/records/${type}`;
|
|
389
|
+
}
|
|
390
|
+
const records = await this.request("GET", endpoint);
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
records: records.map((r) => this.fromGoDaddyRecord(r, domain))
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return {
|
|
397
|
+
success: false,
|
|
398
|
+
records: [],
|
|
399
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async canManageDomain(domain) {
|
|
404
|
+
try {
|
|
405
|
+
const rootDomain = this.getRootDomain(domain);
|
|
406
|
+
await this.request("GET", `/v1/domains/${rootDomain}`);
|
|
407
|
+
return true;
|
|
408
|
+
} catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async listDomains() {
|
|
413
|
+
try {
|
|
414
|
+
const response = await this.request("GET", "/v1/domains");
|
|
415
|
+
return response.map((d) => d.domain);
|
|
416
|
+
} catch {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async getDomainDetails(domain) {
|
|
421
|
+
try {
|
|
422
|
+
const rootDomain = this.getRootDomain(domain);
|
|
423
|
+
const details = await this.request("GET", `/v1/domains/${rootDomain}`);
|
|
424
|
+
return {
|
|
425
|
+
domain: details.domain,
|
|
426
|
+
status: details.status,
|
|
427
|
+
nameServers: details.nameServers,
|
|
428
|
+
expires: details.expires
|
|
429
|
+
};
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async updateNameServers(domain, nameservers) {
|
|
435
|
+
try {
|
|
436
|
+
const rootDomain = this.getRootDomain(domain);
|
|
437
|
+
await this.request("PUT", `/v1/domains/${rootDomain}/records/NS`, nameservers.map((ns) => ({
|
|
438
|
+
type: "NS",
|
|
439
|
+
name: "@",
|
|
440
|
+
data: ns,
|
|
441
|
+
ttl: 3600
|
|
442
|
+
})));
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async checkDomainAvailability(domain) {
|
|
449
|
+
try {
|
|
450
|
+
const result = await this.request("GET", `/v1/domains/available?domain=${encodeURIComponent(domain)}`);
|
|
451
|
+
return {
|
|
452
|
+
available: result.available,
|
|
453
|
+
price: result.price,
|
|
454
|
+
currency: result.currency
|
|
455
|
+
};
|
|
456
|
+
} catch {
|
|
457
|
+
return { available: false };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// src/dns/cloudflare.ts
|
|
462
|
+
var CLOUDFLARE_API_URL = "https://api.cloudflare.com/client/v4";
|
|
463
|
+
|
|
464
|
+
class CloudflareProvider {
|
|
465
|
+
name = "cloudflare";
|
|
466
|
+
apiToken;
|
|
467
|
+
zoneCache = new Map;
|
|
468
|
+
constructor(apiToken) {
|
|
469
|
+
this.apiToken = apiToken;
|
|
470
|
+
}
|
|
471
|
+
async request(method, endpoint, body) {
|
|
472
|
+
const url = `${CLOUDFLARE_API_URL}${endpoint}`;
|
|
473
|
+
const headers = {
|
|
474
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
475
|
+
"Content-Type": "application/json"
|
|
476
|
+
};
|
|
477
|
+
const options = {
|
|
478
|
+
method,
|
|
479
|
+
headers
|
|
480
|
+
};
|
|
481
|
+
if (body) {
|
|
482
|
+
options.body = JSON.stringify(body);
|
|
483
|
+
}
|
|
484
|
+
const response = await fetch(url, options);
|
|
485
|
+
const data = await response.json();
|
|
486
|
+
if (!data.success) {
|
|
487
|
+
const errorMessages = data.errors.map((e) => e.message).join(", ");
|
|
488
|
+
throw new Error(`Cloudflare API error: ${errorMessages}`);
|
|
489
|
+
}
|
|
490
|
+
return data;
|
|
491
|
+
}
|
|
492
|
+
getRootDomain(domain) {
|
|
493
|
+
const parts = domain.replace(/\.$/, "").split(".");
|
|
494
|
+
if (parts.length >= 2) {
|
|
495
|
+
return parts.slice(-2).join(".");
|
|
496
|
+
}
|
|
497
|
+
return domain;
|
|
498
|
+
}
|
|
499
|
+
async getZoneId(domain) {
|
|
500
|
+
const rootDomain = this.getRootDomain(domain);
|
|
501
|
+
const cached = this.zoneCache.get(rootDomain);
|
|
502
|
+
if (cached) {
|
|
503
|
+
return cached;
|
|
504
|
+
}
|
|
505
|
+
const response = await this.request("GET", `/zones?name=${encodeURIComponent(rootDomain)}`);
|
|
506
|
+
if (!response.result || response.result.length === 0) {
|
|
507
|
+
throw new Error(`Zone not found for domain: ${rootDomain}`);
|
|
508
|
+
}
|
|
509
|
+
const zoneId = response.result[0].id;
|
|
510
|
+
this.zoneCache.set(rootDomain, zoneId);
|
|
511
|
+
return zoneId;
|
|
512
|
+
}
|
|
513
|
+
getFullRecordName(name, domain) {
|
|
514
|
+
const rootDomain = this.getRootDomain(domain);
|
|
515
|
+
const cleanName = name.replace(/\.$/, "");
|
|
516
|
+
if (!cleanName || cleanName === rootDomain || cleanName === "@") {
|
|
517
|
+
return rootDomain;
|
|
518
|
+
}
|
|
519
|
+
if (cleanName.endsWith(`.${rootDomain}`)) {
|
|
520
|
+
return cleanName;
|
|
521
|
+
}
|
|
522
|
+
return `${cleanName}.${rootDomain}`;
|
|
523
|
+
}
|
|
524
|
+
toCloudflareRecord(record, domain) {
|
|
525
|
+
const cfRecord = {
|
|
526
|
+
type: record.type,
|
|
527
|
+
name: this.getFullRecordName(record.name, domain),
|
|
528
|
+
content: record.content || record.value || "",
|
|
529
|
+
ttl: record.ttl || 1
|
|
530
|
+
};
|
|
531
|
+
if (record.type === "MX" && record.priority !== undefined) {
|
|
532
|
+
cfRecord.priority = record.priority;
|
|
533
|
+
}
|
|
534
|
+
if (record.type === "SRV") {
|
|
535
|
+
if (record.priority !== undefined) {
|
|
536
|
+
cfRecord.priority = record.priority;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return cfRecord;
|
|
540
|
+
}
|
|
541
|
+
fromCloudflareRecord(record) {
|
|
542
|
+
return {
|
|
543
|
+
id: record.id,
|
|
544
|
+
name: record.name,
|
|
545
|
+
type: record.type,
|
|
546
|
+
content: record.content,
|
|
547
|
+
ttl: record.ttl,
|
|
548
|
+
priority: record.priority
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async createRecord(domain, record) {
|
|
552
|
+
try {
|
|
553
|
+
const zoneId = await this.getZoneId(domain);
|
|
554
|
+
const cfRecord = this.toCloudflareRecord(record, domain);
|
|
555
|
+
const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
id: response.result.id,
|
|
559
|
+
message: "Record created successfully"
|
|
560
|
+
};
|
|
561
|
+
} catch (error) {
|
|
562
|
+
return {
|
|
563
|
+
success: false,
|
|
564
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async upsertRecord(domain, record) {
|
|
569
|
+
try {
|
|
570
|
+
const zoneId = await this.getZoneId(domain);
|
|
571
|
+
const fullName = this.getFullRecordName(record.name, domain);
|
|
572
|
+
const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
573
|
+
const cfRecord = this.toCloudflareRecord(record, domain);
|
|
574
|
+
if (existingResponse.result && existingResponse.result.length > 0) {
|
|
575
|
+
const existingId = existingResponse.result[0].id;
|
|
576
|
+
const response2 = await this.request("PUT", `/zones/${zoneId}/dns_records/${existingId}`, cfRecord);
|
|
577
|
+
return {
|
|
578
|
+
success: true,
|
|
579
|
+
id: response2.result.id,
|
|
580
|
+
message: "Record updated successfully"
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const response = await this.request("POST", `/zones/${zoneId}/dns_records`, cfRecord);
|
|
584
|
+
return {
|
|
585
|
+
success: true,
|
|
586
|
+
id: response.result.id,
|
|
587
|
+
message: "Record created successfully"
|
|
588
|
+
};
|
|
589
|
+
} catch (error) {
|
|
590
|
+
return this.createRecord(domain, record);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async deleteRecord(domain, record) {
|
|
594
|
+
try {
|
|
595
|
+
const zoneId = await this.getZoneId(domain);
|
|
596
|
+
const fullName = this.getFullRecordName(record.name, domain);
|
|
597
|
+
const existingResponse = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
598
|
+
if (!existingResponse.result || existingResponse.result.length === 0) {
|
|
599
|
+
return {
|
|
600
|
+
success: false,
|
|
601
|
+
message: "Record not found"
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const matchingRecord = existingResponse.result.find((r) => r.content === record.content);
|
|
605
|
+
if (!matchingRecord) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
message: "Record with matching content not found"
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
await this.request("DELETE", `/zones/${zoneId}/dns_records/${matchingRecord.id}`);
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
message: "Record deleted successfully"
|
|
615
|
+
};
|
|
616
|
+
} catch (error) {
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async listRecords(domain, type) {
|
|
624
|
+
try {
|
|
625
|
+
const zoneId = await this.getZoneId(domain);
|
|
626
|
+
let endpoint = `/zones/${zoneId}/dns_records?per_page=100`;
|
|
627
|
+
if (type) {
|
|
628
|
+
endpoint += `&type=${type}`;
|
|
629
|
+
}
|
|
630
|
+
const allRecords = [];
|
|
631
|
+
let page = 1;
|
|
632
|
+
let hasMore = true;
|
|
633
|
+
while (hasMore) {
|
|
634
|
+
const response = await this.request("GET", `${endpoint}&page=${page}`);
|
|
635
|
+
allRecords.push(...response.result || []);
|
|
636
|
+
if (response.result_info) {
|
|
637
|
+
hasMore = page < response.result_info.total_pages;
|
|
638
|
+
page++;
|
|
639
|
+
} else {
|
|
640
|
+
hasMore = false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
records: allRecords.map((r) => this.fromCloudflareRecord(r))
|
|
646
|
+
};
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
records: [],
|
|
651
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async canManageDomain(domain) {
|
|
656
|
+
try {
|
|
657
|
+
await this.getZoneId(domain);
|
|
658
|
+
return true;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async listDomains() {
|
|
664
|
+
try {
|
|
665
|
+
const allZones = [];
|
|
666
|
+
let page = 1;
|
|
667
|
+
let hasMore = true;
|
|
668
|
+
while (hasMore) {
|
|
669
|
+
const response = await this.request("GET", `/zones?per_page=50&page=${page}`);
|
|
670
|
+
allZones.push(...response.result || []);
|
|
671
|
+
if (response.result_info) {
|
|
672
|
+
hasMore = page < response.result_info.total_pages;
|
|
673
|
+
page++;
|
|
674
|
+
} else {
|
|
675
|
+
hasMore = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return allZones.map((z) => z.name);
|
|
679
|
+
} catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async getZoneDetails(domain) {
|
|
684
|
+
try {
|
|
685
|
+
const zoneId = await this.getZoneId(domain);
|
|
686
|
+
const response = await this.request("GET", `/zones/${zoneId}`);
|
|
687
|
+
return {
|
|
688
|
+
id: response.result.id,
|
|
689
|
+
name: response.result.name,
|
|
690
|
+
status: response.result.status,
|
|
691
|
+
nameServers: response.result.name_servers,
|
|
692
|
+
paused: response.result.paused
|
|
693
|
+
};
|
|
694
|
+
} catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async purgeCache(domain, options) {
|
|
699
|
+
try {
|
|
700
|
+
const zoneId = await this.getZoneId(domain);
|
|
701
|
+
const body = {};
|
|
702
|
+
if (options?.purgeEverything) {
|
|
703
|
+
body.purge_everything = true;
|
|
704
|
+
} else {
|
|
705
|
+
if (options?.files)
|
|
706
|
+
body.files = options.files;
|
|
707
|
+
if (options?.tags)
|
|
708
|
+
body.tags = options.tags;
|
|
709
|
+
if (options?.hosts)
|
|
710
|
+
body.hosts = options.hosts;
|
|
711
|
+
}
|
|
712
|
+
if (Object.keys(body).length === 0) {
|
|
713
|
+
body.purge_everything = true;
|
|
714
|
+
}
|
|
715
|
+
await this.request("POST", `/zones/${zoneId}/purge_cache`, body);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async getRecordProxyStatus(domain, record) {
|
|
722
|
+
try {
|
|
723
|
+
const zoneId = await this.getZoneId(domain);
|
|
724
|
+
const fullName = this.getFullRecordName(record.name, domain);
|
|
725
|
+
const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
726
|
+
if (response.result && response.result.length > 0) {
|
|
727
|
+
const matchingRecord = response.result.find((r) => r.content === record.content);
|
|
728
|
+
return matchingRecord?.proxied ?? null;
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
} catch {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async setRecordProxyStatus(domain, record, proxied) {
|
|
736
|
+
try {
|
|
737
|
+
const zoneId = await this.getZoneId(domain);
|
|
738
|
+
const fullName = this.getFullRecordName(record.name, domain);
|
|
739
|
+
const response = await this.request("GET", `/zones/${zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(fullName)}`);
|
|
740
|
+
if (!response.result || response.result.length === 0) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
const matchingRecord = response.result.find((r) => r.content === record.content);
|
|
744
|
+
if (!matchingRecord) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
await this.request("PATCH", `/zones/${zoneId}/dns_records/${matchingRecord.id}`, { proxied });
|
|
748
|
+
return true;
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// src/dns/route53-adapter.ts
|
|
755
|
+
class Route53Provider {
|
|
756
|
+
name = "route53";
|
|
757
|
+
client;
|
|
758
|
+
hostedZoneCache = new Map;
|
|
759
|
+
providedHostedZoneId;
|
|
760
|
+
constructor(region = "us-east-1", hostedZoneId) {
|
|
761
|
+
this.client = new Route53Client(region);
|
|
762
|
+
this.providedHostedZoneId = hostedZoneId;
|
|
763
|
+
}
|
|
764
|
+
getRootDomain(domain) {
|
|
765
|
+
const parts = domain.replace(/\.$/, "").split(".");
|
|
766
|
+
if (parts.length >= 2) {
|
|
767
|
+
return parts.slice(-2).join(".");
|
|
768
|
+
}
|
|
769
|
+
return domain;
|
|
770
|
+
}
|
|
771
|
+
async getHostedZoneId(domain) {
|
|
772
|
+
if (this.providedHostedZoneId) {
|
|
773
|
+
return this.providedHostedZoneId;
|
|
774
|
+
}
|
|
775
|
+
const rootDomain = this.getRootDomain(domain);
|
|
776
|
+
const cached = this.hostedZoneCache.get(rootDomain);
|
|
777
|
+
if (cached) {
|
|
778
|
+
return cached;
|
|
779
|
+
}
|
|
780
|
+
const zone = await this.client.findHostedZoneForDomain(domain);
|
|
781
|
+
if (zone) {
|
|
782
|
+
const zoneId = zone.Id.replace("/hostedzone/", "");
|
|
783
|
+
this.hostedZoneCache.set(rootDomain, zoneId);
|
|
784
|
+
return zoneId;
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
normalizeName(name) {
|
|
789
|
+
return name.endsWith(".") ? name : `${name}.`;
|
|
790
|
+
}
|
|
791
|
+
async createRecord(domain, record) {
|
|
792
|
+
try {
|
|
793
|
+
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
794
|
+
if (!hostedZoneId) {
|
|
795
|
+
return {
|
|
796
|
+
success: false,
|
|
797
|
+
message: `No hosted zone found for domain: ${domain}`
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const recordName = this.normalizeName(record.name);
|
|
801
|
+
let recordValue = record.content;
|
|
802
|
+
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
803
|
+
recordValue = `"${recordValue}"`;
|
|
804
|
+
}
|
|
805
|
+
if (record.type === "MX" && record.priority !== undefined) {
|
|
806
|
+
recordValue = `${record.priority} ${recordValue}`;
|
|
807
|
+
}
|
|
808
|
+
const result = await this.client.changeResourceRecordSets({
|
|
809
|
+
HostedZoneId: hostedZoneId,
|
|
810
|
+
ChangeBatch: {
|
|
811
|
+
Comment: `Created by ts-cloud DNS provider`,
|
|
812
|
+
Changes: [{
|
|
813
|
+
Action: "CREATE",
|
|
814
|
+
ResourceRecordSet: {
|
|
815
|
+
Name: recordName,
|
|
816
|
+
Type: record.type,
|
|
817
|
+
TTL: record.ttl || 300,
|
|
818
|
+
ResourceRecords: [{ Value: recordValue }]
|
|
819
|
+
}
|
|
820
|
+
}]
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
return {
|
|
824
|
+
success: true,
|
|
825
|
+
id: result.ChangeInfo?.Id,
|
|
826
|
+
message: "Record created successfully"
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
return {
|
|
830
|
+
success: false,
|
|
831
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async upsertRecord(domain, record) {
|
|
836
|
+
try {
|
|
837
|
+
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
838
|
+
if (!hostedZoneId) {
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
message: `No hosted zone found for domain: ${domain}`
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const recordName = this.normalizeName(record.name);
|
|
845
|
+
let recordValue = record.content;
|
|
846
|
+
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
847
|
+
recordValue = `"${recordValue}"`;
|
|
848
|
+
}
|
|
849
|
+
if (record.type === "MX" && record.priority !== undefined) {
|
|
850
|
+
recordValue = `${record.priority} ${recordValue}`;
|
|
851
|
+
}
|
|
852
|
+
const result = await this.client.changeResourceRecordSets({
|
|
853
|
+
HostedZoneId: hostedZoneId,
|
|
854
|
+
ChangeBatch: {
|
|
855
|
+
Comment: `Upserted by ts-cloud DNS provider`,
|
|
856
|
+
Changes: [{
|
|
857
|
+
Action: "UPSERT",
|
|
858
|
+
ResourceRecordSet: {
|
|
859
|
+
Name: recordName,
|
|
860
|
+
Type: record.type,
|
|
861
|
+
TTL: record.ttl || 300,
|
|
862
|
+
ResourceRecords: [{ Value: recordValue }]
|
|
863
|
+
}
|
|
864
|
+
}]
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
return {
|
|
868
|
+
success: true,
|
|
869
|
+
id: result.ChangeInfo?.Id,
|
|
870
|
+
message: "Record upserted successfully"
|
|
871
|
+
};
|
|
872
|
+
} catch (error) {
|
|
873
|
+
return {
|
|
874
|
+
success: false,
|
|
875
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async deleteRecord(domain, record) {
|
|
880
|
+
try {
|
|
881
|
+
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
882
|
+
if (!hostedZoneId) {
|
|
883
|
+
return {
|
|
884
|
+
success: false,
|
|
885
|
+
message: `No hosted zone found for domain: ${domain}`
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const recordName = this.normalizeName(record.name);
|
|
889
|
+
let recordValue = record.content;
|
|
890
|
+
if (record.type === "TXT" && !recordValue.startsWith('"')) {
|
|
891
|
+
recordValue = `"${recordValue}"`;
|
|
892
|
+
}
|
|
893
|
+
if (record.type === "MX" && record.priority !== undefined) {
|
|
894
|
+
recordValue = `${record.priority} ${recordValue}`;
|
|
895
|
+
}
|
|
896
|
+
await this.client.changeResourceRecordSets({
|
|
897
|
+
HostedZoneId: hostedZoneId,
|
|
898
|
+
ChangeBatch: {
|
|
899
|
+
Comment: `Deleted by ts-cloud DNS provider`,
|
|
900
|
+
Changes: [{
|
|
901
|
+
Action: "DELETE",
|
|
902
|
+
ResourceRecordSet: {
|
|
903
|
+
Name: recordName,
|
|
904
|
+
Type: record.type,
|
|
905
|
+
TTL: record.ttl || 300,
|
|
906
|
+
ResourceRecords: [{ Value: recordValue }]
|
|
907
|
+
}
|
|
908
|
+
}]
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
success: true,
|
|
913
|
+
message: "Record deleted successfully"
|
|
914
|
+
};
|
|
915
|
+
} catch (error) {
|
|
916
|
+
return {
|
|
917
|
+
success: false,
|
|
918
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async listRecords(domain, type) {
|
|
923
|
+
try {
|
|
924
|
+
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
925
|
+
if (!hostedZoneId) {
|
|
926
|
+
return {
|
|
927
|
+
success: false,
|
|
928
|
+
records: [],
|
|
929
|
+
message: `No hosted zone found for domain: ${domain}`
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const result = await this.client.listResourceRecordSets({
|
|
933
|
+
HostedZoneId: hostedZoneId,
|
|
934
|
+
StartRecordType: type
|
|
935
|
+
});
|
|
936
|
+
const records = [];
|
|
937
|
+
for (const rs of result.ResourceRecordSets) {
|
|
938
|
+
if (type && rs.Type !== type) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (rs.AliasTarget) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
for (const rr of rs.ResourceRecords || []) {
|
|
945
|
+
let content = rr.Value;
|
|
946
|
+
let priority;
|
|
947
|
+
if (rs.Type === "MX") {
|
|
948
|
+
const parts = content.split(" ");
|
|
949
|
+
if (parts.length >= 2) {
|
|
950
|
+
priority = Number.parseInt(parts[0], 10);
|
|
951
|
+
content = parts.slice(1).join(" ");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (rs.Type === "TXT" && content.startsWith('"') && content.endsWith('"')) {
|
|
955
|
+
content = content.slice(1, -1);
|
|
956
|
+
}
|
|
957
|
+
records.push({
|
|
958
|
+
name: rs.Name.replace(/\.$/, ""),
|
|
959
|
+
type: rs.Type,
|
|
960
|
+
content,
|
|
961
|
+
ttl: rs.TTL,
|
|
962
|
+
priority
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
success: true,
|
|
968
|
+
records
|
|
969
|
+
};
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
success: false,
|
|
973
|
+
records: [],
|
|
974
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async canManageDomain(domain) {
|
|
979
|
+
const hostedZoneId = await this.getHostedZoneId(domain);
|
|
980
|
+
return hostedZoneId !== null;
|
|
981
|
+
}
|
|
982
|
+
async listDomains() {
|
|
983
|
+
try {
|
|
984
|
+
const result = await this.client.listHostedZones();
|
|
985
|
+
return result.HostedZones.map((z) => z.Name.replace(/\.$/, ""));
|
|
986
|
+
} catch {
|
|
987
|
+
return [];
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
getRoute53Client() {
|
|
991
|
+
return this.client;
|
|
992
|
+
}
|
|
993
|
+
async createAliasRecord(params) {
|
|
994
|
+
try {
|
|
995
|
+
const hostedZoneId = await this.getHostedZoneId(params.domain);
|
|
996
|
+
if (!hostedZoneId) {
|
|
997
|
+
return {
|
|
998
|
+
success: false,
|
|
999
|
+
message: `No hosted zone found for domain: ${params.domain}`
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
const result = await this.client.createAliasRecord({
|
|
1003
|
+
HostedZoneId: hostedZoneId,
|
|
1004
|
+
Name: this.normalizeName(params.name),
|
|
1005
|
+
TargetHostedZoneId: params.targetHostedZoneId,
|
|
1006
|
+
TargetDNSName: params.targetDnsName,
|
|
1007
|
+
EvaluateTargetHealth: params.evaluateTargetHealth,
|
|
1008
|
+
Type: params.type
|
|
1009
|
+
});
|
|
1010
|
+
return {
|
|
1011
|
+
success: true,
|
|
1012
|
+
id: result.ChangeInfo?.Id,
|
|
1013
|
+
message: "Alias record created successfully"
|
|
1014
|
+
};
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
return {
|
|
1017
|
+
success: false,
|
|
1018
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async createCloudFrontAlias(params) {
|
|
1023
|
+
return this.createAliasRecord({
|
|
1024
|
+
domain: params.domain,
|
|
1025
|
+
name: params.name,
|
|
1026
|
+
targetHostedZoneId: Route53Client.CloudFrontHostedZoneId,
|
|
1027
|
+
targetDnsName: params.cloudFrontDomainName,
|
|
1028
|
+
evaluateTargetHealth: false
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
async createAlbAlias(params) {
|
|
1032
|
+
const hostedZoneId = Route53Client.ALBHostedZoneIds[params.region];
|
|
1033
|
+
if (!hostedZoneId) {
|
|
1034
|
+
return {
|
|
1035
|
+
success: false,
|
|
1036
|
+
message: `Unknown region for ALB: ${params.region}`
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return this.createAliasRecord({
|
|
1040
|
+
domain: params.domain,
|
|
1041
|
+
name: params.name,
|
|
1042
|
+
targetHostedZoneId: hostedZoneId,
|
|
1043
|
+
targetDnsName: params.albDnsName,
|
|
1044
|
+
evaluateTargetHealth: true
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// src/aws/acm.ts
|
|
1049
|
+
class ACMClient {
|
|
1050
|
+
client;
|
|
1051
|
+
region;
|
|
1052
|
+
constructor(region = "us-east-1") {
|
|
1053
|
+
this.client = new AWSClient;
|
|
1054
|
+
this.region = region;
|
|
1055
|
+
}
|
|
1056
|
+
async requestCertificate(params) {
|
|
1057
|
+
const requestBody = {
|
|
1058
|
+
DomainName: params.DomainName,
|
|
1059
|
+
ValidationMethod: params.ValidationMethod || "DNS"
|
|
1060
|
+
};
|
|
1061
|
+
if (params.SubjectAlternativeNames) {
|
|
1062
|
+
requestBody.SubjectAlternativeNames = params.SubjectAlternativeNames;
|
|
1063
|
+
}
|
|
1064
|
+
const result = await this.client.request({
|
|
1065
|
+
service: "acm",
|
|
1066
|
+
region: this.region,
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
path: "/",
|
|
1069
|
+
headers: {
|
|
1070
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1071
|
+
"x-amz-target": "CertificateManager.RequestCertificate"
|
|
1072
|
+
},
|
|
1073
|
+
body: JSON.stringify(requestBody)
|
|
1074
|
+
});
|
|
1075
|
+
return {
|
|
1076
|
+
CertificateArn: result.CertificateArn || ""
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
async describeCertificate(params) {
|
|
1080
|
+
const result = await this.client.request({
|
|
1081
|
+
service: "acm",
|
|
1082
|
+
region: this.region,
|
|
1083
|
+
method: "POST",
|
|
1084
|
+
path: "/",
|
|
1085
|
+
headers: {
|
|
1086
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1087
|
+
"x-amz-target": "CertificateManager.DescribeCertificate"
|
|
1088
|
+
},
|
|
1089
|
+
body: JSON.stringify({
|
|
1090
|
+
CertificateArn: params.CertificateArn
|
|
1091
|
+
})
|
|
1092
|
+
});
|
|
1093
|
+
const cert = result.Certificate || {};
|
|
1094
|
+
return {
|
|
1095
|
+
CertificateArn: cert.CertificateArn || "",
|
|
1096
|
+
DomainName: cert.DomainName || "",
|
|
1097
|
+
SubjectAlternativeNames: cert.SubjectAlternativeNames,
|
|
1098
|
+
Status: cert.Status || "PENDING_VALIDATION",
|
|
1099
|
+
Type: cert.Type,
|
|
1100
|
+
DomainValidationOptions: cert.DomainValidationOptions?.map((opt) => ({
|
|
1101
|
+
DomainName: opt.DomainName,
|
|
1102
|
+
ValidationDomain: opt.ValidationDomain,
|
|
1103
|
+
ValidationStatus: opt.ValidationStatus,
|
|
1104
|
+
ResourceRecord: opt.ResourceRecord ? {
|
|
1105
|
+
Name: opt.ResourceRecord.Name,
|
|
1106
|
+
Type: opt.ResourceRecord.Type,
|
|
1107
|
+
Value: opt.ResourceRecord.Value
|
|
1108
|
+
} : undefined,
|
|
1109
|
+
ValidationMethod: opt.ValidationMethod
|
|
1110
|
+
})),
|
|
1111
|
+
CreatedAt: cert.CreatedAt,
|
|
1112
|
+
IssuedAt: cert.IssuedAt,
|
|
1113
|
+
NotBefore: cert.NotBefore,
|
|
1114
|
+
NotAfter: cert.NotAfter
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async listCertificates(params) {
|
|
1118
|
+
const requestBody = {};
|
|
1119
|
+
if (params?.CertificateStatuses) {
|
|
1120
|
+
requestBody.CertificateStatuses = params.CertificateStatuses;
|
|
1121
|
+
}
|
|
1122
|
+
if (params?.MaxItems) {
|
|
1123
|
+
requestBody.MaxItems = params.MaxItems;
|
|
1124
|
+
}
|
|
1125
|
+
if (params?.NextToken) {
|
|
1126
|
+
requestBody.NextToken = params.NextToken;
|
|
1127
|
+
}
|
|
1128
|
+
const result = await this.client.request({
|
|
1129
|
+
service: "acm",
|
|
1130
|
+
region: this.region,
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
path: "/",
|
|
1133
|
+
headers: {
|
|
1134
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1135
|
+
"x-amz-target": "CertificateManager.ListCertificates"
|
|
1136
|
+
},
|
|
1137
|
+
body: JSON.stringify(requestBody)
|
|
1138
|
+
});
|
|
1139
|
+
return {
|
|
1140
|
+
CertificateSummaryList: (result.CertificateSummaryList || []).map((cert) => ({
|
|
1141
|
+
CertificateArn: cert.CertificateArn,
|
|
1142
|
+
DomainName: cert.DomainName
|
|
1143
|
+
})),
|
|
1144
|
+
NextToken: result.NextToken
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async deleteCertificate(params) {
|
|
1148
|
+
await this.client.request({
|
|
1149
|
+
service: "acm",
|
|
1150
|
+
region: this.region,
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
path: "/",
|
|
1153
|
+
headers: {
|
|
1154
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1155
|
+
"x-amz-target": "CertificateManager.DeleteCertificate"
|
|
1156
|
+
},
|
|
1157
|
+
body: JSON.stringify({
|
|
1158
|
+
CertificateArn: params.CertificateArn
|
|
1159
|
+
})
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async listTagsForCertificate(params) {
|
|
1163
|
+
const result = await this.client.request({
|
|
1164
|
+
service: "acm",
|
|
1165
|
+
region: this.region,
|
|
1166
|
+
method: "POST",
|
|
1167
|
+
path: "/",
|
|
1168
|
+
headers: {
|
|
1169
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1170
|
+
"x-amz-target": "CertificateManager.ListTagsForCertificate"
|
|
1171
|
+
},
|
|
1172
|
+
body: JSON.stringify({
|
|
1173
|
+
CertificateArn: params.CertificateArn
|
|
1174
|
+
})
|
|
1175
|
+
});
|
|
1176
|
+
return {
|
|
1177
|
+
Tags: result.Tags || []
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
async addTagsToCertificate(params) {
|
|
1181
|
+
await this.client.request({
|
|
1182
|
+
service: "acm",
|
|
1183
|
+
region: this.region,
|
|
1184
|
+
method: "POST",
|
|
1185
|
+
path: "/",
|
|
1186
|
+
headers: {
|
|
1187
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1188
|
+
"x-amz-target": "CertificateManager.AddTagsToCertificate"
|
|
1189
|
+
},
|
|
1190
|
+
body: JSON.stringify({
|
|
1191
|
+
CertificateArn: params.CertificateArn,
|
|
1192
|
+
Tags: params.Tags
|
|
1193
|
+
})
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async resendValidationEmail(params) {
|
|
1197
|
+
await this.client.request({
|
|
1198
|
+
service: "acm",
|
|
1199
|
+
region: this.region,
|
|
1200
|
+
method: "POST",
|
|
1201
|
+
path: "/",
|
|
1202
|
+
headers: {
|
|
1203
|
+
"content-type": "application/x-amz-json-1.1",
|
|
1204
|
+
"x-amz-target": "CertificateManager.ResendValidationEmail"
|
|
1205
|
+
},
|
|
1206
|
+
body: JSON.stringify({
|
|
1207
|
+
CertificateArn: params.CertificateArn,
|
|
1208
|
+
Domain: params.Domain,
|
|
1209
|
+
ValidationDomain: params.ValidationDomain
|
|
1210
|
+
})
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
async findCertificateByDomain(domainName) {
|
|
1214
|
+
const result = await this.listCertificates({
|
|
1215
|
+
CertificateStatuses: ["ISSUED"]
|
|
1216
|
+
});
|
|
1217
|
+
const summary = result.CertificateSummaryList.find((c) => c.DomainName === domainName || c.DomainName === `*.${domainName.split(".").slice(1).join(".")}`);
|
|
1218
|
+
if (!summary) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
return this.describeCertificate({ CertificateArn: summary.CertificateArn });
|
|
1222
|
+
}
|
|
1223
|
+
async waitForCertificateValidation(certificateArn, maxAttempts = 60, delayMs = 30000) {
|
|
1224
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
1225
|
+
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1226
|
+
if (cert.Status === "ISSUED") {
|
|
1227
|
+
return cert;
|
|
1228
|
+
}
|
|
1229
|
+
if (cert.Status === "FAILED" || cert.Status === "VALIDATION_TIMED_OUT") {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
async getDnsValidationRecords(certificateArn) {
|
|
1237
|
+
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1238
|
+
if (!cert.DomainValidationOptions) {
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
return cert.DomainValidationOptions.filter((opt) => opt.ResourceRecord && opt.ValidationMethod === "DNS").map((opt) => ({
|
|
1242
|
+
domainName: opt.DomainName,
|
|
1243
|
+
recordName: opt.ResourceRecord.Name,
|
|
1244
|
+
recordType: opt.ResourceRecord.Type,
|
|
1245
|
+
recordValue: opt.ResourceRecord.Value
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1248
|
+
async requestCertificateWithSans(params) {
|
|
1249
|
+
const sans = new Set;
|
|
1250
|
+
sans.add(params.DomainName);
|
|
1251
|
+
if (params.IncludeWww !== false) {
|
|
1252
|
+
sans.add(`www.${params.DomainName}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (params.IncludeWildcard) {
|
|
1255
|
+
sans.add(`*.${params.DomainName}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (params.AdditionalSans) {
|
|
1258
|
+
for (const san of params.AdditionalSans) {
|
|
1259
|
+
sans.add(san);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return this.requestCertificate({
|
|
1263
|
+
DomainName: params.DomainName,
|
|
1264
|
+
SubjectAlternativeNames: Array.from(sans),
|
|
1265
|
+
ValidationMethod: "DNS"
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
async isCertificateValidForDomain(certificateArn, domainName) {
|
|
1269
|
+
const cert = await this.describeCertificate({ CertificateArn: certificateArn });
|
|
1270
|
+
if (cert.Status !== "ISSUED") {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
if (cert.DomainName === domainName) {
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
if (cert.DomainName?.startsWith("*.")) {
|
|
1277
|
+
const baseDomain = cert.DomainName.slice(2);
|
|
1278
|
+
const domainParts = domainName.split(".");
|
|
1279
|
+
const baseParts = baseDomain.split(".");
|
|
1280
|
+
if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (cert.SubjectAlternativeNames) {
|
|
1285
|
+
for (const san of cert.SubjectAlternativeNames) {
|
|
1286
|
+
if (san === domainName) {
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
if (san.startsWith("*.")) {
|
|
1290
|
+
const baseDomain = san.slice(2);
|
|
1291
|
+
const domainParts = domainName.split(".");
|
|
1292
|
+
const baseParts = baseDomain.split(".");
|
|
1293
|
+
if (domainParts.slice(-baseParts.length).join(".") === baseDomain) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
class ACMDnsValidator {
|
|
1304
|
+
acm;
|
|
1305
|
+
route53;
|
|
1306
|
+
dnsProvider;
|
|
1307
|
+
constructor(region = "us-east-1", dnsProviderConfig) {
|
|
1308
|
+
this.acm = new ACMClient(region);
|
|
1309
|
+
this.route53 = new Route53Client;
|
|
1310
|
+
if (dnsProviderConfig && dnsProviderConfig.provider !== "route53") {
|
|
1311
|
+
this.dnsProvider = createDnsProvider(dnsProviderConfig);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async requestAndValidate(params) {
|
|
1315
|
+
const {
|
|
1316
|
+
domainName,
|
|
1317
|
+
hostedZoneId,
|
|
1318
|
+
subjectAlternativeNames = [],
|
|
1319
|
+
waitForValidation = false,
|
|
1320
|
+
maxWaitMinutes = 30
|
|
1321
|
+
} = params;
|
|
1322
|
+
if (!this.dnsProvider && !hostedZoneId) {
|
|
1323
|
+
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1324
|
+
}
|
|
1325
|
+
const { CertificateArn } = await this.acm.requestCertificate({
|
|
1326
|
+
DomainName: domainName,
|
|
1327
|
+
SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
|
|
1328
|
+
ValidationMethod: "DNS"
|
|
1329
|
+
});
|
|
1330
|
+
await this.waitForValidationOptions(CertificateArn);
|
|
1331
|
+
const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
|
|
1332
|
+
if (this.dnsProvider) {
|
|
1333
|
+
for (const record of validationRecords) {
|
|
1334
|
+
const result = await this.dnsProvider.upsertRecord(domainName, {
|
|
1335
|
+
name: record.recordName,
|
|
1336
|
+
type: record.recordType,
|
|
1337
|
+
content: record.recordValue,
|
|
1338
|
+
ttl: 300
|
|
1339
|
+
});
|
|
1340
|
+
if (!result.success) {
|
|
1341
|
+
console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} else if (hostedZoneId) {
|
|
1345
|
+
for (const record of validationRecords) {
|
|
1346
|
+
await this.route53.changeResourceRecordSets({
|
|
1347
|
+
HostedZoneId: hostedZoneId,
|
|
1348
|
+
ChangeBatch: {
|
|
1349
|
+
Comment: `ACM DNS validation for ${record.domainName}`,
|
|
1350
|
+
Changes: [{
|
|
1351
|
+
Action: "UPSERT",
|
|
1352
|
+
ResourceRecordSet: {
|
|
1353
|
+
Name: record.recordName,
|
|
1354
|
+
Type: record.recordType,
|
|
1355
|
+
TTL: 300,
|
|
1356
|
+
ResourceRecords: [{ Value: record.recordValue }]
|
|
1357
|
+
}
|
|
1358
|
+
}]
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (waitForValidation) {
|
|
1364
|
+
const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
|
|
1365
|
+
if (!cert) {
|
|
1366
|
+
throw new Error(`Certificate validation timed out after ${maxWaitMinutes} minutes`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
certificateArn: CertificateArn,
|
|
1371
|
+
validationRecords
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
async waitForValidationOptions(certificateArn, maxAttempts = 30) {
|
|
1375
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
1376
|
+
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1377
|
+
if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1381
|
+
}
|
|
1382
|
+
throw new Error("Timeout waiting for DNS validation options");
|
|
1383
|
+
}
|
|
1384
|
+
async createValidationRecords(params) {
|
|
1385
|
+
const { certificateArn, hostedZoneId, domain } = params;
|
|
1386
|
+
if (!this.dnsProvider && !hostedZoneId) {
|
|
1387
|
+
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1388
|
+
}
|
|
1389
|
+
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1390
|
+
const results = [];
|
|
1391
|
+
if (this.dnsProvider) {
|
|
1392
|
+
const targetDomain = domain || validationRecords[0]?.domainName;
|
|
1393
|
+
for (const record of validationRecords) {
|
|
1394
|
+
const result = await this.dnsProvider.upsertRecord(targetDomain, {
|
|
1395
|
+
name: record.recordName,
|
|
1396
|
+
type: record.recordType,
|
|
1397
|
+
content: record.recordValue,
|
|
1398
|
+
ttl: 300
|
|
1399
|
+
});
|
|
1400
|
+
results.push({
|
|
1401
|
+
...record,
|
|
1402
|
+
changeId: result.success ? result.id : undefined
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
} else if (hostedZoneId) {
|
|
1406
|
+
for (const record of validationRecords) {
|
|
1407
|
+
const result = await this.route53.changeResourceRecordSets({
|
|
1408
|
+
HostedZoneId: hostedZoneId,
|
|
1409
|
+
ChangeBatch: {
|
|
1410
|
+
Comment: `ACM DNS validation for ${record.domainName}`,
|
|
1411
|
+
Changes: [{
|
|
1412
|
+
Action: "UPSERT",
|
|
1413
|
+
ResourceRecordSet: {
|
|
1414
|
+
Name: record.recordName,
|
|
1415
|
+
Type: record.recordType,
|
|
1416
|
+
TTL: 300,
|
|
1417
|
+
ResourceRecords: [{ Value: record.recordValue }]
|
|
1418
|
+
}
|
|
1419
|
+
}]
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
results.push({
|
|
1423
|
+
...record,
|
|
1424
|
+
changeId: result.ChangeInfo?.Id
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return results;
|
|
1429
|
+
}
|
|
1430
|
+
async deleteValidationRecords(params) {
|
|
1431
|
+
const { certificateArn, hostedZoneId, domain } = params;
|
|
1432
|
+
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1433
|
+
if (this.dnsProvider) {
|
|
1434
|
+
const targetDomain = domain || validationRecords[0]?.domainName;
|
|
1435
|
+
for (const record of validationRecords) {
|
|
1436
|
+
try {
|
|
1437
|
+
await this.dnsProvider.deleteRecord(targetDomain, {
|
|
1438
|
+
name: record.recordName,
|
|
1439
|
+
type: record.recordType,
|
|
1440
|
+
content: record.recordValue
|
|
1441
|
+
});
|
|
1442
|
+
} catch {}
|
|
1443
|
+
}
|
|
1444
|
+
} else if (hostedZoneId) {
|
|
1445
|
+
for (const record of validationRecords) {
|
|
1446
|
+
try {
|
|
1447
|
+
await this.route53.changeResourceRecordSets({
|
|
1448
|
+
HostedZoneId: hostedZoneId,
|
|
1449
|
+
ChangeBatch: {
|
|
1450
|
+
Comment: `Cleanup ACM DNS validation for ${record.domainName}`,
|
|
1451
|
+
Changes: [{
|
|
1452
|
+
Action: "DELETE",
|
|
1453
|
+
ResourceRecordSet: {
|
|
1454
|
+
Name: record.recordName,
|
|
1455
|
+
Type: record.recordType,
|
|
1456
|
+
TTL: 300,
|
|
1457
|
+
ResourceRecords: [{ Value: record.recordValue }]
|
|
1458
|
+
}
|
|
1459
|
+
}]
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
} catch {}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
async findOrCreateCertificate(params) {
|
|
1467
|
+
const { domainName, hostedZoneId, subjectAlternativeNames, waitForValidation = true } = params;
|
|
1468
|
+
if (!this.dnsProvider && !hostedZoneId) {
|
|
1469
|
+
throw new Error("Either hostedZoneId or external DNS provider configuration is required");
|
|
1470
|
+
}
|
|
1471
|
+
const existing = await this.acm.findCertificateByDomain(domainName);
|
|
1472
|
+
if (existing && existing.Status === "ISSUED") {
|
|
1473
|
+
return {
|
|
1474
|
+
certificateArn: existing.CertificateArn,
|
|
1475
|
+
isNew: false
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
const { certificateArn } = await this.requestAndValidate({
|
|
1479
|
+
domainName,
|
|
1480
|
+
hostedZoneId,
|
|
1481
|
+
subjectAlternativeNames,
|
|
1482
|
+
waitForValidation
|
|
1483
|
+
});
|
|
1484
|
+
return {
|
|
1485
|
+
certificateArn,
|
|
1486
|
+
isNew: true
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
hasExternalDnsProvider() {
|
|
1490
|
+
return this.dnsProvider !== undefined;
|
|
1491
|
+
}
|
|
1492
|
+
getDnsProviderName() {
|
|
1493
|
+
return this.dnsProvider?.name || "route53";
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/dns/validator.ts
|
|
1498
|
+
class UnifiedDnsValidator {
|
|
1499
|
+
acm;
|
|
1500
|
+
dnsProvider;
|
|
1501
|
+
constructor(dnsProvider, acmRegion = "us-east-1") {
|
|
1502
|
+
this.acm = new ACMClient(acmRegion);
|
|
1503
|
+
if ("provider" in dnsProvider) {
|
|
1504
|
+
this.dnsProvider = createDnsProvider(dnsProvider);
|
|
1505
|
+
} else {
|
|
1506
|
+
this.dnsProvider = dnsProvider;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
getProvider() {
|
|
1510
|
+
return this.dnsProvider;
|
|
1511
|
+
}
|
|
1512
|
+
async requestAndValidate(params) {
|
|
1513
|
+
const {
|
|
1514
|
+
domainName,
|
|
1515
|
+
subjectAlternativeNames = [],
|
|
1516
|
+
waitForValidation = false,
|
|
1517
|
+
maxWaitMinutes = 30
|
|
1518
|
+
} = params;
|
|
1519
|
+
const { CertificateArn } = await this.acm.requestCertificate({
|
|
1520
|
+
DomainName: domainName,
|
|
1521
|
+
SubjectAlternativeNames: subjectAlternativeNames.length > 0 ? [domainName, ...subjectAlternativeNames] : undefined,
|
|
1522
|
+
ValidationMethod: "DNS"
|
|
1523
|
+
});
|
|
1524
|
+
await this.waitForValidationOptions(CertificateArn);
|
|
1525
|
+
const validationRecords = await this.acm.getDnsValidationRecords(CertificateArn);
|
|
1526
|
+
for (const record of validationRecords) {
|
|
1527
|
+
const result = await this.dnsProvider.upsertRecord(domainName, {
|
|
1528
|
+
name: record.recordName,
|
|
1529
|
+
type: record.recordType,
|
|
1530
|
+
content: record.recordValue,
|
|
1531
|
+
ttl: 300
|
|
1532
|
+
});
|
|
1533
|
+
if (!result.success) {
|
|
1534
|
+
console.warn(`Failed to create validation record for ${record.domainName}: ${result.message}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
let status = "pending";
|
|
1538
|
+
if (waitForValidation) {
|
|
1539
|
+
const cert = await this.acm.waitForCertificateValidation(CertificateArn, maxWaitMinutes * 2, 30000);
|
|
1540
|
+
status = cert?.Status === "ISSUED" ? "issued" : "failed";
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
certificateArn: CertificateArn,
|
|
1544
|
+
validationRecords: validationRecords.map((r) => ({
|
|
1545
|
+
domainName: r.domainName,
|
|
1546
|
+
recordName: r.recordName,
|
|
1547
|
+
recordType: r.recordType,
|
|
1548
|
+
recordValue: r.recordValue
|
|
1549
|
+
})),
|
|
1550
|
+
isNew: true,
|
|
1551
|
+
status
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
async createValidationRecords(params) {
|
|
1555
|
+
const { certificateArn, domain } = params;
|
|
1556
|
+
const errors = [];
|
|
1557
|
+
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1558
|
+
for (const record of validationRecords) {
|
|
1559
|
+
const result = await this.dnsProvider.upsertRecord(domain, {
|
|
1560
|
+
name: record.recordName,
|
|
1561
|
+
type: record.recordType,
|
|
1562
|
+
content: record.recordValue,
|
|
1563
|
+
ttl: 300
|
|
1564
|
+
});
|
|
1565
|
+
if (!result.success) {
|
|
1566
|
+
errors.push(`Failed to create record for ${record.domainName}: ${result.message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return {
|
|
1570
|
+
success: errors.length === 0,
|
|
1571
|
+
records: validationRecords.map((r) => ({
|
|
1572
|
+
domainName: r.domainName,
|
|
1573
|
+
recordName: r.recordName,
|
|
1574
|
+
recordType: r.recordType,
|
|
1575
|
+
recordValue: r.recordValue
|
|
1576
|
+
})),
|
|
1577
|
+
errors
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
async deleteValidationRecords(params) {
|
|
1581
|
+
const { certificateArn, domain } = params;
|
|
1582
|
+
const errors = [];
|
|
1583
|
+
const validationRecords = await this.acm.getDnsValidationRecords(certificateArn);
|
|
1584
|
+
for (const record of validationRecords) {
|
|
1585
|
+
const result = await this.dnsProvider.deleteRecord(domain, {
|
|
1586
|
+
name: record.recordName,
|
|
1587
|
+
type: record.recordType,
|
|
1588
|
+
content: record.recordValue
|
|
1589
|
+
});
|
|
1590
|
+
if (!result.success) {
|
|
1591
|
+
errors.push(`Failed to delete record for ${record.domainName}: ${result.message}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
success: errors.length === 0,
|
|
1596
|
+
errors
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async findOrCreateCertificate(params) {
|
|
1600
|
+
const { domainName, subjectAlternativeNames = [], waitForValidation = true, maxWaitMinutes } = params;
|
|
1601
|
+
const existing = await this.acm.findCertificateByDomain(domainName);
|
|
1602
|
+
if (existing && existing.Status === "ISSUED") {
|
|
1603
|
+
const existingSans = existing.SubjectAlternativeNames || [existing.DomainName];
|
|
1604
|
+
const hasWildcard = existingSans.some((san) => san === `*.${domainName}`);
|
|
1605
|
+
const allSansCovered = subjectAlternativeNames.every((san) => existingSans.includes(san) || hasWildcard);
|
|
1606
|
+
if (allSansCovered) {
|
|
1607
|
+
return {
|
|
1608
|
+
certificateArn: existing.CertificateArn,
|
|
1609
|
+
validationRecords: [],
|
|
1610
|
+
isNew: false,
|
|
1611
|
+
status: "issued"
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
console.log(`Existing certificate doesn't cover all required SANs, requesting new certificate...`);
|
|
1615
|
+
}
|
|
1616
|
+
return this.requestAndValidate({
|
|
1617
|
+
domainName,
|
|
1618
|
+
subjectAlternativeNames,
|
|
1619
|
+
waitForValidation,
|
|
1620
|
+
maxWaitMinutes
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
async requestCertificateWithCommonSans(params) {
|
|
1624
|
+
const {
|
|
1625
|
+
domainName,
|
|
1626
|
+
includeWww = true,
|
|
1627
|
+
includeWildcard = false,
|
|
1628
|
+
additionalSans = [],
|
|
1629
|
+
waitForValidation = false
|
|
1630
|
+
} = params;
|
|
1631
|
+
const sans = [];
|
|
1632
|
+
if (includeWww) {
|
|
1633
|
+
sans.push(`www.${domainName}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (includeWildcard) {
|
|
1636
|
+
sans.push(`*.${domainName}`);
|
|
1637
|
+
}
|
|
1638
|
+
sans.push(...additionalSans);
|
|
1639
|
+
return this.requestAndValidate({
|
|
1640
|
+
domainName,
|
|
1641
|
+
subjectAlternativeNames: sans,
|
|
1642
|
+
waitForValidation
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
async waitForValidationOptions(certificateArn, maxAttempts = 30) {
|
|
1646
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
1647
|
+
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1648
|
+
if (cert.DomainValidationOptions && cert.DomainValidationOptions.length > 0 && cert.DomainValidationOptions[0].ResourceRecord) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1652
|
+
}
|
|
1653
|
+
throw new Error("Timeout waiting for DNS validation options");
|
|
1654
|
+
}
|
|
1655
|
+
async getCertificateStatus(certificateArn) {
|
|
1656
|
+
const cert = await this.acm.describeCertificate({ CertificateArn: certificateArn });
|
|
1657
|
+
return {
|
|
1658
|
+
status: cert.Status,
|
|
1659
|
+
domainValidations: (cert.DomainValidationOptions || []).map((opt) => ({
|
|
1660
|
+
domain: opt.DomainName,
|
|
1661
|
+
status: opt.ValidationStatus || "UNKNOWN"
|
|
1662
|
+
}))
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function createPorkbunValidator(apiKey, secretKey, acmRegion) {
|
|
1667
|
+
return new UnifiedDnsValidator({ provider: "porkbun", apiKey, secretKey }, acmRegion);
|
|
1668
|
+
}
|
|
1669
|
+
function createGoDaddyValidator(apiKey, apiSecret, acmRegion, environment) {
|
|
1670
|
+
return new UnifiedDnsValidator({ provider: "godaddy", apiKey, apiSecret, environment }, acmRegion);
|
|
1671
|
+
}
|
|
1672
|
+
function createRoute53Validator(region, hostedZoneId, acmRegion) {
|
|
1673
|
+
return new UnifiedDnsValidator({ provider: "route53", region, hostedZoneId }, acmRegion || region);
|
|
1674
|
+
}
|
|
1675
|
+
// src/dns/index.ts
|
|
1676
|
+
function createDnsProvider(config) {
|
|
1677
|
+
switch (config.provider) {
|
|
1678
|
+
case "route53":
|
|
1679
|
+
return new Route53Provider(config.region, config.hostedZoneId);
|
|
1680
|
+
case "porkbun":
|
|
1681
|
+
return new PorkbunProvider(config.apiKey, config.secretKey);
|
|
1682
|
+
case "godaddy":
|
|
1683
|
+
return new GoDaddyProvider(config.apiKey, config.apiSecret, config.environment);
|
|
1684
|
+
case "cloudflare":
|
|
1685
|
+
return new CloudflareProvider(config.apiToken);
|
|
1686
|
+
default:
|
|
1687
|
+
throw new Error(`Unknown DNS provider: ${config.provider}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
async function detectDnsProvider(domain, configs) {
|
|
1691
|
+
for (const config of configs) {
|
|
1692
|
+
const provider = createDnsProvider(config);
|
|
1693
|
+
if (await provider.canManageDomain(domain)) {
|
|
1694
|
+
return provider;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
class DnsProviderFactory {
|
|
1701
|
+
providers = new Map;
|
|
1702
|
+
configs = [];
|
|
1703
|
+
addConfig(config) {
|
|
1704
|
+
this.configs.push(config);
|
|
1705
|
+
return this;
|
|
1706
|
+
}
|
|
1707
|
+
addRoute53(region, hostedZoneId) {
|
|
1708
|
+
this.configs.push({
|
|
1709
|
+
provider: "route53",
|
|
1710
|
+
region,
|
|
1711
|
+
hostedZoneId
|
|
1712
|
+
});
|
|
1713
|
+
return this;
|
|
1714
|
+
}
|
|
1715
|
+
addPorkbun(apiKey, secretKey) {
|
|
1716
|
+
this.configs.push({
|
|
1717
|
+
provider: "porkbun",
|
|
1718
|
+
apiKey,
|
|
1719
|
+
secretKey
|
|
1720
|
+
});
|
|
1721
|
+
return this;
|
|
1722
|
+
}
|
|
1723
|
+
addGoDaddy(apiKey, apiSecret, environment) {
|
|
1724
|
+
this.configs.push({
|
|
1725
|
+
provider: "godaddy",
|
|
1726
|
+
apiKey,
|
|
1727
|
+
apiSecret,
|
|
1728
|
+
environment
|
|
1729
|
+
});
|
|
1730
|
+
return this;
|
|
1731
|
+
}
|
|
1732
|
+
addCloudflare(apiToken) {
|
|
1733
|
+
this.configs.push({
|
|
1734
|
+
provider: "cloudflare",
|
|
1735
|
+
apiToken
|
|
1736
|
+
});
|
|
1737
|
+
return this;
|
|
1738
|
+
}
|
|
1739
|
+
loadFromEnv() {
|
|
1740
|
+
if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_REGION) {
|
|
1741
|
+
this.addRoute53(process.env.AWS_REGION);
|
|
1742
|
+
}
|
|
1743
|
+
const porkbunApiKey = process.env.PORKBUN_API_KEY;
|
|
1744
|
+
const porkbunSecretKey = process.env.PORKBUN_SECRET_KEY;
|
|
1745
|
+
if (porkbunApiKey && porkbunSecretKey) {
|
|
1746
|
+
this.addPorkbun(porkbunApiKey, porkbunSecretKey);
|
|
1747
|
+
}
|
|
1748
|
+
const godaddyApiKey = process.env.GODADDY_API_KEY;
|
|
1749
|
+
const godaddyApiSecret = process.env.GODADDY_API_SECRET;
|
|
1750
|
+
if (godaddyApiKey && godaddyApiSecret) {
|
|
1751
|
+
const env = process.env.GODADDY_ENVIRONMENT;
|
|
1752
|
+
this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env);
|
|
1753
|
+
}
|
|
1754
|
+
const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
1755
|
+
if (cloudflareApiToken) {
|
|
1756
|
+
this.addCloudflare(cloudflareApiToken);
|
|
1757
|
+
}
|
|
1758
|
+
return this;
|
|
1759
|
+
}
|
|
1760
|
+
getProvider(name) {
|
|
1761
|
+
const cached = this.providers.get(name);
|
|
1762
|
+
if (cached) {
|
|
1763
|
+
return cached;
|
|
1764
|
+
}
|
|
1765
|
+
const config = this.configs.find((c) => c.provider === name);
|
|
1766
|
+
if (!config) {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
const provider = createDnsProvider(config);
|
|
1770
|
+
this.providers.set(name, provider);
|
|
1771
|
+
return provider;
|
|
1772
|
+
}
|
|
1773
|
+
async getProviderForDomain(domain) {
|
|
1774
|
+
for (const config of this.configs) {
|
|
1775
|
+
const provider = createDnsProvider(config);
|
|
1776
|
+
if (await provider.canManageDomain(domain)) {
|
|
1777
|
+
return provider;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
getAllProviders() {
|
|
1783
|
+
return this.configs.map((config) => createDnsProvider(config));
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
var dnsProviders = new DnsProviderFactory;
|
|
1787
|
+
|
|
1788
|
+
export { PorkbunProvider, GoDaddyProvider, CloudflareProvider, Route53Provider, UnifiedDnsValidator, createPorkbunValidator, createGoDaddyValidator, createRoute53Validator, createDnsProvider, detectDnsProvider, DnsProviderFactory, dnsProviders, ACMClient, ACMDnsValidator };
|