dkimkit 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -2
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +398 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/dns-host.d.ts +10 -0
- package/dist/src/dns-host.js +31 -0
- package/dist/src/dns-host.js.map +1 -0
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +51 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/hosted/api.d.ts +107 -0
- package/dist/src/hosted/api.js +919 -0
- package/dist/src/hosted/api.js.map +1 -0
- package/dist/src/hosted/auth.d.ts +80 -0
- package/dist/src/hosted/auth.js +147 -0
- package/dist/src/hosted/auth.js.map +1 -0
- package/dist/src/hosted/billing-dodo.d.ts +58 -0
- package/dist/src/hosted/billing-dodo.js +139 -0
- package/dist/src/hosted/billing-dodo.js.map +1 -0
- package/dist/src/hosted/billing-stripe.d.ts +82 -0
- package/dist/src/hosted/billing-stripe.js +205 -0
- package/dist/src/hosted/billing-stripe.js.map +1 -0
- package/dist/src/hosted/billing.d.ts +114 -0
- package/dist/src/hosted/billing.js +173 -0
- package/dist/src/hosted/billing.js.map +1 -0
- package/dist/src/hosted/bulk.d.ts +47 -0
- package/dist/src/hosted/bulk.js +66 -0
- package/dist/src/hosted/bulk.js.map +1 -0
- package/dist/src/hosted/cli.d.ts +2 -0
- package/dist/src/hosted/cli.js +318 -0
- package/dist/src/hosted/cli.js.map +1 -0
- package/dist/src/hosted/crypto.d.ts +22 -0
- package/dist/src/hosted/crypto.js +55 -0
- package/dist/src/hosted/crypto.js.map +1 -0
- package/dist/src/hosted/index.d.ts +7 -0
- package/dist/src/hosted/index.js +11 -0
- package/dist/src/hosted/index.js.map +1 -0
- package/dist/src/hosted/markdown.d.ts +16 -0
- package/dist/src/hosted/markdown.js +168 -0
- package/dist/src/hosted/markdown.js.map +1 -0
- package/dist/src/hosted/net.d.ts +9 -0
- package/dist/src/hosted/net.js +60 -0
- package/dist/src/hosted/net.js.map +1 -0
- package/dist/src/hosted/postgres-store.d.ts +62 -0
- package/dist/src/hosted/postgres-store.js +387 -0
- package/dist/src/hosted/postgres-store.js.map +1 -0
- package/dist/src/hosted/serialize.d.ts +144 -0
- package/dist/src/hosted/serialize.js +126 -0
- package/dist/src/hosted/serialize.js.map +1 -0
- package/dist/src/hosted/service.d.ts +170 -0
- package/dist/src/hosted/service.js +603 -0
- package/dist/src/hosted/service.js.map +1 -0
- package/dist/src/hosted/site.d.ts +84 -0
- package/dist/src/hosted/site.js +653 -0
- package/dist/src/hosted/site.js.map +1 -0
- package/dist/src/hosted/sqlite-store.d.ts +45 -0
- package/dist/src/hosted/sqlite-store.js +351 -0
- package/dist/src/hosted/sqlite-store.js.map +1 -0
- package/dist/src/hosted/store.d.ts +257 -0
- package/dist/src/hosted/store.js +181 -0
- package/dist/src/hosted/store.js.map +1 -0
- package/dist/src/hosted/widget.d.ts +46 -0
- package/dist/src/hosted/widget.js +130 -0
- package/dist/src/hosted/widget.js.map +1 -0
- package/dist/src/hosted/worker.d.ts +45 -0
- package/dist/src/hosted/worker.js +142 -0
- package/dist/src/hosted/worker.js.map +1 -0
- package/dist/src/http.d.ts +14 -0
- package/dist/src/http.js +54 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/orchestrator.d.ts +70 -0
- package/dist/src/orchestrator.js +69 -0
- package/dist/src/orchestrator.js.map +1 -0
- package/dist/src/propagation.d.ts +30 -0
- package/dist/src/propagation.js +76 -0
- package/dist/src/propagation.js.map +1 -0
- package/dist/src/providers/dns/aws.d.ts +28 -0
- package/dist/src/providers/dns/aws.js +103 -0
- package/dist/src/providers/dns/aws.js.map +1 -0
- package/dist/src/providers/dns/cloudflare.d.ts +26 -0
- package/dist/src/providers/dns/cloudflare.js +97 -0
- package/dist/src/providers/dns/cloudflare.js.map +1 -0
- package/dist/src/providers/dns/manual.d.ts +12 -0
- package/dist/src/providers/dns/manual.js +18 -0
- package/dist/src/providers/dns/manual.js.map +1 -0
- package/dist/src/providers/dns/route53.d.ts +34 -0
- package/dist/src/providers/dns/route53.js +176 -0
- package/dist/src/providers/dns/route53.js.map +1 -0
- package/dist/src/providers/email/factory.d.ts +29 -0
- package/dist/src/providers/email/factory.js +67 -0
- package/dist/src/providers/email/factory.js.map +1 -0
- package/dist/src/providers/email/mailgun.d.ts +36 -0
- package/dist/src/providers/email/mailgun.js +141 -0
- package/dist/src/providers/email/mailgun.js.map +1 -0
- package/dist/src/providers/email/postmark.d.ts +37 -0
- package/dist/src/providers/email/postmark.js +121 -0
- package/dist/src/providers/email/postmark.js.map +1 -0
- package/dist/src/providers/email/sendgrid.d.ts +31 -0
- package/dist/src/providers/email/sendgrid.js +109 -0
- package/dist/src/providers/email/sendgrid.js.map +1 -0
- package/dist/src/providers/email/ses.d.ts +39 -0
- package/dist/src/providers/email/ses.js +123 -0
- package/dist/src/providers/email/ses.js.map +1 -0
- package/dist/src/providers/factory.d.ts +14 -0
- package/dist/src/providers/factory.js +32 -0
- package/dist/src/providers/factory.js.map +1 -0
- package/dist/src/records.d.ts +35 -0
- package/dist/src/records.js +109 -0
- package/dist/src/records.js.map +1 -0
- package/dist/src/server.d.ts +67 -0
- package/dist/src/server.js +301 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/types.d.ts +75 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/xml.d.ts +6 -0
- package/dist/src/xml.js +23 -0
- package/dist/src/xml.js.map +1 -0
- package/docs/billing.md +43 -0
- package/docs/getting-started.md +66 -0
- package/docs/hosted-api.md +192 -0
- package/docs/library.md +253 -0
- package/docs/self-hosting.md +153 -0
- package/docs/widget.md +121 -0
- package/package.json +63 -4
- package/web/dashboard.html +669 -0
- package/web/index.html +697 -0
- package/web/widget-frame.html +480 -0
- package/web/widget.js +91 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { ApiError, apiErrorHint, ConfigError, DnsConflictError } from './errors.js';
|
|
8
|
+
import { checkStatus, setupDomain } from './orchestrator.js';
|
|
9
|
+
import { loadAwsCredentials } from './providers/dns/aws.js';
|
|
10
|
+
import { createEmailProvider, EMAIL_PROVIDERS, isEmailProvider } from './providers/email/factory.js';
|
|
11
|
+
import { createDnsProvider, DNS_PROVIDERS } from './providers/factory.js';
|
|
12
|
+
import { isValidDomain, normalizeName } from './records.js';
|
|
13
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
14
|
+
const PAGE = new URL('../../web/index.html', import.meta.url);
|
|
15
|
+
/**
|
|
16
|
+
* Local web UI. Binds to 127.0.0.1 only. A random token in the first URL is
|
|
17
|
+
* swapped for a SameSite=Strict cookie; the Host header is checked to block
|
|
18
|
+
* DNS rebinding; API calls must be JSON POSTs, which cross-origin pages can't
|
|
19
|
+
* send without a CORS preflight this server never approves.
|
|
20
|
+
*/
|
|
21
|
+
export async function startUiServer(opts = {}) {
|
|
22
|
+
const env = opts.env ?? process.env;
|
|
23
|
+
const token = opts.token ?? randomBytes(24).toString('base64url');
|
|
24
|
+
const page = await readFile(PAGE, 'utf8');
|
|
25
|
+
let port = 0;
|
|
26
|
+
const cookieName = () => `dkimkit_${port}`;
|
|
27
|
+
const tokenMatches = (candidate) => {
|
|
28
|
+
if (!candidate)
|
|
29
|
+
return false;
|
|
30
|
+
const a = Buffer.from(candidate);
|
|
31
|
+
const b = Buffer.from(token);
|
|
32
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
33
|
+
};
|
|
34
|
+
const hostAllowed = (host) => host === `127.0.0.1:${port}` || host === `localhost:${port}` || host === `[::1]:${port}`;
|
|
35
|
+
const cookie = (req) => req.headers.cookie
|
|
36
|
+
?.split(';')
|
|
37
|
+
.map((c) => c.trim().split('='))
|
|
38
|
+
.find(([k]) => k === cookieName())?.[1];
|
|
39
|
+
const buildProviders = opts.providers ??
|
|
40
|
+
(async (req) => {
|
|
41
|
+
const c = req.credentials ?? {};
|
|
42
|
+
const provider = req.emailProvider ?? 'sendgrid';
|
|
43
|
+
let credentials;
|
|
44
|
+
switch (provider) {
|
|
45
|
+
case 'postmark':
|
|
46
|
+
credentials = { accountToken: c.postmarkAccountToken || env.POSTMARK_ACCOUNT_TOKEN, inboundServerId: c.postmarkInboundServerId || env.POSTMARK_INBOUND_SERVER_ID };
|
|
47
|
+
break;
|
|
48
|
+
case 'mailgun':
|
|
49
|
+
credentials = { apiKey: c.mailgunApiKey || env.MAILGUN_API_KEY, region: c.mailgunRegion || env.MAILGUN_REGION || 'us' };
|
|
50
|
+
break;
|
|
51
|
+
case 'ses': {
|
|
52
|
+
const aws = c.awsAccessKeyId && c.awsSecretAccessKey
|
|
53
|
+
? { accessKeyId: c.awsAccessKeyId, secretAccessKey: c.awsSecretAccessKey, sessionToken: c.awsSessionToken }
|
|
54
|
+
: await loadAwsCredentials({ profile: c.awsProfile, env });
|
|
55
|
+
credentials = { ...aws, region: c.sesRegion || env.AWS_REGION || env.AWS_DEFAULT_REGION };
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
credentials = { apiKey: c.sendgridApiKey || env.SENDGRID_API_KEY };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
email: createEmailProvider(provider, credentials, { subuser: req.onBehalfOf || undefined }),
|
|
63
|
+
dns: createDnsProvider(req.dns, {
|
|
64
|
+
cloudflareApiToken: c.cloudflareApiToken,
|
|
65
|
+
aws: { accessKeyId: c.awsAccessKeyId, secretAccessKey: c.awsSecretAccessKey, sessionToken: c.awsSessionToken },
|
|
66
|
+
awsProfile: c.awsProfile,
|
|
67
|
+
route53HostedZoneId: c.route53HostedZoneId,
|
|
68
|
+
env,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const runOptions = (req, email) => {
|
|
73
|
+
if (req.inbound && !email.supportsInbound)
|
|
74
|
+
throw new ConfigError(`Receiving email isn't supported with ${email.label} yet.`);
|
|
75
|
+
return {
|
|
76
|
+
domain: req.domain,
|
|
77
|
+
subdomain: req.subdomain,
|
|
78
|
+
overwrite: req.overwrite,
|
|
79
|
+
inbound: req.inbound && {
|
|
80
|
+
hostname: req.inbound.hostname || email.defaultInboundHostname(req.domain),
|
|
81
|
+
url: req.inbound.url,
|
|
82
|
+
spamCheck: req.inbound.spamCheck,
|
|
83
|
+
sendRaw: req.inbound.sendRaw,
|
|
84
|
+
},
|
|
85
|
+
...opts.setupDefaults,
|
|
86
|
+
propagation: { timeoutMs: (req.waitSeconds ?? 300) * 1000, ...opts.setupDefaults?.propagation },
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const routes = {
|
|
90
|
+
'GET /api/config': async (_req, res) => {
|
|
91
|
+
const awsFile = env.AWS_SHARED_CREDENTIALS_FILE ?? join(homedir(), '.aws', 'credentials');
|
|
92
|
+
sendJson(res, 200, {
|
|
93
|
+
version: opts.version,
|
|
94
|
+
providers: DNS_PROVIDERS,
|
|
95
|
+
emailProviders: EMAIL_PROVIDERS,
|
|
96
|
+
env: {
|
|
97
|
+
sendgrid: Boolean(env.SENDGRID_API_KEY),
|
|
98
|
+
postmark: Boolean(env.POSTMARK_ACCOUNT_TOKEN),
|
|
99
|
+
mailgun: Boolean(env.MAILGUN_API_KEY),
|
|
100
|
+
awsRegion: env.AWS_REGION || env.AWS_DEFAULT_REGION || null,
|
|
101
|
+
cloudflare: Boolean(env.CLOUDFLARE_API_TOKEN),
|
|
102
|
+
aws: Boolean(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY) || existsSync(awsFile),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
'POST /api/plan': async (req, res) => {
|
|
107
|
+
const body = parseSetupRequest(await readJson(req));
|
|
108
|
+
const { email, dns } = await buildProviders(body);
|
|
109
|
+
let domain;
|
|
110
|
+
let changes = [];
|
|
111
|
+
try {
|
|
112
|
+
await setupDomain(email, dns, {
|
|
113
|
+
...runOptions(body, email),
|
|
114
|
+
dryRun: true,
|
|
115
|
+
onEvent: (e) => {
|
|
116
|
+
if (e.type === 'domain')
|
|
117
|
+
domain = e.auth;
|
|
118
|
+
if (e.type === 'plan')
|
|
119
|
+
changes = e.changes;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (!(err instanceof DnsConflictError))
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
sendJson(res, 200, { domain, changes, conflicts: changes.some((c) => c.action === 'conflict') });
|
|
128
|
+
},
|
|
129
|
+
'POST /api/setup': async (req, res) => {
|
|
130
|
+
const body = parseSetupRequest(await readJson(req));
|
|
131
|
+
const { email, dns } = await buildProviders(body);
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
res.on('close', () => {
|
|
134
|
+
if (!res.writableFinished)
|
|
135
|
+
controller.abort();
|
|
136
|
+
});
|
|
137
|
+
res.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store' });
|
|
138
|
+
const write = (line) => res.write(JSON.stringify(line) + '\n');
|
|
139
|
+
try {
|
|
140
|
+
const result = await setupDomain(email, dns, {
|
|
141
|
+
...runOptions(body, email),
|
|
142
|
+
onEvent: (event) => write({ type: 'event', event }),
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
});
|
|
145
|
+
write({ type: 'result', result });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!controller.signal.aborted)
|
|
149
|
+
write({ type: 'error', ...serializeError(err) });
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
res.end();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
'POST /api/status': async (req, res) => {
|
|
156
|
+
const body = parseSetupRequest(await readJson(req), { requireDns: false });
|
|
157
|
+
const { email } = await buildProviders({ ...body, dns: 'manual' });
|
|
158
|
+
sendJson(res, 200, await checkStatus(email, body.domain, body.subdomain));
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const server = createServer(async (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
if (!hostAllowed(req.headers.host))
|
|
164
|
+
return sendText(res, 403, 'Forbidden host');
|
|
165
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
166
|
+
if (url.pathname === '/' && req.method === 'GET') {
|
|
167
|
+
const fromQuery = url.searchParams.get('token');
|
|
168
|
+
if (tokenMatches(fromQuery)) {
|
|
169
|
+
res.writeHead(303, {
|
|
170
|
+
location: '/',
|
|
171
|
+
'set-cookie': `${cookieName()}=${token}; HttpOnly; SameSite=Strict; Path=/`,
|
|
172
|
+
});
|
|
173
|
+
return void res.end();
|
|
174
|
+
}
|
|
175
|
+
if (!tokenMatches(cookie(req)))
|
|
176
|
+
return sendHtml(res, 401, LOCKED_PAGE);
|
|
177
|
+
return sendHtml(res, 200, page);
|
|
178
|
+
}
|
|
179
|
+
const route = routes[`${req.method} ${url.pathname}`];
|
|
180
|
+
if (!route)
|
|
181
|
+
return sendJson(res, 404, { error: 'Not found' });
|
|
182
|
+
if (!tokenMatches(cookie(req))) {
|
|
183
|
+
return sendJson(res, 401, { error: 'Session expired: open the link printed in the terminal again.' });
|
|
184
|
+
}
|
|
185
|
+
if (req.method === 'POST' && !req.headers['content-type']?.startsWith('application/json')) {
|
|
186
|
+
return sendJson(res, 415, { error: 'Expected application/json' });
|
|
187
|
+
}
|
|
188
|
+
await route(req, res);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (res.headersSent)
|
|
192
|
+
return void res.end();
|
|
193
|
+
const status = err instanceof ConfigError ? 400 : err instanceof ApiError ? 502 : 500;
|
|
194
|
+
sendJson(res, status, serializeError(err));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
await new Promise((resolve, reject) => {
|
|
198
|
+
server.once('error', reject);
|
|
199
|
+
server.listen(opts.port ?? 4545, '127.0.0.1', () => resolve());
|
|
200
|
+
});
|
|
201
|
+
port = server.address().port;
|
|
202
|
+
return {
|
|
203
|
+
url: `http://127.0.0.1:${port}/?token=${token}`,
|
|
204
|
+
port,
|
|
205
|
+
close: () => new Promise((resolve, reject) => {
|
|
206
|
+
server.closeAllConnections();
|
|
207
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function parseSetupRequest(input, { requireDns = true } = {}) {
|
|
212
|
+
if (!input || typeof input !== 'object')
|
|
213
|
+
throw new ConfigError('Expected a JSON object.');
|
|
214
|
+
const body = input;
|
|
215
|
+
const domain = typeof body.domain === 'string' ? normalizeName(body.domain) : '';
|
|
216
|
+
if (!isValidDomain(domain))
|
|
217
|
+
throw new ConfigError('Enter a valid domain, like example.com.');
|
|
218
|
+
if (requireDns && typeof body.dns !== 'string')
|
|
219
|
+
throw new ConfigError('Choose a DNS provider.');
|
|
220
|
+
const emailProvider = typeof body.emailProvider === 'string' && body.emailProvider ? body.emailProvider : 'sendgrid';
|
|
221
|
+
if (!isEmailProvider(emailProvider))
|
|
222
|
+
throw new ConfigError(`Email provider must be one of: ${EMAIL_PROVIDERS.join(', ')}.`);
|
|
223
|
+
let inbound;
|
|
224
|
+
if (body.inbound) {
|
|
225
|
+
const url = String(body.inbound.url ?? '');
|
|
226
|
+
if (!/^https:\/\/[^\s]+$/.test(url))
|
|
227
|
+
throw new ConfigError('The inbound webhook URL must start with https://.');
|
|
228
|
+
const hostname = body.inbound.hostname ? normalizeName(String(body.inbound.hostname)) : undefined;
|
|
229
|
+
if (hostname && !isValidDomain(hostname))
|
|
230
|
+
throw new ConfigError('Enter a valid inbound hostname.');
|
|
231
|
+
inbound = { url, hostname, spamCheck: Boolean(body.inbound.spamCheck), sendRaw: Boolean(body.inbound.sendRaw) };
|
|
232
|
+
}
|
|
233
|
+
const waitSeconds = body.waitSeconds === undefined ? undefined : Number(body.waitSeconds);
|
|
234
|
+
if (waitSeconds !== undefined && !(waitSeconds >= 0 && waitSeconds <= 3600)) {
|
|
235
|
+
throw new ConfigError('Wait time must be between 0 and 3600 seconds.');
|
|
236
|
+
}
|
|
237
|
+
const credentials = {};
|
|
238
|
+
for (const [k, v] of Object.entries(body.credentials ?? {})) {
|
|
239
|
+
if (typeof v === 'string' && v.trim())
|
|
240
|
+
credentials[k] = v.trim();
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
domain,
|
|
244
|
+
subdomain: typeof body.subdomain === 'string' && body.subdomain.trim() ? body.subdomain.trim() : undefined,
|
|
245
|
+
dns: String(body.dns ?? 'manual'),
|
|
246
|
+
emailProvider,
|
|
247
|
+
onBehalfOf: typeof body.onBehalfOf === 'string' ? body.onBehalfOf.trim() : undefined,
|
|
248
|
+
credentials,
|
|
249
|
+
inbound,
|
|
250
|
+
overwrite: Boolean(body.overwrite),
|
|
251
|
+
waitSeconds,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function serializeError(err) {
|
|
255
|
+
if (err instanceof DnsConflictError)
|
|
256
|
+
return { kind: 'conflict', error: err.message, conflicts: err.conflicts };
|
|
257
|
+
if (err instanceof ApiError) {
|
|
258
|
+
return { kind: 'api', error: err.message, provider: err.provider, status: err.status, hint: apiErrorHint(err) };
|
|
259
|
+
}
|
|
260
|
+
if (err instanceof ConfigError)
|
|
261
|
+
return { kind: 'config', error: err.message };
|
|
262
|
+
return { kind: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
263
|
+
}
|
|
264
|
+
async function readJson(req) {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
let size = 0;
|
|
267
|
+
for await (const chunk of req) {
|
|
268
|
+
size += chunk.length;
|
|
269
|
+
if (size > MAX_BODY_BYTES)
|
|
270
|
+
throw new ConfigError('Request body too large.');
|
|
271
|
+
chunks.push(chunk);
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
throw new ConfigError('Request body is not valid JSON.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function sendJson(res, status, body) {
|
|
281
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
282
|
+
res.end(JSON.stringify(body));
|
|
283
|
+
}
|
|
284
|
+
function sendText(res, status, text) {
|
|
285
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
286
|
+
res.end(text);
|
|
287
|
+
}
|
|
288
|
+
function sendHtml(res, status, html) {
|
|
289
|
+
res.writeHead(status, {
|
|
290
|
+
'content-type': 'text/html; charset=utf-8',
|
|
291
|
+
'cache-control': 'no-store',
|
|
292
|
+
'content-security-policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'",
|
|
293
|
+
'referrer-policy': 'no-referrer',
|
|
294
|
+
});
|
|
295
|
+
res.end(html);
|
|
296
|
+
}
|
|
297
|
+
const LOCKED_PAGE = `<!doctype html><meta charset="utf-8"><title>DKIMkit</title>
|
|
298
|
+
<body style="font:16px system-ui;max-width:32rem;margin:15vh auto;padding:0 16px;color:#333">
|
|
299
|
+
<h1 style="font-size:1.25rem">Open the link from your terminal</h1>
|
|
300
|
+
<p>For security, this page only opens from the one-time link printed by <code>dkimkit ui</code>.</p></body>`;
|
|
301
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type RecordType = 'CNAME' | 'TXT' | 'MX';
|
|
2
|
+
export interface DnsRecord {
|
|
3
|
+
type: RecordType;
|
|
4
|
+
/** Fully qualified name, lowercase, no trailing dot. */
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
/** MX only. */
|
|
8
|
+
priority?: number;
|
|
9
|
+
/** What the record is for (e.g. "dkim1", "inbound_mx"). Display only. */
|
|
10
|
+
purpose?: string;
|
|
11
|
+
/** Guidance for people adding it by hand (e.g. merging into an existing SPF record). */
|
|
12
|
+
note?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DomainAuthentication {
|
|
15
|
+
id: string;
|
|
16
|
+
domain: string;
|
|
17
|
+
valid: boolean;
|
|
18
|
+
records: DnsRecord[];
|
|
19
|
+
/** True when this run created it, false when an existing one was reused. */
|
|
20
|
+
created: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface RecordValidation {
|
|
23
|
+
record: string;
|
|
24
|
+
valid: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
results: RecordValidation[];
|
|
30
|
+
}
|
|
31
|
+
export interface InboundParseOptions {
|
|
32
|
+
hostname: string;
|
|
33
|
+
url: string;
|
|
34
|
+
spamCheck?: boolean;
|
|
35
|
+
sendRaw?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export type InboundAction = 'created' | 'updated' | 'unchanged';
|
|
38
|
+
/** An email service provider (SendGrid, Postmark, Mailgun, Amazon SES) that issues DNS records for a sending domain. */
|
|
39
|
+
export interface EmailProvider {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Human-readable name, e.g. "Amazon SES". */
|
|
42
|
+
readonly label: string;
|
|
43
|
+
/** Whether inbound email (receiving and forwarding to a webhook) is supported. */
|
|
44
|
+
readonly supportsInbound: boolean;
|
|
45
|
+
/** Where inbound mail is received when the caller doesn't choose (e.g. parse.<domain>). */
|
|
46
|
+
defaultInboundHostname(domain: string): string;
|
|
47
|
+
findDomain(domain: string, subdomain?: string): Promise<DomainAuthentication | null>;
|
|
48
|
+
/** Returns the existing authentication for the domain, or creates one. */
|
|
49
|
+
ensureDomain(domain: string, opts?: {
|
|
50
|
+
subdomain?: string;
|
|
51
|
+
}): Promise<DomainAuthentication>;
|
|
52
|
+
getDomain(id: string): Promise<DomainAuthentication>;
|
|
53
|
+
deleteDomain(id: string): Promise<void>;
|
|
54
|
+
validateDomain(id: string): Promise<ValidationResult>;
|
|
55
|
+
/** DNS records needed to route inbound mail for `hostname` to the provider. */
|
|
56
|
+
inboundRecords(hostname: string): DnsRecord[];
|
|
57
|
+
ensureInboundParse(opts: InboundParseOptions): Promise<InboundAction>;
|
|
58
|
+
deleteInboundParse(hostname: string): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export type ChangeAction = 'create' | 'update' | 'noop' | 'conflict';
|
|
61
|
+
export interface PlannedChange {
|
|
62
|
+
record: DnsRecord;
|
|
63
|
+
action: ChangeAction;
|
|
64
|
+
/** Provider record id to update, for `update`. */
|
|
65
|
+
existingId?: string;
|
|
66
|
+
reason?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A DNS host that can write records. */
|
|
69
|
+
export interface DnsProvider {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
plan(records: DnsRecord[], opts?: {
|
|
72
|
+
overwrite?: boolean;
|
|
73
|
+
}): Promise<PlannedChange[]>;
|
|
74
|
+
apply(changes: PlannedChange[]): Promise<void>;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Inner content of every `<tag>…</tag>`. Assumes the tag doesn't nest inside itself. */
|
|
2
|
+
export declare function xmlTags(xml: string, tag: string): string[];
|
|
3
|
+
/** Decoded text of the first `<tag>`, if present. */
|
|
4
|
+
export declare function xmlText(xml: string, tag: string): string | undefined;
|
|
5
|
+
export declare function decodeXml(s: string): string;
|
|
6
|
+
export declare function encodeXml(s: string): string;
|
package/dist/src/xml.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Just enough XML for the flat, well-known shapes of the Route53 API.
|
|
2
|
+
/** Inner content of every `<tag>…</tag>`. Assumes the tag doesn't nest inside itself. */
|
|
3
|
+
export function xmlTags(xml, tag) {
|
|
4
|
+
const re = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, 'g');
|
|
5
|
+
return [...xml.matchAll(re)].map((m) => m[1]);
|
|
6
|
+
}
|
|
7
|
+
/** Decoded text of the first `<tag>`, if present. */
|
|
8
|
+
export function xmlText(xml, tag) {
|
|
9
|
+
const inner = xmlTags(xml, tag)[0];
|
|
10
|
+
return inner === undefined ? undefined : decodeXml(inner.trim());
|
|
11
|
+
}
|
|
12
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
13
|
+
export function decodeXml(s) {
|
|
14
|
+
return s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e) => {
|
|
15
|
+
if (e[0] === '#')
|
|
16
|
+
return String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1)));
|
|
17
|
+
return ENTITIES[e] ?? m;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function encodeXml(s) {
|
|
21
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=xml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../../src/xml.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,yFAAyF;AACzF,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,GAAW;IAC9C,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,+BAA+B,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;AACjD,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,GAAW;IAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,QAAQ,GAA2B,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAE9F,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,CAAC,CAAC,OAAO,CAAC,4BAA4B,EAAE,CAAC,CAAC,EAAE,CAAS,EAAE,EAAE;QAC9D,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5H,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC9H,CAAC"}
|
package/docs/billing.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Plans and billing
|
|
2
|
+
|
|
3
|
+
Start free, and pay only for what you use once you grow past the free allowance.
|
|
4
|
+
|
|
5
|
+
| | Free | Pay as you go |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Price | Free, no card needed | First {{FREE_DOMAINS}} active domains free, then **{{PRICE}} per active domain per month** |
|
|
8
|
+
| Domains | Up to {{FREE_DOMAINS}} | Unlimited |
|
|
9
|
+
| Features | API, embeddable widget, Cloudflare, Route 53 and manual DNS, webhooks, usage reports | Everything in Free |
|
|
10
|
+
|
|
11
|
+
## What counts as an active domain
|
|
12
|
+
|
|
13
|
+
A domain is **active** in a billing period if it was verified on at least one day of that period. Each domain counts once per period, however many days it was active.
|
|
14
|
+
|
|
15
|
+
- A domain verified on the 3rd and deleted on the 10th counts once that month.
|
|
16
|
+
- Deleting a domain doesn't remove it from a period it was already active in.
|
|
17
|
+
- Domains that never verify, for example because the DNS records were never added, aren't counted.
|
|
18
|
+
|
|
19
|
+
**Example:** you have 8 active domains this month. The first {{FREE_DOMAINS}} are free, so you pay for the rest, at {{PRICE}} each.
|
|
20
|
+
|
|
21
|
+
## Upgrading
|
|
22
|
+
|
|
23
|
+
Sign in to your [dashboard](/dashboard) and choose **Add payment method**. You'll go to our payment partner's secure checkout page. Once it's done, the domain cap is lifted immediately.
|
|
24
|
+
|
|
25
|
+
On the free plan, creating more than {{FREE_DOMAINS}} domains returns `402 plan_limit_reached` from the API, with an `upgrade_url` that points to your dashboard. Your existing domains keep working either way.
|
|
26
|
+
|
|
27
|
+
## Your bill
|
|
28
|
+
|
|
29
|
+
You're billed monthly for the active domains in the previous period. Your dashboard shows this period's active domains and the estimated amount so far, and the [usage API](/docs/hosted-api#usage-and-billing) gives the same numbers, broken down by your own customers if you like.
|
|
30
|
+
|
|
31
|
+
## Cancelling
|
|
32
|
+
|
|
33
|
+
Cancel any time from **Manage billing** in the dashboard. Your existing domains keep working, and you go back to the free plan's limit for new domains. You're billed only for domains active during the period before you cancelled.
|
|
34
|
+
|
|
35
|
+
## If a payment fails
|
|
36
|
+
|
|
37
|
+
We keep your plan active while the payment is retried, and the dashboard asks you to update your payment method. If the payment can't be collected, the subscription ends and the account returns to the free plan.
|
|
38
|
+
|
|
39
|
+
## Payments and tax
|
|
40
|
+
|
|
41
|
+
Payments are processed by [Dodo Payments](https://dodopayments.com), our merchant of record. Dodo is the seller on your invoice, charges your payment method, and handles VAT, GST and sales tax for your country. Your card details go to Dodo and never reach our servers. Invoices and payment methods are available from **Manage billing** in the dashboard.
|
|
42
|
+
|
|
43
|
+
Questions about a charge? See our [refund policy](/refunds) or [contact us](/contact).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
{{APP_NAME}} authenticates your customers' sending domains with SendGrid, Postmark, Mailgun or Amazon SES. It creates the DNS records, writes them for your customers or shows them what to add, keeps checking until your email provider verifies the domain, and tells your app when each domain is ready. This guide takes you from sign-up to your first verified domain.
|
|
4
|
+
|
|
5
|
+
## 1. Create an account
|
|
6
|
+
|
|
7
|
+
[Sign up](/dashboard) with your email address. We email you a link; there's no password.{{#billing}} You start on the free plan, which includes {{FREE_DOMAINS}} domains.{{/billing}}
|
|
8
|
+
|
|
9
|
+
## 2. Connect your email provider
|
|
10
|
+
|
|
11
|
+
In the dashboard under **Settings**, choose your email provider and enter its credentials:
|
|
12
|
+
|
|
13
|
+
| Provider | What to enter |
|
|
14
|
+
|---|---|
|
|
15
|
+
| SendGrid | An API key with full **Sender Authentication** access, plus **Inbound Parse** to receive email |
|
|
16
|
+
| Postmark | An **account** API token (Account → API Tokens), plus a server ID if you want to receive email |
|
|
17
|
+
| Mailgun | A private API key and your account's region (US or EU) |
|
|
18
|
+
| Amazon SES | An IAM access key with SES permissions (create, get and delete identities, set MAIL FROM) and your SES region |
|
|
19
|
+
|
|
20
|
+
Credentials are encrypted at rest and never shown again. Each account uses one provider; to switch, delete your domains first.
|
|
21
|
+
|
|
22
|
+
Each provider asks for slightly different DNS records, and we handle the differences. For example, if a domain already has an SPF record, we add the provider to it rather than creating a second SPF record, which would break SPF.
|
|
23
|
+
|
|
24
|
+
### No code: add your domains in the dashboard
|
|
25
|
+
|
|
26
|
+
Bought domains at GoDaddy, Namecheap, Hostinger or anywhere else? Paste them into **Domains** in the dashboard, one per line, up to 100 at a time. Each one gets sender authentication at your email provider, plus inbound email if you tick **Also receive email** and enter the URL received mail should be posted to.
|
|
27
|
+
|
|
28
|
+
We detect where each domain's DNS is hosted. Domains on Cloudflare or Route 53 can have their records written for you, if you enter a Cloudflare API token or AWS keys under **Write DNS records automatically**. For every other domain, the dashboard lists the records to add, grouped by DNS host, with a CSV and a zone file you can import. We keep checking and verify each domain as soon as its records are live.
|
|
29
|
+
|
|
30
|
+
The steps below are for adding domains from your own app with the API.
|
|
31
|
+
|
|
32
|
+
## 3. Create an API key
|
|
33
|
+
|
|
34
|
+
Choose **Create new API key** in the dashboard and copy it. It's shown once. Use it from your backend as a bearer token:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export API_KEY=dk_live_…
|
|
38
|
+
curl {{PUBLIC_URL}}/v1/account -H "Authorization: Bearer $API_KEY"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 4. Register a domain
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
curl -X POST {{PUBLIC_URL}}/v1/domains \
|
|
45
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
46
|
+
-H "Content-Type: application/json" \
|
|
47
|
+
-d '{"domain": "customer.com", "tenant_id": "cus_42", "dns": {"provider": "manual"}}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The response includes the DNS records your customer needs to add. Then you have two options:
|
|
51
|
+
|
|
52
|
+
- **Show the records:** show them in your app, so your customer can add them at their DNS host. We detect when they're live.
|
|
53
|
+
- **Write them for the customer:** pass `"dns": {"provider": "cloudflare", "cloudflare_api_token": "…"}`, or the Route 53 equivalent, and we add the records to their DNS ourselves.
|
|
54
|
+
|
|
55
|
+
Or skip building UI entirely and use the [embeddable widget](/docs/widget), which walks your customers through it, including connecting Cloudflare or Route 53.
|
|
56
|
+
|
|
57
|
+
## 5. Find out when it's verified
|
|
58
|
+
|
|
59
|
+
Check `GET /v1/domains/{id}`, or better, set a **Webhook URL** in the dashboard and we'll POST a signed `domain.verified` event as soon as your email provider confirms the domain. We keep checking in the background, and re-check verified domains daily so you hear about it if a record is later removed.
|
|
60
|
+
|
|
61
|
+
## Next steps
|
|
62
|
+
|
|
63
|
+
- [API reference](/docs/hosted-api): every endpoint, the domain lifecycle, errors and webhooks
|
|
64
|
+
- [Embeddable widget](/docs/widget): let your customers connect their own DNS inside your app
|
|
65
|
+
- [Node.js library and CLI](/docs/library): run the same setup from your own code or terminal, with your own keys
|
|
66
|
+
{{#billing}}- [Plans and billing](/docs/billing): pricing and what counts as an active domain{{/billing}}
|