mailery 0.2.2 → 0.3.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 CHANGED
@@ -86,6 +86,7 @@ A self-hosted library you `npm install` into your Express + MongoDB app. Fire ev
86
86
  - **Both transactional and marketing in one engine.** Right defaults per kind (suppression scope, sender identity, circuit-breaker behavior).
87
87
  - **Provider-agnostic.** SendGrid + NullProvider ship; Postmark / SES / Resend pluggable.
88
88
  - **Pluggable queue** — BullMQ + Redis (default) or Agenda + Mongo (no Redis required).
89
+ - **`mailery setup-sendgrid` CLI** that wires up domain auth, Cloudflare DNS records, Signed Event Webhook key, and Event Webhook URL in one idempotent command. See [docs/guide/deliverability](https://jeffjassky.github.io/mailery/guide/deliverability#automated-setup-cloudflare).
89
90
  - **MJML** templates with click + open tracking, plain-text auto-derivation, scope-aware suppression at send time.
90
91
  - **React admin SPA** (Vite-bundled, served as static assets — no build step in your app).
91
92
 
package/dist/cli.cjs ADDED
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var readline = require('readline/promises');
5
+ var process$1 = require('process');
6
+
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
26
+
27
+ // src/cli/setup-sendgrid.ts
28
+ async function setupSendgrid(opts) {
29
+ const env = opts.env ?? process.env;
30
+ const log = opts.logger ?? console;
31
+ const info = (m) => log.log?.(m);
32
+ const warn = (m) => log.warn?.(m);
33
+ const f = opts.fetchFn ?? globalThis.fetch;
34
+ const sgKey = env.SENDGRID_API_KEY;
35
+ if (!sgKey) {
36
+ throw new Error(
37
+ 'SENDGRID_API_KEY env var is required.\n Create one at: https://app.sendgrid.com/settings/api_keys\n Needs at least: "Mail Settings" (Full Access) and "Sender Authentication" (Full Access).\n Then add to your shell rc (e.g. ~/.zshrc): export SENDGRID_API_KEY="SG.xxx"'
38
+ );
39
+ }
40
+ if (!/^SG\./.test(sgKey)) {
41
+ throw new Error(
42
+ 'SENDGRID_API_KEY does not look like a SendGrid API key (expected to start with "SG."). Double-check you copied the full key from https://app.sendgrid.com/settings/api_keys.'
43
+ );
44
+ }
45
+ if (opts.cloudflare && !env.CLOUDFLARE_API_TOKEN) {
46
+ throw new Error(
47
+ `CLOUDFLARE_API_TOKEN env var is required when --cloudflare is set.
48
+ Create one at: https://dash.cloudflare.com/profile/api-tokens
49
+ Use the "Edit zone DNS" template, restricted to the specific zone you're publishing into.
50
+ Permissions: Zone:Read + DNS:Edit.
51
+ Then add to your shell rc: export CLOUDFLARE_API_TOKEN="cf-xxx"`
52
+ );
53
+ }
54
+ if (!opts.domains || opts.domains.length === 0) {
55
+ throw new Error("At least one --domain is required");
56
+ }
57
+ const sg = sendgridClient(sgKey, f);
58
+ const cf = opts.cloudflare ? cloudflareClient(env.CLOUDFLARE_API_TOKEN, f) : null;
59
+ const domainResults = [];
60
+ for (const domain of opts.domains) {
61
+ info("");
62
+ info(`=== ${domain} ===`);
63
+ info(`SendGrid \u2192 checking domain authentication for ${domain}\u2026`);
64
+ const existing = await sg.findDomainAuth(domain);
65
+ let auth;
66
+ if (existing) {
67
+ info(` \u2192 reusing existing domain auth #${existing.id}`);
68
+ auth = existing;
69
+ } else {
70
+ info(` \u2192 creating new domain auth`);
71
+ auth = await sg.createDomainAuth({
72
+ domain,
73
+ subdomain: opts.subdomain ?? "em"
74
+ });
75
+ }
76
+ const dnsRecords = Object.values(auth.dns).map((r) => ({ type: r.type, host: r.host, data: r.data }));
77
+ let cloudflarePushed = 0;
78
+ if (cf) {
79
+ info(`Cloudflare \u2192 publishing ${dnsRecords.length} record(s)\u2026`);
80
+ const zoneName = opts.cloudflareZone ?? inferZone(domain);
81
+ const zoneId = await cf.findZoneId(zoneName);
82
+ if (!zoneId) {
83
+ throw new Error(`Cloudflare zone "${zoneName}" not found on this account`);
84
+ }
85
+ for (const rec of dnsRecords) {
86
+ const result = await cf.upsertRecord(zoneId, rec);
87
+ if (result === "created") {
88
+ cloudflarePushed++;
89
+ info(` + ${rec.type} ${rec.host} \u2192 ${rec.data}`);
90
+ } else if (result === "updated") {
91
+ cloudflarePushed++;
92
+ info(` ~ ${rec.type} ${rec.host} (updated to ${rec.data})`);
93
+ } else {
94
+ info(` = ${rec.type} ${rec.host} (already correct)`);
95
+ }
96
+ }
97
+ info("SendGrid \u2192 triggering validation\u2026");
98
+ const validated = await sg.validateDomainAuth(auth.id);
99
+ if (validated) {
100
+ info(" \u2713 domain authenticated");
101
+ } else {
102
+ warn(" ! validation not yet successful \u2014 DNS may still be propagating. Re-run in a few minutes.");
103
+ }
104
+ auth.valid = validated;
105
+ } else {
106
+ info("\u2014 skipping Cloudflare publish (no --cloudflare). Add these records to your DNS provider:");
107
+ for (const rec of dnsRecords) {
108
+ info(` ${rec.type.padEnd(7)} ${rec.host.padEnd(50)} ${rec.data}`);
109
+ }
110
+ }
111
+ domainResults.push({
112
+ domain,
113
+ domainAuthId: auth.id,
114
+ alreadyValidated: auth.valid,
115
+ dnsRecords,
116
+ cloudflarePushed
117
+ });
118
+ }
119
+ info("");
120
+ info("=== Event webhook (account-level) ===");
121
+ info("SendGrid \u2192 checking Signed Event Webhook\u2026");
122
+ const signed = await sg.getSignedWebhookSettings();
123
+ let webhookKey;
124
+ if (signed.public_key) {
125
+ info(" = signing already enabled; reusing existing public key");
126
+ webhookKey = signed.public_key;
127
+ } else {
128
+ info(" \u2192 enabling signing");
129
+ webhookKey = await sg.enableSignedWebhook();
130
+ info(" \u2713 verification key fetched");
131
+ }
132
+ info("SendGrid \u2192 checking Event Webhook\u2026");
133
+ const currentEvents = await sg.getEventWebhookSettings();
134
+ const desiredEvents = {
135
+ url: opts.webhookUrl,
136
+ enabled: true,
137
+ delivered: true,
138
+ open: true,
139
+ click: true,
140
+ bounce: true,
141
+ dropped: true,
142
+ spam_report: true,
143
+ unsubscribe: true,
144
+ deferred: false,
145
+ processed: false,
146
+ group_resubscribe: false,
147
+ group_unsubscribe: false
148
+ };
149
+ if (currentEvents.url && currentEvents.url !== opts.webhookUrl && !opts.force) {
150
+ throw new Error(
151
+ `Event webhook is already configured for ${currentEvents.url}. Pass --force to overwrite (this will redirect all future events to ${opts.webhookUrl}).`
152
+ );
153
+ }
154
+ if (eventSettingsMatch(currentEvents, desiredEvents)) {
155
+ info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`);
156
+ } else {
157
+ await sg.setEventWebhookSettings(desiredEvents);
158
+ info(` \u2713 webhook URL set to ${opts.webhookUrl}`);
159
+ }
160
+ const envSnippet = [
161
+ `# Add to your environment (or .env file):`,
162
+ `SENDGRID_WEBHOOK_VERIFICATION_KEY="${webhookKey.replace(/\n/g, "\\n")}"`
163
+ ].join("\n");
164
+ info("");
165
+ info("--- Done. Add to your environment ---");
166
+ info(envSnippet);
167
+ return {
168
+ domains: domainResults,
169
+ webhookKey,
170
+ webhookUrl: opts.webhookUrl,
171
+ envSnippet
172
+ };
173
+ }
174
+ function sendgridClient(apiKey, fetchFn) {
175
+ const base = "https://api.sendgrid.com/v3";
176
+ const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
177
+ async function call(path, init = {}) {
178
+ const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
179
+ const text = await res.text();
180
+ const body = text ? safeJson(text) : null;
181
+ if (res.status === 401 || res.status === 403) {
182
+ throw new Error(
183
+ `SendGrid rejected the request (HTTP ${res.status}). Your SENDGRID_API_KEY is missing the required permissions or is invalid. The key needs "Mail Settings" (Full Access) and "Sender Authentication" (Full Access). Manage keys at https://app.sendgrid.com/settings/api_keys.`
184
+ );
185
+ }
186
+ if (!res.ok) {
187
+ const message = body?.errors?.[0]?.message ?? text ?? res.statusText;
188
+ throw new Error(`SendGrid ${init.method ?? "GET"} ${path} \u2192 ${res.status}: ${message}`);
189
+ }
190
+ return body;
191
+ }
192
+ return {
193
+ async findDomainAuth(domain) {
194
+ const list = await call(`/whitelabel/domains?limit=200`) ?? [];
195
+ return list.find((d) => d.domain === domain) ?? null;
196
+ },
197
+ async createDomainAuth({ domain, subdomain }) {
198
+ return call(`/whitelabel/domains`, {
199
+ method: "POST",
200
+ body: JSON.stringify({ domain, subdomain, automatic_security: true })
201
+ });
202
+ },
203
+ async validateDomainAuth(id) {
204
+ const r = await call(`/whitelabel/domains/${id}/validate`, { method: "POST" });
205
+ return Boolean(r?.valid);
206
+ },
207
+ async getSignedWebhookSettings() {
208
+ return call(`/user/webhooks/event/settings/signed`);
209
+ },
210
+ async enableSignedWebhook() {
211
+ await call(`/user/webhooks/event/settings/signed`, {
212
+ method: "PATCH",
213
+ body: JSON.stringify({ enabled: true })
214
+ });
215
+ const r = await call(`/user/webhooks/event/settings/signed`);
216
+ if (!r?.public_key) throw new Error("SendGrid did not return a public_key for the signed event webhook");
217
+ return r.public_key;
218
+ },
219
+ async getEventWebhookSettings() {
220
+ return call(`/user/webhooks/event/settings`);
221
+ },
222
+ async setEventWebhookSettings(payload) {
223
+ await call(`/user/webhooks/event/settings`, {
224
+ method: "PATCH",
225
+ body: JSON.stringify(payload)
226
+ });
227
+ }
228
+ };
229
+ }
230
+ function cloudflareClient(apiToken, fetchFn) {
231
+ const base = "https://api.cloudflare.com/client/v4";
232
+ const headers = { authorization: `Bearer ${apiToken}`, "content-type": "application/json" };
233
+ async function call(path, init = {}) {
234
+ const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
235
+ const body = await res.json().catch(() => null);
236
+ if (res.status === 401 || res.status === 403) {
237
+ throw new Error(
238
+ `Cloudflare rejected the request (HTTP ${res.status}). Your CLOUDFLARE_API_TOKEN is invalid, expired, or missing the required permissions. The token needs Zone:Read + DNS:Edit on the specific zone you're publishing into. Manage tokens at https://dash.cloudflare.com/profile/api-tokens.`
239
+ );
240
+ }
241
+ if (!res.ok || body && body.success === false) {
242
+ const message = body?.errors?.[0]?.message ?? res.statusText;
243
+ throw new Error(`Cloudflare ${init.method ?? "GET"} ${path} \u2192 ${res.status}: ${message}`);
244
+ }
245
+ return body;
246
+ }
247
+ return {
248
+ async findZoneId(name) {
249
+ const r = await call(`/zones?name=${encodeURIComponent(name)}`);
250
+ return r?.result?.[0]?.id ?? null;
251
+ },
252
+ async upsertRecord(zoneId, rec) {
253
+ const existing = await call(
254
+ `/zones/${zoneId}/dns_records?type=${encodeURIComponent(rec.type)}&name=${encodeURIComponent(rec.host)}`
255
+ );
256
+ const match = existing?.result?.[0];
257
+ const desiredBody = {
258
+ type: rec.type,
259
+ name: rec.host,
260
+ content: rec.data,
261
+ ttl: 1,
262
+ // auto
263
+ proxied: false
264
+ // critical: SendGrid DKIM CNAMEs must NOT be proxied
265
+ };
266
+ if (!match) {
267
+ await call(`/zones/${zoneId}/dns_records`, { method: "POST", body: JSON.stringify(desiredBody) });
268
+ return "created";
269
+ }
270
+ if (match.content === rec.data && match.proxied === false) {
271
+ return "noop";
272
+ }
273
+ await call(`/zones/${zoneId}/dns_records/${match.id}`, {
274
+ method: "PUT",
275
+ body: JSON.stringify(desiredBody)
276
+ });
277
+ return "updated";
278
+ }
279
+ };
280
+ }
281
+ function inferZone(domain) {
282
+ const labels = domain.split(".").filter(Boolean);
283
+ if (labels.length <= 2) return domain;
284
+ return labels.slice(-2).join(".");
285
+ }
286
+ function safeJson(s) {
287
+ try {
288
+ return JSON.parse(s);
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+ function eventSettingsMatch(current, desired) {
294
+ for (const [k, v] of Object.entries(desired)) {
295
+ if (current[k] !== v) return false;
296
+ }
297
+ return true;
298
+ }
299
+ function makeReadlinePrompter() {
300
+ const rl = readline__namespace.createInterface({ input: process$1.stdin, output: process$1.stdout });
301
+ const prompter = async (q, opts) => {
302
+ const suffix = opts?.defaultValue ? ` [${opts.defaultValue}]` : "";
303
+ const raw = await rl.question(`${q}${suffix}: `);
304
+ const trimmed = raw.trim();
305
+ return trimmed.length === 0 && opts?.defaultValue ? opts.defaultValue : trimmed;
306
+ };
307
+ return { prompter, close: () => rl.close() };
308
+ }
309
+ async function runInteractive(prompter, env, logger = console) {
310
+ const log = (m) => logger.log(m);
311
+ const exportLines = [];
312
+ const envOverrides = {};
313
+ log("");
314
+ log("mailery setup-sendgrid (interactive)");
315
+ log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
316
+ log("");
317
+ if (!env.SENDGRID_API_KEY) {
318
+ log("SENDGRID_API_KEY is not set in your environment.");
319
+ log(" Create one at https://app.sendgrid.com/settings/api_keys");
320
+ log(" Permissions: Mail Settings (Full Access) + Sender Authentication (Full Access).");
321
+ log("");
322
+ const key = await prompter("Paste your SendGrid API key (starts with SG.)");
323
+ if (!key) throw new Error("SENDGRID_API_KEY is required");
324
+ envOverrides.SENDGRID_API_KEY = key;
325
+ exportLines.push(`export SENDGRID_API_KEY="${key}"`);
326
+ }
327
+ log("");
328
+ log("Which sender domain(s) do you want to authenticate?");
329
+ log(" Examples:");
330
+ log(" news.example.com (one)");
331
+ log(" news.example.com, mail.example.com (two \u2014 comma-separated, in one line)");
332
+ log(" Splitting marketing from transactional onto separate domains is recommended.");
333
+ log("");
334
+ const domainsRaw = await prompter("Domains");
335
+ const domains = domainsRaw.split(",").map((d) => d.trim()).filter(Boolean);
336
+ if (domains.length === 0) throw new Error("at least one --domain is required");
337
+ const defaultWebhook = `https://${apex(domains[0])}/m/webhooks/sendgrid`;
338
+ log("");
339
+ log("SendGrid will POST event webhooks to this URL.");
340
+ log(" This must be reachable from the public internet (not behind admin auth).");
341
+ log("");
342
+ const webhookUrl = await prompter("Webhook URL", { defaultValue: defaultWebhook });
343
+ log("");
344
+ const cloudflareRaw = await prompter("Publish DNS records via Cloudflare API? (y/N)", { defaultValue: "n" });
345
+ const cloudflare = /^y/i.test(cloudflareRaw);
346
+ let cloudflareZone;
347
+ if (cloudflare) {
348
+ if (!env.CLOUDFLARE_API_TOKEN) {
349
+ log("");
350
+ log("CLOUDFLARE_API_TOKEN is not set in your environment.");
351
+ log(" Create one at https://dash.cloudflare.com/profile/api-tokens");
352
+ log(' Use the "Edit zone DNS" template, restricted to your zone(s).');
353
+ log(" Permissions: Zone:Read + DNS:Edit.");
354
+ log("");
355
+ const token = await prompter("Paste your Cloudflare API token");
356
+ if (!token) throw new Error("CLOUDFLARE_API_TOKEN is required when publishing via Cloudflare");
357
+ envOverrides.CLOUDFLARE_API_TOKEN = token;
358
+ exportLines.push(`export CLOUDFLARE_API_TOKEN="${token}"`);
359
+ }
360
+ log("");
361
+ const zoneOverride = await prompter("Override Cloudflare zone (blank to infer per-domain)", { defaultValue: "" });
362
+ if (zoneOverride.length > 0) cloudflareZone = zoneOverride;
363
+ }
364
+ log("");
365
+ const forceRaw = await prompter(
366
+ "If a different webhook URL is already configured on this SendGrid account, overwrite it? (y/N)",
367
+ { defaultValue: "n" }
368
+ );
369
+ const force = /^y/i.test(forceRaw);
370
+ log("");
371
+ log("About to run setup with:");
372
+ log(` domains: ${domains.join(", ")}`);
373
+ log(` webhook URL: ${webhookUrl}`);
374
+ log(` cloudflare: ${cloudflare ? "yes" : "no"}`);
375
+ if (cloudflareZone) log(` cloudflare zone: ${cloudflareZone}`);
376
+ log(` force: ${force ? "yes" : "no"}`);
377
+ log("");
378
+ const confirmRaw = await prompter("Proceed? (Y/n)", { defaultValue: "y" });
379
+ const proceed = !/^n/i.test(confirmRaw);
380
+ return {
381
+ opts: { domains, webhookUrl, cloudflare, cloudflareZone, force },
382
+ envOverrides,
383
+ exportLines,
384
+ proceed
385
+ };
386
+ }
387
+ function apex(domain) {
388
+ const labels = domain.split(".").filter(Boolean);
389
+ if (labels.length <= 2) return domain;
390
+ return labels.slice(-2).join(".");
391
+ }
392
+
393
+ // src/cli/index.ts
394
+ async function main() {
395
+ const argv = process.argv.slice(2);
396
+ const cmd = argv[0];
397
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
398
+ printUsage();
399
+ process.exit(cmd ? 0 : 1);
400
+ }
401
+ switch (cmd) {
402
+ case "setup-sendgrid": {
403
+ const opts = parseArgs(argv.slice(1));
404
+ const argDomains = collectStringList(opts.domain);
405
+ const argWebhook = typeof opts["webhook-url"] === "string" ? opts["webhook-url"] : null;
406
+ const explicitInteractive = Boolean(opts.interactive);
407
+ const explicitNonInteractive = Boolean(opts["no-interactive"]);
408
+ const wantsInteractive = explicitInteractive || !explicitNonInteractive && argDomains.length === 0 && !argWebhook && Boolean(process.stdin.isTTY);
409
+ try {
410
+ if (wantsInteractive) {
411
+ const { prompter, close } = makeReadlinePrompter();
412
+ let interactive;
413
+ try {
414
+ interactive = await runInteractive(prompter, process.env);
415
+ } finally {
416
+ close();
417
+ }
418
+ if (!interactive.proceed) {
419
+ console.log("Aborted.");
420
+ process.exit(0);
421
+ }
422
+ await setupSendgrid({
423
+ ...interactive.opts,
424
+ env: { ...process.env, ...interactive.envOverrides }
425
+ });
426
+ if (interactive.exportLines.length > 0) {
427
+ console.log("");
428
+ console.log("Add these to your shell rc (e.g. ~/.zshrc) so future runs find them:");
429
+ for (const line of interactive.exportLines) console.log(` ${line}`);
430
+ }
431
+ } else {
432
+ if (argDomains.length === 0) {
433
+ console.error("error: --domain is required (repeat or comma-separate for multiple, or run without args for interactive mode)");
434
+ printUsage();
435
+ process.exit(2);
436
+ }
437
+ if (!argWebhook) {
438
+ console.error("error: --webhook-url is required");
439
+ printUsage();
440
+ process.exit(2);
441
+ }
442
+ await setupSendgrid({
443
+ domains: argDomains,
444
+ subdomain: opts.subdomain ? String(opts.subdomain) : void 0,
445
+ webhookUrl: argWebhook,
446
+ cloudflare: Boolean(opts.cloudflare),
447
+ cloudflareZone: opts["cloudflare-zone"] ? String(opts["cloudflare-zone"]) : void 0,
448
+ force: Boolean(opts.force)
449
+ });
450
+ }
451
+ } catch (err) {
452
+ console.error(`
453
+ \u2717 ${err?.message ?? err}`);
454
+ process.exit(1);
455
+ }
456
+ break;
457
+ }
458
+ default:
459
+ console.error(`unknown command: ${cmd}`);
460
+ printUsage();
461
+ process.exit(2);
462
+ }
463
+ }
464
+ function parseArgs(argv) {
465
+ const out = {};
466
+ function setKey(key, value) {
467
+ const existing = out[key];
468
+ if (existing === void 0) {
469
+ out[key] = value;
470
+ } else if (Array.isArray(existing)) {
471
+ if (typeof value === "string") existing.push(value);
472
+ } else if (typeof existing === "string" && typeof value === "string") {
473
+ out[key] = [existing, value];
474
+ } else {
475
+ out[key] = value;
476
+ }
477
+ }
478
+ for (let i = 0; i < argv.length; i++) {
479
+ const tok = argv[i];
480
+ if (!tok.startsWith("--")) continue;
481
+ const eq = tok.indexOf("=");
482
+ if (eq > 0) {
483
+ setKey(tok.slice(2, eq), tok.slice(eq + 1));
484
+ continue;
485
+ }
486
+ const key = tok.slice(2);
487
+ const next = argv[i + 1];
488
+ if (next && !next.startsWith("--")) {
489
+ setKey(key, next);
490
+ i++;
491
+ } else {
492
+ setKey(key, true);
493
+ }
494
+ }
495
+ return out;
496
+ }
497
+ function collectStringList(value) {
498
+ if (value === void 0 || value === true || value === false) return [];
499
+ const raw = Array.isArray(value) ? value : [value];
500
+ return raw.flatMap((s) => String(s).split(",")).map((s) => s.trim()).filter(Boolean);
501
+ }
502
+ function printUsage() {
503
+ console.log(`
504
+ mailery CLI
505
+
506
+ Usage:
507
+ mailery setup-sendgrid (interactive \u2014 prompts you for everything)
508
+ mailery setup-sendgrid --domain <d>[,<d>...] --webhook-url <u> [options] (non-interactive)
509
+
510
+ Options:
511
+ --domain <d> Domain to authenticate. Repeat (or comma-separate)
512
+ to authenticate multiple in one run, e.g. when
513
+ isolating marketing from transactional. The event
514
+ webhook itself is account-level and only gets set up
515
+ once.
516
+ --subdomain <s> Sub-label for the link branding CNAME (default: em)
517
+ --webhook-url <u> Public URL for the SendGrid event webhook
518
+ --cloudflare Publish DNS records via the Cloudflare API
519
+ --cloudflare-zone <z> Override the inferred zone (e.g. for multi-label
520
+ public suffixes like .co.uk)
521
+ --force Replace an existing event webhook URL if one is set
522
+ --interactive Force interactive mode even when args are supplied
523
+ --no-interactive Disable interactive mode even when args are missing
524
+ (the script exits with usage instead of prompting \u2014
525
+ intended for CI / scripts)
526
+
527
+ Env vars:
528
+ SENDGRID_API_KEY Required. See https://app.sendgrid.com/settings/api_keys
529
+ Permissions: Mail Settings + Sender Authentication.
530
+ CLOUDFLARE_API_TOKEN Required when --cloudflare is set. See
531
+ https://dash.cloudflare.com/profile/api-tokens
532
+ Permissions: Zone:Read + DNS:Edit on the zone(s)
533
+ being published into.
534
+
535
+ Examples:
536
+ # one domain
537
+ mailery setup-sendgrid \\
538
+ --domain news.example.com \\
539
+ --webhook-url https://example.com/m/webhooks/sendgrid \\
540
+ --cloudflare
541
+
542
+ # marketing + transactional in one shot
543
+ mailery setup-sendgrid \\
544
+ --domain news.example.com \\
545
+ --domain mail.example.com \\
546
+ --webhook-url https://example.com/m/webhooks/sendgrid \\
547
+ --cloudflare
548
+ `);
549
+ }
550
+ main();
551
+ //# sourceMappingURL=cli.cjs.map
552
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/setup-sendgrid.ts","../src/cli/interactive.ts","../src/cli/index.ts"],"names":["readline","input","output"],"mappings":"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","file":"cli.cjs","sourcesContent":["/**\n * `mailery setup-sendgrid` — one-shot wiring of SendGrid for a domain.\n *\n * Idempotent. Re-running converges: existing domain auths are reused, existing\n * webhook config is preserved unless `--force` is passed, existing Cloudflare\n * DNS records with the correct values are left alone.\n *\n * npx mailery setup-sendgrid \\\n * --domain news.example.com \\\n * --webhook-url https://example.com/m/webhooks/sendgrid \\\n * --cloudflare\n *\n * Required env:\n * SENDGRID_API_KEY — full-access (or at minimum Mail Settings + Sender Auth)\n * CLOUDFLARE_API_TOKEN — only if --cloudflare; needs Zone:Read + DNS:Edit on the zone\n */\n\nexport interface SetupSendgridOpts {\n /** One or more domains to authenticate, e.g. [\"news.example.com\", \"mail.example.com\"]. */\n domains: string[]\n /** Sub-label SendGrid uses for the link branding CNAME (default 'em'). */\n subdomain?: string\n /** Where SendGrid should POST event webhooks. Account-level — only configured once regardless of domain count. */\n webhookUrl: string\n /** Publish DNS records via Cloudflare API. Requires CLOUDFLARE_API_TOKEN. */\n cloudflare?: boolean\n /** Override the parent zone if any domain isn't a subdomain of its eTLD+1. */\n cloudflareZone?: string\n /** Replace an existing webhook URL if one is already set. */\n force?: boolean\n /** Logger; defaults to console. Pass {} to silence. */\n logger?: { log?: (...a: unknown[]) => void; warn?: (...a: unknown[]) => void }\n /** Override fetch (for tests). */\n fetchFn?: typeof fetch\n /** Override env (for tests). */\n env?: Record<string, string | undefined>\n}\n\nexport interface DomainSetupResult {\n domain: string\n domainAuthId: number\n alreadyValidated: boolean\n dnsRecords: Array<{ type: string; host: string; data: string }>\n cloudflarePushed: number\n}\n\nexport interface SetupSendgridResult {\n domains: DomainSetupResult[]\n webhookKey: string\n webhookUrl: string\n envSnippet: string\n}\n\ninterface SgDomainAuth {\n id: number\n domain: string\n subdomain: string\n username: string\n valid: boolean\n dns: Record<string, { host: string; type: string; data: string; valid: boolean }>\n}\n\nexport async function setupSendgrid(opts: SetupSendgridOpts): Promise<SetupSendgridResult> {\n const env = opts.env ?? process.env\n const log = opts.logger ?? console\n const info = (m: string) => log.log?.(m)\n const warn = (m: string) => log.warn?.(m)\n const f = opts.fetchFn ?? globalThis.fetch\n\n const sgKey = env.SENDGRID_API_KEY\n if (!sgKey) {\n throw new Error(\n 'SENDGRID_API_KEY env var is required.\\n' +\n ' Create one at: https://app.sendgrid.com/settings/api_keys\\n' +\n ' Needs at least: \"Mail Settings\" (Full Access) and \"Sender Authentication\" (Full Access).\\n' +\n ' Then add to your shell rc (e.g. ~/.zshrc): export SENDGRID_API_KEY=\"SG.xxx\"',\n )\n }\n if (!/^SG\\./.test(sgKey)) {\n throw new Error(\n 'SENDGRID_API_KEY does not look like a SendGrid API key (expected to start with \"SG.\"). ' +\n 'Double-check you copied the full key from https://app.sendgrid.com/settings/api_keys.',\n )\n }\n\n if (opts.cloudflare && !env.CLOUDFLARE_API_TOKEN) {\n throw new Error(\n 'CLOUDFLARE_API_TOKEN env var is required when --cloudflare is set.\\n' +\n ' Create one at: https://dash.cloudflare.com/profile/api-tokens\\n' +\n ' Use the \"Edit zone DNS\" template, restricted to the specific zone you\\'re publishing into.\\n' +\n ' Permissions: Zone:Read + DNS:Edit.\\n' +\n ' Then add to your shell rc: export CLOUDFLARE_API_TOKEN=\"cf-xxx\"',\n )\n }\n\n if (!opts.domains || opts.domains.length === 0) {\n throw new Error('At least one --domain is required')\n }\n\n const sg = sendgridClient(sgKey, f)\n const cf = opts.cloudflare ? cloudflareClient(env.CLOUDFLARE_API_TOKEN!, f) : null\n\n // ---- Per-domain: authentication + DNS publish ---------------------------\n const domainResults: DomainSetupResult[] = []\n for (const domain of opts.domains) {\n info('')\n info(`=== ${domain} ===`)\n info(`SendGrid → checking domain authentication for ${domain}…`)\n const existing = await sg.findDomainAuth(domain)\n let auth: SgDomainAuth\n if (existing) {\n info(` → reusing existing domain auth #${existing.id}`)\n auth = existing\n } else {\n info(` → creating new domain auth`)\n auth = await sg.createDomainAuth({\n domain,\n subdomain: opts.subdomain ?? 'em',\n })\n }\n\n const dnsRecords = Object.values(auth.dns).map((r) => ({ type: r.type, host: r.host, data: r.data }))\n let cloudflarePushed = 0\n\n if (cf) {\n info(`Cloudflare → publishing ${dnsRecords.length} record(s)…`)\n const zoneName = opts.cloudflareZone ?? inferZone(domain)\n const zoneId = await cf.findZoneId(zoneName)\n if (!zoneId) {\n throw new Error(`Cloudflare zone \"${zoneName}\" not found on this account`)\n }\n for (const rec of dnsRecords) {\n const result = await cf.upsertRecord(zoneId, rec)\n if (result === 'created') {\n cloudflarePushed++\n info(` + ${rec.type} ${rec.host} → ${rec.data}`)\n } else if (result === 'updated') {\n cloudflarePushed++\n info(` ~ ${rec.type} ${rec.host} (updated to ${rec.data})`)\n } else {\n info(` = ${rec.type} ${rec.host} (already correct)`)\n }\n }\n\n info('SendGrid → triggering validation…')\n const validated = await sg.validateDomainAuth(auth.id)\n if (validated) {\n info(' ✓ domain authenticated')\n } else {\n warn(' ! validation not yet successful — DNS may still be propagating. Re-run in a few minutes.')\n }\n auth.valid = validated\n } else {\n info('— skipping Cloudflare publish (no --cloudflare). Add these records to your DNS provider:')\n for (const rec of dnsRecords) {\n info(` ${rec.type.padEnd(7)} ${rec.host.padEnd(50)} ${rec.data}`)\n }\n }\n\n domainResults.push({\n domain,\n domainAuthId: auth.id,\n alreadyValidated: auth.valid,\n dnsRecords,\n cloudflarePushed,\n })\n }\n\n info('')\n info('=== Event webhook (account-level) ===')\n\n // ---- Account-level: Enable Signed Event Webhook + fetch public key -----\n info('SendGrid → checking Signed Event Webhook…')\n // SendGrid's GET /user/webhooks/event/settings/signed only returns\n // `{ public_key }`. A non-empty key implies signing is already enabled —\n // re-PATCHing `enabled: true` rotates the keypair, silently breaking\n // signature verification everywhere the old key was deployed.\n const signed = await sg.getSignedWebhookSettings()\n let webhookKey: string\n if (signed.public_key) {\n info(' = signing already enabled; reusing existing public key')\n webhookKey = signed.public_key\n } else {\n info(' → enabling signing')\n webhookKey = await sg.enableSignedWebhook()\n info(' ✓ verification key fetched')\n }\n\n // ---- Step 4: Configure Event Webhook URL + event toggles ----------------\n info('SendGrid → checking Event Webhook…')\n const currentEvents = await sg.getEventWebhookSettings()\n const desiredEvents = {\n url: opts.webhookUrl,\n enabled: true,\n delivered: true,\n open: true,\n click: true,\n bounce: true,\n dropped: true,\n spam_report: true,\n unsubscribe: true,\n deferred: false,\n processed: false,\n group_resubscribe: false,\n group_unsubscribe: false,\n }\n if (currentEvents.url && currentEvents.url !== opts.webhookUrl && !opts.force) {\n throw new Error(\n `Event webhook is already configured for ${currentEvents.url}. ` +\n `Pass --force to overwrite (this will redirect all future events to ${opts.webhookUrl}).`,\n )\n }\n if (eventSettingsMatch(currentEvents, desiredEvents)) {\n info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`)\n } else {\n await sg.setEventWebhookSettings(desiredEvents)\n info(` ✓ webhook URL set to ${opts.webhookUrl}`)\n }\n\n const envSnippet = [\n `# Add to your environment (or .env file):`,\n `SENDGRID_WEBHOOK_VERIFICATION_KEY=\"${webhookKey.replace(/\\n/g, '\\\\n')}\"`,\n ].join('\\n')\n\n info('')\n info('--- Done. Add to your environment ---')\n info(envSnippet)\n\n return {\n domains: domainResults,\n webhookKey,\n webhookUrl: opts.webhookUrl,\n envSnippet,\n }\n}\n\n// ---------------------------------------------------------------------------\n// SendGrid API client\n// ---------------------------------------------------------------------------\n\ninterface SendGridClient {\n findDomainAuth(domain: string): Promise<SgDomainAuth | null>\n createDomainAuth(input: { domain: string; subdomain: string }): Promise<SgDomainAuth>\n validateDomainAuth(id: number): Promise<boolean>\n getSignedWebhookSettings(): Promise<{ enabled?: boolean; public_key?: string }>\n enableSignedWebhook(): Promise<string>\n getEventWebhookSettings(): Promise<{ url?: string; enabled?: boolean; [k: string]: unknown }>\n setEventWebhookSettings(payload: Record<string, unknown>): Promise<void>\n}\n\nfunction sendgridClient(apiKey: string, fetchFn: typeof fetch): SendGridClient {\n const base = 'https://api.sendgrid.com/v3'\n const headers = { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }\n\n async function call(path: string, init: RequestInit = {}): Promise<any> {\n const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...(init.headers as any) } })\n const text = await res.text()\n const body = text ? safeJson(text) : null\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `SendGrid rejected the request (HTTP ${res.status}). Your SENDGRID_API_KEY is missing the required permissions ` +\n 'or is invalid. The key needs \"Mail Settings\" (Full Access) and \"Sender Authentication\" (Full Access). ' +\n 'Manage keys at https://app.sendgrid.com/settings/api_keys.',\n )\n }\n if (!res.ok) {\n const message = body?.errors?.[0]?.message ?? text ?? res.statusText\n throw new Error(`SendGrid ${init.method ?? 'GET'} ${path} → ${res.status}: ${message}`)\n }\n return body\n }\n\n return {\n async findDomainAuth(domain: string) {\n const list: SgDomainAuth[] = (await call(`/whitelabel/domains?limit=200`)) ?? []\n return list.find((d) => d.domain === domain) ?? null\n },\n async createDomainAuth({ domain, subdomain }) {\n return call(`/whitelabel/domains`, {\n method: 'POST',\n body: JSON.stringify({ domain, subdomain, automatic_security: true }),\n }) as Promise<SgDomainAuth>\n },\n async validateDomainAuth(id: number) {\n const r = await call(`/whitelabel/domains/${id}/validate`, { method: 'POST' })\n return Boolean(r?.valid)\n },\n async getSignedWebhookSettings() {\n return call(`/user/webhooks/event/settings/signed`)\n },\n async enableSignedWebhook() {\n await call(`/user/webhooks/event/settings/signed`, {\n method: 'PATCH',\n body: JSON.stringify({ enabled: true }),\n })\n const r = await call(`/user/webhooks/event/settings/signed`)\n if (!r?.public_key) throw new Error('SendGrid did not return a public_key for the signed event webhook')\n return r.public_key as string\n },\n async getEventWebhookSettings() {\n return call(`/user/webhooks/event/settings`)\n },\n async setEventWebhookSettings(payload) {\n await call(`/user/webhooks/event/settings`, {\n method: 'PATCH',\n body: JSON.stringify(payload),\n })\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloudflare API client\n// ---------------------------------------------------------------------------\n\ninterface CloudflareClient {\n findZoneId(name: string): Promise<string | null>\n upsertRecord(zoneId: string, rec: { type: string; host: string; data: string }): Promise<'created' | 'updated' | 'noop'>\n}\n\nfunction cloudflareClient(apiToken: string, fetchFn: typeof fetch): CloudflareClient {\n const base = 'https://api.cloudflare.com/client/v4'\n const headers = { authorization: `Bearer ${apiToken}`, 'content-type': 'application/json' }\n\n async function call(path: string, init: RequestInit = {}): Promise<any> {\n const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...(init.headers as any) } })\n const body: any = await res.json().catch(() => null)\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `Cloudflare rejected the request (HTTP ${res.status}). Your CLOUDFLARE_API_TOKEN is invalid, expired, ` +\n 'or missing the required permissions. The token needs Zone:Read + DNS:Edit on the specific zone you\\'re ' +\n 'publishing into. Manage tokens at https://dash.cloudflare.com/profile/api-tokens.',\n )\n }\n if (!res.ok || (body && body.success === false)) {\n const message = body?.errors?.[0]?.message ?? res.statusText\n throw new Error(`Cloudflare ${init.method ?? 'GET'} ${path} → ${res.status}: ${message}`)\n }\n return body\n }\n\n return {\n async findZoneId(name: string) {\n const r = await call(`/zones?name=${encodeURIComponent(name)}`)\n return r?.result?.[0]?.id ?? null\n },\n async upsertRecord(zoneId, rec) {\n // CF requires the host to be a FQDN, which SendGrid already gives us.\n const existing = await call(\n `/zones/${zoneId}/dns_records?type=${encodeURIComponent(rec.type)}&name=${encodeURIComponent(rec.host)}`,\n )\n const match = existing?.result?.[0]\n const desiredBody = {\n type: rec.type,\n name: rec.host,\n content: rec.data,\n ttl: 1, // auto\n proxied: false, // critical: SendGrid DKIM CNAMEs must NOT be proxied\n }\n if (!match) {\n await call(`/zones/${zoneId}/dns_records`, { method: 'POST', body: JSON.stringify(desiredBody) })\n return 'created'\n }\n if (match.content === rec.data && match.proxied === false) {\n return 'noop'\n }\n await call(`/zones/${zoneId}/dns_records/${match.id}`, {\n method: 'PUT',\n body: JSON.stringify(desiredBody),\n })\n return 'updated'\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Best-effort eTLD+1 inference. Falls back to last two labels — fine for most\n * `.com` / `.io` style domains. For multi-label public suffixes (`.co.uk`)\n * pass `--cloudflare-zone` explicitly. */\nfunction inferZone(domain: string): string {\n const labels = domain.split('.').filter(Boolean)\n if (labels.length <= 2) return domain\n return labels.slice(-2).join('.')\n}\n\nfunction safeJson(s: string): any {\n try { return JSON.parse(s) } catch { return null }\n}\n\n/** Compare current SendGrid event webhook settings to the desired payload.\n * Treats missing keys on `current` as \"not yet set\" rather than equal-to-false,\n * so a partially-configured webhook still triggers a PATCH. */\nfunction eventSettingsMatch(current: Record<string, unknown>, desired: Record<string, unknown>): boolean {\n for (const [k, v] of Object.entries(desired)) {\n if (current[k] !== v) return false\n }\n return true\n}\n","/**\n * Interactive wizard for `mailery setup-sendgrid`. Walks the user through:\n * - Required env vars (offers to paste them in, then prints export lines)\n * - Domains (accept multiple via repeated entry or comma-separation)\n * - Webhook URL (with a default derived from the first domain's apex)\n * - Cloudflare toggle + zone override\n * - Force flag\n *\n * Renders a summary before any API call and asks for confirmation. All\n * questions are funneled through a `Prompter` so tests can inject canned\n * answers without touching stdin.\n */\n\nimport * as readline from 'node:readline/promises'\nimport { stdin as input, stdout as output } from 'node:process'\n\nimport type { SetupSendgridOpts } from './setup-sendgrid.js'\n\nexport type Prompter = (question: string, opts?: { defaultValue?: string }) => Promise<string>\n\nexport interface InteractiveResult {\n opts: Omit<SetupSendgridOpts, 'fetchFn' | 'logger' | 'env'>\n envOverrides: Record<string, string>\n exportLines: string[]\n proceed: boolean\n}\n\nexport function makeReadlinePrompter(): { prompter: Prompter; close: () => void } {\n const rl = readline.createInterface({ input, output })\n const prompter: Prompter = async (q, opts) => {\n const suffix = opts?.defaultValue ? ` [${opts.defaultValue}]` : ''\n const raw = await rl.question(`${q}${suffix}: `)\n const trimmed = raw.trim()\n return trimmed.length === 0 && opts?.defaultValue ? opts.defaultValue : trimmed\n }\n return { prompter, close: () => rl.close() }\n}\n\nexport async function runInteractive(\n prompter: Prompter,\n env: Record<string, string | undefined>,\n logger: { log: (...a: unknown[]) => void } = console,\n): Promise<InteractiveResult> {\n const log = (m: string) => logger.log(m)\n const exportLines: string[] = []\n const envOverrides: Record<string, string> = {}\n\n log('')\n log('mailery setup-sendgrid (interactive)')\n log('────────────────────────────────────')\n log('')\n\n // ---- SendGrid API key ---------------------------------------------------\n if (!env.SENDGRID_API_KEY) {\n log('SENDGRID_API_KEY is not set in your environment.')\n log(' Create one at https://app.sendgrid.com/settings/api_keys')\n log(' Permissions: Mail Settings (Full Access) + Sender Authentication (Full Access).')\n log('')\n const key = await prompter('Paste your SendGrid API key (starts with SG.)')\n if (!key) throw new Error('SENDGRID_API_KEY is required')\n envOverrides.SENDGRID_API_KEY = key\n exportLines.push(`export SENDGRID_API_KEY=\"${key}\"`)\n }\n\n // ---- Domains ------------------------------------------------------------\n log('')\n log('Which sender domain(s) do you want to authenticate?')\n log(' Examples:')\n log(' news.example.com (one)')\n log(' news.example.com, mail.example.com (two — comma-separated, in one line)')\n log(' Splitting marketing from transactional onto separate domains is recommended.')\n log('')\n const domainsRaw = await prompter('Domains')\n const domains = domainsRaw\n .split(',')\n .map((d) => d.trim())\n .filter(Boolean)\n if (domains.length === 0) throw new Error('at least one --domain is required')\n\n // ---- Webhook URL --------------------------------------------------------\n const defaultWebhook = `https://${apex(domains[0]!)}/m/webhooks/sendgrid`\n log('')\n log('SendGrid will POST event webhooks to this URL.')\n log(' This must be reachable from the public internet (not behind admin auth).')\n log('')\n const webhookUrl = await prompter('Webhook URL', { defaultValue: defaultWebhook })\n\n // ---- Cloudflare ---------------------------------------------------------\n log('')\n const cloudflareRaw = await prompter('Publish DNS records via Cloudflare API? (y/N)', { defaultValue: 'n' })\n const cloudflare = /^y/i.test(cloudflareRaw)\n let cloudflareZone: string | undefined\n\n if (cloudflare) {\n if (!env.CLOUDFLARE_API_TOKEN) {\n log('')\n log('CLOUDFLARE_API_TOKEN is not set in your environment.')\n log(' Create one at https://dash.cloudflare.com/profile/api-tokens')\n log(' Use the \"Edit zone DNS\" template, restricted to your zone(s).')\n log(' Permissions: Zone:Read + DNS:Edit.')\n log('')\n const token = await prompter('Paste your Cloudflare API token')\n if (!token) throw new Error('CLOUDFLARE_API_TOKEN is required when publishing via Cloudflare')\n envOverrides.CLOUDFLARE_API_TOKEN = token\n exportLines.push(`export CLOUDFLARE_API_TOKEN=\"${token}\"`)\n }\n log('')\n const zoneOverride = await prompter('Override Cloudflare zone (blank to infer per-domain)', { defaultValue: '' })\n if (zoneOverride.length > 0) cloudflareZone = zoneOverride\n }\n\n // ---- Force --------------------------------------------------------------\n log('')\n const forceRaw = await prompter(\n 'If a different webhook URL is already configured on this SendGrid account, overwrite it? (y/N)',\n { defaultValue: 'n' },\n )\n const force = /^y/i.test(forceRaw)\n\n // ---- Summary + confirm --------------------------------------------------\n log('')\n log('About to run setup with:')\n log(` domains: ${domains.join(', ')}`)\n log(` webhook URL: ${webhookUrl}`)\n log(` cloudflare: ${cloudflare ? 'yes' : 'no'}`)\n if (cloudflareZone) log(` cloudflare zone: ${cloudflareZone}`)\n log(` force: ${force ? 'yes' : 'no'}`)\n log('')\n const confirmRaw = await prompter('Proceed? (Y/n)', { defaultValue: 'y' })\n const proceed = !/^n/i.test(confirmRaw)\n\n return {\n opts: { domains, webhookUrl, cloudflare, cloudflareZone, force },\n envOverrides,\n exportLines,\n proceed,\n }\n}\n\n/** Strip a leading sub-label so we get the apex (best-effort for webhook URL default). */\nfunction apex(domain: string): string {\n const labels = domain.split('.').filter(Boolean)\n if (labels.length <= 2) return domain\n return labels.slice(-2).join('.')\n}\n","#!/usr/bin/env node\n/**\n * mailery CLI entry. Currently exposes a single command:\n *\n * npx mailery setup-sendgrid --domain X --webhook-url Y [--cloudflare]\n *\n * See src/cli/setup-sendgrid.ts for the full option list.\n */\n\nimport { setupSendgrid } from './setup-sendgrid.js'\nimport { makeReadlinePrompter, runInteractive } from './interactive.js'\n\nasync function main() {\n const argv = process.argv.slice(2)\n const cmd = argv[0]\n\n if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {\n printUsage()\n process.exit(cmd ? 0 : 1)\n }\n\n switch (cmd) {\n case 'setup-sendgrid': {\n const opts = parseArgs(argv.slice(1))\n const argDomains = collectStringList(opts.domain)\n const argWebhook = typeof opts['webhook-url'] === 'string' ? (opts['webhook-url'] as string) : null\n const explicitInteractive = Boolean(opts.interactive)\n const explicitNonInteractive = Boolean(opts['no-interactive'])\n\n // Decide interactive vs args. Interactive when: --interactive, OR no\n // required args provided AND we have a TTY. Force non-interactive with\n // --no-interactive (useful for CI).\n const wantsInteractive =\n explicitInteractive ||\n (!explicitNonInteractive && argDomains.length === 0 && !argWebhook && Boolean(process.stdin.isTTY))\n\n try {\n if (wantsInteractive) {\n const { prompter, close } = makeReadlinePrompter()\n let interactive\n try {\n interactive = await runInteractive(prompter, process.env)\n } finally {\n close()\n }\n if (!interactive.proceed) {\n console.log('Aborted.')\n process.exit(0)\n }\n await setupSendgrid({\n ...interactive.opts,\n env: { ...process.env, ...interactive.envOverrides },\n })\n if (interactive.exportLines.length > 0) {\n console.log('')\n console.log('Add these to your shell rc (e.g. ~/.zshrc) so future runs find them:')\n for (const line of interactive.exportLines) console.log(` ${line}`)\n }\n } else {\n if (argDomains.length === 0) {\n console.error('error: --domain is required (repeat or comma-separate for multiple, or run without args for interactive mode)')\n printUsage()\n process.exit(2)\n }\n if (!argWebhook) {\n console.error('error: --webhook-url is required')\n printUsage()\n process.exit(2)\n }\n await setupSendgrid({\n domains: argDomains,\n subdomain: opts.subdomain ? String(opts.subdomain) : undefined,\n webhookUrl: argWebhook,\n cloudflare: Boolean(opts.cloudflare),\n cloudflareZone: opts['cloudflare-zone'] ? String(opts['cloudflare-zone']) : undefined,\n force: Boolean(opts.force),\n })\n }\n } catch (err: any) {\n console.error(`\\n✗ ${err?.message ?? err}`)\n process.exit(1)\n }\n break\n }\n default:\n console.error(`unknown command: ${cmd}`)\n printUsage()\n process.exit(2)\n }\n}\n\nfunction parseArgs(argv: string[]): Record<string, string | string[] | boolean> {\n const out: Record<string, string | string[] | boolean> = {}\n function setKey(key: string, value: string | boolean) {\n const existing = out[key]\n if (existing === undefined) {\n out[key] = value\n } else if (Array.isArray(existing)) {\n if (typeof value === 'string') existing.push(value)\n } else if (typeof existing === 'string' && typeof value === 'string') {\n out[key] = [existing, value]\n } else {\n out[key] = value\n }\n }\n for (let i = 0; i < argv.length; i++) {\n const tok = argv[i]!\n if (!tok.startsWith('--')) continue\n const eq = tok.indexOf('=')\n if (eq > 0) {\n setKey(tok.slice(2, eq), tok.slice(eq + 1))\n continue\n }\n const key = tok.slice(2)\n const next = argv[i + 1]\n if (next && !next.startsWith('--')) {\n setKey(key, next)\n i++\n } else {\n setKey(key, true)\n }\n }\n return out\n}\n\n/** Collapse a repeated/comma-separated flag value into a clean string[]. */\nfunction collectStringList(value: string | string[] | boolean | undefined): string[] {\n if (value === undefined || value === true || value === false) return []\n const raw = Array.isArray(value) ? value : [value]\n return raw\n .flatMap((s) => String(s).split(','))\n .map((s) => s.trim())\n .filter(Boolean)\n}\n\nfunction printUsage() {\n console.log(`\nmailery CLI\n\nUsage:\n mailery setup-sendgrid (interactive — prompts you for everything)\n mailery setup-sendgrid --domain <d>[,<d>...] --webhook-url <u> [options] (non-interactive)\n\nOptions:\n --domain <d> Domain to authenticate. Repeat (or comma-separate)\n to authenticate multiple in one run, e.g. when\n isolating marketing from transactional. The event\n webhook itself is account-level and only gets set up\n once.\n --subdomain <s> Sub-label for the link branding CNAME (default: em)\n --webhook-url <u> Public URL for the SendGrid event webhook\n --cloudflare Publish DNS records via the Cloudflare API\n --cloudflare-zone <z> Override the inferred zone (e.g. for multi-label\n public suffixes like .co.uk)\n --force Replace an existing event webhook URL if one is set\n --interactive Force interactive mode even when args are supplied\n --no-interactive Disable interactive mode even when args are missing\n (the script exits with usage instead of prompting —\n intended for CI / scripts)\n\nEnv vars:\n SENDGRID_API_KEY Required. See https://app.sendgrid.com/settings/api_keys\n Permissions: Mail Settings + Sender Authentication.\n CLOUDFLARE_API_TOKEN Required when --cloudflare is set. See\n https://dash.cloudflare.com/profile/api-tokens\n Permissions: Zone:Read + DNS:Edit on the zone(s)\n being published into.\n\nExamples:\n # one domain\n mailery setup-sendgrid \\\\\n --domain news.example.com \\\\\n --webhook-url https://example.com/m/webhooks/sendgrid \\\\\n --cloudflare\n\n # marketing + transactional in one shot\n mailery setup-sendgrid \\\\\n --domain news.example.com \\\\\n --domain mail.example.com \\\\\n --webhook-url https://example.com/m/webhooks/sendgrid \\\\\n --cloudflare\n`)\n}\n\nmain()\n"]}
package/dist/cli.js ADDED
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env node
2
+ import * as readline from 'readline/promises';
3
+ import { stdout, stdin } from 'process';
4
+
5
+ // src/cli/setup-sendgrid.ts
6
+ async function setupSendgrid(opts) {
7
+ const env = opts.env ?? process.env;
8
+ const log = opts.logger ?? console;
9
+ const info = (m) => log.log?.(m);
10
+ const warn = (m) => log.warn?.(m);
11
+ const f = opts.fetchFn ?? globalThis.fetch;
12
+ const sgKey = env.SENDGRID_API_KEY;
13
+ if (!sgKey) {
14
+ throw new Error(
15
+ 'SENDGRID_API_KEY env var is required.\n Create one at: https://app.sendgrid.com/settings/api_keys\n Needs at least: "Mail Settings" (Full Access) and "Sender Authentication" (Full Access).\n Then add to your shell rc (e.g. ~/.zshrc): export SENDGRID_API_KEY="SG.xxx"'
16
+ );
17
+ }
18
+ if (!/^SG\./.test(sgKey)) {
19
+ throw new Error(
20
+ 'SENDGRID_API_KEY does not look like a SendGrid API key (expected to start with "SG."). Double-check you copied the full key from https://app.sendgrid.com/settings/api_keys.'
21
+ );
22
+ }
23
+ if (opts.cloudflare && !env.CLOUDFLARE_API_TOKEN) {
24
+ throw new Error(
25
+ `CLOUDFLARE_API_TOKEN env var is required when --cloudflare is set.
26
+ Create one at: https://dash.cloudflare.com/profile/api-tokens
27
+ Use the "Edit zone DNS" template, restricted to the specific zone you're publishing into.
28
+ Permissions: Zone:Read + DNS:Edit.
29
+ Then add to your shell rc: export CLOUDFLARE_API_TOKEN="cf-xxx"`
30
+ );
31
+ }
32
+ if (!opts.domains || opts.domains.length === 0) {
33
+ throw new Error("At least one --domain is required");
34
+ }
35
+ const sg = sendgridClient(sgKey, f);
36
+ const cf = opts.cloudflare ? cloudflareClient(env.CLOUDFLARE_API_TOKEN, f) : null;
37
+ const domainResults = [];
38
+ for (const domain of opts.domains) {
39
+ info("");
40
+ info(`=== ${domain} ===`);
41
+ info(`SendGrid \u2192 checking domain authentication for ${domain}\u2026`);
42
+ const existing = await sg.findDomainAuth(domain);
43
+ let auth;
44
+ if (existing) {
45
+ info(` \u2192 reusing existing domain auth #${existing.id}`);
46
+ auth = existing;
47
+ } else {
48
+ info(` \u2192 creating new domain auth`);
49
+ auth = await sg.createDomainAuth({
50
+ domain,
51
+ subdomain: opts.subdomain ?? "em"
52
+ });
53
+ }
54
+ const dnsRecords = Object.values(auth.dns).map((r) => ({ type: r.type, host: r.host, data: r.data }));
55
+ let cloudflarePushed = 0;
56
+ if (cf) {
57
+ info(`Cloudflare \u2192 publishing ${dnsRecords.length} record(s)\u2026`);
58
+ const zoneName = opts.cloudflareZone ?? inferZone(domain);
59
+ const zoneId = await cf.findZoneId(zoneName);
60
+ if (!zoneId) {
61
+ throw new Error(`Cloudflare zone "${zoneName}" not found on this account`);
62
+ }
63
+ for (const rec of dnsRecords) {
64
+ const result = await cf.upsertRecord(zoneId, rec);
65
+ if (result === "created") {
66
+ cloudflarePushed++;
67
+ info(` + ${rec.type} ${rec.host} \u2192 ${rec.data}`);
68
+ } else if (result === "updated") {
69
+ cloudflarePushed++;
70
+ info(` ~ ${rec.type} ${rec.host} (updated to ${rec.data})`);
71
+ } else {
72
+ info(` = ${rec.type} ${rec.host} (already correct)`);
73
+ }
74
+ }
75
+ info("SendGrid \u2192 triggering validation\u2026");
76
+ const validated = await sg.validateDomainAuth(auth.id);
77
+ if (validated) {
78
+ info(" \u2713 domain authenticated");
79
+ } else {
80
+ warn(" ! validation not yet successful \u2014 DNS may still be propagating. Re-run in a few minutes.");
81
+ }
82
+ auth.valid = validated;
83
+ } else {
84
+ info("\u2014 skipping Cloudflare publish (no --cloudflare). Add these records to your DNS provider:");
85
+ for (const rec of dnsRecords) {
86
+ info(` ${rec.type.padEnd(7)} ${rec.host.padEnd(50)} ${rec.data}`);
87
+ }
88
+ }
89
+ domainResults.push({
90
+ domain,
91
+ domainAuthId: auth.id,
92
+ alreadyValidated: auth.valid,
93
+ dnsRecords,
94
+ cloudflarePushed
95
+ });
96
+ }
97
+ info("");
98
+ info("=== Event webhook (account-level) ===");
99
+ info("SendGrid \u2192 checking Signed Event Webhook\u2026");
100
+ const signed = await sg.getSignedWebhookSettings();
101
+ let webhookKey;
102
+ if (signed.public_key) {
103
+ info(" = signing already enabled; reusing existing public key");
104
+ webhookKey = signed.public_key;
105
+ } else {
106
+ info(" \u2192 enabling signing");
107
+ webhookKey = await sg.enableSignedWebhook();
108
+ info(" \u2713 verification key fetched");
109
+ }
110
+ info("SendGrid \u2192 checking Event Webhook\u2026");
111
+ const currentEvents = await sg.getEventWebhookSettings();
112
+ const desiredEvents = {
113
+ url: opts.webhookUrl,
114
+ enabled: true,
115
+ delivered: true,
116
+ open: true,
117
+ click: true,
118
+ bounce: true,
119
+ dropped: true,
120
+ spam_report: true,
121
+ unsubscribe: true,
122
+ deferred: false,
123
+ processed: false,
124
+ group_resubscribe: false,
125
+ group_unsubscribe: false
126
+ };
127
+ if (currentEvents.url && currentEvents.url !== opts.webhookUrl && !opts.force) {
128
+ throw new Error(
129
+ `Event webhook is already configured for ${currentEvents.url}. Pass --force to overwrite (this will redirect all future events to ${opts.webhookUrl}).`
130
+ );
131
+ }
132
+ if (eventSettingsMatch(currentEvents, desiredEvents)) {
133
+ info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`);
134
+ } else {
135
+ await sg.setEventWebhookSettings(desiredEvents);
136
+ info(` \u2713 webhook URL set to ${opts.webhookUrl}`);
137
+ }
138
+ const envSnippet = [
139
+ `# Add to your environment (or .env file):`,
140
+ `SENDGRID_WEBHOOK_VERIFICATION_KEY="${webhookKey.replace(/\n/g, "\\n")}"`
141
+ ].join("\n");
142
+ info("");
143
+ info("--- Done. Add to your environment ---");
144
+ info(envSnippet);
145
+ return {
146
+ domains: domainResults,
147
+ webhookKey,
148
+ webhookUrl: opts.webhookUrl,
149
+ envSnippet
150
+ };
151
+ }
152
+ function sendgridClient(apiKey, fetchFn) {
153
+ const base = "https://api.sendgrid.com/v3";
154
+ const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
155
+ async function call(path, init = {}) {
156
+ const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
157
+ const text = await res.text();
158
+ const body = text ? safeJson(text) : null;
159
+ if (res.status === 401 || res.status === 403) {
160
+ throw new Error(
161
+ `SendGrid rejected the request (HTTP ${res.status}). Your SENDGRID_API_KEY is missing the required permissions or is invalid. The key needs "Mail Settings" (Full Access) and "Sender Authentication" (Full Access). Manage keys at https://app.sendgrid.com/settings/api_keys.`
162
+ );
163
+ }
164
+ if (!res.ok) {
165
+ const message = body?.errors?.[0]?.message ?? text ?? res.statusText;
166
+ throw new Error(`SendGrid ${init.method ?? "GET"} ${path} \u2192 ${res.status}: ${message}`);
167
+ }
168
+ return body;
169
+ }
170
+ return {
171
+ async findDomainAuth(domain) {
172
+ const list = await call(`/whitelabel/domains?limit=200`) ?? [];
173
+ return list.find((d) => d.domain === domain) ?? null;
174
+ },
175
+ async createDomainAuth({ domain, subdomain }) {
176
+ return call(`/whitelabel/domains`, {
177
+ method: "POST",
178
+ body: JSON.stringify({ domain, subdomain, automatic_security: true })
179
+ });
180
+ },
181
+ async validateDomainAuth(id) {
182
+ const r = await call(`/whitelabel/domains/${id}/validate`, { method: "POST" });
183
+ return Boolean(r?.valid);
184
+ },
185
+ async getSignedWebhookSettings() {
186
+ return call(`/user/webhooks/event/settings/signed`);
187
+ },
188
+ async enableSignedWebhook() {
189
+ await call(`/user/webhooks/event/settings/signed`, {
190
+ method: "PATCH",
191
+ body: JSON.stringify({ enabled: true })
192
+ });
193
+ const r = await call(`/user/webhooks/event/settings/signed`);
194
+ if (!r?.public_key) throw new Error("SendGrid did not return a public_key for the signed event webhook");
195
+ return r.public_key;
196
+ },
197
+ async getEventWebhookSettings() {
198
+ return call(`/user/webhooks/event/settings`);
199
+ },
200
+ async setEventWebhookSettings(payload) {
201
+ await call(`/user/webhooks/event/settings`, {
202
+ method: "PATCH",
203
+ body: JSON.stringify(payload)
204
+ });
205
+ }
206
+ };
207
+ }
208
+ function cloudflareClient(apiToken, fetchFn) {
209
+ const base = "https://api.cloudflare.com/client/v4";
210
+ const headers = { authorization: `Bearer ${apiToken}`, "content-type": "application/json" };
211
+ async function call(path, init = {}) {
212
+ const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
213
+ const body = await res.json().catch(() => null);
214
+ if (res.status === 401 || res.status === 403) {
215
+ throw new Error(
216
+ `Cloudflare rejected the request (HTTP ${res.status}). Your CLOUDFLARE_API_TOKEN is invalid, expired, or missing the required permissions. The token needs Zone:Read + DNS:Edit on the specific zone you're publishing into. Manage tokens at https://dash.cloudflare.com/profile/api-tokens.`
217
+ );
218
+ }
219
+ if (!res.ok || body && body.success === false) {
220
+ const message = body?.errors?.[0]?.message ?? res.statusText;
221
+ throw new Error(`Cloudflare ${init.method ?? "GET"} ${path} \u2192 ${res.status}: ${message}`);
222
+ }
223
+ return body;
224
+ }
225
+ return {
226
+ async findZoneId(name) {
227
+ const r = await call(`/zones?name=${encodeURIComponent(name)}`);
228
+ return r?.result?.[0]?.id ?? null;
229
+ },
230
+ async upsertRecord(zoneId, rec) {
231
+ const existing = await call(
232
+ `/zones/${zoneId}/dns_records?type=${encodeURIComponent(rec.type)}&name=${encodeURIComponent(rec.host)}`
233
+ );
234
+ const match = existing?.result?.[0];
235
+ const desiredBody = {
236
+ type: rec.type,
237
+ name: rec.host,
238
+ content: rec.data,
239
+ ttl: 1,
240
+ // auto
241
+ proxied: false
242
+ // critical: SendGrid DKIM CNAMEs must NOT be proxied
243
+ };
244
+ if (!match) {
245
+ await call(`/zones/${zoneId}/dns_records`, { method: "POST", body: JSON.stringify(desiredBody) });
246
+ return "created";
247
+ }
248
+ if (match.content === rec.data && match.proxied === false) {
249
+ return "noop";
250
+ }
251
+ await call(`/zones/${zoneId}/dns_records/${match.id}`, {
252
+ method: "PUT",
253
+ body: JSON.stringify(desiredBody)
254
+ });
255
+ return "updated";
256
+ }
257
+ };
258
+ }
259
+ function inferZone(domain) {
260
+ const labels = domain.split(".").filter(Boolean);
261
+ if (labels.length <= 2) return domain;
262
+ return labels.slice(-2).join(".");
263
+ }
264
+ function safeJson(s) {
265
+ try {
266
+ return JSON.parse(s);
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+ function eventSettingsMatch(current, desired) {
272
+ for (const [k, v] of Object.entries(desired)) {
273
+ if (current[k] !== v) return false;
274
+ }
275
+ return true;
276
+ }
277
+ function makeReadlinePrompter() {
278
+ const rl = readline.createInterface({ input: stdin, output: stdout });
279
+ const prompter = async (q, opts) => {
280
+ const suffix = opts?.defaultValue ? ` [${opts.defaultValue}]` : "";
281
+ const raw = await rl.question(`${q}${suffix}: `);
282
+ const trimmed = raw.trim();
283
+ return trimmed.length === 0 && opts?.defaultValue ? opts.defaultValue : trimmed;
284
+ };
285
+ return { prompter, close: () => rl.close() };
286
+ }
287
+ async function runInteractive(prompter, env, logger = console) {
288
+ const log = (m) => logger.log(m);
289
+ const exportLines = [];
290
+ const envOverrides = {};
291
+ log("");
292
+ log("mailery setup-sendgrid (interactive)");
293
+ log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
294
+ log("");
295
+ if (!env.SENDGRID_API_KEY) {
296
+ log("SENDGRID_API_KEY is not set in your environment.");
297
+ log(" Create one at https://app.sendgrid.com/settings/api_keys");
298
+ log(" Permissions: Mail Settings (Full Access) + Sender Authentication (Full Access).");
299
+ log("");
300
+ const key = await prompter("Paste your SendGrid API key (starts with SG.)");
301
+ if (!key) throw new Error("SENDGRID_API_KEY is required");
302
+ envOverrides.SENDGRID_API_KEY = key;
303
+ exportLines.push(`export SENDGRID_API_KEY="${key}"`);
304
+ }
305
+ log("");
306
+ log("Which sender domain(s) do you want to authenticate?");
307
+ log(" Examples:");
308
+ log(" news.example.com (one)");
309
+ log(" news.example.com, mail.example.com (two \u2014 comma-separated, in one line)");
310
+ log(" Splitting marketing from transactional onto separate domains is recommended.");
311
+ log("");
312
+ const domainsRaw = await prompter("Domains");
313
+ const domains = domainsRaw.split(",").map((d) => d.trim()).filter(Boolean);
314
+ if (domains.length === 0) throw new Error("at least one --domain is required");
315
+ const defaultWebhook = `https://${apex(domains[0])}/m/webhooks/sendgrid`;
316
+ log("");
317
+ log("SendGrid will POST event webhooks to this URL.");
318
+ log(" This must be reachable from the public internet (not behind admin auth).");
319
+ log("");
320
+ const webhookUrl = await prompter("Webhook URL", { defaultValue: defaultWebhook });
321
+ log("");
322
+ const cloudflareRaw = await prompter("Publish DNS records via Cloudflare API? (y/N)", { defaultValue: "n" });
323
+ const cloudflare = /^y/i.test(cloudflareRaw);
324
+ let cloudflareZone;
325
+ if (cloudflare) {
326
+ if (!env.CLOUDFLARE_API_TOKEN) {
327
+ log("");
328
+ log("CLOUDFLARE_API_TOKEN is not set in your environment.");
329
+ log(" Create one at https://dash.cloudflare.com/profile/api-tokens");
330
+ log(' Use the "Edit zone DNS" template, restricted to your zone(s).');
331
+ log(" Permissions: Zone:Read + DNS:Edit.");
332
+ log("");
333
+ const token = await prompter("Paste your Cloudflare API token");
334
+ if (!token) throw new Error("CLOUDFLARE_API_TOKEN is required when publishing via Cloudflare");
335
+ envOverrides.CLOUDFLARE_API_TOKEN = token;
336
+ exportLines.push(`export CLOUDFLARE_API_TOKEN="${token}"`);
337
+ }
338
+ log("");
339
+ const zoneOverride = await prompter("Override Cloudflare zone (blank to infer per-domain)", { defaultValue: "" });
340
+ if (zoneOverride.length > 0) cloudflareZone = zoneOverride;
341
+ }
342
+ log("");
343
+ const forceRaw = await prompter(
344
+ "If a different webhook URL is already configured on this SendGrid account, overwrite it? (y/N)",
345
+ { defaultValue: "n" }
346
+ );
347
+ const force = /^y/i.test(forceRaw);
348
+ log("");
349
+ log("About to run setup with:");
350
+ log(` domains: ${domains.join(", ")}`);
351
+ log(` webhook URL: ${webhookUrl}`);
352
+ log(` cloudflare: ${cloudflare ? "yes" : "no"}`);
353
+ if (cloudflareZone) log(` cloudflare zone: ${cloudflareZone}`);
354
+ log(` force: ${force ? "yes" : "no"}`);
355
+ log("");
356
+ const confirmRaw = await prompter("Proceed? (Y/n)", { defaultValue: "y" });
357
+ const proceed = !/^n/i.test(confirmRaw);
358
+ return {
359
+ opts: { domains, webhookUrl, cloudflare, cloudflareZone, force },
360
+ envOverrides,
361
+ exportLines,
362
+ proceed
363
+ };
364
+ }
365
+ function apex(domain) {
366
+ const labels = domain.split(".").filter(Boolean);
367
+ if (labels.length <= 2) return domain;
368
+ return labels.slice(-2).join(".");
369
+ }
370
+
371
+ // src/cli/index.ts
372
+ async function main() {
373
+ const argv = process.argv.slice(2);
374
+ const cmd = argv[0];
375
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
376
+ printUsage();
377
+ process.exit(cmd ? 0 : 1);
378
+ }
379
+ switch (cmd) {
380
+ case "setup-sendgrid": {
381
+ const opts = parseArgs(argv.slice(1));
382
+ const argDomains = collectStringList(opts.domain);
383
+ const argWebhook = typeof opts["webhook-url"] === "string" ? opts["webhook-url"] : null;
384
+ const explicitInteractive = Boolean(opts.interactive);
385
+ const explicitNonInteractive = Boolean(opts["no-interactive"]);
386
+ const wantsInteractive = explicitInteractive || !explicitNonInteractive && argDomains.length === 0 && !argWebhook && Boolean(process.stdin.isTTY);
387
+ try {
388
+ if (wantsInteractive) {
389
+ const { prompter, close } = makeReadlinePrompter();
390
+ let interactive;
391
+ try {
392
+ interactive = await runInteractive(prompter, process.env);
393
+ } finally {
394
+ close();
395
+ }
396
+ if (!interactive.proceed) {
397
+ console.log("Aborted.");
398
+ process.exit(0);
399
+ }
400
+ await setupSendgrid({
401
+ ...interactive.opts,
402
+ env: { ...process.env, ...interactive.envOverrides }
403
+ });
404
+ if (interactive.exportLines.length > 0) {
405
+ console.log("");
406
+ console.log("Add these to your shell rc (e.g. ~/.zshrc) so future runs find them:");
407
+ for (const line of interactive.exportLines) console.log(` ${line}`);
408
+ }
409
+ } else {
410
+ if (argDomains.length === 0) {
411
+ console.error("error: --domain is required (repeat or comma-separate for multiple, or run without args for interactive mode)");
412
+ printUsage();
413
+ process.exit(2);
414
+ }
415
+ if (!argWebhook) {
416
+ console.error("error: --webhook-url is required");
417
+ printUsage();
418
+ process.exit(2);
419
+ }
420
+ await setupSendgrid({
421
+ domains: argDomains,
422
+ subdomain: opts.subdomain ? String(opts.subdomain) : void 0,
423
+ webhookUrl: argWebhook,
424
+ cloudflare: Boolean(opts.cloudflare),
425
+ cloudflareZone: opts["cloudflare-zone"] ? String(opts["cloudflare-zone"]) : void 0,
426
+ force: Boolean(opts.force)
427
+ });
428
+ }
429
+ } catch (err) {
430
+ console.error(`
431
+ \u2717 ${err?.message ?? err}`);
432
+ process.exit(1);
433
+ }
434
+ break;
435
+ }
436
+ default:
437
+ console.error(`unknown command: ${cmd}`);
438
+ printUsage();
439
+ process.exit(2);
440
+ }
441
+ }
442
+ function parseArgs(argv) {
443
+ const out = {};
444
+ function setKey(key, value) {
445
+ const existing = out[key];
446
+ if (existing === void 0) {
447
+ out[key] = value;
448
+ } else if (Array.isArray(existing)) {
449
+ if (typeof value === "string") existing.push(value);
450
+ } else if (typeof existing === "string" && typeof value === "string") {
451
+ out[key] = [existing, value];
452
+ } else {
453
+ out[key] = value;
454
+ }
455
+ }
456
+ for (let i = 0; i < argv.length; i++) {
457
+ const tok = argv[i];
458
+ if (!tok.startsWith("--")) continue;
459
+ const eq = tok.indexOf("=");
460
+ if (eq > 0) {
461
+ setKey(tok.slice(2, eq), tok.slice(eq + 1));
462
+ continue;
463
+ }
464
+ const key = tok.slice(2);
465
+ const next = argv[i + 1];
466
+ if (next && !next.startsWith("--")) {
467
+ setKey(key, next);
468
+ i++;
469
+ } else {
470
+ setKey(key, true);
471
+ }
472
+ }
473
+ return out;
474
+ }
475
+ function collectStringList(value) {
476
+ if (value === void 0 || value === true || value === false) return [];
477
+ const raw = Array.isArray(value) ? value : [value];
478
+ return raw.flatMap((s) => String(s).split(",")).map((s) => s.trim()).filter(Boolean);
479
+ }
480
+ function printUsage() {
481
+ console.log(`
482
+ mailery CLI
483
+
484
+ Usage:
485
+ mailery setup-sendgrid (interactive \u2014 prompts you for everything)
486
+ mailery setup-sendgrid --domain <d>[,<d>...] --webhook-url <u> [options] (non-interactive)
487
+
488
+ Options:
489
+ --domain <d> Domain to authenticate. Repeat (or comma-separate)
490
+ to authenticate multiple in one run, e.g. when
491
+ isolating marketing from transactional. The event
492
+ webhook itself is account-level and only gets set up
493
+ once.
494
+ --subdomain <s> Sub-label for the link branding CNAME (default: em)
495
+ --webhook-url <u> Public URL for the SendGrid event webhook
496
+ --cloudflare Publish DNS records via the Cloudflare API
497
+ --cloudflare-zone <z> Override the inferred zone (e.g. for multi-label
498
+ public suffixes like .co.uk)
499
+ --force Replace an existing event webhook URL if one is set
500
+ --interactive Force interactive mode even when args are supplied
501
+ --no-interactive Disable interactive mode even when args are missing
502
+ (the script exits with usage instead of prompting \u2014
503
+ intended for CI / scripts)
504
+
505
+ Env vars:
506
+ SENDGRID_API_KEY Required. See https://app.sendgrid.com/settings/api_keys
507
+ Permissions: Mail Settings + Sender Authentication.
508
+ CLOUDFLARE_API_TOKEN Required when --cloudflare is set. See
509
+ https://dash.cloudflare.com/profile/api-tokens
510
+ Permissions: Zone:Read + DNS:Edit on the zone(s)
511
+ being published into.
512
+
513
+ Examples:
514
+ # one domain
515
+ mailery setup-sendgrid \\
516
+ --domain news.example.com \\
517
+ --webhook-url https://example.com/m/webhooks/sendgrid \\
518
+ --cloudflare
519
+
520
+ # marketing + transactional in one shot
521
+ mailery setup-sendgrid \\
522
+ --domain news.example.com \\
523
+ --domain mail.example.com \\
524
+ --webhook-url https://example.com/m/webhooks/sendgrid \\
525
+ --cloudflare
526
+ `);
527
+ }
528
+ main();
529
+ //# sourceMappingURL=cli.js.map
530
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/setup-sendgrid.ts","../src/cli/interactive.ts","../src/cli/index.ts"],"names":["input","output"],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * `mailery setup-sendgrid` — one-shot wiring of SendGrid for a domain.\n *\n * Idempotent. Re-running converges: existing domain auths are reused, existing\n * webhook config is preserved unless `--force` is passed, existing Cloudflare\n * DNS records with the correct values are left alone.\n *\n * npx mailery setup-sendgrid \\\n * --domain news.example.com \\\n * --webhook-url https://example.com/m/webhooks/sendgrid \\\n * --cloudflare\n *\n * Required env:\n * SENDGRID_API_KEY — full-access (or at minimum Mail Settings + Sender Auth)\n * CLOUDFLARE_API_TOKEN — only if --cloudflare; needs Zone:Read + DNS:Edit on the zone\n */\n\nexport interface SetupSendgridOpts {\n /** One or more domains to authenticate, e.g. [\"news.example.com\", \"mail.example.com\"]. */\n domains: string[]\n /** Sub-label SendGrid uses for the link branding CNAME (default 'em'). */\n subdomain?: string\n /** Where SendGrid should POST event webhooks. Account-level — only configured once regardless of domain count. */\n webhookUrl: string\n /** Publish DNS records via Cloudflare API. Requires CLOUDFLARE_API_TOKEN. */\n cloudflare?: boolean\n /** Override the parent zone if any domain isn't a subdomain of its eTLD+1. */\n cloudflareZone?: string\n /** Replace an existing webhook URL if one is already set. */\n force?: boolean\n /** Logger; defaults to console. Pass {} to silence. */\n logger?: { log?: (...a: unknown[]) => void; warn?: (...a: unknown[]) => void }\n /** Override fetch (for tests). */\n fetchFn?: typeof fetch\n /** Override env (for tests). */\n env?: Record<string, string | undefined>\n}\n\nexport interface DomainSetupResult {\n domain: string\n domainAuthId: number\n alreadyValidated: boolean\n dnsRecords: Array<{ type: string; host: string; data: string }>\n cloudflarePushed: number\n}\n\nexport interface SetupSendgridResult {\n domains: DomainSetupResult[]\n webhookKey: string\n webhookUrl: string\n envSnippet: string\n}\n\ninterface SgDomainAuth {\n id: number\n domain: string\n subdomain: string\n username: string\n valid: boolean\n dns: Record<string, { host: string; type: string; data: string; valid: boolean }>\n}\n\nexport async function setupSendgrid(opts: SetupSendgridOpts): Promise<SetupSendgridResult> {\n const env = opts.env ?? process.env\n const log = opts.logger ?? console\n const info = (m: string) => log.log?.(m)\n const warn = (m: string) => log.warn?.(m)\n const f = opts.fetchFn ?? globalThis.fetch\n\n const sgKey = env.SENDGRID_API_KEY\n if (!sgKey) {\n throw new Error(\n 'SENDGRID_API_KEY env var is required.\\n' +\n ' Create one at: https://app.sendgrid.com/settings/api_keys\\n' +\n ' Needs at least: \"Mail Settings\" (Full Access) and \"Sender Authentication\" (Full Access).\\n' +\n ' Then add to your shell rc (e.g. ~/.zshrc): export SENDGRID_API_KEY=\"SG.xxx\"',\n )\n }\n if (!/^SG\\./.test(sgKey)) {\n throw new Error(\n 'SENDGRID_API_KEY does not look like a SendGrid API key (expected to start with \"SG.\"). ' +\n 'Double-check you copied the full key from https://app.sendgrid.com/settings/api_keys.',\n )\n }\n\n if (opts.cloudflare && !env.CLOUDFLARE_API_TOKEN) {\n throw new Error(\n 'CLOUDFLARE_API_TOKEN env var is required when --cloudflare is set.\\n' +\n ' Create one at: https://dash.cloudflare.com/profile/api-tokens\\n' +\n ' Use the \"Edit zone DNS\" template, restricted to the specific zone you\\'re publishing into.\\n' +\n ' Permissions: Zone:Read + DNS:Edit.\\n' +\n ' Then add to your shell rc: export CLOUDFLARE_API_TOKEN=\"cf-xxx\"',\n )\n }\n\n if (!opts.domains || opts.domains.length === 0) {\n throw new Error('At least one --domain is required')\n }\n\n const sg = sendgridClient(sgKey, f)\n const cf = opts.cloudflare ? cloudflareClient(env.CLOUDFLARE_API_TOKEN!, f) : null\n\n // ---- Per-domain: authentication + DNS publish ---------------------------\n const domainResults: DomainSetupResult[] = []\n for (const domain of opts.domains) {\n info('')\n info(`=== ${domain} ===`)\n info(`SendGrid → checking domain authentication for ${domain}…`)\n const existing = await sg.findDomainAuth(domain)\n let auth: SgDomainAuth\n if (existing) {\n info(` → reusing existing domain auth #${existing.id}`)\n auth = existing\n } else {\n info(` → creating new domain auth`)\n auth = await sg.createDomainAuth({\n domain,\n subdomain: opts.subdomain ?? 'em',\n })\n }\n\n const dnsRecords = Object.values(auth.dns).map((r) => ({ type: r.type, host: r.host, data: r.data }))\n let cloudflarePushed = 0\n\n if (cf) {\n info(`Cloudflare → publishing ${dnsRecords.length} record(s)…`)\n const zoneName = opts.cloudflareZone ?? inferZone(domain)\n const zoneId = await cf.findZoneId(zoneName)\n if (!zoneId) {\n throw new Error(`Cloudflare zone \"${zoneName}\" not found on this account`)\n }\n for (const rec of dnsRecords) {\n const result = await cf.upsertRecord(zoneId, rec)\n if (result === 'created') {\n cloudflarePushed++\n info(` + ${rec.type} ${rec.host} → ${rec.data}`)\n } else if (result === 'updated') {\n cloudflarePushed++\n info(` ~ ${rec.type} ${rec.host} (updated to ${rec.data})`)\n } else {\n info(` = ${rec.type} ${rec.host} (already correct)`)\n }\n }\n\n info('SendGrid → triggering validation…')\n const validated = await sg.validateDomainAuth(auth.id)\n if (validated) {\n info(' ✓ domain authenticated')\n } else {\n warn(' ! validation not yet successful — DNS may still be propagating. Re-run in a few minutes.')\n }\n auth.valid = validated\n } else {\n info('— skipping Cloudflare publish (no --cloudflare). Add these records to your DNS provider:')\n for (const rec of dnsRecords) {\n info(` ${rec.type.padEnd(7)} ${rec.host.padEnd(50)} ${rec.data}`)\n }\n }\n\n domainResults.push({\n domain,\n domainAuthId: auth.id,\n alreadyValidated: auth.valid,\n dnsRecords,\n cloudflarePushed,\n })\n }\n\n info('')\n info('=== Event webhook (account-level) ===')\n\n // ---- Account-level: Enable Signed Event Webhook + fetch public key -----\n info('SendGrid → checking Signed Event Webhook…')\n // SendGrid's GET /user/webhooks/event/settings/signed only returns\n // `{ public_key }`. A non-empty key implies signing is already enabled —\n // re-PATCHing `enabled: true` rotates the keypair, silently breaking\n // signature verification everywhere the old key was deployed.\n const signed = await sg.getSignedWebhookSettings()\n let webhookKey: string\n if (signed.public_key) {\n info(' = signing already enabled; reusing existing public key')\n webhookKey = signed.public_key\n } else {\n info(' → enabling signing')\n webhookKey = await sg.enableSignedWebhook()\n info(' ✓ verification key fetched')\n }\n\n // ---- Step 4: Configure Event Webhook URL + event toggles ----------------\n info('SendGrid → checking Event Webhook…')\n const currentEvents = await sg.getEventWebhookSettings()\n const desiredEvents = {\n url: opts.webhookUrl,\n enabled: true,\n delivered: true,\n open: true,\n click: true,\n bounce: true,\n dropped: true,\n spam_report: true,\n unsubscribe: true,\n deferred: false,\n processed: false,\n group_resubscribe: false,\n group_unsubscribe: false,\n }\n if (currentEvents.url && currentEvents.url !== opts.webhookUrl && !opts.force) {\n throw new Error(\n `Event webhook is already configured for ${currentEvents.url}. ` +\n `Pass --force to overwrite (this will redirect all future events to ${opts.webhookUrl}).`,\n )\n }\n if (eventSettingsMatch(currentEvents, desiredEvents)) {\n info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`)\n } else {\n await sg.setEventWebhookSettings(desiredEvents)\n info(` ✓ webhook URL set to ${opts.webhookUrl}`)\n }\n\n const envSnippet = [\n `# Add to your environment (or .env file):`,\n `SENDGRID_WEBHOOK_VERIFICATION_KEY=\"${webhookKey.replace(/\\n/g, '\\\\n')}\"`,\n ].join('\\n')\n\n info('')\n info('--- Done. Add to your environment ---')\n info(envSnippet)\n\n return {\n domains: domainResults,\n webhookKey,\n webhookUrl: opts.webhookUrl,\n envSnippet,\n }\n}\n\n// ---------------------------------------------------------------------------\n// SendGrid API client\n// ---------------------------------------------------------------------------\n\ninterface SendGridClient {\n findDomainAuth(domain: string): Promise<SgDomainAuth | null>\n createDomainAuth(input: { domain: string; subdomain: string }): Promise<SgDomainAuth>\n validateDomainAuth(id: number): Promise<boolean>\n getSignedWebhookSettings(): Promise<{ enabled?: boolean; public_key?: string }>\n enableSignedWebhook(): Promise<string>\n getEventWebhookSettings(): Promise<{ url?: string; enabled?: boolean; [k: string]: unknown }>\n setEventWebhookSettings(payload: Record<string, unknown>): Promise<void>\n}\n\nfunction sendgridClient(apiKey: string, fetchFn: typeof fetch): SendGridClient {\n const base = 'https://api.sendgrid.com/v3'\n const headers = { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }\n\n async function call(path: string, init: RequestInit = {}): Promise<any> {\n const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...(init.headers as any) } })\n const text = await res.text()\n const body = text ? safeJson(text) : null\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `SendGrid rejected the request (HTTP ${res.status}). Your SENDGRID_API_KEY is missing the required permissions ` +\n 'or is invalid. The key needs \"Mail Settings\" (Full Access) and \"Sender Authentication\" (Full Access). ' +\n 'Manage keys at https://app.sendgrid.com/settings/api_keys.',\n )\n }\n if (!res.ok) {\n const message = body?.errors?.[0]?.message ?? text ?? res.statusText\n throw new Error(`SendGrid ${init.method ?? 'GET'} ${path} → ${res.status}: ${message}`)\n }\n return body\n }\n\n return {\n async findDomainAuth(domain: string) {\n const list: SgDomainAuth[] = (await call(`/whitelabel/domains?limit=200`)) ?? []\n return list.find((d) => d.domain === domain) ?? null\n },\n async createDomainAuth({ domain, subdomain }) {\n return call(`/whitelabel/domains`, {\n method: 'POST',\n body: JSON.stringify({ domain, subdomain, automatic_security: true }),\n }) as Promise<SgDomainAuth>\n },\n async validateDomainAuth(id: number) {\n const r = await call(`/whitelabel/domains/${id}/validate`, { method: 'POST' })\n return Boolean(r?.valid)\n },\n async getSignedWebhookSettings() {\n return call(`/user/webhooks/event/settings/signed`)\n },\n async enableSignedWebhook() {\n await call(`/user/webhooks/event/settings/signed`, {\n method: 'PATCH',\n body: JSON.stringify({ enabled: true }),\n })\n const r = await call(`/user/webhooks/event/settings/signed`)\n if (!r?.public_key) throw new Error('SendGrid did not return a public_key for the signed event webhook')\n return r.public_key as string\n },\n async getEventWebhookSettings() {\n return call(`/user/webhooks/event/settings`)\n },\n async setEventWebhookSettings(payload) {\n await call(`/user/webhooks/event/settings`, {\n method: 'PATCH',\n body: JSON.stringify(payload),\n })\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloudflare API client\n// ---------------------------------------------------------------------------\n\ninterface CloudflareClient {\n findZoneId(name: string): Promise<string | null>\n upsertRecord(zoneId: string, rec: { type: string; host: string; data: string }): Promise<'created' | 'updated' | 'noop'>\n}\n\nfunction cloudflareClient(apiToken: string, fetchFn: typeof fetch): CloudflareClient {\n const base = 'https://api.cloudflare.com/client/v4'\n const headers = { authorization: `Bearer ${apiToken}`, 'content-type': 'application/json' }\n\n async function call(path: string, init: RequestInit = {}): Promise<any> {\n const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...(init.headers as any) } })\n const body: any = await res.json().catch(() => null)\n if (res.status === 401 || res.status === 403) {\n throw new Error(\n `Cloudflare rejected the request (HTTP ${res.status}). Your CLOUDFLARE_API_TOKEN is invalid, expired, ` +\n 'or missing the required permissions. The token needs Zone:Read + DNS:Edit on the specific zone you\\'re ' +\n 'publishing into. Manage tokens at https://dash.cloudflare.com/profile/api-tokens.',\n )\n }\n if (!res.ok || (body && body.success === false)) {\n const message = body?.errors?.[0]?.message ?? res.statusText\n throw new Error(`Cloudflare ${init.method ?? 'GET'} ${path} → ${res.status}: ${message}`)\n }\n return body\n }\n\n return {\n async findZoneId(name: string) {\n const r = await call(`/zones?name=${encodeURIComponent(name)}`)\n return r?.result?.[0]?.id ?? null\n },\n async upsertRecord(zoneId, rec) {\n // CF requires the host to be a FQDN, which SendGrid already gives us.\n const existing = await call(\n `/zones/${zoneId}/dns_records?type=${encodeURIComponent(rec.type)}&name=${encodeURIComponent(rec.host)}`,\n )\n const match = existing?.result?.[0]\n const desiredBody = {\n type: rec.type,\n name: rec.host,\n content: rec.data,\n ttl: 1, // auto\n proxied: false, // critical: SendGrid DKIM CNAMEs must NOT be proxied\n }\n if (!match) {\n await call(`/zones/${zoneId}/dns_records`, { method: 'POST', body: JSON.stringify(desiredBody) })\n return 'created'\n }\n if (match.content === rec.data && match.proxied === false) {\n return 'noop'\n }\n await call(`/zones/${zoneId}/dns_records/${match.id}`, {\n method: 'PUT',\n body: JSON.stringify(desiredBody),\n })\n return 'updated'\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Best-effort eTLD+1 inference. Falls back to last two labels — fine for most\n * `.com` / `.io` style domains. For multi-label public suffixes (`.co.uk`)\n * pass `--cloudflare-zone` explicitly. */\nfunction inferZone(domain: string): string {\n const labels = domain.split('.').filter(Boolean)\n if (labels.length <= 2) return domain\n return labels.slice(-2).join('.')\n}\n\nfunction safeJson(s: string): any {\n try { return JSON.parse(s) } catch { return null }\n}\n\n/** Compare current SendGrid event webhook settings to the desired payload.\n * Treats missing keys on `current` as \"not yet set\" rather than equal-to-false,\n * so a partially-configured webhook still triggers a PATCH. */\nfunction eventSettingsMatch(current: Record<string, unknown>, desired: Record<string, unknown>): boolean {\n for (const [k, v] of Object.entries(desired)) {\n if (current[k] !== v) return false\n }\n return true\n}\n","/**\n * Interactive wizard for `mailery setup-sendgrid`. Walks the user through:\n * - Required env vars (offers to paste them in, then prints export lines)\n * - Domains (accept multiple via repeated entry or comma-separation)\n * - Webhook URL (with a default derived from the first domain's apex)\n * - Cloudflare toggle + zone override\n * - Force flag\n *\n * Renders a summary before any API call and asks for confirmation. All\n * questions are funneled through a `Prompter` so tests can inject canned\n * answers without touching stdin.\n */\n\nimport * as readline from 'node:readline/promises'\nimport { stdin as input, stdout as output } from 'node:process'\n\nimport type { SetupSendgridOpts } from './setup-sendgrid.js'\n\nexport type Prompter = (question: string, opts?: { defaultValue?: string }) => Promise<string>\n\nexport interface InteractiveResult {\n opts: Omit<SetupSendgridOpts, 'fetchFn' | 'logger' | 'env'>\n envOverrides: Record<string, string>\n exportLines: string[]\n proceed: boolean\n}\n\nexport function makeReadlinePrompter(): { prompter: Prompter; close: () => void } {\n const rl = readline.createInterface({ input, output })\n const prompter: Prompter = async (q, opts) => {\n const suffix = opts?.defaultValue ? ` [${opts.defaultValue}]` : ''\n const raw = await rl.question(`${q}${suffix}: `)\n const trimmed = raw.trim()\n return trimmed.length === 0 && opts?.defaultValue ? opts.defaultValue : trimmed\n }\n return { prompter, close: () => rl.close() }\n}\n\nexport async function runInteractive(\n prompter: Prompter,\n env: Record<string, string | undefined>,\n logger: { log: (...a: unknown[]) => void } = console,\n): Promise<InteractiveResult> {\n const log = (m: string) => logger.log(m)\n const exportLines: string[] = []\n const envOverrides: Record<string, string> = {}\n\n log('')\n log('mailery setup-sendgrid (interactive)')\n log('────────────────────────────────────')\n log('')\n\n // ---- SendGrid API key ---------------------------------------------------\n if (!env.SENDGRID_API_KEY) {\n log('SENDGRID_API_KEY is not set in your environment.')\n log(' Create one at https://app.sendgrid.com/settings/api_keys')\n log(' Permissions: Mail Settings (Full Access) + Sender Authentication (Full Access).')\n log('')\n const key = await prompter('Paste your SendGrid API key (starts with SG.)')\n if (!key) throw new Error('SENDGRID_API_KEY is required')\n envOverrides.SENDGRID_API_KEY = key\n exportLines.push(`export SENDGRID_API_KEY=\"${key}\"`)\n }\n\n // ---- Domains ------------------------------------------------------------\n log('')\n log('Which sender domain(s) do you want to authenticate?')\n log(' Examples:')\n log(' news.example.com (one)')\n log(' news.example.com, mail.example.com (two — comma-separated, in one line)')\n log(' Splitting marketing from transactional onto separate domains is recommended.')\n log('')\n const domainsRaw = await prompter('Domains')\n const domains = domainsRaw\n .split(',')\n .map((d) => d.trim())\n .filter(Boolean)\n if (domains.length === 0) throw new Error('at least one --domain is required')\n\n // ---- Webhook URL --------------------------------------------------------\n const defaultWebhook = `https://${apex(domains[0]!)}/m/webhooks/sendgrid`\n log('')\n log('SendGrid will POST event webhooks to this URL.')\n log(' This must be reachable from the public internet (not behind admin auth).')\n log('')\n const webhookUrl = await prompter('Webhook URL', { defaultValue: defaultWebhook })\n\n // ---- Cloudflare ---------------------------------------------------------\n log('')\n const cloudflareRaw = await prompter('Publish DNS records via Cloudflare API? (y/N)', { defaultValue: 'n' })\n const cloudflare = /^y/i.test(cloudflareRaw)\n let cloudflareZone: string | undefined\n\n if (cloudflare) {\n if (!env.CLOUDFLARE_API_TOKEN) {\n log('')\n log('CLOUDFLARE_API_TOKEN is not set in your environment.')\n log(' Create one at https://dash.cloudflare.com/profile/api-tokens')\n log(' Use the \"Edit zone DNS\" template, restricted to your zone(s).')\n log(' Permissions: Zone:Read + DNS:Edit.')\n log('')\n const token = await prompter('Paste your Cloudflare API token')\n if (!token) throw new Error('CLOUDFLARE_API_TOKEN is required when publishing via Cloudflare')\n envOverrides.CLOUDFLARE_API_TOKEN = token\n exportLines.push(`export CLOUDFLARE_API_TOKEN=\"${token}\"`)\n }\n log('')\n const zoneOverride = await prompter('Override Cloudflare zone (blank to infer per-domain)', { defaultValue: '' })\n if (zoneOverride.length > 0) cloudflareZone = zoneOverride\n }\n\n // ---- Force --------------------------------------------------------------\n log('')\n const forceRaw = await prompter(\n 'If a different webhook URL is already configured on this SendGrid account, overwrite it? (y/N)',\n { defaultValue: 'n' },\n )\n const force = /^y/i.test(forceRaw)\n\n // ---- Summary + confirm --------------------------------------------------\n log('')\n log('About to run setup with:')\n log(` domains: ${domains.join(', ')}`)\n log(` webhook URL: ${webhookUrl}`)\n log(` cloudflare: ${cloudflare ? 'yes' : 'no'}`)\n if (cloudflareZone) log(` cloudflare zone: ${cloudflareZone}`)\n log(` force: ${force ? 'yes' : 'no'}`)\n log('')\n const confirmRaw = await prompter('Proceed? (Y/n)', { defaultValue: 'y' })\n const proceed = !/^n/i.test(confirmRaw)\n\n return {\n opts: { domains, webhookUrl, cloudflare, cloudflareZone, force },\n envOverrides,\n exportLines,\n proceed,\n }\n}\n\n/** Strip a leading sub-label so we get the apex (best-effort for webhook URL default). */\nfunction apex(domain: string): string {\n const labels = domain.split('.').filter(Boolean)\n if (labels.length <= 2) return domain\n return labels.slice(-2).join('.')\n}\n","#!/usr/bin/env node\n/**\n * mailery CLI entry. Currently exposes a single command:\n *\n * npx mailery setup-sendgrid --domain X --webhook-url Y [--cloudflare]\n *\n * See src/cli/setup-sendgrid.ts for the full option list.\n */\n\nimport { setupSendgrid } from './setup-sendgrid.js'\nimport { makeReadlinePrompter, runInteractive } from './interactive.js'\n\nasync function main() {\n const argv = process.argv.slice(2)\n const cmd = argv[0]\n\n if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {\n printUsage()\n process.exit(cmd ? 0 : 1)\n }\n\n switch (cmd) {\n case 'setup-sendgrid': {\n const opts = parseArgs(argv.slice(1))\n const argDomains = collectStringList(opts.domain)\n const argWebhook = typeof opts['webhook-url'] === 'string' ? (opts['webhook-url'] as string) : null\n const explicitInteractive = Boolean(opts.interactive)\n const explicitNonInteractive = Boolean(opts['no-interactive'])\n\n // Decide interactive vs args. Interactive when: --interactive, OR no\n // required args provided AND we have a TTY. Force non-interactive with\n // --no-interactive (useful for CI).\n const wantsInteractive =\n explicitInteractive ||\n (!explicitNonInteractive && argDomains.length === 0 && !argWebhook && Boolean(process.stdin.isTTY))\n\n try {\n if (wantsInteractive) {\n const { prompter, close } = makeReadlinePrompter()\n let interactive\n try {\n interactive = await runInteractive(prompter, process.env)\n } finally {\n close()\n }\n if (!interactive.proceed) {\n console.log('Aborted.')\n process.exit(0)\n }\n await setupSendgrid({\n ...interactive.opts,\n env: { ...process.env, ...interactive.envOverrides },\n })\n if (interactive.exportLines.length > 0) {\n console.log('')\n console.log('Add these to your shell rc (e.g. ~/.zshrc) so future runs find them:')\n for (const line of interactive.exportLines) console.log(` ${line}`)\n }\n } else {\n if (argDomains.length === 0) {\n console.error('error: --domain is required (repeat or comma-separate for multiple, or run without args for interactive mode)')\n printUsage()\n process.exit(2)\n }\n if (!argWebhook) {\n console.error('error: --webhook-url is required')\n printUsage()\n process.exit(2)\n }\n await setupSendgrid({\n domains: argDomains,\n subdomain: opts.subdomain ? String(opts.subdomain) : undefined,\n webhookUrl: argWebhook,\n cloudflare: Boolean(opts.cloudflare),\n cloudflareZone: opts['cloudflare-zone'] ? String(opts['cloudflare-zone']) : undefined,\n force: Boolean(opts.force),\n })\n }\n } catch (err: any) {\n console.error(`\\n✗ ${err?.message ?? err}`)\n process.exit(1)\n }\n break\n }\n default:\n console.error(`unknown command: ${cmd}`)\n printUsage()\n process.exit(2)\n }\n}\n\nfunction parseArgs(argv: string[]): Record<string, string | string[] | boolean> {\n const out: Record<string, string | string[] | boolean> = {}\n function setKey(key: string, value: string | boolean) {\n const existing = out[key]\n if (existing === undefined) {\n out[key] = value\n } else if (Array.isArray(existing)) {\n if (typeof value === 'string') existing.push(value)\n } else if (typeof existing === 'string' && typeof value === 'string') {\n out[key] = [existing, value]\n } else {\n out[key] = value\n }\n }\n for (let i = 0; i < argv.length; i++) {\n const tok = argv[i]!\n if (!tok.startsWith('--')) continue\n const eq = tok.indexOf('=')\n if (eq > 0) {\n setKey(tok.slice(2, eq), tok.slice(eq + 1))\n continue\n }\n const key = tok.slice(2)\n const next = argv[i + 1]\n if (next && !next.startsWith('--')) {\n setKey(key, next)\n i++\n } else {\n setKey(key, true)\n }\n }\n return out\n}\n\n/** Collapse a repeated/comma-separated flag value into a clean string[]. */\nfunction collectStringList(value: string | string[] | boolean | undefined): string[] {\n if (value === undefined || value === true || value === false) return []\n const raw = Array.isArray(value) ? value : [value]\n return raw\n .flatMap((s) => String(s).split(','))\n .map((s) => s.trim())\n .filter(Boolean)\n}\n\nfunction printUsage() {\n console.log(`\nmailery CLI\n\nUsage:\n mailery setup-sendgrid (interactive — prompts you for everything)\n mailery setup-sendgrid --domain <d>[,<d>...] --webhook-url <u> [options] (non-interactive)\n\nOptions:\n --domain <d> Domain to authenticate. Repeat (or comma-separate)\n to authenticate multiple in one run, e.g. when\n isolating marketing from transactional. The event\n webhook itself is account-level and only gets set up\n once.\n --subdomain <s> Sub-label for the link branding CNAME (default: em)\n --webhook-url <u> Public URL for the SendGrid event webhook\n --cloudflare Publish DNS records via the Cloudflare API\n --cloudflare-zone <z> Override the inferred zone (e.g. for multi-label\n public suffixes like .co.uk)\n --force Replace an existing event webhook URL if one is set\n --interactive Force interactive mode even when args are supplied\n --no-interactive Disable interactive mode even when args are missing\n (the script exits with usage instead of prompting —\n intended for CI / scripts)\n\nEnv vars:\n SENDGRID_API_KEY Required. See https://app.sendgrid.com/settings/api_keys\n Permissions: Mail Settings + Sender Authentication.\n CLOUDFLARE_API_TOKEN Required when --cloudflare is set. See\n https://dash.cloudflare.com/profile/api-tokens\n Permissions: Zone:Read + DNS:Edit on the zone(s)\n being published into.\n\nExamples:\n # one domain\n mailery setup-sendgrid \\\\\n --domain news.example.com \\\\\n --webhook-url https://example.com/m/webhooks/sendgrid \\\\\n --cloudflare\n\n # marketing + transactional in one shot\n mailery setup-sendgrid \\\\\n --domain news.example.com \\\\\n --domain mail.example.com \\\\\n --webhook-url https://example.com/m/webhooks/sendgrid \\\\\n --cloudflare\n`)\n}\n\nmain()\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailery",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Embedded, MongoDB-backed email automation for Node.js. Triggered flows, broadcasts, tracking, suppression, and an admin UI you mount inside your Express app.",
5
5
  "license": "MIT",
6
6
  "author": "Jeff Jassky <jeff@jeffjassky.com>",
@@ -40,6 +40,9 @@
40
40
  },
41
41
  "./package.json": "./package.json"
42
42
  },
43
+ "bin": {
44
+ "mailery": "./dist/cli.js"
45
+ },
43
46
  "files": [
44
47
  "dist",
45
48
  "README.md",