mailery 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/admin/spa/index-csgOs39s.js +41 -0
- package/dist/admin/spa/index-csgOs39s.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-DUZfzI5F.js → template-editor-BuYSO1FP.js} +2 -2
- package/dist/admin/spa/{template-editor-DUZfzI5F.js.map → template-editor-BuYSO1FP.js.map} +1 -1
- package/dist/index.cjs +657 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +657 -134
- package/dist/index.js.map +1 -1
- package/dist/{null-OzIqP7A8.d.cts → null-DaisDvB_.d.cts} +123 -59
- package/dist/{null-OzIqP7A8.d.ts → null-DaisDvB_.d.ts} +123 -59
- package/dist/testing.cjs +365 -136
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +365 -136
- package/dist/testing.js.map +1 -1
- package/package.json +29 -4
- package/dist/admin/spa/index-DlhleV6M.js +0 -41
- package/dist/admin/spa/index-DlhleV6M.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
|
|
2
|
-
import IORedis from 'ioredis';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import Handlebars from 'handlebars';
|
|
4
|
+
import IORedis from 'ioredis';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Shared types — used by both server and client. Stub placeholders for Phase 0.
|
|
@@ -310,6 +310,109 @@ declare const sendOneOffInputSchema: z.ZodObject<{
|
|
|
310
310
|
}, z.core.$strip>;
|
|
311
311
|
type SendOneOffInput = z.infer<typeof sendOneOffInputSchema>;
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Queue driver abstraction. Mailer talks to a `QueueDriver` whose only required
|
|
315
|
+
* vocabulary is "add a job to one of four named queues" and "spin up a worker
|
|
316
|
+
* per queue with these handlers." Concrete drivers (BullMQ, @hokify/agenda,
|
|
317
|
+
* noop) implement this surface.
|
|
318
|
+
*
|
|
319
|
+
* Mailery targets single-process deployments; drivers are not expected to
|
|
320
|
+
* coordinate state across processes. Rate-limit semantics are documented per
|
|
321
|
+
* driver below.
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
/** Per-call options passed to `queue.add(name, data, opts)`. */
|
|
325
|
+
interface AddOptions {
|
|
326
|
+
/** Defer execution by this many ms. */
|
|
327
|
+
delay?: number;
|
|
328
|
+
/** Max attempts including the initial run. Driver may apply per-define instead of per-add. */
|
|
329
|
+
attempts?: number;
|
|
330
|
+
/** Exponential backoff base delay (ms). Driver may apply per-define instead of per-add. */
|
|
331
|
+
backoff?: {
|
|
332
|
+
type: 'exponential';
|
|
333
|
+
delay: number;
|
|
334
|
+
};
|
|
335
|
+
/** Idempotency key: if a pending job with this id already exists for this queue, the add is a no-op. */
|
|
336
|
+
jobId?: string;
|
|
337
|
+
}
|
|
338
|
+
interface QueueAPI {
|
|
339
|
+
add(name: string, data: unknown, opts?: AddOptions): Promise<unknown>;
|
|
340
|
+
/** Approximate count of jobs eligible-but-not-running. Used by broadcast backpressure. */
|
|
341
|
+
getWaitingCount(): Promise<number>;
|
|
342
|
+
close(): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
interface Queues {
|
|
345
|
+
tick: QueueAPI;
|
|
346
|
+
advance: QueueAPI;
|
|
347
|
+
send: QueueAPI;
|
|
348
|
+
webhook: QueueAPI;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Driver selection at Mailer.init time. The driver and its connection are
|
|
352
|
+
* declared explicitly rather than inferred — this keeps the wiring obvious and
|
|
353
|
+
* lets hosts mix-and-match (e.g. Bull in prod, noop in tests).
|
|
354
|
+
*/
|
|
355
|
+
type QueueDriverConfig = {
|
|
356
|
+
driver: 'bull';
|
|
357
|
+
redis: RedisOptions | IORedis;
|
|
358
|
+
} | {
|
|
359
|
+
driver: 'agenda';
|
|
360
|
+
db?: Db;
|
|
361
|
+
processEverySeconds?: number;
|
|
362
|
+
lockLifetimeSeconds?: number;
|
|
363
|
+
collectionName?: string;
|
|
364
|
+
} | {
|
|
365
|
+
driver: 'noop';
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Shared enums — string-literal unions for status fields and other discriminators.
|
|
370
|
+
* Kept separate from types.ts so the client can import these without pulling in
|
|
371
|
+
* server-shaped interfaces.
|
|
372
|
+
*/
|
|
373
|
+
type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
|
|
374
|
+
type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed';
|
|
375
|
+
type TemplateKind = 'transactional' | 'marketing';
|
|
376
|
+
type SuppressionScope = 'all' | 'marketing' | 'transactional';
|
|
377
|
+
type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
|
|
378
|
+
type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
|
|
379
|
+
type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
|
|
380
|
+
type HealthStatus = 'healthy' | 'degraded' | 'tripped';
|
|
381
|
+
type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Sender-domain validator.
|
|
385
|
+
*
|
|
386
|
+
* Hosts may declare a `senderDomains` registry mapping a domain to which kind
|
|
387
|
+
* of email is allowed to send from it. Used to isolate marketing reputation
|
|
388
|
+
* from transactional reputation:
|
|
389
|
+
*
|
|
390
|
+
* senderDomains: {
|
|
391
|
+
* 'news.example.com': { kind: 'marketing' },
|
|
392
|
+
* 'mail.example.com': { kind: 'transactional' },
|
|
393
|
+
* 'tools.example.com': { kind: 'both' }, // OK for either kind
|
|
394
|
+
* }
|
|
395
|
+
*
|
|
396
|
+
* The check runs at template publish time. If a template's `fromEmail` domain
|
|
397
|
+
* isn't declared, or is declared for the wrong kind, publish fails with a
|
|
398
|
+
* descriptive 400. Hosts who don't set `senderDomains` get no enforcement —
|
|
399
|
+
* back-compat default.
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
interface SenderDomainConfig {
|
|
403
|
+
/** Which template `kind` may use this domain as a From address. */
|
|
404
|
+
kind: 'marketing' | 'transactional' | 'both';
|
|
405
|
+
}
|
|
406
|
+
type SenderDomainRegistry = Record<string, SenderDomainConfig>;
|
|
407
|
+
type SenderDomainValidation = {
|
|
408
|
+
ok: true;
|
|
409
|
+
} | {
|
|
410
|
+
ok: false;
|
|
411
|
+
code: 'invalid_email' | 'unregistered_domain' | 'wrong_kind';
|
|
412
|
+
reason: string;
|
|
413
|
+
};
|
|
414
|
+
declare function validateSenderDomain(fromEmail: string, templateKind: TemplateKind, registry: SenderDomainRegistry | undefined): SenderDomainValidation;
|
|
415
|
+
|
|
313
416
|
/**
|
|
314
417
|
* Mailer configuration shape. Required + optional surfaces with sane defaults.
|
|
315
418
|
*/
|
|
@@ -337,10 +440,12 @@ interface MailerConfig {
|
|
|
337
440
|
collectionPrefix?: string;
|
|
338
441
|
adapter: ContactAdapter;
|
|
339
442
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
443
|
+
* Queue driver selection. One of:
|
|
444
|
+
* - `{ driver: 'bull', redis: ... }` — BullMQ (default for prod; requires Redis)
|
|
445
|
+
* - `{ driver: 'agenda' }` — @hokify/agenda using this Mongo (no Redis required, single-process)
|
|
446
|
+
* - `{ driver: 'noop' }` — no background workers (tests, synchronous-only hosts)
|
|
342
447
|
*/
|
|
343
|
-
|
|
448
|
+
queue: QueueDriverConfig;
|
|
344
449
|
providers: Record<string, MailProvider>;
|
|
345
450
|
defaultProvider: string;
|
|
346
451
|
defaultTransactionalProvider?: string;
|
|
@@ -355,6 +460,15 @@ interface MailerConfig {
|
|
|
355
460
|
name: string;
|
|
356
461
|
email: string;
|
|
357
462
|
};
|
|
463
|
+
/**
|
|
464
|
+
* Optional registry of sender domains by kind. When set, mailery enforces at
|
|
465
|
+
* template publish time that a template's `fromEmail` domain is declared and
|
|
466
|
+
* matches the template's `kind`. Used to keep marketing reputation isolated
|
|
467
|
+
* from transactional reputation by sending from separate verified domains.
|
|
468
|
+
*
|
|
469
|
+
* Domains are case-insensitive. If unset, no enforcement happens.
|
|
470
|
+
*/
|
|
471
|
+
senderDomains?: SenderDomainRegistry;
|
|
358
472
|
requireDoubleOptIn?: boolean;
|
|
359
473
|
unsubscribeTokenLifetimeDays?: number;
|
|
360
474
|
transactionalRespectUnsubscribe?: boolean;
|
|
@@ -393,21 +507,6 @@ type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireD
|
|
|
393
507
|
circuitBreaker: CircuitBreakerThresholds;
|
|
394
508
|
} & MailerConfig;
|
|
395
509
|
|
|
396
|
-
/**
|
|
397
|
-
* Shared enums — string-literal unions for status fields and other discriminators.
|
|
398
|
-
* Kept separate from types.ts so the client can import these without pulling in
|
|
399
|
-
* server-shaped interfaces.
|
|
400
|
-
*/
|
|
401
|
-
type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
|
|
402
|
-
type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed';
|
|
403
|
-
type TemplateKind = 'transactional' | 'marketing';
|
|
404
|
-
type SuppressionScope = 'all' | 'marketing' | 'transactional';
|
|
405
|
-
type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
|
|
406
|
-
type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
|
|
407
|
-
type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
|
|
408
|
-
type HealthStatus = 'healthy' | 'degraded' | 'tripped';
|
|
409
|
-
type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
|
|
410
|
-
|
|
411
510
|
/**
|
|
412
511
|
* Mongo collection helpers + indexes for every mailer-owned collection.
|
|
413
512
|
*
|
|
@@ -641,6 +740,8 @@ interface SendDoc {
|
|
|
641
740
|
unsubscribedAt: Date | null;
|
|
642
741
|
complainedAt: Date | null;
|
|
643
742
|
queuedAt: Date;
|
|
743
|
+
/** Last time the row was mutated; used by the stranded-send sweep. */
|
|
744
|
+
updatedAt: Date;
|
|
644
745
|
sentAt: Date | null;
|
|
645
746
|
deliveredAt: Date | null;
|
|
646
747
|
}
|
|
@@ -804,42 +905,6 @@ declare class EventRegistry {
|
|
|
804
905
|
deriveKey(name: string, externalId: string, passedKey: string | undefined, now: Date): string | null;
|
|
805
906
|
}
|
|
806
907
|
|
|
807
|
-
/**
|
|
808
|
-
* BullMQ wiring: four queues + the corresponding worker factories.
|
|
809
|
-
*
|
|
810
|
-
* mailer:tick → recovery sweep + event-trigger scan + scheduled broadcasts + outbox drain
|
|
811
|
-
* mailer:advance → per-flow_run wakeup at nextActionAt (delayed jobs)
|
|
812
|
-
* mailer:send → provider dispatch for a single send row
|
|
813
|
-
* mailer:webhook → async normalization + apply of inbound provider events
|
|
814
|
-
*
|
|
815
|
-
* The Mailer class instantiates queues at init() and workers at startWorkers().
|
|
816
|
-
* Job handlers themselves live in `runner/` and `api/webhook-processor.ts`.
|
|
817
|
-
*/
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Minimal queue surface the runner depends on. Production wraps BullMQ; tests
|
|
821
|
-
* can supply a no-op implementation.
|
|
822
|
-
*/
|
|
823
|
-
interface QueueAPI {
|
|
824
|
-
add(name: string, data: unknown, opts?: {
|
|
825
|
-
delay?: number;
|
|
826
|
-
attempts?: number;
|
|
827
|
-
backoff?: {
|
|
828
|
-
type: 'exponential';
|
|
829
|
-
delay: number;
|
|
830
|
-
};
|
|
831
|
-
jobId?: string;
|
|
832
|
-
}): Promise<unknown>;
|
|
833
|
-
getWaitingCount(): Promise<number>;
|
|
834
|
-
close(): Promise<void>;
|
|
835
|
-
}
|
|
836
|
-
interface Queues {
|
|
837
|
-
tick: QueueAPI;
|
|
838
|
-
advance: QueueAPI;
|
|
839
|
-
send: QueueAPI;
|
|
840
|
-
webhook: QueueAPI;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
908
|
/**
|
|
844
909
|
* Runner context + public entry points. The shared context object is passed
|
|
845
910
|
* to every handler so the runner stays a pure function over (state, action).
|
|
@@ -868,12 +933,11 @@ declare class Mailer {
|
|
|
868
933
|
readonly collections: Collections;
|
|
869
934
|
readonly adapter: ContactAdapter;
|
|
870
935
|
readonly providers: Record<string, MailProvider>;
|
|
871
|
-
readonly redis: IORedis | null;
|
|
872
936
|
readonly queues: Queues;
|
|
873
937
|
readonly config: ResolvedConfig;
|
|
874
938
|
readonly events: EventRegistry;
|
|
875
|
-
private
|
|
876
|
-
private
|
|
939
|
+
private queueDriver;
|
|
940
|
+
private workersStarted;
|
|
877
941
|
private runnerContext;
|
|
878
942
|
private constructor();
|
|
879
943
|
/**
|
|
@@ -956,4 +1020,4 @@ declare class NullProvider implements MailProvider {
|
|
|
956
1020
|
reset(): void;
|
|
957
1021
|
}
|
|
958
1022
|
|
|
959
|
-
export { type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type
|
|
1023
|
+
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 };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Db, ObjectId, Collection, ClientSession } from 'mongodb';
|
|
2
|
-
import IORedis from 'ioredis';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import Handlebars from 'handlebars';
|
|
4
|
+
import IORedis from 'ioredis';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Shared types — used by both server and client. Stub placeholders for Phase 0.
|
|
@@ -310,6 +310,109 @@ declare const sendOneOffInputSchema: z.ZodObject<{
|
|
|
310
310
|
}, z.core.$strip>;
|
|
311
311
|
type SendOneOffInput = z.infer<typeof sendOneOffInputSchema>;
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Queue driver abstraction. Mailer talks to a `QueueDriver` whose only required
|
|
315
|
+
* vocabulary is "add a job to one of four named queues" and "spin up a worker
|
|
316
|
+
* per queue with these handlers." Concrete drivers (BullMQ, @hokify/agenda,
|
|
317
|
+
* noop) implement this surface.
|
|
318
|
+
*
|
|
319
|
+
* Mailery targets single-process deployments; drivers are not expected to
|
|
320
|
+
* coordinate state across processes. Rate-limit semantics are documented per
|
|
321
|
+
* driver below.
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
/** Per-call options passed to `queue.add(name, data, opts)`. */
|
|
325
|
+
interface AddOptions {
|
|
326
|
+
/** Defer execution by this many ms. */
|
|
327
|
+
delay?: number;
|
|
328
|
+
/** Max attempts including the initial run. Driver may apply per-define instead of per-add. */
|
|
329
|
+
attempts?: number;
|
|
330
|
+
/** Exponential backoff base delay (ms). Driver may apply per-define instead of per-add. */
|
|
331
|
+
backoff?: {
|
|
332
|
+
type: 'exponential';
|
|
333
|
+
delay: number;
|
|
334
|
+
};
|
|
335
|
+
/** Idempotency key: if a pending job with this id already exists for this queue, the add is a no-op. */
|
|
336
|
+
jobId?: string;
|
|
337
|
+
}
|
|
338
|
+
interface QueueAPI {
|
|
339
|
+
add(name: string, data: unknown, opts?: AddOptions): Promise<unknown>;
|
|
340
|
+
/** Approximate count of jobs eligible-but-not-running. Used by broadcast backpressure. */
|
|
341
|
+
getWaitingCount(): Promise<number>;
|
|
342
|
+
close(): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
interface Queues {
|
|
345
|
+
tick: QueueAPI;
|
|
346
|
+
advance: QueueAPI;
|
|
347
|
+
send: QueueAPI;
|
|
348
|
+
webhook: QueueAPI;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Driver selection at Mailer.init time. The driver and its connection are
|
|
352
|
+
* declared explicitly rather than inferred — this keeps the wiring obvious and
|
|
353
|
+
* lets hosts mix-and-match (e.g. Bull in prod, noop in tests).
|
|
354
|
+
*/
|
|
355
|
+
type QueueDriverConfig = {
|
|
356
|
+
driver: 'bull';
|
|
357
|
+
redis: RedisOptions | IORedis;
|
|
358
|
+
} | {
|
|
359
|
+
driver: 'agenda';
|
|
360
|
+
db?: Db;
|
|
361
|
+
processEverySeconds?: number;
|
|
362
|
+
lockLifetimeSeconds?: number;
|
|
363
|
+
collectionName?: string;
|
|
364
|
+
} | {
|
|
365
|
+
driver: 'noop';
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Shared enums — string-literal unions for status fields and other discriminators.
|
|
370
|
+
* Kept separate from types.ts so the client can import these without pulling in
|
|
371
|
+
* server-shaped interfaces.
|
|
372
|
+
*/
|
|
373
|
+
type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
|
|
374
|
+
type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed';
|
|
375
|
+
type TemplateKind = 'transactional' | 'marketing';
|
|
376
|
+
type SuppressionScope = 'all' | 'marketing' | 'transactional';
|
|
377
|
+
type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
|
|
378
|
+
type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
|
|
379
|
+
type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
|
|
380
|
+
type HealthStatus = 'healthy' | 'degraded' | 'tripped';
|
|
381
|
+
type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Sender-domain validator.
|
|
385
|
+
*
|
|
386
|
+
* Hosts may declare a `senderDomains` registry mapping a domain to which kind
|
|
387
|
+
* of email is allowed to send from it. Used to isolate marketing reputation
|
|
388
|
+
* from transactional reputation:
|
|
389
|
+
*
|
|
390
|
+
* senderDomains: {
|
|
391
|
+
* 'news.example.com': { kind: 'marketing' },
|
|
392
|
+
* 'mail.example.com': { kind: 'transactional' },
|
|
393
|
+
* 'tools.example.com': { kind: 'both' }, // OK for either kind
|
|
394
|
+
* }
|
|
395
|
+
*
|
|
396
|
+
* The check runs at template publish time. If a template's `fromEmail` domain
|
|
397
|
+
* isn't declared, or is declared for the wrong kind, publish fails with a
|
|
398
|
+
* descriptive 400. Hosts who don't set `senderDomains` get no enforcement —
|
|
399
|
+
* back-compat default.
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
interface SenderDomainConfig {
|
|
403
|
+
/** Which template `kind` may use this domain as a From address. */
|
|
404
|
+
kind: 'marketing' | 'transactional' | 'both';
|
|
405
|
+
}
|
|
406
|
+
type SenderDomainRegistry = Record<string, SenderDomainConfig>;
|
|
407
|
+
type SenderDomainValidation = {
|
|
408
|
+
ok: true;
|
|
409
|
+
} | {
|
|
410
|
+
ok: false;
|
|
411
|
+
code: 'invalid_email' | 'unregistered_domain' | 'wrong_kind';
|
|
412
|
+
reason: string;
|
|
413
|
+
};
|
|
414
|
+
declare function validateSenderDomain(fromEmail: string, templateKind: TemplateKind, registry: SenderDomainRegistry | undefined): SenderDomainValidation;
|
|
415
|
+
|
|
313
416
|
/**
|
|
314
417
|
* Mailer configuration shape. Required + optional surfaces with sane defaults.
|
|
315
418
|
*/
|
|
@@ -337,10 +440,12 @@ interface MailerConfig {
|
|
|
337
440
|
collectionPrefix?: string;
|
|
338
441
|
adapter: ContactAdapter;
|
|
339
442
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
443
|
+
* Queue driver selection. One of:
|
|
444
|
+
* - `{ driver: 'bull', redis: ... }` — BullMQ (default for prod; requires Redis)
|
|
445
|
+
* - `{ driver: 'agenda' }` — @hokify/agenda using this Mongo (no Redis required, single-process)
|
|
446
|
+
* - `{ driver: 'noop' }` — no background workers (tests, synchronous-only hosts)
|
|
342
447
|
*/
|
|
343
|
-
|
|
448
|
+
queue: QueueDriverConfig;
|
|
344
449
|
providers: Record<string, MailProvider>;
|
|
345
450
|
defaultProvider: string;
|
|
346
451
|
defaultTransactionalProvider?: string;
|
|
@@ -355,6 +460,15 @@ interface MailerConfig {
|
|
|
355
460
|
name: string;
|
|
356
461
|
email: string;
|
|
357
462
|
};
|
|
463
|
+
/**
|
|
464
|
+
* Optional registry of sender domains by kind. When set, mailery enforces at
|
|
465
|
+
* template publish time that a template's `fromEmail` domain is declared and
|
|
466
|
+
* matches the template's `kind`. Used to keep marketing reputation isolated
|
|
467
|
+
* from transactional reputation by sending from separate verified domains.
|
|
468
|
+
*
|
|
469
|
+
* Domains are case-insensitive. If unset, no enforcement happens.
|
|
470
|
+
*/
|
|
471
|
+
senderDomains?: SenderDomainRegistry;
|
|
358
472
|
requireDoubleOptIn?: boolean;
|
|
359
473
|
unsubscribeTokenLifetimeDays?: number;
|
|
360
474
|
transactionalRespectUnsubscribe?: boolean;
|
|
@@ -393,21 +507,6 @@ type ResolvedConfig = Required<Pick<MailerConfig, 'collectionPrefix' | 'requireD
|
|
|
393
507
|
circuitBreaker: CircuitBreakerThresholds;
|
|
394
508
|
} & MailerConfig;
|
|
395
509
|
|
|
396
|
-
/**
|
|
397
|
-
* Shared enums — string-literal unions for status fields and other discriminators.
|
|
398
|
-
* Kept separate from types.ts so the client can import these without pulling in
|
|
399
|
-
* server-shaped interfaces.
|
|
400
|
-
*/
|
|
401
|
-
type SubscriptionStatus = 'subscribed' | 'pending_doi' | 'unsubscribed' | 'bounced' | 'complained';
|
|
402
|
-
type SendStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'bounced' | 'complained' | 'failed' | 'suppressed';
|
|
403
|
-
type TemplateKind = 'transactional' | 'marketing';
|
|
404
|
-
type SuppressionScope = 'all' | 'marketing' | 'transactional';
|
|
405
|
-
type SuppressionReason = 'unsubscribed' | 'hard_bounce' | 'complaint' | 'manual' | 'list_cleaning' | 'gdpr_forget';
|
|
406
|
-
type FlowRunStatus = 'active' | 'completed' | 'exited' | 'failed';
|
|
407
|
-
type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed';
|
|
408
|
-
type HealthStatus = 'healthy' | 'degraded' | 'tripped';
|
|
409
|
-
type FlowGoal = 'activation' | 'conversion' | 'retention' | 'reactivation' | 'transactional' | 'broadcast';
|
|
410
|
-
|
|
411
510
|
/**
|
|
412
511
|
* Mongo collection helpers + indexes for every mailer-owned collection.
|
|
413
512
|
*
|
|
@@ -641,6 +740,8 @@ interface SendDoc {
|
|
|
641
740
|
unsubscribedAt: Date | null;
|
|
642
741
|
complainedAt: Date | null;
|
|
643
742
|
queuedAt: Date;
|
|
743
|
+
/** Last time the row was mutated; used by the stranded-send sweep. */
|
|
744
|
+
updatedAt: Date;
|
|
644
745
|
sentAt: Date | null;
|
|
645
746
|
deliveredAt: Date | null;
|
|
646
747
|
}
|
|
@@ -804,42 +905,6 @@ declare class EventRegistry {
|
|
|
804
905
|
deriveKey(name: string, externalId: string, passedKey: string | undefined, now: Date): string | null;
|
|
805
906
|
}
|
|
806
907
|
|
|
807
|
-
/**
|
|
808
|
-
* BullMQ wiring: four queues + the corresponding worker factories.
|
|
809
|
-
*
|
|
810
|
-
* mailer:tick → recovery sweep + event-trigger scan + scheduled broadcasts + outbox drain
|
|
811
|
-
* mailer:advance → per-flow_run wakeup at nextActionAt (delayed jobs)
|
|
812
|
-
* mailer:send → provider dispatch for a single send row
|
|
813
|
-
* mailer:webhook → async normalization + apply of inbound provider events
|
|
814
|
-
*
|
|
815
|
-
* The Mailer class instantiates queues at init() and workers at startWorkers().
|
|
816
|
-
* Job handlers themselves live in `runner/` and `api/webhook-processor.ts`.
|
|
817
|
-
*/
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Minimal queue surface the runner depends on. Production wraps BullMQ; tests
|
|
821
|
-
* can supply a no-op implementation.
|
|
822
|
-
*/
|
|
823
|
-
interface QueueAPI {
|
|
824
|
-
add(name: string, data: unknown, opts?: {
|
|
825
|
-
delay?: number;
|
|
826
|
-
attempts?: number;
|
|
827
|
-
backoff?: {
|
|
828
|
-
type: 'exponential';
|
|
829
|
-
delay: number;
|
|
830
|
-
};
|
|
831
|
-
jobId?: string;
|
|
832
|
-
}): Promise<unknown>;
|
|
833
|
-
getWaitingCount(): Promise<number>;
|
|
834
|
-
close(): Promise<void>;
|
|
835
|
-
}
|
|
836
|
-
interface Queues {
|
|
837
|
-
tick: QueueAPI;
|
|
838
|
-
advance: QueueAPI;
|
|
839
|
-
send: QueueAPI;
|
|
840
|
-
webhook: QueueAPI;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
908
|
/**
|
|
844
909
|
* Runner context + public entry points. The shared context object is passed
|
|
845
910
|
* to every handler so the runner stays a pure function over (state, action).
|
|
@@ -868,12 +933,11 @@ declare class Mailer {
|
|
|
868
933
|
readonly collections: Collections;
|
|
869
934
|
readonly adapter: ContactAdapter;
|
|
870
935
|
readonly providers: Record<string, MailProvider>;
|
|
871
|
-
readonly redis: IORedis | null;
|
|
872
936
|
readonly queues: Queues;
|
|
873
937
|
readonly config: ResolvedConfig;
|
|
874
938
|
readonly events: EventRegistry;
|
|
875
|
-
private
|
|
876
|
-
private
|
|
939
|
+
private queueDriver;
|
|
940
|
+
private workersStarted;
|
|
877
941
|
private runnerContext;
|
|
878
942
|
private constructor();
|
|
879
943
|
/**
|
|
@@ -956,4 +1020,4 @@ declare class NullProvider implements MailProvider {
|
|
|
956
1020
|
reset(): void;
|
|
957
1021
|
}
|
|
958
1022
|
|
|
959
|
-
export { type AdapterFilter as A, type BroadcastDoc as B, type ContactAdapter as C, type
|
|
1023
|
+
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 };
|