@marlinjai/mail-contract 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +16086 -2134
- package/dist/index.d.ts +16086 -2134
- package/dist/index.js +1955 -762
- package/dist/index.mjs +1853 -762
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -22,6 +22,29 @@ function page(item) {
|
|
|
22
22
|
}
|
|
23
23
|
var IdParams = z.object({ id: Id });
|
|
24
24
|
var Ok = z.object({ ok: z.literal(true) });
|
|
25
|
+
var LOCALE_TAG_PATTERN = /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?$/;
|
|
26
|
+
var LocaleTag = z.string().min(2).max(35).regex(LOCALE_TAG_PATTERN, "a language tag such as en, fr or pt-BR");
|
|
27
|
+
function canonicalLocale(tag) {
|
|
28
|
+
if (typeof tag !== "string") return null;
|
|
29
|
+
const trimmed = tag.trim().replace(/_/g, "-");
|
|
30
|
+
if (!LOCALE_TAG_PATTERN.test(trimmed)) return null;
|
|
31
|
+
return trimmed.split("-").map((part, i) => {
|
|
32
|
+
if (i === 0) return part.toLowerCase();
|
|
33
|
+
if (part.length === 4) return part[0].toUpperCase() + part.slice(1).toLowerCase();
|
|
34
|
+
return part.toUpperCase();
|
|
35
|
+
}).join("-");
|
|
36
|
+
}
|
|
37
|
+
function primaryLanguage(tag) {
|
|
38
|
+
return canonicalLocale(tag)?.split("-")[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
function matchLocale(wanted, available) {
|
|
41
|
+
const canonical = canonicalLocale(wanted);
|
|
42
|
+
if (!canonical) return null;
|
|
43
|
+
const exact = available.find((a) => canonicalLocale(a) === canonical);
|
|
44
|
+
if (exact !== void 0) return exact;
|
|
45
|
+
const primary = canonical.split("-")[0];
|
|
46
|
+
return available.find((a) => primaryLanguage(a) === primary) ?? null;
|
|
47
|
+
}
|
|
25
48
|
|
|
26
49
|
// src/errors.ts
|
|
27
50
|
import { z as z2 } from "zod";
|
|
@@ -122,119 +145,483 @@ var API_VERSION_PREFIX = "/v1";
|
|
|
122
145
|
var HEALTH_PATH = "/healthz";
|
|
123
146
|
|
|
124
147
|
// src/workspace.ts
|
|
125
|
-
import { z as
|
|
148
|
+
import { z as z9 } from "zod";
|
|
126
149
|
|
|
127
150
|
// src/automations.ts
|
|
128
|
-
import { z as
|
|
151
|
+
import { z as z8 } from "zod";
|
|
129
152
|
|
|
130
153
|
// src/templates.ts
|
|
154
|
+
import { z as z4 } from "zod";
|
|
155
|
+
|
|
156
|
+
// src/insertions.ts
|
|
131
157
|
import { z as z3 } from "zod";
|
|
158
|
+
|
|
159
|
+
// src/unsubscribe.ts
|
|
160
|
+
var UNSUBSCRIBE_PATH_PREFIX = "/u/";
|
|
161
|
+
var LIST_UNSUBSCRIBE_POST_VALUE = "List-Unsubscribe=One-Click";
|
|
162
|
+
var RESERVED_MERGE_FIELDS = {
|
|
163
|
+
/** From the recipient's merge values, else the contact. Supports a fallback. */
|
|
164
|
+
first_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
165
|
+
last_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
166
|
+
email: { source: "contact", fallback: false, required_for_broadcast: false },
|
|
167
|
+
/** The hosted unsubscribe page for this recipient. A broadcast without it is refused. */
|
|
168
|
+
unsubscribe_url: { source: "service", fallback: false, required_for_broadcast: true }
|
|
169
|
+
};
|
|
170
|
+
var MERGE_FIELD_PATTERN = /\{\{\s*([a-z][a-z0-9_]*)\s*(?:\|([^}]*))?\}\}/g;
|
|
171
|
+
function findMergeFields(html) {
|
|
172
|
+
const uses = [];
|
|
173
|
+
for (const match of html.matchAll(MERGE_FIELD_PATTERN)) {
|
|
174
|
+
uses.push({ name: match[1], fallback: match[2] === void 0 ? null : match[2].trim() });
|
|
175
|
+
}
|
|
176
|
+
return uses;
|
|
177
|
+
}
|
|
178
|
+
function missingRequiredMergeFields(html) {
|
|
179
|
+
const used = new Set(findMergeFields(html).map((u) => u.name));
|
|
180
|
+
return Object.keys(RESERVED_MERGE_FIELDS).filter(
|
|
181
|
+
(name) => RESERVED_MERGE_FIELDS[name].required_for_broadcast && !used.has(name)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/insertions.ts
|
|
186
|
+
function ownEntry(record, key) {
|
|
187
|
+
return record != null && Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
|
|
188
|
+
}
|
|
189
|
+
var MAX_INSERTIONS = 20;
|
|
190
|
+
var MAX_INSERTION_CHOICES = 30;
|
|
191
|
+
var MAX_INSERTION_TEXT_LENGTH = 2e3;
|
|
192
|
+
var NAME = /^[a-z][a-z0-9_]*$/;
|
|
193
|
+
var InsertionName = z3.string().max(64).regex(NAME, "a merge-field name: lowercase letters, digits and underscores, starting with a letter");
|
|
194
|
+
var INSERTION_TEXT_SLOTS = ["filled", "otherwise"];
|
|
195
|
+
var InsertionChoiceKey = z3.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/, "lowercase letters, digits, hyphens and underscores, up to 64").refine((key) => !INSERTION_TEXT_SLOTS.includes(key), "filled and otherwise name the other two texts, not a choice");
|
|
196
|
+
var InsertionText = z3.string().max(MAX_INSERTION_TEXT_LENGTH);
|
|
197
|
+
var Insertion = z3.object({
|
|
198
|
+
by: InsertionName,
|
|
199
|
+
choices: z3.record(InsertionChoiceKey, InsertionText).refine((c) => Object.keys(c).length <= MAX_INSERTION_CHOICES, `at most ${MAX_INSERTION_CHOICES} choices`),
|
|
200
|
+
filled: InsertionText.nullable(),
|
|
201
|
+
otherwise: InsertionText
|
|
202
|
+
});
|
|
203
|
+
var Insertions = z3.record(InsertionName, Insertion).refine((m) => Object.keys(m).length <= MAX_INSERTIONS, `at most ${MAX_INSERTIONS} insertions`);
|
|
204
|
+
var TranslationInsertion = z3.object({
|
|
205
|
+
choices: z3.record(InsertionChoiceKey, InsertionText.nullable()),
|
|
206
|
+
filled: InsertionText.nullable(),
|
|
207
|
+
otherwise: InsertionText.nullable()
|
|
208
|
+
});
|
|
209
|
+
var TranslationInsertions = z3.record(InsertionName, TranslationInsertion);
|
|
210
|
+
var INSERTION_PROBLEM_REASONS = [
|
|
211
|
+
/** A reserved merge field (`first_name`, `email`, ...) cannot be an insertion's name. */
|
|
212
|
+
"insertion_name",
|
|
213
|
+
/** `by` is `unsubscribe_url` or another insertion's name. */
|
|
214
|
+
"insertion_by",
|
|
215
|
+
/** A text places an insertion: texts hold merge fields only, one level. */
|
|
216
|
+
"insertion_text",
|
|
217
|
+
/** `{{opening|...}}`: an insertion's `otherwise` is its fallback, in every language. */
|
|
218
|
+
"insertion_with_fallback",
|
|
219
|
+
/** A language version names an insertion or a choice the main language does not have. */
|
|
220
|
+
"unknown_translation_insertion"
|
|
221
|
+
];
|
|
222
|
+
function insertionTokens(text, insertions) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const match of text.matchAll(MERGE_FIELD_PATTERN)) {
|
|
225
|
+
const name = match[1];
|
|
226
|
+
if (Object.prototype.hasOwnProperty.call(insertions, name)) {
|
|
227
|
+
out.push({ name, fallback: match[2] === void 0 ? null : match[2] });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function eachText(ins) {
|
|
233
|
+
const texts = [];
|
|
234
|
+
for (const [key, text] of Object.entries(ins.choices)) if (text !== null) texts.push([`choices.${key}`, text]);
|
|
235
|
+
if (ins.filled !== null) texts.push(["filled", ins.filled]);
|
|
236
|
+
if (ins.otherwise !== null) texts.push(["otherwise", ins.otherwise]);
|
|
237
|
+
return texts;
|
|
238
|
+
}
|
|
239
|
+
function insertionProblems(insertions) {
|
|
240
|
+
const problems = [];
|
|
241
|
+
for (const [name, ins] of Object.entries(insertions)) {
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(RESERVED_MERGE_FIELDS, name)) {
|
|
243
|
+
problems.push({ reason: "insertion_name", path: name, message: `"${name}" is a merge field the service fills; name the insertion something else` });
|
|
244
|
+
}
|
|
245
|
+
if (ins.by === "unsubscribe_url" || Object.prototype.hasOwnProperty.call(insertions, ins.by)) {
|
|
246
|
+
problems.push({ reason: "insertion_by", path: `${name}.by`, message: `"${ins.by}" cannot choose an insertion: use a value the client or the contact provides` });
|
|
247
|
+
}
|
|
248
|
+
for (const [where, text] of eachText(ins)) {
|
|
249
|
+
if (insertionTokens(text, insertions).length > 0) {
|
|
250
|
+
problems.push({ reason: "insertion_text", path: `${name}.${where}`, message: "an insertion text can hold merge fields, not other insertions" });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return problems;
|
|
255
|
+
}
|
|
256
|
+
function translationInsertionProblems(main, translation) {
|
|
257
|
+
const problems = [];
|
|
258
|
+
for (const [name, ins] of Object.entries(translation)) {
|
|
259
|
+
const def = ownEntry(main, name);
|
|
260
|
+
if (!def) {
|
|
261
|
+
problems.push({ reason: "unknown_translation_insertion", path: name, message: `the main language has no insertion "${name}"` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
for (const key of Object.keys(ins.choices)) {
|
|
265
|
+
if (!Object.prototype.hasOwnProperty.call(def.choices, key)) {
|
|
266
|
+
problems.push({ reason: "unknown_translation_insertion", path: `${name}.choices.${key}`, message: `"${name}" has no choice "${key}" in the main language` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const [where, text] of eachText(ins)) {
|
|
270
|
+
if (insertionTokens(text, main).length > 0) {
|
|
271
|
+
problems.push({ reason: "insertion_text", path: `${name}.${where}`, message: "an insertion text can hold merge fields, not other insertions" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return problems;
|
|
276
|
+
}
|
|
277
|
+
function placementProblems(texts, insertions) {
|
|
278
|
+
const problems = [];
|
|
279
|
+
for (const [where, text] of Object.entries(texts)) {
|
|
280
|
+
if (!text) continue;
|
|
281
|
+
for (const use of insertionTokens(text, insertions)) {
|
|
282
|
+
if (use.fallback !== null) {
|
|
283
|
+
problems.push({
|
|
284
|
+
reason: "insertion_with_fallback",
|
|
285
|
+
path: where,
|
|
286
|
+
message: `{{${use.name}|...}}: an insertion takes no fallback, its "otherwise" text is the fallback`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return problems;
|
|
292
|
+
}
|
|
293
|
+
function unplacedInsertions(texts, insertions) {
|
|
294
|
+
const placed = /* @__PURE__ */ new Set();
|
|
295
|
+
for (const text of texts) if (text) for (const use of insertionTokens(text, insertions)) placed.add(use.name);
|
|
296
|
+
return Object.keys(insertions).filter((name) => !placed.has(name));
|
|
297
|
+
}
|
|
298
|
+
function insertionCoverage(main, translation) {
|
|
299
|
+
const missing = [];
|
|
300
|
+
for (const [name, def] of Object.entries(main)) {
|
|
301
|
+
const t = ownEntry(translation, name);
|
|
302
|
+
for (const key of Object.keys(def.choices)) {
|
|
303
|
+
const text = ownEntry(t?.choices, key);
|
|
304
|
+
if (typeof text !== "string") missing.push({ insertion: name, choice: key });
|
|
305
|
+
}
|
|
306
|
+
if (def.filled !== null && typeof t?.filled !== "string") missing.push({ insertion: name, choice: "filled" });
|
|
307
|
+
if (typeof t?.otherwise !== "string") missing.push({ insertion: name, choice: "otherwise" });
|
|
308
|
+
}
|
|
309
|
+
return missing;
|
|
310
|
+
}
|
|
311
|
+
function alignTranslationInsertions(main, translation) {
|
|
312
|
+
const out = {};
|
|
313
|
+
for (const [name, def] of Object.entries(main)) {
|
|
314
|
+
const t = ownEntry(translation, name);
|
|
315
|
+
const choices = {};
|
|
316
|
+
for (const key of Object.keys(def.choices)) choices[key] = ownEntry(t?.choices, key) ?? null;
|
|
317
|
+
out[name] = { choices, filled: def.filled === null ? null : t?.filled ?? null, otherwise: t?.otherwise ?? null };
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
function languageInsertions(main, translation) {
|
|
322
|
+
if (insertionCoverage(main, translation).length > 0) return null;
|
|
323
|
+
const out = {};
|
|
324
|
+
for (const [name, def] of Object.entries(main)) {
|
|
325
|
+
const t = ownEntry(translation, name);
|
|
326
|
+
const choices = {};
|
|
327
|
+
for (const key of Object.keys(def.choices)) choices[key] = ownEntry(t.choices, key);
|
|
328
|
+
out[name] = { by: def.by, choices, filled: def.filled === null ? null : t.filled, otherwise: t.otherwise };
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
function insertionInputs(insertions) {
|
|
333
|
+
const out = {};
|
|
334
|
+
for (const [name, def] of Object.entries(insertions)) {
|
|
335
|
+
const entry = ownEntry(out, def.by) ?? (out[def.by] = { insertions: [], keys: [], filled: false });
|
|
336
|
+
entry.insertions.push(name);
|
|
337
|
+
for (const key of Object.keys(def.choices)) if (!entry.keys.includes(key)) entry.keys.push(key);
|
|
338
|
+
if (def.filled !== null) entry.filled = true;
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
function insertionValue(raw) {
|
|
343
|
+
if (typeof raw === "string") return raw.trim() === "" ? null : raw.trim();
|
|
344
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
|
|
345
|
+
if (raw === true) return "true";
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
var InsertionChoice = z3.object({
|
|
349
|
+
value: z3.string().nullable(),
|
|
350
|
+
chosen: z3.string(),
|
|
351
|
+
matched: z3.boolean()
|
|
352
|
+
});
|
|
353
|
+
var InsertionChoices = z3.record(InsertionName, InsertionChoice);
|
|
354
|
+
function chooseInsertion(insertion, raw) {
|
|
355
|
+
const value = insertionValue(raw);
|
|
356
|
+
if (value !== null) {
|
|
357
|
+
const key = value.toLowerCase();
|
|
358
|
+
if (Object.prototype.hasOwnProperty.call(insertion.choices, key)) {
|
|
359
|
+
return { value, chosen: key, matched: true, text: insertion.choices[key] };
|
|
360
|
+
}
|
|
361
|
+
if (insertion.filled !== null) return { value, chosen: "filled", matched: true, text: insertion.filled };
|
|
362
|
+
return { value, chosen: "otherwise", matched: false, text: insertion.otherwise };
|
|
363
|
+
}
|
|
364
|
+
return { value: null, chosen: "otherwise", matched: true, text: insertion.otherwise };
|
|
365
|
+
}
|
|
366
|
+
function resolveInsertions(insertions, raw) {
|
|
367
|
+
const out = {};
|
|
368
|
+
for (const [name, def] of Object.entries(insertions)) out[name] = chooseInsertion(def, raw(def.by));
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
function insertionChoices(resolved) {
|
|
372
|
+
const out = {};
|
|
373
|
+
for (const [name, r] of Object.entries(resolved)) out[name] = { value: r.value, chosen: r.chosen, matched: r.matched };
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
var ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
377
|
+
function escapeMergeHtml(value) {
|
|
378
|
+
return value.replace(/[&<>"']/g, (ch) => ESCAPES[ch]);
|
|
379
|
+
}
|
|
380
|
+
function pattern() {
|
|
381
|
+
return new RegExp(MERGE_FIELD_PATTERN.source, "g");
|
|
382
|
+
}
|
|
383
|
+
function insertionHtml(text, p) {
|
|
384
|
+
let out = "";
|
|
385
|
+
let last = 0;
|
|
386
|
+
for (const match of text.matchAll(pattern())) {
|
|
387
|
+
out += escapeMergeHtml(text.slice(last, match.index));
|
|
388
|
+
out += escapeMergeHtml(p.value(match[1], match[2]));
|
|
389
|
+
last = match.index + match[0].length;
|
|
390
|
+
}
|
|
391
|
+
out += escapeMergeHtml(text.slice(last));
|
|
392
|
+
return out.trim() === "" ? "" : out.replace(/\r\n|\r|\n/g, "<br>");
|
|
393
|
+
}
|
|
394
|
+
function insertionPlain(text, p) {
|
|
395
|
+
return text.replace(pattern(), (_m, name, fallback) => p.value(name, fallback));
|
|
396
|
+
}
|
|
397
|
+
function tokenHtml(name, fallback, p) {
|
|
398
|
+
const ins = ownEntry(p.insertions, name);
|
|
399
|
+
if (ins) return insertionHtml(ins.text, p);
|
|
400
|
+
return escapeMergeHtml(p.value(name, fallback));
|
|
401
|
+
}
|
|
402
|
+
var FIELD_ONLY_PARAGRAPH = new RegExp(
|
|
403
|
+
String.raw`<(p|div|li|h[1-6])(\s[^>]*)?>((?:\s| | |<br\s*\/?>|<\/?(?:strong|b|em|i|u|span|font|a)(?:\s[^>]*)?>|\{\{\s*[a-z][a-z0-9_]*\s*(?:\|[^}]*)?\}\})*)<\/\1>`,
|
|
404
|
+
"g"
|
|
405
|
+
);
|
|
406
|
+
var TEXT_BLOCK_ROW = /<tr>\s*<td\b[^>]*\bclass="[^"]*\bel-text\b[^"]*"[^>]*>([\s\S]*?)<\/td>\s*<\/tr>/g;
|
|
407
|
+
function hasVisibleContent(html) {
|
|
408
|
+
if (/<(img|hr|video|svg|input|iframe|table)\b/i.test(html)) return true;
|
|
409
|
+
const text = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]*>/g, "").replace(/ | | /g, "").trim();
|
|
410
|
+
return text !== "";
|
|
411
|
+
}
|
|
412
|
+
function dropEmptyParagraphs(html, p) {
|
|
413
|
+
return html.replace(FIELD_ONLY_PARAGRAPH, (whole, _tag, _attrs, content) => {
|
|
414
|
+
const tokens = [...content.matchAll(pattern())];
|
|
415
|
+
if (tokens.length === 0) return whole;
|
|
416
|
+
return tokens.every((m) => tokenHtml(m[1], m[2], p).trim() === "") ? "" : whole;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function personalizeHtml(html, p) {
|
|
420
|
+
let out = html.replace(TEXT_BLOCK_ROW, (whole, inner) => {
|
|
421
|
+
if (!pattern().test(inner)) return whole;
|
|
422
|
+
const kept = dropEmptyParagraphs(inner, p);
|
|
423
|
+
const filled = kept.replace(pattern(), (_m, name, fallback) => tokenHtml(name, fallback, p));
|
|
424
|
+
return hasVisibleContent(filled) ? whole : "";
|
|
425
|
+
});
|
|
426
|
+
out = dropEmptyParagraphs(out, p);
|
|
427
|
+
return out.replace(pattern(), (_m, name, fallback) => tokenHtml(name, fallback, p));
|
|
428
|
+
}
|
|
429
|
+
var CONTROL = new RegExp("[\\u0000-\\u001f\\u007f]+", "g");
|
|
430
|
+
function personalizeText(text, p) {
|
|
431
|
+
return text.replace(pattern(), (_m, name, fallback) => {
|
|
432
|
+
const ins = ownEntry(p.insertions, name);
|
|
433
|
+
return ins ? insertionPlain(ins.text, p) : p.value(name, fallback);
|
|
434
|
+
}).replace(CONTROL, " ").replace(/ {2,}/g, " ").trim();
|
|
435
|
+
}
|
|
436
|
+
function sampleMergeValue(values) {
|
|
437
|
+
return (name, fallback) => {
|
|
438
|
+
const v = ownEntry(values, name);
|
|
439
|
+
const text = typeof v === "string" ? v : typeof v === "number" && Number.isFinite(v) ? String(v) : typeof v === "boolean" ? String(v) : null;
|
|
440
|
+
if (text === null || text.trim() === "") return fallback?.trim() ?? "";
|
|
441
|
+
return text;
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/templates.ts
|
|
132
446
|
var DOCUMENT_SCHEMA_VERSIONS = ["1.0", "1.1"];
|
|
133
|
-
var DocumentSchemaVersion =
|
|
134
|
-
var TemplateDocument =
|
|
447
|
+
var DocumentSchemaVersion = z4.enum(DOCUMENT_SCHEMA_VERSIONS);
|
|
448
|
+
var TemplateDocument = z4.object({
|
|
135
449
|
version: DocumentSchemaVersion,
|
|
136
|
-
metadata:
|
|
137
|
-
sections:
|
|
450
|
+
metadata: z4.record(z4.unknown()),
|
|
451
|
+
sections: z4.array(z4.record(z4.unknown()))
|
|
138
452
|
}).passthrough();
|
|
139
453
|
var MAX_DOCUMENT_BYTES = 1e6;
|
|
140
|
-
var
|
|
454
|
+
var MAX_SUBJECT_LENGTH = 998;
|
|
455
|
+
var MAX_PREHEADER_LENGTH = 500;
|
|
456
|
+
var TemplateSubject = z4.string().max(MAX_SUBJECT_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
|
|
457
|
+
var TemplatePreheader = z4.string().max(MAX_PREHEADER_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
|
|
458
|
+
var TEMPLATE_LANGUAGE_STATUSES = ["draft", "ready"];
|
|
459
|
+
var TemplateLanguageStatus = z4.enum(TEMPLATE_LANGUAGE_STATUSES);
|
|
460
|
+
var TemplateTranslation = z4.object({
|
|
461
|
+
subject: TemplateSubject.nullable(),
|
|
462
|
+
preheader: TemplatePreheader.nullable(),
|
|
463
|
+
document: TemplateDocument,
|
|
464
|
+
status: TemplateLanguageStatus,
|
|
465
|
+
/**
|
|
466
|
+
* This language's texts for the main language's insertions
|
|
467
|
+
* (docs/plans/2026-09-25-translated-insertions.md). The service always
|
|
468
|
+
* returns it, aligned to the main language's structure (a text not written
|
|
469
|
+
* yet is null). In a save, leaving it out keeps the stored texts, so a
|
|
470
|
+
* client that does not know insertions never erases them. A `ready` version
|
|
471
|
+
* needs every text: marking one ready without is `ready_needs_insertions`,
|
|
472
|
+
* and a save that leaves a ready version without one (the main language
|
|
473
|
+
* added a choice) sets it back to `draft`.
|
|
474
|
+
*/
|
|
475
|
+
insertions: TranslationInsertions.optional()
|
|
476
|
+
});
|
|
477
|
+
var MAX_TEMPLATE_TRANSLATIONS = 19;
|
|
478
|
+
var TemplateTranslations = z4.record(LocaleTag, TemplateTranslation).refine((t) => Object.keys(t).length <= MAX_TEMPLATE_TRANSLATIONS, `at most ${MAX_TEMPLATE_TRANSLATIONS} language versions`);
|
|
479
|
+
var Template = z4.object({
|
|
141
480
|
id: Id,
|
|
142
|
-
name:
|
|
143
|
-
description:
|
|
481
|
+
name: z4.string().min(1).max(200),
|
|
482
|
+
description: z4.string().max(2e3).nullable(),
|
|
483
|
+
/**
|
|
484
|
+
* The main language: what `subject`, `preheader` and `document` are written
|
|
485
|
+
* in, and what every recipient without a ready version of their own gets.
|
|
486
|
+
* Set from the workspace's `default_locale` when the template is created
|
|
487
|
+
* unless given, and never changed by a later change of that setting.
|
|
488
|
+
*/
|
|
489
|
+
locale: LocaleTag,
|
|
490
|
+
/** The main language's subject line; null when the mailing that uses the template gives one. */
|
|
491
|
+
subject: TemplateSubject.nullable(),
|
|
492
|
+
preheader: TemplatePreheader.nullable(),
|
|
144
493
|
document: TemplateDocument,
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
494
|
+
/**
|
|
495
|
+
* The other languages. A version whose language the workspace no longer
|
|
496
|
+
* lists is kept, but no send uses it until the language is listed again.
|
|
497
|
+
*/
|
|
498
|
+
translations: TemplateTranslations,
|
|
499
|
+
/**
|
|
500
|
+
* The main language's insertions: named sentences chosen per recipient by
|
|
501
|
+
* one merge value, placed as `{{name}}` (see `insertions.ts`). Their
|
|
502
|
+
* structure is the main language's; every other language holds its own
|
|
503
|
+
* texts in `translations[tag].insertions`.
|
|
504
|
+
*/
|
|
505
|
+
insertions: Insertions,
|
|
506
|
+
/** Increments on every saved change, of any language. */
|
|
507
|
+
version: z4.number().int().min(1),
|
|
508
|
+
thumbnail_url: z4.string().url().nullable(),
|
|
148
509
|
archived_at: Timestamp.nullable(),
|
|
149
510
|
created_at: Timestamp,
|
|
150
511
|
updated_at: Timestamp
|
|
151
512
|
});
|
|
152
|
-
var TemplateSummary = Template.omit({ document: true })
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
513
|
+
var TemplateSummary = Template.omit({ document: true, translations: true, insertions: true }).extend({
|
|
514
|
+
languages: z4.record(LocaleTag, TemplateLanguageStatus),
|
|
515
|
+
/** The names of the template's insertions, sorted alphabetically. */
|
|
516
|
+
insertion_names: z4.array(z4.string())
|
|
517
|
+
});
|
|
518
|
+
var TemplateCreate = z4.object({
|
|
519
|
+
name: z4.string().min(1).max(200),
|
|
520
|
+
description: z4.string().max(2e3).optional(),
|
|
521
|
+
locale: LocaleTag.optional(),
|
|
522
|
+
subject: TemplateSubject.nullable().optional(),
|
|
523
|
+
preheader: TemplatePreheader.nullable().optional(),
|
|
524
|
+
document: TemplateDocument,
|
|
525
|
+
translations: TemplateTranslations.optional(),
|
|
526
|
+
insertions: Insertions.optional()
|
|
527
|
+
});
|
|
528
|
+
var TemplateUpdate = z4.object({
|
|
529
|
+
base_version: z4.number().int().min(1),
|
|
530
|
+
name: z4.string().min(1).max(200).optional(),
|
|
531
|
+
description: z4.string().max(2e3).nullable().optional(),
|
|
532
|
+
locale: LocaleTag.optional(),
|
|
533
|
+
subject: TemplateSubject.nullable().optional(),
|
|
534
|
+
preheader: TemplatePreheader.nullable().optional(),
|
|
162
535
|
document: TemplateDocument.optional(),
|
|
163
|
-
|
|
536
|
+
translations: z4.record(LocaleTag, TemplateTranslation.nullable()).optional(),
|
|
537
|
+
/**
|
|
538
|
+
* The main language's insertions, whole: what is left out is removed, from
|
|
539
|
+
* every language in the same save. A rename is a removal and an addition,
|
|
540
|
+
* so an editor carries each language's texts to the new name in the same
|
|
541
|
+
* save (it sends those languages in `translations`).
|
|
542
|
+
*/
|
|
543
|
+
insertions: Insertions.optional(),
|
|
544
|
+
archived: z4.boolean().optional()
|
|
164
545
|
}).refine(
|
|
165
|
-
(v) => v.name !== void 0 || v.description !== void 0 || v.document !== void 0 || v.archived !== void 0,
|
|
546
|
+
(v) => v.name !== void 0 || v.description !== void 0 || v.locale !== void 0 || v.subject !== void 0 || v.preheader !== void 0 || v.document !== void 0 || v.translations !== void 0 || v.insertions !== void 0 || v.archived !== void 0,
|
|
166
547
|
"at least one field besides base_version"
|
|
167
548
|
);
|
|
168
|
-
var SavedSectionBody =
|
|
549
|
+
var SavedSectionBody = z4.record(z4.unknown());
|
|
169
550
|
var MAX_SAVED_SECTIONS = 50;
|
|
170
|
-
var SavedSection =
|
|
551
|
+
var SavedSection = z4.object({
|
|
171
552
|
id: Id,
|
|
172
|
-
name:
|
|
173
|
-
description:
|
|
553
|
+
name: z4.string().min(1).max(200),
|
|
554
|
+
description: z4.string().max(2e3).nullable(),
|
|
174
555
|
section: SavedSectionBody,
|
|
175
|
-
created_by:
|
|
556
|
+
created_by: z4.string().nullable(),
|
|
176
557
|
created_at: Timestamp,
|
|
177
558
|
updated_at: Timestamp
|
|
178
559
|
});
|
|
179
|
-
var SavedSectionCreate =
|
|
180
|
-
name:
|
|
181
|
-
description:
|
|
560
|
+
var SavedSectionCreate = z4.object({
|
|
561
|
+
name: z4.string().min(1).max(200),
|
|
562
|
+
description: z4.string().max(2e3).optional(),
|
|
182
563
|
section: SavedSectionBody
|
|
183
564
|
});
|
|
184
|
-
var SavedSectionUpdate =
|
|
185
|
-
name:
|
|
186
|
-
description:
|
|
565
|
+
var SavedSectionUpdate = z4.object({
|
|
566
|
+
name: z4.string().min(1).max(200).optional(),
|
|
567
|
+
description: z4.string().max(2e3).nullable().optional(),
|
|
187
568
|
section: SavedSectionBody.optional()
|
|
188
569
|
}).refine((v) => v.name !== void 0 || v.description !== void 0 || v.section !== void 0, "at least one field");
|
|
189
570
|
var TemplateListQuery = PageQuery.extend({
|
|
190
|
-
archived:
|
|
571
|
+
archived: z4.enum(["true", "false"]).optional()
|
|
191
572
|
});
|
|
192
|
-
var TemplateVersion =
|
|
573
|
+
var TemplateVersion = z4.object({
|
|
193
574
|
template_id: Id,
|
|
194
|
-
version:
|
|
575
|
+
version: z4.number().int().min(1),
|
|
576
|
+
locale: LocaleTag,
|
|
577
|
+
subject: TemplateSubject.nullable(),
|
|
578
|
+
preheader: TemplatePreheader.nullable(),
|
|
195
579
|
document: TemplateDocument,
|
|
196
|
-
|
|
580
|
+
translations: TemplateTranslations,
|
|
581
|
+
insertions: Insertions,
|
|
582
|
+
created_by: z4.string().nullable(),
|
|
197
583
|
created_at: Timestamp
|
|
198
584
|
});
|
|
199
|
-
var TemplateVersionParams =
|
|
585
|
+
var TemplateVersionParams = z4.object({
|
|
200
586
|
id: Id,
|
|
201
|
-
version:
|
|
587
|
+
version: z4.coerce.number().int().min(1)
|
|
202
588
|
});
|
|
203
|
-
var CompileMessage =
|
|
204
|
-
message:
|
|
589
|
+
var CompileMessage = z4.object({
|
|
590
|
+
message: z4.string().min(1),
|
|
205
591
|
/** Where in the document or the MJML the message points, when known. */
|
|
206
|
-
path:
|
|
207
|
-
line:
|
|
592
|
+
path: z4.string().optional(),
|
|
593
|
+
line: z4.number().int().min(1).optional()
|
|
208
594
|
});
|
|
209
|
-
var CompileResult =
|
|
210
|
-
mjml:
|
|
211
|
-
html:
|
|
212
|
-
warnings:
|
|
213
|
-
errors:
|
|
595
|
+
var CompileResult = z4.object({
|
|
596
|
+
mjml: z4.string(),
|
|
597
|
+
html: z4.string(),
|
|
598
|
+
warnings: z4.array(CompileMessage),
|
|
599
|
+
errors: z4.array(CompileMessage)
|
|
214
600
|
});
|
|
215
|
-
var TemplateCompileRequest =
|
|
216
|
-
version:
|
|
601
|
+
var TemplateCompileRequest = z4.object({
|
|
602
|
+
version: z4.number().int().min(1).optional(),
|
|
603
|
+
locale: LocaleTag.optional()
|
|
217
604
|
});
|
|
218
|
-
var CompileRequest =
|
|
605
|
+
var CompileRequest = z4.object({
|
|
219
606
|
document: TemplateDocument
|
|
220
607
|
});
|
|
221
608
|
var ASSET_CONTENT_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
222
|
-
var AssetContentType =
|
|
609
|
+
var AssetContentType = z4.enum(ASSET_CONTENT_TYPES);
|
|
223
610
|
var MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
224
|
-
var Asset =
|
|
611
|
+
var Asset = z4.object({
|
|
225
612
|
id: Id,
|
|
226
|
-
url:
|
|
613
|
+
url: z4.string().url(),
|
|
227
614
|
content_type: AssetContentType,
|
|
228
|
-
size_bytes:
|
|
229
|
-
width:
|
|
230
|
-
height:
|
|
231
|
-
filename:
|
|
615
|
+
size_bytes: z4.number().int().min(1).max(MAX_ASSET_BYTES),
|
|
616
|
+
width: z4.number().int().min(1).nullable(),
|
|
617
|
+
height: z4.number().int().min(1).nullable(),
|
|
618
|
+
filename: z4.string().min(1).max(255),
|
|
232
619
|
created_at: Timestamp
|
|
233
620
|
});
|
|
234
621
|
var ASSET_UPLOAD_FIELD = "file";
|
|
235
622
|
var MAX_IMPORT_URL_LENGTH = 2048;
|
|
236
|
-
var AssetImport =
|
|
237
|
-
url:
|
|
623
|
+
var AssetImport = z4.object({
|
|
624
|
+
url: z4.string().max(MAX_IMPORT_URL_LENGTH).url().refine((u) => {
|
|
238
625
|
try {
|
|
239
626
|
const url = new URL(u);
|
|
240
627
|
if (url.protocol === "https:") return true;
|
|
@@ -244,7 +631,7 @@ var AssetImport = z3.object({
|
|
|
244
631
|
}
|
|
245
632
|
}, "must be https (http only for localhost)"),
|
|
246
633
|
/** The stored file name; derived from the address when omitted. */
|
|
247
|
-
filename:
|
|
634
|
+
filename: z4.string().min(1).max(255).optional()
|
|
248
635
|
});
|
|
249
636
|
var MAX_MJML_IMPORT_BYTES = 512 * 1024;
|
|
250
637
|
var MAX_MJML_DEPTH = 32;
|
|
@@ -259,55 +646,56 @@ var MJML_IMPORT_REFUSALS = [
|
|
|
259
646
|
"too_complex",
|
|
260
647
|
"invalid_document"
|
|
261
648
|
];
|
|
262
|
-
var ImportWarning =
|
|
263
|
-
severity:
|
|
649
|
+
var ImportWarning = z4.object({
|
|
650
|
+
severity: z4.enum(["info", "warning"]),
|
|
264
651
|
/** Stable kind, e.g. `kept_as_html`, `unknown_component`, `remote_image_not_imported`. */
|
|
265
|
-
code:
|
|
652
|
+
code: z4.string().min(1).max(64),
|
|
266
653
|
/** Where in the source, e.g. `mj-body > mj-section[2] > mj-column[1] > mj-social[1]`. */
|
|
267
|
-
path:
|
|
268
|
-
line:
|
|
269
|
-
message:
|
|
270
|
-
fragment:
|
|
654
|
+
path: z4.string(),
|
|
655
|
+
line: z4.number().int().min(1).optional(),
|
|
656
|
+
message: z4.string().min(1),
|
|
657
|
+
fragment: z4.string().optional()
|
|
271
658
|
});
|
|
272
|
-
var RemoteImage =
|
|
273
|
-
url:
|
|
659
|
+
var RemoteImage = z4.object({
|
|
660
|
+
url: z4.string().min(1),
|
|
274
661
|
/** The element and attribute it is loaded from, e.g. `<img src>`. */
|
|
275
|
-
where:
|
|
662
|
+
where: z4.string()
|
|
276
663
|
});
|
|
277
|
-
var ImportedAsset =
|
|
278
|
-
source_url:
|
|
664
|
+
var ImportedAsset = z4.object({
|
|
665
|
+
source_url: z4.string().min(1),
|
|
279
666
|
asset_id: Id,
|
|
280
|
-
url:
|
|
667
|
+
url: z4.string().url()
|
|
281
668
|
});
|
|
282
|
-
var mjmlSource =
|
|
283
|
-
var TemplateImportPreviewRequest =
|
|
284
|
-
var TemplateImportPreview =
|
|
669
|
+
var mjmlSource = z4.string().min(1, "paste or upload the MJML");
|
|
670
|
+
var TemplateImportPreviewRequest = z4.object({ mjml: mjmlSource });
|
|
671
|
+
var TemplateImportPreview = z4.object({
|
|
285
672
|
document: TemplateDocument,
|
|
286
|
-
warnings:
|
|
673
|
+
warnings: z4.array(ImportWarning),
|
|
287
674
|
/** The document compiled under the workspace's asset policy, exactly as a send would. */
|
|
288
675
|
compiled: CompileResult,
|
|
289
676
|
/** Images loaded from outside the service; under `service_only` each is also a compile error. */
|
|
290
|
-
remote_images:
|
|
291
|
-
asset_policy:
|
|
677
|
+
remote_images: z4.array(RemoteImage),
|
|
678
|
+
asset_policy: z4.enum(["any", "service_only"])
|
|
292
679
|
});
|
|
293
|
-
var TemplateImport =
|
|
294
|
-
name:
|
|
295
|
-
description:
|
|
680
|
+
var TemplateImport = z4.object({
|
|
681
|
+
name: z4.string().min(1).max(200),
|
|
682
|
+
description: z4.string().max(2e3).optional(),
|
|
296
683
|
mjml: mjmlSource,
|
|
297
|
-
import_remote_assets:
|
|
684
|
+
import_remote_assets: z4.boolean().optional()
|
|
298
685
|
});
|
|
299
|
-
var TemplateImportResult =
|
|
686
|
+
var TemplateImportResult = z4.object({
|
|
300
687
|
template: Template,
|
|
301
|
-
warnings:
|
|
302
|
-
imported_assets:
|
|
688
|
+
warnings: z4.array(ImportWarning),
|
|
689
|
+
imported_assets: z4.array(ImportedAsset)
|
|
303
690
|
});
|
|
304
691
|
var EXPORT_FORMATS = ["mjml", "html"];
|
|
305
|
-
var ExportFormat =
|
|
306
|
-
var TemplateExportQuery =
|
|
692
|
+
var ExportFormat = z4.enum(EXPORT_FORMATS);
|
|
693
|
+
var TemplateExportQuery = z4.object({
|
|
307
694
|
format: ExportFormat,
|
|
308
|
-
version:
|
|
695
|
+
version: z4.coerce.number().int().min(1).optional(),
|
|
696
|
+
locale: LocaleTag.optional()
|
|
309
697
|
});
|
|
310
|
-
var MailingExportQuery =
|
|
698
|
+
var MailingExportQuery = z4.object({ format: ExportFormat });
|
|
311
699
|
var EXPORT_CONTENT_TYPES = {
|
|
312
700
|
mjml: "text/plain; charset=utf-8",
|
|
313
701
|
html: "text/html; charset=utf-8"
|
|
@@ -326,7 +714,7 @@ function formatExportWarningsHeader(messages) {
|
|
|
326
714
|
function parseExportWarningsHeader(value) {
|
|
327
715
|
if (!value) return [];
|
|
328
716
|
try {
|
|
329
|
-
const parsed =
|
|
717
|
+
const parsed = z4.array(CompileMessage).safeParse(JSON.parse(decodeURIComponent(value)));
|
|
330
718
|
return parsed.success ? parsed.data : [];
|
|
331
719
|
} catch {
|
|
332
720
|
return [];
|
|
@@ -338,32 +726,32 @@ function exportFilename(name, format) {
|
|
|
338
726
|
}
|
|
339
727
|
|
|
340
728
|
// src/platform.ts
|
|
341
|
-
import { z as
|
|
729
|
+
import { z as z7 } from "zod";
|
|
342
730
|
|
|
343
731
|
// src/mailings.ts
|
|
344
|
-
import { z as
|
|
732
|
+
import { z as z6 } from "zod";
|
|
345
733
|
|
|
346
734
|
// src/ab-test.ts
|
|
347
|
-
import { z as
|
|
348
|
-
var AbVariant =
|
|
349
|
-
key:
|
|
350
|
-
subject:
|
|
735
|
+
import { z as z5 } from "zod";
|
|
736
|
+
var AbVariant = z5.object({
|
|
737
|
+
key: z5.string().regex(/^[a-z]$/, "a single lowercase letter"),
|
|
738
|
+
subject: z5.string().min(1).max(998).optional(),
|
|
351
739
|
/** Replaces the mailing's document for this variant. */
|
|
352
|
-
document:
|
|
740
|
+
document: z5.record(z5.unknown()).optional(),
|
|
353
741
|
/**
|
|
354
742
|
* Keeps the document this variant key already has in the mailing's current
|
|
355
743
|
* test, so a test can be changed without sending every document again. The
|
|
356
744
|
* service refuses it (`validation_failed`) when that variant has none.
|
|
357
745
|
*/
|
|
358
|
-
keep_document:
|
|
746
|
+
keep_document: z5.literal(true).optional()
|
|
359
747
|
});
|
|
360
748
|
var AB_WINNER_METRICS = ["opens", "clicks", "manual"];
|
|
361
|
-
var AbTestConfig =
|
|
362
|
-
variants:
|
|
749
|
+
var AbTestConfig = z5.object({
|
|
750
|
+
variants: z5.array(AbVariant).min(2).max(5),
|
|
363
751
|
/** Share of recipients in the test group, split evenly across variants; the rest get the winner. */
|
|
364
|
-
test_fraction:
|
|
365
|
-
winner_metric:
|
|
366
|
-
decide_after_minutes:
|
|
752
|
+
test_fraction: z5.number().gt(0).lte(1),
|
|
753
|
+
winner_metric: z5.enum(AB_WINNER_METRICS),
|
|
754
|
+
decide_after_minutes: z5.number().int().min(15).max(10080).optional()
|
|
367
755
|
}).refine((c) => new Set(c.variants.map((v) => v.key)).size === c.variants.length, "variant keys must be unique").refine((c) => c.variants.every((v) => v.subject !== void 0 || v.document !== void 0 || v.keep_document === true), {
|
|
368
756
|
message: "every variant changes the subject, the document or both",
|
|
369
757
|
path: ["variants"]
|
|
@@ -375,19 +763,19 @@ var AbTestConfig = z4.object({
|
|
|
375
763
|
path: ["decide_after_minutes"]
|
|
376
764
|
});
|
|
377
765
|
var AB_TEST_STATUSES = ["pending", "testing", "awaiting_pick", "decided"];
|
|
378
|
-
var AbTestState =
|
|
379
|
-
variants:
|
|
380
|
-
test_fraction:
|
|
381
|
-
winner_metric:
|
|
382
|
-
decide_after_minutes:
|
|
383
|
-
status:
|
|
766
|
+
var AbTestState = z5.object({
|
|
767
|
+
variants: z5.array(z5.object({ key: z5.string(), subject: z5.string().nullable(), has_document: z5.boolean() })),
|
|
768
|
+
test_fraction: z5.number(),
|
|
769
|
+
winner_metric: z5.enum(AB_WINNER_METRICS),
|
|
770
|
+
decide_after_minutes: z5.number().int().nullable(),
|
|
771
|
+
status: z5.enum(AB_TEST_STATUSES),
|
|
384
772
|
/** When the metric decides; null for manual or before the start. */
|
|
385
773
|
decide_at: Timestamp.nullable(),
|
|
386
|
-
winner:
|
|
387
|
-
decided_by:
|
|
774
|
+
winner: z5.string().nullable(),
|
|
775
|
+
decided_by: z5.enum(["metric", "manual"]).nullable(),
|
|
388
776
|
decided_at: Timestamp.nullable()
|
|
389
777
|
});
|
|
390
|
-
var AbWinnerRequest =
|
|
778
|
+
var AbWinnerRequest = z5.object({ variant: z5.string().regex(/^[a-z]$/) });
|
|
391
779
|
function testGroupSize(total, fraction, variants) {
|
|
392
780
|
return Math.min(total, Math.max(variants, Math.ceil(total * fraction)));
|
|
393
781
|
}
|
|
@@ -402,9 +790,9 @@ var MAILING_STATUSES = [
|
|
|
402
790
|
"partially_failed",
|
|
403
791
|
"cancelled"
|
|
404
792
|
];
|
|
405
|
-
var MailingStatus =
|
|
793
|
+
var MailingStatus = z6.enum(MAILING_STATUSES);
|
|
406
794
|
var MAILING_ACTIONS = ["send", "schedule", "unschedule", "pause", "resume", "cancel", "retry-failed"];
|
|
407
|
-
var MailingAction =
|
|
795
|
+
var MailingAction = z6.enum(MAILING_ACTIONS);
|
|
408
796
|
var MAILING_TRANSITIONS = {
|
|
409
797
|
draft: ["send", "schedule", "cancel"],
|
|
410
798
|
scheduled: ["send", "schedule", "unschedule", "cancel"],
|
|
@@ -419,26 +807,46 @@ function canTransition(status, action) {
|
|
|
419
807
|
}
|
|
420
808
|
var EDITABLE_MAILING_STATUSES = ["draft", "scheduled"];
|
|
421
809
|
var TERMINAL_MAILING_STATUSES = ["sent", "cancelled"];
|
|
422
|
-
var MailingMetadata =
|
|
423
|
-
var MailingCounts =
|
|
424
|
-
total:
|
|
425
|
-
queued:
|
|
426
|
-
sending:
|
|
427
|
-
sent:
|
|
428
|
-
failed:
|
|
429
|
-
skipped:
|
|
810
|
+
var MailingMetadata = z6.record(z6.string().max(500)).refine((v) => Object.keys(v).length <= 20, "at most 20 keys").refine((v) => Object.keys(v).every((k) => k.length >= 1 && k.length <= 64), "keys are 1 to 64 characters");
|
|
811
|
+
var MailingCounts = z6.object({
|
|
812
|
+
total: z6.number().int().min(0),
|
|
813
|
+
queued: z6.number().int().min(0),
|
|
814
|
+
sending: z6.number().int().min(0),
|
|
815
|
+
sent: z6.number().int().min(0),
|
|
816
|
+
failed: z6.number().int().min(0),
|
|
817
|
+
skipped: z6.number().int().min(0)
|
|
430
818
|
});
|
|
431
819
|
var MAILING_KINDS = ["broadcast", "automation", "notification"];
|
|
432
|
-
var MailingKind =
|
|
433
|
-
var Mailing =
|
|
820
|
+
var MailingKind = z6.enum(MAILING_KINDS);
|
|
821
|
+
var Mailing = z6.object({
|
|
434
822
|
id: Id,
|
|
435
823
|
kind: MailingKind,
|
|
436
|
-
name:
|
|
437
|
-
subject:
|
|
438
|
-
preheader:
|
|
439
|
-
/**
|
|
824
|
+
name: z6.string().max(200).nullable(),
|
|
825
|
+
subject: z6.string().min(1).max(998),
|
|
826
|
+
preheader: z6.string().max(500).nullable(),
|
|
827
|
+
/**
|
|
828
|
+
* The template the document was taken from, if any. The snapshot is what is
|
|
829
|
+
* sent in the main language; the template's other language versions are
|
|
830
|
+
* taken, the ready ones, when the mailing starts (see "Languages" in the
|
|
831
|
+
* contract's documentation).
|
|
832
|
+
*/
|
|
440
833
|
template_id: Id.nullable(),
|
|
441
834
|
document: TemplateDocument,
|
|
835
|
+
/**
|
|
836
|
+
* The main language: what `subject`, `preheader` and `document` are written
|
|
837
|
+
* in, and what every recipient without a ready version in their own language
|
|
838
|
+
* gets. The template's main language, or the workspace's default for a
|
|
839
|
+
* mailing made from a document.
|
|
840
|
+
*/
|
|
841
|
+
locale: LocaleTag,
|
|
842
|
+
/**
|
|
843
|
+
* The language every recipient is treated as reading, or null to read each
|
|
844
|
+
* recipient's own (their `merge.locale`, else their contact's language). A
|
|
845
|
+
* letter sets it to the language its one reader wrote in; a broadcast can use
|
|
846
|
+
* it to send one language version to everyone. It is matched like anyone's
|
|
847
|
+
* language: without a ready version in it, the main language goes out.
|
|
848
|
+
*/
|
|
849
|
+
send_locale: LocaleTag.nullable(),
|
|
442
850
|
/**
|
|
443
851
|
* The topic the mailing is sent under. Null for a letter (a one-to-one
|
|
444
852
|
* mailing with at most one recipient, which belongs to no topic and whose
|
|
@@ -460,15 +868,17 @@ var Mailing = z5.object({
|
|
|
460
868
|
* recipients refused as dead addresses in one run (see the provider's
|
|
461
869
|
* `rejections.anomaly`). Resuming clears it and starts a new run.
|
|
462
870
|
*/
|
|
463
|
-
pause_reason:
|
|
871
|
+
pause_reason: z6.string().nullable().default(null),
|
|
464
872
|
created_at: Timestamp,
|
|
465
873
|
updated_at: Timestamp
|
|
466
874
|
});
|
|
467
875
|
var MailingSummary = Mailing.omit({ document: true });
|
|
468
876
|
var MailingContent = {
|
|
469
|
-
name:
|
|
470
|
-
subject:
|
|
471
|
-
preheader:
|
|
877
|
+
name: z6.string().max(200).optional(),
|
|
878
|
+
subject: z6.string().min(1).max(998),
|
|
879
|
+
preheader: z6.string().max(500).optional(),
|
|
880
|
+
/** See `Mailing.send_locale`. */
|
|
881
|
+
send_locale: LocaleTag.nullable().optional(),
|
|
472
882
|
/**
|
|
473
883
|
* Omitted or null makes the mailing a letter: one recipient at most, sent to
|
|
474
884
|
* anybody not blocked on every topic, with an unsubscribe link that offers
|
|
@@ -479,20 +889,25 @@ var MailingContent = {
|
|
|
479
889
|
provider_id: Id,
|
|
480
890
|
metadata: MailingMetadata.optional()
|
|
481
891
|
};
|
|
482
|
-
var MailingCreate =
|
|
892
|
+
var MailingCreate = z6.object({
|
|
483
893
|
...MailingContent,
|
|
894
|
+
subject: MailingContent.subject.optional(),
|
|
484
895
|
document: TemplateDocument.optional(),
|
|
485
896
|
template_id: Id.optional()
|
|
486
897
|
}).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
|
|
487
898
|
message: "exactly one of document or template_id",
|
|
488
899
|
path: ["document"]
|
|
900
|
+
}).refine((v) => v.template_id !== void 0 || v.subject !== void 0, {
|
|
901
|
+
message: "a mailing made from a document needs a subject",
|
|
902
|
+
path: ["subject"]
|
|
489
903
|
});
|
|
490
|
-
var MailingUpdate =
|
|
491
|
-
name:
|
|
904
|
+
var MailingUpdate = z6.object({
|
|
905
|
+
name: z6.string().max(200).nullable().optional(),
|
|
492
906
|
subject: MailingContent.subject.optional(),
|
|
493
|
-
preheader:
|
|
907
|
+
preheader: z6.string().max(500).nullable().optional(),
|
|
494
908
|
/** Null turns a draft into a letter, which it can only be with at most one recipient. */
|
|
495
909
|
topic: Slug.nullable().optional(),
|
|
910
|
+
send_locale: LocaleTag.nullable().optional(),
|
|
496
911
|
provider_id: Id.optional(),
|
|
497
912
|
metadata: MailingMetadata.optional(),
|
|
498
913
|
document: TemplateDocument.optional()
|
|
@@ -503,29 +918,30 @@ var MailingListQuery = PageQuery.extend({
|
|
|
503
918
|
kind: MailingKind.optional(),
|
|
504
919
|
automation_id: Id.optional()
|
|
505
920
|
});
|
|
506
|
-
var MailingSendRequest =
|
|
507
|
-
var MailingRetryFailedRequest =
|
|
921
|
+
var MailingSendRequest = z6.object({}).strict();
|
|
922
|
+
var MailingRetryFailedRequest = z6.object({
|
|
508
923
|
/**
|
|
509
924
|
* Also requeue recipients whose outcome is unknown after a crash. They may
|
|
510
925
|
* already have received the message, so this is a human's decision, never a
|
|
511
926
|
* default.
|
|
512
927
|
*/
|
|
513
|
-
include_outcome_unknown:
|
|
928
|
+
include_outcome_unknown: z6.boolean().optional()
|
|
514
929
|
}).strict();
|
|
515
|
-
var MailingActionRequest =
|
|
516
|
-
var MailingTestRequest =
|
|
930
|
+
var MailingActionRequest = z6.object({}).strict();
|
|
931
|
+
var MailingTestRequest = z6.object({
|
|
517
932
|
to: Email,
|
|
518
|
-
merge: Properties.optional()
|
|
933
|
+
merge: Properties.optional(),
|
|
934
|
+
locale: LocaleTag.optional()
|
|
519
935
|
});
|
|
520
|
-
var MailingTestResult =
|
|
936
|
+
var MailingTestResult = z6.object({
|
|
521
937
|
message_id: Id,
|
|
522
|
-
provider_message_id:
|
|
938
|
+
provider_message_id: z6.string().nullable()
|
|
523
939
|
});
|
|
524
940
|
var RECIPIENT_STATUSES = ["queued", "sending", "sent", "failed", "skipped"];
|
|
525
|
-
var RecipientStatus =
|
|
941
|
+
var RecipientStatus = z6.enum(RECIPIENT_STATUSES);
|
|
526
942
|
var SKIP_REASONS = ["suppressed", "not_subscribed", "contact_erased", "cancelled", "outcome_unknown"];
|
|
527
|
-
var SkipReason =
|
|
528
|
-
var Recipient =
|
|
943
|
+
var SkipReason = z6.enum(SKIP_REASONS);
|
|
944
|
+
var Recipient = z6.object({
|
|
529
945
|
id: Id,
|
|
530
946
|
mailing_id: Id,
|
|
531
947
|
contact_id: Id.nullable(),
|
|
@@ -533,84 +949,125 @@ var Recipient = z5.object({
|
|
|
533
949
|
merge: Properties,
|
|
534
950
|
status: RecipientStatus,
|
|
535
951
|
skip_reason: SkipReason.nullable(),
|
|
536
|
-
attempts:
|
|
952
|
+
attempts: z6.number().int().min(0),
|
|
537
953
|
message_id: Id.nullable(),
|
|
538
|
-
last_error:
|
|
954
|
+
last_error: z6.string().nullable(),
|
|
539
955
|
created_at: Timestamp,
|
|
540
956
|
updated_at: Timestamp
|
|
541
957
|
});
|
|
542
958
|
var MAX_RECIPIENTS_PER_BATCH = 1e3;
|
|
543
959
|
var MAX_LETTER_RECIPIENTS = 1;
|
|
544
960
|
var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
|
|
545
|
-
var RecipientInput =
|
|
961
|
+
var RecipientInput = z6.object({
|
|
546
962
|
contact_id: Id.optional(),
|
|
547
|
-
external_id:
|
|
963
|
+
external_id: z6.string().min(1).max(255).optional(),
|
|
548
964
|
email: Email.optional(),
|
|
549
965
|
merge: Properties.optional()
|
|
550
966
|
}).refine((v) => v.contact_id !== void 0 || v.external_id !== void 0 || v.email !== void 0, {
|
|
551
967
|
message: "contact_id, external_id or email is required",
|
|
552
968
|
path: ["email"]
|
|
553
969
|
});
|
|
554
|
-
var RecipientBatch =
|
|
555
|
-
recipients:
|
|
970
|
+
var RecipientBatch = z6.object({
|
|
971
|
+
recipients: z6.array(RecipientInput).min(1).max(MAX_RECIPIENTS_PER_BATCH)
|
|
556
972
|
});
|
|
557
973
|
var RECIPIENT_REJECTIONS = ["unknown_contact", "duplicate_in_batch"];
|
|
558
|
-
var RecipientBatchResult =
|
|
974
|
+
var RecipientBatchResult = z6.object({
|
|
559
975
|
/** Newly queued. */
|
|
560
|
-
added:
|
|
976
|
+
added: z6.number().int().min(0),
|
|
561
977
|
/** Already on the mailing from an earlier batch: left unchanged. */
|
|
562
|
-
already_present:
|
|
978
|
+
already_present: z6.number().int().min(0),
|
|
563
979
|
/** Items not added, by their index in the request. */
|
|
564
|
-
rejected:
|
|
980
|
+
rejected: z6.array(z6.object({ index: z6.number().int().min(0), reason: z6.enum(RECIPIENT_REJECTIONS) }))
|
|
565
981
|
});
|
|
566
982
|
var RecipientListQuery = PageQuery.extend({
|
|
567
983
|
status: RecipientStatus.optional(),
|
|
568
984
|
skip_reason: SkipReason.optional()
|
|
569
985
|
});
|
|
570
986
|
var MESSAGE_OUTCOMES = ["sent", "failed"];
|
|
571
|
-
var MessageOutcome =
|
|
572
|
-
var MessageSummary =
|
|
987
|
+
var MessageOutcome = z6.enum(MESSAGE_OUTCOMES);
|
|
988
|
+
var MessageSummary = z6.object({
|
|
573
989
|
id: Id,
|
|
574
990
|
mailing_id: Id.nullable(),
|
|
575
991
|
recipient_id: Id.nullable(),
|
|
576
992
|
contact_id: Id.nullable(),
|
|
577
993
|
to: Email,
|
|
578
|
-
subject:
|
|
994
|
+
subject: z6.string(),
|
|
579
995
|
provider_id: Id,
|
|
580
|
-
provider_message_id:
|
|
996
|
+
provider_message_id: z6.string().nullable(),
|
|
581
997
|
outcome: MessageOutcome,
|
|
582
|
-
error:
|
|
583
|
-
is_test:
|
|
584
|
-
recipient_count:
|
|
998
|
+
error: z6.string().nullable(),
|
|
999
|
+
is_test: z6.boolean(),
|
|
1000
|
+
recipient_count: z6.number().int().min(1),
|
|
1001
|
+
/** The language version the message was sent in; null for messages archived before languages existed. */
|
|
1002
|
+
locale: LocaleTag.nullable(),
|
|
1003
|
+
/**
|
|
1004
|
+
* The choice each of the template's insertions made for this recipient
|
|
1005
|
+
* (`{ opening: { value: "venue", chosen: "venue", matched: true } }`);
|
|
1006
|
+
* `matched: false` is a filled value no choice took, sent with the
|
|
1007
|
+
* "otherwise" text. Null when the message had no insertions.
|
|
1008
|
+
*/
|
|
1009
|
+
insertions: InsertionChoices.nullable(),
|
|
585
1010
|
created_at: Timestamp
|
|
586
1011
|
});
|
|
587
|
-
var Message = MessageSummary.extend({ html:
|
|
1012
|
+
var Message = MessageSummary.extend({ html: z6.string() });
|
|
1013
|
+
var MailingLanguageStatus = z6.enum(["main", "ready", "draft", "not_a_workspace_language"]);
|
|
1014
|
+
var MailingLanguage = z6.object({
|
|
1015
|
+
locale: LocaleTag,
|
|
1016
|
+
status: MailingLanguageStatus,
|
|
1017
|
+
/** Queued recipients who will get this version. */
|
|
1018
|
+
planned: z6.number().int().min(0),
|
|
1019
|
+
/** Messages already sent in this language (tests excluded). */
|
|
1020
|
+
sent: z6.number().int().min(0)
|
|
1021
|
+
});
|
|
1022
|
+
var MailingLanguageReport = z6.object({
|
|
1023
|
+
main_locale: LocaleTag,
|
|
1024
|
+
send_locale: LocaleTag.nullable(),
|
|
1025
|
+
languages: z6.array(MailingLanguage),
|
|
1026
|
+
fallback: z6.array(z6.object({ wanted: z6.string(), planned: z6.number().int().min(1) })),
|
|
1027
|
+
no_language: z6.number().int().min(0),
|
|
1028
|
+
/**
|
|
1029
|
+
* Values that no choice of an insertion takes, so the "otherwise" text goes
|
|
1030
|
+
* out (docs/plans/2026-09-25-translated-insertions.md, decision 3): per
|
|
1031
|
+
* insertion and value, the queued recipients who send it (`planned`) and the
|
|
1032
|
+
* messages that went out with it (`sent`). A client whose form offers the
|
|
1033
|
+
* template's keys never produces one; a non-empty list means the client and
|
|
1034
|
+
* the template have drifted apart.
|
|
1035
|
+
*/
|
|
1036
|
+
insertion_misses: z6.array(
|
|
1037
|
+
z6.object({
|
|
1038
|
+
insertion: z6.string(),
|
|
1039
|
+
value: z6.string(),
|
|
1040
|
+
planned: z6.number().int().min(0),
|
|
1041
|
+
sent: z6.number().int().min(0)
|
|
1042
|
+
})
|
|
1043
|
+
)
|
|
1044
|
+
});
|
|
588
1045
|
var MessageListQuery = PageQuery.extend({
|
|
589
1046
|
mailing_id: Id.optional(),
|
|
590
1047
|
outcome: MessageOutcome.optional()
|
|
591
1048
|
});
|
|
592
1049
|
|
|
593
1050
|
// src/platform.ts
|
|
594
|
-
var Tag =
|
|
1051
|
+
var Tag = z7.object({
|
|
595
1052
|
id: Id,
|
|
596
1053
|
slug: Slug,
|
|
597
|
-
name:
|
|
598
|
-
contact_count:
|
|
1054
|
+
name: z7.string().min(1).max(120),
|
|
1055
|
+
contact_count: z7.number().int().min(0),
|
|
599
1056
|
created_at: Timestamp
|
|
600
1057
|
});
|
|
601
|
-
var TagCreate =
|
|
602
|
-
var TagAssignment =
|
|
603
|
-
contact_ids:
|
|
1058
|
+
var TagCreate = z7.object({ slug: Slug, name: z7.string().min(1).max(120) });
|
|
1059
|
+
var TagAssignment = z7.object({
|
|
1060
|
+
contact_ids: z7.array(Id).min(1).max(1e3)
|
|
604
1061
|
});
|
|
605
1062
|
var CONTACT_PROPERTY_TYPES = ["string", "number", "boolean", "date"];
|
|
606
|
-
var ContactPropertyType =
|
|
607
|
-
var ContactPropertyKey =
|
|
608
|
-
var ContactPropertyDefinition =
|
|
1063
|
+
var ContactPropertyType = z7.enum(CONTACT_PROPERTY_TYPES);
|
|
1064
|
+
var ContactPropertyKey = z7.string().regex(/^[A-Za-z0-9_.-]{1,64}$/, "letters, digits, dot, underscore and hyphen, 1 to 64");
|
|
1065
|
+
var ContactPropertyDefinition = z7.object({
|
|
609
1066
|
key: ContactPropertyKey,
|
|
610
|
-
label:
|
|
1067
|
+
label: z7.string().min(1).max(120),
|
|
611
1068
|
type: ContactPropertyType
|
|
612
1069
|
});
|
|
613
|
-
var ContactPropertyParams =
|
|
1070
|
+
var ContactPropertyParams = z7.object({ key: ContactPropertyKey });
|
|
614
1071
|
var FILTER_OPERATORS = [
|
|
615
1072
|
"eq",
|
|
616
1073
|
"neq",
|
|
@@ -626,8 +1083,8 @@ var FILTER_OPERATORS = [
|
|
|
626
1083
|
"in",
|
|
627
1084
|
"not_in"
|
|
628
1085
|
];
|
|
629
|
-
var FilterOperator =
|
|
630
|
-
var FilterField =
|
|
1086
|
+
var FilterOperator = z7.enum(FILTER_OPERATORS);
|
|
1087
|
+
var FilterField = z7.string().regex(
|
|
631
1088
|
/^(email|first_name|last_name|locale|created_at|tag|topic|engagement:(opened|clicked)|property:[A-Za-z0-9_.-]{1,64})$/,
|
|
632
1089
|
"unknown filter field"
|
|
633
1090
|
);
|
|
@@ -643,18 +1100,18 @@ var FILTER_FIELD_OPERATORS = {
|
|
|
643
1100
|
property: FILTER_OPERATORS
|
|
644
1101
|
};
|
|
645
1102
|
var MAX_FILTER_DEPTH = 5;
|
|
646
|
-
var FilterCondition =
|
|
1103
|
+
var FilterCondition = z7.object({ field: FilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
|
|
647
1104
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
648
1105
|
path: ["value"]
|
|
649
1106
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
650
1107
|
message: "in and not_in take an array",
|
|
651
1108
|
path: ["value"]
|
|
652
1109
|
});
|
|
653
|
-
var SegmentFilter =
|
|
654
|
-
() =>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1110
|
+
var SegmentFilter = z7.lazy(
|
|
1111
|
+
() => z7.union([
|
|
1112
|
+
z7.object({ and: z7.array(SegmentFilter).min(1).max(50) }).strict(),
|
|
1113
|
+
z7.object({ or: z7.array(SegmentFilter).min(1).max(50) }).strict(),
|
|
1114
|
+
z7.object({ not: SegmentFilter }).strict(),
|
|
658
1115
|
FilterCondition
|
|
659
1116
|
])
|
|
660
1117
|
);
|
|
@@ -742,42 +1199,42 @@ function untypedValueProblem(c, every) {
|
|
|
742
1199
|
if (orderOp) return every(isFilterNumber, "numbers (define the property to compare dates)");
|
|
743
1200
|
return every((v) => isScalar(v) || v === null, "strings, numbers, booleans or null");
|
|
744
1201
|
}
|
|
745
|
-
var Segment =
|
|
1202
|
+
var Segment = z7.object({
|
|
746
1203
|
id: Id,
|
|
747
|
-
name:
|
|
1204
|
+
name: z7.string().min(1).max(120),
|
|
748
1205
|
filter: SegmentFilter,
|
|
749
1206
|
/** Computed on read. */
|
|
750
|
-
contact_count:
|
|
1207
|
+
contact_count: z7.number().int().min(0),
|
|
751
1208
|
created_at: Timestamp,
|
|
752
1209
|
updated_at: Timestamp
|
|
753
1210
|
});
|
|
754
|
-
var SegmentCreate =
|
|
755
|
-
var SegmentPreviewRequest =
|
|
756
|
-
var SegmentPreview =
|
|
757
|
-
contact_count:
|
|
1211
|
+
var SegmentCreate = z7.object({ name: z7.string().min(1).max(120), filter: BoundedSegmentFilter });
|
|
1212
|
+
var SegmentPreviewRequest = z7.object({ filter: BoundedSegmentFilter });
|
|
1213
|
+
var SegmentPreview = z7.object({
|
|
1214
|
+
contact_count: z7.number().int().min(0),
|
|
758
1215
|
/** Up to five matching contacts, for a human to sanity-check the filter. */
|
|
759
|
-
sample:
|
|
1216
|
+
sample: z7.array(z7.object({ id: Id, email: Email })).max(5)
|
|
760
1217
|
});
|
|
761
|
-
var MailingAudienceFromSegment =
|
|
1218
|
+
var MailingAudienceFromSegment = z7.object({ segment_id: Id });
|
|
762
1219
|
var SIGNUP_FORM_FIELDS = ["first_name", "last_name"];
|
|
763
1220
|
var HOSTED_PAGE_LOCALES = ["en", "de", "it", "fr", "es"];
|
|
764
|
-
var SignupFormTranslation =
|
|
765
|
-
title:
|
|
766
|
-
consent_text:
|
|
1221
|
+
var SignupFormTranslation = z7.object({
|
|
1222
|
+
title: z7.string().min(1).max(120),
|
|
1223
|
+
consent_text: z7.string().min(1).max(2e3)
|
|
767
1224
|
});
|
|
768
|
-
var SignupForm =
|
|
1225
|
+
var SignupForm = z7.object({
|
|
769
1226
|
id: Id,
|
|
770
|
-
name:
|
|
1227
|
+
name: z7.string().min(1).max(120),
|
|
771
1228
|
/** The heading of the hosted page. */
|
|
772
|
-
title:
|
|
1229
|
+
title: z7.string().min(1).max(120),
|
|
773
1230
|
/** What the person agrees to, shown above the button and recorded with the confirmation. */
|
|
774
|
-
consent_text:
|
|
775
|
-
translations:
|
|
776
|
-
topics:
|
|
777
|
-
tags:
|
|
1231
|
+
consent_text: z7.string().min(1).max(2e3),
|
|
1232
|
+
translations: z7.record(SignupFormTranslation),
|
|
1233
|
+
topics: z7.array(Slug).min(1),
|
|
1234
|
+
tags: z7.array(Slug),
|
|
778
1235
|
/** Fields shown besides email. */
|
|
779
|
-
fields:
|
|
780
|
-
double_opt_in:
|
|
1236
|
+
fields: z7.array(z7.enum(SIGNUP_FORM_FIELDS)),
|
|
1237
|
+
double_opt_in: z7.literal(true),
|
|
781
1238
|
/** The provider the confirmation mail is sent through. */
|
|
782
1239
|
provider_id: Id,
|
|
783
1240
|
/**
|
|
@@ -786,11 +1243,11 @@ var SignupForm = z6.object({
|
|
|
786
1243
|
*/
|
|
787
1244
|
confirmation_template_id: Id.nullable(),
|
|
788
1245
|
/** Where the person lands after confirming. Null shows the hosted confirmation page. */
|
|
789
|
-
redirect_url:
|
|
1246
|
+
redirect_url: z7.string().url().nullable(),
|
|
790
1247
|
/** Origins allowed to embed the form and call the submission route from a browser. */
|
|
791
|
-
allowed_origins:
|
|
1248
|
+
allowed_origins: z7.array(z7.string().url()).max(20),
|
|
792
1249
|
/** Bumped on every change; recorded with each confirmation. */
|
|
793
|
-
version:
|
|
1250
|
+
version: z7.number().int().min(1),
|
|
794
1251
|
created_at: Timestamp,
|
|
795
1252
|
updated_at: Timestamp
|
|
796
1253
|
});
|
|
@@ -805,56 +1262,56 @@ var SignupFormCreate = SignupForm.omit({
|
|
|
805
1262
|
tags: SignupForm.shape.tags.optional(),
|
|
806
1263
|
fields: SignupForm.shape.fields.optional(),
|
|
807
1264
|
confirmation_template_id: Id.nullable().optional(),
|
|
808
|
-
redirect_url:
|
|
1265
|
+
redirect_url: z7.string().url().nullable().optional(),
|
|
809
1266
|
allowed_origins: SignupForm.shape.allowed_origins.optional()
|
|
810
1267
|
});
|
|
811
|
-
var SignupFormEmbed =
|
|
812
|
-
hosted_url:
|
|
1268
|
+
var SignupFormEmbed = z7.object({
|
|
1269
|
+
hosted_url: z7.string().url(),
|
|
813
1270
|
/** A plain HTML form posting to the hosted page; works without JavaScript. */
|
|
814
|
-
html:
|
|
1271
|
+
html: z7.string(),
|
|
815
1272
|
/** A small optional script that adds the time check and submits in place. */
|
|
816
|
-
script_url:
|
|
1273
|
+
script_url: z7.string().url(),
|
|
817
1274
|
/** Where a browser fetches a fresh `form_token` from (GET, public, CORS for `allowed_origins`). */
|
|
818
|
-
token_url:
|
|
1275
|
+
token_url: z7.string().url()
|
|
819
1276
|
});
|
|
820
|
-
var SignupSubmission =
|
|
1277
|
+
var SignupSubmission = z7.object({
|
|
821
1278
|
email: Email,
|
|
822
|
-
first_name:
|
|
823
|
-
last_name:
|
|
824
|
-
locale:
|
|
1279
|
+
first_name: z7.string().max(200).optional(),
|
|
1280
|
+
last_name: z7.string().max(200).optional(),
|
|
1281
|
+
locale: z7.string().min(2).max(35).optional(),
|
|
825
1282
|
/** Honeypot: must be empty. */
|
|
826
|
-
website:
|
|
1283
|
+
website: z7.string().max(0).optional(),
|
|
827
1284
|
/**
|
|
828
1285
|
* The signed render time from the form's token URL: a submission sooner than a
|
|
829
1286
|
* human could fill the form, or long after, is refused.
|
|
830
1287
|
*/
|
|
831
|
-
form_token:
|
|
1288
|
+
form_token: z7.string().min(1).max(512)
|
|
832
1289
|
});
|
|
833
1290
|
var IMPORT_STATUSES = ["uploaded", "validating", "validated", "committing", "completed", "failed", "cancelled"];
|
|
834
|
-
var ImportStatus =
|
|
1291
|
+
var ImportStatus = z7.enum(IMPORT_STATUSES);
|
|
835
1292
|
var IMPORT_TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
836
|
-
var ImportColumnMapping =
|
|
837
|
-
|
|
1293
|
+
var ImportColumnMapping = z7.record(
|
|
1294
|
+
z7.string().regex(/^(email|external_id|first_name|last_name|locale|property:[A-Za-z0-9_.-]{1,64}|ignore)$/)
|
|
838
1295
|
);
|
|
839
1296
|
var IMPORT_FILE_FIELD = "file";
|
|
840
1297
|
var MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
|
841
1298
|
var MAX_IMPORT_ROWS = 2e5;
|
|
842
|
-
var ImportMappingRequest =
|
|
1299
|
+
var ImportMappingRequest = z7.object({
|
|
843
1300
|
/** CSV header -> contact field. Exactly one column maps to `email`. */
|
|
844
1301
|
mapping: ImportColumnMapping.refine(
|
|
845
1302
|
(m) => Object.values(m).filter((v) => v === "email").length === 1,
|
|
846
1303
|
"exactly one column maps to email"
|
|
847
1304
|
),
|
|
848
|
-
topics:
|
|
849
|
-
tags:
|
|
1305
|
+
topics: z7.array(Slug).max(100),
|
|
1306
|
+
tags: z7.array(Slug).max(100),
|
|
850
1307
|
/** Existing contacts get the mapped fields overwritten; false only adds topics, tags and missing fields. */
|
|
851
|
-
update_existing:
|
|
1308
|
+
update_existing: z7.boolean().optional(),
|
|
852
1309
|
/** The importer states the people consented; the service records who said so and when. */
|
|
853
|
-
consent_confirmed:
|
|
1310
|
+
consent_confirmed: z7.literal(true)
|
|
854
1311
|
});
|
|
855
|
-
var ImportCommitRequest =
|
|
1312
|
+
var ImportCommitRequest = z7.object({ mapping_version: z7.number().int().min(1) });
|
|
856
1313
|
var IMPORT_ROW_OUTCOMES = ["created", "updated", "unchanged", "suppressed", "skipped"];
|
|
857
|
-
var ImportRowOutcome =
|
|
1314
|
+
var ImportRowOutcome = z7.enum(IMPORT_ROW_OUTCOMES);
|
|
858
1315
|
var IMPORT_SKIP_REASONS = [
|
|
859
1316
|
"missing_email",
|
|
860
1317
|
"invalid_email",
|
|
@@ -863,90 +1320,90 @@ var IMPORT_SKIP_REASONS = [
|
|
|
863
1320
|
"external_id_conflict",
|
|
864
1321
|
"wrong_column_count"
|
|
865
1322
|
];
|
|
866
|
-
var ImportSkipReason =
|
|
867
|
-
var ImportReport =
|
|
868
|
-
created:
|
|
869
|
-
updated:
|
|
870
|
-
unchanged:
|
|
871
|
-
suppressed:
|
|
872
|
-
skipped:
|
|
873
|
-
skipped_by_reason:
|
|
1323
|
+
var ImportSkipReason = z7.enum(IMPORT_SKIP_REASONS);
|
|
1324
|
+
var ImportReport = z7.object({
|
|
1325
|
+
created: z7.number().int().min(0),
|
|
1326
|
+
updated: z7.number().int().min(0),
|
|
1327
|
+
unchanged: z7.number().int().min(0),
|
|
1328
|
+
suppressed: z7.number().int().min(0),
|
|
1329
|
+
skipped: z7.number().int().min(0),
|
|
1330
|
+
skipped_by_reason: z7.record(ImportSkipReason, z7.number().int().min(0)),
|
|
874
1331
|
/** Rows whose contact was subscribed without some topic, because the address is blocked for it. */
|
|
875
|
-
topics_withheld:
|
|
1332
|
+
topics_withheld: z7.number().int().min(0)
|
|
876
1333
|
});
|
|
877
|
-
var ImportJob =
|
|
1334
|
+
var ImportJob = z7.object({
|
|
878
1335
|
id: Id,
|
|
879
1336
|
status: ImportStatus,
|
|
880
|
-
file_name:
|
|
881
|
-
file_bytes:
|
|
882
|
-
total_rows:
|
|
1337
|
+
file_name: z7.string().max(255).nullable(),
|
|
1338
|
+
file_bytes: z7.number().int().min(0),
|
|
1339
|
+
total_rows: z7.number().int().min(0),
|
|
883
1340
|
/** The header row, as in the file. */
|
|
884
|
-
columns:
|
|
1341
|
+
columns: z7.array(z7.string()),
|
|
885
1342
|
/** The first rows, for the mapping screen. */
|
|
886
|
-
sample:
|
|
1343
|
+
sample: z7.array(z7.array(z7.string())).max(5),
|
|
887
1344
|
/** A mapping guessed from the headers, for the mapping screen to start from. */
|
|
888
1345
|
suggested_mapping: ImportColumnMapping,
|
|
889
1346
|
mapping: ImportColumnMapping.nullable(),
|
|
890
|
-
mapping_version:
|
|
891
|
-
topics:
|
|
892
|
-
tags:
|
|
893
|
-
update_existing:
|
|
1347
|
+
mapping_version: z7.number().int().min(0),
|
|
1348
|
+
topics: z7.array(Slug),
|
|
1349
|
+
tags: z7.array(Slug),
|
|
1350
|
+
update_existing: z7.boolean(),
|
|
894
1351
|
/** The dry run of the current mapping; null before one finished. */
|
|
895
1352
|
dry_run: ImportReport.nullable(),
|
|
896
1353
|
/** What the commit has written so far; null before it started. */
|
|
897
1354
|
result: ImportReport.nullable(),
|
|
898
1355
|
/** Rows the current phase (dry run or commit) has processed. */
|
|
899
|
-
processed_rows:
|
|
1356
|
+
processed_rows: z7.number().int().min(0),
|
|
900
1357
|
/** Why a `failed` import failed. */
|
|
901
|
-
error:
|
|
1358
|
+
error: z7.string().nullable(),
|
|
902
1359
|
created_at: Timestamp,
|
|
903
1360
|
updated_at: Timestamp,
|
|
904
1361
|
finished_at: Timestamp.nullable()
|
|
905
1362
|
});
|
|
906
|
-
var ImportRow =
|
|
1363
|
+
var ImportRow = z7.object({
|
|
907
1364
|
/** The line in the file, 1 being the first line after the header. */
|
|
908
|
-
row:
|
|
909
|
-
email:
|
|
1365
|
+
row: z7.number().int().min(1),
|
|
1366
|
+
email: z7.string().nullable(),
|
|
910
1367
|
outcome: ImportRowOutcome,
|
|
911
1368
|
reason: ImportSkipReason.nullable(),
|
|
912
1369
|
/** A human explanation for a skipped row (which value, which column). */
|
|
913
|
-
message:
|
|
1370
|
+
message: z7.string().nullable(),
|
|
914
1371
|
contact_id: Id.nullable()
|
|
915
1372
|
});
|
|
916
1373
|
var ImportRowListQuery = PageQuery.extend({ outcome: ImportRowOutcome.optional() });
|
|
917
|
-
var MailingScheduleRequest =
|
|
918
|
-
var TrackingSettings =
|
|
919
|
-
var VariantAnalytics =
|
|
920
|
-
key:
|
|
921
|
-
sent:
|
|
922
|
-
unique_opens:
|
|
923
|
-
unique_clicks:
|
|
924
|
-
});
|
|
925
|
-
var MailingAnalytics =
|
|
1374
|
+
var MailingScheduleRequest = z7.object({ send_at: Timestamp });
|
|
1375
|
+
var TrackingSettings = z7.object({ opens: z7.boolean(), clicks: z7.boolean() });
|
|
1376
|
+
var VariantAnalytics = z7.object({
|
|
1377
|
+
key: z7.string(),
|
|
1378
|
+
sent: z7.number().int().min(0),
|
|
1379
|
+
unique_opens: z7.number().int().min(0).nullable(),
|
|
1380
|
+
unique_clicks: z7.number().int().min(0).nullable()
|
|
1381
|
+
});
|
|
1382
|
+
var MailingAnalytics = z7.object({
|
|
926
1383
|
mailing_id: Id,
|
|
927
1384
|
counts: MailingCounts,
|
|
928
1385
|
/** What was tracked for this mailing, fixed when it started; null before it started. */
|
|
929
1386
|
tracking: TrackingSettings.nullable(),
|
|
930
1387
|
/** Unique recipients with a human open. Null when opens were not tracked. */
|
|
931
|
-
unique_opens:
|
|
1388
|
+
unique_opens: z7.number().int().min(0).nullable(),
|
|
932
1389
|
/**
|
|
933
1390
|
* Recipients whose only opens came from Apple Mail Privacy Protection, which
|
|
934
1391
|
* loads every image on delivery: counted apart, never as human opens.
|
|
935
1392
|
*/
|
|
936
|
-
apple_mpp_opens:
|
|
1393
|
+
apple_mpp_opens: z7.number().int().min(0).nullable(),
|
|
937
1394
|
/** Opens and clicks from security scanners and prefetchers, filtered out of the unique counts. */
|
|
938
|
-
machine_events:
|
|
939
|
-
unique_clicks:
|
|
940
|
-
unsubscribes:
|
|
941
|
-
bounces:
|
|
942
|
-
complaints:
|
|
943
|
-
links:
|
|
944
|
-
variants:
|
|
1395
|
+
machine_events: z7.number().int().min(0).nullable(),
|
|
1396
|
+
unique_clicks: z7.number().int().min(0).nullable(),
|
|
1397
|
+
unsubscribes: z7.number().int().min(0),
|
|
1398
|
+
bounces: z7.number().int().min(0),
|
|
1399
|
+
complaints: z7.number().int().min(0),
|
|
1400
|
+
links: z7.array(z7.object({ url: z7.string(), unique_clicks: z7.number().int().min(0) })).nullable(),
|
|
1401
|
+
variants: z7.array(VariantAnalytics).nullable()
|
|
945
1402
|
});
|
|
946
1403
|
|
|
947
1404
|
// src/automations.ts
|
|
948
1405
|
var AUTOMATION_FILTER_FIELD_PATTERN = /^(email|first_name|last_name|locale|created_at|tag|topic|segment|engagement:(opened|clicked)|step:(opened|clicked)|property:[A-Za-z0-9_.-]{1,64}|event:[A-Za-z0-9_.-]{1,64})$/;
|
|
949
|
-
var AutomationFilterField =
|
|
1406
|
+
var AutomationFilterField = z8.string().regex(AUTOMATION_FILTER_FIELD_PATTERN, "unknown filter field");
|
|
950
1407
|
var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
951
1408
|
...FILTER_FIELD_OPERATORS,
|
|
952
1409
|
segment: ["eq", "neq", "in", "not_in"],
|
|
@@ -956,18 +1413,18 @@ var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
|
956
1413
|
step: ["eq", "neq"]
|
|
957
1414
|
};
|
|
958
1415
|
var automationFilterFamily = filterFieldFamily;
|
|
959
|
-
var AutomationFilterCondition =
|
|
1416
|
+
var AutomationFilterCondition = z8.object({ field: AutomationFilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
|
|
960
1417
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
961
1418
|
path: ["value"]
|
|
962
1419
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
963
1420
|
message: "in and not_in take an array",
|
|
964
1421
|
path: ["value"]
|
|
965
1422
|
});
|
|
966
|
-
var AutomationFilter =
|
|
967
|
-
() =>
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1423
|
+
var AutomationFilter = z8.lazy(
|
|
1424
|
+
() => z8.union([
|
|
1425
|
+
z8.object({ and: z8.array(AutomationFilter).min(1).max(50) }).strict(),
|
|
1426
|
+
z8.object({ or: z8.array(AutomationFilter).min(1).max(50) }).strict(),
|
|
1427
|
+
z8.object({ not: AutomationFilter }).strict(),
|
|
971
1428
|
AutomationFilterCondition
|
|
972
1429
|
])
|
|
973
1430
|
);
|
|
@@ -980,20 +1437,20 @@ function filterConditions(filter) {
|
|
|
980
1437
|
if ("not" in filter) return filterConditions(filter.not);
|
|
981
1438
|
return [filter];
|
|
982
1439
|
}
|
|
983
|
-
var StepId =
|
|
984
|
-
var TimeOfDay =
|
|
985
|
-
var Weekday =
|
|
986
|
-
var TimezoneName =
|
|
1440
|
+
var StepId = z8.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "letters, digits, underscore and hyphen, 1 to 64");
|
|
1441
|
+
var TimeOfDay = z8.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "HH:MM in 24 hours");
|
|
1442
|
+
var Weekday = z8.number().int().min(0).max(6);
|
|
1443
|
+
var TimezoneName = z8.string().min(1).max(64).regex(/^(UTC|[A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+)+)$/, "an IANA time zone name such as Europe/Berlin");
|
|
987
1444
|
var MAX_DELAY_SECONDS = 365 * 24 * 60 * 60;
|
|
988
|
-
var DateOnly =
|
|
1445
|
+
var DateOnly = z8.string().regex(/^\d{4}-\d{2}-\d{2}$/, "a calendar date such as 2026-09-20").refine((v) => {
|
|
989
1446
|
const [y, m, d] = v.split("-").map(Number);
|
|
990
1447
|
const t = new Date(Date.UTC(y, m - 1, d));
|
|
991
1448
|
return t.getUTCFullYear() === y && t.getUTCMonth() + 1 === m && t.getUTCDate() === d;
|
|
992
1449
|
}, "a real calendar date");
|
|
993
|
-
var OffsetDays =
|
|
1450
|
+
var OffsetDays = z8.number().int().min(-365).max(365);
|
|
994
1451
|
var DATE_PASSED_ANSWERS = ["skip", "stop", "continue"];
|
|
995
|
-
var DatePassedAnswer =
|
|
996
|
-
var EventName =
|
|
1452
|
+
var DatePassedAnswer = z8.enum(DATE_PASSED_ANSWERS);
|
|
1453
|
+
var EventName = z8.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/, "letters, digits, dot, colon, underscore and hyphen, 1 to 80");
|
|
997
1454
|
var AUTOMATION_TRIGGER_KINDS = [
|
|
998
1455
|
"topic_subscribed",
|
|
999
1456
|
"form_confirmed",
|
|
@@ -1006,17 +1463,17 @@ var AUTOMATION_TRIGGER_KINDS = [
|
|
|
1006
1463
|
"segment_entered",
|
|
1007
1464
|
"link_clicked"
|
|
1008
1465
|
];
|
|
1009
|
-
var AutomationTriggerKind =
|
|
1466
|
+
var AutomationTriggerKind = z8.enum(AUTOMATION_TRIGGER_KINDS);
|
|
1010
1467
|
var exclude = { exclude: BoundedAutomationFilter.nullish() };
|
|
1011
|
-
var AutomationTrigger =
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1468
|
+
var AutomationTrigger = z8.discriminatedUnion("kind", [
|
|
1469
|
+
z8.object({ kind: z8.literal("topic_subscribed"), topic: Slug, ...exclude }).strict(),
|
|
1470
|
+
z8.object({ kind: z8.literal("form_confirmed"), form_id: Id, ...exclude }).strict(),
|
|
1471
|
+
z8.object({ kind: z8.literal("tag_added"), tag: Slug, ...exclude }).strict(),
|
|
1015
1472
|
/** `to` given: only a change that lands on this value fires. Absent: any change of the key. */
|
|
1016
|
-
|
|
1473
|
+
z8.object({ kind: z8.literal("property_changed"), key: ContactPropertyKey, to: JsonValue.optional(), ...exclude }).strict(),
|
|
1017
1474
|
/** `where` filters the event's own data, so only `event:<key>` leaves belong in it. */
|
|
1018
|
-
|
|
1019
|
-
|
|
1475
|
+
z8.object({ kind: z8.literal("event"), name: EventName, where: BoundedAutomationFilter.nullish(), ...exclude }).strict(),
|
|
1476
|
+
z8.object({ kind: z8.literal("manual"), ...exclude }).strict(),
|
|
1020
1477
|
/**
|
|
1021
1478
|
* Every year on a date the contact carries: a birthday, a joining date, a
|
|
1022
1479
|
* renewal. `offset_days` moves it ("three days before"), `at` is the wall
|
|
@@ -1026,8 +1483,8 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1026
1483
|
* A 29 February date fires on 1 March in a year that has no 29 February, so
|
|
1027
1484
|
* nobody is skipped every fourth year.
|
|
1028
1485
|
*/
|
|
1029
|
-
|
|
1030
|
-
kind:
|
|
1486
|
+
z8.object({
|
|
1487
|
+
kind: z8.literal("date_anniversary"),
|
|
1031
1488
|
key: ContactPropertyKey,
|
|
1032
1489
|
offset_days: OffsetDays,
|
|
1033
1490
|
at: TimeOfDay,
|
|
@@ -1039,8 +1496,8 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1039
1496
|
* not ask about a segment or about an event's data: there is no event, and a
|
|
1040
1497
|
* segment is its own trigger kind.
|
|
1041
1498
|
*/
|
|
1042
|
-
|
|
1043
|
-
kind:
|
|
1499
|
+
z8.object({
|
|
1500
|
+
kind: z8.literal("exact_date"),
|
|
1044
1501
|
on: DateOnly,
|
|
1045
1502
|
at: TimeOfDay,
|
|
1046
1503
|
where: BoundedAutomationFilter.nullish(),
|
|
@@ -1059,7 +1516,7 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1059
1516
|
* already exists. Entering those people as well is the publish dialog's
|
|
1060
1517
|
* backfill question, answered by a person rather than by the sweep.
|
|
1061
1518
|
*/
|
|
1062
|
-
|
|
1519
|
+
z8.object({ kind: z8.literal("segment_entered"), segment_id: Id, ...exclude }).strict(),
|
|
1063
1520
|
/**
|
|
1064
1521
|
* S4. When somebody clicks a link in a mailing that has already gone out.
|
|
1065
1522
|
*
|
|
@@ -1069,7 +1526,7 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1069
1526
|
* tracking off there is no click to hear about, so the validator refuses it
|
|
1070
1527
|
* rather than leaving an automation that can never fire.
|
|
1071
1528
|
*/
|
|
1072
|
-
|
|
1529
|
+
z8.object({ kind: z8.literal("link_clicked"), mailing_id: Id, url: z8.string().min(1).max(2048).optional(), ...exclude }).strict()
|
|
1073
1530
|
]);
|
|
1074
1531
|
var AUTOMATION_DATE_TRIGGER_KINDS = ["date_anniversary", "exact_date"];
|
|
1075
1532
|
var MAX_TRIGGERS = 10;
|
|
@@ -1088,34 +1545,45 @@ var AUTOMATION_STEP_KINDS = [
|
|
|
1088
1545
|
"webhook",
|
|
1089
1546
|
"notify"
|
|
1090
1547
|
];
|
|
1091
|
-
var AutomationStepKind =
|
|
1548
|
+
var AutomationStepKind = z8.enum(AUTOMATION_STEP_KINDS);
|
|
1092
1549
|
var next = { next: StepId.nullable() };
|
|
1093
|
-
var Unchosen =
|
|
1094
|
-
var DraftSlug =
|
|
1095
|
-
var DraftPropertyKey =
|
|
1096
|
-
var label = { label:
|
|
1097
|
-
var
|
|
1098
|
-
|
|
1550
|
+
var Unchosen = z8.literal("");
|
|
1551
|
+
var DraftSlug = z8.union([Slug, Unchosen]);
|
|
1552
|
+
var DraftPropertyKey = z8.union([ContactPropertyKey, Unchosen]);
|
|
1553
|
+
var label = { label: z8.string().max(120).nullish() };
|
|
1554
|
+
var EmailStepLanguage = z8.object({
|
|
1555
|
+
subject: z8.string().min(1).max(998),
|
|
1556
|
+
preheader: z8.string().max(500).nullable(),
|
|
1557
|
+
document: TemplateDocument,
|
|
1558
|
+
/** This language's insertions, its texts over the template's structure (every text present). */
|
|
1559
|
+
insertions: Insertions.nullish()
|
|
1560
|
+
}).strict();
|
|
1561
|
+
var EmailStep = z8.object({
|
|
1562
|
+
kind: z8.literal("email"),
|
|
1099
1563
|
...label,
|
|
1100
1564
|
...next,
|
|
1101
1565
|
template_id: Id.nullish(),
|
|
1102
1566
|
document: TemplateDocument.nullish(),
|
|
1103
|
-
/** Empty while the step is being written; `not_chosen` refuses to publish it. */
|
|
1104
|
-
subject:
|
|
1105
|
-
preheader:
|
|
1567
|
+
/** Empty while the step is being written, or when its template has a subject; else `not_chosen` refuses to publish it. */
|
|
1568
|
+
subject: z8.string().max(998),
|
|
1569
|
+
preheader: z8.string().max(500).nullish(),
|
|
1570
|
+
locale: LocaleTag.nullish(),
|
|
1571
|
+
languages: z8.record(LocaleTag, EmailStepLanguage).nullish(),
|
|
1572
|
+
/** The main language's insertions, snapshot from the template when the automation is published. */
|
|
1573
|
+
insertions: Insertions.nullish()
|
|
1106
1574
|
}).strict();
|
|
1107
|
-
var DelayStep =
|
|
1108
|
-
kind:
|
|
1575
|
+
var DelayStep = z8.object({
|
|
1576
|
+
kind: z8.literal("delay"),
|
|
1109
1577
|
...label,
|
|
1110
1578
|
...next,
|
|
1111
|
-
wait:
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1579
|
+
wait: z8.discriminatedUnion("mode", [
|
|
1580
|
+
z8.object({ mode: z8.literal("duration"), seconds: z8.number().int().min(1).max(MAX_DELAY_SECONDS) }).strict(),
|
|
1581
|
+
z8.object({ mode: z8.literal("time_of_day"), at: TimeOfDay }).strict(),
|
|
1582
|
+
z8.object({ mode: z8.literal("weekday"), weekday: Weekday, at: TimeOfDay }).strict(),
|
|
1115
1583
|
/** A fixed day has no next occurrence, so `continue` is not one of its answers. */
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
mode:
|
|
1584
|
+
z8.object({ mode: z8.literal("date"), on: DateOnly, at: TimeOfDay, passed: z8.enum(["skip", "stop"]) }).strict(),
|
|
1585
|
+
z8.object({
|
|
1586
|
+
mode: z8.literal("date_property"),
|
|
1119
1587
|
key: ContactPropertyKey,
|
|
1120
1588
|
offset_days: OffsetDays,
|
|
1121
1589
|
at: TimeOfDay,
|
|
@@ -1123,8 +1591,8 @@ var DelayStep = z7.object({
|
|
|
1123
1591
|
}).strict()
|
|
1124
1592
|
])
|
|
1125
1593
|
}).strict();
|
|
1126
|
-
var ConditionStep =
|
|
1127
|
-
kind:
|
|
1594
|
+
var ConditionStep = z8.object({
|
|
1595
|
+
kind: z8.literal("condition"),
|
|
1128
1596
|
...label,
|
|
1129
1597
|
filter: BoundedAutomationFilter,
|
|
1130
1598
|
yes: StepId.nullable(),
|
|
@@ -1135,49 +1603,49 @@ var ConditionStep = z7.object({
|
|
|
1135
1603
|
* neither half means anything without the other: a window with no answer
|
|
1136
1604
|
* for the end of it would be a silent decision.
|
|
1137
1605
|
*/
|
|
1138
|
-
wait_for:
|
|
1606
|
+
wait_for: z8.object({ up_to_s: z8.number().int().min(1).max(MAX_DELAY_SECONDS), on_timeout: z8.enum(["yes", "no"]) }).strict().nullish()
|
|
1139
1607
|
}).strict();
|
|
1140
|
-
var SplitBranchKey =
|
|
1608
|
+
var SplitBranchKey = z8.string().regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, underscore and hyphen, 1 to 32");
|
|
1141
1609
|
var MIN_SPLIT_BRANCHES = 2;
|
|
1142
1610
|
var MAX_SPLIT_BRANCHES = 4;
|
|
1143
|
-
var SplitStep =
|
|
1144
|
-
kind:
|
|
1611
|
+
var SplitStep = z8.object({
|
|
1612
|
+
kind: z8.literal("split"),
|
|
1145
1613
|
...label,
|
|
1146
|
-
branches:
|
|
1147
|
-
|
|
1614
|
+
branches: z8.array(
|
|
1615
|
+
z8.object({
|
|
1148
1616
|
key: SplitBranchKey,
|
|
1149
1617
|
/** A whole percentage. Zero is allowed: a branch kept in the flow but switched off. */
|
|
1150
|
-
weight:
|
|
1618
|
+
weight: z8.number().int().min(0).max(100),
|
|
1151
1619
|
next: StepId.nullable()
|
|
1152
1620
|
}).strict()
|
|
1153
1621
|
).min(MIN_SPLIT_BRANCHES).max(MAX_SPLIT_BRANCHES).refine((bs) => new Set(bs.map((b) => b.key)).size === bs.length, "each branch needs its own key").refine((bs) => bs.reduce((sum, b) => sum + b.weight, 0) === 100, "the weights have to come to 100")
|
|
1154
1622
|
}).strict();
|
|
1155
|
-
var GotoStep =
|
|
1623
|
+
var GotoStep = z8.object({ kind: z8.literal("goto"), ...label, target: StepId.nullable() }).strict();
|
|
1156
1624
|
var MAX_STEP_VISITS = 20;
|
|
1157
|
-
var WebhookStep =
|
|
1158
|
-
var NotifyRecipients =
|
|
1159
|
-
var NotifyStep =
|
|
1160
|
-
kind:
|
|
1625
|
+
var WebhookStep = z8.object({ kind: z8.literal("webhook"), ...label, ...next, endpoint_id: Id.nullable(), payload: JsonValue.nullish() }).strict();
|
|
1626
|
+
var NotifyRecipients = z8.union([z8.literal("owner"), z8.literal("admins"), z8.object({ member_id: Id }).strict()]);
|
|
1627
|
+
var NotifyStep = z8.object({
|
|
1628
|
+
kind: z8.literal("notify"),
|
|
1161
1629
|
...label,
|
|
1162
1630
|
...next,
|
|
1163
1631
|
to: NotifyRecipients,
|
|
1164
1632
|
/** Empty while the step is being written; `not_chosen` refuses to publish it. */
|
|
1165
|
-
subject:
|
|
1166
|
-
body:
|
|
1633
|
+
subject: z8.string().max(998),
|
|
1634
|
+
body: z8.string().max(1e4)
|
|
1167
1635
|
}).strict();
|
|
1168
|
-
var AutomationStep =
|
|
1636
|
+
var AutomationStep = z8.discriminatedUnion("kind", [
|
|
1169
1637
|
EmailStep,
|
|
1170
1638
|
DelayStep,
|
|
1171
1639
|
ConditionStep,
|
|
1172
|
-
|
|
1173
|
-
|
|
1640
|
+
z8.object({ kind: z8.literal("add_tag"), ...label, ...next, tag: DraftSlug }).strict(),
|
|
1641
|
+
z8.object({ kind: z8.literal("remove_tag"), ...label, ...next, tag: DraftSlug }).strict(),
|
|
1174
1642
|
/** `value` null clears the property, as a contact upsert does. */
|
|
1175
|
-
|
|
1643
|
+
z8.object({ kind: z8.literal("set_property"), ...label, ...next, key: DraftPropertyKey, value: JsonValue }).strict(),
|
|
1176
1644
|
/** Subscribing never lifts a suppression: the service checks that, not the flow. */
|
|
1177
|
-
|
|
1645
|
+
z8.object({ kind: z8.literal("subscribe_topic"), ...label, ...next, topic: DraftSlug }).strict(),
|
|
1178
1646
|
/** `topic` unsubscribes from the automation's own topic, `all` from every topic. */
|
|
1179
|
-
|
|
1180
|
-
|
|
1647
|
+
z8.object({ kind: z8.literal("unsubscribe"), ...label, ...next, scope: z8.enum(["topic", "all"]) }).strict(),
|
|
1648
|
+
z8.object({ kind: z8.literal("exit"), ...label }).strict(),
|
|
1181
1649
|
SplitStep,
|
|
1182
1650
|
GotoStep,
|
|
1183
1651
|
WebhookStep,
|
|
@@ -1201,25 +1669,25 @@ function stepTargets(step) {
|
|
|
1201
1669
|
}
|
|
1202
1670
|
}
|
|
1203
1671
|
var MAX_STEPS = 200;
|
|
1204
|
-
var AutomationFlow =
|
|
1205
|
-
triggers:
|
|
1672
|
+
var AutomationFlow = z8.object({
|
|
1673
|
+
triggers: z8.array(AutomationTrigger).max(MAX_TRIGGERS),
|
|
1206
1674
|
/** The first step. `null` is an empty flow. */
|
|
1207
1675
|
entry: StepId.nullable(),
|
|
1208
|
-
steps:
|
|
1676
|
+
steps: z8.record(StepId, AutomationStep)
|
|
1209
1677
|
}).strict();
|
|
1210
1678
|
var emptyFlow = () => ({ triggers: [], entry: null, steps: {} });
|
|
1211
|
-
var QuietHours =
|
|
1679
|
+
var QuietHours = z8.object({
|
|
1212
1680
|
start: TimeOfDay,
|
|
1213
1681
|
end: TimeOfDay,
|
|
1214
|
-
weekdays:
|
|
1682
|
+
weekdays: z8.array(Weekday).min(1).max(7).nullish()
|
|
1215
1683
|
}).strict();
|
|
1216
1684
|
var RE_ENTRY_MODES = ["off", "after_exit"];
|
|
1217
|
-
var ReEntryMode =
|
|
1685
|
+
var ReEntryMode = z8.enum(RE_ENTRY_MODES);
|
|
1218
1686
|
var MAX_RE_ENTRY_COOLDOWN_SECONDS = 365 * 24 * 60 * 60;
|
|
1219
|
-
var AutomationSettings =
|
|
1687
|
+
var AutomationSettings = z8.object({
|
|
1220
1688
|
re_entry: ReEntryMode,
|
|
1221
1689
|
/** Seconds a person must wait after leaving before entering again. Only with `after_exit`. */
|
|
1222
|
-
re_entry_cooldown_s:
|
|
1690
|
+
re_entry_cooldown_s: z8.number().int().min(0).max(MAX_RE_ENTRY_COOLDOWN_SECONDS).nullish(),
|
|
1223
1691
|
quiet_hours: QuietHours.nullish(),
|
|
1224
1692
|
/** Checked before every step; when it matches, the run ends. */
|
|
1225
1693
|
exit_filter: BoundedAutomationFilter.nullish()
|
|
@@ -1277,18 +1745,18 @@ var AUTOMATION_ISSUE_CODES = [
|
|
|
1277
1745
|
/** A one-off date that is already over everywhere on earth, so it would fire for nobody. */
|
|
1278
1746
|
"date_in_the_past"
|
|
1279
1747
|
];
|
|
1280
|
-
var AutomationIssueCode =
|
|
1281
|
-
var AutomationIssue =
|
|
1748
|
+
var AutomationIssueCode = z8.enum(AUTOMATION_ISSUE_CODES);
|
|
1749
|
+
var AutomationIssue = z8.object({
|
|
1282
1750
|
code: AutomationIssueCode,
|
|
1283
1751
|
/** The step the issue is about, when it is about one. */
|
|
1284
1752
|
step_id: StepId.nullable(),
|
|
1285
1753
|
/** Where inside the document, for the canvas to point at a field. */
|
|
1286
|
-
path:
|
|
1287
|
-
message:
|
|
1754
|
+
path: z8.array(z8.union([z8.string(), z8.number()])),
|
|
1755
|
+
message: z8.string()
|
|
1288
1756
|
});
|
|
1289
|
-
var AutomationValidation =
|
|
1290
|
-
ok:
|
|
1291
|
-
issues:
|
|
1757
|
+
var AutomationValidation = z8.object({
|
|
1758
|
+
ok: z8.boolean(),
|
|
1759
|
+
issues: z8.array(AutomationIssue)
|
|
1292
1760
|
});
|
|
1293
1761
|
function validateAutomation(flow, settings, ctx) {
|
|
1294
1762
|
const issues = [];
|
|
@@ -1408,7 +1876,7 @@ function validateAutomation(flow, settings, ctx) {
|
|
|
1408
1876
|
if (!step.template_id && !step.document) {
|
|
1409
1877
|
add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
|
|
1410
1878
|
}
|
|
1411
|
-
if (step.subject.trim() === "") {
|
|
1879
|
+
if (step.subject.trim() === "" && !(step.template_id && known.templates_with_subject?.has(step.template_id))) {
|
|
1412
1880
|
add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
|
|
1413
1881
|
}
|
|
1414
1882
|
if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
|
|
@@ -1679,7 +2147,7 @@ function cycles(flow) {
|
|
|
1679
2147
|
return out.sort();
|
|
1680
2148
|
}
|
|
1681
2149
|
var AUTOMATION_STATUSES = ["draft", "active", "paused", "archived"];
|
|
1682
|
-
var AutomationStatus =
|
|
2150
|
+
var AutomationStatus = z8.enum(AUTOMATION_STATUSES);
|
|
1683
2151
|
var AUTOMATION_END_REASONS = [
|
|
1684
2152
|
"completed",
|
|
1685
2153
|
"exit_filter",
|
|
@@ -1698,9 +2166,9 @@ var AUTOMATION_END_REASONS = [
|
|
|
1698
2166
|
"ended_by_hand",
|
|
1699
2167
|
"failed"
|
|
1700
2168
|
];
|
|
1701
|
-
var AutomationEndReason =
|
|
2169
|
+
var AutomationEndReason = z8.enum(AUTOMATION_END_REASONS);
|
|
1702
2170
|
var AUTOMATION_WAIT_REASONS = ["delay", "quiet_hours", "plan_limit", "sending", "condition"];
|
|
1703
|
-
var AutomationWaitReason =
|
|
2171
|
+
var AutomationWaitReason = z8.enum(AUTOMATION_WAIT_REASONS);
|
|
1704
2172
|
var AUTOMATION_ENTRY_REFUSALS = [
|
|
1705
2173
|
"already_inside",
|
|
1706
2174
|
"re_entry_off",
|
|
@@ -1711,26 +2179,26 @@ var AUTOMATION_ENTRY_REFUSALS = [
|
|
|
1711
2179
|
"not_active",
|
|
1712
2180
|
"unknown_contact"
|
|
1713
2181
|
];
|
|
1714
|
-
var AutomationEntryRefusal =
|
|
1715
|
-
var AutomationCounts =
|
|
1716
|
-
started:
|
|
1717
|
-
in_progress:
|
|
1718
|
-
completed:
|
|
1719
|
-
exited:
|
|
1720
|
-
failed:
|
|
1721
|
-
});
|
|
1722
|
-
var Automation =
|
|
2182
|
+
var AutomationEntryRefusal = z8.enum(AUTOMATION_ENTRY_REFUSALS);
|
|
2183
|
+
var AutomationCounts = z8.object({
|
|
2184
|
+
started: z8.number().int().min(0),
|
|
2185
|
+
in_progress: z8.number().int().min(0),
|
|
2186
|
+
completed: z8.number().int().min(0),
|
|
2187
|
+
exited: z8.number().int().min(0),
|
|
2188
|
+
failed: z8.number().int().min(0)
|
|
2189
|
+
});
|
|
2190
|
+
var Automation = z8.object({
|
|
1723
2191
|
id: Id,
|
|
1724
|
-
name:
|
|
2192
|
+
name: z8.string().min(1).max(200),
|
|
1725
2193
|
status: AutomationStatus,
|
|
1726
2194
|
/** Every automation sends under one topic; there are no transactional automations. */
|
|
1727
2195
|
topic: Slug,
|
|
1728
2196
|
provider_id: Id.nullable(),
|
|
1729
2197
|
settings: AutomationSettings,
|
|
1730
2198
|
/** The flow people are running through, null while the automation has never been published. */
|
|
1731
|
-
published_version:
|
|
2199
|
+
published_version: z8.number().int().min(1).nullable(),
|
|
1732
2200
|
/** The flow being edited. Increments on every save, like a template's version. */
|
|
1733
|
-
draft_version:
|
|
2201
|
+
draft_version: z8.number().int().min(1),
|
|
1734
2202
|
counts: AutomationCounts,
|
|
1735
2203
|
created_at: Timestamp,
|
|
1736
2204
|
updated_at: Timestamp,
|
|
@@ -1743,16 +2211,16 @@ var AutomationWithFlow = Automation.extend({
|
|
|
1743
2211
|
published_flow: AutomationFlow.nullable()
|
|
1744
2212
|
});
|
|
1745
2213
|
var AutomationSummary = Automation;
|
|
1746
|
-
var AutomationCreate =
|
|
1747
|
-
name:
|
|
2214
|
+
var AutomationCreate = z8.object({
|
|
2215
|
+
name: z8.string().min(1).max(200),
|
|
1748
2216
|
topic: Slug,
|
|
1749
2217
|
provider_id: Id.optional(),
|
|
1750
2218
|
flow: AutomationFlow.optional(),
|
|
1751
2219
|
settings: AutomationSettings.partial().optional()
|
|
1752
2220
|
});
|
|
1753
|
-
var AutomationUpdate =
|
|
1754
|
-
base_version:
|
|
1755
|
-
name:
|
|
2221
|
+
var AutomationUpdate = z8.object({
|
|
2222
|
+
base_version: z8.number().int().min(1),
|
|
2223
|
+
name: z8.string().min(1).max(200).optional(),
|
|
1756
2224
|
topic: Slug.optional(),
|
|
1757
2225
|
provider_id: Id.optional(),
|
|
1758
2226
|
flow: AutomationFlow.optional(),
|
|
@@ -1765,42 +2233,42 @@ var AutomationListQuery = PageQuery.extend({
|
|
|
1765
2233
|
status: AutomationStatus.optional(),
|
|
1766
2234
|
topic: Slug.optional()
|
|
1767
2235
|
});
|
|
1768
|
-
var AutomationPublishRequest =
|
|
2236
|
+
var AutomationPublishRequest = z8.object({
|
|
1769
2237
|
/**
|
|
1770
2238
|
* Answer what this publish would do and change nothing, whatever the flow
|
|
1771
2239
|
* looks like. Without it a publish that has no issues and moves nobody
|
|
1772
2240
|
* simply goes ahead, which is right for an API caller and wrong for a
|
|
1773
2241
|
* dialog that is only showing somebody what they are about to do.
|
|
1774
2242
|
*/
|
|
1775
|
-
dry_run:
|
|
1776
|
-
confirm:
|
|
2243
|
+
dry_run: z8.boolean().optional(),
|
|
2244
|
+
confirm: z8.boolean().optional(),
|
|
1777
2245
|
/** Per removed step: the step its waiting runs move to, or null to end them. */
|
|
1778
|
-
moves:
|
|
2246
|
+
moves: z8.record(StepId, StepId.nullable()).optional(),
|
|
1779
2247
|
/** Also enter everyone who matches a trigger's condition right now. */
|
|
1780
|
-
backfill:
|
|
2248
|
+
backfill: z8.boolean().optional()
|
|
1781
2249
|
}).strict();
|
|
1782
|
-
var AutomationPublishMove =
|
|
2250
|
+
var AutomationPublishMove = z8.object({
|
|
1783
2251
|
step_id: StepId,
|
|
1784
2252
|
/** Why the step's people have to move: it is gone, or its delay changed. */
|
|
1785
|
-
change:
|
|
1786
|
-
waiting:
|
|
2253
|
+
change: z8.enum(["removed", "delay_changed", "kind_changed"]),
|
|
2254
|
+
waiting: z8.number().int().min(0),
|
|
1787
2255
|
/** Where they go: a step id, or null to end their runs with `step_removed`. */
|
|
1788
2256
|
destination: StepId.nullable()
|
|
1789
2257
|
});
|
|
1790
|
-
var AutomationPublishResult =
|
|
1791
|
-
published:
|
|
2258
|
+
var AutomationPublishResult = z8.object({
|
|
2259
|
+
published: z8.boolean(),
|
|
1792
2260
|
automation: Automation,
|
|
1793
2261
|
/** Empty when nothing has to move; non-empty and `published` false asks for a confirmation. */
|
|
1794
|
-
moves:
|
|
2262
|
+
moves: z8.array(AutomationPublishMove),
|
|
1795
2263
|
/** How many people the backfill entered, or would enter when it is not confirmed. */
|
|
1796
|
-
backfill:
|
|
1797
|
-
issues:
|
|
1798
|
-
});
|
|
1799
|
-
var AutomationDuplicateRequest =
|
|
1800
|
-
var AutomationEnrollRequest =
|
|
1801
|
-
contact_ids:
|
|
1802
|
-
external_ids:
|
|
1803
|
-
emails:
|
|
2264
|
+
backfill: z8.number().int().min(0),
|
|
2265
|
+
issues: z8.array(AutomationIssue)
|
|
2266
|
+
});
|
|
2267
|
+
var AutomationDuplicateRequest = z8.object({ name: z8.string().min(1).max(200).optional() }).strict();
|
|
2268
|
+
var AutomationEnrollRequest = z8.object({
|
|
2269
|
+
contact_ids: z8.array(Id).min(1).max(1e3).optional(),
|
|
2270
|
+
external_ids: z8.array(z8.string().min(1).max(255)).min(1).max(1e3).optional(),
|
|
2271
|
+
emails: z8.array(Email).min(1).max(1e3).optional(),
|
|
1804
2272
|
segment_id: Id.optional(),
|
|
1805
2273
|
/** Where to start them; the flow's entry step by default. */
|
|
1806
2274
|
step_id: StepId.optional()
|
|
@@ -1808,34 +2276,34 @@ var AutomationEnrollRequest = z7.object({
|
|
|
1808
2276
|
(v) => [v.contact_ids, v.external_ids, v.emails, v.segment_id].filter((x) => x !== void 0).length === 1,
|
|
1809
2277
|
"name the people exactly one way: contact_ids, external_ids, emails or segment_id"
|
|
1810
2278
|
);
|
|
1811
|
-
var AutomationEnrollment =
|
|
2279
|
+
var AutomationEnrollment = z8.object({
|
|
1812
2280
|
contact_id: Id.nullable(),
|
|
1813
2281
|
email: Email.nullable(),
|
|
1814
|
-
external_id:
|
|
1815
|
-
entered:
|
|
2282
|
+
external_id: z8.string().nullable(),
|
|
2283
|
+
entered: z8.boolean(),
|
|
1816
2284
|
run_id: Id.nullable(),
|
|
1817
2285
|
reason: AutomationEntryRefusal.nullable()
|
|
1818
2286
|
});
|
|
1819
|
-
var AutomationEnrollResult =
|
|
1820
|
-
entered:
|
|
1821
|
-
refused:
|
|
1822
|
-
results:
|
|
2287
|
+
var AutomationEnrollResult = z8.object({
|
|
2288
|
+
entered: z8.number().int().min(0),
|
|
2289
|
+
refused: z8.number().int().min(0),
|
|
2290
|
+
results: z8.array(AutomationEnrollment)
|
|
1823
2291
|
});
|
|
1824
|
-
var AutomationRunStatus =
|
|
1825
|
-
var AutomationRun =
|
|
2292
|
+
var AutomationRunStatus = z8.enum(["active", "waiting", "completed", "exited", "failed"]);
|
|
2293
|
+
var AutomationRun = z8.object({
|
|
1826
2294
|
id: Id,
|
|
1827
2295
|
automation_id: Id,
|
|
1828
2296
|
contact_id: Id,
|
|
1829
2297
|
email: Email,
|
|
1830
|
-
external_id:
|
|
1831
|
-
version:
|
|
2298
|
+
external_id: z8.string().nullable(),
|
|
2299
|
+
version: z8.number().int().min(1),
|
|
1832
2300
|
status: AutomationRunStatus,
|
|
1833
2301
|
current_step_id: StepId.nullable(),
|
|
1834
2302
|
next_action_at: Timestamp.nullable(),
|
|
1835
2303
|
wait_reason: AutomationWaitReason.nullable(),
|
|
1836
|
-
end_reason:
|
|
2304
|
+
end_reason: z8.string().nullable(),
|
|
1837
2305
|
/** What went wrong the last time this run tried to take its step, null otherwise. */
|
|
1838
|
-
last_error:
|
|
2306
|
+
last_error: z8.string().nullable(),
|
|
1839
2307
|
entered_at: Timestamp,
|
|
1840
2308
|
ended_at: Timestamp.nullable()
|
|
1841
2309
|
});
|
|
@@ -1845,13 +2313,13 @@ var AutomationRunListQuery = PageQuery.extend({
|
|
|
1845
2313
|
contact_id: Id.optional()
|
|
1846
2314
|
});
|
|
1847
2315
|
var ContactAutomationRun = AutomationRun.extend({
|
|
1848
|
-
automation:
|
|
2316
|
+
automation: z8.object({ id: Id, name: z8.string(), status: AutomationStatus })
|
|
1849
2317
|
});
|
|
1850
2318
|
var ContactAutomationListQuery = PageQuery.extend({ status: AutomationRunStatus.optional() });
|
|
1851
|
-
var AutomationStepExecution =
|
|
2319
|
+
var AutomationStepExecution = z8.object({
|
|
1852
2320
|
step_id: StepId,
|
|
1853
2321
|
step_kind: AutomationStepKind,
|
|
1854
|
-
visit:
|
|
2322
|
+
visit: z8.number().int().min(1),
|
|
1855
2323
|
started_at: Timestamp,
|
|
1856
2324
|
finished_at: Timestamp.nullable(),
|
|
1857
2325
|
/**
|
|
@@ -1863,48 +2331,48 @@ var AutomationStepExecution = z7.object({
|
|
|
1863
2331
|
*/
|
|
1864
2332
|
outcome: Properties
|
|
1865
2333
|
});
|
|
1866
|
-
var AutomationJourney =
|
|
2334
|
+
var AutomationJourney = z8.object({
|
|
1867
2335
|
run: AutomationRun,
|
|
1868
|
-
steps:
|
|
2336
|
+
steps: z8.array(AutomationStepExecution)
|
|
1869
2337
|
});
|
|
1870
|
-
var AutomationStepReport =
|
|
2338
|
+
var AutomationStepReport = z8.object({
|
|
1871
2339
|
step_id: StepId,
|
|
1872
2340
|
step_kind: AutomationStepKind,
|
|
1873
|
-
reached:
|
|
1874
|
-
waiting:
|
|
1875
|
-
passed:
|
|
2341
|
+
reached: z8.number().int().min(0),
|
|
2342
|
+
waiting: z8.number().int().min(0),
|
|
2343
|
+
passed: z8.number().int().min(0),
|
|
1876
2344
|
/** Per branch, for a condition: how many took `yes` and how many `no`. */
|
|
1877
|
-
branches:
|
|
2345
|
+
branches: z8.record(z8.number().int().min(0)),
|
|
1878
2346
|
/** The step mailing's id, for an email step of the published version. */
|
|
1879
2347
|
mailing_id: Id.nullable()
|
|
1880
2348
|
});
|
|
1881
|
-
var AutomationReport =
|
|
2349
|
+
var AutomationReport = z8.object({
|
|
1882
2350
|
counts: AutomationCounts,
|
|
1883
2351
|
/** Why the people who left did: `AutomationEndReason` to a count. */
|
|
1884
|
-
end_reasons:
|
|
2352
|
+
end_reasons: z8.record(z8.number().int().min(0)),
|
|
1885
2353
|
/**
|
|
1886
2354
|
* Why people a trigger named did not enter: `AutomationEntryRefusal` to a
|
|
1887
2355
|
* count. Without it a pause, an exclusion or a missing subscription would
|
|
1888
2356
|
* turn people away invisibly.
|
|
1889
2357
|
*/
|
|
1890
|
-
refusals:
|
|
1891
|
-
steps:
|
|
1892
|
-
});
|
|
1893
|
-
var AutomationExitRunRequest =
|
|
1894
|
-
var AutomationRunParams =
|
|
1895
|
-
var AutomationStepParams =
|
|
1896
|
-
var AutomationTestEmailRequest =
|
|
1897
|
-
var AutomationValidateRequest =
|
|
1898
|
-
var EventCreate =
|
|
2358
|
+
refusals: z8.record(z8.number().int().min(0)),
|
|
2359
|
+
steps: z8.array(AutomationStepReport)
|
|
2360
|
+
});
|
|
2361
|
+
var AutomationExitRunRequest = z8.object({ reason: z8.string().max(200).optional() }).strict();
|
|
2362
|
+
var AutomationRunParams = z8.object({ id: Id, run_id: Id });
|
|
2363
|
+
var AutomationStepParams = z8.object({ id: Id, step_id: StepId });
|
|
2364
|
+
var AutomationTestEmailRequest = z8.object({ to: Email, merge: Properties.optional(), locale: LocaleTag.optional() }).strict();
|
|
2365
|
+
var AutomationValidateRequest = z8.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
|
|
2366
|
+
var EventCreate = z8.object({
|
|
1899
2367
|
name: EventName,
|
|
1900
|
-
contact:
|
|
2368
|
+
contact: z8.object({ external_id: z8.string().min(1).max(255).optional(), email: Email.optional(), id: Id.optional() }).refine((c) => [c.external_id, c.email, c.id].filter((x) => x !== void 0).length === 1, "name the contact exactly one way"),
|
|
1901
2369
|
data: Properties.optional(),
|
|
1902
2370
|
occurred_at: Timestamp.optional(),
|
|
1903
|
-
id:
|
|
2371
|
+
id: z8.string().min(1).max(128).optional()
|
|
1904
2372
|
}).strict();
|
|
1905
|
-
var Event =
|
|
2373
|
+
var Event = z8.object({
|
|
1906
2374
|
id: Id,
|
|
1907
|
-
client_event_id:
|
|
2375
|
+
client_event_id: z8.string().nullable(),
|
|
1908
2376
|
name: EventName,
|
|
1909
2377
|
contact_id: Id,
|
|
1910
2378
|
data: Properties,
|
|
@@ -1915,59 +2383,42 @@ var Event = z7.object({
|
|
|
1915
2383
|
* hand. Each is then admitted or refused by the runtime on its own rules
|
|
1916
2384
|
* (consent, exclusions, re-entry), which `automations.report` counts.
|
|
1917
2385
|
*/
|
|
1918
|
-
entered:
|
|
2386
|
+
entered: z8.number().int().min(0)
|
|
1919
2387
|
});
|
|
1920
2388
|
var EventListQuery = PageQuery.extend({
|
|
1921
2389
|
name: EventName.optional(),
|
|
1922
2390
|
contact_id: Id.optional()
|
|
1923
2391
|
});
|
|
1924
2392
|
|
|
1925
|
-
// src/unsubscribe.ts
|
|
1926
|
-
var UNSUBSCRIBE_PATH_PREFIX = "/u/";
|
|
1927
|
-
var LIST_UNSUBSCRIBE_POST_VALUE = "List-Unsubscribe=One-Click";
|
|
1928
|
-
var RESERVED_MERGE_FIELDS = {
|
|
1929
|
-
/** From the recipient's merge values, else the contact. Supports a fallback. */
|
|
1930
|
-
first_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
1931
|
-
last_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
1932
|
-
email: { source: "contact", fallback: false, required_for_broadcast: false },
|
|
1933
|
-
/** The hosted unsubscribe page for this recipient. A broadcast without it is refused. */
|
|
1934
|
-
unsubscribe_url: { source: "service", fallback: false, required_for_broadcast: true }
|
|
1935
|
-
};
|
|
1936
|
-
var MERGE_FIELD_PATTERN = /\{\{\s*([a-z][a-z0-9_]*)\s*(?:\|([^}]*))?\}\}/g;
|
|
1937
|
-
function findMergeFields(html) {
|
|
1938
|
-
const uses = [];
|
|
1939
|
-
for (const match of html.matchAll(MERGE_FIELD_PATTERN)) {
|
|
1940
|
-
uses.push({ name: match[1], fallback: match[2] === void 0 ? null : match[2].trim() });
|
|
1941
|
-
}
|
|
1942
|
-
return uses;
|
|
1943
|
-
}
|
|
1944
|
-
function missingRequiredMergeFields(html) {
|
|
1945
|
-
const used = new Set(findMergeFields(html).map((u) => u.name));
|
|
1946
|
-
return Object.keys(RESERVED_MERGE_FIELDS).filter(
|
|
1947
|
-
(name) => RESERVED_MERGE_FIELDS[name].required_for_broadcast && !used.has(name)
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
2393
|
// src/workspace.ts
|
|
1952
|
-
var CompanyId =
|
|
2394
|
+
var CompanyId = z9.string().min(1).max(64);
|
|
1953
2395
|
var ASSET_POLICIES = ["any", "service_only"];
|
|
1954
|
-
var AssetPolicy =
|
|
2396
|
+
var AssetPolicy = z9.enum(ASSET_POLICIES);
|
|
1955
2397
|
var MAX_ALLOWED_ASSET_HOSTS = 20;
|
|
1956
|
-
var AssetHost =
|
|
2398
|
+
var AssetHost = z9.string().trim().toLowerCase().min(1).max(253).regex(/^(?=.{1,253}(?::\d{1,5})?$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+(:\d{1,5})?$/, "a host name such as example.com");
|
|
1957
2399
|
var MAX_MERGE_DEFAULTS = 50;
|
|
1958
2400
|
var MAX_MERGE_DEFAULT_LENGTH = 2e3;
|
|
1959
|
-
var MergeFieldName =
|
|
1960
|
-
var MergeDefaultValue =
|
|
1961
|
-
var MergeDefaults =
|
|
2401
|
+
var MergeFieldName = z9.string().regex(/^[a-z][a-z0-9_]*$/, "a merge-field name: lowercase letters, digits and underscores, starting with a letter").max(64);
|
|
2402
|
+
var MergeDefaultValue = z9.union([z9.string().max(MAX_MERGE_DEFAULT_LENGTH), z9.number().finite(), z9.boolean()]);
|
|
2403
|
+
var MergeDefaults = z9.record(MergeFieldName, MergeDefaultValue).refine((m) => Object.keys(m).length <= MAX_MERGE_DEFAULTS, `at most ${MAX_MERGE_DEFAULTS} merge defaults`).refine((m) => !Object.keys(m).some((k) => k in RESERVED_MERGE_FIELDS), {
|
|
1962
2404
|
message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
|
|
1963
2405
|
});
|
|
1964
|
-
var WorkspaceSettings =
|
|
1965
|
-
/**
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2406
|
+
var WorkspaceSettings = z9.object({
|
|
2407
|
+
/**
|
|
2408
|
+
* The workspace's main language (a {@link LocaleTag}, e.g. "de"): the main
|
|
2409
|
+
* language of a new template, and of the hosted pages when nothing else
|
|
2410
|
+
* decides.
|
|
2411
|
+
*/
|
|
2412
|
+
default_locale: LocaleTag,
|
|
2413
|
+
/**
|
|
2414
|
+
* The languages the workspace writes in, including `default_locale`, none
|
|
2415
|
+
* twice (compared without regard to casing). A template can have a version in
|
|
2416
|
+
* each; the hosted pages are offered in those of them they support (English,
|
|
2417
|
+
* German, French, Italian, Spanish). The service stores canonical casing.
|
|
2418
|
+
*/
|
|
2419
|
+
locales: z9.array(LocaleTag).min(1).max(20).refine((l) => new Set(l.map((t) => t.toLowerCase())).size === l.length, "a language appears twice"),
|
|
1969
2420
|
/** Open and click tracking. Off by default, opt-in per workspace. */
|
|
1970
|
-
tracking_enabled:
|
|
2421
|
+
tracking_enabled: z9.boolean(),
|
|
1971
2422
|
/** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
|
|
1972
2423
|
asset_policy: AssetPolicy,
|
|
1973
2424
|
/**
|
|
@@ -1975,7 +2426,7 @@ var WorkspaceSettings = z8.object({
|
|
|
1975
2426
|
* the workspace's website, typically. Empty by default. Ignored under `any`,
|
|
1976
2427
|
* which allows everything already.
|
|
1977
2428
|
*/
|
|
1978
|
-
allowed_asset_hosts:
|
|
2429
|
+
allowed_asset_hosts: z9.array(AssetHost).max(MAX_ALLOWED_ASSET_HOSTS),
|
|
1979
2430
|
/** Merge values every mail of the workspace can use (see `MergeDefaults`). Empty by default. */
|
|
1980
2431
|
merge_defaults: MergeDefaults,
|
|
1981
2432
|
/**
|
|
@@ -1984,10 +2435,10 @@ var WorkspaceSettings = z8.object({
|
|
|
1984
2435
|
*/
|
|
1985
2436
|
default_timezone: TimezoneName.nullable()
|
|
1986
2437
|
});
|
|
1987
|
-
var Workspace =
|
|
2438
|
+
var Workspace = z9.object({
|
|
1988
2439
|
id: Id,
|
|
1989
2440
|
slug: Slug,
|
|
1990
|
-
name:
|
|
2441
|
+
name: z9.string().min(1).max(120),
|
|
1991
2442
|
settings: WorkspaceSettings,
|
|
1992
2443
|
/**
|
|
1993
2444
|
* The auth-brain company (tenant) the workspace was created for, or null for
|
|
@@ -1998,13 +2449,13 @@ var Workspace = z8.object({
|
|
|
1998
2449
|
created_at: Timestamp,
|
|
1999
2450
|
updated_at: Timestamp
|
|
2000
2451
|
});
|
|
2001
|
-
var WorkspaceCreate =
|
|
2452
|
+
var WorkspaceCreate = z9.object({
|
|
2002
2453
|
slug: Slug,
|
|
2003
|
-
name:
|
|
2454
|
+
name: z9.string().min(1).max(120),
|
|
2004
2455
|
settings: WorkspaceSettings.partial().optional(),
|
|
2005
|
-
owner:
|
|
2456
|
+
owner: z9.object({
|
|
2006
2457
|
email: Email,
|
|
2007
|
-
name:
|
|
2458
|
+
name: z9.string().max(200).nullable().optional()
|
|
2008
2459
|
}),
|
|
2009
2460
|
/**
|
|
2010
2461
|
* The signed-in person's active auth-brain company. The dashboard always
|
|
@@ -2014,49 +2465,49 @@ var WorkspaceCreate = z8.object({
|
|
|
2014
2465
|
*/
|
|
2015
2466
|
company_id: CompanyId.optional()
|
|
2016
2467
|
});
|
|
2017
|
-
var WorkspaceUpdate =
|
|
2018
|
-
name:
|
|
2468
|
+
var WorkspaceUpdate = z9.object({
|
|
2469
|
+
name: z9.string().min(1).max(120),
|
|
2019
2470
|
settings: WorkspaceSettings.partial()
|
|
2020
2471
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2021
|
-
var WorkspaceMove =
|
|
2472
|
+
var WorkspaceMove = z9.object({ company_id: CompanyId });
|
|
2022
2473
|
var MEMBER_ROLES = ["owner", "admin", "editor", "viewer"];
|
|
2023
|
-
var MemberRole =
|
|
2474
|
+
var MemberRole = z9.enum(MEMBER_ROLES);
|
|
2024
2475
|
var WorkspaceMembership = Workspace.extend({ role: MemberRole });
|
|
2025
|
-
var Member =
|
|
2476
|
+
var Member = z9.object({
|
|
2026
2477
|
id: Id,
|
|
2027
2478
|
/** The person's auth-brain subject. */
|
|
2028
|
-
subject:
|
|
2479
|
+
subject: z9.string().min(1).max(255),
|
|
2029
2480
|
email: Email,
|
|
2030
|
-
name:
|
|
2481
|
+
name: z9.string().max(200).nullable(),
|
|
2031
2482
|
role: MemberRole,
|
|
2032
2483
|
created_at: Timestamp
|
|
2033
2484
|
});
|
|
2034
|
-
var MemberCreate =
|
|
2035
|
-
subject:
|
|
2485
|
+
var MemberCreate = z9.object({
|
|
2486
|
+
subject: z9.string().min(1).max(255),
|
|
2036
2487
|
email: Email,
|
|
2037
|
-
name:
|
|
2488
|
+
name: z9.string().max(200).nullable().optional(),
|
|
2038
2489
|
role: MemberRole
|
|
2039
2490
|
});
|
|
2040
|
-
var MemberUpdate =
|
|
2491
|
+
var MemberUpdate = z9.object({ role: MemberRole });
|
|
2041
2492
|
var API_KEY_SCOPES = ["full", "read", "send"];
|
|
2042
|
-
var ApiKeyScope =
|
|
2043
|
-
var ApiKey =
|
|
2493
|
+
var ApiKeyScope = z9.enum(API_KEY_SCOPES);
|
|
2494
|
+
var ApiKey = z9.object({
|
|
2044
2495
|
id: Id,
|
|
2045
|
-
name:
|
|
2496
|
+
name: z9.string().min(1).max(120),
|
|
2046
2497
|
/** The first characters of the key, for recognising it in a list. */
|
|
2047
|
-
prefix:
|
|
2498
|
+
prefix: z9.string().min(4).max(32),
|
|
2048
2499
|
scope: ApiKeyScope,
|
|
2049
2500
|
last_used_at: Timestamp.nullable(),
|
|
2050
2501
|
revoked_at: Timestamp.nullable(),
|
|
2051
2502
|
created_at: Timestamp
|
|
2052
2503
|
});
|
|
2053
|
-
var ApiKeyCreate =
|
|
2054
|
-
name:
|
|
2504
|
+
var ApiKeyCreate = z9.object({
|
|
2505
|
+
name: z9.string().min(1).max(120),
|
|
2055
2506
|
/** Omitted means `full`. */
|
|
2056
2507
|
scope: ApiKeyScope.optional()
|
|
2057
2508
|
});
|
|
2058
|
-
var ApiKeyCreated =
|
|
2059
|
-
key:
|
|
2509
|
+
var ApiKeyCreated = z9.object({
|
|
2510
|
+
key: z9.string().min(20),
|
|
2060
2511
|
api_key: ApiKey
|
|
2061
2512
|
});
|
|
2062
2513
|
var AUDIT_ACTIONS = [
|
|
@@ -2144,29 +2595,29 @@ var AUDIT_ACTIONS = [
|
|
|
2144
2595
|
"automation.run_ended",
|
|
2145
2596
|
"event.recorded"
|
|
2146
2597
|
];
|
|
2147
|
-
var AuditAction =
|
|
2148
|
-
var AuditActor =
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
type:
|
|
2598
|
+
var AuditAction = z9.enum(AUDIT_ACTIONS);
|
|
2599
|
+
var AuditActor = z9.discriminatedUnion("type", [
|
|
2600
|
+
z9.object({ type: z9.literal("member"), member_id: Id, subject: z9.string().min(1) }),
|
|
2601
|
+
z9.object({
|
|
2602
|
+
type: z9.literal("api_key"),
|
|
2152
2603
|
api_key_id: Id,
|
|
2153
2604
|
/**
|
|
2154
2605
|
* Who the client's application said it was acting for, from
|
|
2155
2606
|
* `ON_BEHALF_OF_HEADER`: reported by that application, never verified.
|
|
2156
2607
|
* Absent when the call carried no label.
|
|
2157
2608
|
*/
|
|
2158
|
-
on_behalf_of:
|
|
2609
|
+
on_behalf_of: z9.string().min(1).max(ON_BEHALF_OF_MAX_LENGTH).optional()
|
|
2159
2610
|
}),
|
|
2160
2611
|
/** The service itself (the worker, a bounce, the hosted unsubscribe page). */
|
|
2161
|
-
|
|
2612
|
+
z9.object({ type: z9.literal("system"), reason: z9.string().min(1).max(200) })
|
|
2162
2613
|
]);
|
|
2163
|
-
var AuditEntry =
|
|
2614
|
+
var AuditEntry = z9.object({
|
|
2164
2615
|
id: Id,
|
|
2165
2616
|
action: AuditAction,
|
|
2166
2617
|
actor: AuditActor,
|
|
2167
|
-
target_type:
|
|
2618
|
+
target_type: z9.string().min(1).max(64),
|
|
2168
2619
|
target_id: Id.nullable(),
|
|
2169
|
-
details:
|
|
2620
|
+
details: z9.record(z9.unknown()),
|
|
2170
2621
|
created_at: Timestamp
|
|
2171
2622
|
});
|
|
2172
2623
|
var AuditQuery = PageQuery.extend({
|
|
@@ -2175,12 +2626,12 @@ var AuditQuery = PageQuery.extend({
|
|
|
2175
2626
|
});
|
|
2176
2627
|
|
|
2177
2628
|
// src/invites.ts
|
|
2178
|
-
import { z as
|
|
2629
|
+
import { z as z10 } from "zod";
|
|
2179
2630
|
var INVITE_STATUSES = ["pending", "accepted", "revoked", "expired"];
|
|
2180
|
-
var InviteStatus =
|
|
2631
|
+
var InviteStatus = z10.enum(INVITE_STATUSES);
|
|
2181
2632
|
var DEFAULT_INVITE_TTL_DAYS = 7;
|
|
2182
2633
|
var MAX_INVITE_TTL_DAYS = 30;
|
|
2183
|
-
var Invite =
|
|
2634
|
+
var Invite = z10.object({
|
|
2184
2635
|
id: Id,
|
|
2185
2636
|
/** Stored lowercased; only a person signed in with this address can accept. */
|
|
2186
2637
|
email: Email,
|
|
@@ -2188,43 +2639,565 @@ var Invite = z9.object({
|
|
|
2188
2639
|
/** Derived from the timestamps below, never stored. */
|
|
2189
2640
|
status: InviteStatus,
|
|
2190
2641
|
/** The member who invited, while they are still one (null after they left). */
|
|
2191
|
-
invited_by:
|
|
2642
|
+
invited_by: z10.object({ member_id: Id.nullable(), email: Email.nullable() }),
|
|
2192
2643
|
expires_at: Timestamp,
|
|
2193
2644
|
accepted_at: Timestamp.nullable(),
|
|
2194
2645
|
revoked_at: Timestamp.nullable(),
|
|
2195
2646
|
created_at: Timestamp
|
|
2196
2647
|
});
|
|
2197
|
-
var InviteCreate =
|
|
2648
|
+
var InviteCreate = z10.object({
|
|
2198
2649
|
email: Email,
|
|
2199
2650
|
role: MemberRole,
|
|
2200
|
-
expires_in_days:
|
|
2651
|
+
expires_in_days: z10.number().int().min(1).max(MAX_INVITE_TTL_DAYS).optional()
|
|
2201
2652
|
});
|
|
2202
|
-
var InviteCreated =
|
|
2653
|
+
var InviteCreated = z10.object({
|
|
2203
2654
|
invite: Invite,
|
|
2204
|
-
token:
|
|
2655
|
+
token: z10.string().min(32).max(128)
|
|
2205
2656
|
});
|
|
2206
2657
|
var InviteListQuery = PageQuery.extend({
|
|
2207
2658
|
status: InviteStatus.optional()
|
|
2208
2659
|
});
|
|
2209
|
-
var InviteAccept =
|
|
2210
|
-
token:
|
|
2660
|
+
var InviteAccept = z10.object({
|
|
2661
|
+
token: z10.string().min(32).max(128),
|
|
2211
2662
|
email: Email,
|
|
2212
|
-
name:
|
|
2663
|
+
name: z10.string().max(200).nullable().optional()
|
|
2213
2664
|
});
|
|
2214
|
-
var InviteAccepted =
|
|
2665
|
+
var InviteAccepted = z10.object({
|
|
2215
2666
|
workspace: WorkspaceMembership,
|
|
2216
|
-
already_member:
|
|
2667
|
+
already_member: z10.boolean()
|
|
2217
2668
|
});
|
|
2218
2669
|
var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
|
|
2219
2670
|
|
|
2671
|
+
// src/template-languages.ts
|
|
2672
|
+
function languageTabs(mainLocale, versions, workspaceLocales) {
|
|
2673
|
+
const main = canonicalLocale(mainLocale) ?? mainLocale;
|
|
2674
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
2675
|
+
for (const [tag, status] of Object.entries(versions)) {
|
|
2676
|
+
const canonical = canonicalLocale(tag) ?? tag;
|
|
2677
|
+
if (canonical !== main) statuses.set(canonical, status);
|
|
2678
|
+
}
|
|
2679
|
+
const tabs = [{ locale: main, kind: "main", status: "ready" }];
|
|
2680
|
+
const seen = /* @__PURE__ */ new Set([main]);
|
|
2681
|
+
for (const tag of workspaceLocales) {
|
|
2682
|
+
const canonical = canonicalLocale(tag) ?? tag;
|
|
2683
|
+
if (seen.has(canonical)) continue;
|
|
2684
|
+
seen.add(canonical);
|
|
2685
|
+
tabs.push({ locale: canonical, kind: "translation", status: statuses.get(canonical) ?? "missing" });
|
|
2686
|
+
}
|
|
2687
|
+
const kept = [...statuses.keys()].filter((tag) => !seen.has(tag)).sort();
|
|
2688
|
+
for (const tag of kept) tabs.push({ locale: tag, kind: "kept", status: statuses.get(tag) });
|
|
2689
|
+
return tabs;
|
|
2690
|
+
}
|
|
2691
|
+
function languageName(tag) {
|
|
2692
|
+
const canonical = canonicalLocale(tag) ?? tag;
|
|
2693
|
+
try {
|
|
2694
|
+
const names = new Intl.DisplayNames(["en"], { type: "language", fallback: "none" });
|
|
2695
|
+
const name = names.of(canonical);
|
|
2696
|
+
return name && name !== canonical ? name : canonical;
|
|
2697
|
+
} catch {
|
|
2698
|
+
return canonical;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function storedTemplateLine(value) {
|
|
2702
|
+
if (value === null || value === void 0) return null;
|
|
2703
|
+
const trimmed = value.trim();
|
|
2704
|
+
return trimmed === "" ? null : trimmed;
|
|
2705
|
+
}
|
|
2706
|
+
function buildTemplatePatch(args) {
|
|
2707
|
+
const { loaded, main, documentChanged, translations, changed, removed } = args;
|
|
2708
|
+
const out = {};
|
|
2709
|
+
if (args.insertions !== void 0 && !sameJson(args.insertions, loaded.insertions ?? {})) out.insertions = args.insertions;
|
|
2710
|
+
const subject = storedTemplateLine(main.subject);
|
|
2711
|
+
if (subject !== storedTemplateLine(loaded.subject)) out.subject = subject;
|
|
2712
|
+
const preheader = storedTemplateLine(main.preheader);
|
|
2713
|
+
if (preheader !== storedTemplateLine(loaded.preheader)) out.preheader = preheader;
|
|
2714
|
+
if (documentChanged) out.document = main.document;
|
|
2715
|
+
const patch = {};
|
|
2716
|
+
for (const tag of changed) {
|
|
2717
|
+
const version = translations[tag];
|
|
2718
|
+
if (version && !removed.has(tag)) {
|
|
2719
|
+
patch[tag] = {
|
|
2720
|
+
subject: storedTemplateLine(version.subject),
|
|
2721
|
+
preheader: storedTemplateLine(version.preheader),
|
|
2722
|
+
document: version.document,
|
|
2723
|
+
status: version.status,
|
|
2724
|
+
...version.insertions !== void 0 ? { insertions: version.insertions } : {}
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
for (const tag of removed) {
|
|
2729
|
+
if (Object.prototype.hasOwnProperty.call(loaded.translations, tag)) patch[tag] = null;
|
|
2730
|
+
}
|
|
2731
|
+
if (Object.keys(patch).length > 0) out.translations = patch;
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2734
|
+
function sameJson(a, b) {
|
|
2735
|
+
const norm = (v) => Array.isArray(v) ? v.map(norm) : v !== null && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, norm(v[k])])) : v;
|
|
2736
|
+
return JSON.stringify(norm(a)) === JSON.stringify(norm(b));
|
|
2737
|
+
}
|
|
2738
|
+
function restorePatch(old, current, workspaceLocales) {
|
|
2739
|
+
const oldMain = canonicalLocale(old.locale) ?? old.locale;
|
|
2740
|
+
const listed = workspaceLocales ? new Set(workspaceLocales.map((t) => canonicalLocale(t) ?? t)) : null;
|
|
2741
|
+
const patch = {};
|
|
2742
|
+
const skipped = [];
|
|
2743
|
+
const untouched = /* @__PURE__ */ new Set();
|
|
2744
|
+
for (const [tag, version] of Object.entries(old.translations)) {
|
|
2745
|
+
const canonical = canonicalLocale(tag) ?? tag;
|
|
2746
|
+
if (canonical === oldMain) continue;
|
|
2747
|
+
if (listed && !listed.has(canonical)) {
|
|
2748
|
+
skipped.push(canonical);
|
|
2749
|
+
untouched.add(canonical);
|
|
2750
|
+
continue;
|
|
2751
|
+
}
|
|
2752
|
+
patch[canonical] = { ...version, insertions: version.insertions ?? {} };
|
|
2753
|
+
}
|
|
2754
|
+
for (const tag of Object.keys(current.translations)) {
|
|
2755
|
+
const canonical = canonicalLocale(tag) ?? tag;
|
|
2756
|
+
if (!(canonical in patch) && !untouched.has(canonical)) patch[canonical] = null;
|
|
2757
|
+
if (canonical === oldMain) patch[canonical] = null;
|
|
2758
|
+
}
|
|
2759
|
+
const update = {
|
|
2760
|
+
...oldMain !== (canonicalLocale(current.locale) ?? current.locale) ? { locale: oldMain } : {},
|
|
2761
|
+
subject: old.subject,
|
|
2762
|
+
preheader: old.preheader,
|
|
2763
|
+
document: old.document,
|
|
2764
|
+
insertions: old.insertions ?? {},
|
|
2765
|
+
...Object.keys(patch).length > 0 ? { translations: patch } : {}
|
|
2766
|
+
};
|
|
2767
|
+
return { update, skipped: skipped.sort() };
|
|
2768
|
+
}
|
|
2769
|
+
function draftsFromTemplate(template) {
|
|
2770
|
+
const insertions = template.insertions ?? {};
|
|
2771
|
+
const translations = {};
|
|
2772
|
+
for (const [tag, version] of Object.entries(template.translations)) {
|
|
2773
|
+
translations[canonicalLocale(tag) ?? tag] = { ...version, insertions: alignTranslationInsertions(insertions, version.insertions) };
|
|
2774
|
+
}
|
|
2775
|
+
return {
|
|
2776
|
+
main: { subject: template.subject ?? "", preheader: template.preheader ?? "", document: template.document },
|
|
2777
|
+
insertions,
|
|
2778
|
+
translations,
|
|
2779
|
+
mainDocumentChanged: false,
|
|
2780
|
+
changed: [],
|
|
2781
|
+
removed: []
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
var withChanged = (list2, tag) => list2.includes(tag) ? [...list2] : [...list2, tag];
|
|
2785
|
+
var without = (list2, tag) => list2.filter((t) => t !== tag);
|
|
2786
|
+
function draftDocument(drafts, locale) {
|
|
2787
|
+
if (locale === null) return drafts.main.document;
|
|
2788
|
+
return drafts.translations[locale]?.document;
|
|
2789
|
+
}
|
|
2790
|
+
function addLanguageDraft(drafts, locale) {
|
|
2791
|
+
const tag = canonicalLocale(locale) ?? locale;
|
|
2792
|
+
if (drafts.translations[tag]) return drafts;
|
|
2793
|
+
return {
|
|
2794
|
+
...drafts,
|
|
2795
|
+
translations: {
|
|
2796
|
+
...drafts.translations,
|
|
2797
|
+
[tag]: {
|
|
2798
|
+
subject: drafts.main.subject,
|
|
2799
|
+
preheader: drafts.main.preheader,
|
|
2800
|
+
document: structuredClone(drafts.main.document),
|
|
2801
|
+
status: "draft",
|
|
2802
|
+
insertions: copyInsertionTexts(drafts.insertions)
|
|
2803
|
+
}
|
|
2804
|
+
},
|
|
2805
|
+
changed: withChanged(drafts.changed, tag),
|
|
2806
|
+
removed: without(drafts.removed, tag)
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function copyInsertionTexts(insertions) {
|
|
2810
|
+
const out = {};
|
|
2811
|
+
for (const [name, def] of Object.entries(insertions)) out[name] = { choices: { ...def.choices }, filled: def.filled, otherwise: def.otherwise };
|
|
2812
|
+
return out;
|
|
2813
|
+
}
|
|
2814
|
+
function removeLanguageDraft(drafts, locale) {
|
|
2815
|
+
const tag = canonicalLocale(locale) ?? locale;
|
|
2816
|
+
const { [tag]: _gone, ...rest } = drafts.translations;
|
|
2817
|
+
return { ...drafts, translations: rest, changed: without(drafts.changed, tag), removed: withChanged(drafts.removed, tag) };
|
|
2818
|
+
}
|
|
2819
|
+
function editLanguageDraft(drafts, locale, edit) {
|
|
2820
|
+
if (locale === null) {
|
|
2821
|
+
return {
|
|
2822
|
+
...drafts,
|
|
2823
|
+
main: { ...drafts.main, ...edit },
|
|
2824
|
+
mainDocumentChanged: drafts.mainDocumentChanged || edit.document !== void 0
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
const version = drafts.translations[locale];
|
|
2828
|
+
if (!version) return drafts;
|
|
2829
|
+
const next2 = { ...version, ...edit };
|
|
2830
|
+
return { ...drafts, translations: { ...drafts.translations, [locale]: next2 }, changed: withChanged(drafts.changed, locale) };
|
|
2831
|
+
}
|
|
2832
|
+
function keepLanguageDocument(drafts, locale, document) {
|
|
2833
|
+
if (locale === null) return { ...drafts, main: { ...drafts.main, document } };
|
|
2834
|
+
const version = drafts.translations[locale];
|
|
2835
|
+
if (!version) return drafts;
|
|
2836
|
+
return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, document } } };
|
|
2837
|
+
}
|
|
2838
|
+
function setLanguageStatus(drafts, locale, status) {
|
|
2839
|
+
const version = drafts.translations[locale];
|
|
2840
|
+
if (!version || version.status === status) return drafts;
|
|
2841
|
+
return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, status } }, changed: withChanged(drafts.changed, locale) };
|
|
2842
|
+
}
|
|
2843
|
+
function draftsPatch(drafts, loaded) {
|
|
2844
|
+
return buildTemplatePatch({
|
|
2845
|
+
loaded,
|
|
2846
|
+
insertions: drafts.insertions,
|
|
2847
|
+
main: drafts.main,
|
|
2848
|
+
documentChanged: drafts.mainDocumentChanged,
|
|
2849
|
+
translations: drafts.translations,
|
|
2850
|
+
changed: new Set(drafts.changed),
|
|
2851
|
+
removed: new Set(drafts.removed)
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
function draftsDirty(drafts, loaded) {
|
|
2855
|
+
return Object.keys(draftsPatch(drafts, loaded)).length > 0;
|
|
2856
|
+
}
|
|
2857
|
+
function readyWithoutSubject(drafts) {
|
|
2858
|
+
return Object.entries(drafts.translations).filter(([tag, v]) => drafts.changed.includes(tag) && v.status === "ready" && storedTemplateLine(v.subject) === null).map(([tag]) => tag);
|
|
2859
|
+
}
|
|
2860
|
+
function rebaseDrafts(sent, now, saved) {
|
|
2861
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2862
|
+
let out = draftsFromTemplate(saved);
|
|
2863
|
+
if (!sameJson(now.insertions, sent.insertions)) out = { ...out, insertions: now.insertions };
|
|
2864
|
+
if (now.main.subject !== sent.main.subject) out = editLanguageDraft(out, null, { subject: now.main.subject });
|
|
2865
|
+
if (now.main.preheader !== sent.main.preheader) out = editLanguageDraft(out, null, { preheader: now.main.preheader });
|
|
2866
|
+
out = same(now.main.document, sent.main.document) ? keepLanguageDocument(out, null, now.main.document) : editLanguageDraft(out, null, { document: now.main.document });
|
|
2867
|
+
const tags = /* @__PURE__ */ new Set([...Object.keys(sent.translations), ...Object.keys(now.translations)]);
|
|
2868
|
+
for (const tag of tags) {
|
|
2869
|
+
const before = sent.translations[tag];
|
|
2870
|
+
const after = now.translations[tag];
|
|
2871
|
+
if (same(before, after)) {
|
|
2872
|
+
if (after) out = keepLanguageDocument(out, tag, after.document);
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
if (!after) {
|
|
2876
|
+
out = removeLanguageDraft(out, tag);
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
out = {
|
|
2880
|
+
...out,
|
|
2881
|
+
translations: { ...out.translations, [tag]: after },
|
|
2882
|
+
changed: withChanged(out.changed, tag),
|
|
2883
|
+
removed: without(out.removed, tag)
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
return sameJson(out.insertions, saved.insertions ?? {}) ? out : alignDrafts(out);
|
|
2887
|
+
}
|
|
2888
|
+
function alignDrafts(drafts) {
|
|
2889
|
+
const translations = {};
|
|
2890
|
+
for (const [tag, version] of Object.entries(drafts.translations)) {
|
|
2891
|
+
translations[tag] = { ...version, insertions: alignTranslationInsertions(drafts.insertions, version.insertions) };
|
|
2892
|
+
}
|
|
2893
|
+
return { ...drafts, translations };
|
|
2894
|
+
}
|
|
2895
|
+
function editMainInsertions(drafts, insertions) {
|
|
2896
|
+
return alignDrafts({ ...drafts, insertions });
|
|
2897
|
+
}
|
|
2898
|
+
function renameKey(record, from, to) {
|
|
2899
|
+
const out = {};
|
|
2900
|
+
for (const [k, v] of Object.entries(record)) out[k === from ? to : k] = v;
|
|
2901
|
+
return out;
|
|
2902
|
+
}
|
|
2903
|
+
function renameInsertion(drafts, from, to) {
|
|
2904
|
+
if (from === to || !ownEntry(drafts.insertions, from) || ownEntry(drafts.insertions, to)) return drafts;
|
|
2905
|
+
const translations = {};
|
|
2906
|
+
let changed = drafts.changed;
|
|
2907
|
+
for (const [tag, version] of Object.entries(drafts.translations)) {
|
|
2908
|
+
if (version.insertions && ownEntry(version.insertions, from)) {
|
|
2909
|
+
translations[tag] = { ...version, insertions: renameKey(version.insertions, from, to) };
|
|
2910
|
+
changed = withChanged(changed, tag);
|
|
2911
|
+
} else translations[tag] = version;
|
|
2912
|
+
}
|
|
2913
|
+
return { ...drafts, insertions: renameKey(drafts.insertions, from, to), translations, changed };
|
|
2914
|
+
}
|
|
2915
|
+
function renameInsertionChoice(drafts, name, from, to) {
|
|
2916
|
+
const def = ownEntry(drafts.insertions, name);
|
|
2917
|
+
if (!def || from === to || INSERTION_TEXT_SLOTS.includes(to) || ownEntry(def.choices, from) === void 0 || ownEntry(def.choices, to) !== void 0) {
|
|
2918
|
+
return drafts;
|
|
2919
|
+
}
|
|
2920
|
+
const translations = {};
|
|
2921
|
+
let changed = drafts.changed;
|
|
2922
|
+
for (const [tag, version] of Object.entries(drafts.translations)) {
|
|
2923
|
+
const t = ownEntry(version.insertions, name);
|
|
2924
|
+
if (t && Object.prototype.hasOwnProperty.call(t.choices, from)) {
|
|
2925
|
+
translations[tag] = { ...version, insertions: { ...version.insertions, [name]: { ...t, choices: renameKey(t.choices, from, to) } } };
|
|
2926
|
+
changed = withChanged(changed, tag);
|
|
2927
|
+
} else translations[tag] = version;
|
|
2928
|
+
}
|
|
2929
|
+
return {
|
|
2930
|
+
...drafts,
|
|
2931
|
+
insertions: { ...drafts.insertions, [name]: { ...def, choices: renameKey(def.choices, from, to) } },
|
|
2932
|
+
translations,
|
|
2933
|
+
changed
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
function editInsertionText(drafts, locale, name, choice, text) {
|
|
2937
|
+
const version = drafts.translations[locale];
|
|
2938
|
+
const def = ownEntry(drafts.insertions, name);
|
|
2939
|
+
if (!version || !def) return drafts;
|
|
2940
|
+
const aligned = alignTranslationInsertions(drafts.insertions, version.insertions);
|
|
2941
|
+
const current = aligned[name];
|
|
2942
|
+
let next2;
|
|
2943
|
+
if (choice === "filled") {
|
|
2944
|
+
if (def.filled === null) return drafts;
|
|
2945
|
+
next2 = { ...current, filled: text };
|
|
2946
|
+
} else if (choice === "otherwise") next2 = { ...current, otherwise: text };
|
|
2947
|
+
else if (Object.prototype.hasOwnProperty.call(def.choices, choice)) next2 = { ...current, choices: { ...current.choices, [choice]: text } };
|
|
2948
|
+
else return drafts;
|
|
2949
|
+
return {
|
|
2950
|
+
...drafts,
|
|
2951
|
+
translations: { ...drafts.translations, [locale]: { ...version, insertions: { ...aligned, [name]: next2 } } },
|
|
2952
|
+
changed: withChanged(drafts.changed, locale)
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
function missingInsertionTexts(drafts, locale) {
|
|
2956
|
+
const version = drafts.translations[locale];
|
|
2957
|
+
return version ? insertionCoverage(drafts.insertions, version.insertions) : [];
|
|
2958
|
+
}
|
|
2959
|
+
function readyWithoutInsertions(drafts) {
|
|
2960
|
+
return Object.entries(drafts.translations).filter(([tag, v]) => drafts.changed.includes(tag) && v.status === "ready" && insertionCoverage(drafts.insertions, v.insertions).length > 0).map(([tag]) => tag);
|
|
2961
|
+
}
|
|
2962
|
+
function demotedBySave(drafts, loaded) {
|
|
2963
|
+
if (sameJson(drafts.insertions, loaded.insertions ?? {})) return [];
|
|
2964
|
+
return Object.entries(drafts.translations).filter(([tag, v]) => !drafts.changed.includes(tag) && v.status === "ready" && insertionCoverage(drafts.insertions, v.insertions).length > 0).map(([tag]) => tag).sort();
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// src/insertion-editing.ts
|
|
2968
|
+
var has = (record, key) => Object.prototype.hasOwnProperty.call(record, key);
|
|
2969
|
+
function insertionNameProblem(insertions, name, current) {
|
|
2970
|
+
if (!InsertionName.safeParse(name).success) return "Use lowercase letters, digits and underscores, starting with a letter (like opening).";
|
|
2971
|
+
if (has(RESERVED_MERGE_FIELDS, name)) return `"${name}" is a merge field the service fills itself. Choose another name.`;
|
|
2972
|
+
if (name !== current && has(insertions, name)) return `There is already an insertion called "${name}".`;
|
|
2973
|
+
const chooser = Object.entries(insertions).find(([other, def]) => def.by === name && other !== current);
|
|
2974
|
+
if (chooser) return `"${name}" is the value that chooses "${chooser[0]}". Choose another name.`;
|
|
2975
|
+
if (current !== void 0 && ownEntry(insertions, current)?.by === name) return `"${name}" is the value that chooses this insertion. Choose another name.`;
|
|
2976
|
+
return null;
|
|
2977
|
+
}
|
|
2978
|
+
function insertionByProblem(insertions, by, self) {
|
|
2979
|
+
if (!InsertionName.safeParse(by).success) return "The value that chooses is a merge-field name: lowercase letters, digits and underscores (like reason).";
|
|
2980
|
+
if (by === "unsubscribe_url") return "The unsubscribe link cannot choose a text. Use a value the client or the contact provides.";
|
|
2981
|
+
if (by === self || has(insertions, by)) return `"${by}" is an insertion, not a value. Use a value the client or the contact provides.`;
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
function normalizeChoiceKey(raw) {
|
|
2985
|
+
return raw.trim().toLowerCase();
|
|
2986
|
+
}
|
|
2987
|
+
function choiceKeyProblem(insertion, key, current) {
|
|
2988
|
+
if (INSERTION_TEXT_SLOTS.includes(key)) {
|
|
2989
|
+
return `"${key}" names the insertion's ${key === "filled" ? '"any other filled value"' : '"otherwise"'} text, so it cannot be a choice. Choose another value.`;
|
|
2990
|
+
}
|
|
2991
|
+
if (!InsertionChoiceKey.safeParse(key).success) return "A choice is the value a client sends: lowercase letters, digits, hyphens and underscores (like venue).";
|
|
2992
|
+
if (key !== current && has(insertion.choices, key)) return `There is already a choice "${key}".`;
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2995
|
+
function addInsertion(drafts, rawName, rawBy) {
|
|
2996
|
+
const name = rawName.trim();
|
|
2997
|
+
const by = rawBy.trim();
|
|
2998
|
+
if (Object.keys(drafts.insertions).length >= MAX_INSERTIONS) return { ok: false, message: `A template holds at most ${MAX_INSERTIONS} insertions.` };
|
|
2999
|
+
const nameProblem = insertionNameProblem(drafts.insertions, name);
|
|
3000
|
+
if (nameProblem) return { ok: false, message: nameProblem };
|
|
3001
|
+
if (name === by) return { ok: false, message: "An insertion cannot be chosen by its own name. Use the value the client sends (like reason)." };
|
|
3002
|
+
const byProblem = insertionByProblem(drafts.insertions, by);
|
|
3003
|
+
if (byProblem) return { ok: false, message: byProblem };
|
|
3004
|
+
return { ok: true, drafts: editMainInsertions(drafts, { ...drafts.insertions, [name]: { by, choices: {}, filled: null, otherwise: "" } }) };
|
|
3005
|
+
}
|
|
3006
|
+
function removeInsertion(drafts, name) {
|
|
3007
|
+
if (!has(drafts.insertions, name)) return drafts;
|
|
3008
|
+
const { [name]: _gone, ...rest } = drafts.insertions;
|
|
3009
|
+
return editMainInsertions(drafts, rest);
|
|
3010
|
+
}
|
|
3011
|
+
function renameInsertionChecked(drafts, from, rawTo) {
|
|
3012
|
+
const to = rawTo.trim();
|
|
3013
|
+
if (to === from) return { ok: true, drafts };
|
|
3014
|
+
const problem = insertionNameProblem(drafts.insertions, to, from);
|
|
3015
|
+
if (problem) return { ok: false, message: problem };
|
|
3016
|
+
return { ok: true, drafts: renameInsertion(drafts, from, to) };
|
|
3017
|
+
}
|
|
3018
|
+
function setInsertionBy(drafts, name, rawBy) {
|
|
3019
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3020
|
+
if (!def) return { ok: false, message: `There is no insertion "${name}".` };
|
|
3021
|
+
const by = rawBy.trim();
|
|
3022
|
+
const problem = insertionByProblem(drafts.insertions, by, name);
|
|
3023
|
+
if (problem) return { ok: false, message: problem };
|
|
3024
|
+
if (by === def.by) return { ok: true, drafts };
|
|
3025
|
+
return { ok: true, drafts: editMainInsertions(drafts, { ...drafts.insertions, [name]: { ...def, by } }) };
|
|
3026
|
+
}
|
|
3027
|
+
function addInsertionChoice(drafts, name, rawKey) {
|
|
3028
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3029
|
+
if (!def) return { ok: false, message: `There is no insertion "${name}".` };
|
|
3030
|
+
if (Object.keys(def.choices).length >= MAX_INSERTION_CHOICES) return { ok: false, message: `An insertion holds at most ${MAX_INSERTION_CHOICES} choices.` };
|
|
3031
|
+
const key = normalizeChoiceKey(rawKey);
|
|
3032
|
+
const problem = choiceKeyProblem(def, key);
|
|
3033
|
+
if (problem) return { ok: false, message: problem };
|
|
3034
|
+
return { ok: true, drafts: editMainInsertions(drafts, { ...drafts.insertions, [name]: { ...def, choices: { ...def.choices, [key]: "" } } }) };
|
|
3035
|
+
}
|
|
3036
|
+
function removeInsertionChoice(drafts, name, key) {
|
|
3037
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3038
|
+
if (!def || !has(def.choices, key)) return drafts;
|
|
3039
|
+
const { [key]: _gone, ...choices } = def.choices;
|
|
3040
|
+
return editMainInsertions(drafts, { ...drafts.insertions, [name]: { ...def, choices } });
|
|
3041
|
+
}
|
|
3042
|
+
function renameInsertionChoiceChecked(drafts, name, from, rawTo) {
|
|
3043
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3044
|
+
if (!def) return { ok: false, message: `There is no insertion "${name}".` };
|
|
3045
|
+
const to = normalizeChoiceKey(rawTo);
|
|
3046
|
+
if (to === from) return { ok: true, drafts };
|
|
3047
|
+
const problem = choiceKeyProblem(def, to, from);
|
|
3048
|
+
if (problem) return { ok: false, message: problem };
|
|
3049
|
+
return { ok: true, drafts: renameInsertionChoice(drafts, name, from, to) };
|
|
3050
|
+
}
|
|
3051
|
+
function setInsertionFilled(drafts, name, on) {
|
|
3052
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3053
|
+
if (!def || def.filled !== null === on) return drafts;
|
|
3054
|
+
return editMainInsertions(drafts, { ...drafts.insertions, [name]: { ...def, filled: on ? "" : null } });
|
|
3055
|
+
}
|
|
3056
|
+
function editMainInsertionText(drafts, name, choice, text) {
|
|
3057
|
+
const def = ownEntry(drafts.insertions, name);
|
|
3058
|
+
if (!def) return drafts;
|
|
3059
|
+
let next2;
|
|
3060
|
+
if (choice === "otherwise") next2 = { ...def, otherwise: text };
|
|
3061
|
+
else if (choice === "filled") {
|
|
3062
|
+
if (def.filled === null) return drafts;
|
|
3063
|
+
next2 = { ...def, filled: text };
|
|
3064
|
+
} else if (has(def.choices, choice)) next2 = { ...def, choices: { ...def.choices, [choice]: text } };
|
|
3065
|
+
else return drafts;
|
|
3066
|
+
return editMainInsertions(drafts, { ...drafts.insertions, [name]: next2 });
|
|
3067
|
+
}
|
|
3068
|
+
function mainInsertionText(insertion, choice) {
|
|
3069
|
+
if (choice === "otherwise") return insertion.otherwise;
|
|
3070
|
+
if (choice === "filled") return insertion.filled;
|
|
3071
|
+
return ownEntry(insertion.choices, choice) ?? null;
|
|
3072
|
+
}
|
|
3073
|
+
function translationTextFromInput(mainText, value) {
|
|
3074
|
+
if (value.trim() !== "") return value;
|
|
3075
|
+
return mainText !== null && mainText.trim() === "" ? "" : null;
|
|
3076
|
+
}
|
|
3077
|
+
function insertionTextSlots(insertion) {
|
|
3078
|
+
return [...Object.keys(insertion.choices), ...insertion.filled !== null ? ["filled"] : [], "otherwise"];
|
|
3079
|
+
}
|
|
3080
|
+
function insertionSlotLabel(choice) {
|
|
3081
|
+
if (choice === "filled") return "any other filled value";
|
|
3082
|
+
if (choice === "otherwise") return "otherwise";
|
|
3083
|
+
return `'${choice}'`;
|
|
3084
|
+
}
|
|
3085
|
+
function joinWords(items) {
|
|
3086
|
+
if (items.length <= 1) return items.join("");
|
|
3087
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
3088
|
+
}
|
|
3089
|
+
function describeMissingTexts(missing) {
|
|
3090
|
+
if (missing.length === 0) return "";
|
|
3091
|
+
return `no text for ${joinWords(missing.map((m) => `${insertionSlotLabel(m.choice)} in ${m.insertion}`))}`;
|
|
3092
|
+
}
|
|
3093
|
+
function saveDemotions(drafts, loaded) {
|
|
3094
|
+
const tags = [.../* @__PURE__ */ new Set([...demotedBySave(drafts, loaded), ...readyWithoutInsertions(drafts)])].sort();
|
|
3095
|
+
return tags.map((locale) => ({ locale, missing: insertionCoverage(drafts.insertions, ownEntry(drafts.translations, locale)?.insertions) }));
|
|
3096
|
+
}
|
|
3097
|
+
function demoteForSave(drafts) {
|
|
3098
|
+
let out = drafts;
|
|
3099
|
+
for (const tag of readyWithoutInsertions(drafts)) out = setLanguageStatus(out, tag, "draft");
|
|
3100
|
+
return out;
|
|
3101
|
+
}
|
|
3102
|
+
function draftInsertionProblems(drafts) {
|
|
3103
|
+
const out = [];
|
|
3104
|
+
const parsed = Insertions.safeParse(drafts.insertions);
|
|
3105
|
+
if (!parsed.success) {
|
|
3106
|
+
for (const issue of parsed.error.issues) out.push({ locale: null, reason: "invalid", path: issue.path.join("."), message: issue.message });
|
|
3107
|
+
return out;
|
|
3108
|
+
}
|
|
3109
|
+
for (const p of insertionProblems(drafts.insertions)) out.push({ ...p, locale: null });
|
|
3110
|
+
const places = (subject, preheader, document) => ({ subject, preheader, document: JSON.stringify(document) });
|
|
3111
|
+
for (const p of placementProblems(places(drafts.main.subject, drafts.main.preheader, drafts.main.document), drafts.insertions)) out.push({ ...p, locale: null });
|
|
3112
|
+
for (const [tag, version] of Object.entries(drafts.translations)) {
|
|
3113
|
+
if (!drafts.changed.includes(tag)) continue;
|
|
3114
|
+
for (const p of translationInsertionProblems(drafts.insertions, version.insertions ?? {})) out.push({ ...p, locale: tag });
|
|
3115
|
+
for (const p of placementProblems(places(version.subject, version.preheader, version.document), drafts.insertions)) out.push({ ...p, locale: tag });
|
|
3116
|
+
}
|
|
3117
|
+
return out;
|
|
3118
|
+
}
|
|
3119
|
+
var PREVIEW_FILLED = ":filled";
|
|
3120
|
+
var PREVIEW_EMPTY = ":empty";
|
|
3121
|
+
function previewControls(insertions) {
|
|
3122
|
+
return Object.entries(insertionInputs(insertions)).map(([by, input]) => ({
|
|
3123
|
+
by,
|
|
3124
|
+
insertions: input.insertions,
|
|
3125
|
+
options: [
|
|
3126
|
+
...input.keys.map((key) => ({ value: key, label: key })),
|
|
3127
|
+
...input.filled ? [{ value: PREVIEW_FILLED, label: "any other value" }] : [],
|
|
3128
|
+
{ value: PREVIEW_EMPTY, label: "empty" }
|
|
3129
|
+
]
|
|
3130
|
+
}));
|
|
3131
|
+
}
|
|
3132
|
+
function defaultPreviewPick(control) {
|
|
3133
|
+
return control.options[0].value;
|
|
3134
|
+
}
|
|
3135
|
+
function effectivePreviewPicks(controls, picks) {
|
|
3136
|
+
const out = {};
|
|
3137
|
+
for (const control of controls) {
|
|
3138
|
+
const picked = ownEntry(picks, control.by);
|
|
3139
|
+
out[control.by] = picked !== void 0 && control.options.some((o) => o.value === picked) ? picked : defaultPreviewPick(control);
|
|
3140
|
+
}
|
|
3141
|
+
return out;
|
|
3142
|
+
}
|
|
3143
|
+
function sampleFilledValue(by, keys) {
|
|
3144
|
+
const sample = by === "first_name" ? "Alex" : by === "last_name" ? "Morgan" : "Sample value";
|
|
3145
|
+
return keys.includes(sample.toLowerCase()) ? "Sample value" : sample;
|
|
3146
|
+
}
|
|
3147
|
+
function previewInsertions(main, translation, missing) {
|
|
3148
|
+
if (translation === void 0) return main;
|
|
3149
|
+
const aligned = alignTranslationInsertions(main, translation);
|
|
3150
|
+
const out = {};
|
|
3151
|
+
for (const [name, def] of Object.entries(main)) {
|
|
3152
|
+
const t = ownEntry(aligned, name);
|
|
3153
|
+
const choices = {};
|
|
3154
|
+
for (const key of Object.keys(def.choices)) choices[key] = ownEntry(t.choices, key) ?? missing;
|
|
3155
|
+
out[name] = { by: def.by, choices, filled: def.filled === null ? null : t.filled ?? missing, otherwise: t.otherwise ?? missing };
|
|
3156
|
+
}
|
|
3157
|
+
return out;
|
|
3158
|
+
}
|
|
3159
|
+
function previewPersonalization(insertions, picks) {
|
|
3160
|
+
const values = {};
|
|
3161
|
+
const controlled = /* @__PURE__ */ new Set();
|
|
3162
|
+
const shown = effectivePreviewPicks(previewControls(insertions), picks);
|
|
3163
|
+
for (const [by, input] of Object.entries(insertionInputs(insertions))) {
|
|
3164
|
+
controlled.add(by);
|
|
3165
|
+
const pick = shown[by];
|
|
3166
|
+
if (pick === PREVIEW_EMPTY) continue;
|
|
3167
|
+
values[by] = pick === PREVIEW_FILLED ? sampleFilledValue(by, input.keys) : pick;
|
|
3168
|
+
}
|
|
3169
|
+
const sample = sampleMergeValue(values);
|
|
3170
|
+
return {
|
|
3171
|
+
insertions: resolveInsertions(insertions, (name) => ownEntry(values, name)),
|
|
3172
|
+
value: (name, fallback) => controlled.has(name) ? sample(name, fallback) : `{{${name}${fallback === void 0 ? "" : `|${fallback}`}}}`
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
var REFUSAL_HINTS = {
|
|
3176
|
+
ready_needs_insertions: "Write the missing texts in that language, or set it back to draft, then save.",
|
|
3177
|
+
insertion_name: "Rename the insertion: the names first_name, last_name, email and unsubscribe_url are filled by the service.",
|
|
3178
|
+
insertion_by: "Choose the insertion by a value the client or the contact provides, not by the unsubscribe link or another insertion.",
|
|
3179
|
+
insertion_choice_key: "Use lowercase letters, digits, hyphens and underscores for a choice.",
|
|
3180
|
+
insertion_text: "An insertion text can hold merge fields such as {{first_name}}, but not another insertion.",
|
|
3181
|
+
insertion_with_fallback: 'Remove the |fallback from the insertion in the subject, preheader or body: its "otherwise" text is the fallback.',
|
|
3182
|
+
unknown_translation_insertion: "A language names an insertion or a choice the main language no longer has. Reload the template and try again."
|
|
3183
|
+
};
|
|
3184
|
+
function insertionRefusalHint(details) {
|
|
3185
|
+
const reason = details?.reason;
|
|
3186
|
+
if (typeof reason !== "string" || !has(REFUSAL_HINTS, reason)) return null;
|
|
3187
|
+
const hint = REFUSAL_HINTS[reason];
|
|
3188
|
+
if (reason !== "ready_needs_insertions") return hint;
|
|
3189
|
+
const missing = Array.isArray(details?.missing) ? details.missing.filter((m) => typeof m === "object" && m !== null && typeof m.insertion === "string" && typeof m.choice === "string") : [];
|
|
3190
|
+
return missing.length > 0 ? `The language has ${describeMissingTexts(missing)}. ${hint}` : hint;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
2220
3193
|
// src/providers.ts
|
|
2221
|
-
import { z as
|
|
3194
|
+
import { z as z11 } from "zod";
|
|
2222
3195
|
var PROVIDER_KINDS = ["smtp", "resend"];
|
|
2223
|
-
var ProviderKind =
|
|
2224
|
-
var ProviderPolicy =
|
|
2225
|
-
daily_recipient_budget:
|
|
2226
|
-
min_interval_ms:
|
|
2227
|
-
max_recipients_per_message:
|
|
3196
|
+
var ProviderKind = z11.enum(PROVIDER_KINDS);
|
|
3197
|
+
var ProviderPolicy = z11.object({
|
|
3198
|
+
daily_recipient_budget: z11.number().int().min(1).max(1e7),
|
|
3199
|
+
min_interval_ms: z11.number().int().min(0).max(36e5),
|
|
3200
|
+
max_recipients_per_message: z11.number().int().min(1).max(1e3)
|
|
2228
3201
|
});
|
|
2229
3202
|
var ICLOUD_SMTP_POLICY = {
|
|
2230
3203
|
daily_recipient_budget: 800,
|
|
@@ -2232,17 +3205,17 @@ var ICLOUD_SMTP_POLICY = {
|
|
|
2232
3205
|
max_recipients_per_message: 1
|
|
2233
3206
|
};
|
|
2234
3207
|
var SMTP_SECURITY = ["tls", "starttls"];
|
|
2235
|
-
var SmtpConfigBase =
|
|
2236
|
-
host:
|
|
2237
|
-
port:
|
|
3208
|
+
var SmtpConfigBase = z11.object({
|
|
3209
|
+
host: z11.string().min(1).max(255),
|
|
3210
|
+
port: z11.number().int().min(1).max(65535),
|
|
2238
3211
|
/** `tls` connects encrypted (465), `starttls` upgrades (587). Never plaintext. */
|
|
2239
|
-
security:
|
|
2240
|
-
username:
|
|
3212
|
+
security: z11.enum(SMTP_SECURITY),
|
|
3213
|
+
username: z11.string().min(1).max(255)
|
|
2241
3214
|
});
|
|
2242
|
-
var ResendConfigBase =
|
|
2243
|
-
var ProviderRejections =
|
|
2244
|
-
count:
|
|
2245
|
-
last_error:
|
|
3215
|
+
var ResendConfigBase = z11.object({});
|
|
3216
|
+
var ProviderRejections = z11.object({
|
|
3217
|
+
count: z11.number().int().min(0),
|
|
3218
|
+
last_error: z11.string().nullable(),
|
|
2246
3219
|
last_at: Timestamp.nullable(),
|
|
2247
3220
|
/**
|
|
2248
3221
|
* The last time the bounce circuit breaker tripped on one of this provider's
|
|
@@ -2252,59 +3225,59 @@ var ProviderRejections = z10.object({
|
|
|
2252
3225
|
* run's bounce blocks were undone and the mailing paused. `sample` is the
|
|
2253
3226
|
* reply that tripped it.
|
|
2254
3227
|
*/
|
|
2255
|
-
anomaly:
|
|
3228
|
+
anomaly: z11.object({
|
|
2256
3229
|
at: Timestamp,
|
|
2257
3230
|
/** `mailing`: one mailing was paused. `provider`: 5 refusals in a row with the same reply across the provider's sends (test sends and one-recipient mailings included) within 24 hours. */
|
|
2258
|
-
scope:
|
|
3231
|
+
scope: z11.enum(["mailing", "provider"]),
|
|
2259
3232
|
/**
|
|
2260
3233
|
* True while the provider-wide breaker is open: no bounce blocks through
|
|
2261
3234
|
* this provider, test sends refused and mailings not started or resumed
|
|
2262
3235
|
* (`provider_anomaly`), until an admin clears it (`providers.clearAnomaly`).
|
|
2263
3236
|
*/
|
|
2264
|
-
blocking:
|
|
3237
|
+
blocking: z11.boolean(),
|
|
2265
3238
|
mailing_id: Id.nullable(),
|
|
2266
|
-
reason:
|
|
2267
|
-
sample:
|
|
3239
|
+
reason: z11.string(),
|
|
3240
|
+
sample: z11.string()
|
|
2268
3241
|
}).nullable()
|
|
2269
3242
|
});
|
|
2270
|
-
var ProviderEvents =
|
|
2271
|
-
status:
|
|
2272
|
-
source:
|
|
2273
|
-
url:
|
|
2274
|
-
error:
|
|
3243
|
+
var ProviderEvents = z11.object({
|
|
3244
|
+
status: z11.enum(["active", "needs_secret"]),
|
|
3245
|
+
source: z11.enum(["automatic", "manual"]).nullable(),
|
|
3246
|
+
url: z11.string().url(),
|
|
3247
|
+
error: z11.string().nullable(),
|
|
2275
3248
|
/**
|
|
2276
3249
|
* Events that named an email this provider never sent through the service
|
|
2277
3250
|
* (another workspace or system sharing the Resend account): counted, never
|
|
2278
3251
|
* acted on.
|
|
2279
3252
|
*/
|
|
2280
|
-
unmatched:
|
|
3253
|
+
unmatched: z11.number().int().min(0)
|
|
2281
3254
|
});
|
|
2282
3255
|
var RESEND_EVENT_TYPES = ["email.bounced", "email.complained", "email.delivery_delayed"];
|
|
2283
|
-
var ResendSigningSecret =
|
|
2284
|
-
var ProviderEventsSecret =
|
|
3256
|
+
var ResendSigningSecret = z11.string().max(200).regex(/^whsec_[A-Za-z0-9+/]{16,}={0,2}$/, "must be the signing secret Resend shows, starting with whsec_");
|
|
3257
|
+
var ProviderEventsSecret = z11.object({ signing_secret: ResendSigningSecret });
|
|
2285
3258
|
var ProviderFields = {
|
|
2286
|
-
name:
|
|
2287
|
-
from_name:
|
|
3259
|
+
name: z11.string().min(1).max(120),
|
|
3260
|
+
from_name: z11.string().min(1).max(120),
|
|
2288
3261
|
from_email: Email,
|
|
2289
3262
|
reply_to: Email.nullable(),
|
|
2290
3263
|
policy: ProviderPolicy
|
|
2291
3264
|
};
|
|
2292
|
-
var Provider =
|
|
2293
|
-
|
|
3265
|
+
var Provider = z11.discriminatedUnion("kind", [
|
|
3266
|
+
z11.object({
|
|
2294
3267
|
id: Id,
|
|
2295
|
-
kind:
|
|
3268
|
+
kind: z11.literal("smtp"),
|
|
2296
3269
|
config: SmtpConfigBase,
|
|
2297
|
-
has_secret:
|
|
3270
|
+
has_secret: z11.boolean(),
|
|
2298
3271
|
...ProviderFields,
|
|
2299
3272
|
rejections: ProviderRejections,
|
|
2300
3273
|
created_at: Timestamp,
|
|
2301
3274
|
updated_at: Timestamp
|
|
2302
3275
|
}),
|
|
2303
|
-
|
|
3276
|
+
z11.object({
|
|
2304
3277
|
id: Id,
|
|
2305
|
-
kind:
|
|
3278
|
+
kind: z11.literal("resend"),
|
|
2306
3279
|
config: ResendConfigBase,
|
|
2307
|
-
has_secret:
|
|
3280
|
+
has_secret: z11.boolean(),
|
|
2308
3281
|
...ProviderFields,
|
|
2309
3282
|
rejections: ProviderRejections,
|
|
2310
3283
|
events: ProviderEvents,
|
|
@@ -2312,31 +3285,31 @@ var Provider = z10.discriminatedUnion("kind", [
|
|
|
2312
3285
|
updated_at: Timestamp
|
|
2313
3286
|
})
|
|
2314
3287
|
]);
|
|
2315
|
-
var ProviderCreate =
|
|
2316
|
-
|
|
2317
|
-
kind:
|
|
2318
|
-
config: SmtpConfigBase.extend({ password:
|
|
3288
|
+
var ProviderCreate = z11.discriminatedUnion("kind", [
|
|
3289
|
+
z11.object({
|
|
3290
|
+
kind: z11.literal("smtp"),
|
|
3291
|
+
config: SmtpConfigBase.extend({ password: z11.string().min(1).max(1024) }),
|
|
2319
3292
|
...ProviderFields
|
|
2320
3293
|
}),
|
|
2321
|
-
|
|
2322
|
-
kind:
|
|
2323
|
-
config: ResendConfigBase.extend({ api_key:
|
|
3294
|
+
z11.object({
|
|
3295
|
+
kind: z11.literal("resend"),
|
|
3296
|
+
config: ResendConfigBase.extend({ api_key: z11.string().min(1).max(1024) }),
|
|
2324
3297
|
...ProviderFields
|
|
2325
3298
|
})
|
|
2326
3299
|
]);
|
|
2327
|
-
var ProviderUpdate =
|
|
2328
|
-
|
|
2329
|
-
kind:
|
|
2330
|
-
config: SmtpConfigBase.extend({ password:
|
|
3300
|
+
var ProviderUpdate = z11.discriminatedUnion("kind", [
|
|
3301
|
+
z11.object({
|
|
3302
|
+
kind: z11.literal("smtp"),
|
|
3303
|
+
config: SmtpConfigBase.extend({ password: z11.string().min(1).max(1024).optional() }).partial().optional(),
|
|
2331
3304
|
name: ProviderFields.name.optional(),
|
|
2332
3305
|
from_name: ProviderFields.from_name.optional(),
|
|
2333
3306
|
from_email: ProviderFields.from_email.optional(),
|
|
2334
3307
|
reply_to: ProviderFields.reply_to.optional(),
|
|
2335
3308
|
policy: ProviderPolicy.partial().optional()
|
|
2336
3309
|
}),
|
|
2337
|
-
|
|
2338
|
-
kind:
|
|
2339
|
-
config: ResendConfigBase.extend({ api_key:
|
|
3310
|
+
z11.object({
|
|
3311
|
+
kind: z11.literal("resend"),
|
|
3312
|
+
config: ResendConfigBase.extend({ api_key: z11.string().min(1).max(1024).optional() }).optional(),
|
|
2340
3313
|
name: ProviderFields.name.optional(),
|
|
2341
3314
|
from_name: ProviderFields.from_name.optional(),
|
|
2342
3315
|
from_email: ProviderFields.from_email.optional(),
|
|
@@ -2344,48 +3317,48 @@ var ProviderUpdate = z10.discriminatedUnion("kind", [
|
|
|
2344
3317
|
policy: ProviderPolicy.partial().optional()
|
|
2345
3318
|
})
|
|
2346
3319
|
]);
|
|
2347
|
-
var ProviderUsage =
|
|
3320
|
+
var ProviderUsage = z11.object({
|
|
2348
3321
|
provider_id: Id,
|
|
2349
|
-
recipients_last_24h:
|
|
2350
|
-
remaining_budget:
|
|
3322
|
+
recipients_last_24h: z11.number().int().min(0),
|
|
3323
|
+
remaining_budget: z11.number().int().min(0),
|
|
2351
3324
|
/** When enough of the rolling window frees up to send again, if exhausted. */
|
|
2352
3325
|
next_capacity_at: Timestamp.nullable()
|
|
2353
3326
|
});
|
|
2354
|
-
var ProviderVerifyResult =
|
|
2355
|
-
ok:
|
|
2356
|
-
error:
|
|
3327
|
+
var ProviderVerifyResult = z11.object({
|
|
3328
|
+
ok: z11.boolean(),
|
|
3329
|
+
error: z11.string().nullable()
|
|
2357
3330
|
});
|
|
2358
3331
|
|
|
2359
3332
|
// src/contacts.ts
|
|
2360
|
-
import { z as
|
|
2361
|
-
var Topic =
|
|
3333
|
+
import { z as z12 } from "zod";
|
|
3334
|
+
var Topic = z12.object({
|
|
2362
3335
|
id: Id,
|
|
2363
3336
|
slug: Slug,
|
|
2364
|
-
name:
|
|
3337
|
+
name: z12.string().min(1).max(120),
|
|
2365
3338
|
/** Shown on the hosted preference page. */
|
|
2366
|
-
description:
|
|
3339
|
+
description: z12.string().max(1e3).nullable(),
|
|
2367
3340
|
/** Translations of `name` and `description` per locale, for the hosted page. */
|
|
2368
|
-
translations:
|
|
2369
|
-
|
|
3341
|
+
translations: z12.record(
|
|
3342
|
+
z12.object({ name: z12.string().min(1).max(120), description: z12.string().max(1e3).nullable() })
|
|
2370
3343
|
),
|
|
2371
3344
|
created_at: Timestamp,
|
|
2372
3345
|
updated_at: Timestamp
|
|
2373
3346
|
});
|
|
2374
|
-
var TopicCreate =
|
|
3347
|
+
var TopicCreate = z12.object({
|
|
2375
3348
|
slug: Slug,
|
|
2376
|
-
name:
|
|
2377
|
-
description:
|
|
3349
|
+
name: z12.string().min(1).max(120),
|
|
3350
|
+
description: z12.string().max(1e3).optional(),
|
|
2378
3351
|
translations: Topic.shape.translations.optional()
|
|
2379
3352
|
});
|
|
2380
3353
|
var TopicUpdate = TopicCreate.omit({ slug: true }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2381
|
-
var Contact =
|
|
3354
|
+
var Contact = z12.object({
|
|
2382
3355
|
id: Id,
|
|
2383
|
-
external_id:
|
|
3356
|
+
external_id: z12.string().min(1).max(255).nullable(),
|
|
2384
3357
|
/** Stored lowercased, unique per workspace. */
|
|
2385
3358
|
email: Email,
|
|
2386
|
-
first_name:
|
|
2387
|
-
last_name:
|
|
2388
|
-
locale:
|
|
3359
|
+
first_name: z12.string().max(200).nullable(),
|
|
3360
|
+
last_name: z12.string().max(200).nullable(),
|
|
3361
|
+
locale: z12.string().min(2).max(35).nullable(),
|
|
2389
3362
|
/**
|
|
2390
3363
|
* The person's own time zone, an IANA name such as `Europe/Berlin` (A1).
|
|
2391
3364
|
* Every wall clock an automation waits for is read in it, falling back to the
|
|
@@ -2393,65 +3366,65 @@ var Contact = z11.object({
|
|
|
2393
3366
|
*/
|
|
2394
3367
|
timezone: TimezoneName.nullable(),
|
|
2395
3368
|
properties: Properties,
|
|
2396
|
-
topics:
|
|
3369
|
+
topics: z12.array(Slug),
|
|
2397
3370
|
/** The slugs of the contact's tags (S4). */
|
|
2398
|
-
tags:
|
|
3371
|
+
tags: z12.array(Slug),
|
|
2399
3372
|
created_at: Timestamp,
|
|
2400
3373
|
updated_at: Timestamp
|
|
2401
3374
|
});
|
|
2402
|
-
var ContactUpsert =
|
|
2403
|
-
external_id:
|
|
3375
|
+
var ContactUpsert = z12.object({
|
|
3376
|
+
external_id: z12.string().min(1).max(255).optional(),
|
|
2404
3377
|
email: Email.optional(),
|
|
2405
|
-
first_name:
|
|
2406
|
-
last_name:
|
|
2407
|
-
locale:
|
|
3378
|
+
first_name: z12.string().max(200).nullable().optional(),
|
|
3379
|
+
last_name: z12.string().max(200).nullable().optional(),
|
|
3380
|
+
locale: z12.string().min(2).max(35).nullable().optional(),
|
|
2408
3381
|
timezone: TimezoneName.nullable().optional(),
|
|
2409
3382
|
properties: Properties.optional(),
|
|
2410
|
-
topics:
|
|
3383
|
+
topics: z12.array(Slug).max(100).optional()
|
|
2411
3384
|
}).refine((v) => v.external_id !== void 0 || v.email !== void 0, {
|
|
2412
3385
|
message: "external_id or email is required",
|
|
2413
3386
|
path: ["email"]
|
|
2414
3387
|
});
|
|
2415
|
-
var ContactUpsertResult =
|
|
3388
|
+
var ContactUpsertResult = z12.object({
|
|
2416
3389
|
contact: Contact,
|
|
2417
|
-
created:
|
|
3390
|
+
created: z12.boolean()
|
|
2418
3391
|
});
|
|
2419
3392
|
var ContactListQuery = PageQuery.extend({
|
|
2420
|
-
email:
|
|
2421
|
-
external_id:
|
|
3393
|
+
email: z12.string().min(1).max(254).optional(),
|
|
3394
|
+
external_id: z12.string().min(1).max(255).optional(),
|
|
2422
3395
|
topic: Slug.optional()
|
|
2423
3396
|
});
|
|
2424
|
-
var ContactErased =
|
|
2425
|
-
ok:
|
|
2426
|
-
erased_messages:
|
|
2427
|
-
erased_recipients:
|
|
2428
|
-
suppressions_kept:
|
|
3397
|
+
var ContactErased = z12.object({
|
|
3398
|
+
ok: z12.literal(true),
|
|
3399
|
+
erased_messages: z12.number().int().min(0),
|
|
3400
|
+
erased_recipients: z12.number().int().min(0),
|
|
3401
|
+
suppressions_kept: z12.number().int().min(0)
|
|
2429
3402
|
});
|
|
2430
3403
|
var SUPPRESSION_REASONS = ["unsubscribed", "bounced", "complained", "manual"];
|
|
2431
|
-
var SuppressionReason =
|
|
2432
|
-
var Suppression =
|
|
3404
|
+
var SuppressionReason = z12.enum(SUPPRESSION_REASONS);
|
|
3405
|
+
var Suppression = z12.object({
|
|
2433
3406
|
id: Id,
|
|
2434
3407
|
email: Email,
|
|
2435
3408
|
reason: SuppressionReason,
|
|
2436
3409
|
topic: Slug.nullable(),
|
|
2437
3410
|
source_message_id: Id.nullable(),
|
|
2438
|
-
note:
|
|
3411
|
+
note: z12.string().max(1e3).nullable(),
|
|
2439
3412
|
created_at: Timestamp
|
|
2440
3413
|
});
|
|
2441
|
-
var SuppressionCreate =
|
|
3414
|
+
var SuppressionCreate = z12.object({
|
|
2442
3415
|
email: Email,
|
|
2443
|
-
reason:
|
|
3416
|
+
reason: z12.enum(["manual", "unsubscribed"]),
|
|
2444
3417
|
topic: Slug.nullable().optional(),
|
|
2445
|
-
note:
|
|
3418
|
+
note: z12.string().max(1e3).optional()
|
|
2446
3419
|
});
|
|
2447
3420
|
var SuppressionListQuery = PageQuery.extend({
|
|
2448
|
-
email:
|
|
3421
|
+
email: z12.string().min(1).max(254).optional(),
|
|
2449
3422
|
reason: SuppressionReason.optional(),
|
|
2450
3423
|
topic: Slug.optional()
|
|
2451
3424
|
});
|
|
2452
3425
|
|
|
2453
3426
|
// src/webhooks.ts
|
|
2454
|
-
import { z as
|
|
3427
|
+
import { z as z13 } from "zod";
|
|
2455
3428
|
var WEBHOOK_EVENT_TYPES = [
|
|
2456
3429
|
"message.sent",
|
|
2457
3430
|
"message.failed",
|
|
@@ -2495,40 +3468,47 @@ var WEBHOOK_EVENT_TYPES = [
|
|
|
2495
3468
|
"template.updated",
|
|
2496
3469
|
"template.deleted"
|
|
2497
3470
|
];
|
|
2498
|
-
var WebhookEventType =
|
|
2499
|
-
var MessageEventBase =
|
|
3471
|
+
var WebhookEventType = z13.enum(WEBHOOK_EVENT_TYPES);
|
|
3472
|
+
var MessageEventBase = z13.object({
|
|
2500
3473
|
message_id: Id,
|
|
2501
3474
|
mailing_id: Id.nullable(),
|
|
2502
3475
|
mailing_metadata: MailingMetadata.nullable(),
|
|
2503
3476
|
recipient_id: Id.nullable(),
|
|
2504
3477
|
contact_id: Id.nullable(),
|
|
2505
|
-
external_id:
|
|
3478
|
+
external_id: z13.string().nullable(),
|
|
2506
3479
|
email: Email,
|
|
2507
|
-
subject:
|
|
3480
|
+
subject: z13.string(),
|
|
2508
3481
|
topic: Slug.nullable(),
|
|
2509
|
-
provider_message_id:
|
|
2510
|
-
is_test:
|
|
3482
|
+
provider_message_id: z13.string().nullable(),
|
|
3483
|
+
is_test: z13.boolean()
|
|
2511
3484
|
});
|
|
2512
3485
|
var MessageSentData = MessageEventBase.extend({
|
|
2513
3486
|
/** The final HTML as sent, so the client can archive it beside its own records. */
|
|
2514
|
-
html:
|
|
2515
|
-
sent_at: Timestamp
|
|
3487
|
+
html: z13.string(),
|
|
3488
|
+
sent_at: Timestamp,
|
|
3489
|
+
/**
|
|
3490
|
+
* The choice each insertion made (see `Message.insertions`); absent or null
|
|
3491
|
+
* when the message had none. A `matched: false` entry is a value the
|
|
3492
|
+
* template has no choice for: the client sent something its form should
|
|
3493
|
+
* not offer.
|
|
3494
|
+
*/
|
|
3495
|
+
insertions: InsertionChoices.nullish()
|
|
2516
3496
|
});
|
|
2517
3497
|
var MessageFailedData = MessageEventBase.extend({
|
|
2518
|
-
error:
|
|
3498
|
+
error: z13.string(),
|
|
2519
3499
|
/** False when the provider rejected permanently; the recipient will not be retried. */
|
|
2520
|
-
retryable:
|
|
3500
|
+
retryable: z13.boolean(),
|
|
2521
3501
|
failed_at: Timestamp
|
|
2522
3502
|
});
|
|
2523
3503
|
var UNSUBSCRIBE_SOURCES = ["hosted_page", "one_click", "api", "dashboard", "signup_form"];
|
|
2524
|
-
var ContactUnsubscribedData =
|
|
3504
|
+
var ContactUnsubscribedData = z13.object({
|
|
2525
3505
|
contact_id: Id.nullable(),
|
|
2526
|
-
external_id:
|
|
3506
|
+
external_id: z13.string().nullable(),
|
|
2527
3507
|
email: Email,
|
|
2528
3508
|
/** The topic unsubscribed from; null means every topic. */
|
|
2529
3509
|
topic: Slug.nullable(),
|
|
2530
3510
|
mailing_id: Id.nullable(),
|
|
2531
|
-
source:
|
|
3511
|
+
source: z13.enum(UNSUBSCRIBE_SOURCES),
|
|
2532
3512
|
unsubscribed_at: Timestamp
|
|
2533
3513
|
});
|
|
2534
3514
|
var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: true }).extend({
|
|
@@ -2537,142 +3517,142 @@ var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: tr
|
|
|
2537
3517
|
* breaker undid a `bounced` block it judged false (a `contact.bounced` for the
|
|
2538
3518
|
* same address and mailing went out before); `topic` is null.
|
|
2539
3519
|
*/
|
|
2540
|
-
source:
|
|
3520
|
+
source: z13.enum([...UNSUBSCRIBE_SOURCES, "bounce_reverted"]),
|
|
2541
3521
|
resubscribed_at: Timestamp
|
|
2542
3522
|
});
|
|
2543
|
-
var ContactBouncedData =
|
|
3523
|
+
var ContactBouncedData = z13.object({
|
|
2544
3524
|
contact_id: Id.nullable(),
|
|
2545
|
-
external_id:
|
|
3525
|
+
external_id: z13.string().nullable(),
|
|
2546
3526
|
email: Email,
|
|
2547
3527
|
/** `bounced` for a hard bounce, `complained` for a spam complaint. */
|
|
2548
|
-
reason:
|
|
3528
|
+
reason: z13.enum(["bounced", "complained"]),
|
|
2549
3529
|
message_id: Id.nullable(),
|
|
2550
|
-
diagnostic:
|
|
3530
|
+
diagnostic: z13.string().nullable(),
|
|
2551
3531
|
bounced_at: Timestamp
|
|
2552
3532
|
});
|
|
2553
|
-
var MailingFinishedData =
|
|
3533
|
+
var MailingFinishedData = z13.object({
|
|
2554
3534
|
mailing_id: Id,
|
|
2555
3535
|
mailing_metadata: MailingMetadata,
|
|
2556
|
-
status:
|
|
3536
|
+
status: z13.enum(["sent", "partially_failed", "cancelled"]),
|
|
2557
3537
|
counts: MailingCounts,
|
|
2558
3538
|
finished_at: Timestamp
|
|
2559
3539
|
});
|
|
2560
|
-
var ContactSubscribedData =
|
|
3540
|
+
var ContactSubscribedData = z13.object({
|
|
2561
3541
|
contact_id: Id,
|
|
2562
|
-
external_id:
|
|
3542
|
+
external_id: z13.string().nullable(),
|
|
2563
3543
|
email: Email,
|
|
2564
|
-
topics:
|
|
2565
|
-
source:
|
|
3544
|
+
topics: z13.array(Slug),
|
|
3545
|
+
source: z13.enum(["signup_form", "api", "dashboard", "automation"]),
|
|
2566
3546
|
signup_form_id: Id.nullable(),
|
|
2567
|
-
signup_form_version:
|
|
3547
|
+
signup_form_version: z13.number().int().min(1).nullable(),
|
|
2568
3548
|
subscribed_at: Timestamp
|
|
2569
3549
|
});
|
|
2570
|
-
var ImportFinishedData =
|
|
3550
|
+
var ImportFinishedData = z13.object({
|
|
2571
3551
|
import_id: Id,
|
|
2572
|
-
status:
|
|
3552
|
+
status: z13.enum(["completed", "cancelled", "failed"]),
|
|
2573
3553
|
result: ImportReport.nullable(),
|
|
2574
|
-
error:
|
|
3554
|
+
error: z13.string().nullable(),
|
|
2575
3555
|
finished_at: Timestamp
|
|
2576
3556
|
});
|
|
2577
|
-
var MailingScheduledData =
|
|
3557
|
+
var MailingScheduledData = z13.object({
|
|
2578
3558
|
mailing_id: Id,
|
|
2579
3559
|
mailing_metadata: MailingMetadata,
|
|
2580
3560
|
scheduled_at: Timestamp.nullable()
|
|
2581
3561
|
});
|
|
2582
|
-
var MailingStartedData =
|
|
3562
|
+
var MailingStartedData = z13.object({
|
|
2583
3563
|
mailing_id: Id,
|
|
2584
3564
|
mailing_metadata: MailingMetadata,
|
|
2585
|
-
trigger:
|
|
2586
|
-
recipients:
|
|
3565
|
+
trigger: z13.enum(["send", "schedule"]),
|
|
3566
|
+
recipients: z13.number().int().min(0),
|
|
2587
3567
|
started_at: Timestamp
|
|
2588
3568
|
});
|
|
2589
|
-
var MailingScheduleFailedData =
|
|
3569
|
+
var MailingScheduleFailedData = z13.object({
|
|
2590
3570
|
mailing_id: Id,
|
|
2591
3571
|
mailing_metadata: MailingMetadata,
|
|
2592
|
-
code:
|
|
2593
|
-
message:
|
|
3572
|
+
code: z13.string(),
|
|
3573
|
+
message: z13.string(),
|
|
2594
3574
|
failed_at: Timestamp
|
|
2595
3575
|
});
|
|
2596
|
-
var MailingAbWinnerSelectedData =
|
|
3576
|
+
var MailingAbWinnerSelectedData = z13.object({
|
|
2597
3577
|
mailing_id: Id,
|
|
2598
3578
|
mailing_metadata: MailingMetadata,
|
|
2599
|
-
winner:
|
|
2600
|
-
decided_by:
|
|
2601
|
-
variants:
|
|
3579
|
+
winner: z13.string(),
|
|
3580
|
+
decided_by: z13.enum(["metric", "manual"]),
|
|
3581
|
+
variants: z13.array(VariantAnalytics),
|
|
2602
3582
|
decided_at: Timestamp
|
|
2603
3583
|
});
|
|
2604
|
-
var ContactTaggedData =
|
|
3584
|
+
var ContactTaggedData = z13.object({
|
|
2605
3585
|
contact_id: Id,
|
|
2606
|
-
external_id:
|
|
3586
|
+
external_id: z13.string().nullable(),
|
|
2607
3587
|
email: Email,
|
|
2608
3588
|
/** Only the tags the contact did not already have. */
|
|
2609
|
-
tags:
|
|
2610
|
-
source:
|
|
3589
|
+
tags: z13.array(Slug).min(1),
|
|
3590
|
+
source: z13.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
2611
3591
|
tagged_at: Timestamp
|
|
2612
3592
|
});
|
|
2613
|
-
var ContactUpdatedData =
|
|
3593
|
+
var ContactUpdatedData = z13.object({
|
|
2614
3594
|
contact_id: Id,
|
|
2615
|
-
external_id:
|
|
3595
|
+
external_id: z13.string().nullable(),
|
|
2616
3596
|
email: Email,
|
|
2617
|
-
changed:
|
|
3597
|
+
changed: z13.array(z13.string()).min(1),
|
|
2618
3598
|
properties: Properties,
|
|
2619
|
-
source:
|
|
3599
|
+
source: z13.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
2620
3600
|
updated_at: Timestamp
|
|
2621
3601
|
});
|
|
2622
|
-
var AutomationRunEventBase =
|
|
3602
|
+
var AutomationRunEventBase = z13.object({
|
|
2623
3603
|
automation_id: Id,
|
|
2624
|
-
automation_name:
|
|
3604
|
+
automation_name: z13.string(),
|
|
2625
3605
|
run_id: Id,
|
|
2626
|
-
version:
|
|
3606
|
+
version: z13.number().int().min(1),
|
|
2627
3607
|
contact_id: Id,
|
|
2628
|
-
external_id:
|
|
3608
|
+
external_id: z13.string().nullable(),
|
|
2629
3609
|
email: Email
|
|
2630
3610
|
});
|
|
2631
3611
|
var AutomationRunStartedData = AutomationRunEventBase.extend({
|
|
2632
3612
|
/** What let them in: a trigger kind, or `manual` for an explicit enrolment. */
|
|
2633
|
-
trigger_kind:
|
|
3613
|
+
trigger_kind: z13.string(),
|
|
2634
3614
|
entered_at: Timestamp
|
|
2635
3615
|
});
|
|
2636
3616
|
var AutomationRunCompletedData = AutomationRunEventBase.extend({
|
|
2637
|
-
last_step_id:
|
|
3617
|
+
last_step_id: z13.string().nullable(),
|
|
2638
3618
|
completed_at: Timestamp
|
|
2639
3619
|
});
|
|
2640
3620
|
var AutomationRunExitedData = AutomationRunEventBase.extend({
|
|
2641
3621
|
reason: AutomationEndReason,
|
|
2642
|
-
step_id:
|
|
3622
|
+
step_id: z13.string().nullable(),
|
|
2643
3623
|
exited_at: Timestamp
|
|
2644
3624
|
});
|
|
2645
3625
|
var AutomationRunFailedData = AutomationRunEventBase.extend({
|
|
2646
|
-
reason:
|
|
2647
|
-
step_id:
|
|
3626
|
+
reason: z13.string(),
|
|
3627
|
+
step_id: z13.string().nullable(),
|
|
2648
3628
|
failed_at: Timestamp
|
|
2649
3629
|
});
|
|
2650
3630
|
var AutomationWebhookData = AutomationRunEventBase.extend({
|
|
2651
|
-
step_id:
|
|
3631
|
+
step_id: z13.string(),
|
|
2652
3632
|
/** The step's own label, when it was given one, so a receiver can branch on a name. */
|
|
2653
|
-
step_label:
|
|
3633
|
+
step_label: z13.string().nullable(),
|
|
2654
3634
|
/** Whatever the step carries, verbatim. Null when it carries nothing. */
|
|
2655
3635
|
payload: JsonValue.nullable(),
|
|
2656
3636
|
fired_at: Timestamp
|
|
2657
3637
|
});
|
|
2658
|
-
var TemplateEventBase =
|
|
3638
|
+
var TemplateEventBase = z13.object({
|
|
2659
3639
|
template_id: Id,
|
|
2660
|
-
name:
|
|
2661
|
-
version:
|
|
2662
|
-
source:
|
|
3640
|
+
name: z13.string().min(1).max(200),
|
|
3641
|
+
version: z13.number().int().min(1),
|
|
3642
|
+
source: z13.enum(["api", "dashboard", "mjml_import"])
|
|
2663
3643
|
});
|
|
2664
3644
|
var TemplateCreatedData = TemplateEventBase.extend({ created_at: Timestamp });
|
|
2665
|
-
var TemplateUpdatedData = TemplateEventBase.extend({ archived:
|
|
3645
|
+
var TemplateUpdatedData = TemplateEventBase.extend({ archived: z13.boolean(), updated_at: Timestamp });
|
|
2666
3646
|
var TemplateDeletedData = TemplateEventBase.extend({ deleted_at: Timestamp });
|
|
2667
|
-
var envelope = (type, data) =>
|
|
3647
|
+
var envelope = (type, data) => z13.object({
|
|
2668
3648
|
/** Unique per event: a receiver deduplicates on it (deliveries may repeat). */
|
|
2669
3649
|
id: Id,
|
|
2670
|
-
type:
|
|
3650
|
+
type: z13.literal(type),
|
|
2671
3651
|
created_at: Timestamp,
|
|
2672
3652
|
workspace_id: Id,
|
|
2673
3653
|
data
|
|
2674
3654
|
});
|
|
2675
|
-
var WebhookEvent =
|
|
3655
|
+
var WebhookEvent = z13.discriminatedUnion("type", [
|
|
2676
3656
|
envelope("message.sent", MessageSentData),
|
|
2677
3657
|
envelope("message.failed", MessageFailedData),
|
|
2678
3658
|
envelope("contact.unsubscribed", ContactUnsubscribedData),
|
|
@@ -2696,16 +3676,16 @@ var WebhookEvent = z12.discriminatedUnion("type", [
|
|
|
2696
3676
|
envelope("template.updated", TemplateUpdatedData),
|
|
2697
3677
|
envelope("template.deleted", TemplateDeletedData)
|
|
2698
3678
|
]);
|
|
2699
|
-
var WebhookEndpoint =
|
|
3679
|
+
var WebhookEndpoint = z13.object({
|
|
2700
3680
|
id: Id,
|
|
2701
|
-
url:
|
|
2702
|
-
description:
|
|
2703
|
-
events:
|
|
2704
|
-
enabled:
|
|
3681
|
+
url: z13.string().url(),
|
|
3682
|
+
description: z13.string().max(500).nullable(),
|
|
3683
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3684
|
+
enabled: z13.boolean(),
|
|
2705
3685
|
created_at: Timestamp,
|
|
2706
3686
|
updated_at: Timestamp
|
|
2707
3687
|
});
|
|
2708
|
-
var WebhookUrl =
|
|
3688
|
+
var WebhookUrl = z13.string().url().refine((u) => {
|
|
2709
3689
|
try {
|
|
2710
3690
|
const url = new URL(u);
|
|
2711
3691
|
if (url.protocol === "https:") return true;
|
|
@@ -2714,35 +3694,35 @@ var WebhookUrl = z12.string().url().refine((u) => {
|
|
|
2714
3694
|
return false;
|
|
2715
3695
|
}
|
|
2716
3696
|
}, "must be https (http only for localhost)");
|
|
2717
|
-
var WebhookEndpointCreate =
|
|
3697
|
+
var WebhookEndpointCreate = z13.object({
|
|
2718
3698
|
url: WebhookUrl,
|
|
2719
|
-
description:
|
|
2720
|
-
events:
|
|
2721
|
-
enabled:
|
|
3699
|
+
description: z13.string().max(500).optional(),
|
|
3700
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3701
|
+
enabled: z13.boolean().optional()
|
|
2722
3702
|
});
|
|
2723
|
-
var WebhookEndpointUpdate =
|
|
3703
|
+
var WebhookEndpointUpdate = z13.object({
|
|
2724
3704
|
url: WebhookUrl,
|
|
2725
|
-
description:
|
|
2726
|
-
events:
|
|
2727
|
-
enabled:
|
|
3705
|
+
description: z13.string().max(500).nullable(),
|
|
3706
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3707
|
+
enabled: z13.boolean()
|
|
2728
3708
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2729
|
-
var WebhookEndpointWithSecret =
|
|
3709
|
+
var WebhookEndpointWithSecret = z13.object({
|
|
2730
3710
|
endpoint: WebhookEndpoint,
|
|
2731
|
-
secret:
|
|
3711
|
+
secret: z13.string().min(32)
|
|
2732
3712
|
});
|
|
2733
3713
|
var WEBHOOK_DELIVERY_STATUSES = ["pending", "succeeded", "failed"];
|
|
2734
|
-
var WebhookDeliveryStatus =
|
|
3714
|
+
var WebhookDeliveryStatus = z13.enum(WEBHOOK_DELIVERY_STATUSES);
|
|
2735
3715
|
var WEBHOOK_MAX_ATTEMPTS = 8;
|
|
2736
3716
|
var WEBHOOK_RETRY_DELAYS_SECONDS = [30, 120, 600, 1800, 3600, 7200, 21600];
|
|
2737
|
-
var WebhookDelivery =
|
|
3717
|
+
var WebhookDelivery = z13.object({
|
|
2738
3718
|
id: Id,
|
|
2739
3719
|
endpoint_id: Id,
|
|
2740
3720
|
event_id: Id,
|
|
2741
3721
|
event_type: WebhookEventType,
|
|
2742
3722
|
status: WebhookDeliveryStatus,
|
|
2743
|
-
attempts:
|
|
2744
|
-
last_status_code:
|
|
2745
|
-
last_error:
|
|
3723
|
+
attempts: z13.number().int().min(0),
|
|
3724
|
+
last_status_code: z13.number().int().min(100).max(599).nullable(),
|
|
3725
|
+
last_error: z13.string().nullable(),
|
|
2746
3726
|
next_attempt_at: Timestamp.nullable(),
|
|
2747
3727
|
delivered_at: Timestamp.nullable(),
|
|
2748
3728
|
created_at: Timestamp
|
|
@@ -2751,7 +3731,7 @@ var WebhookDeliveryListQuery = PageQuery.extend({
|
|
|
2751
3731
|
status: WebhookDeliveryStatus.optional(),
|
|
2752
3732
|
event_type: WebhookEventType.optional()
|
|
2753
3733
|
});
|
|
2754
|
-
var WebhookDeliveryParams =
|
|
3734
|
+
var WebhookDeliveryParams = z13.object({ id: Id, delivery_id: Id });
|
|
2755
3735
|
|
|
2756
3736
|
// src/webhook-signing.ts
|
|
2757
3737
|
var WEBHOOK_SIGNATURE_HEADER = "x-mail-signature";
|
|
@@ -2818,79 +3798,79 @@ async function verifyWebhook(input) {
|
|
|
2818
3798
|
}
|
|
2819
3799
|
|
|
2820
3800
|
// src/billing.ts
|
|
2821
|
-
import { z as
|
|
3801
|
+
import { z as z14 } from "zod";
|
|
2822
3802
|
var PLAN_IDS = ["free", "starter", "growth", "design_partner"];
|
|
2823
|
-
var PlanId =
|
|
2824
|
-
var PlanLimits =
|
|
2825
|
-
monthly_messages:
|
|
2826
|
-
contacts:
|
|
2827
|
-
members:
|
|
2828
|
-
providers:
|
|
2829
|
-
webhook_endpoints:
|
|
3803
|
+
var PlanId = z14.enum(PLAN_IDS);
|
|
3804
|
+
var PlanLimits = z14.object({
|
|
3805
|
+
monthly_messages: z14.number().int().min(0).nullable(),
|
|
3806
|
+
contacts: z14.number().int().min(0).nullable(),
|
|
3807
|
+
members: z14.number().int().min(1).nullable(),
|
|
3808
|
+
providers: z14.number().int().min(1).nullable(),
|
|
3809
|
+
webhook_endpoints: z14.number().int().min(0).nullable(),
|
|
2830
3810
|
/** Automations that may be live (active or paused) at once (A1). */
|
|
2831
|
-
active_automations:
|
|
3811
|
+
active_automations: z14.number().int().min(0).nullable(),
|
|
2832
3812
|
/** Steps one automation may hold. Null is the flow's own cap, not unlimited. */
|
|
2833
|
-
automation_steps:
|
|
3813
|
+
automation_steps: z14.number().int().min(1).nullable()
|
|
2834
3814
|
});
|
|
2835
|
-
var PlanFeatures =
|
|
3815
|
+
var PlanFeatures = z14.object({
|
|
2836
3816
|
/** A/B tests of subject and content on a mailing (S4). */
|
|
2837
|
-
ab_testing:
|
|
3817
|
+
ab_testing: z14.boolean(),
|
|
2838
3818
|
/** Open and click tracking, still off until a workspace turns it on (`settings.tracking_enabled`). */
|
|
2839
|
-
tracking:
|
|
3819
|
+
tracking: z14.boolean(),
|
|
2840
3820
|
/** Sending and hosting pages on the workspace's own domain (a later phase). */
|
|
2841
|
-
custom_domains:
|
|
3821
|
+
custom_domains: z14.boolean(),
|
|
2842
3822
|
/** Automations: flows a person walks through step by step (A1). */
|
|
2843
|
-
automations:
|
|
3823
|
+
automations: z14.boolean()
|
|
2844
3824
|
});
|
|
2845
3825
|
var PLAN_FEATURES = ["ab_testing", "tracking", "custom_domains", "automations"];
|
|
2846
|
-
var Plan =
|
|
3826
|
+
var Plan = z14.object({
|
|
2847
3827
|
id: PlanId,
|
|
2848
|
-
name:
|
|
3828
|
+
name: z14.string().min(1),
|
|
2849
3829
|
/** Price in the smallest currency unit per month; null for plans not sold. */
|
|
2850
|
-
monthly_price_cents:
|
|
2851
|
-
currency:
|
|
3830
|
+
monthly_price_cents: z14.number().int().min(0).nullable(),
|
|
3831
|
+
currency: z14.string().length(3),
|
|
2852
3832
|
limits: PlanLimits,
|
|
2853
3833
|
features: PlanFeatures
|
|
2854
3834
|
});
|
|
2855
|
-
var CatalogPlan = Plan.extend({ sellable:
|
|
2856
|
-
var Subscription =
|
|
3835
|
+
var CatalogPlan = Plan.extend({ sellable: z14.boolean() });
|
|
3836
|
+
var Subscription = z14.object({
|
|
2857
3837
|
workspace_id: Id,
|
|
2858
3838
|
plan: PlanId,
|
|
2859
|
-
status:
|
|
3839
|
+
status: z14.enum(["active", "trialing", "past_due", "cancelled"]),
|
|
2860
3840
|
current_period_start: Timestamp,
|
|
2861
3841
|
current_period_end: Timestamp,
|
|
2862
|
-
cancel_at_period_end:
|
|
3842
|
+
cancel_at_period_end: z14.boolean(),
|
|
2863
3843
|
/**
|
|
2864
3844
|
* A design partner outside billing (plan `design_partner`, no limits, no
|
|
2865
3845
|
* checkout). Set only by an operator, never through the API.
|
|
2866
3846
|
*/
|
|
2867
|
-
billing_exempt:
|
|
3847
|
+
billing_exempt: z14.boolean()
|
|
2868
3848
|
});
|
|
2869
3849
|
var USAGE_METRICS = ["messages", "contacts", "members", "providers", "webhook_endpoints", "active_automations"];
|
|
2870
|
-
var UsageMetric =
|
|
3850
|
+
var UsageMetric = z14.enum(USAGE_METRICS);
|
|
2871
3851
|
var USAGE_WARNING_RATIO = 0.8;
|
|
2872
|
-
var UsageWarning =
|
|
3852
|
+
var UsageWarning = z14.object({
|
|
2873
3853
|
metric: UsageMetric,
|
|
2874
|
-
used:
|
|
2875
|
-
limit:
|
|
3854
|
+
used: z14.number().int().min(0),
|
|
3855
|
+
limit: z14.number().int().min(0),
|
|
2876
3856
|
/** `approaching`: at or above USAGE_WARNING_RATIO of the limit; `reached`: at or above the limit. */
|
|
2877
|
-
level:
|
|
3857
|
+
level: z14.enum(["approaching", "reached"])
|
|
2878
3858
|
});
|
|
2879
|
-
var Usage =
|
|
3859
|
+
var Usage = z14.object({
|
|
2880
3860
|
plan: PlanId,
|
|
2881
3861
|
period_start: Timestamp,
|
|
2882
3862
|
period_end: Timestamp,
|
|
2883
|
-
metrics:
|
|
2884
|
-
warnings:
|
|
3863
|
+
metrics: z14.record(UsageMetric, z14.object({ used: z14.number().int().min(0), limit: z14.number().int().min(0).nullable() })),
|
|
3864
|
+
warnings: z14.array(UsageWarning)
|
|
2885
3865
|
});
|
|
2886
|
-
var CheckoutRequest =
|
|
2887
|
-
plan:
|
|
2888
|
-
success_url:
|
|
2889
|
-
cancel_url:
|
|
3866
|
+
var CheckoutRequest = z14.object({
|
|
3867
|
+
plan: z14.enum(["starter", "growth"]),
|
|
3868
|
+
success_url: z14.string().url(),
|
|
3869
|
+
cancel_url: z14.string().url()
|
|
2890
3870
|
});
|
|
2891
|
-
var CheckoutSession =
|
|
2892
|
-
var PortalRequest =
|
|
2893
|
-
var PortalSession =
|
|
3871
|
+
var CheckoutSession = z14.object({ url: z14.string().url() });
|
|
3872
|
+
var PortalRequest = z14.object({ return_url: z14.string().url() });
|
|
3873
|
+
var PortalSession = z14.object({ url: z14.string().url() });
|
|
2894
3874
|
var USAGE_WARNING_HEADER = "x-mail-usage-warning";
|
|
2895
3875
|
function formatUsageWarningHeader(warnings) {
|
|
2896
3876
|
if (warnings.length === 0) return null;
|
|
@@ -2910,7 +3890,7 @@ function parseUsageWarningHeader(value) {
|
|
|
2910
3890
|
}
|
|
2911
3891
|
|
|
2912
3892
|
// src/routes.ts
|
|
2913
|
-
import { z as
|
|
3893
|
+
import { z as z15 } from "zod";
|
|
2914
3894
|
var list = (item) => page(item);
|
|
2915
3895
|
var foundationRoutes = {
|
|
2916
3896
|
"workspaces.create": {
|
|
@@ -3118,7 +4098,7 @@ var templateRoutes = {
|
|
|
3118
4098
|
path: "/v1/templates/:id/export",
|
|
3119
4099
|
params: IdParams,
|
|
3120
4100
|
query: TemplateExportQuery,
|
|
3121
|
-
response:
|
|
4101
|
+
response: z15.string(),
|
|
3122
4102
|
responseType: "text",
|
|
3123
4103
|
status: 200,
|
|
3124
4104
|
access: "read",
|
|
@@ -3442,7 +4422,7 @@ var sendingRoutes = {
|
|
|
3442
4422
|
path: "/v1/mailings/:id/export",
|
|
3443
4423
|
params: IdParams,
|
|
3444
4424
|
query: MailingExportQuery,
|
|
3445
|
-
response:
|
|
4425
|
+
response: z15.string(),
|
|
3446
4426
|
responseType: "text",
|
|
3447
4427
|
status: 200,
|
|
3448
4428
|
access: "read",
|
|
@@ -3675,7 +4655,7 @@ var platformRoutes = {
|
|
|
3675
4655
|
"contactProperties.list": {
|
|
3676
4656
|
method: "GET",
|
|
3677
4657
|
path: "/v1/contact-properties",
|
|
3678
|
-
response:
|
|
4658
|
+
response: z15.object({ data: z15.array(ContactPropertyDefinition) }),
|
|
3679
4659
|
status: 200,
|
|
3680
4660
|
access: "read",
|
|
3681
4661
|
phase: "S4"
|
|
@@ -3811,6 +4791,15 @@ var platformRoutes = {
|
|
|
3811
4791
|
access: "read",
|
|
3812
4792
|
phase: "S4"
|
|
3813
4793
|
},
|
|
4794
|
+
"mailings.languages": {
|
|
4795
|
+
method: "GET",
|
|
4796
|
+
path: "/v1/mailings/:id/languages",
|
|
4797
|
+
params: IdParams,
|
|
4798
|
+
response: MailingLanguageReport,
|
|
4799
|
+
status: 200,
|
|
4800
|
+
access: "read",
|
|
4801
|
+
phase: "S4"
|
|
4802
|
+
},
|
|
3814
4803
|
"signupForms.list": {
|
|
3815
4804
|
method: "GET",
|
|
3816
4805
|
path: "/v1/signup-forms",
|
|
@@ -3965,7 +4954,7 @@ var billingRoutes = {
|
|
|
3965
4954
|
"billing.plans": {
|
|
3966
4955
|
method: "GET",
|
|
3967
4956
|
path: "/v1/billing/plans",
|
|
3968
|
-
response:
|
|
4957
|
+
response: z15.object({ data: z15.array(CatalogPlan) }),
|
|
3969
4958
|
status: 200,
|
|
3970
4959
|
access: "read",
|
|
3971
4960
|
phase: "S5"
|
|
@@ -4107,7 +5096,7 @@ var automationRoutes = {
|
|
|
4107
5096
|
method: "POST",
|
|
4108
5097
|
path: "/v1/automations/:id/pause",
|
|
4109
5098
|
params: IdParams,
|
|
4110
|
-
body:
|
|
5099
|
+
body: z15.object({}).strict(),
|
|
4111
5100
|
response: Automation,
|
|
4112
5101
|
status: 200,
|
|
4113
5102
|
access: "write",
|
|
@@ -4117,7 +5106,7 @@ var automationRoutes = {
|
|
|
4117
5106
|
method: "POST",
|
|
4118
5107
|
path: "/v1/automations/:id/resume",
|
|
4119
5108
|
params: IdParams,
|
|
4120
|
-
body:
|
|
5109
|
+
body: z15.object({}).strict(),
|
|
4121
5110
|
response: Automation,
|
|
4122
5111
|
status: 200,
|
|
4123
5112
|
access: "write",
|
|
@@ -4127,7 +5116,7 @@ var automationRoutes = {
|
|
|
4127
5116
|
method: "POST",
|
|
4128
5117
|
path: "/v1/automations/:id/archive",
|
|
4129
5118
|
params: IdParams,
|
|
4130
|
-
body:
|
|
5119
|
+
body: z15.object({}).strict(),
|
|
4131
5120
|
response: Automation,
|
|
4132
5121
|
status: 200,
|
|
4133
5122
|
access: "write",
|
|
@@ -4261,13 +5250,13 @@ function matchRoute(method, pathname) {
|
|
|
4261
5250
|
for (const [id, route] of Object.entries(routes)) {
|
|
4262
5251
|
if (route.method !== method) continue;
|
|
4263
5252
|
const names = [];
|
|
4264
|
-
const
|
|
5253
|
+
const pattern2 = new RegExp(
|
|
4265
5254
|
"^" + route.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-z_]+)/g, (_, name) => {
|
|
4266
5255
|
names.push(name);
|
|
4267
5256
|
return "([^/]+)";
|
|
4268
5257
|
}) + "$"
|
|
4269
5258
|
);
|
|
4270
|
-
const match =
|
|
5259
|
+
const match = pattern2.exec(pathname);
|
|
4271
5260
|
if (!match) continue;
|
|
4272
5261
|
const params = {};
|
|
4273
5262
|
names.forEach((name, i) => {
|
|
@@ -4408,6 +5397,7 @@ export {
|
|
|
4408
5397
|
EXPORT_WARNING_COUNT_HEADER,
|
|
4409
5398
|
Email,
|
|
4410
5399
|
EmailStep,
|
|
5400
|
+
EmailStepLanguage,
|
|
4411
5401
|
ErrorBody,
|
|
4412
5402
|
ErrorCodeSchema,
|
|
4413
5403
|
Event,
|
|
@@ -4432,6 +5422,8 @@ export {
|
|
|
4432
5422
|
IMPORT_SKIP_REASONS,
|
|
4433
5423
|
IMPORT_STATUSES,
|
|
4434
5424
|
IMPORT_TERMINAL_STATUSES,
|
|
5425
|
+
INSERTION_PROBLEM_REASONS,
|
|
5426
|
+
INSERTION_TEXT_SLOTS,
|
|
4435
5427
|
INVITE_REFUSAL_REASONS,
|
|
4436
5428
|
INVITE_STATUSES,
|
|
4437
5429
|
Id,
|
|
@@ -4449,6 +5441,13 @@ export {
|
|
|
4449
5441
|
ImportStatus,
|
|
4450
5442
|
ImportWarning,
|
|
4451
5443
|
ImportedAsset,
|
|
5444
|
+
Insertion,
|
|
5445
|
+
InsertionChoice,
|
|
5446
|
+
InsertionChoiceKey,
|
|
5447
|
+
InsertionChoices,
|
|
5448
|
+
InsertionName,
|
|
5449
|
+
InsertionText,
|
|
5450
|
+
Insertions,
|
|
4452
5451
|
Invite,
|
|
4453
5452
|
InviteAccept,
|
|
4454
5453
|
InviteAccepted,
|
|
@@ -4459,6 +5458,8 @@ export {
|
|
|
4459
5458
|
JsonValue,
|
|
4460
5459
|
LETTER_RECIPIENT_REFUSAL,
|
|
4461
5460
|
LIST_UNSUBSCRIBE_POST_VALUE,
|
|
5461
|
+
LOCALE_TAG_PATTERN,
|
|
5462
|
+
LocaleTag,
|
|
4462
5463
|
MAILING_ACTIONS,
|
|
4463
5464
|
MAILING_KINDS,
|
|
4464
5465
|
MAILING_STATUSES,
|
|
@@ -4474,6 +5475,9 @@ export {
|
|
|
4474
5475
|
MAX_IMPORT_BYTES,
|
|
4475
5476
|
MAX_IMPORT_ROWS,
|
|
4476
5477
|
MAX_IMPORT_URL_LENGTH,
|
|
5478
|
+
MAX_INSERTIONS,
|
|
5479
|
+
MAX_INSERTION_CHOICES,
|
|
5480
|
+
MAX_INSERTION_TEXT_LENGTH,
|
|
4477
5481
|
MAX_INVITE_TTL_DAYS,
|
|
4478
5482
|
MAX_IN_LIST,
|
|
4479
5483
|
MAX_LETTER_RECIPIENTS,
|
|
@@ -4483,12 +5487,15 @@ export {
|
|
|
4483
5487
|
MAX_MJML_ELEMENTS,
|
|
4484
5488
|
MAX_MJML_IMPORT_BYTES,
|
|
4485
5489
|
MAX_PAGE_LIMIT,
|
|
5490
|
+
MAX_PREHEADER_LENGTH,
|
|
4486
5491
|
MAX_RECIPIENTS_PER_BATCH,
|
|
4487
5492
|
MAX_RE_ENTRY_COOLDOWN_SECONDS,
|
|
4488
5493
|
MAX_SAVED_SECTIONS,
|
|
4489
5494
|
MAX_SPLIT_BRANCHES,
|
|
4490
5495
|
MAX_STEPS,
|
|
4491
5496
|
MAX_STEP_VISITS,
|
|
5497
|
+
MAX_SUBJECT_LENGTH,
|
|
5498
|
+
MAX_TEMPLATE_TRANSLATIONS,
|
|
4492
5499
|
MAX_TRIGGERS,
|
|
4493
5500
|
MEMBER_ROLES,
|
|
4494
5501
|
MERGE_FIELD_PATTERN,
|
|
@@ -4506,6 +5513,9 @@ export {
|
|
|
4506
5513
|
MailingExportQuery,
|
|
4507
5514
|
MailingFinishedData,
|
|
4508
5515
|
MailingKind,
|
|
5516
|
+
MailingLanguage,
|
|
5517
|
+
MailingLanguageReport,
|
|
5518
|
+
MailingLanguageStatus,
|
|
4509
5519
|
MailingListQuery,
|
|
4510
5520
|
MailingMetadata,
|
|
4511
5521
|
MailingRetryFailedRequest,
|
|
@@ -4540,6 +5550,8 @@ export {
|
|
|
4540
5550
|
Ok,
|
|
4541
5551
|
PLAN_FEATURES,
|
|
4542
5552
|
PLAN_IDS,
|
|
5553
|
+
PREVIEW_EMPTY,
|
|
5554
|
+
PREVIEW_FILLED,
|
|
4543
5555
|
PROVIDER_KINDS,
|
|
4544
5556
|
PageQuery,
|
|
4545
5557
|
Plan,
|
|
@@ -4606,6 +5618,7 @@ export {
|
|
|
4606
5618
|
SuppressionCreate,
|
|
4607
5619
|
SuppressionListQuery,
|
|
4608
5620
|
SuppressionReason,
|
|
5621
|
+
TEMPLATE_LANGUAGE_STATUSES,
|
|
4609
5622
|
TERMINAL_MAILING_STATUSES,
|
|
4610
5623
|
Tag,
|
|
4611
5624
|
TagAssignment,
|
|
@@ -4621,8 +5634,13 @@ export {
|
|
|
4621
5634
|
TemplateImportPreview,
|
|
4622
5635
|
TemplateImportPreviewRequest,
|
|
4623
5636
|
TemplateImportResult,
|
|
5637
|
+
TemplateLanguageStatus,
|
|
4624
5638
|
TemplateListQuery,
|
|
5639
|
+
TemplatePreheader,
|
|
5640
|
+
TemplateSubject,
|
|
4625
5641
|
TemplateSummary,
|
|
5642
|
+
TemplateTranslation,
|
|
5643
|
+
TemplateTranslations,
|
|
4626
5644
|
TemplateUpdate,
|
|
4627
5645
|
TemplateUpdatedData,
|
|
4628
5646
|
TemplateVersion,
|
|
@@ -4634,6 +5652,8 @@ export {
|
|
|
4634
5652
|
TopicCreate,
|
|
4635
5653
|
TopicUpdate,
|
|
4636
5654
|
TrackingSettings,
|
|
5655
|
+
TranslationInsertion,
|
|
5656
|
+
TranslationInsertions,
|
|
4637
5657
|
UNSUBSCRIBE_PATH_PREFIX,
|
|
4638
5658
|
UNSUBSCRIBE_SOURCES,
|
|
4639
5659
|
USAGE_METRICS,
|
|
@@ -4675,16 +5695,39 @@ export {
|
|
|
4675
5695
|
WorkspaceSettings,
|
|
4676
5696
|
WorkspaceUpdate,
|
|
4677
5697
|
acceptsIdempotencyKey,
|
|
5698
|
+
addInsertion,
|
|
5699
|
+
addInsertionChoice,
|
|
5700
|
+
addLanguageDraft,
|
|
5701
|
+
alignTranslationInsertions,
|
|
4678
5702
|
alwaysWaits,
|
|
4679
5703
|
automationFilterFamily,
|
|
4680
5704
|
automationRoutes,
|
|
4681
5705
|
billingRoutes,
|
|
4682
5706
|
buildPath,
|
|
5707
|
+
buildTemplatePatch,
|
|
4683
5708
|
canTransition,
|
|
5709
|
+
canonicalLocale,
|
|
5710
|
+
choiceKeyProblem,
|
|
5711
|
+
chooseInsertion,
|
|
4684
5712
|
cycles,
|
|
5713
|
+
defaultPreviewPick,
|
|
4685
5714
|
defaultSettings,
|
|
5715
|
+
demoteForSave,
|
|
5716
|
+
demotedBySave,
|
|
5717
|
+
describeMissingTexts,
|
|
5718
|
+
draftDocument,
|
|
5719
|
+
draftInsertionProblems,
|
|
5720
|
+
draftsDirty,
|
|
5721
|
+
draftsFromTemplate,
|
|
5722
|
+
draftsPatch,
|
|
5723
|
+
editInsertionText,
|
|
5724
|
+
editLanguageDraft,
|
|
5725
|
+
editMainInsertionText,
|
|
5726
|
+
editMainInsertions,
|
|
5727
|
+
effectivePreviewPicks,
|
|
4686
5728
|
emptyFlow,
|
|
4687
5729
|
errorBody,
|
|
5730
|
+
escapeMergeHtml,
|
|
4688
5731
|
exportFilename,
|
|
4689
5732
|
filterConditions,
|
|
4690
5733
|
filterDepth,
|
|
@@ -4694,30 +5737,78 @@ export {
|
|
|
4694
5737
|
formatExportWarningsHeader,
|
|
4695
5738
|
formatUsageWarningHeader,
|
|
4696
5739
|
foundationRoutes,
|
|
5740
|
+
insertionByProblem,
|
|
5741
|
+
insertionChoices,
|
|
5742
|
+
insertionCoverage,
|
|
5743
|
+
insertionInputs,
|
|
5744
|
+
insertionNameProblem,
|
|
5745
|
+
insertionProblems,
|
|
5746
|
+
insertionRefusalHint,
|
|
5747
|
+
insertionSlotLabel,
|
|
5748
|
+
insertionTextSlots,
|
|
5749
|
+
insertionTokens,
|
|
5750
|
+
insertionValue,
|
|
4697
5751
|
inviteRoutes,
|
|
4698
5752
|
isDayEntirelyPast,
|
|
4699
5753
|
isPublishable,
|
|
4700
5754
|
isRetryableError,
|
|
5755
|
+
keepLanguageDocument,
|
|
5756
|
+
languageInsertions,
|
|
5757
|
+
languageName,
|
|
5758
|
+
languageTabs,
|
|
5759
|
+
mainInsertionText,
|
|
5760
|
+
matchLocale,
|
|
4701
5761
|
matchRoute,
|
|
5762
|
+
missingInsertionTexts,
|
|
4702
5763
|
missingRequiredMergeFields,
|
|
5764
|
+
normalizeChoiceKey,
|
|
4703
5765
|
nowSeconds,
|
|
4704
5766
|
operationsWithAccess,
|
|
5767
|
+
ownEntry,
|
|
4705
5768
|
page,
|
|
4706
5769
|
parseExportWarningsHeader,
|
|
4707
5770
|
parseUsageWarningHeader,
|
|
5771
|
+
personalizeHtml,
|
|
5772
|
+
personalizeText,
|
|
5773
|
+
placementProblems,
|
|
4708
5774
|
platformRoutes,
|
|
5775
|
+
previewControls,
|
|
5776
|
+
previewInsertions,
|
|
5777
|
+
previewPersonalization,
|
|
5778
|
+
primaryLanguage,
|
|
4709
5779
|
reachable,
|
|
5780
|
+
readyWithoutInsertions,
|
|
5781
|
+
readyWithoutSubject,
|
|
5782
|
+
rebaseDrafts,
|
|
5783
|
+
removeInsertion,
|
|
5784
|
+
removeInsertionChoice,
|
|
5785
|
+
removeLanguageDraft,
|
|
5786
|
+
renameInsertion,
|
|
5787
|
+
renameInsertionChecked,
|
|
5788
|
+
renameInsertionChoice,
|
|
5789
|
+
renameInsertionChoiceChecked,
|
|
5790
|
+
resolveInsertions,
|
|
5791
|
+
restorePatch,
|
|
4710
5792
|
routes,
|
|
5793
|
+
sampleMergeValue,
|
|
5794
|
+
saveDemotions,
|
|
4711
5795
|
sendingRoutes,
|
|
5796
|
+
setInsertionBy,
|
|
5797
|
+
setInsertionFilled,
|
|
5798
|
+
setLanguageStatus,
|
|
4712
5799
|
signWebhook,
|
|
4713
5800
|
statusForError,
|
|
4714
5801
|
stepTargets,
|
|
5802
|
+
storedTemplateLine,
|
|
4715
5803
|
stronglyConnected,
|
|
4716
5804
|
templateRoutes,
|
|
4717
5805
|
testGroupSize,
|
|
4718
5806
|
tightLoops,
|
|
4719
5807
|
timingSafeEqualString,
|
|
5808
|
+
translationInsertionProblems,
|
|
5809
|
+
translationTextFromInput,
|
|
4720
5810
|
triggerKey,
|
|
5811
|
+
unplacedInsertions,
|
|
4721
5812
|
validateAutomation,
|
|
4722
5813
|
verifyWebhook
|
|
4723
5814
|
};
|