mailery 0.5.1 → 0.8.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/README.md +3 -1
- package/dist/admin/spa/{index-Dmsla2eL.js → index-B3ndbezm.js} +8 -8
- package/dist/admin/spa/index-B3ndbezm.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-Bn28OPMU.js → template-editor-DwMKy5sw.js} +105 -105
- package/dist/admin/spa/template-editor-DwMKy5sw.js.map +1 -0
- package/dist/index.cjs +477 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +475 -41
- package/dist/index.js.map +1 -1
- package/dist/{null-BYB6v4Iv.d.cts → null-BCxlHRJ0.d.cts} +134 -3
- package/dist/{null-BYB6v4Iv.d.ts → null-BCxlHRJ0.d.ts} +134 -3
- package/dist/testing.cjs +634 -82
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +233 -13
- package/dist/testing.d.ts +233 -13
- package/dist/testing.js +628 -83
- package/dist/testing.js.map +1 -1
- package/package.json +4 -1
- package/dist/admin/spa/index-Dmsla2eL.js.map +0 -1
- package/dist/admin/spa/template-editor-Bn28OPMU.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { ObjectId } from 'mongodb';
|
|
2
3
|
import crypto2 from 'crypto';
|
|
3
4
|
import sgMail from '@sendgrid/mail';
|
|
4
|
-
import { z } from 'zod';
|
|
5
5
|
import IORedis from 'ioredis';
|
|
6
6
|
import Handlebars from 'handlebars';
|
|
7
7
|
import { convert } from 'html-to-text';
|
|
@@ -31,6 +31,55 @@ var __export = (target, all) => {
|
|
|
31
31
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// src/server/adapters/vars.ts
|
|
35
|
+
var vars_exports = {};
|
|
36
|
+
__export(vars_exports, {
|
|
37
|
+
RESERVED_VAR_KEYS: () => RESERVED_VAR_KEYS,
|
|
38
|
+
assertNoReservedVarKeys: () => assertNoReservedVarKeys,
|
|
39
|
+
defineVars: () => defineVars,
|
|
40
|
+
resolveVars: () => resolveVars,
|
|
41
|
+
varsJsonSchema: () => varsJsonSchema
|
|
42
|
+
});
|
|
43
|
+
function defineVars(adapter) {
|
|
44
|
+
return adapter;
|
|
45
|
+
}
|
|
46
|
+
function assertNoReservedVarKeys(adapter) {
|
|
47
|
+
const json = varsJsonSchema(adapter);
|
|
48
|
+
const props = json && typeof json === "object" ? json.properties : void 0;
|
|
49
|
+
if (!props) return;
|
|
50
|
+
const clashes = RESERVED_VAR_KEYS.filter((k) => k in props);
|
|
51
|
+
if (clashes.length > 0) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`varsAdapter schema declares reserved key(s): ${clashes.join(", ")}. These names are provided by mailery itself \u2014 rename them in your schema.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function varsJsonSchema(adapter) {
|
|
58
|
+
return z.toJSONSchema(adapter.schema, { io: "output" });
|
|
59
|
+
}
|
|
60
|
+
async function resolveVars(adapter, contact, info) {
|
|
61
|
+
if (!adapter) return {};
|
|
62
|
+
const resolved = await adapter.resolve(contact, info);
|
|
63
|
+
if (!resolved || typeof resolved !== "object") return {};
|
|
64
|
+
const out = { ...resolved };
|
|
65
|
+
for (const k of RESERVED_VAR_KEYS) delete out[k];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
var RESERVED_VAR_KEYS;
|
|
69
|
+
var init_vars = __esm({
|
|
70
|
+
"src/server/adapters/vars.ts"() {
|
|
71
|
+
RESERVED_VAR_KEYS = [
|
|
72
|
+
"contact",
|
|
73
|
+
"vars",
|
|
74
|
+
"event",
|
|
75
|
+
"unsubscribeUrl",
|
|
76
|
+
"viewInBrowserUrl",
|
|
77
|
+
"preferenceCenterUrl",
|
|
78
|
+
"senderAddress"
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
34
83
|
// src/server/adapters/mongo.ts
|
|
35
84
|
var mongo_exports = {};
|
|
36
85
|
__export(mongo_exports, {
|
|
@@ -344,6 +393,12 @@ var tagInputSchema = z.object({
|
|
|
344
393
|
externalId: externalIdSchema,
|
|
345
394
|
tag: z.string().min(1).max(128)
|
|
346
395
|
});
|
|
396
|
+
var abortFlowInputSchema = z.object({
|
|
397
|
+
flowSlug: slugSchema,
|
|
398
|
+
externalId: externalIdSchema,
|
|
399
|
+
reason: z.string().min(1).max(200).optional()
|
|
400
|
+
});
|
|
401
|
+
var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
|
|
347
402
|
var sendOneOffInputSchema = z.object({
|
|
348
403
|
templateSlug: slugSchema,
|
|
349
404
|
externalId: externalIdSchema,
|
|
@@ -373,7 +428,13 @@ var flowStepSchema = z.lazy(
|
|
|
373
428
|
type: z.literal("send"),
|
|
374
429
|
templateSlug: slugSchema,
|
|
375
430
|
providerOverride: z.string().optional(),
|
|
376
|
-
vars: z.record(z.string(), z.unknown()).optional()
|
|
431
|
+
vars: z.record(z.string(), z.unknown()).optional(),
|
|
432
|
+
delivery: z.object({
|
|
433
|
+
weekdaysOnly: z.boolean().optional(),
|
|
434
|
+
timeOfDay: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:mm").optional(),
|
|
435
|
+
useContactTimezone: z.boolean().optional(),
|
|
436
|
+
timezone: z.string().optional()
|
|
437
|
+
}).optional()
|
|
377
438
|
}),
|
|
378
439
|
z.object({
|
|
379
440
|
type: z.literal("tag"),
|
|
@@ -1163,6 +1224,7 @@ async function tryEnterFlow(flow, event, ctx) {
|
|
|
1163
1224
|
flowSlug: flow.slug,
|
|
1164
1225
|
flowVersion: flow.version,
|
|
1165
1226
|
emailAtEntry: sub.emailAtSubscribe,
|
|
1227
|
+
triggerEvent: { name: event.name, properties: event.properties ?? {}, occurredAt: event.occurredAt },
|
|
1166
1228
|
enteredAt: /* @__PURE__ */ new Date(),
|
|
1167
1229
|
status: "active",
|
|
1168
1230
|
currentStepIndex: 0,
|
|
@@ -1260,6 +1322,105 @@ function effectiveLowerBound(ctx, opts) {
|
|
|
1260
1322
|
}
|
|
1261
1323
|
return null;
|
|
1262
1324
|
}
|
|
1325
|
+
|
|
1326
|
+
// src/server/runner/delivery-window.ts
|
|
1327
|
+
var TIME_OF_DAY_GRACE_MS = 60 * 6e4;
|
|
1328
|
+
function computeDeliveryTime(now, window, contactTimezone) {
|
|
1329
|
+
const tz = pickTimezone(window, contactTimezone);
|
|
1330
|
+
let candidate = now;
|
|
1331
|
+
if (window.timeOfDay) {
|
|
1332
|
+
const [hh, mm] = window.timeOfDay.split(":").map(Number);
|
|
1333
|
+
const local = localParts(candidate, tz);
|
|
1334
|
+
const todaySlot = utcFromLocal(local.y, local.mo, local.d, hh, mm, tz);
|
|
1335
|
+
if (candidate.getTime() < todaySlot.getTime()) {
|
|
1336
|
+
candidate = todaySlot;
|
|
1337
|
+
} else if (candidate.getTime() - todaySlot.getTime() > TIME_OF_DAY_GRACE_MS) {
|
|
1338
|
+
const next = addLocalDays(local, 1);
|
|
1339
|
+
candidate = utcFromLocal(next.y, next.mo, next.d, hh, mm, tz);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (window.weekdaysOnly) {
|
|
1343
|
+
for (let guard = 0; guard < 3; guard++) {
|
|
1344
|
+
const local = localParts(candidate, tz);
|
|
1345
|
+
if (local.weekday !== "Sat" && local.weekday !== "Sun") break;
|
|
1346
|
+
const shift = local.weekday === "Sat" ? 2 : 1;
|
|
1347
|
+
const moved = addLocalDays(local, shift);
|
|
1348
|
+
candidate = utcFromLocal(moved.y, moved.mo, moved.d, local.hh, local.mi, tz);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return candidate;
|
|
1352
|
+
}
|
|
1353
|
+
function pickTimezone(window, contactTimezone) {
|
|
1354
|
+
const candidates = [
|
|
1355
|
+
window.useContactTimezone ? contactTimezone : void 0,
|
|
1356
|
+
window.timezone,
|
|
1357
|
+
"UTC"
|
|
1358
|
+
];
|
|
1359
|
+
for (const tz of candidates) {
|
|
1360
|
+
if (tz && isValidTimezone(tz)) return tz;
|
|
1361
|
+
}
|
|
1362
|
+
return "UTC";
|
|
1363
|
+
}
|
|
1364
|
+
var validatedZones = /* @__PURE__ */ new Map();
|
|
1365
|
+
function isValidTimezone(tz) {
|
|
1366
|
+
const cached = validatedZones.get(tz);
|
|
1367
|
+
if (cached !== void 0) return cached;
|
|
1368
|
+
let ok = true;
|
|
1369
|
+
try {
|
|
1370
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1371
|
+
} catch {
|
|
1372
|
+
ok = false;
|
|
1373
|
+
}
|
|
1374
|
+
validatedZones.set(tz, ok);
|
|
1375
|
+
return ok;
|
|
1376
|
+
}
|
|
1377
|
+
var partFormatters = /* @__PURE__ */ new Map();
|
|
1378
|
+
function formatterFor(tz) {
|
|
1379
|
+
let f = partFormatters.get(tz);
|
|
1380
|
+
if (!f) {
|
|
1381
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
1382
|
+
timeZone: tz,
|
|
1383
|
+
year: "numeric",
|
|
1384
|
+
month: "2-digit",
|
|
1385
|
+
day: "2-digit",
|
|
1386
|
+
hour: "2-digit",
|
|
1387
|
+
minute: "2-digit",
|
|
1388
|
+
second: "2-digit",
|
|
1389
|
+
weekday: "short",
|
|
1390
|
+
hour12: false
|
|
1391
|
+
});
|
|
1392
|
+
partFormatters.set(tz, f);
|
|
1393
|
+
}
|
|
1394
|
+
return f;
|
|
1395
|
+
}
|
|
1396
|
+
function localParts(date, tz) {
|
|
1397
|
+
const parts = {};
|
|
1398
|
+
for (const p of formatterFor(tz).formatToParts(date)) parts[p.type] = p.value;
|
|
1399
|
+
return {
|
|
1400
|
+
y: Number(parts.year),
|
|
1401
|
+
mo: Number(parts.month),
|
|
1402
|
+
d: Number(parts.day),
|
|
1403
|
+
hh: Number(parts.hour) % 24,
|
|
1404
|
+
// Intl emits '24' for midnight in some locales
|
|
1405
|
+
mi: Number(parts.minute),
|
|
1406
|
+
ss: Number(parts.second),
|
|
1407
|
+
weekday: parts.weekday
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function utcFromLocal(y, mo, d, hh, mi, tz) {
|
|
1411
|
+
let ts = Date.UTC(y, mo - 1, d, hh, mi, 0);
|
|
1412
|
+
for (let i = 0; i < 2; i++) {
|
|
1413
|
+
const p = localParts(new Date(ts), tz);
|
|
1414
|
+
const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mi, p.ss);
|
|
1415
|
+
const offset = asUtc - ts;
|
|
1416
|
+
ts = Date.UTC(y, mo - 1, d, hh, mi, 0) - offset;
|
|
1417
|
+
}
|
|
1418
|
+
return new Date(ts);
|
|
1419
|
+
}
|
|
1420
|
+
function addLocalDays(p, days) {
|
|
1421
|
+
const dt = new Date(Date.UTC(p.y, p.mo - 1, p.d + days));
|
|
1422
|
+
return { y: dt.getUTCFullYear(), mo: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
|
|
1423
|
+
}
|
|
1263
1424
|
async function compileTemplate(mjml) {
|
|
1264
1425
|
const out = await mjml2html(mjml, { validationLevel: "soft", minify: false });
|
|
1265
1426
|
const plainText = derivePlaintext(out.html);
|
|
@@ -1383,6 +1544,9 @@ function makeHandlebars(extra) {
|
|
|
1383
1544
|
return hb;
|
|
1384
1545
|
}
|
|
1385
1546
|
|
|
1547
|
+
// src/server/runner/send.ts
|
|
1548
|
+
init_vars();
|
|
1549
|
+
|
|
1386
1550
|
// src/server/runner/suppression.ts
|
|
1387
1551
|
var SCOPES_BY_KIND = {
|
|
1388
1552
|
marketing: ["all", "marketing"],
|
|
@@ -1701,13 +1865,29 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1701
1865
|
return;
|
|
1702
1866
|
}
|
|
1703
1867
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1868
|
+
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
1869
|
+
await ctx.collections.sends.updateOne(
|
|
1870
|
+
{ _id: send._id },
|
|
1871
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${run.exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
1872
|
+
);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
let renderCtx;
|
|
1876
|
+
let rendered;
|
|
1877
|
+
try {
|
|
1878
|
+
const resolved = await resolveVars(ctx.varsAdapter, contact, {
|
|
1879
|
+
reason: "send",
|
|
1880
|
+
templateSlug: template.slug,
|
|
1881
|
+
flowSlug: run?.flowSlug,
|
|
1882
|
+
eventName: run?.triggerEvent?.name,
|
|
1883
|
+
eventProperties: run?.triggerEvent?.properties
|
|
1884
|
+
});
|
|
1885
|
+
renderCtx = buildRenderContext(contact, run, send.vars ?? {}, ctx, resolved);
|
|
1886
|
+
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
|
|
1889
|
+
throw err;
|
|
1890
|
+
}
|
|
1711
1891
|
const tracking = applyTracking(rendered.html, {
|
|
1712
1892
|
sendId: String(send._id),
|
|
1713
1893
|
publicUrl: ctx.config.publicUrl,
|
|
@@ -1734,6 +1914,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1734
1914
|
await markFailed(send._id, `provider_unknown: ${send.provider}`, ctx);
|
|
1735
1915
|
return;
|
|
1736
1916
|
}
|
|
1917
|
+
const headers = send.kind === "marketing" ? {
|
|
1918
|
+
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
1919
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
1920
|
+
} : {};
|
|
1737
1921
|
try {
|
|
1738
1922
|
const result = await provider.send({
|
|
1739
1923
|
to: send.emailAtSend,
|
|
@@ -1743,10 +1927,7 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1743
1927
|
subject: rendered.subject,
|
|
1744
1928
|
html: tracking.html,
|
|
1745
1929
|
text: rendered.plainText,
|
|
1746
|
-
headers
|
|
1747
|
-
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
1748
|
-
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
1749
|
-
},
|
|
1930
|
+
headers,
|
|
1750
1931
|
messageMeta: { sendId: String(send._id) }
|
|
1751
1932
|
});
|
|
1752
1933
|
await ctx.collections.sends.updateOne(
|
|
@@ -1783,7 +1964,7 @@ function pickProviderName(stepOverride, tpl, ctx) {
|
|
|
1783
1964
|
}
|
|
1784
1965
|
return ctx.config.defaultProvider;
|
|
1785
1966
|
}
|
|
1786
|
-
function buildRenderContext(contact, run, vars, ctx) {
|
|
1967
|
+
function buildRenderContext(contact, run, vars, ctx, resolved = {}) {
|
|
1787
1968
|
const scope = "marketing";
|
|
1788
1969
|
const expiresAt = new Date(Date.now() + ctx.config.unsubscribeTokenLifetimeDays * 24 * 60 * 60 * 1e3);
|
|
1789
1970
|
const token = signUnsubscribeToken(
|
|
@@ -1792,8 +1973,10 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
1792
1973
|
);
|
|
1793
1974
|
const unsubscribeUrl = `${ctx.config.publicUrl}/m/unsub/${token}`;
|
|
1794
1975
|
return {
|
|
1976
|
+
...resolved,
|
|
1795
1977
|
contact,
|
|
1796
1978
|
vars,
|
|
1979
|
+
event: run?.triggerEvent?.properties ?? {},
|
|
1797
1980
|
unsubscribeUrl,
|
|
1798
1981
|
senderAddress: ctx.config.senderAddress
|
|
1799
1982
|
};
|
|
@@ -1878,8 +2061,15 @@ async function processOneRunStep(runId, ctx) {
|
|
|
1878
2061
|
return handleCondition(run, step, contact, ctx);
|
|
1879
2062
|
case "branch":
|
|
1880
2063
|
return handleBranch(run, step, contact, ctx);
|
|
1881
|
-
case "send":
|
|
2064
|
+
case "send": {
|
|
2065
|
+
if (step.delivery) {
|
|
2066
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step.delivery, contact.timezone);
|
|
2067
|
+
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
2068
|
+
return deferSendForWindow(run, deliverAt, ctx);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
1882
2071
|
return handleSend(run, step, contact, flow, ctx);
|
|
2072
|
+
}
|
|
1883
2073
|
case "tag":
|
|
1884
2074
|
return handleTag(run, step, ctx);
|
|
1885
2075
|
case "fire_event":
|
|
@@ -2030,6 +2220,34 @@ async function handleWebhookStep(run, step, ctx) {
|
|
|
2030
2220
|
}
|
|
2031
2221
|
}
|
|
2032
2222
|
}
|
|
2223
|
+
async function deferSendForWindow(run, deliverAt, ctx) {
|
|
2224
|
+
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
2225
|
+
// Only write once per deferral — if nextActionAt already points at (or
|
|
2226
|
+
// past) the slot, another worker/tick got here first.
|
|
2227
|
+
{ _id: run._id, currentStepIndex: run.currentStepIndex, nextActionAt: { $lt: deliverAt } },
|
|
2228
|
+
{
|
|
2229
|
+
$set: { nextActionAt: deliverAt, updatedAt: /* @__PURE__ */ new Date() },
|
|
2230
|
+
$push: {
|
|
2231
|
+
history: {
|
|
2232
|
+
stepIndex: run.currentStepIndex,
|
|
2233
|
+
action: "send_deferred",
|
|
2234
|
+
at: /* @__PURE__ */ new Date(),
|
|
2235
|
+
details: { until: deliverAt }
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
{ returnDocument: "after" }
|
|
2240
|
+
);
|
|
2241
|
+
if (!updated) return;
|
|
2242
|
+
await ctx.queues.advance.add(
|
|
2243
|
+
"advance",
|
|
2244
|
+
{ flowRunId: String(run._id) },
|
|
2245
|
+
{
|
|
2246
|
+
delay: Math.max(0, deliverAt.getTime() - Date.now()),
|
|
2247
|
+
jobId: `advance:${run._id}:${run.currentStepIndex}:window:${deliverAt.getTime()}`
|
|
2248
|
+
}
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2033
2251
|
async function advanceStep(run, ctx, log, opts = {}) {
|
|
2034
2252
|
const stepInc = opts.stepInc ?? 1;
|
|
2035
2253
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
@@ -3554,6 +3772,7 @@ var Mailer = class _Mailer {
|
|
|
3554
3772
|
db: this.db,
|
|
3555
3773
|
collections: this.collections,
|
|
3556
3774
|
adapter: this.adapter,
|
|
3775
|
+
varsAdapter: this.config.varsAdapter,
|
|
3557
3776
|
providers: this.providers,
|
|
3558
3777
|
queues: this.queues,
|
|
3559
3778
|
config: this.config,
|
|
@@ -3633,6 +3852,10 @@ var Mailer = class _Mailer {
|
|
|
3633
3852
|
if (!config.providers[config.defaultProvider]) {
|
|
3634
3853
|
throw new Error(`defaultProvider "${config.defaultProvider}" not in providers map`);
|
|
3635
3854
|
}
|
|
3855
|
+
if (config.varsAdapter) {
|
|
3856
|
+
const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
|
|
3857
|
+
assertNoReservedVarKeys2(config.varsAdapter);
|
|
3858
|
+
}
|
|
3636
3859
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
3637
3860
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
3638
3861
|
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
@@ -3848,6 +4071,72 @@ var Mailer = class _Mailer {
|
|
|
3848
4071
|
await this.collections.contactTags.deleteOne({ externalId: parsed.externalId, tag: parsed.tag });
|
|
3849
4072
|
}
|
|
3850
4073
|
}
|
|
4074
|
+
// -------------------------------------------------------------------------
|
|
4075
|
+
// Flow abort
|
|
4076
|
+
// -------------------------------------------------------------------------
|
|
4077
|
+
/**
|
|
4078
|
+
* Abort every active run of one flow for a contact, immediately. Runs parked
|
|
4079
|
+
* in a `wait` exit too — their delayed wake-up jobs find the run exited and
|
|
4080
|
+
* no-op. Also cancels any of the flow's emails still sitting in the send
|
|
4081
|
+
* queue for this contact (queued or awaiting retry), so an abort means no
|
|
4082
|
+
* further mail, not just no further steps.
|
|
4083
|
+
*
|
|
4084
|
+
* No-op (returns zero counts) when nothing is active. Safe to call from the
|
|
4085
|
+
* same handler that processes the business event ("user upgraded").
|
|
4086
|
+
*/
|
|
4087
|
+
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
4088
|
+
const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
|
|
4089
|
+
const flow = await this.collections.flows.findOne(
|
|
4090
|
+
{ slug: parsed.flowSlug },
|
|
4091
|
+
{ projection: { _id: 1 } }
|
|
4092
|
+
);
|
|
4093
|
+
if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
|
|
4094
|
+
const result = await this.abortActiveRuns(
|
|
4095
|
+
{ externalId: parsed.externalId, flowId: flow._id },
|
|
4096
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
4097
|
+
);
|
|
4098
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
4099
|
+
await this.audit({
|
|
4100
|
+
actor: "host",
|
|
4101
|
+
action: "flow.abort",
|
|
4102
|
+
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
4103
|
+
diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
return result;
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Abort every active flow run for a contact across all flows. Same semantics
|
|
4110
|
+
* as `abortFlow` — for "stop everything" events (account deleted, churned).
|
|
4111
|
+
*/
|
|
4112
|
+
async abortAllFlows(externalId, opts = {}) {
|
|
4113
|
+
const parsed = abortAllFlowsInputSchema.parse({ externalId, reason: opts.reason });
|
|
4114
|
+
const result = await this.abortActiveRuns(
|
|
4115
|
+
{ externalId: parsed.externalId },
|
|
4116
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
4117
|
+
);
|
|
4118
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
4119
|
+
await this.audit({
|
|
4120
|
+
actor: "host",
|
|
4121
|
+
action: "flow.abort_all",
|
|
4122
|
+
resource: { collection: "mailer_flow_runs" },
|
|
4123
|
+
diffSummary: `abortAllFlows externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
return result;
|
|
4127
|
+
}
|
|
4128
|
+
async abortActiveRuns(filter, exitReason) {
|
|
4129
|
+
const runs = await this.collections.flowRuns.find({ ...filter, status: "active" }).toArray();
|
|
4130
|
+
if (runs.length === 0) return { abortedRuns: 0, cancelledSends: 0 };
|
|
4131
|
+
for (const run of runs) {
|
|
4132
|
+
await exitFlowRun(run, exitReason, this.runnerContext);
|
|
4133
|
+
}
|
|
4134
|
+
const cancelled = await this.collections.sends.updateMany(
|
|
4135
|
+
{ flowRunId: { $in: runs.map((r) => r._id) }, status: { $in: ["queued", "failed"] } },
|
|
4136
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
4137
|
+
);
|
|
4138
|
+
return { abortedRuns: runs.length, cancelledSends: cancelled.modifiedCount };
|
|
4139
|
+
}
|
|
3851
4140
|
/**
|
|
3852
4141
|
* GDPR right-to-erasure. Hard-deletes the contact's PII and leaves a hashed
|
|
3853
4142
|
* suppression row to block re-import. INVARIANT 9.
|
|
@@ -4050,6 +4339,7 @@ var Mailer = class _Mailer {
|
|
|
4050
4339
|
|
|
4051
4340
|
// src/server/index.ts
|
|
4052
4341
|
init_mongo();
|
|
4342
|
+
init_vars();
|
|
4053
4343
|
|
|
4054
4344
|
// src/server/providers/null.ts
|
|
4055
4345
|
var counter = 0;
|
|
@@ -4188,6 +4478,18 @@ ${input.plainText}`);
|
|
|
4188
4478
|
hint: "Front-load the important words so the truncated preview still makes sense."
|
|
4189
4479
|
});
|
|
4190
4480
|
}
|
|
4481
|
+
if (config.varsJsonSchema) {
|
|
4482
|
+
const sources = [input.subject, input.preheader, input.mjml, JSON.stringify(input.editorJson ?? null)];
|
|
4483
|
+
const unknown = findUnknownVariables(sources.join("\n"), config.varsJsonSchema);
|
|
4484
|
+
if (unknown.length > 0) {
|
|
4485
|
+
issues.push({
|
|
4486
|
+
rule: "unknown_variable",
|
|
4487
|
+
severity: "warning",
|
|
4488
|
+
message: `Template references variable(s) not in the vars schema: ${unknown.join(", ")}.`,
|
|
4489
|
+
hint: "These render as empty strings. Check for typos, or add the key to your varsAdapter schema. Paths inside {{#each}}/{{#with}} blocks are relative and not checked."
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4191
4493
|
const linkCount = countMatches(input.html, /<a\s[^>]*\bhref\s*=/gi);
|
|
4192
4494
|
if (linkCount > 10) {
|
|
4193
4495
|
issues.push({
|
|
@@ -4250,6 +4552,60 @@ function findSpamSignals(text) {
|
|
|
4250
4552
|
if (/!{3,}/.test(text)) out.add('excessive "!!!"');
|
|
4251
4553
|
return Array.from(out);
|
|
4252
4554
|
}
|
|
4555
|
+
var BUILTIN_VAR_ROOTS = /* @__PURE__ */ new Set([
|
|
4556
|
+
"contact",
|
|
4557
|
+
"vars",
|
|
4558
|
+
"event",
|
|
4559
|
+
"unsubscribeUrl",
|
|
4560
|
+
"viewInBrowserUrl",
|
|
4561
|
+
"preferenceCenterUrl",
|
|
4562
|
+
"senderAddress",
|
|
4563
|
+
"this"
|
|
4564
|
+
]);
|
|
4565
|
+
var PATH_TOKEN = /^@?[A-Za-z_][\w$]*(\.[A-Za-z_][\w$]*)*$/;
|
|
4566
|
+
function findUnknownVariables(source, schema) {
|
|
4567
|
+
const hasBlockScopes = /\{\{[#^]\s*(each|with)\b/.test(source);
|
|
4568
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
4569
|
+
const re = /\{\{\{?\s*([^{}]+?)\s*\}?\}\}/g;
|
|
4570
|
+
let m;
|
|
4571
|
+
while (m = re.exec(source)) {
|
|
4572
|
+
const expr = m[1].trim();
|
|
4573
|
+
if (!expr || /^[#^/>!]/.test(expr) || expr === "else") continue;
|
|
4574
|
+
if (expr.includes("(")) continue;
|
|
4575
|
+
const parts = expr.split(/\s+/);
|
|
4576
|
+
const candidates = parts.length > 1 ? parts.slice(1) : parts;
|
|
4577
|
+
for (const raw of candidates) {
|
|
4578
|
+
if (!PATH_TOKEN.test(raw)) continue;
|
|
4579
|
+
if (raw.startsWith("@")) continue;
|
|
4580
|
+
const segments = raw.split(".");
|
|
4581
|
+
if (BUILTIN_VAR_ROOTS.has(segments[0])) continue;
|
|
4582
|
+
if (hasBlockScopes && segments.length === 1) continue;
|
|
4583
|
+
if (!schemaHasPath(schema, segments)) unknown.add(raw);
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return Array.from(unknown);
|
|
4587
|
+
}
|
|
4588
|
+
function schemaHasPath(node, segments) {
|
|
4589
|
+
if (segments.length === 0) return true;
|
|
4590
|
+
if (!node || typeof node !== "object") return true;
|
|
4591
|
+
const n = node;
|
|
4592
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
4593
|
+
if (Array.isArray(n[key])) {
|
|
4594
|
+
return n[key].some((branch) => schemaHasPath(branch, segments));
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
if (Array.isArray(n.type) && n.type.includes("object") && !n.properties) return true;
|
|
4598
|
+
if (n.type === "array") {
|
|
4599
|
+
if (segments[0] === "length") return true;
|
|
4600
|
+
return schemaHasPath(n.items, segments);
|
|
4601
|
+
}
|
|
4602
|
+
const props = n.properties;
|
|
4603
|
+
if (!props || typeof props !== "object") return true;
|
|
4604
|
+
const [head, ...rest] = segments;
|
|
4605
|
+
if (head in props) return schemaHasPath(props[head], rest);
|
|
4606
|
+
if (n.additionalProperties === void 0 || n.additionalProperties === false) return false;
|
|
4607
|
+
return true;
|
|
4608
|
+
}
|
|
4253
4609
|
function isAllCaps(subject) {
|
|
4254
4610
|
const letters = subject.replace(/[^a-zA-Z]/g, "");
|
|
4255
4611
|
if (letters.length < 5) return false;
|
|
@@ -4257,6 +4613,9 @@ function isAllCaps(subject) {
|
|
|
4257
4613
|
return upper / letters.length > 0.5;
|
|
4258
4614
|
}
|
|
4259
4615
|
|
|
4616
|
+
// src/server/api/admin.ts
|
|
4617
|
+
init_vars();
|
|
4618
|
+
|
|
4260
4619
|
// src/server/api/setup-status.ts
|
|
4261
4620
|
async function runSetupChecks(mailer) {
|
|
4262
4621
|
const checks = [];
|
|
@@ -4695,7 +5054,7 @@ async function checkPostalAddress(mailer) {
|
|
|
4695
5054
|
label: "CAN-SPAM postal address",
|
|
4696
5055
|
severity: "warn",
|
|
4697
5056
|
message: `${marketingCount} published marketing template${marketingCount === 1 ? "" : "s"} but senderAddress is unset`,
|
|
4698
|
-
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference
|
|
5057
|
+
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference the `{{senderAddress}}` render variable in your templates."
|
|
4699
5058
|
};
|
|
4700
5059
|
}
|
|
4701
5060
|
async function checkDoiTemplate(mailer) {
|
|
@@ -4989,19 +5348,30 @@ async function persistScore(ctx, input) {
|
|
|
4989
5348
|
}
|
|
4990
5349
|
async function evaluateMailTesterGate(ctx, input) {
|
|
4991
5350
|
const cfg = ctx.config.mailTester;
|
|
4992
|
-
if (!cfg?.apiKey) return { allowed: true, reason: null, score: null };
|
|
5351
|
+
if (!cfg?.apiKey) return { allowed: true, reason: null, score: null, code: null };
|
|
4993
5352
|
const minScore = cfg.minScore ?? 8;
|
|
4994
5353
|
const key = mailTesterContentKey(input);
|
|
4995
5354
|
const cached = await findCachedScore(ctx, key);
|
|
4996
|
-
if (!cached)
|
|
5355
|
+
if (!cached) {
|
|
5356
|
+
if (cfg.requireScore) {
|
|
5357
|
+
return {
|
|
5358
|
+
allowed: false,
|
|
5359
|
+
reason: "No Mail-Tester score for this exact content. Run a deliverability check before publishing.",
|
|
5360
|
+
score: null,
|
|
5361
|
+
code: "no_score"
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
return { allowed: true, reason: null, score: null, code: null };
|
|
5365
|
+
}
|
|
4997
5366
|
if (cached.score < minScore) {
|
|
4998
5367
|
return {
|
|
4999
5368
|
allowed: false,
|
|
5000
5369
|
reason: `Mail-Tester score ${cached.score.toFixed(1)} is below minimum ${minScore.toFixed(1)}`,
|
|
5001
|
-
score: cached
|
|
5370
|
+
score: cached,
|
|
5371
|
+
code: "low_score"
|
|
5002
5372
|
};
|
|
5003
5373
|
}
|
|
5004
|
-
return { allowed: true, reason: null, score: cached };
|
|
5374
|
+
return { allowed: true, reason: null, score: cached, code: null };
|
|
5005
5375
|
}
|
|
5006
5376
|
|
|
5007
5377
|
// src/server/api/admin.ts
|
|
@@ -5036,12 +5406,19 @@ function createAdminRouter(mailer, opts = {}) {
|
|
|
5036
5406
|
function apiRouter(mailer, opts = {}) {
|
|
5037
5407
|
const r = Router();
|
|
5038
5408
|
const c = mailer.collections;
|
|
5409
|
+
const varsSchema = mailer.config.varsAdapter ? varsJsonSchema(mailer.config.varsAdapter) : null;
|
|
5039
5410
|
function getMailTesterClient() {
|
|
5040
5411
|
if (opts.mailTesterClient) return opts.mailTesterClient;
|
|
5041
5412
|
const cfg = mailer.config.mailTester;
|
|
5042
5413
|
if (!cfg?.apiKey) return null;
|
|
5043
5414
|
return createMailTesterClient(cfg);
|
|
5044
5415
|
}
|
|
5416
|
+
r.get(
|
|
5417
|
+
"/vars-schema",
|
|
5418
|
+
asyncHandler(async (_req, res) => {
|
|
5419
|
+
res.json({ schema: varsSchema, builtins: RESERVED_VAR_KEYS });
|
|
5420
|
+
})
|
|
5421
|
+
);
|
|
5045
5422
|
r.get(
|
|
5046
5423
|
"/me",
|
|
5047
5424
|
asyncHandler(async (req, res) => {
|
|
@@ -5988,6 +6365,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
5988
6365
|
res.json({
|
|
5989
6366
|
configured,
|
|
5990
6367
|
minScore: cfg?.minScore ?? 8,
|
|
6368
|
+
requireScore: !!cfg?.requireScore,
|
|
5991
6369
|
cacheHours: cfg?.cacheHours ?? 24,
|
|
5992
6370
|
score: cached
|
|
5993
6371
|
});
|
|
@@ -6128,9 +6506,11 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6128
6506
|
compileFailed: true
|
|
6129
6507
|
});
|
|
6130
6508
|
}
|
|
6509
|
+
if (!html && typeof tpl.body?.html === "string") html = tpl.body.html;
|
|
6510
|
+
if (!plainText && typeof tpl.body?.plainText === "string") plainText = tpl.body.plainText;
|
|
6131
6511
|
const lint = lintTemplate(
|
|
6132
6512
|
{ subject, preheader, mjml, editorJson, html, plainText, kind, fromEmail },
|
|
6133
|
-
{ senderDomains: mailer.config.senderDomains }
|
|
6513
|
+
{ senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
|
|
6134
6514
|
);
|
|
6135
6515
|
res.json({ ...lint, compileFailed: false });
|
|
6136
6516
|
})
|
|
@@ -6181,7 +6561,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6181
6561
|
kind: tpl.kind,
|
|
6182
6562
|
fromEmail: tpl.fromEmail
|
|
6183
6563
|
},
|
|
6184
|
-
{ senderDomains: mailer.config.senderDomains }
|
|
6564
|
+
{ senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
|
|
6185
6565
|
);
|
|
6186
6566
|
if (lint.errors.length > 0) {
|
|
6187
6567
|
return res.status(422).json({
|
|
@@ -6200,9 +6580,10 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6200
6580
|
if (!gate.allowed) {
|
|
6201
6581
|
return res.status(422).json({
|
|
6202
6582
|
error: "mail_tester_blocked",
|
|
6583
|
+
code: gate.code,
|
|
6203
6584
|
message: gate.reason,
|
|
6204
6585
|
score: gate.score,
|
|
6205
|
-
hint: "Re-run the deliverability check after fixing the feedback, or POST `bypassMailTester: true` to publish anyway."
|
|
6586
|
+
hint: gate.code === "no_score" ? "POST to `/templates/:slug/mail-tester-check`, poll `/mail-tester-result`, then publish \u2014 or POST `bypassMailTester: true` to publish anyway." : "Re-run the deliverability check after fixing the feedback, or POST `bypassMailTester: true` to publish anyway."
|
|
6206
6587
|
});
|
|
6207
6588
|
}
|
|
6208
6589
|
}
|
|
@@ -6273,40 +6654,93 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6273
6654
|
html = tpl.body.html;
|
|
6274
6655
|
plainText = tpl.body.plainText;
|
|
6275
6656
|
}
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6657
|
+
let contact;
|
|
6658
|
+
const contactId = typeof req.body?.contactId === "string" ? req.body.contactId : null;
|
|
6659
|
+
if (contactId) {
|
|
6660
|
+
const found = await mailer.adapter.getById(contactId);
|
|
6661
|
+
if (!found) return res.status(404).json({ error: "contact_not_found", contactId });
|
|
6662
|
+
contact = found;
|
|
6663
|
+
} else {
|
|
6664
|
+
contact = req.body?.sampleContact ?? {
|
|
6665
|
+
externalId: "preview-contact",
|
|
6666
|
+
email: "preview@example.com",
|
|
6667
|
+
tags: [],
|
|
6668
|
+
fields: { firstName: "Alex" }
|
|
6669
|
+
};
|
|
6670
|
+
}
|
|
6671
|
+
const eventProperties = req.body?.eventProperties && typeof req.body.eventProperties === "object" ? req.body.eventProperties : void 0;
|
|
6672
|
+
let resolved = {};
|
|
6673
|
+
try {
|
|
6674
|
+
resolved = await resolveVars(mailer.config.varsAdapter, contact, {
|
|
6675
|
+
reason: "preview",
|
|
6676
|
+
templateSlug: tpl.slug,
|
|
6677
|
+
eventProperties
|
|
6678
|
+
});
|
|
6679
|
+
} catch (err) {
|
|
6680
|
+
return res.status(502).json({
|
|
6681
|
+
error: "vars_resolve_failed",
|
|
6682
|
+
message: `varsAdapter.resolve threw: ${String(err?.message ?? err)}`
|
|
6683
|
+
});
|
|
6684
|
+
}
|
|
6282
6685
|
const renderCtx = {
|
|
6283
|
-
|
|
6686
|
+
...resolved,
|
|
6687
|
+
contact,
|
|
6284
6688
|
vars: req.body?.vars ?? {},
|
|
6689
|
+
event: eventProperties ?? {},
|
|
6285
6690
|
unsubscribeUrl: `${mailer.config.publicUrl}/m/unsub/preview`,
|
|
6286
6691
|
senderAddress: mailer.config.senderAddress
|
|
6287
6692
|
};
|
|
6288
6693
|
const previewTpl = { ...tpl, body: { ...tpl.body, html, plainText } };
|
|
6289
6694
|
const rendered = await renderTemplate(previewTpl, renderCtx, { helpers: mailer.config.handlebarsHelpers });
|
|
6290
|
-
return res.json({
|
|
6695
|
+
return res.json({
|
|
6696
|
+
subject: rendered.subject,
|
|
6697
|
+
preheader: rendered.preheader,
|
|
6698
|
+
html: rendered.html,
|
|
6699
|
+
plainText: rendered.plainText,
|
|
6700
|
+
contact: { externalId: contact.externalId, email: contact.email }
|
|
6701
|
+
});
|
|
6291
6702
|
})
|
|
6292
6703
|
);
|
|
6293
6704
|
r.post(
|
|
6294
6705
|
"/templates/:slug/send-test",
|
|
6295
6706
|
asyncHandler(async (req, res) => {
|
|
6296
|
-
const { to, sampleData } = req.body ?? {};
|
|
6707
|
+
const { to, sampleData, contactId, eventProperties } = req.body ?? {};
|
|
6297
6708
|
if (!to) return res.status(400).json({ error: "to_required" });
|
|
6298
6709
|
const tpl = await c.templates.findOne({ slug: req.params.slug });
|
|
6299
6710
|
if (!tpl) return res.status(404).json({ error: "not_found" });
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
}
|
|
6306
|
-
|
|
6711
|
+
let contact;
|
|
6712
|
+
if (typeof contactId === "string" && contactId) {
|
|
6713
|
+
const found = await mailer.adapter.getById(contactId);
|
|
6714
|
+
if (!found) return res.status(404).json({ error: "contact_not_found", contactId });
|
|
6715
|
+
contact = { ...found, email: to };
|
|
6716
|
+
} else {
|
|
6717
|
+
contact = sampleData?.contact ?? {
|
|
6718
|
+
externalId: "test-recipient",
|
|
6719
|
+
email: to,
|
|
6720
|
+
tags: [],
|
|
6721
|
+
fields: { firstName: "Test" }
|
|
6722
|
+
};
|
|
6723
|
+
contact.email = to;
|
|
6724
|
+
}
|
|
6725
|
+
const evProps = eventProperties && typeof eventProperties === "object" ? eventProperties : void 0;
|
|
6726
|
+
let resolved = {};
|
|
6727
|
+
try {
|
|
6728
|
+
resolved = await resolveVars(mailer.config.varsAdapter, contact, {
|
|
6729
|
+
reason: "test",
|
|
6730
|
+
templateSlug: tpl.slug,
|
|
6731
|
+
eventProperties: evProps
|
|
6732
|
+
});
|
|
6733
|
+
} catch (err) {
|
|
6734
|
+
return res.status(502).json({
|
|
6735
|
+
error: "vars_resolve_failed",
|
|
6736
|
+
message: `varsAdapter.resolve threw: ${String(err?.message ?? err)}`
|
|
6737
|
+
});
|
|
6738
|
+
}
|
|
6307
6739
|
const renderCtx = {
|
|
6740
|
+
...resolved,
|
|
6308
6741
|
contact,
|
|
6309
6742
|
vars: sampleData?.vars ?? {},
|
|
6743
|
+
event: evProps ?? {},
|
|
6310
6744
|
unsubscribeUrl: `${mailer.config.publicUrl}/m/unsub/test`,
|
|
6311
6745
|
senderAddress: mailer.config.senderAddress
|
|
6312
6746
|
};
|
|
@@ -6957,8 +7391,8 @@ var DEDUPE_POLICIES = [
|
|
|
6957
7391
|
];
|
|
6958
7392
|
|
|
6959
7393
|
// src/server/index.ts
|
|
6960
|
-
var VERSION = "0.
|
|
7394
|
+
var VERSION = "0.8.0";
|
|
6961
7395
|
|
|
6962
|
-
export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, verifyUnsubscribeToken };
|
|
7396
|
+
export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, varsJsonSchema, verifyUnsubscribeToken };
|
|
6963
7397
|
//# sourceMappingURL=index.js.map
|
|
6964
7398
|
//# sourceMappingURL=index.js.map
|