mailery 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,24 @@ interface ContactAdapter {
47
47
  addTags?(externalId: string, tags: string[]): Promise<void>;
48
48
  removeTags?(externalId: string, tags: string[]): Promise<void>;
49
49
  }
50
+ /**
51
+ * Constrains WHEN a send step's email may go out. The flow's waits decide the
52
+ * earliest moment (T + N days); the window then pushes that moment forward —
53
+ * never backward — to the next allowed slot:
54
+ *
55
+ * - `timeOfDay` — deliver at this local wall-clock time ('HH:mm'). A send
56
+ * arriving after that time waits for the next day's slot (with a short
57
+ * grace period so tick jitter doesn't add 24h).
58
+ * - `weekdaysOnly` — a slot landing on Saturday/Sunday moves to Monday.
59
+ * - `useContactTimezone` — interpret times in `contact.timezone` when set,
60
+ * else fall back to `timezone` (IANA name, default UTC).
61
+ */
62
+ interface DeliveryWindow {
63
+ weekdaysOnly?: boolean;
64
+ timeOfDay?: string;
65
+ useContactTimezone?: boolean;
66
+ timezone?: string;
67
+ }
50
68
  type FlowStep = {
51
69
  type: 'wait';
52
70
  value: number;
@@ -65,6 +83,7 @@ type FlowStep = {
65
83
  templateSlug: string;
66
84
  providerOverride?: string;
67
85
  vars?: Record<string, unknown>;
86
+ delivery?: DeliveryWindow;
68
87
  } | {
69
88
  type: 'tag';
70
89
  addTags?: string[];
@@ -375,7 +394,7 @@ type QueueDriverConfig = {
375
394
  * server-shaped interfaces.
376
395
  */
377
396
  type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
378
- type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed';
397
+ type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed' | 'cancelled';
379
398
  type TemplateKind = 'transactional' | 'marketing';
380
399
  type SuppressionScope = 'all' | 'marketing' | 'transactional';
381
400
  type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
@@ -417,6 +436,63 @@ type SenderDomainValidation = {
417
436
  };
418
437
  declare function validateSenderDomain(fromEmail: string, templateKind: TemplateKind, registry: SenderDomainRegistry | undefined): SenderDomainValidation;
419
438
 
439
+ /**
440
+ * VarsAdapter — host-provided template variables, resolved at render time.
441
+ *
442
+ * The host declares a zod schema (the contract templates can rely on) plus a
443
+ * `resolve` function that loads those values from the host's own database for
444
+ * a given contact. Resolved keys are merged into the render context root, so
445
+ * a schema key `user` is referenced in templates as `{{user.name}}`.
446
+ *
447
+ * The schema does double duty: it is exposed to the admin SPA as JSON Schema
448
+ * (GET /vars-schema) to drive editor autocomplete, and the content linter
449
+ * uses it to flag `{{paths}}` that don't exist.
450
+ */
451
+
452
+ interface VarsResolveInfo {
453
+ /** Why the vars are being resolved. Previews and tests should be side-effect free. */
454
+ reason: 'send' | 'preview' | 'test';
455
+ /** Slug of the template being rendered, when known. */
456
+ templateSlug?: string;
457
+ /** Slug of the flow the send belongs to, when the render is part of a flow run. */
458
+ flowSlug?: string;
459
+ /** Name of the event that triggered the flow run, when applicable. */
460
+ eventName?: string;
461
+ /**
462
+ * Properties of the triggering event (`mailer.fire(name, id, properties)`).
463
+ * This is how a resolver scopes lookups for account/topic flows:
464
+ * `info.eventProperties?.accountId` tells it WHICH account the run is about.
465
+ * Also available raw in templates as `{{event.*}}`.
466
+ */
467
+ eventProperties?: Record<string, unknown>;
468
+ }
469
+ interface VarsAdapter<S extends z.ZodType = z.ZodType> {
470
+ /** Contract for what `resolve` returns. Root keys become root template variables. */
471
+ schema: S;
472
+ /** Load the variables for one contact from the host's data. */
473
+ resolve(contact: Contact, info: VarsResolveInfo): Promise<z.infer<S>> | z.infer<S>;
474
+ }
475
+ /**
476
+ * Identity helper that pins `resolve`'s return type to `z.infer<schema>` —
477
+ * without it, TypeScript widens the schema generic and the return type is
478
+ * unchecked.
479
+ *
480
+ * const varsAdapter = defineVars({
481
+ * schema: z.object({ user: z.object({ name: z.string() }) }),
482
+ * async resolve(contact) {
483
+ * return { user: { name: await lookupName(contact.externalId) } }
484
+ * },
485
+ * })
486
+ */
487
+ declare function defineVars<S extends z.ZodType>(adapter: VarsAdapter<S>): VarsAdapter<S>;
488
+ /**
489
+ * Render-context keys mailery owns. Resolved vars never override these — a
490
+ * schema that declares one is rejected at Mailer.init.
491
+ */
492
+ declare const RESERVED_VAR_KEYS: readonly ["contact", "vars", "event", "unsubscribeUrl", "viewInBrowserUrl", "preferenceCenterUrl", "senderAddress"];
493
+ /** JSON Schema for the adapter's zod schema (wire format for the admin SPA + linter). */
494
+ declare function varsJsonSchema(adapter: VarsAdapter): Record<string, unknown>;
495
+
420
496
  /**
421
497
  * Mailer configuration shape. Required + optional surfaces with sane defaults.
422
498
  */
@@ -430,6 +506,94 @@ interface RedisOptions {
430
506
  username?: string;
431
507
  tls?: boolean;
432
508
  }
509
+ interface DnsblListSpec {
510
+ /** DNS suffix to query, e.g. `'dbl.spamhaus.org'`. */
511
+ host: string;
512
+ /** Display label for the UI, e.g. `'Spamhaus DBL'`. */
513
+ label: string;
514
+ }
515
+ interface DnsblConfig {
516
+ /** Domain block lists (DBLs) — queried as `<domain>.<host>`. */
517
+ domainLists?: DnsblListSpec[];
518
+ /** IP block lists — queried as `<reversed-octets>.<host>`. */
519
+ ipLists?: DnsblListSpec[];
520
+ /** Dedicated sending IPs to check. Most operators on shared ESPs leave empty. */
521
+ dedicatedIps?: string[];
522
+ /** Hours between automatic runs. Default 24. Set to 0 to disable scheduled runs. */
523
+ intervalHours?: number;
524
+ }
525
+ interface MailTesterConfig {
526
+ /** API key from your Mail-Tester paid plan. */
527
+ apiKey: string;
528
+ /** Minimum score (0-10) below which publish is blocked. Default 8.0. */
529
+ minScore?: number;
530
+ /**
531
+ * How long a successful score remains cached for the same content. Default 24.
532
+ * Re-publishing the same body within the window does not burn a credit.
533
+ */
534
+ cacheHours?: number;
535
+ /** API base URL. Defaults to `https://mail-tester.com/api`. Override for staging or alternate provider. */
536
+ baseUrl?: string;
537
+ }
538
+ interface DmarcSourceTag {
539
+ /** Source IP, e.g. `'149.72.45.10'`. */
540
+ ip: string;
541
+ /** Operator-provided label, e.g. `'SendGrid (transactional)'` or `'Hubspot'`. */
542
+ label: string;
543
+ /** When true, ignore failures from this source in the alignment-trend headline. */
544
+ ignored?: boolean;
545
+ }
546
+ interface DmarcConfig {
547
+ /**
548
+ * Operator-tagged sources. After RUA reports start arriving, the operator
549
+ * marks each source IP as known/legit (with a label) or rogue. Tagged
550
+ * sources are excluded from the headline failure count, surfacing only
551
+ * unknown senders.
552
+ */
553
+ knownSources?: DmarcSourceTag[];
554
+ /**
555
+ * How many days of failure rows to retain. Default 90. Older rows are
556
+ * trimmed by the daily tick to keep the collection bounded.
557
+ */
558
+ retentionDays?: number;
559
+ }
560
+ interface SndsConfig {
561
+ /**
562
+ * SNDS automated-access key. Obtained per-account from the SNDS portal at
563
+ * https://sendersupport.olc.protection.outlook.com/snds/ once each
564
+ * sending IP has been added and verified.
565
+ */
566
+ accessKey: string;
567
+ /**
568
+ * Optional IP filter — when set, only rows for these IPs are persisted.
569
+ * Useful when an SNDS account covers a wider IP pool than this mailery
570
+ * deployment uses. Defaults to keeping all rows the API returns.
571
+ */
572
+ ips?: string[];
573
+ /** Hours between automatic pulls. Default 24. Set to 0 to disable scheduled pulls. */
574
+ intervalHours?: number;
575
+ }
576
+ interface PostmasterConfig {
577
+ /** OAuth client ID from a Google Cloud project with Postmaster Tools API enabled. */
578
+ clientId: string;
579
+ clientSecret: string;
580
+ /**
581
+ * A refresh token obtained via the OAuth consent flow with scope
582
+ * `https://www.googleapis.com/auth/postmaster.readonly` for the user that
583
+ * verified the domain(s) in Postmaster Tools. See
584
+ * docs/guide/deliverability.md → "Google Postmaster Tools" for the
585
+ * one-time setup procedure.
586
+ */
587
+ refreshToken: string;
588
+ /**
589
+ * Domains to pull data for. When omitted, mailery uses every domain from the
590
+ * senderDomains registry plus the From defaults. Each must be verified in
591
+ * Postmaster Tools under the same Google account.
592
+ */
593
+ domains?: string[];
594
+ /** Hours between automatic pulls. Default 24. Set to 0 to disable scheduled pulls. */
595
+ intervalHours?: number;
596
+ }
433
597
  interface CircuitBreakerThresholds {
434
598
  /** Trip when last-hour hard-bounce rate >= this percent (e.g. 2 = 2%). */
435
599
  hardBounceRatePctTrip: number;
@@ -443,6 +607,14 @@ interface MailerConfig {
443
607
  db: Db;
444
608
  collectionPrefix?: string;
445
609
  adapter: ContactAdapter;
610
+ /**
611
+ * Optional host-provided template variables. `resolve(contact)` runs at
612
+ * send/preview/test render time; its result is merged into the template
613
+ * context root (schema key `user` → `{{user.name}}`). The zod schema
614
+ * drives admin-editor autocomplete and the `unknown_variable` lint rule.
615
+ * Build one with `defineVars({ schema, resolve })`.
616
+ */
617
+ varsAdapter?: VarsAdapter<any>;
446
618
  /**
447
619
  * Queue driver selection. One of:
448
620
  * - `{ driver: 'bull', redis: ... }` — BullMQ (default for prod; requires Redis)
@@ -481,6 +653,41 @@ interface MailerConfig {
481
653
  /** How long the DOI token stays valid. Default 7 days. */
482
654
  doiTokenLifetimeDays?: number;
483
655
  circuitBreaker?: Partial<CircuitBreakerThresholds>;
656
+ /**
657
+ * Daily DNS-based block-list checks on sender domains and (optionally) any
658
+ * dedicated sending IPs. Results surface in setup-status and on the Health
659
+ * screen. No external API needed — plain DNS lookups.
660
+ */
661
+ dnsbl?: DnsblConfig;
662
+ /**
663
+ * Pulls daily reputation tiers + spam ratio + auth pass rates from Google
664
+ * Postmaster Tools. Trips the (domain × marketing) circuit breaker when a
665
+ * domain falls to reputation `BAD`. Only meaningful at >100/day to Gmail.
666
+ */
667
+ postmaster?: PostmasterConfig;
668
+ /**
669
+ * Pulls daily Outlook/Hotmail/Live IP reputation from Microsoft's Smart
670
+ * Network Data Services. IP-level rather than domain-level — only useful
671
+ * if you send from a dedicated IP. Visibility-only: RED filter results
672
+ * surface as a setup-status error, no auto-trip.
673
+ */
674
+ snds?: SndsConfig;
675
+ /**
676
+ * Optional Mail-Tester integration. When set, the template editor exposes
677
+ * a "Run deliverability check" button that sends a copy of the rendered
678
+ * draft to a Mail-Tester address and polls for a score (0-10) covering
679
+ * SpamAssassin verdict, auth alignment, content red flags, and link
680
+ * reputation. Score < minScore blocks publish until re-run or overridden.
681
+ */
682
+ mailTester?: MailTesterConfig;
683
+ /**
684
+ * DMARC RUA aggregate reports tell you who is sending mail that claims to
685
+ * be from your domain — both legitimate sources and spoofers. Operator
686
+ * uploads the report attachments (or POSTs them via SendGrid Inbound Parse
687
+ * to /admin/mailer/api/dmarc/upload). Mailery decompresses, parses, and
688
+ * surfaces alignment failures + unknown senders.
689
+ */
690
+ dmarc?: DmarcConfig;
484
691
  broadcastConfirmationThreshold?: number;
485
692
  broadcastEnqueueBatchSize?: number;
486
693
  broadcastEnqueueMaxWaiting?: number;
@@ -611,7 +818,7 @@ interface FlowVersionDoc {
611
818
  }
612
819
  interface FlowRunHistoryEntry {
613
820
  stepIndex: number;
614
- action: 'entered' | 'wait_started' | 'wait_completed' | 'condition_evaluated' | 'branch_taken' | 'sent' | 'send_skipped' | 'tagged' | 'event_fired' | 'webhook_called' | 'exited' | 'failed';
821
+ action: 'entered' | 'wait_started' | 'wait_completed' | 'condition_evaluated' | 'branch_taken' | 'sent' | 'send_skipped' | 'send_deferred' | 'tagged' | 'event_fired' | 'webhook_called' | 'exited' | 'failed';
615
822
  at: Date;
616
823
  details?: Record<string, unknown>;
617
824
  }
@@ -622,6 +829,17 @@ interface FlowRunDoc {
622
829
  flowSlug: string;
623
830
  flowVersion: number;
624
831
  emailAtEntry: string;
832
+ /**
833
+ * Snapshot of the event that triggered this run. Properties surface in
834
+ * templates as `{{event.*}}` and in `varsAdapter.resolve` via
835
+ * `info.eventProperties` — this is how account/topic-scoped flows know
836
+ * which account or topic the run is about. Null for non-event entries.
837
+ */
838
+ triggerEvent?: {
839
+ name: string;
840
+ properties: Record<string, unknown>;
841
+ occurredAt: Date;
842
+ } | null;
625
843
  enteredAt: Date;
626
844
  status: FlowRunStatus;
627
845
  currentStepIndex: number;
@@ -834,8 +1052,22 @@ interface WebhookEventDoc {
834
1052
  processed: boolean;
835
1053
  raw: unknown;
836
1054
  }
1055
+ /**
1056
+ * Health bucket. One doc per (senderDomain, kind) pair plus a single aggregate
1057
+ * doc with _id='agg' that rolls up everything.
1058
+ *
1059
+ * _id: 'agg' // cross-domain, cross-kind roll-up
1060
+ * _id: 'd:<senderDomain>|k:<kind>' // per-bucket
1061
+ *
1062
+ * The aggregate is informational only — it never trips. Trips live on
1063
+ * buckets so one subdomain tanking doesn't hold mail for the others.
1064
+ */
837
1065
  interface HealthDoc {
838
- _id: 'singleton';
1066
+ _id: string;
1067
+ /** null on the aggregate doc, set on bucket docs. */
1068
+ senderDomain: string | null;
1069
+ /** null on the aggregate doc, set on bucket docs. */
1070
+ kind: TemplateKind | null;
839
1071
  windowStartedAt: Date;
840
1072
  windowDurationMs: number;
841
1073
  counters: {
@@ -859,6 +1091,167 @@ interface HealthDoc {
859
1091
  manuallyResumedAt: Date | null;
860
1092
  updatedAt: Date;
861
1093
  }
1094
+ type PostmasterReputation = 'HIGH' | 'MEDIUM' | 'LOW' | 'BAD' | 'REPUTATION_CATEGORY_UNSPECIFIED';
1095
+ /**
1096
+ * One day's traffic stats for a domain from Google Postmaster Tools.
1097
+ * Stored per (domain, date) so we can chart trends.
1098
+ */
1099
+ interface PostmasterSnapshotDoc {
1100
+ _id?: ObjectId;
1101
+ domain: string;
1102
+ /** Reporting date in YYYY-MM-DD per Postmaster's stat day. */
1103
+ date: string;
1104
+ domainReputation: PostmasterReputation | null;
1105
+ ipReputations: Array<{
1106
+ reputation: PostmasterReputation;
1107
+ ipCount: number;
1108
+ }> | null;
1109
+ userReportedSpamRatio: number | null;
1110
+ spfSuccessRatio: number | null;
1111
+ dkimSuccessRatio: number | null;
1112
+ dmarcSuccessRatio: number | null;
1113
+ outboundEncryptionRatio: number | null;
1114
+ inboundEncryptionRatio: number | null;
1115
+ deliveryErrors: Array<{
1116
+ errorType: string;
1117
+ errorClass: string;
1118
+ errorRatio: number;
1119
+ }> | null;
1120
+ spammyFeedbackLoops: Array<{
1121
+ id: string;
1122
+ spamRatio: number;
1123
+ }> | null;
1124
+ fetchedAt: Date;
1125
+ }
1126
+ interface MailTesterFeedback {
1127
+ category: string;
1128
+ severity: 'info' | 'warning' | 'error';
1129
+ message: string;
1130
+ }
1131
+ /**
1132
+ * Cached Mail-Tester deliverability score for a given (template content)
1133
+ * fingerprint. Operators don't burn a credit re-running on identical
1134
+ * content within cacheHours.
1135
+ */
1136
+ interface MailTesterScoreDoc {
1137
+ _id?: ObjectId;
1138
+ templateSlug: string;
1139
+ /** Hash of bodyHash + subject + fromEmail — the cache key. */
1140
+ contentKey: string;
1141
+ checkId: string;
1142
+ score: number;
1143
+ feedback: MailTesterFeedback[];
1144
+ rawSummary: string | null;
1145
+ fetchedAt: Date;
1146
+ expiresAt: Date;
1147
+ }
1148
+ type DmarcPolicy = 'none' | 'quarantine' | 'reject';
1149
+ type DmarcAuthResult = 'pass' | 'fail' | 'softfail' | 'neutral' | 'temperror' | 'permerror' | 'none' | 'unknown';
1150
+ /**
1151
+ * One DMARC RUA aggregate report. Receivers (Google, Yahoo, Microsoft, etc.)
1152
+ * email one per day per sending domain. We store one DmarcReportDoc per
1153
+ * incoming report, plus one DmarcFailureDoc per non-aligned record row.
1154
+ */
1155
+ interface DmarcReportDoc {
1156
+ _id?: ObjectId;
1157
+ reportId: string;
1158
+ /** Reporting org, e.g. 'google.com', 'enterprise.protection.outlook.com'. */
1159
+ orgName: string;
1160
+ /** Reporter contact email from <report_metadata>. */
1161
+ email: string;
1162
+ /** The domain we publish DMARC for, from <policy_published><domain>. */
1163
+ domain: string;
1164
+ policyP: DmarcPolicy;
1165
+ /** % of mail subject to the policy (1-100). */
1166
+ policyPct: number;
1167
+ /** Activity window the report covers. */
1168
+ rangeStart: Date;
1169
+ rangeEnd: Date;
1170
+ /** Sum of <count> across all records. */
1171
+ totalMessages: number;
1172
+ /** Messages that passed DMARC (DKIM aligned OR SPF aligned). */
1173
+ passCount: number;
1174
+ failCount: number;
1175
+ receivedAt: Date;
1176
+ }
1177
+ /**
1178
+ * One alignment-failing source × day. Composite key `(sourceIp, domain, day)`
1179
+ * lets us roll up "this IP sent N misaligned messages over the past week."
1180
+ * Failures from multiple reports for the same key get counted once per
1181
+ * report (we de-dupe on reportId × sourceIp).
1182
+ */
1183
+ interface DmarcFailureDoc {
1184
+ _id?: ObjectId;
1185
+ reportId: string;
1186
+ domain: string;
1187
+ sourceIp: string;
1188
+ count: number;
1189
+ headerFrom: string;
1190
+ dkimResult: DmarcAuthResult;
1191
+ spfResult: DmarcAuthResult;
1192
+ dispositionApplied: 'none' | 'quarantine' | 'reject' | string;
1193
+ /** YYYY-MM-DD bucket derived from the reporting window's mid-point. */
1194
+ day: string;
1195
+ receivedAt: Date;
1196
+ }
1197
+ /**
1198
+ * Operator-set DMARC source tag. Created/updated/deleted from the admin UI
1199
+ * so operators can mark "this IP is our SendGrid", "this is Hubspot", etc.
1200
+ * Merged with `MailerConfig.dmarc.knownSources` at read time — the config
1201
+ * version is read-only baseline, the collection is mutable runtime state.
1202
+ */
1203
+ interface DmarcSourceTagDoc {
1204
+ _id?: ObjectId;
1205
+ ip: string;
1206
+ label: string;
1207
+ ignored: boolean;
1208
+ setBy: string;
1209
+ setAt: Date;
1210
+ }
1211
+ type SndsFilterResult = 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN';
1212
+ /**
1213
+ * One row from a Microsoft SNDS data export covering a single activity
1214
+ * window for one sending IP. Stored unique per (ip, activityStart) so
1215
+ * historical windows accumulate without re-writing.
1216
+ */
1217
+ interface SndsSnapshotDoc {
1218
+ _id?: ObjectId;
1219
+ ip: string;
1220
+ activityStart: Date;
1221
+ activityEnd: Date;
1222
+ rcptCommands: number;
1223
+ dataCommands: number;
1224
+ messageRecipients: number;
1225
+ filterResult: SndsFilterResult;
1226
+ /** 0-1 fraction. SNDS reports as "<0.1%", "0.1-0.3%", etc; we store the upper bound. null when unknown. */
1227
+ complaintRate: number | null;
1228
+ trapMessageCount: number;
1229
+ sampleHelo: string | null;
1230
+ sampleMailFrom: string | null;
1231
+ fetchedAt: Date;
1232
+ }
1233
+ type DnsblResult = 'clean' | 'listed' | 'error';
1234
+ type DnsblTargetKind = 'domain' | 'ip';
1235
+ /**
1236
+ * Latest DNSBL check result per (target, list). Replaced in-place on each run
1237
+ * so the collection stays small. History (if needed later) lives in audit_log.
1238
+ */
1239
+ interface DnsblCheckDoc {
1240
+ _id?: ObjectId;
1241
+ /** Domain (e.g. `mkt.example.com`) or dotted-quad IP (e.g. `203.0.113.5`). */
1242
+ target: string;
1243
+ targetKind: DnsblTargetKind;
1244
+ /** DNS suffix queried (e.g. `dbl.spamhaus.org`). */
1245
+ list: string;
1246
+ /** Display label for the UI (e.g. `Spamhaus DBL`). */
1247
+ listLabel: string;
1248
+ result: DnsblResult;
1249
+ /** A records returned by the lookup, when listed. */
1250
+ returnCodes: string[];
1251
+ /** When result is 'error', the message; otherwise null. */
1252
+ errorMessage: string | null;
1253
+ runAt: Date;
1254
+ }
862
1255
  interface ContactTagDoc {
863
1256
  _id?: ObjectId;
864
1257
  externalId: string;
@@ -883,6 +1276,13 @@ interface Collections {
883
1276
  webhookEvents: Collection<WebhookEventDoc>;
884
1277
  health: Collection<HealthDoc>;
885
1278
  contactTags: Collection<ContactTagDoc>;
1279
+ dnsblChecks: Collection<DnsblCheckDoc>;
1280
+ postmasterSnapshots: Collection<PostmasterSnapshotDoc>;
1281
+ sndsSnapshots: Collection<SndsSnapshotDoc>;
1282
+ dmarcReports: Collection<DmarcReportDoc>;
1283
+ dmarcFailures: Collection<DmarcFailureDoc>;
1284
+ dmarcSourceTags: Collection<DmarcSourceTagDoc>;
1285
+ mailTesterScores: Collection<MailTesterScoreDoc>;
886
1286
  }
887
1287
  declare function getCollections(db: Db, prefix?: string): Collections;
888
1288
  declare function ensureIndexes(db: Db, prefix?: string): Promise<void>;
@@ -902,6 +1302,7 @@ declare class EventRegistry {
902
1302
  register(reg: EventRegistration): void;
903
1303
  has(name: string): boolean;
904
1304
  policy(name: string): DedupePolicy | undefined;
1305
+ list(): EventRegistration[];
905
1306
  /**
906
1307
  * Derive a dedupeKey for an event call. Returns null when no policy is
907
1308
  * registered AND no key was passed — caller should throw.
@@ -918,10 +1319,27 @@ interface RunnerContext {
918
1319
  db: Db;
919
1320
  collections: Collections;
920
1321
  adapter: ContactAdapter;
1322
+ varsAdapter?: VarsAdapter;
921
1323
  providers: Record<string, MailProvider>;
922
1324
  queues: Queues;
923
1325
  config: ResolvedConfig;
924
1326
  handlebarsHelpers?: Record<string, Handlebars.HelperDelegate>;
1327
+ /**
1328
+ * Optional audit-log writer. Available when the runner context is derived
1329
+ * from a Mailer instance (Mailer.getRunnerContext()); absent in some
1330
+ * lightweight test harnesses. Runner code that uses this must tolerate
1331
+ * `undefined`.
1332
+ */
1333
+ audit?: (entry: {
1334
+ actor: string;
1335
+ action: string;
1336
+ resource: {
1337
+ collection: string;
1338
+ id?: string;
1339
+ slug?: string;
1340
+ };
1341
+ diffSummary?: string;
1342
+ }) => Promise<void>;
925
1343
  }
926
1344
 
927
1345
  /**
@@ -975,6 +1393,33 @@ declare class Mailer {
975
1393
  suppress(email: string, opts: Omit<SuppressInput, 'email'>): Promise<void>;
976
1394
  tag(externalId: string, tag: string): Promise<void>;
977
1395
  untag(externalId: string, tag: string): Promise<void>;
1396
+ /**
1397
+ * Abort every active run of one flow for a contact, immediately. Runs parked
1398
+ * in a `wait` exit too — their delayed wake-up jobs find the run exited and
1399
+ * no-op. Also cancels any of the flow's emails still sitting in the send
1400
+ * queue for this contact (queued or awaiting retry), so an abort means no
1401
+ * further mail, not just no further steps.
1402
+ *
1403
+ * No-op (returns zero counts) when nothing is active. Safe to call from the
1404
+ * same handler that processes the business event ("user upgraded").
1405
+ */
1406
+ abortFlow(flowSlug: string, externalId: string, opts?: {
1407
+ reason?: string;
1408
+ }): Promise<{
1409
+ abortedRuns: number;
1410
+ cancelledSends: number;
1411
+ }>;
1412
+ /**
1413
+ * Abort every active flow run for a contact across all flows. Same semantics
1414
+ * as `abortFlow` — for "stop everything" events (account deleted, churned).
1415
+ */
1416
+ abortAllFlows(externalId: string, opts?: {
1417
+ reason?: string;
1418
+ }): Promise<{
1419
+ abortedRuns: number;
1420
+ cancelledSends: number;
1421
+ }>;
1422
+ private abortActiveRuns;
978
1423
  /**
979
1424
  * GDPR right-to-erasure. Hard-deletes the contact's PII and leaves a hashed
980
1425
  * suppression row to block re-import. INVARIANT 9.
@@ -1024,4 +1469,4 @@ declare class NullProvider implements MailProvider {
1024
1469
  reset(): void;
1025
1470
  }
1026
1471
 
1027
- export { type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type SubscriptionStatus as D, type EventDoc as E, type FlowStep as F, type SuppressionDoc as G, type HealthDoc as H, type SuppressionReason as I, type TemplateKind as J, type TemplateVersionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, ensureIndexes as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, getCollections as U, validateSenderDomain as V, type WebhookEventDoc as W, type Contact as a, type SendResult as b, Mailer as c, type SuppressionScope as d, type SegmentFilter as e, type AuditLogDoc as f, type BroadcastStatus as g, type CircuitBreakerThresholds as h, type Collections as i, type ContactTagDoc as j, type FlowDoc as k, type FlowGoal as l, type FlowRunDoc as m, type FlowRunStatus as n, type FlowVersionDoc as o, type HealthStatus as p, type MailerConfig as q, NullProvider as r, type RedisOptions as s, type SegmentDefinition as t, type SendDoc as u, type SendStatus as v, type SenderDomainConfig as w, type SenderDomainRegistry as x, type SenderDomainValidation as y, type SubscriptionDoc as z };
1472
+ export { getCollections as $, type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type DeliveryWindow as D, type EventDoc as E, type FlowStep as F, type SenderDomainValidation as G, type HealthDoc as H, type SubscriptionDoc as I, type SubscriptionStatus as J, type SuppressionDoc as K, type LeadDoc as L, type MailProvider as M, type NormalizedEvent as N, type OutboxDoc as O, type Predicate as P, type SuppressionReason as Q, type RunnerContext as R, type SendArgs as S, type TemplateDoc as T, type TemplateKind as U, type TemplateVersionDoc as V, type VarsAdapter as W, type VarsResolveInfo as X, type WebhookEventDoc as Y, defineVars as Z, ensureIndexes as _, type Contact as a, validateSenderDomain as a0, varsJsonSchema as a1, type SendResult as b, type MailTesterFeedback as c, Mailer as d, type SuppressionScope as e, type SegmentFilter as f, type AuditLogDoc as g, type BroadcastStatus as h, type CircuitBreakerThresholds as i, type Collections as j, type ContactTagDoc as k, type FlowDoc as l, type FlowGoal as m, type FlowRunDoc as n, type FlowRunStatus as o, type FlowVersionDoc as p, type HealthStatus as q, type MailerConfig as r, NullProvider as s, RESERVED_VAR_KEYS as t, type RedisOptions as u, type SegmentDefinition as v, type SendDoc as w, type SendStatus as x, type SenderDomainConfig as y, type SenderDomainRegistry as z };