groundmail 0.0.0-stage → 0.14.1
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/README.md +48 -2
- package/dist/authjs.d.ts +72 -0
- package/dist/authjs.js +101 -0
- package/dist/better-auth.d.ts +122 -0
- package/dist/better-auth.js +148 -0
- package/dist/chunk-IZYE7HGT.js +1367 -0
- package/dist/clerk.d.ts +64 -0
- package/dist/clerk.js +51 -0
- package/dist/client-DdrSVDkA.d.ts +950 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +20 -0
- package/dist/nodemailer.d.ts +1 -0
- package/dist/nodemailer.js +17 -0
- package/dist/payload.d.ts +87 -0
- package/dist/payload.js +114 -0
- package/package.json +99 -4
|
@@ -0,0 +1,1367 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var GroundmailError = class _GroundmailError extends Error {
|
|
3
|
+
constructor(message, status, details, options) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "GroundmailError";
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.code = options?.code;
|
|
9
|
+
this.upgradeUrl = options?.upgradeUrl;
|
|
10
|
+
}
|
|
11
|
+
static fromResponse(status, body) {
|
|
12
|
+
const err = body.error;
|
|
13
|
+
const message = typeof err === "string" ? err : err?.message || `Request failed with status ${status}`;
|
|
14
|
+
const details = typeof err === "object" && err ? err.details : void 0;
|
|
15
|
+
const code = typeof err === "object" && err && "code" in err ? String(err.code) : void 0;
|
|
16
|
+
const upgradeUrl = typeof err === "object" && err && "upgrade_url" in err ? String(err.upgrade_url) : void 0;
|
|
17
|
+
return new _GroundmailError(message, status, details, { code, upgradeUrl });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/dns/spf.ts
|
|
22
|
+
function mergeSpfInclude(existing, includeHost) {
|
|
23
|
+
const include = `include:${includeHost.replace(/^include:/i, "")}`;
|
|
24
|
+
const trimmed = existing?.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return `v=spf1 ${include} ~all`;
|
|
27
|
+
}
|
|
28
|
+
if (trimmed.toLowerCase().includes(include.toLowerCase())) {
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
if (!/^v=spf1\b/i.test(trimmed)) {
|
|
32
|
+
return `v=spf1 ${include} ~all`;
|
|
33
|
+
}
|
|
34
|
+
if (/\s[~?+\-]all\s*$/i.test(trimmed)) {
|
|
35
|
+
return trimmed.replace(/\s([~?+\-]all)\s*$/i, ` ${include} $1`);
|
|
36
|
+
}
|
|
37
|
+
return `${trimmed} ${include} ~all`;
|
|
38
|
+
}
|
|
39
|
+
function isSpfRecord(value) {
|
|
40
|
+
return value.trim().toLowerCase().startsWith("v=spf1");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/dns/cloudflare.ts
|
|
44
|
+
function relativeName(fqdn, zone) {
|
|
45
|
+
const domain = zone.replace(/\.$/, "").toLowerCase();
|
|
46
|
+
const host = fqdn.replace(/\.$/, "").toLowerCase();
|
|
47
|
+
if (host === domain) return "@";
|
|
48
|
+
if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
|
|
49
|
+
return host;
|
|
50
|
+
}
|
|
51
|
+
function zoneNameCandidates(domain) {
|
|
52
|
+
const labels = domain.replace(/\.$/, "").toLowerCase().split(".").filter(Boolean);
|
|
53
|
+
const candidates = [];
|
|
54
|
+
for (let i = 0; i <= labels.length - 2; i++) {
|
|
55
|
+
candidates.push(labels.slice(i).join("."));
|
|
56
|
+
}
|
|
57
|
+
return candidates;
|
|
58
|
+
}
|
|
59
|
+
function createCloudflareProvider(options) {
|
|
60
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
61
|
+
const headers = {
|
|
62
|
+
Authorization: `Bearer ${options.apiToken}`,
|
|
63
|
+
"Content-Type": "application/json"
|
|
64
|
+
};
|
|
65
|
+
async function cf(path, init) {
|
|
66
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
|
|
67
|
+
...init,
|
|
68
|
+
headers: { ...headers, ...init?.headers || {} }
|
|
69
|
+
});
|
|
70
|
+
const body = await response.json().catch(() => ({}));
|
|
71
|
+
if (!response.ok || body.success === false) {
|
|
72
|
+
const message = body.errors?.map((e) => e.message).filter(Boolean).join("; ") || `Cloudflare HTTP ${response.status}`;
|
|
73
|
+
throw new Error(message);
|
|
74
|
+
}
|
|
75
|
+
return body.result;
|
|
76
|
+
}
|
|
77
|
+
async function resolveZoneId(domain) {
|
|
78
|
+
if (options.zoneId) return options.zoneId;
|
|
79
|
+
for (const candidate of zoneNameCandidates(domain)) {
|
|
80
|
+
const zones = await cf(
|
|
81
|
+
`/zones?name=${encodeURIComponent(candidate)}&status=active`
|
|
82
|
+
);
|
|
83
|
+
const zone = zones?.[0];
|
|
84
|
+
if (zone) return zone.id;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`No active Cloudflare zone found for ${domain}`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
name: "cloudflare",
|
|
90
|
+
async apply(domain, records) {
|
|
91
|
+
const zoneId = await resolveZoneId(domain);
|
|
92
|
+
const zoneName = domain.toLowerCase();
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
const recordType = record.type.toUpperCase();
|
|
96
|
+
const name = relativeName(record.host, zoneName);
|
|
97
|
+
const absoluteName = name === "@" ? zoneName : `${name}.${zoneName}`;
|
|
98
|
+
if (recordType === "CNAME") {
|
|
99
|
+
const target = record.value.replace(/\.$/, "").toLowerCase();
|
|
100
|
+
const existingList2 = await cf(
|
|
101
|
+
`/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(absoluteName)}&per_page=100`
|
|
102
|
+
);
|
|
103
|
+
const existing = (existingList2 || [])[0];
|
|
104
|
+
if (existing && existing.content.replace(/\.$/, "").toLowerCase() === target) {
|
|
105
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (existing) {
|
|
109
|
+
await cf(`/zones/${zoneId}/dns_records/${existing.id}`, {
|
|
110
|
+
method: "PUT",
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
type: "CNAME",
|
|
113
|
+
name: absoluteName,
|
|
114
|
+
content: target,
|
|
115
|
+
ttl: 1,
|
|
116
|
+
proxied: false
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
results.push({ purpose: record.purpose, host: record.host, action: "updated" });
|
|
120
|
+
} else {
|
|
121
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
type: "CNAME",
|
|
125
|
+
name: absoluteName,
|
|
126
|
+
content: target,
|
|
127
|
+
ttl: 1,
|
|
128
|
+
proxied: false
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
results.push({ purpose: record.purpose, host: record.host, action: "created" });
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (recordType === "MX") {
|
|
136
|
+
const priority = record.priority ?? 10;
|
|
137
|
+
const target = record.value.replace(/\.$/, "").toLowerCase();
|
|
138
|
+
const existingList2 = await cf(
|
|
139
|
+
`/zones/${zoneId}/dns_records?type=MX&name=${encodeURIComponent(absoluteName)}&per_page=100`
|
|
140
|
+
);
|
|
141
|
+
const existing = (existingList2 || []).find(
|
|
142
|
+
(row) => row.content.replace(/\.$/, "").toLowerCase() === target && (row.priority ?? 10) === priority
|
|
143
|
+
);
|
|
144
|
+
if (existing) {
|
|
145
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const list = existingList2 || [];
|
|
149
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
type: "MX",
|
|
153
|
+
name: absoluteName,
|
|
154
|
+
content: target,
|
|
155
|
+
priority,
|
|
156
|
+
ttl: 1
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
results.push({
|
|
160
|
+
purpose: record.purpose,
|
|
161
|
+
host: record.host,
|
|
162
|
+
action: list.length > 0 ? "merged" : "created"
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (recordType !== "TXT") {
|
|
167
|
+
results.push({
|
|
168
|
+
purpose: record.purpose,
|
|
169
|
+
host: record.host,
|
|
170
|
+
action: "unchanged",
|
|
171
|
+
detail: `Skipped unsupported type ${record.type}`
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (record.purpose === "SPF") {
|
|
176
|
+
const allTxt = await cf(
|
|
177
|
+
`/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(zoneName)}&per_page=100`
|
|
178
|
+
);
|
|
179
|
+
const spfRecords = (allTxt || []).filter((r) => isSpfRecord(r.content));
|
|
180
|
+
const existing = spfRecords[0];
|
|
181
|
+
const includeHost = record.value.match(/include:([^\s]+)/i)?.[1] || "spf.groundmail.dev";
|
|
182
|
+
const merged = mergeSpfInclude(existing?.content, includeHost);
|
|
183
|
+
if (existing) {
|
|
184
|
+
if (existing.content.trim() === merged.trim()) {
|
|
185
|
+
results.push({
|
|
186
|
+
purpose: record.purpose,
|
|
187
|
+
host: record.host,
|
|
188
|
+
action: "unchanged",
|
|
189
|
+
detail: "SPF already includes Groundmail"
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
await cf(`/zones/${zoneId}/dns_records/${existing.id}`, {
|
|
193
|
+
method: "PUT",
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
type: "TXT",
|
|
196
|
+
name: zoneName,
|
|
197
|
+
content: merged,
|
|
198
|
+
ttl: 1
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
results.push({
|
|
202
|
+
purpose: record.purpose,
|
|
203
|
+
host: record.host,
|
|
204
|
+
action: "merged",
|
|
205
|
+
detail: merged
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
type: "TXT",
|
|
213
|
+
name: zoneName,
|
|
214
|
+
content: merged,
|
|
215
|
+
ttl: 1
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
results.push({
|
|
219
|
+
purpose: record.purpose,
|
|
220
|
+
host: record.host,
|
|
221
|
+
action: "created",
|
|
222
|
+
detail: merged
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const existingList = await cf(
|
|
228
|
+
`/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(absoluteName)}&per_page=100`
|
|
229
|
+
);
|
|
230
|
+
const match = (existingList || []).find((r) => r.content === record.value);
|
|
231
|
+
if (match) {
|
|
232
|
+
results.push({
|
|
233
|
+
purpose: record.purpose,
|
|
234
|
+
host: record.host,
|
|
235
|
+
action: "unchanged"
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const replaceable = (existingList || []).find((r) => {
|
|
240
|
+
if (record.purpose === "Domain ownership") return r.content.includes("groundmail-verify=");
|
|
241
|
+
if (record.purpose === "DKIM") return r.content.toLowerCase().includes("v=dkim1");
|
|
242
|
+
if (record.purpose === "DMARC") return r.content.toLowerCase().includes("v=dmarc1");
|
|
243
|
+
return false;
|
|
244
|
+
});
|
|
245
|
+
if (replaceable) {
|
|
246
|
+
await cf(`/zones/${zoneId}/dns_records/${replaceable.id}`, {
|
|
247
|
+
method: "PUT",
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
type: "TXT",
|
|
250
|
+
name: absoluteName,
|
|
251
|
+
content: record.value,
|
|
252
|
+
ttl: 1
|
|
253
|
+
})
|
|
254
|
+
});
|
|
255
|
+
results.push({ purpose: record.purpose, host: record.host, action: "updated" });
|
|
256
|
+
} else {
|
|
257
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
type: "TXT",
|
|
261
|
+
name: absoluteName,
|
|
262
|
+
content: record.value,
|
|
263
|
+
ttl: 1
|
|
264
|
+
})
|
|
265
|
+
});
|
|
266
|
+
results.push({ purpose: record.purpose, host: record.host, action: "created" });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return results;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/dns/godaddy.ts
|
|
275
|
+
function relativeName2(fqdn, zone) {
|
|
276
|
+
const domain = zone.replace(/\.$/, "").toLowerCase();
|
|
277
|
+
const host = fqdn.replace(/\.$/, "").toLowerCase();
|
|
278
|
+
if (host === domain) return "@";
|
|
279
|
+
if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
|
|
280
|
+
return host;
|
|
281
|
+
}
|
|
282
|
+
function serializeGoDaddyMxRecords(name, rows) {
|
|
283
|
+
return rows.map((row) => ({
|
|
284
|
+
type: "MX",
|
|
285
|
+
name: row.name || name,
|
|
286
|
+
data: row.data,
|
|
287
|
+
priority: row.priority ?? 10,
|
|
288
|
+
ttl: Math.max(row.ttl || 600, 600)
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
function createGoDaddyProvider(options) {
|
|
292
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
293
|
+
const headers = {
|
|
294
|
+
Authorization: `sso-key ${options.apiKey}:${options.apiSecret}`,
|
|
295
|
+
"Content-Type": "application/json",
|
|
296
|
+
Accept: "application/json"
|
|
297
|
+
};
|
|
298
|
+
async function gd(path, init) {
|
|
299
|
+
const response = await fetchImpl(`https://api.godaddy.com${path}`, {
|
|
300
|
+
...init,
|
|
301
|
+
headers: { ...headers, ...init?.headers || {} }
|
|
302
|
+
});
|
|
303
|
+
if (response.status === 204) return void 0;
|
|
304
|
+
const body = await response.json().catch(() => ({}));
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const message = body.message || (Array.isArray(body.errors) ? body.errors.map((e) => e.message).join("; ") : null) || `GoDaddy HTTP ${response.status}`;
|
|
307
|
+
throw new Error(message);
|
|
308
|
+
}
|
|
309
|
+
return body;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
name: "godaddy",
|
|
313
|
+
async apply(domain, records) {
|
|
314
|
+
const zone = domain.toLowerCase();
|
|
315
|
+
const results = [];
|
|
316
|
+
for (const record of records) {
|
|
317
|
+
const recordType = record.type.toUpperCase();
|
|
318
|
+
const name = relativeName2(record.host, zone);
|
|
319
|
+
if (recordType === "CNAME") {
|
|
320
|
+
const target = record.value.replace(/\.$/, "");
|
|
321
|
+
const existing2 = await gd(
|
|
322
|
+
`/v1/domains/${zone}/records/CNAME/${encodeURIComponent(name)}`
|
|
323
|
+
);
|
|
324
|
+
const list2 = existing2 || [];
|
|
325
|
+
const current = list2[0];
|
|
326
|
+
if (current && current.data.replace(/\.$/, "").toLowerCase() === target.toLowerCase()) {
|
|
327
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
await gd(`/v1/domains/${zone}/records/CNAME/${encodeURIComponent(name)}`, {
|
|
331
|
+
method: "PUT",
|
|
332
|
+
body: JSON.stringify([{ type: "CNAME", name, data: target, ttl: 600 }])
|
|
333
|
+
});
|
|
334
|
+
results.push({
|
|
335
|
+
purpose: record.purpose,
|
|
336
|
+
host: record.host,
|
|
337
|
+
action: current ? "updated" : "created"
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (recordType === "MX") {
|
|
342
|
+
const priority = record.priority ?? 10;
|
|
343
|
+
const target = record.value.replace(/\.$/, "");
|
|
344
|
+
const existing2 = await gd(
|
|
345
|
+
`/v1/domains/${zone}/records/MX/${encodeURIComponent(name)}`
|
|
346
|
+
);
|
|
347
|
+
const list2 = existing2 || [];
|
|
348
|
+
const current = list2.find(
|
|
349
|
+
(row) => row.data.replace(/\.$/, "").toLowerCase() === target.toLowerCase() && (row.priority ?? 10) === priority
|
|
350
|
+
);
|
|
351
|
+
if (current) {
|
|
352
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const next2 = [
|
|
356
|
+
...list2,
|
|
357
|
+
{ type: "MX", name, data: target, priority, ttl: 600 }
|
|
358
|
+
];
|
|
359
|
+
await gd(`/v1/domains/${zone}/records/MX/${encodeURIComponent(name)}`, {
|
|
360
|
+
method: "PUT",
|
|
361
|
+
body: JSON.stringify(serializeGoDaddyMxRecords(name, next2))
|
|
362
|
+
});
|
|
363
|
+
results.push({
|
|
364
|
+
purpose: record.purpose,
|
|
365
|
+
host: record.host,
|
|
366
|
+
action: list2.length > 0 ? "merged" : "created"
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (recordType !== "TXT") {
|
|
371
|
+
results.push({
|
|
372
|
+
purpose: record.purpose,
|
|
373
|
+
host: record.host,
|
|
374
|
+
action: "unchanged",
|
|
375
|
+
detail: `Skipped unsupported type ${record.type}`
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (record.purpose === "SPF") {
|
|
380
|
+
const existing2 = await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`);
|
|
381
|
+
const list2 = existing2 || [];
|
|
382
|
+
const spfIndex = list2.findIndex((r) => isSpfRecord(r.data));
|
|
383
|
+
const includeHost = record.value.match(/include:([^\s]+)/i)?.[1] || "spf.groundmail.dev";
|
|
384
|
+
const currentSpf = spfIndex >= 0 ? list2[spfIndex].data : null;
|
|
385
|
+
const merged = mergeSpfInclude(currentSpf, includeHost);
|
|
386
|
+
if (spfIndex >= 0 && list2[spfIndex].data.trim() === merged.trim()) {
|
|
387
|
+
results.push({
|
|
388
|
+
purpose: record.purpose,
|
|
389
|
+
host: record.host,
|
|
390
|
+
action: "unchanged",
|
|
391
|
+
detail: "SPF already includes Groundmail"
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const next2 = list2.map(
|
|
396
|
+
(r, i) => i === spfIndex ? { type: "TXT", name, data: merged, ttl: Math.max(r.ttl || 600, 600) } : r
|
|
397
|
+
);
|
|
398
|
+
if (spfIndex < 0) {
|
|
399
|
+
next2.push({ type: "TXT", name, data: merged, ttl: 600 });
|
|
400
|
+
}
|
|
401
|
+
await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`, {
|
|
402
|
+
method: "PUT",
|
|
403
|
+
body: JSON.stringify(next2.map(({ type, name: n, data, ttl }) => ({ type, name: n, data, ttl })))
|
|
404
|
+
});
|
|
405
|
+
results.push({
|
|
406
|
+
purpose: record.purpose,
|
|
407
|
+
host: record.host,
|
|
408
|
+
action: spfIndex >= 0 ? "merged" : "created",
|
|
409
|
+
detail: merged
|
|
410
|
+
});
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const existing = await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`);
|
|
414
|
+
const list = existing || [];
|
|
415
|
+
const exact = list.find((r) => r.data === record.value);
|
|
416
|
+
if (exact) {
|
|
417
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const replaceableIndex = list.findIndex((r) => {
|
|
421
|
+
if (record.purpose === "Domain ownership") return r.data.includes("groundmail-verify=");
|
|
422
|
+
if (record.purpose === "DKIM") return r.data.toLowerCase().includes("v=dkim1");
|
|
423
|
+
if (record.purpose === "DMARC") return r.data.toLowerCase().includes("v=dmarc1");
|
|
424
|
+
return false;
|
|
425
|
+
});
|
|
426
|
+
const next = replaceableIndex >= 0 ? list.map(
|
|
427
|
+
(r, i) => i === replaceableIndex ? { type: "TXT", name, data: record.value, ttl: Math.max(r.ttl || 600, 600) } : r
|
|
428
|
+
) : [...list, { type: "TXT", name, data: record.value, ttl: 600 }];
|
|
429
|
+
await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`, {
|
|
430
|
+
method: "PUT",
|
|
431
|
+
body: JSON.stringify(next.map(({ type, name: n, data, ttl }) => ({ type, name: n, data, ttl })))
|
|
432
|
+
});
|
|
433
|
+
results.push({
|
|
434
|
+
purpose: record.purpose,
|
|
435
|
+
host: record.host,
|
|
436
|
+
action: replaceableIndex >= 0 ? "updated" : "created"
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return results;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/dns/vercel.ts
|
|
445
|
+
function zoneNameCandidates2(domain) {
|
|
446
|
+
const labels = domain.replace(/\.$/, "").toLowerCase().split(".").filter(Boolean);
|
|
447
|
+
const candidates = [];
|
|
448
|
+
for (let i = 0; i <= labels.length - 2; i++) {
|
|
449
|
+
candidates.push(labels.slice(i).join("."));
|
|
450
|
+
}
|
|
451
|
+
return candidates;
|
|
452
|
+
}
|
|
453
|
+
function relativeName3(fqdn, zone) {
|
|
454
|
+
const domain = zone.replace(/\.$/, "").toLowerCase();
|
|
455
|
+
const host = fqdn.replace(/\.$/, "").toLowerCase();
|
|
456
|
+
if (host === domain) return "";
|
|
457
|
+
if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
|
|
458
|
+
return host;
|
|
459
|
+
}
|
|
460
|
+
function teamQuery(teamId) {
|
|
461
|
+
return teamId?.trim() ? `?teamId=${encodeURIComponent(teamId.trim())}` : "";
|
|
462
|
+
}
|
|
463
|
+
function createVercelProvider(options) {
|
|
464
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
465
|
+
const headers = {
|
|
466
|
+
Authorization: `Bearer ${options.apiToken}`,
|
|
467
|
+
"Content-Type": "application/json"
|
|
468
|
+
};
|
|
469
|
+
const teamQs = teamQuery(options.teamId);
|
|
470
|
+
async function vercel(path, init) {
|
|
471
|
+
const url = path.includes("?") ? `https://api.vercel.com${path}${options.teamId?.trim() ? `&teamId=${encodeURIComponent(options.teamId.trim())}` : ""}` : `https://api.vercel.com${path}${teamQs}`;
|
|
472
|
+
const response = await fetchImpl(url, {
|
|
473
|
+
...init,
|
|
474
|
+
headers: { ...headers, ...init?.headers || {} }
|
|
475
|
+
});
|
|
476
|
+
const body = await response.json().catch(() => ({}));
|
|
477
|
+
if (!response.ok) {
|
|
478
|
+
const message = body.error?.message || body.message || `Vercel HTTP ${response.status}`;
|
|
479
|
+
throw new Error(message);
|
|
480
|
+
}
|
|
481
|
+
return body;
|
|
482
|
+
}
|
|
483
|
+
async function listRecords(domain) {
|
|
484
|
+
const all = [];
|
|
485
|
+
let until;
|
|
486
|
+
for (let page = 0; page < 20; page++) {
|
|
487
|
+
const qs = new URLSearchParams({ limit: "100" });
|
|
488
|
+
if (until != null) qs.set("until", String(until));
|
|
489
|
+
if (options.teamId?.trim()) qs.set("teamId", options.teamId.trim());
|
|
490
|
+
const body = await vercel(
|
|
491
|
+
`/v5/domains/${encodeURIComponent(domain)}/records?${qs.toString()}`
|
|
492
|
+
);
|
|
493
|
+
const records = body.records || [];
|
|
494
|
+
all.push(...records);
|
|
495
|
+
const next = body.pagination?.next;
|
|
496
|
+
if (next == null || records.length === 0) break;
|
|
497
|
+
until = next;
|
|
498
|
+
}
|
|
499
|
+
return all;
|
|
500
|
+
}
|
|
501
|
+
async function resolveDomain(domain) {
|
|
502
|
+
const errors = [];
|
|
503
|
+
for (const candidate of zoneNameCandidates2(domain)) {
|
|
504
|
+
try {
|
|
505
|
+
await listRecords(candidate);
|
|
506
|
+
return candidate;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
509
|
+
errors.push(`${candidate}: ${message}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
throw new Error(
|
|
513
|
+
`No Vercel DNS domain found for ${domain}. The domain must use Vercel nameservers. (${errors.join("; ")})`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
name: "vercel",
|
|
518
|
+
async apply(domain, records) {
|
|
519
|
+
const zone = await resolveDomain(domain);
|
|
520
|
+
const results = [];
|
|
521
|
+
let cached = await listRecords(zone);
|
|
522
|
+
const refresh = async () => {
|
|
523
|
+
cached = await listRecords(zone);
|
|
524
|
+
};
|
|
525
|
+
for (const record of records) {
|
|
526
|
+
const recordType = record.type.toUpperCase();
|
|
527
|
+
const name = relativeName3(record.host, zone);
|
|
528
|
+
if (recordType === "CNAME") {
|
|
529
|
+
const target = record.value.replace(/\.$/, "").toLowerCase();
|
|
530
|
+
const existing = cached.find(
|
|
531
|
+
(row) => row.type === "CNAME" && row.name === name
|
|
532
|
+
);
|
|
533
|
+
if (existing && existing.value.replace(/\.$/, "").toLowerCase() === target) {
|
|
534
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (existing) {
|
|
538
|
+
await vercel(`/v1/domains/records/${existing.id}`, {
|
|
539
|
+
method: "PATCH",
|
|
540
|
+
body: JSON.stringify({ type: "CNAME", name, value: target, ttl: 60 })
|
|
541
|
+
});
|
|
542
|
+
results.push({ purpose: record.purpose, host: record.host, action: "updated" });
|
|
543
|
+
} else {
|
|
544
|
+
await vercel(`/v2/domains/${encodeURIComponent(zone)}/records`, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
body: JSON.stringify({ type: "CNAME", name, value: target, ttl: 60 })
|
|
547
|
+
});
|
|
548
|
+
results.push({ purpose: record.purpose, host: record.host, action: "created" });
|
|
549
|
+
}
|
|
550
|
+
await refresh();
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (recordType === "MX") {
|
|
554
|
+
const priority = record.priority ?? 10;
|
|
555
|
+
const target = record.value.replace(/\.$/, "").toLowerCase();
|
|
556
|
+
const existing = cached.find(
|
|
557
|
+
(row) => row.type === "MX" && row.name === name && row.value.replace(/\.$/, "").toLowerCase() === target && (row.mxPriority ?? 10) === priority
|
|
558
|
+
);
|
|
559
|
+
if (existing) {
|
|
560
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const sameName = cached.filter((row) => row.type === "MX" && row.name === name);
|
|
564
|
+
await vercel(`/v2/domains/${encodeURIComponent(zone)}/records`, {
|
|
565
|
+
method: "POST",
|
|
566
|
+
body: JSON.stringify({
|
|
567
|
+
type: "MX",
|
|
568
|
+
name,
|
|
569
|
+
value: target,
|
|
570
|
+
mxPriority: priority,
|
|
571
|
+
ttl: 60
|
|
572
|
+
})
|
|
573
|
+
});
|
|
574
|
+
results.push({
|
|
575
|
+
purpose: record.purpose,
|
|
576
|
+
host: record.host,
|
|
577
|
+
action: sameName.length > 0 ? "merged" : "created"
|
|
578
|
+
});
|
|
579
|
+
await refresh();
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (recordType !== "TXT") {
|
|
583
|
+
results.push({
|
|
584
|
+
purpose: record.purpose,
|
|
585
|
+
host: record.host,
|
|
586
|
+
action: "unchanged",
|
|
587
|
+
detail: `Skipped unsupported type ${record.type}`
|
|
588
|
+
});
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (record.purpose === "SPF") {
|
|
592
|
+
const spfRecords = cached.filter(
|
|
593
|
+
(row) => row.type === "TXT" && row.name === "" && isSpfRecord(row.value)
|
|
594
|
+
);
|
|
595
|
+
const existing = spfRecords[0];
|
|
596
|
+
const includeHost = record.value.match(/include:([^\s]+)/i)?.[1] || "spf.groundmail.dev";
|
|
597
|
+
const merged = mergeSpfInclude(existing?.value, includeHost);
|
|
598
|
+
if (existing) {
|
|
599
|
+
if (existing.value.trim() === merged.trim()) {
|
|
600
|
+
results.push({
|
|
601
|
+
purpose: record.purpose,
|
|
602
|
+
host: record.host,
|
|
603
|
+
action: "unchanged",
|
|
604
|
+
detail: "SPF already includes Groundmail"
|
|
605
|
+
});
|
|
606
|
+
} else {
|
|
607
|
+
await vercel(`/v1/domains/records/${existing.id}`, {
|
|
608
|
+
method: "PATCH",
|
|
609
|
+
body: JSON.stringify({ type: "TXT", name: "", value: merged, ttl: 60 })
|
|
610
|
+
});
|
|
611
|
+
results.push({
|
|
612
|
+
purpose: record.purpose,
|
|
613
|
+
host: record.host,
|
|
614
|
+
action: "merged",
|
|
615
|
+
detail: merged
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
await vercel(`/v2/domains/${encodeURIComponent(zone)}/records`, {
|
|
620
|
+
method: "POST",
|
|
621
|
+
body: JSON.stringify({ type: "TXT", name: "", value: merged, ttl: 60 })
|
|
622
|
+
});
|
|
623
|
+
results.push({
|
|
624
|
+
purpose: record.purpose,
|
|
625
|
+
host: record.host,
|
|
626
|
+
action: "created",
|
|
627
|
+
detail: merged
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
await refresh();
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
const existingList = cached.filter((row) => row.type === "TXT" && row.name === name);
|
|
634
|
+
const match = existingList.find((row) => row.value === record.value);
|
|
635
|
+
if (match) {
|
|
636
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const replaceable = existingList.find((row) => {
|
|
640
|
+
if (record.purpose === "Domain ownership") return row.value.includes("groundmail-verify=");
|
|
641
|
+
if (record.purpose === "DKIM") return row.value.toLowerCase().includes("v=dkim1");
|
|
642
|
+
if (record.purpose === "DMARC") return row.value.toLowerCase().includes("v=dmarc1");
|
|
643
|
+
return false;
|
|
644
|
+
});
|
|
645
|
+
if (replaceable) {
|
|
646
|
+
await vercel(`/v1/domains/records/${replaceable.id}`, {
|
|
647
|
+
method: "PATCH",
|
|
648
|
+
body: JSON.stringify({ type: "TXT", name, value: record.value, ttl: 60 })
|
|
649
|
+
});
|
|
650
|
+
results.push({ purpose: record.purpose, host: record.host, action: "updated" });
|
|
651
|
+
} else {
|
|
652
|
+
await vercel(`/v2/domains/${encodeURIComponent(zone)}/records`, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
body: JSON.stringify({ type: "TXT", name, value: record.value, ttl: 60 })
|
|
655
|
+
});
|
|
656
|
+
results.push({ purpose: record.purpose, host: record.host, action: "created" });
|
|
657
|
+
}
|
|
658
|
+
await refresh();
|
|
659
|
+
}
|
|
660
|
+
return results;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/dns/apply.ts
|
|
666
|
+
function createDnsProvider(provider, credentials, fetchImpl) {
|
|
667
|
+
if (provider === "cloudflare") {
|
|
668
|
+
const token = credentials.cloudflareApiToken;
|
|
669
|
+
if (!token) {
|
|
670
|
+
throw new Error("Missing Cloudflare token. Set CLOUDFLARE_API_TOKEN or pass cloudflareApiToken.");
|
|
671
|
+
}
|
|
672
|
+
return createCloudflareProvider({
|
|
673
|
+
apiToken: token,
|
|
674
|
+
zoneId: credentials.cloudflareZoneId,
|
|
675
|
+
fetchImpl
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (provider === "godaddy") {
|
|
679
|
+
const apiKey = credentials.godaddyApiKey;
|
|
680
|
+
const apiSecret = credentials.godaddyApiSecret;
|
|
681
|
+
if (!apiKey || !apiSecret) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
"Missing GoDaddy credentials. Set GODADDY_API_KEY and GODADDY_API_SECRET."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return createGoDaddyProvider({ apiKey, apiSecret, fetchImpl });
|
|
687
|
+
}
|
|
688
|
+
if (provider === "vercel") {
|
|
689
|
+
const apiToken = credentials.vercelApiToken;
|
|
690
|
+
if (!apiToken) {
|
|
691
|
+
throw new Error("Missing Vercel token. Set VERCEL_API_TOKEN or pass vercelApiToken.");
|
|
692
|
+
}
|
|
693
|
+
return createVercelProvider({
|
|
694
|
+
apiToken,
|
|
695
|
+
teamId: credentials.vercelTeamId,
|
|
696
|
+
fetchImpl
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
throw new Error(`Unsupported DNS provider: ${provider}`);
|
|
700
|
+
}
|
|
701
|
+
async function applyDnsRecords(input) {
|
|
702
|
+
const provider = createDnsProvider(input.provider, input.credentials, input.fetchImpl);
|
|
703
|
+
const results = await provider.apply(input.domain, input.records);
|
|
704
|
+
return {
|
|
705
|
+
provider: input.provider,
|
|
706
|
+
domain: input.domain,
|
|
707
|
+
results
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/webhooks/verify.ts
|
|
712
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
713
|
+
function signWebhookPayload(secret, payload, timestamp) {
|
|
714
|
+
const signed = `${timestamp}.${payload}`;
|
|
715
|
+
return createHmac("sha256", secret).update(signed).digest("hex");
|
|
716
|
+
}
|
|
717
|
+
function buildSignatureHeader(secret, payload, timestamp = Math.floor(Date.now() / 1e3)) {
|
|
718
|
+
const signature = signWebhookPayload(secret, payload, timestamp);
|
|
719
|
+
return `t=${timestamp},v1=${signature}`;
|
|
720
|
+
}
|
|
721
|
+
function verifyWebhookSignature(secret, payload, header, toleranceSec = 300) {
|
|
722
|
+
if (!header) return false;
|
|
723
|
+
const parts = Object.fromEntries(
|
|
724
|
+
header.split(",").map((part) => {
|
|
725
|
+
const [key, value] = part.trim().split("=");
|
|
726
|
+
return [key, value];
|
|
727
|
+
})
|
|
728
|
+
);
|
|
729
|
+
const timestamp = Number(parts.t);
|
|
730
|
+
const signature = parts.v1;
|
|
731
|
+
if (!timestamp || !signature) return false;
|
|
732
|
+
if (Math.abs(Math.floor(Date.now() / 1e3) - timestamp) > toleranceSec) return false;
|
|
733
|
+
const expected = signWebhookPayload(secret, payload, timestamp);
|
|
734
|
+
try {
|
|
735
|
+
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/automations.ts
|
|
742
|
+
var AutomationsResource = class {
|
|
743
|
+
constructor(client) {
|
|
744
|
+
this.client = client;
|
|
745
|
+
}
|
|
746
|
+
async list() {
|
|
747
|
+
return this.client.request("/automations", { method: "GET" });
|
|
748
|
+
}
|
|
749
|
+
async get(id) {
|
|
750
|
+
return this.client.request(`/automations/${encodeURIComponent(id)}`, { method: "GET" });
|
|
751
|
+
}
|
|
752
|
+
async create(params) {
|
|
753
|
+
return this.client.request("/automations", {
|
|
754
|
+
method: "POST",
|
|
755
|
+
body: JSON.stringify(params)
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
async update(id, params) {
|
|
759
|
+
return this.client.request(`/automations/${encodeURIComponent(id)}`, {
|
|
760
|
+
method: "PATCH",
|
|
761
|
+
body: JSON.stringify(params)
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async remove(id) {
|
|
765
|
+
return this.client.request(`/automations/${encodeURIComponent(id)}`, {
|
|
766
|
+
method: "DELETE"
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
async activate(id) {
|
|
770
|
+
return this.client.request(`/automations/${encodeURIComponent(id)}`, {
|
|
771
|
+
method: "POST",
|
|
772
|
+
body: JSON.stringify({ action: "activate" })
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async pause(id) {
|
|
776
|
+
return this.client.request(`/automations/${encodeURIComponent(id)}`, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
body: JSON.stringify({ action: "pause" })
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
async listRuns(params = {}) {
|
|
782
|
+
const search = new URLSearchParams();
|
|
783
|
+
if (params.automation_id) search.set("automation_id", params.automation_id);
|
|
784
|
+
const query = search.toString();
|
|
785
|
+
return this.client.request(`/automations/runs${query ? `?${query}` : ""}`, { method: "GET" });
|
|
786
|
+
}
|
|
787
|
+
async getRun(id) {
|
|
788
|
+
return this.client.request(`/automations/runs/${encodeURIComponent(id)}`, { method: "GET" });
|
|
789
|
+
}
|
|
790
|
+
async cancelRun(id) {
|
|
791
|
+
return this.client.request(`/automations/runs/${encodeURIComponent(id)}`, {
|
|
792
|
+
method: "POST",
|
|
793
|
+
body: JSON.stringify({ action: "cancel" })
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
async retryRun(id) {
|
|
797
|
+
return this.client.request(`/automations/runs/${encodeURIComponent(id)}`, {
|
|
798
|
+
method: "POST",
|
|
799
|
+
body: JSON.stringify({ action: "retry" })
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/domains.ts
|
|
805
|
+
var DomainsResource = class {
|
|
806
|
+
constructor(client) {
|
|
807
|
+
this.client = client;
|
|
808
|
+
}
|
|
809
|
+
async list(params = {}) {
|
|
810
|
+
const search = new URLSearchParams();
|
|
811
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
812
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
813
|
+
const inboundEnabled = params.inbound_enabled ?? params.inboundEnabled;
|
|
814
|
+
if (inboundEnabled !== void 0) search.set("inbound_enabled", String(inboundEnabled));
|
|
815
|
+
const query = search.toString();
|
|
816
|
+
return this.client.request(`/domains${query ? `?${query}` : ""}`, { method: "GET" });
|
|
817
|
+
}
|
|
818
|
+
async get(idOrName) {
|
|
819
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, { method: "GET" });
|
|
820
|
+
}
|
|
821
|
+
async create(params) {
|
|
822
|
+
const inboundEnabled = params.inbound_enabled ?? params.inboundEnabled;
|
|
823
|
+
return this.client.request("/domains", {
|
|
824
|
+
method: "POST",
|
|
825
|
+
body: JSON.stringify({
|
|
826
|
+
name: params.name,
|
|
827
|
+
...inboundEnabled !== void 0 ? { inbound_enabled: inboundEnabled } : {}
|
|
828
|
+
})
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
async verify(idOrName) {
|
|
832
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
833
|
+
method: "POST",
|
|
834
|
+
body: JSON.stringify({ action: "verify" })
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
async update(idOrName, params) {
|
|
838
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
839
|
+
method: "PATCH",
|
|
840
|
+
body: JSON.stringify(params)
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
async delete(idOrName) {
|
|
844
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
845
|
+
method: "DELETE"
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Apply DNS using the public API. Cloudflare, Vercel, and GoDaddy can use stored dashboard
|
|
850
|
+
* credentials; GoDaddy also accepts one-time credentials on the request.
|
|
851
|
+
*/
|
|
852
|
+
async apply(idOrName, options = {}) {
|
|
853
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
body: JSON.stringify({
|
|
856
|
+
action: "apply",
|
|
857
|
+
provider: options.provider || "cloudflare",
|
|
858
|
+
...options.credentials ? { credentials: options.credentials } : {}
|
|
859
|
+
})
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Fetch Groundmail DNS records, then write them at Cloudflare, Vercel, or GoDaddy
|
|
864
|
+
* using the caller's provider credentials (never sent to Groundmail).
|
|
865
|
+
*/
|
|
866
|
+
async applyDns(params) {
|
|
867
|
+
const detail = await this.get(params.idOrName);
|
|
868
|
+
const records = [
|
|
869
|
+
...detail.records,
|
|
870
|
+
...detail.domain.inbound_enabled ? detail.inbound_records || [] : []
|
|
871
|
+
];
|
|
872
|
+
return applyDnsRecords({
|
|
873
|
+
provider: params.provider,
|
|
874
|
+
domain: detail.domain.name,
|
|
875
|
+
records,
|
|
876
|
+
credentials: {
|
|
877
|
+
cloudflareApiToken: params.cloudflareApiToken,
|
|
878
|
+
cloudflareZoneId: params.cloudflareZoneId,
|
|
879
|
+
godaddyApiKey: params.godaddyApiKey,
|
|
880
|
+
godaddyApiSecret: params.godaddyApiSecret,
|
|
881
|
+
vercelApiToken: params.vercelApiToken,
|
|
882
|
+
vercelTeamId: params.vercelTeamId
|
|
883
|
+
},
|
|
884
|
+
fetchImpl: this.client.fetchImpl
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/react-email.ts
|
|
890
|
+
async function renderReactEmail(react) {
|
|
891
|
+
let render;
|
|
892
|
+
try {
|
|
893
|
+
const mod = await import("@react-email/render");
|
|
894
|
+
render = mod.render ?? mod.default?.render;
|
|
895
|
+
} catch {
|
|
896
|
+
throw new Error(
|
|
897
|
+
"The `react` option requires peer dependency `@react-email/render`. Install it with: npm install @react-email/render react"
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
if (typeof render !== "function") {
|
|
901
|
+
throw new Error(
|
|
902
|
+
"Could not load `render` from `@react-email/render`. Ensure a compatible version is installed."
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return render(react);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/emails.ts
|
|
909
|
+
var EmailsResource = class {
|
|
910
|
+
constructor(client) {
|
|
911
|
+
this.client = client;
|
|
912
|
+
}
|
|
913
|
+
async list(params = {}) {
|
|
914
|
+
const search = new URLSearchParams();
|
|
915
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
916
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
917
|
+
const query = search.toString();
|
|
918
|
+
return this.client.request(`/emails${query ? `?${query}` : ""}`, {
|
|
919
|
+
method: "GET"
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
async get(id) {
|
|
923
|
+
return this.client.request(`/emails/${encodeURIComponent(id)}`, {
|
|
924
|
+
method: "GET"
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
async insights(window = "30d") {
|
|
928
|
+
const search = new URLSearchParams({ window });
|
|
929
|
+
return this.client.request(`/deliverability?${search.toString()}`, {
|
|
930
|
+
method: "GET"
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
async send(params) {
|
|
934
|
+
const payload = await this.serializeSendParams(params);
|
|
935
|
+
const headers = {};
|
|
936
|
+
if (params.idempotencyKey) {
|
|
937
|
+
headers["Idempotency-Key"] = params.idempotencyKey;
|
|
938
|
+
}
|
|
939
|
+
return this.client.request("/emails", {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers,
|
|
942
|
+
body: JSON.stringify(payload)
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async batch(emails) {
|
|
946
|
+
const serialized = await Promise.all(emails.map((email) => this.serializeSendParams(email)));
|
|
947
|
+
return this.client.request("/emails/batch", {
|
|
948
|
+
method: "POST",
|
|
949
|
+
body: JSON.stringify({ emails: serialized })
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
async update(id, params) {
|
|
953
|
+
const payload = {};
|
|
954
|
+
if (params.cancel !== void 0) payload.cancel = params.cancel;
|
|
955
|
+
const scheduledAt = params.scheduledAt ?? params.scheduled_at;
|
|
956
|
+
if (scheduledAt !== void 0) {
|
|
957
|
+
payload.scheduled_at = scheduledAt instanceof Date ? scheduledAt.toISOString() : scheduledAt;
|
|
958
|
+
}
|
|
959
|
+
return this.client.request(`/emails/${encodeURIComponent(id)}`, {
|
|
960
|
+
method: "PATCH",
|
|
961
|
+
body: JSON.stringify(payload)
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
async cancel(id) {
|
|
965
|
+
return this.update(id, { cancel: true });
|
|
966
|
+
}
|
|
967
|
+
/** Create a new send from a previously stored email (new `msg_…` id). */
|
|
968
|
+
async resend(id) {
|
|
969
|
+
return this.client.request(`/emails/${encodeURIComponent(id)}/resend`, {
|
|
970
|
+
method: "POST"
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/** Simulate a webhook event for CI / local testing (`POST /emails/test`). */
|
|
974
|
+
async testWebhook(params) {
|
|
975
|
+
return this.client.request("/emails/test", {
|
|
976
|
+
method: "POST",
|
|
977
|
+
body: JSON.stringify({
|
|
978
|
+
event: params.event,
|
|
979
|
+
...params.email_id !== void 0 ? { email_id: params.email_id } : {},
|
|
980
|
+
...params.webhook_id !== void 0 ? { webhook_id: params.webhook_id } : {},
|
|
981
|
+
...params.sync !== void 0 ? { sync: params.sync } : {},
|
|
982
|
+
...params.deliver !== void 0 ? { deliver: params.deliver } : {}
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
async serializeSendParams(params) {
|
|
987
|
+
const hasReact = params.react !== void 0;
|
|
988
|
+
const hasHtml = params.html !== void 0;
|
|
989
|
+
const hasText = params.text !== void 0;
|
|
990
|
+
const hasTemplate = params.template !== void 0;
|
|
991
|
+
if (hasReact && (hasHtml || hasText || hasTemplate)) {
|
|
992
|
+
throw new Error("`react` cannot be combined with `html`, `text`, or `template`");
|
|
993
|
+
}
|
|
994
|
+
if (hasTemplate && (hasHtml || hasText)) {
|
|
995
|
+
throw new Error("`template` cannot be combined with `html` or `text`");
|
|
996
|
+
}
|
|
997
|
+
const payload = {
|
|
998
|
+
from: params.from,
|
|
999
|
+
to: params.to
|
|
1000
|
+
};
|
|
1001
|
+
if (params.subject !== void 0) payload.subject = params.subject;
|
|
1002
|
+
if (hasReact) {
|
|
1003
|
+
payload.html = await renderReactEmail(params.react);
|
|
1004
|
+
} else {
|
|
1005
|
+
const html = params.html ?? params.htmlBody ?? params.html_body;
|
|
1006
|
+
const text = params.text ?? params.textBody ?? params.text_body ?? params.plain_body;
|
|
1007
|
+
if (html !== void 0) payload.html = html;
|
|
1008
|
+
if (text !== void 0) payload.text = text;
|
|
1009
|
+
}
|
|
1010
|
+
if (params.template !== void 0) {
|
|
1011
|
+
payload.template = params.template;
|
|
1012
|
+
}
|
|
1013
|
+
const replyTo = params.replyTo ?? params.reply_to;
|
|
1014
|
+
if (replyTo !== void 0) payload.reply_to = replyTo;
|
|
1015
|
+
if (params.cc !== void 0) payload.cc = params.cc;
|
|
1016
|
+
if (params.bcc !== void 0) payload.bcc = params.bcc;
|
|
1017
|
+
if (params.tags !== void 0) payload.tags = params.tags;
|
|
1018
|
+
if (params.headers !== void 0) payload.headers = params.headers;
|
|
1019
|
+
if (params.tag !== void 0) payload.tag = params.tag;
|
|
1020
|
+
if (params.attachments !== void 0) {
|
|
1021
|
+
payload.attachments = params.attachments.map((attachment) => ({
|
|
1022
|
+
name: attachment.name ?? attachment.filename,
|
|
1023
|
+
content_type: attachment.content_type ?? attachment.contentType,
|
|
1024
|
+
data: attachment.data ?? attachment.content,
|
|
1025
|
+
...attachment.content_id ?? attachment.contentId ? { content_id: attachment.content_id ?? attachment.contentId } : {}
|
|
1026
|
+
}));
|
|
1027
|
+
}
|
|
1028
|
+
const scheduledAt = params.scheduledAt ?? params.scheduled_at;
|
|
1029
|
+
if (scheduledAt !== void 0) {
|
|
1030
|
+
payload.scheduled_at = scheduledAt instanceof Date ? scheduledAt.toISOString() : scheduledAt;
|
|
1031
|
+
}
|
|
1032
|
+
if (params.unsubscribe !== void 0) payload.unsubscribe = params.unsubscribe;
|
|
1033
|
+
if (params.category !== void 0) payload.category = params.category;
|
|
1034
|
+
return payload;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// src/events.ts
|
|
1039
|
+
var EventsResource = class {
|
|
1040
|
+
constructor(client) {
|
|
1041
|
+
this.client = client;
|
|
1042
|
+
}
|
|
1043
|
+
async trigger(params) {
|
|
1044
|
+
const headers = {};
|
|
1045
|
+
if (params.idempotencyKey) {
|
|
1046
|
+
headers["Idempotency-Key"] = params.idempotencyKey;
|
|
1047
|
+
}
|
|
1048
|
+
const payload = {};
|
|
1049
|
+
if (params.name !== void 0) payload.name = params.name;
|
|
1050
|
+
if (params.type !== void 0) payload.type = params.type;
|
|
1051
|
+
const userId = params.user_id ?? params.userId;
|
|
1052
|
+
if (userId !== void 0) payload.user_id = userId;
|
|
1053
|
+
if (params.email !== void 0) payload.email = params.email;
|
|
1054
|
+
if (params.data !== void 0) payload.data = params.data;
|
|
1055
|
+
return this.client.request("/events", {
|
|
1056
|
+
method: "POST",
|
|
1057
|
+
headers,
|
|
1058
|
+
body: JSON.stringify(payload)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// src/integrations.ts
|
|
1064
|
+
var IntegrationsResource = class {
|
|
1065
|
+
constructor(client) {
|
|
1066
|
+
this.client = client;
|
|
1067
|
+
}
|
|
1068
|
+
async getTwilio() {
|
|
1069
|
+
return this.client.request("/integrations/twilio", { method: "GET" });
|
|
1070
|
+
}
|
|
1071
|
+
async connectTwilio(params) {
|
|
1072
|
+
return this.client.request("/integrations/twilio", {
|
|
1073
|
+
method: "PUT",
|
|
1074
|
+
body: JSON.stringify(params)
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
async disconnectTwilio() {
|
|
1078
|
+
return this.client.request("/integrations/twilio", { method: "DELETE" });
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
// src/keys.ts
|
|
1083
|
+
var KeysResource = class {
|
|
1084
|
+
constructor(client) {
|
|
1085
|
+
this.client = client;
|
|
1086
|
+
}
|
|
1087
|
+
async list(params = {}) {
|
|
1088
|
+
const search = new URLSearchParams();
|
|
1089
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1090
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1091
|
+
const query = search.toString();
|
|
1092
|
+
return this.client.request(`/api-keys${query ? `?${query}` : ""}`, {
|
|
1093
|
+
method: "GET"
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async create(params = {}) {
|
|
1097
|
+
return this.client.request("/api-keys", {
|
|
1098
|
+
method: "POST",
|
|
1099
|
+
body: JSON.stringify({
|
|
1100
|
+
...params.name ? { name: params.name } : {},
|
|
1101
|
+
...params.scope ? { scope: params.scope } : {}
|
|
1102
|
+
})
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
async remove(id) {
|
|
1106
|
+
return this.client.request(`/api-keys/${encodeURIComponent(id)}`, {
|
|
1107
|
+
method: "DELETE"
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/smtp-credentials.ts
|
|
1113
|
+
var SmtpCredentialsResource = class {
|
|
1114
|
+
constructor(client) {
|
|
1115
|
+
this.client = client;
|
|
1116
|
+
}
|
|
1117
|
+
async list(params = {}) {
|
|
1118
|
+
const search = new URLSearchParams();
|
|
1119
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1120
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1121
|
+
const query = search.toString();
|
|
1122
|
+
return this.client.request(
|
|
1123
|
+
`/smtp-credentials${query ? `?${query}` : ""}`,
|
|
1124
|
+
{ method: "GET" }
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
async create(params = {}) {
|
|
1128
|
+
return this.client.request("/smtp-credentials", {
|
|
1129
|
+
method: "POST",
|
|
1130
|
+
body: JSON.stringify({
|
|
1131
|
+
...params.name ? { name: params.name } : {}
|
|
1132
|
+
})
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
async remove(id) {
|
|
1136
|
+
return this.client.request(`/smtp-credentials/${encodeURIComponent(id)}`, {
|
|
1137
|
+
method: "DELETE"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// src/received-emails.ts
|
|
1143
|
+
var ReceivedEmailsResource = class {
|
|
1144
|
+
constructor(client) {
|
|
1145
|
+
this.client = client;
|
|
1146
|
+
}
|
|
1147
|
+
async list(params = {}) {
|
|
1148
|
+
const search = new URLSearchParams();
|
|
1149
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1150
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1151
|
+
if (params.domain) search.set("domain", params.domain);
|
|
1152
|
+
const query = search.toString();
|
|
1153
|
+
return this.client.request(
|
|
1154
|
+
`/received-emails${query ? `?${query}` : ""}`,
|
|
1155
|
+
{
|
|
1156
|
+
method: "GET"
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
async get(id) {
|
|
1161
|
+
return this.client.request(
|
|
1162
|
+
`/received-emails/${encodeURIComponent(id)}`,
|
|
1163
|
+
{
|
|
1164
|
+
method: "GET"
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/suppressions.ts
|
|
1171
|
+
var SuppressionsResource = class {
|
|
1172
|
+
constructor(client) {
|
|
1173
|
+
this.client = client;
|
|
1174
|
+
}
|
|
1175
|
+
async list(params = {}) {
|
|
1176
|
+
const search = new URLSearchParams();
|
|
1177
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1178
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1179
|
+
if (params.email) search.set("email", params.email);
|
|
1180
|
+
const query = search.toString();
|
|
1181
|
+
return this.client.request(`/suppressions${query ? `?${query}` : ""}`, { method: "GET" });
|
|
1182
|
+
}
|
|
1183
|
+
async create(params) {
|
|
1184
|
+
return this.client.request("/suppressions", {
|
|
1185
|
+
method: "POST",
|
|
1186
|
+
body: JSON.stringify(params)
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/** Remove by suppression id or email address. */
|
|
1190
|
+
async remove(idOrEmail) {
|
|
1191
|
+
if (idOrEmail.includes("@")) {
|
|
1192
|
+
return this.client.request(`/suppressions?email=${encodeURIComponent(idOrEmail)}`, {
|
|
1193
|
+
method: "DELETE"
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
return this.client.request(`/suppressions/${encodeURIComponent(idOrEmail)}`, {
|
|
1197
|
+
method: "DELETE"
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// src/templates.ts
|
|
1203
|
+
var TemplatesResource = class {
|
|
1204
|
+
constructor(client) {
|
|
1205
|
+
this.client = client;
|
|
1206
|
+
}
|
|
1207
|
+
async list(params = {}) {
|
|
1208
|
+
const search = new URLSearchParams();
|
|
1209
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1210
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1211
|
+
if (params.status) search.set("status", params.status);
|
|
1212
|
+
const query = search.toString();
|
|
1213
|
+
return this.client.request(`/templates${query ? `?${query}` : ""}`, { method: "GET" });
|
|
1214
|
+
}
|
|
1215
|
+
async get(idOrAlias) {
|
|
1216
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}`, { method: "GET" });
|
|
1217
|
+
}
|
|
1218
|
+
async create(params) {
|
|
1219
|
+
return this.client.request("/templates", {
|
|
1220
|
+
method: "POST",
|
|
1221
|
+
body: JSON.stringify(params)
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
async update(idOrAlias, params) {
|
|
1225
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}`, {
|
|
1226
|
+
method: "PATCH",
|
|
1227
|
+
body: JSON.stringify(params)
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
async remove(idOrAlias) {
|
|
1231
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}`, {
|
|
1232
|
+
method: "DELETE"
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
async publish(idOrAlias) {
|
|
1236
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}`, {
|
|
1237
|
+
method: "POST",
|
|
1238
|
+
body: JSON.stringify({ action: "publish" })
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
async duplicate(idOrAlias, params = {}) {
|
|
1242
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}`, {
|
|
1243
|
+
method: "POST",
|
|
1244
|
+
body: JSON.stringify({ action: "duplicate", ...params })
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
async listStarters() {
|
|
1248
|
+
return this.client.request("/templates/starters", { method: "GET" });
|
|
1249
|
+
}
|
|
1250
|
+
async fromStarter(alias) {
|
|
1251
|
+
return this.client.request("/templates/from-starter", {
|
|
1252
|
+
method: "POST",
|
|
1253
|
+
body: JSON.stringify({ alias })
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async listVersions(idOrAlias) {
|
|
1257
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}/versions`, {
|
|
1258
|
+
method: "GET"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async restoreVersion(idOrAlias, version) {
|
|
1262
|
+
return this.client.request(
|
|
1263
|
+
`/templates/${encodeURIComponent(idOrAlias)}/versions/${version}`,
|
|
1264
|
+
{
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
body: JSON.stringify({ action: "restore" })
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
async testSend(idOrAlias, params) {
|
|
1271
|
+
return this.client.request(`/templates/${encodeURIComponent(idOrAlias)}/test-send`, {
|
|
1272
|
+
method: "POST",
|
|
1273
|
+
body: JSON.stringify(params)
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
// src/webhooks/index.ts
|
|
1279
|
+
var WebhooksResource = class {
|
|
1280
|
+
constructor(client) {
|
|
1281
|
+
this.client = client;
|
|
1282
|
+
}
|
|
1283
|
+
async list(params = {}) {
|
|
1284
|
+
const search = new URLSearchParams();
|
|
1285
|
+
if (params.limit != null) search.set("limit", String(params.limit));
|
|
1286
|
+
if (params.cursor) search.set("cursor", params.cursor);
|
|
1287
|
+
const query = search.toString();
|
|
1288
|
+
return this.client.request(`/webhooks${query ? `?${query}` : ""}`, { method: "GET" });
|
|
1289
|
+
}
|
|
1290
|
+
async get(id) {
|
|
1291
|
+
return this.client.request(`/webhooks/${encodeURIComponent(id)}`, { method: "GET" });
|
|
1292
|
+
}
|
|
1293
|
+
async create(params) {
|
|
1294
|
+
return this.client.request("/webhooks", {
|
|
1295
|
+
method: "POST",
|
|
1296
|
+
body: JSON.stringify(params)
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
async update(id, params) {
|
|
1300
|
+
return this.client.request(`/webhooks/${encodeURIComponent(id)}`, {
|
|
1301
|
+
method: "PATCH",
|
|
1302
|
+
body: JSON.stringify(params)
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
async delete(id) {
|
|
1306
|
+
return this.client.request(`/webhooks/${id}`, { method: "DELETE" });
|
|
1307
|
+
}
|
|
1308
|
+
/** Verify an incoming webhook signature (same as `verifyWebhookSignature`). */
|
|
1309
|
+
verify(secret, payload, header, toleranceSec = 300) {
|
|
1310
|
+
return verifyWebhookSignature(secret, payload, header, toleranceSec);
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
// src/client.ts
|
|
1315
|
+
var DEFAULT_API_BASE_URL = "https://api.groundmail.dev";
|
|
1316
|
+
var Groundmail = class {
|
|
1317
|
+
constructor(apiKey, options = {}) {
|
|
1318
|
+
this.apiKey = apiKey;
|
|
1319
|
+
this.options = options;
|
|
1320
|
+
if (!apiKey?.startsWith("gm_")) {
|
|
1321
|
+
throw new Error("Groundmail API key must start with gm_");
|
|
1322
|
+
}
|
|
1323
|
+
this.domains = new DomainsResource(this);
|
|
1324
|
+
this.emails = new EmailsResource(this);
|
|
1325
|
+
this.receivedEmails = new ReceivedEmailsResource(this);
|
|
1326
|
+
this.events = new EventsResource(this);
|
|
1327
|
+
this.automations = new AutomationsResource(this);
|
|
1328
|
+
this.webhooks = new WebhooksResource(this);
|
|
1329
|
+
this.integrations = new IntegrationsResource(this);
|
|
1330
|
+
this.apiKeys = new KeysResource(this);
|
|
1331
|
+
this.smtpCredentials = new SmtpCredentialsResource(this);
|
|
1332
|
+
this.suppressions = new SuppressionsResource(this);
|
|
1333
|
+
this.templates = new TemplatesResource(this);
|
|
1334
|
+
}
|
|
1335
|
+
get baseUrl() {
|
|
1336
|
+
return (this.options.baseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
1337
|
+
}
|
|
1338
|
+
get fetchImpl() {
|
|
1339
|
+
return this.options.fetch || fetch;
|
|
1340
|
+
}
|
|
1341
|
+
async request(path, init) {
|
|
1342
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1343
|
+
...init,
|
|
1344
|
+
headers: {
|
|
1345
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1346
|
+
"Content-Type": "application/json",
|
|
1347
|
+
...init.headers || {}
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
const body = await response.json().catch(() => ({}));
|
|
1351
|
+
if (!response.ok) {
|
|
1352
|
+
throw GroundmailError.fromResponse(response.status, body);
|
|
1353
|
+
}
|
|
1354
|
+
return body;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
export {
|
|
1359
|
+
GroundmailError,
|
|
1360
|
+
mergeSpfInclude,
|
|
1361
|
+
applyDnsRecords,
|
|
1362
|
+
signWebhookPayload,
|
|
1363
|
+
buildSignatureHeader,
|
|
1364
|
+
verifyWebhookSignature,
|
|
1365
|
+
DEFAULT_API_BASE_URL,
|
|
1366
|
+
Groundmail
|
|
1367
|
+
};
|