mailery 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -131,11 +131,11 @@ async function setupSendgrid(opts) {
131
131
  info("");
132
132
  info(`=== ${domain} ===`);
133
133
  info(`SendGrid \u2192 checking domain authentication for ${domain}\u2026`);
134
- const existing = await sg.findDomainAuth(domain);
134
+ const existing2 = await sg.findDomainAuth(domain);
135
135
  let auth;
136
- if (existing) {
137
- info(` \u2192 reusing existing domain auth #${existing.id}`);
138
- auth = existing;
136
+ if (existing2) {
137
+ info(` \u2192 reusing existing domain auth #${existing2.id}`);
138
+ auth = existing2;
139
139
  } else {
140
140
  info(` \u2192 creating new domain auth`);
141
141
  auth = await sg.createDomainAuth({
@@ -187,20 +187,8 @@ async function setupSendgrid(opts) {
187
187
  });
188
188
  }
189
189
  info("");
190
- info("=== Event webhook (account-level) ===");
191
- info("SendGrid \u2192 checking Signed Event Webhook\u2026");
192
- const signed = await sg.getSignedWebhookSettings();
193
- let webhookKey;
194
- if (signed.public_key) {
195
- info(" = signing already enabled; reusing existing public key");
196
- webhookKey = signed.public_key;
197
- } else {
198
- info(" \u2192 enabling signing");
199
- webhookKey = await sg.enableSignedWebhook();
200
- info(" \u2713 verification key fetched");
201
- }
190
+ info("=== Event webhook ===");
202
191
  info("SendGrid \u2192 checking Event Webhook\u2026");
203
- const currentEvents = await sg.getEventWebhookSettings();
204
192
  const desiredEvents = {
205
193
  url: opts.webhookUrl,
206
194
  enabled: true,
@@ -216,16 +204,59 @@ async function setupSendgrid(opts) {
216
204
  group_resubscribe: false,
217
205
  group_unsubscribe: false
218
206
  };
219
- if (currentEvents.url && currentEvents.url !== opts.webhookUrl && !opts.force) {
207
+ const listed = await sg.listEventWebhooks();
208
+ const existing = listed.webhooks.find((w) => sameWebhookUrl(w.url, opts.webhookUrl));
209
+ let webhookId;
210
+ if (listed.mode === "multi" && existing && !existing.id) {
220
211
  throw new Error(
221
- `Event webhook is already configured for ${currentEvents.url}. Pass --force to overwrite (this will redirect all future events to ${opts.webhookUrl}).`
212
+ `SendGrid listed an event webhook for ${opts.webhookUrl} but did not return an id for it, so it can't be updated without risking another webhook on the account. Update it from the SendGrid dashboard instead.`
222
213
  );
223
214
  }
224
- if (eventSettingsMatch(currentEvents, desiredEvents)) {
225
- info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`);
215
+ if (listed.mode === "multi" && existing) {
216
+ webhookId = existing.id;
217
+ const label = ` #${existing.id}`;
218
+ if (eventSettingsMatch(existing, desiredEvents)) {
219
+ info(` = webhook${label} already configured for ${opts.webhookUrl}; nothing to change`);
220
+ } else {
221
+ await sg.updateEventWebhook(existing.id, desiredEvents);
222
+ info(` \u2713 webhook${label} updated for ${opts.webhookUrl}`);
223
+ }
224
+ } else if (listed.mode === "multi") {
225
+ const friendlyName = (opts.webhookName ?? defaultWebhookName(opts.webhookUrl, opts.domains)).slice(0, 64);
226
+ const created = await sg.createEventWebhook({ ...desiredEvents, friendly_name: friendlyName });
227
+ webhookId = created.id;
228
+ info(` + created event webhook${created.id ? ` #${created.id}` : ""} \u2192 ${opts.webhookUrl} ("${friendlyName}")`);
229
+ if (listed.webhooks.length > 0) {
230
+ info(` = ${listed.webhooks.length} other event webhook(s) on this account left untouched`);
231
+ if (opts.force) info(" (--force had no effect: other webhooks are never repointed on this account)");
232
+ }
226
233
  } else {
227
- await sg.setEventWebhookSettings(desiredEvents);
228
- info(` \u2713 webhook URL set to ${opts.webhookUrl}`);
234
+ const current = listed.webhooks[0] ?? {};
235
+ if (current.url && !sameWebhookUrl(current.url, opts.webhookUrl)) {
236
+ if (!opts.force) {
237
+ throw new Error(
238
+ `This SendGrid account only exposes the legacy single-webhook API, and its event webhook is already pointed at ${current.url}. Repointing it would silently stop event delivery to whatever consumes that URL (bounces, spam reports and unsubscribes would stop ingesting there). Pass --force to repoint it to ${opts.webhookUrl} anyway.`
239
+ );
240
+ }
241
+ warn(` ! repointing the account's only event webhook away from ${current.url} (--force)`);
242
+ }
243
+ if (eventSettingsMatch(current, desiredEvents)) {
244
+ info(` = webhook already configured for ${opts.webhookUrl}; nothing to change`);
245
+ } else {
246
+ await sg.setEventWebhookSettings(desiredEvents);
247
+ info(` \u2713 webhook URL set to ${opts.webhookUrl}`);
248
+ }
249
+ }
250
+ info("SendGrid \u2192 checking Signed Event Webhook\u2026");
251
+ const signed = await sg.getSignedWebhookSettings(webhookId);
252
+ let webhookKey;
253
+ if (signed.public_key) {
254
+ info(" = signing already enabled; reusing existing public key");
255
+ webhookKey = signed.public_key;
256
+ } else {
257
+ info(" \u2192 enabling signing");
258
+ webhookKey = await sg.enableSignedWebhook(webhookId);
259
+ info(" \u2713 verification key fetched");
229
260
  }
230
261
  const envSnippet = [
231
262
  `# Add to your environment (or .env file):`,
@@ -238,16 +269,18 @@ async function setupSendgrid(opts) {
238
269
  domains: domainResults,
239
270
  webhookKey,
240
271
  webhookUrl: opts.webhookUrl,
272
+ webhookId,
241
273
  envSnippet
242
274
  };
243
275
  }
244
276
  function sendgridClient(apiKey, fetchFn) {
245
277
  const base = "https://api.sendgrid.com/v3";
246
278
  const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
247
- async function call(path, init = {}) {
279
+ async function call(path, init = {}, o = {}) {
248
280
  const res = await fetchFn(`${base}${path}`, { ...init, headers: { ...headers, ...init.headers } });
249
281
  const text = await res.text();
250
282
  const body = text ? safeJson(text) : null;
283
+ if (o.soft404 && res.status === 404) return null;
251
284
  if (res.status === 401 || res.status === 403) {
252
285
  throw new Error(
253
286
  `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.`
@@ -259,6 +292,7 @@ function sendgridClient(apiKey, fetchFn) {
259
292
  }
260
293
  return body;
261
294
  }
295
+ const signedPath = (id) => `/user/webhooks/event/settings/signed${id ? `/${encodeURIComponent(id)}` : ""}`;
262
296
  return {
263
297
  async findDomainAuth(domain) {
264
298
  const list = await call(`/whitelabel/domains?limit=200`) ?? [];
@@ -274,20 +308,37 @@ function sendgridClient(apiKey, fetchFn) {
274
308
  const r = await call(`/whitelabel/domains/${id}/validate`, { method: "POST" });
275
309
  return Boolean(r?.valid);
276
310
  },
277
- async getSignedWebhookSettings() {
278
- return call(`/user/webhooks/event/settings/signed`);
311
+ async getSignedWebhookSettings(id) {
312
+ return call(signedPath(id));
279
313
  },
280
- async enableSignedWebhook() {
281
- await call(`/user/webhooks/event/settings/signed`, {
314
+ async enableSignedWebhook(id) {
315
+ const patched = await call(signedPath(id), {
282
316
  method: "PATCH",
283
317
  body: JSON.stringify({ enabled: true })
284
318
  });
285
- const r = await call(`/user/webhooks/event/settings/signed`);
319
+ if (patched?.public_key) return patched.public_key;
320
+ const r = await call(signedPath(id));
286
321
  if (!r?.public_key) throw new Error("SendGrid did not return a public_key for the signed event webhook");
287
322
  return r.public_key;
288
323
  },
289
- async getEventWebhookSettings() {
290
- return call(`/user/webhooks/event/settings`);
324
+ async listEventWebhooks() {
325
+ const all = await call(`/user/webhooks/event/settings/all`, {}, { soft404: true });
326
+ const multi = Array.isArray(all) ? all : all?.webhooks;
327
+ if (Array.isArray(multi)) return { mode: "multi", webhooks: multi };
328
+ const legacy = await call(`/user/webhooks/event/settings`);
329
+ return { mode: "legacy", webhooks: legacy ? [legacy] : [] };
330
+ },
331
+ async createEventWebhook(payload) {
332
+ return call(`/user/webhooks/event/settings`, {
333
+ method: "POST",
334
+ body: JSON.stringify(payload)
335
+ });
336
+ },
337
+ async updateEventWebhook(id, payload) {
338
+ await call(`/user/webhooks/event/settings/${encodeURIComponent(id)}`, {
339
+ method: "PATCH",
340
+ body: JSON.stringify(payload)
341
+ });
291
342
  },
292
343
  async setEventWebhookSettings(payload) {
293
344
  await call(`/user/webhooks/event/settings`, {
@@ -304,6 +355,19 @@ function safeJson(s) {
304
355
  return null;
305
356
  }
306
357
  }
358
+ function sameWebhookUrl(a, b) {
359
+ if (!a) return false;
360
+ const norm = (u) => u.trim().replace(/\/+$/, "");
361
+ return norm(a) === norm(b);
362
+ }
363
+ function defaultWebhookName(webhookUrl, domains) {
364
+ let host = "";
365
+ try {
366
+ host = new URL(webhookUrl).host;
367
+ } catch {
368
+ }
369
+ return `mailery ${host || domains[0] || "events"}`;
370
+ }
307
371
  function eventSettingsMatch(current, desired) {
308
372
  for (const [k, v] of Object.entries(desired)) {
309
373
  if (current[k] !== v) return false;
@@ -467,7 +531,7 @@ async function runInteractive(prompter, env, logger = console) {
467
531
  }
468
532
  log("");
469
533
  const forceRaw = await prompter(
470
- "If a different webhook URL is already configured on this SendGrid account, overwrite it? (y/N)",
534
+ "If this account only supports one event webhook and it points elsewhere, repoint it? (y/N \u2014 stops event delivery to the old URL; ignored on multi-webhook accounts)",
471
535
  { defaultValue: "n" }
472
536
  );
473
537
  const force = /^y/i.test(forceRaw);
@@ -547,6 +611,7 @@ async function main() {
547
611
  domains: argDomains,
548
612
  subdomain: opts.subdomain ? String(opts.subdomain) : void 0,
549
613
  webhookUrl: argWebhook,
614
+ webhookName: opts["webhook-name"] ? String(opts["webhook-name"]) : void 0,
550
615
  cloudflare: Boolean(opts.cloudflare),
551
616
  cloudflareZone: opts["cloudflare-zone"] ? String(opts["cloudflare-zone"]) : void 0,
552
617
  force: Boolean(opts.force)
@@ -685,15 +750,22 @@ Usage:
685
750
  Options:
686
751
  --domain <d> Domain to authenticate. Repeat (or comma-separate)
687
752
  to authenticate multiple in one run, e.g. when
688
- isolating marketing from transactional. The event
689
- webhook itself is account-level and only gets set up
690
- once.
753
+ isolating marketing from transactional. One event
754
+ webhook is set up per run, regardless of domain count.
691
755
  --subdomain <s> Sub-label for the link branding CNAME (default: em)
692
- --webhook-url <u> Public URL for the SendGrid event webhook
756
+ --webhook-url <u> Public URL for the SendGrid event webhook. Matched
757
+ against the account's existing webhooks: the one with
758
+ this URL is updated, otherwise a new one is created
759
+ alongside them. Other apps' webhooks are left alone.
760
+ --webhook-name <n> friendly_name for a newly created webhook, shown in
761
+ the SendGrid dashboard (default: "mailery <host>")
693
762
  --cloudflare Publish DNS records via the Cloudflare API
694
763
  --cloudflare-zone <z> Override the inferred zone (e.g. for multi-label
695
764
  public suffixes like .co.uk)
696
- --force Replace an existing event webhook URL if one is set
765
+ --force Legacy single-webhook accounts only: repoint the
766
+ account's one event webhook at --webhook-url. This
767
+ stops event delivery to whatever consumed the old URL.
768
+ Ignored on accounts with the multi-webhook API.
697
769
  --interactive Force interactive mode even when args are supplied
698
770
  --no-interactive Disable interactive mode even when args are missing
699
771
  (the script exits with usage instead of prompting \u2014
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
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":["readline","input","output"],"mappings":"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","file":"cli.cjs","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"]}
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":["existing","readline","input","output"],"mappings":"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","file":"cli.cjs","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, the event\n * webhook matching `--webhook-url` is updated in place (others on the account\n * are never touched), existing Cloudflare DNS records with the correct values\n * 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. One webhook per install, regardless of domain count. */\n webhookUrl: string\n /** `friendly_name` for a newly created webhook, so the SendGrid dashboard shows which app owns it. */\n webhookName?: 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 /**\n * Legacy single-webhook accounts only: repoint the account's one event webhook\n * at `webhookUrl`. Ignored on accounts exposing the multi-webhook API, where a\n * new webhook is created alongside the existing ones instead.\n */\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 /** SendGrid's id for this install's webhook. Absent on legacy single-webhook accounts. */\n webhookId?: 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 ===')\n\n // ---- Resolve this install's event webhook -------------------------------\n // SendGrid's legacy endpoints (`/user/webhooks/event/settings`, `/settings/signed`)\n // present the account as if it had exactly one event webhook: a PATCH there\n // repoints whichever webhook is oldest by created_date. On an account shared\n // with another mailery install (or staging alongside prod) that silently\n // steals its events — no error anywhere, the other endpoint just stops being\n // called, so its suppression list quietly freezes. Prefer the multi-webhook\n // endpoints, which address webhooks by id, and match on url so each install\n // only ever touches its own. Fall back to the legacy singleton only when the\n // account/key can't see `/settings/all`.\n info('SendGrid → checking Event Webhook…')\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\n const listed = await sg.listEventWebhooks()\n const existing = listed.webhooks.find((w) => sameWebhookUrl(w.url, opts.webhookUrl))\n let webhookId: string | undefined\n\n if (listed.mode === 'multi' && existing && !existing.id) {\n // Without an id the only way to write is the account-wide endpoint, which\n // repoints the oldest webhook — possibly someone else's. Stop instead.\n throw new Error(\n `SendGrid listed an event webhook for ${opts.webhookUrl} but did not return an id for it, so it can't be ` +\n `updated without risking another webhook on the account. Update it from the SendGrid dashboard instead.`,\n )\n }\n\n if (listed.mode === 'multi' && existing) {\n webhookId = existing.id\n const label = ` #${existing.id}`\n if (eventSettingsMatch(existing, desiredEvents)) {\n info(` = webhook${label} already configured for ${opts.webhookUrl}; nothing to change`)\n } else {\n await sg.updateEventWebhook(existing.id!, desiredEvents)\n info(` ✓ webhook${label} updated for ${opts.webhookUrl}`)\n }\n } else if (listed.mode === 'multi') {\n const friendlyName = (opts.webhookName ?? defaultWebhookName(opts.webhookUrl, opts.domains)).slice(0, 64)\n const created = await sg.createEventWebhook({ ...desiredEvents, friendly_name: friendlyName })\n webhookId = created.id\n info(` + created event webhook${created.id ? ` #${created.id}` : ''} → ${opts.webhookUrl} (\"${friendlyName}\")`)\n if (listed.webhooks.length > 0) {\n info(` = ${listed.webhooks.length} other event webhook(s) on this account left untouched`)\n if (opts.force) info(' (--force had no effect: other webhooks are never repointed on this account)')\n }\n } else {\n // Legacy singleton account: there is only one webhook slot, so pointing it\n // at our URL takes it away from whoever had it. Refuse unless --force.\n const current = listed.webhooks[0] ?? {}\n if (current.url && !sameWebhookUrl(current.url, opts.webhookUrl)) {\n if (!opts.force) {\n throw new Error(\n `This SendGrid account only exposes the legacy single-webhook API, and its event webhook is already ` +\n `pointed at ${current.url}. Repointing it would silently stop event delivery to whatever consumes ` +\n `that URL (bounces, spam reports and unsubscribes would stop ingesting there). Pass --force to ` +\n `repoint it to ${opts.webhookUrl} anyway.`,\n )\n }\n warn(` ! repointing the account's only event webhook away from ${current.url} (--force)`)\n }\n if (eventSettingsMatch(current, 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\n // ---- Enable Signed Event Webhook + fetch this webhook's public key ------\n info('SendGrid → checking Signed Event Webhook…')\n // GET /user/webhooks/event/settings/signed[/{id}] returns `{ public_key }`\n // (plus `id` on the multi form). A non-empty key implies signing is already\n // enabled — re-PATCHing `enabled: true` rotates the keypair, silently\n // breaking signature verification everywhere the old key was deployed.\n const signed = await sg.getSignedWebhookSettings(webhookId)\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(webhookId)\n info(' ✓ verification key fetched')\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 webhookId,\n envSnippet,\n }\n}\n\n// ---------------------------------------------------------------------------\n// SendGrid API client\n// ---------------------------------------------------------------------------\n\ninterface SgEventWebhook {\n id?: string\n url?: string\n friendly_name?: string\n enabled?: boolean\n public_key?: string | null\n [k: string]: unknown\n}\n\ninterface SgEventWebhookList {\n /** 'multi' when `/settings/all` answered; 'legacy' when only the singleton endpoint is available. */\n mode: 'multi' | 'legacy'\n webhooks: SgEventWebhook[]\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(id?: string): Promise<{ id?: string; public_key?: string }>\n enableSignedWebhook(id?: string): Promise<string>\n listEventWebhooks(): Promise<SgEventWebhookList>\n createEventWebhook(payload: Record<string, unknown>): Promise<SgEventWebhook>\n updateEventWebhook(id: string, payload: Record<string, unknown>): Promise<void>\n /** Legacy singleton PATCH — repoints the account's oldest webhook. Fallback only. */\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 = {}, o: { soft404?: boolean } = {}): 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 (o.soft404 && res.status === 404) return 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 /** Per-webhook signing key when we know the id; account-oldest fallback when we don't. */\n const signedPath = (id?: string) =>\n `/user/webhooks/event/settings/signed${id ? `/${encodeURIComponent(id)}` : ''}`\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(id?: string) {\n return call(signedPath(id))\n },\n async enableSignedWebhook(id?: string) {\n // PATCH returns `{ id, public_key }`; re-GET only if some proxy strips it.\n const patched = await call(signedPath(id), {\n method: 'PATCH',\n body: JSON.stringify({ enabled: true }),\n })\n if (patched?.public_key) return patched.public_key as string\n const r = await call(signedPath(id))\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 listEventWebhooks(): Promise<SgEventWebhookList> {\n const all = await call(`/user/webhooks/event/settings/all`, {}, { soft404: true })\n const multi = Array.isArray(all) ? all : all?.webhooks\n if (Array.isArray(multi)) return { mode: 'multi', webhooks: multi as SgEventWebhook[] }\n const legacy = await call(`/user/webhooks/event/settings`)\n return { mode: 'legacy', webhooks: legacy ? [legacy as SgEventWebhook] : [] }\n },\n async createEventWebhook(payload) {\n return call(`/user/webhooks/event/settings`, {\n method: 'POST',\n body: JSON.stringify(payload),\n }) as Promise<SgEventWebhook>\n },\n async updateEventWebhook(id, payload) {\n await call(`/user/webhooks/event/settings/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n body: JSON.stringify(payload),\n })\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/** Match a SendGrid-reported webhook URL against the one we were asked to configure.\n * Exact compare apart from a trailing slash — anything looser risks adopting (and\n * then rewriting) a webhook that belongs to a different app. */\nfunction sameWebhookUrl(a: string | undefined | null, b: string): boolean {\n if (!a) return false\n const norm = (u: string) => u.trim().replace(/\\/+$/, '')\n return norm(a) === norm(b)\n}\n\n/** `friendly_name` for a webhook we create, so the SendGrid dashboard shows its owner. */\nfunction defaultWebhookName(webhookUrl: string, domains: string[]): string {\n let host = ''\n try {\n host = new URL(webhookUrl).host\n } catch {\n // Non-absolute URL — fall back to the first domain being authenticated.\n }\n return `mailery ${host || domains[0] || 'events'}`\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 this account only supports one event webhook and it points elsewhere, repoint it? ' +\n '(y/N — stops event delivery to the old URL; ignored on multi-webhook accounts)',\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 webhookName: opts['webhook-name'] ? String(opts['webhook-name']) : undefined,\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. One event\n webhook is set up per run, regardless of domain count.\n --subdomain <s> Sub-label for the link branding CNAME (default: em)\n --webhook-url <u> Public URL for the SendGrid event webhook. Matched\n against the account's existing webhooks: the one with\n this URL is updated, otherwise a new one is created\n alongside them. Other apps' webhooks are left alone.\n --webhook-name <n> friendly_name for a newly created webhook, shown in\n the SendGrid dashboard (default: \"mailery <host>\")\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 Legacy single-webhook accounts only: repoint the\n account's one event webhook at --webhook-url. This\n stops event delivery to whatever consumed the old URL.\n Ignored on accounts with the multi-webhook API.\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"]}