mailery 0.3.2 → 0.5.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/dist/cli.js CHANGED
@@ -2,6 +2,76 @@
2
2
  import * as readline from 'readline/promises';
3
3
  import { stdout, stdin } from 'process';
4
4
 
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
11
+
12
+ // src/cli/cloudflare.ts
13
+ function cloudflareClient(apiToken, fetchFn) {
14
+ const base = "https://api.cloudflare.com/client/v4";
15
+ const headers = { authorization: `Bearer ${apiToken}`, "content-type": "application/json" };
16
+ async function call(path, init = {}) {
17
+ const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
18
+ const body = await res.json().catch(() => null);
19
+ if (res.status === 401 || res.status === 403) {
20
+ throw new Error(
21
+ `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.`
22
+ );
23
+ }
24
+ if (!res.ok || body && body.success === false) {
25
+ const message = body?.errors?.[0]?.message ?? res.statusText;
26
+ throw new Error(`Cloudflare ${init.method ?? "GET"} ${path} \u2192 ${res.status}: ${message}`);
27
+ }
28
+ return body;
29
+ }
30
+ return {
31
+ async findZoneId(name) {
32
+ const r = await call(`/zones?name=${encodeURIComponent(name)}`);
33
+ return r?.result?.[0]?.id ?? null;
34
+ },
35
+ async upsertRecord(zoneId, rec) {
36
+ const existing = await call(
37
+ `/zones/${zoneId}/dns_records?type=${encodeURIComponent(rec.type)}&name=${encodeURIComponent(rec.host)}`
38
+ );
39
+ const match = existing?.result?.[0];
40
+ const desiredBody = {
41
+ type: rec.type,
42
+ name: rec.host,
43
+ content: rec.data,
44
+ ttl: 1,
45
+ proxied: false
46
+ };
47
+ if (!match) {
48
+ await call(`/zones/${zoneId}/dns_records`, { method: "POST", body: JSON.stringify(desiredBody) });
49
+ return "created";
50
+ }
51
+ const normalizeTxt = (s) => {
52
+ if (rec.type !== "TXT" || typeof s !== "string") return s;
53
+ const re = /"((?:[^"\\]|\\.)*)"/g;
54
+ const segments = [];
55
+ let m;
56
+ while (m = re.exec(s)) segments.push(m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
57
+ return segments.length > 0 ? segments.join("") : s;
58
+ };
59
+ if (normalizeTxt(match.content) === rec.data && match.proxied === false) {
60
+ return "noop";
61
+ }
62
+ await call(`/zones/${zoneId}/dns_records/${match.id}`, {
63
+ method: "PUT",
64
+ body: JSON.stringify(desiredBody)
65
+ });
66
+ return "updated";
67
+ }
68
+ };
69
+ }
70
+ function inferZone(domain) {
71
+ const psl = __require("psl");
72
+ return psl.get(domain) ?? domain;
73
+ }
74
+
5
75
  // src/cli/setup-sendgrid.ts
6
76
  async function setupSendgrid(opts) {
7
77
  const env = opts.env ?? process.env;
@@ -205,62 +275,6 @@ function sendgridClient(apiKey, fetchFn) {
205
275
  }
206
276
  };
207
277
  }
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
278
  function safeJson(s) {
265
279
  try {
266
280
  return JSON.parse(s);
@@ -274,6 +288,96 @@ function eventSettingsMatch(current, desired) {
274
288
  }
275
289
  return true;
276
290
  }
291
+
292
+ // src/cli/setup-dmarc.ts
293
+ async function setupDmarc(opts) {
294
+ const env = opts.env ?? process.env;
295
+ const log = opts.logger ?? console;
296
+ const info = (m) => log.log?.(m);
297
+ const warn = (m) => log.warn?.(m);
298
+ const f = opts.fetchFn ?? globalThis.fetch;
299
+ if (!opts.domain) throw new Error("--domain is required");
300
+ if (!opts.ruaMailbox) throw new Error("--rua-mailbox is required (RUA aggregate-report mailbox)");
301
+ const ruaMailboxes = opts.ruaMailbox.split(",").map((s) => s.trim()).filter(Boolean);
302
+ for (const m of ruaMailboxes) {
303
+ if (!isValidEmail(m)) throw new Error(`--rua-mailbox is not a valid email: ${m}`);
304
+ }
305
+ if (opts.rufMailbox && !isValidEmail(opts.rufMailbox)) {
306
+ throw new Error(`--ruf-mailbox is not a valid email: ${opts.rufMailbox}`);
307
+ }
308
+ const pct = opts.pct ?? 100;
309
+ if (pct < 1 || pct > 100) throw new Error("--pct must be between 1 and 100");
310
+ if (opts.cloudflare && !env.CLOUDFLARE_API_TOKEN) {
311
+ throw new Error(
312
+ "CLOUDFLARE_API_TOKEN env var is required when --cloudflare is set.\n Create one at: https://dash.cloudflare.com/profile/api-tokens\n Needs: Zone:Read + DNS:Edit on the zone you're publishing into."
313
+ );
314
+ }
315
+ const recordValue = buildDmarcRecord({
316
+ policy: opts.policy ?? "none",
317
+ rua: ruaMailboxes.join(","),
318
+ ruf: opts.rufMailbox,
319
+ pct,
320
+ aspf: opts.aspf ?? "r",
321
+ adkim: opts.adkim ?? "r"
322
+ });
323
+ const recordHost = `_dmarc.${opts.domain}`;
324
+ info("");
325
+ info(`=== ${opts.domain} ===`);
326
+ info(`DMARC record:`);
327
+ info(` ${recordHost} TXT "${recordValue}"`);
328
+ let cloudflarePushed = "skipped";
329
+ if (opts.cloudflare) {
330
+ const cf = cloudflareClient(env.CLOUDFLARE_API_TOKEN, f);
331
+ const zoneName = opts.cloudflareZone ?? inferZone(opts.domain);
332
+ const zoneId = await cf.findZoneId(zoneName);
333
+ if (!zoneId) {
334
+ throw new Error(
335
+ `Cloudflare zone "${zoneName}" not found. Either the token doesn't have access, or the zone isn't on Cloudflare. Pass --cloudflare-zone <zone> if the zone name differs from the eTLD+1.`
336
+ );
337
+ }
338
+ cloudflarePushed = await cf.upsertRecord(zoneId, { type: "TXT", host: recordHost, data: recordValue });
339
+ info(`Cloudflare ${cloudflarePushed}: ${recordHost} TXT`);
340
+ } else {
341
+ info("");
342
+ info("\u2014 skipping Cloudflare publish (no --cloudflare). Add this record to your DNS provider:");
343
+ info(` Host: ${recordHost}`);
344
+ info(` Type: TXT`);
345
+ info(` Value: ${recordValue}`);
346
+ }
347
+ if ((opts.policy ?? "none") === "none") {
348
+ info("");
349
+ info("Recommended next steps:");
350
+ info(" 1. Wait 1-2 weeks while RUA reports arrive at " + opts.ruaMailbox);
351
+ info(" 2. Upload reports to mailery (Health \u2192 DMARC RUA reports \u2192 Upload)");
352
+ info(" 3. Tag known sources in the admin UI so untagged senders surface");
353
+ info(" 4. Once all your legitimate sources align, tighten to p=quarantine pct=10, then ramp pct, then p=reject");
354
+ }
355
+ if ((opts.policy ?? "none") !== "none" && (opts.pct ?? 100) === 100) {
356
+ warn("");
357
+ warn("You are publishing a non-monitor policy at pct=100. If any of your legitimate sending sources are not");
358
+ warn("yet fully aligned, mail will be quarantined or rejected starting immediately. Consider --pct 10 first.");
359
+ }
360
+ return { domain: opts.domain, recordValue, recordHost, cloudflarePushed };
361
+ }
362
+ function buildDmarcRecord(opts) {
363
+ const ruaList = opts.rua.split(",").map((m) => `mailto:${m.trim()}`).filter((m) => m !== "mailto:").join(",");
364
+ const parts = [
365
+ "v=DMARC1",
366
+ `p=${opts.policy}`,
367
+ `rua=${ruaList}`
368
+ ];
369
+ if (opts.ruf) {
370
+ const rufList = opts.ruf.split(",").map((m) => `mailto:${m.trim()}`).filter((m) => m !== "mailto:").join(",");
371
+ parts.push(`ruf=${rufList}`);
372
+ }
373
+ if (opts.pct !== 100) parts.push(`pct=${opts.pct}`);
374
+ if (opts.aspf !== "r") parts.push(`aspf=${opts.aspf}`);
375
+ if (opts.adkim !== "r") parts.push(`adkim=${opts.adkim}`);
376
+ return parts.join("; ");
377
+ }
378
+ function isValidEmail(s) {
379
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
380
+ }
277
381
  function makeReadlinePrompter() {
278
382
  const rl = readline.createInterface({ input: stdin, output: stdout });
279
383
  const prompter = async (q, opts) => {
@@ -428,6 +532,53 @@ async function main() {
428
532
  }
429
533
  } catch (err) {
430
534
  console.error(`
535
+ \u2717 ${err?.message ?? err}`);
536
+ process.exit(1);
537
+ }
538
+ break;
539
+ }
540
+ case "setup-dmarc": {
541
+ const opts = parseArgs(argv.slice(1));
542
+ const domain = typeof opts.domain === "string" ? opts.domain : null;
543
+ const ruaMailbox = typeof opts["rua-mailbox"] === "string" ? opts["rua-mailbox"] : null;
544
+ if (!domain) {
545
+ console.error("error: --domain is required");
546
+ printDmarcUsage();
547
+ process.exit(2);
548
+ }
549
+ if (!ruaMailbox) {
550
+ console.error("error: --rua-mailbox is required (mailbox to receive DMARC aggregate reports)");
551
+ printDmarcUsage();
552
+ process.exit(2);
553
+ }
554
+ const policy = typeof opts.policy === "string" ? opts.policy : "none";
555
+ if (!["none", "quarantine", "reject"].includes(policy)) {
556
+ console.error(`error: --policy must be one of none|quarantine|reject (got "${policy}")`);
557
+ process.exit(2);
558
+ }
559
+ let pct;
560
+ if (opts.pct !== void 0 && opts.pct !== true) {
561
+ const n = Number(opts.pct);
562
+ if (!Number.isFinite(n)) {
563
+ console.error(`error: --pct must be numeric (got "${String(opts.pct)}")`);
564
+ process.exit(2);
565
+ }
566
+ pct = n;
567
+ }
568
+ try {
569
+ await setupDmarc({
570
+ domain,
571
+ ruaMailbox,
572
+ rufMailbox: typeof opts["ruf-mailbox"] === "string" ? opts["ruf-mailbox"] : void 0,
573
+ policy,
574
+ pct,
575
+ aspf: opts.aspf === "s" ? "s" : "r",
576
+ adkim: opts.adkim === "s" ? "s" : "r",
577
+ cloudflare: Boolean(opts.cloudflare),
578
+ cloudflareZone: opts["cloudflare-zone"] ? String(opts["cloudflare-zone"]) : void 0
579
+ });
580
+ } catch (err) {
581
+ console.error(`
431
582
  \u2717 ${err?.message ?? err}`);
432
583
  process.exit(1);
433
584
  }
@@ -439,6 +590,30 @@ async function main() {
439
590
  process.exit(2);
440
591
  }
441
592
  }
593
+ function printDmarcUsage() {
594
+ console.log(`
595
+ mailery setup-dmarc \u2014 publish a DMARC TXT record for a domain.
596
+
597
+ Usage:
598
+ mailery setup-dmarc --domain <d> --rua-mailbox <email> [options]
599
+
600
+ Options:
601
+ --domain <d> Domain to publish DMARC for (e.g. example.com)
602
+ --rua-mailbox <email> Mailbox to receive RUA aggregate reports
603
+ --ruf-mailbox <email> Mailbox to receive RUF forensic reports (rarely used)
604
+ --policy <p> none | quarantine | reject (default: none)
605
+ --pct <n> Percent of failing mail subject to the policy (1-100, default 100)
606
+ --aspf r|s SPF alignment mode (default r \u2014 relaxed)
607
+ --adkim r|s DKIM alignment mode (default r \u2014 relaxed)
608
+ --cloudflare Publish TXT record via Cloudflare API
609
+ --cloudflare-zone <z> Override inferred zone
610
+
611
+ Env:
612
+ CLOUDFLARE_API_TOKEN Required when --cloudflare is set.
613
+
614
+ Recommended progression: p=none \u2192 wait, monitor RUA \u2192 p=quarantine pct=10 \u2192 ramp pct \u2192 p=reject.
615
+ `);
616
+ }
442
617
  function parseArgs(argv) {
443
618
  const out = {};
444
619
  function setKey(key, value) {
package/dist/cli.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/cli/cloudflare.ts","../src/cli/setup-sendgrid.ts","../src/cli/setup-dmarc.ts","../src/cli/interactive.ts","../src/cli/index.ts"],"names":["input","output"],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Cloudflare DNS helpers shared by setup-sendgrid + setup-dmarc.\n * Wraps the v4 zones / dns_records endpoints with idempotent upsert.\n */\n\nexport interface 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\nexport function 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 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,\n proxied: false,\n }\n if (!match) {\n await call(`/zones/${zoneId}/dns_records`, { method: 'POST', body: JSON.stringify(desiredBody) })\n return 'created'\n }\n // Cloudflare normalizes TXT record content by wrapping in double\n // quotes when the string contains spaces or punctuation. Long TXT\n // values (>255 chars, the DNS string-segment limit) are also\n // returned as a sequence of quoted segments separated by spaces:\n // \"v=DMARC1; p=quarantine; rua=...\" \"extra=stuff\"\n // Strip *all* quoted segments and concat before comparing.\n const normalizeTxt = (s: string) => {\n if (rec.type !== 'TXT' || typeof s !== 'string') return s\n const re = /\"((?:[^\"\\\\]|\\\\.)*)\"/g\n const segments: string[] = []\n let m: RegExpExecArray | null\n while ((m = re.exec(s))) segments.push(m[1]!.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\'))\n return segments.length > 0 ? segments.join('') : s\n }\n if (normalizeTxt(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 * Public Suffix List-aware eTLD+1 inference. Handles multi-label public\n * suffixes (`.co.uk`, `.com.br`, etc.) correctly. Falls back to the\n * original input if PSL can't classify the domain (e.g., private TLD).\n */\nexport function inferZone(domain: string): string {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const psl = require('psl') as typeof import('psl')\n return psl.get(domain) ?? domain\n}\n","/**\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\nimport { cloudflareClient, inferZone } from './cloudflare.js'\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// Helpers\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 * `mailery setup-dmarc` — publish a DMARC policy record for a domain.\n *\n * npx mailery setup-dmarc \\\n * --domain news.example.com \\\n * --rua-mailbox dmarc-reports@example.com \\\n * --policy none \\\n * --cloudflare\n *\n * What it does:\n * 1. Build the DMARC TXT record string (RFC 7489).\n * 2. If `--cloudflare`, publish it as a TXT record at `_dmarc.<domain>`.\n * Otherwise print the record so the operator can publish manually.\n * 3. Idempotent — re-running with the same inputs is a no-op.\n *\n * Why default to `p=none`:\n * Tightening straight to `p=quarantine` or `p=reject` without monitoring\n * first can drop legitimate mail you didn't know about (SaaS tools sending\n * as you, etc). The recommended progression is none → quarantine → reject.\n *\n * Required env:\n * CLOUDFLARE_API_TOKEN — only if --cloudflare; needs Zone:Read + DNS:Edit\n */\n\nimport { cloudflareClient, inferZone } from './cloudflare.js'\n\nexport type DmarcPolicy = 'none' | 'quarantine' | 'reject'\n\nexport interface SetupDmarcOpts {\n /** Domain to publish DMARC for, e.g. `'news.example.com'`. */\n domain: string\n /** RUA aggregate-report mailbox, e.g. `'dmarc-reports@example.com'`. */\n ruaMailbox: string\n /** Forensic-report mailbox (RUF). Most operators leave this unset. */\n rufMailbox?: string\n /** DMARC enforcement policy. Default 'none' (monitor only). */\n policy?: DmarcPolicy\n /** Percent of failing mail subject to the policy (1-100). Default 100. */\n pct?: number\n /** SPF alignment mode: 'r' (relaxed, default) or 's' (strict). */\n aspf?: 'r' | 's'\n /** DKIM alignment mode: 'r' (relaxed, default) or 's' (strict). */\n adkim?: 'r' | 's'\n /** Publish via Cloudflare API. Otherwise prints DNS instructions. */\n cloudflare?: boolean\n /** Override the parent zone when --cloudflare is set. */\n cloudflareZone?: string\n logger?: { log?: (...a: unknown[]) => void; warn?: (...a: unknown[]) => void }\n fetchFn?: typeof fetch\n env?: Record<string, string | undefined>\n}\n\nexport interface SetupDmarcResult {\n domain: string\n /** The full TXT record value. */\n recordValue: string\n /** The DNS host the record is published at, e.g. `_dmarc.news.example.com`. */\n recordHost: string\n /** Whether DNS was actually pushed (vs printed for manual publish). */\n cloudflarePushed: 'created' | 'updated' | 'noop' | 'skipped'\n}\n\nexport async function setupDmarc(opts: SetupDmarcOpts): Promise<SetupDmarcResult> {\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 if (!opts.domain) throw new Error('--domain is required')\n if (!opts.ruaMailbox) throw new Error('--rua-mailbox is required (RUA aggregate-report mailbox)')\n // RFC 7489 allows a comma-separated list of mailboxes (e.g. for parallel\n // ingest to a primary and a backup mailbox). Validate each address.\n const ruaMailboxes = opts.ruaMailbox.split(',').map((s) => s.trim()).filter(Boolean)\n for (const m of ruaMailboxes) {\n if (!isValidEmail(m)) throw new Error(`--rua-mailbox is not a valid email: ${m}`)\n }\n if (opts.rufMailbox && !isValidEmail(opts.rufMailbox)) {\n throw new Error(`--ruf-mailbox is not a valid email: ${opts.rufMailbox}`)\n }\n\n const pct = opts.pct ?? 100\n if (pct < 1 || pct > 100) throw new Error('--pct must be between 1 and 100')\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 ' Needs: Zone:Read + DNS:Edit on the zone you\\'re publishing into.',\n )\n }\n\n const recordValue = buildDmarcRecord({\n policy: opts.policy ?? 'none',\n rua: ruaMailboxes.join(','),\n ruf: opts.rufMailbox,\n pct,\n aspf: opts.aspf ?? 'r',\n adkim: opts.adkim ?? 'r',\n })\n\n const recordHost = `_dmarc.${opts.domain}`\n\n info('')\n info(`=== ${opts.domain} ===`)\n info(`DMARC record:`)\n info(` ${recordHost} TXT \"${recordValue}\"`)\n\n let cloudflarePushed: SetupDmarcResult['cloudflarePushed'] = 'skipped'\n\n if (opts.cloudflare) {\n const cf = cloudflareClient(env.CLOUDFLARE_API_TOKEN!, f)\n const zoneName = opts.cloudflareZone ?? inferZone(opts.domain)\n const zoneId = await cf.findZoneId(zoneName)\n if (!zoneId) {\n throw new Error(\n `Cloudflare zone \"${zoneName}\" not found. ` +\n `Either the token doesn't have access, or the zone isn't on Cloudflare. ` +\n `Pass --cloudflare-zone <zone> if the zone name differs from the eTLD+1.`,\n )\n }\n cloudflarePushed = await cf.upsertRecord(zoneId, { type: 'TXT', host: recordHost, data: recordValue })\n info(`Cloudflare ${cloudflarePushed}: ${recordHost} TXT`)\n } else {\n info('')\n info('— skipping Cloudflare publish (no --cloudflare). Add this record to your DNS provider:')\n info(` Host: ${recordHost}`)\n info(` Type: TXT`)\n info(` Value: ${recordValue}`)\n }\n\n if ((opts.policy ?? 'none') === 'none') {\n info('')\n info('Recommended next steps:')\n info(' 1. Wait 1-2 weeks while RUA reports arrive at ' + opts.ruaMailbox)\n info(' 2. Upload reports to mailery (Health → DMARC RUA reports → Upload)')\n info(' 3. Tag known sources in the admin UI so untagged senders surface')\n info(' 4. Once all your legitimate sources align, tighten to p=quarantine pct=10, then ramp pct, then p=reject')\n }\n if ((opts.policy ?? 'none') !== 'none' && (opts.pct ?? 100) === 100) {\n warn('')\n warn('You are publishing a non-monitor policy at pct=100. If any of your legitimate sending sources are not')\n warn('yet fully aligned, mail will be quarantined or rejected starting immediately. Consider --pct 10 first.')\n }\n\n return { domain: opts.domain, recordValue, recordHost, cloudflarePushed }\n}\n\n// ---------------------------------------------------------------------------\n// Record builder\n// ---------------------------------------------------------------------------\n\nexport interface BuildDmarcRecordOpts {\n policy: DmarcPolicy\n rua: string\n ruf?: string\n pct: number\n aspf: 'r' | 's'\n adkim: 'r' | 's'\n}\n\nexport function buildDmarcRecord(opts: BuildDmarcRecordOpts): string {\n // RFC 7489 allows a comma-separated list of `mailto:` URIs in rua / ruf.\n const ruaList = opts.rua.split(',').map((m) => `mailto:${m.trim()}`).filter((m) => m !== 'mailto:').join(',')\n const parts: string[] = [\n 'v=DMARC1',\n `p=${opts.policy}`,\n `rua=${ruaList}`,\n ]\n if (opts.ruf) {\n const rufList = opts.ruf.split(',').map((m) => `mailto:${m.trim()}`).filter((m) => m !== 'mailto:').join(',')\n parts.push(`ruf=${rufList}`)\n }\n if (opts.pct !== 100) parts.push(`pct=${opts.pct}`)\n if (opts.aspf !== 'r') parts.push(`aspf=${opts.aspf}`)\n if (opts.adkim !== 'r') parts.push(`adkim=${opts.adkim}`)\n return parts.join('; ')\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction isValidEmail(s: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s)\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. Available commands:\n *\n * npx mailery setup-sendgrid --domain X --webhook-url Y [--cloudflare]\n * npx mailery setup-dmarc --domain X --rua-mailbox Y [--cloudflare]\n *\n * See src/cli/setup-{sendgrid,dmarc}.ts for the full option lists.\n */\n\nimport { setupSendgrid } from './setup-sendgrid.js'\nimport { setupDmarc, type DmarcPolicy } from './setup-dmarc.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 case 'setup-dmarc': {\n const opts = parseArgs(argv.slice(1))\n const domain = typeof opts.domain === 'string' ? opts.domain : null\n const ruaMailbox = typeof opts['rua-mailbox'] === 'string' ? (opts['rua-mailbox'] as string) : null\n if (!domain) {\n console.error('error: --domain is required')\n printDmarcUsage()\n process.exit(2)\n }\n if (!ruaMailbox) {\n console.error('error: --rua-mailbox is required (mailbox to receive DMARC aggregate reports)')\n printDmarcUsage()\n process.exit(2)\n }\n const policy = (typeof opts.policy === 'string' ? opts.policy : 'none') as DmarcPolicy\n if (!['none', 'quarantine', 'reject'].includes(policy)) {\n console.error(`error: --policy must be one of none|quarantine|reject (got \"${policy}\")`)\n process.exit(2)\n }\n let pct: number | undefined\n if (opts.pct !== undefined && opts.pct !== true) {\n const n = Number(opts.pct)\n if (!Number.isFinite(n)) {\n console.error(`error: --pct must be numeric (got \"${String(opts.pct)}\")`)\n process.exit(2)\n }\n pct = n\n }\n try {\n await setupDmarc({\n domain,\n ruaMailbox,\n rufMailbox: typeof opts['ruf-mailbox'] === 'string' ? (opts['ruf-mailbox'] as string) : undefined,\n policy,\n pct,\n aspf: opts.aspf === 's' ? 's' : 'r',\n adkim: opts.adkim === 's' ? 's' : 'r',\n cloudflare: Boolean(opts.cloudflare),\n cloudflareZone: opts['cloudflare-zone'] ? String(opts['cloudflare-zone']) : undefined,\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 printDmarcUsage() {\n console.log(`\nmailery setup-dmarc — publish a DMARC TXT record for a domain.\n\nUsage:\n mailery setup-dmarc --domain <d> --rua-mailbox <email> [options]\n\nOptions:\n --domain <d> Domain to publish DMARC for (e.g. example.com)\n --rua-mailbox <email> Mailbox to receive RUA aggregate reports\n --ruf-mailbox <email> Mailbox to receive RUF forensic reports (rarely used)\n --policy <p> none | quarantine | reject (default: none)\n --pct <n> Percent of failing mail subject to the policy (1-100, default 100)\n --aspf r|s SPF alignment mode (default r — relaxed)\n --adkim r|s DKIM alignment mode (default r — relaxed)\n --cloudflare Publish TXT record via Cloudflare API\n --cloudflare-zone <z> Override inferred zone\n\nEnv:\n CLOUDFLARE_API_TOKEN Required when --cloudflare is set.\n\nRecommended progression: p=none → wait, monitor RUA → p=quarantine pct=10 → ramp pct → p=reject.\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"]}