@marlinjai/mail-contract 0.5.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 +8526 -1390
- package/dist/index.d.ts +8526 -1390
- package/dist/index.js +1570 -778
- package/dist/index.mjs +1502 -778
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -145,39 +145,341 @@ var API_VERSION_PREFIX = "/v1";
|
|
|
145
145
|
var HEALTH_PATH = "/healthz";
|
|
146
146
|
|
|
147
147
|
// src/workspace.ts
|
|
148
|
-
import { z as
|
|
148
|
+
import { z as z9 } from "zod";
|
|
149
149
|
|
|
150
150
|
// src/automations.ts
|
|
151
|
-
import { z as
|
|
151
|
+
import { z as z8 } from "zod";
|
|
152
152
|
|
|
153
153
|
// src/templates.ts
|
|
154
|
+
import { z as z4 } from "zod";
|
|
155
|
+
|
|
156
|
+
// src/insertions.ts
|
|
154
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
|
|
155
446
|
var DOCUMENT_SCHEMA_VERSIONS = ["1.0", "1.1"];
|
|
156
|
-
var DocumentSchemaVersion =
|
|
157
|
-
var TemplateDocument =
|
|
447
|
+
var DocumentSchemaVersion = z4.enum(DOCUMENT_SCHEMA_VERSIONS);
|
|
448
|
+
var TemplateDocument = z4.object({
|
|
158
449
|
version: DocumentSchemaVersion,
|
|
159
|
-
metadata:
|
|
160
|
-
sections:
|
|
450
|
+
metadata: z4.record(z4.unknown()),
|
|
451
|
+
sections: z4.array(z4.record(z4.unknown()))
|
|
161
452
|
}).passthrough();
|
|
162
453
|
var MAX_DOCUMENT_BYTES = 1e6;
|
|
163
454
|
var MAX_SUBJECT_LENGTH = 998;
|
|
164
455
|
var MAX_PREHEADER_LENGTH = 500;
|
|
165
|
-
var TemplateSubject =
|
|
166
|
-
var TemplatePreheader =
|
|
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");
|
|
167
458
|
var TEMPLATE_LANGUAGE_STATUSES = ["draft", "ready"];
|
|
168
|
-
var TemplateLanguageStatus =
|
|
169
|
-
var TemplateTranslation =
|
|
459
|
+
var TemplateLanguageStatus = z4.enum(TEMPLATE_LANGUAGE_STATUSES);
|
|
460
|
+
var TemplateTranslation = z4.object({
|
|
170
461
|
subject: TemplateSubject.nullable(),
|
|
171
462
|
preheader: TemplatePreheader.nullable(),
|
|
172
463
|
document: TemplateDocument,
|
|
173
|
-
status: TemplateLanguageStatus
|
|
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()
|
|
174
476
|
});
|
|
175
477
|
var MAX_TEMPLATE_TRANSLATIONS = 19;
|
|
176
|
-
var TemplateTranslations =
|
|
177
|
-
var Template =
|
|
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({
|
|
178
480
|
id: Id,
|
|
179
|
-
name:
|
|
180
|
-
description:
|
|
481
|
+
name: z4.string().min(1).max(200),
|
|
482
|
+
description: z4.string().max(2e3).nullable(),
|
|
181
483
|
/**
|
|
182
484
|
* The main language: what `subject`, `preheader` and `document` are written
|
|
183
485
|
* in, and what every recipient without a ready version of their own gets.
|
|
@@ -194,114 +496,132 @@ var Template = z3.object({
|
|
|
194
496
|
* lists is kept, but no send uses it until the language is listed again.
|
|
195
497
|
*/
|
|
196
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,
|
|
197
506
|
/** Increments on every saved change, of any language. */
|
|
198
|
-
version:
|
|
199
|
-
thumbnail_url:
|
|
507
|
+
version: z4.number().int().min(1),
|
|
508
|
+
thumbnail_url: z4.string().url().nullable(),
|
|
200
509
|
archived_at: Timestamp.nullable(),
|
|
201
510
|
created_at: Timestamp,
|
|
202
511
|
updated_at: Timestamp
|
|
203
512
|
});
|
|
204
|
-
var TemplateSummary = Template.omit({ document: true, translations: true }).extend({
|
|
205
|
-
languages:
|
|
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())
|
|
206
517
|
});
|
|
207
|
-
var TemplateCreate =
|
|
208
|
-
name:
|
|
209
|
-
description:
|
|
518
|
+
var TemplateCreate = z4.object({
|
|
519
|
+
name: z4.string().min(1).max(200),
|
|
520
|
+
description: z4.string().max(2e3).optional(),
|
|
210
521
|
locale: LocaleTag.optional(),
|
|
211
522
|
subject: TemplateSubject.nullable().optional(),
|
|
212
523
|
preheader: TemplatePreheader.nullable().optional(),
|
|
213
524
|
document: TemplateDocument,
|
|
214
|
-
translations: TemplateTranslations.optional()
|
|
525
|
+
translations: TemplateTranslations.optional(),
|
|
526
|
+
insertions: Insertions.optional()
|
|
215
527
|
});
|
|
216
|
-
var TemplateUpdate =
|
|
217
|
-
base_version:
|
|
218
|
-
name:
|
|
219
|
-
description:
|
|
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(),
|
|
220
532
|
locale: LocaleTag.optional(),
|
|
221
533
|
subject: TemplateSubject.nullable().optional(),
|
|
222
534
|
preheader: TemplatePreheader.nullable().optional(),
|
|
223
535
|
document: TemplateDocument.optional(),
|
|
224
|
-
translations:
|
|
225
|
-
|
|
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()
|
|
226
545
|
}).refine(
|
|
227
|
-
(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.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,
|
|
228
547
|
"at least one field besides base_version"
|
|
229
548
|
);
|
|
230
|
-
var SavedSectionBody =
|
|
549
|
+
var SavedSectionBody = z4.record(z4.unknown());
|
|
231
550
|
var MAX_SAVED_SECTIONS = 50;
|
|
232
|
-
var SavedSection =
|
|
551
|
+
var SavedSection = z4.object({
|
|
233
552
|
id: Id,
|
|
234
|
-
name:
|
|
235
|
-
description:
|
|
553
|
+
name: z4.string().min(1).max(200),
|
|
554
|
+
description: z4.string().max(2e3).nullable(),
|
|
236
555
|
section: SavedSectionBody,
|
|
237
|
-
created_by:
|
|
556
|
+
created_by: z4.string().nullable(),
|
|
238
557
|
created_at: Timestamp,
|
|
239
558
|
updated_at: Timestamp
|
|
240
559
|
});
|
|
241
|
-
var SavedSectionCreate =
|
|
242
|
-
name:
|
|
243
|
-
description:
|
|
560
|
+
var SavedSectionCreate = z4.object({
|
|
561
|
+
name: z4.string().min(1).max(200),
|
|
562
|
+
description: z4.string().max(2e3).optional(),
|
|
244
563
|
section: SavedSectionBody
|
|
245
564
|
});
|
|
246
|
-
var SavedSectionUpdate =
|
|
247
|
-
name:
|
|
248
|
-
description:
|
|
565
|
+
var SavedSectionUpdate = z4.object({
|
|
566
|
+
name: z4.string().min(1).max(200).optional(),
|
|
567
|
+
description: z4.string().max(2e3).nullable().optional(),
|
|
249
568
|
section: SavedSectionBody.optional()
|
|
250
569
|
}).refine((v) => v.name !== void 0 || v.description !== void 0 || v.section !== void 0, "at least one field");
|
|
251
570
|
var TemplateListQuery = PageQuery.extend({
|
|
252
|
-
archived:
|
|
571
|
+
archived: z4.enum(["true", "false"]).optional()
|
|
253
572
|
});
|
|
254
|
-
var TemplateVersion =
|
|
573
|
+
var TemplateVersion = z4.object({
|
|
255
574
|
template_id: Id,
|
|
256
|
-
version:
|
|
575
|
+
version: z4.number().int().min(1),
|
|
257
576
|
locale: LocaleTag,
|
|
258
577
|
subject: TemplateSubject.nullable(),
|
|
259
578
|
preheader: TemplatePreheader.nullable(),
|
|
260
579
|
document: TemplateDocument,
|
|
261
580
|
translations: TemplateTranslations,
|
|
262
|
-
|
|
581
|
+
insertions: Insertions,
|
|
582
|
+
created_by: z4.string().nullable(),
|
|
263
583
|
created_at: Timestamp
|
|
264
584
|
});
|
|
265
|
-
var TemplateVersionParams =
|
|
585
|
+
var TemplateVersionParams = z4.object({
|
|
266
586
|
id: Id,
|
|
267
|
-
version:
|
|
587
|
+
version: z4.coerce.number().int().min(1)
|
|
268
588
|
});
|
|
269
|
-
var CompileMessage =
|
|
270
|
-
message:
|
|
589
|
+
var CompileMessage = z4.object({
|
|
590
|
+
message: z4.string().min(1),
|
|
271
591
|
/** Where in the document or the MJML the message points, when known. */
|
|
272
|
-
path:
|
|
273
|
-
line:
|
|
592
|
+
path: z4.string().optional(),
|
|
593
|
+
line: z4.number().int().min(1).optional()
|
|
274
594
|
});
|
|
275
|
-
var CompileResult =
|
|
276
|
-
mjml:
|
|
277
|
-
html:
|
|
278
|
-
warnings:
|
|
279
|
-
errors:
|
|
595
|
+
var CompileResult = z4.object({
|
|
596
|
+
mjml: z4.string(),
|
|
597
|
+
html: z4.string(),
|
|
598
|
+
warnings: z4.array(CompileMessage),
|
|
599
|
+
errors: z4.array(CompileMessage)
|
|
280
600
|
});
|
|
281
|
-
var TemplateCompileRequest =
|
|
282
|
-
version:
|
|
601
|
+
var TemplateCompileRequest = z4.object({
|
|
602
|
+
version: z4.number().int().min(1).optional(),
|
|
283
603
|
locale: LocaleTag.optional()
|
|
284
604
|
});
|
|
285
|
-
var CompileRequest =
|
|
605
|
+
var CompileRequest = z4.object({
|
|
286
606
|
document: TemplateDocument
|
|
287
607
|
});
|
|
288
608
|
var ASSET_CONTENT_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
289
|
-
var AssetContentType =
|
|
609
|
+
var AssetContentType = z4.enum(ASSET_CONTENT_TYPES);
|
|
290
610
|
var MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
291
|
-
var Asset =
|
|
611
|
+
var Asset = z4.object({
|
|
292
612
|
id: Id,
|
|
293
|
-
url:
|
|
613
|
+
url: z4.string().url(),
|
|
294
614
|
content_type: AssetContentType,
|
|
295
|
-
size_bytes:
|
|
296
|
-
width:
|
|
297
|
-
height:
|
|
298
|
-
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),
|
|
299
619
|
created_at: Timestamp
|
|
300
620
|
});
|
|
301
621
|
var ASSET_UPLOAD_FIELD = "file";
|
|
302
622
|
var MAX_IMPORT_URL_LENGTH = 2048;
|
|
303
|
-
var AssetImport =
|
|
304
|
-
url:
|
|
623
|
+
var AssetImport = z4.object({
|
|
624
|
+
url: z4.string().max(MAX_IMPORT_URL_LENGTH).url().refine((u) => {
|
|
305
625
|
try {
|
|
306
626
|
const url = new URL(u);
|
|
307
627
|
if (url.protocol === "https:") return true;
|
|
@@ -311,7 +631,7 @@ var AssetImport = z3.object({
|
|
|
311
631
|
}
|
|
312
632
|
}, "must be https (http only for localhost)"),
|
|
313
633
|
/** The stored file name; derived from the address when omitted. */
|
|
314
|
-
filename:
|
|
634
|
+
filename: z4.string().min(1).max(255).optional()
|
|
315
635
|
});
|
|
316
636
|
var MAX_MJML_IMPORT_BYTES = 512 * 1024;
|
|
317
637
|
var MAX_MJML_DEPTH = 32;
|
|
@@ -326,56 +646,56 @@ var MJML_IMPORT_REFUSALS = [
|
|
|
326
646
|
"too_complex",
|
|
327
647
|
"invalid_document"
|
|
328
648
|
];
|
|
329
|
-
var ImportWarning =
|
|
330
|
-
severity:
|
|
649
|
+
var ImportWarning = z4.object({
|
|
650
|
+
severity: z4.enum(["info", "warning"]),
|
|
331
651
|
/** Stable kind, e.g. `kept_as_html`, `unknown_component`, `remote_image_not_imported`. */
|
|
332
|
-
code:
|
|
652
|
+
code: z4.string().min(1).max(64),
|
|
333
653
|
/** Where in the source, e.g. `mj-body > mj-section[2] > mj-column[1] > mj-social[1]`. */
|
|
334
|
-
path:
|
|
335
|
-
line:
|
|
336
|
-
message:
|
|
337
|
-
fragment:
|
|
654
|
+
path: z4.string(),
|
|
655
|
+
line: z4.number().int().min(1).optional(),
|
|
656
|
+
message: z4.string().min(1),
|
|
657
|
+
fragment: z4.string().optional()
|
|
338
658
|
});
|
|
339
|
-
var RemoteImage =
|
|
340
|
-
url:
|
|
659
|
+
var RemoteImage = z4.object({
|
|
660
|
+
url: z4.string().min(1),
|
|
341
661
|
/** The element and attribute it is loaded from, e.g. `<img src>`. */
|
|
342
|
-
where:
|
|
662
|
+
where: z4.string()
|
|
343
663
|
});
|
|
344
|
-
var ImportedAsset =
|
|
345
|
-
source_url:
|
|
664
|
+
var ImportedAsset = z4.object({
|
|
665
|
+
source_url: z4.string().min(1),
|
|
346
666
|
asset_id: Id,
|
|
347
|
-
url:
|
|
667
|
+
url: z4.string().url()
|
|
348
668
|
});
|
|
349
|
-
var mjmlSource =
|
|
350
|
-
var TemplateImportPreviewRequest =
|
|
351
|
-
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({
|
|
352
672
|
document: TemplateDocument,
|
|
353
|
-
warnings:
|
|
673
|
+
warnings: z4.array(ImportWarning),
|
|
354
674
|
/** The document compiled under the workspace's asset policy, exactly as a send would. */
|
|
355
675
|
compiled: CompileResult,
|
|
356
676
|
/** Images loaded from outside the service; under `service_only` each is also a compile error. */
|
|
357
|
-
remote_images:
|
|
358
|
-
asset_policy:
|
|
677
|
+
remote_images: z4.array(RemoteImage),
|
|
678
|
+
asset_policy: z4.enum(["any", "service_only"])
|
|
359
679
|
});
|
|
360
|
-
var TemplateImport =
|
|
361
|
-
name:
|
|
362
|
-
description:
|
|
680
|
+
var TemplateImport = z4.object({
|
|
681
|
+
name: z4.string().min(1).max(200),
|
|
682
|
+
description: z4.string().max(2e3).optional(),
|
|
363
683
|
mjml: mjmlSource,
|
|
364
|
-
import_remote_assets:
|
|
684
|
+
import_remote_assets: z4.boolean().optional()
|
|
365
685
|
});
|
|
366
|
-
var TemplateImportResult =
|
|
686
|
+
var TemplateImportResult = z4.object({
|
|
367
687
|
template: Template,
|
|
368
|
-
warnings:
|
|
369
|
-
imported_assets:
|
|
688
|
+
warnings: z4.array(ImportWarning),
|
|
689
|
+
imported_assets: z4.array(ImportedAsset)
|
|
370
690
|
});
|
|
371
691
|
var EXPORT_FORMATS = ["mjml", "html"];
|
|
372
|
-
var ExportFormat =
|
|
373
|
-
var TemplateExportQuery =
|
|
692
|
+
var ExportFormat = z4.enum(EXPORT_FORMATS);
|
|
693
|
+
var TemplateExportQuery = z4.object({
|
|
374
694
|
format: ExportFormat,
|
|
375
|
-
version:
|
|
695
|
+
version: z4.coerce.number().int().min(1).optional(),
|
|
376
696
|
locale: LocaleTag.optional()
|
|
377
697
|
});
|
|
378
|
-
var MailingExportQuery =
|
|
698
|
+
var MailingExportQuery = z4.object({ format: ExportFormat });
|
|
379
699
|
var EXPORT_CONTENT_TYPES = {
|
|
380
700
|
mjml: "text/plain; charset=utf-8",
|
|
381
701
|
html: "text/html; charset=utf-8"
|
|
@@ -394,7 +714,7 @@ function formatExportWarningsHeader(messages) {
|
|
|
394
714
|
function parseExportWarningsHeader(value) {
|
|
395
715
|
if (!value) return [];
|
|
396
716
|
try {
|
|
397
|
-
const parsed =
|
|
717
|
+
const parsed = z4.array(CompileMessage).safeParse(JSON.parse(decodeURIComponent(value)));
|
|
398
718
|
return parsed.success ? parsed.data : [];
|
|
399
719
|
} catch {
|
|
400
720
|
return [];
|
|
@@ -406,32 +726,32 @@ function exportFilename(name, format) {
|
|
|
406
726
|
}
|
|
407
727
|
|
|
408
728
|
// src/platform.ts
|
|
409
|
-
import { z as
|
|
729
|
+
import { z as z7 } from "zod";
|
|
410
730
|
|
|
411
731
|
// src/mailings.ts
|
|
412
|
-
import { z as
|
|
732
|
+
import { z as z6 } from "zod";
|
|
413
733
|
|
|
414
734
|
// src/ab-test.ts
|
|
415
|
-
import { z as
|
|
416
|
-
var AbVariant =
|
|
417
|
-
key:
|
|
418
|
-
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(),
|
|
419
739
|
/** Replaces the mailing's document for this variant. */
|
|
420
|
-
document:
|
|
740
|
+
document: z5.record(z5.unknown()).optional(),
|
|
421
741
|
/**
|
|
422
742
|
* Keeps the document this variant key already has in the mailing's current
|
|
423
743
|
* test, so a test can be changed without sending every document again. The
|
|
424
744
|
* service refuses it (`validation_failed`) when that variant has none.
|
|
425
745
|
*/
|
|
426
|
-
keep_document:
|
|
746
|
+
keep_document: z5.literal(true).optional()
|
|
427
747
|
});
|
|
428
748
|
var AB_WINNER_METRICS = ["opens", "clicks", "manual"];
|
|
429
|
-
var AbTestConfig =
|
|
430
|
-
variants:
|
|
749
|
+
var AbTestConfig = z5.object({
|
|
750
|
+
variants: z5.array(AbVariant).min(2).max(5),
|
|
431
751
|
/** Share of recipients in the test group, split evenly across variants; the rest get the winner. */
|
|
432
|
-
test_fraction:
|
|
433
|
-
winner_metric:
|
|
434
|
-
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()
|
|
435
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), {
|
|
436
756
|
message: "every variant changes the subject, the document or both",
|
|
437
757
|
path: ["variants"]
|
|
@@ -443,19 +763,19 @@ var AbTestConfig = z4.object({
|
|
|
443
763
|
path: ["decide_after_minutes"]
|
|
444
764
|
});
|
|
445
765
|
var AB_TEST_STATUSES = ["pending", "testing", "awaiting_pick", "decided"];
|
|
446
|
-
var AbTestState =
|
|
447
|
-
variants:
|
|
448
|
-
test_fraction:
|
|
449
|
-
winner_metric:
|
|
450
|
-
decide_after_minutes:
|
|
451
|
-
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),
|
|
452
772
|
/** When the metric decides; null for manual or before the start. */
|
|
453
773
|
decide_at: Timestamp.nullable(),
|
|
454
|
-
winner:
|
|
455
|
-
decided_by:
|
|
774
|
+
winner: z5.string().nullable(),
|
|
775
|
+
decided_by: z5.enum(["metric", "manual"]).nullable(),
|
|
456
776
|
decided_at: Timestamp.nullable()
|
|
457
777
|
});
|
|
458
|
-
var AbWinnerRequest =
|
|
778
|
+
var AbWinnerRequest = z5.object({ variant: z5.string().regex(/^[a-z]$/) });
|
|
459
779
|
function testGroupSize(total, fraction, variants) {
|
|
460
780
|
return Math.min(total, Math.max(variants, Math.ceil(total * fraction)));
|
|
461
781
|
}
|
|
@@ -470,9 +790,9 @@ var MAILING_STATUSES = [
|
|
|
470
790
|
"partially_failed",
|
|
471
791
|
"cancelled"
|
|
472
792
|
];
|
|
473
|
-
var MailingStatus =
|
|
793
|
+
var MailingStatus = z6.enum(MAILING_STATUSES);
|
|
474
794
|
var MAILING_ACTIONS = ["send", "schedule", "unschedule", "pause", "resume", "cancel", "retry-failed"];
|
|
475
|
-
var MailingAction =
|
|
795
|
+
var MailingAction = z6.enum(MAILING_ACTIONS);
|
|
476
796
|
var MAILING_TRANSITIONS = {
|
|
477
797
|
draft: ["send", "schedule", "cancel"],
|
|
478
798
|
scheduled: ["send", "schedule", "unschedule", "cancel"],
|
|
@@ -487,23 +807,23 @@ function canTransition(status, action) {
|
|
|
487
807
|
}
|
|
488
808
|
var EDITABLE_MAILING_STATUSES = ["draft", "scheduled"];
|
|
489
809
|
var TERMINAL_MAILING_STATUSES = ["sent", "cancelled"];
|
|
490
|
-
var MailingMetadata =
|
|
491
|
-
var MailingCounts =
|
|
492
|
-
total:
|
|
493
|
-
queued:
|
|
494
|
-
sending:
|
|
495
|
-
sent:
|
|
496
|
-
failed:
|
|
497
|
-
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)
|
|
498
818
|
});
|
|
499
819
|
var MAILING_KINDS = ["broadcast", "automation", "notification"];
|
|
500
|
-
var MailingKind =
|
|
501
|
-
var Mailing =
|
|
820
|
+
var MailingKind = z6.enum(MAILING_KINDS);
|
|
821
|
+
var Mailing = z6.object({
|
|
502
822
|
id: Id,
|
|
503
823
|
kind: MailingKind,
|
|
504
|
-
name:
|
|
505
|
-
subject:
|
|
506
|
-
preheader:
|
|
824
|
+
name: z6.string().max(200).nullable(),
|
|
825
|
+
subject: z6.string().min(1).max(998),
|
|
826
|
+
preheader: z6.string().max(500).nullable(),
|
|
507
827
|
/**
|
|
508
828
|
* The template the document was taken from, if any. The snapshot is what is
|
|
509
829
|
* sent in the main language; the template's other language versions are
|
|
@@ -548,15 +868,15 @@ var Mailing = z5.object({
|
|
|
548
868
|
* recipients refused as dead addresses in one run (see the provider's
|
|
549
869
|
* `rejections.anomaly`). Resuming clears it and starts a new run.
|
|
550
870
|
*/
|
|
551
|
-
pause_reason:
|
|
871
|
+
pause_reason: z6.string().nullable().default(null),
|
|
552
872
|
created_at: Timestamp,
|
|
553
873
|
updated_at: Timestamp
|
|
554
874
|
});
|
|
555
875
|
var MailingSummary = Mailing.omit({ document: true });
|
|
556
876
|
var MailingContent = {
|
|
557
|
-
name:
|
|
558
|
-
subject:
|
|
559
|
-
preheader:
|
|
877
|
+
name: z6.string().max(200).optional(),
|
|
878
|
+
subject: z6.string().min(1).max(998),
|
|
879
|
+
preheader: z6.string().max(500).optional(),
|
|
560
880
|
/** See `Mailing.send_locale`. */
|
|
561
881
|
send_locale: LocaleTag.nullable().optional(),
|
|
562
882
|
/**
|
|
@@ -569,7 +889,7 @@ var MailingContent = {
|
|
|
569
889
|
provider_id: Id,
|
|
570
890
|
metadata: MailingMetadata.optional()
|
|
571
891
|
};
|
|
572
|
-
var MailingCreate =
|
|
892
|
+
var MailingCreate = z6.object({
|
|
573
893
|
...MailingContent,
|
|
574
894
|
subject: MailingContent.subject.optional(),
|
|
575
895
|
document: TemplateDocument.optional(),
|
|
@@ -581,10 +901,10 @@ var MailingCreate = z5.object({
|
|
|
581
901
|
message: "a mailing made from a document needs a subject",
|
|
582
902
|
path: ["subject"]
|
|
583
903
|
});
|
|
584
|
-
var MailingUpdate =
|
|
585
|
-
name:
|
|
904
|
+
var MailingUpdate = z6.object({
|
|
905
|
+
name: z6.string().max(200).nullable().optional(),
|
|
586
906
|
subject: MailingContent.subject.optional(),
|
|
587
|
-
preheader:
|
|
907
|
+
preheader: z6.string().max(500).nullable().optional(),
|
|
588
908
|
/** Null turns a draft into a letter, which it can only be with at most one recipient. */
|
|
589
909
|
topic: Slug.nullable().optional(),
|
|
590
910
|
send_locale: LocaleTag.nullable().optional(),
|
|
@@ -598,30 +918,30 @@ var MailingListQuery = PageQuery.extend({
|
|
|
598
918
|
kind: MailingKind.optional(),
|
|
599
919
|
automation_id: Id.optional()
|
|
600
920
|
});
|
|
601
|
-
var MailingSendRequest =
|
|
602
|
-
var MailingRetryFailedRequest =
|
|
921
|
+
var MailingSendRequest = z6.object({}).strict();
|
|
922
|
+
var MailingRetryFailedRequest = z6.object({
|
|
603
923
|
/**
|
|
604
924
|
* Also requeue recipients whose outcome is unknown after a crash. They may
|
|
605
925
|
* already have received the message, so this is a human's decision, never a
|
|
606
926
|
* default.
|
|
607
927
|
*/
|
|
608
|
-
include_outcome_unknown:
|
|
928
|
+
include_outcome_unknown: z6.boolean().optional()
|
|
609
929
|
}).strict();
|
|
610
|
-
var MailingActionRequest =
|
|
611
|
-
var MailingTestRequest =
|
|
930
|
+
var MailingActionRequest = z6.object({}).strict();
|
|
931
|
+
var MailingTestRequest = z6.object({
|
|
612
932
|
to: Email,
|
|
613
933
|
merge: Properties.optional(),
|
|
614
934
|
locale: LocaleTag.optional()
|
|
615
935
|
});
|
|
616
|
-
var MailingTestResult =
|
|
936
|
+
var MailingTestResult = z6.object({
|
|
617
937
|
message_id: Id,
|
|
618
|
-
provider_message_id:
|
|
938
|
+
provider_message_id: z6.string().nullable()
|
|
619
939
|
});
|
|
620
940
|
var RECIPIENT_STATUSES = ["queued", "sending", "sent", "failed", "skipped"];
|
|
621
|
-
var RecipientStatus =
|
|
941
|
+
var RecipientStatus = z6.enum(RECIPIENT_STATUSES);
|
|
622
942
|
var SKIP_REASONS = ["suppressed", "not_subscribed", "contact_erased", "cancelled", "outcome_unknown"];
|
|
623
|
-
var SkipReason =
|
|
624
|
-
var Recipient =
|
|
943
|
+
var SkipReason = z6.enum(SKIP_REASONS);
|
|
944
|
+
var Recipient = z6.object({
|
|
625
945
|
id: Id,
|
|
626
946
|
mailing_id: Id,
|
|
627
947
|
contact_id: Id.nullable(),
|
|
@@ -629,75 +949,98 @@ var Recipient = z5.object({
|
|
|
629
949
|
merge: Properties,
|
|
630
950
|
status: RecipientStatus,
|
|
631
951
|
skip_reason: SkipReason.nullable(),
|
|
632
|
-
attempts:
|
|
952
|
+
attempts: z6.number().int().min(0),
|
|
633
953
|
message_id: Id.nullable(),
|
|
634
|
-
last_error:
|
|
954
|
+
last_error: z6.string().nullable(),
|
|
635
955
|
created_at: Timestamp,
|
|
636
956
|
updated_at: Timestamp
|
|
637
957
|
});
|
|
638
958
|
var MAX_RECIPIENTS_PER_BATCH = 1e3;
|
|
639
959
|
var MAX_LETTER_RECIPIENTS = 1;
|
|
640
960
|
var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
|
|
641
|
-
var RecipientInput =
|
|
961
|
+
var RecipientInput = z6.object({
|
|
642
962
|
contact_id: Id.optional(),
|
|
643
|
-
external_id:
|
|
963
|
+
external_id: z6.string().min(1).max(255).optional(),
|
|
644
964
|
email: Email.optional(),
|
|
645
965
|
merge: Properties.optional()
|
|
646
966
|
}).refine((v) => v.contact_id !== void 0 || v.external_id !== void 0 || v.email !== void 0, {
|
|
647
967
|
message: "contact_id, external_id or email is required",
|
|
648
968
|
path: ["email"]
|
|
649
969
|
});
|
|
650
|
-
var RecipientBatch =
|
|
651
|
-
recipients:
|
|
970
|
+
var RecipientBatch = z6.object({
|
|
971
|
+
recipients: z6.array(RecipientInput).min(1).max(MAX_RECIPIENTS_PER_BATCH)
|
|
652
972
|
});
|
|
653
973
|
var RECIPIENT_REJECTIONS = ["unknown_contact", "duplicate_in_batch"];
|
|
654
|
-
var RecipientBatchResult =
|
|
974
|
+
var RecipientBatchResult = z6.object({
|
|
655
975
|
/** Newly queued. */
|
|
656
|
-
added:
|
|
976
|
+
added: z6.number().int().min(0),
|
|
657
977
|
/** Already on the mailing from an earlier batch: left unchanged. */
|
|
658
|
-
already_present:
|
|
978
|
+
already_present: z6.number().int().min(0),
|
|
659
979
|
/** Items not added, by their index in the request. */
|
|
660
|
-
rejected:
|
|
980
|
+
rejected: z6.array(z6.object({ index: z6.number().int().min(0), reason: z6.enum(RECIPIENT_REJECTIONS) }))
|
|
661
981
|
});
|
|
662
982
|
var RecipientListQuery = PageQuery.extend({
|
|
663
983
|
status: RecipientStatus.optional(),
|
|
664
984
|
skip_reason: SkipReason.optional()
|
|
665
985
|
});
|
|
666
986
|
var MESSAGE_OUTCOMES = ["sent", "failed"];
|
|
667
|
-
var MessageOutcome =
|
|
668
|
-
var MessageSummary =
|
|
987
|
+
var MessageOutcome = z6.enum(MESSAGE_OUTCOMES);
|
|
988
|
+
var MessageSummary = z6.object({
|
|
669
989
|
id: Id,
|
|
670
990
|
mailing_id: Id.nullable(),
|
|
671
991
|
recipient_id: Id.nullable(),
|
|
672
992
|
contact_id: Id.nullable(),
|
|
673
993
|
to: Email,
|
|
674
|
-
subject:
|
|
994
|
+
subject: z6.string(),
|
|
675
995
|
provider_id: Id,
|
|
676
|
-
provider_message_id:
|
|
996
|
+
provider_message_id: z6.string().nullable(),
|
|
677
997
|
outcome: MessageOutcome,
|
|
678
|
-
error:
|
|
679
|
-
is_test:
|
|
680
|
-
recipient_count:
|
|
998
|
+
error: z6.string().nullable(),
|
|
999
|
+
is_test: z6.boolean(),
|
|
1000
|
+
recipient_count: z6.number().int().min(1),
|
|
681
1001
|
/** The language version the message was sent in; null for messages archived before languages existed. */
|
|
682
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(),
|
|
683
1010
|
created_at: Timestamp
|
|
684
1011
|
});
|
|
685
|
-
var Message = MessageSummary.extend({ html:
|
|
686
|
-
var MailingLanguageStatus =
|
|
687
|
-
var MailingLanguage =
|
|
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({
|
|
688
1015
|
locale: LocaleTag,
|
|
689
1016
|
status: MailingLanguageStatus,
|
|
690
1017
|
/** Queued recipients who will get this version. */
|
|
691
|
-
planned:
|
|
1018
|
+
planned: z6.number().int().min(0),
|
|
692
1019
|
/** Messages already sent in this language (tests excluded). */
|
|
693
|
-
sent:
|
|
1020
|
+
sent: z6.number().int().min(0)
|
|
694
1021
|
});
|
|
695
|
-
var MailingLanguageReport =
|
|
1022
|
+
var MailingLanguageReport = z6.object({
|
|
696
1023
|
main_locale: LocaleTag,
|
|
697
1024
|
send_locale: LocaleTag.nullable(),
|
|
698
|
-
languages:
|
|
699
|
-
fallback:
|
|
700
|
-
no_language:
|
|
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
|
+
)
|
|
701
1044
|
});
|
|
702
1045
|
var MessageListQuery = PageQuery.extend({
|
|
703
1046
|
mailing_id: Id.optional(),
|
|
@@ -705,26 +1048,26 @@ var MessageListQuery = PageQuery.extend({
|
|
|
705
1048
|
});
|
|
706
1049
|
|
|
707
1050
|
// src/platform.ts
|
|
708
|
-
var Tag =
|
|
1051
|
+
var Tag = z7.object({
|
|
709
1052
|
id: Id,
|
|
710
1053
|
slug: Slug,
|
|
711
|
-
name:
|
|
712
|
-
contact_count:
|
|
1054
|
+
name: z7.string().min(1).max(120),
|
|
1055
|
+
contact_count: z7.number().int().min(0),
|
|
713
1056
|
created_at: Timestamp
|
|
714
1057
|
});
|
|
715
|
-
var TagCreate =
|
|
716
|
-
var TagAssignment =
|
|
717
|
-
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)
|
|
718
1061
|
});
|
|
719
1062
|
var CONTACT_PROPERTY_TYPES = ["string", "number", "boolean", "date"];
|
|
720
|
-
var ContactPropertyType =
|
|
721
|
-
var ContactPropertyKey =
|
|
722
|
-
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({
|
|
723
1066
|
key: ContactPropertyKey,
|
|
724
|
-
label:
|
|
1067
|
+
label: z7.string().min(1).max(120),
|
|
725
1068
|
type: ContactPropertyType
|
|
726
1069
|
});
|
|
727
|
-
var ContactPropertyParams =
|
|
1070
|
+
var ContactPropertyParams = z7.object({ key: ContactPropertyKey });
|
|
728
1071
|
var FILTER_OPERATORS = [
|
|
729
1072
|
"eq",
|
|
730
1073
|
"neq",
|
|
@@ -740,8 +1083,8 @@ var FILTER_OPERATORS = [
|
|
|
740
1083
|
"in",
|
|
741
1084
|
"not_in"
|
|
742
1085
|
];
|
|
743
|
-
var FilterOperator =
|
|
744
|
-
var FilterField =
|
|
1086
|
+
var FilterOperator = z7.enum(FILTER_OPERATORS);
|
|
1087
|
+
var FilterField = z7.string().regex(
|
|
745
1088
|
/^(email|first_name|last_name|locale|created_at|tag|topic|engagement:(opened|clicked)|property:[A-Za-z0-9_.-]{1,64})$/,
|
|
746
1089
|
"unknown filter field"
|
|
747
1090
|
);
|
|
@@ -757,18 +1100,18 @@ var FILTER_FIELD_OPERATORS = {
|
|
|
757
1100
|
property: FILTER_OPERATORS
|
|
758
1101
|
};
|
|
759
1102
|
var MAX_FILTER_DEPTH = 5;
|
|
760
|
-
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), {
|
|
761
1104
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
762
1105
|
path: ["value"]
|
|
763
1106
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
764
1107
|
message: "in and not_in take an array",
|
|
765
1108
|
path: ["value"]
|
|
766
1109
|
});
|
|
767
|
-
var SegmentFilter =
|
|
768
|
-
() =>
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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(),
|
|
772
1115
|
FilterCondition
|
|
773
1116
|
])
|
|
774
1117
|
);
|
|
@@ -856,42 +1199,42 @@ function untypedValueProblem(c, every) {
|
|
|
856
1199
|
if (orderOp) return every(isFilterNumber, "numbers (define the property to compare dates)");
|
|
857
1200
|
return every((v) => isScalar(v) || v === null, "strings, numbers, booleans or null");
|
|
858
1201
|
}
|
|
859
|
-
var Segment =
|
|
1202
|
+
var Segment = z7.object({
|
|
860
1203
|
id: Id,
|
|
861
|
-
name:
|
|
1204
|
+
name: z7.string().min(1).max(120),
|
|
862
1205
|
filter: SegmentFilter,
|
|
863
1206
|
/** Computed on read. */
|
|
864
|
-
contact_count:
|
|
1207
|
+
contact_count: z7.number().int().min(0),
|
|
865
1208
|
created_at: Timestamp,
|
|
866
1209
|
updated_at: Timestamp
|
|
867
1210
|
});
|
|
868
|
-
var SegmentCreate =
|
|
869
|
-
var SegmentPreviewRequest =
|
|
870
|
-
var SegmentPreview =
|
|
871
|
-
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),
|
|
872
1215
|
/** Up to five matching contacts, for a human to sanity-check the filter. */
|
|
873
|
-
sample:
|
|
1216
|
+
sample: z7.array(z7.object({ id: Id, email: Email })).max(5)
|
|
874
1217
|
});
|
|
875
|
-
var MailingAudienceFromSegment =
|
|
1218
|
+
var MailingAudienceFromSegment = z7.object({ segment_id: Id });
|
|
876
1219
|
var SIGNUP_FORM_FIELDS = ["first_name", "last_name"];
|
|
877
1220
|
var HOSTED_PAGE_LOCALES = ["en", "de", "it", "fr", "es"];
|
|
878
|
-
var SignupFormTranslation =
|
|
879
|
-
title:
|
|
880
|
-
consent_text:
|
|
1221
|
+
var SignupFormTranslation = z7.object({
|
|
1222
|
+
title: z7.string().min(1).max(120),
|
|
1223
|
+
consent_text: z7.string().min(1).max(2e3)
|
|
881
1224
|
});
|
|
882
|
-
var SignupForm =
|
|
1225
|
+
var SignupForm = z7.object({
|
|
883
1226
|
id: Id,
|
|
884
|
-
name:
|
|
1227
|
+
name: z7.string().min(1).max(120),
|
|
885
1228
|
/** The heading of the hosted page. */
|
|
886
|
-
title:
|
|
1229
|
+
title: z7.string().min(1).max(120),
|
|
887
1230
|
/** What the person agrees to, shown above the button and recorded with the confirmation. */
|
|
888
|
-
consent_text:
|
|
889
|
-
translations:
|
|
890
|
-
topics:
|
|
891
|
-
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),
|
|
892
1235
|
/** Fields shown besides email. */
|
|
893
|
-
fields:
|
|
894
|
-
double_opt_in:
|
|
1236
|
+
fields: z7.array(z7.enum(SIGNUP_FORM_FIELDS)),
|
|
1237
|
+
double_opt_in: z7.literal(true),
|
|
895
1238
|
/** The provider the confirmation mail is sent through. */
|
|
896
1239
|
provider_id: Id,
|
|
897
1240
|
/**
|
|
@@ -900,11 +1243,11 @@ var SignupForm = z6.object({
|
|
|
900
1243
|
*/
|
|
901
1244
|
confirmation_template_id: Id.nullable(),
|
|
902
1245
|
/** Where the person lands after confirming. Null shows the hosted confirmation page. */
|
|
903
|
-
redirect_url:
|
|
1246
|
+
redirect_url: z7.string().url().nullable(),
|
|
904
1247
|
/** Origins allowed to embed the form and call the submission route from a browser. */
|
|
905
|
-
allowed_origins:
|
|
1248
|
+
allowed_origins: z7.array(z7.string().url()).max(20),
|
|
906
1249
|
/** Bumped on every change; recorded with each confirmation. */
|
|
907
|
-
version:
|
|
1250
|
+
version: z7.number().int().min(1),
|
|
908
1251
|
created_at: Timestamp,
|
|
909
1252
|
updated_at: Timestamp
|
|
910
1253
|
});
|
|
@@ -919,56 +1262,56 @@ var SignupFormCreate = SignupForm.omit({
|
|
|
919
1262
|
tags: SignupForm.shape.tags.optional(),
|
|
920
1263
|
fields: SignupForm.shape.fields.optional(),
|
|
921
1264
|
confirmation_template_id: Id.nullable().optional(),
|
|
922
|
-
redirect_url:
|
|
1265
|
+
redirect_url: z7.string().url().nullable().optional(),
|
|
923
1266
|
allowed_origins: SignupForm.shape.allowed_origins.optional()
|
|
924
1267
|
});
|
|
925
|
-
var SignupFormEmbed =
|
|
926
|
-
hosted_url:
|
|
1268
|
+
var SignupFormEmbed = z7.object({
|
|
1269
|
+
hosted_url: z7.string().url(),
|
|
927
1270
|
/** A plain HTML form posting to the hosted page; works without JavaScript. */
|
|
928
|
-
html:
|
|
1271
|
+
html: z7.string(),
|
|
929
1272
|
/** A small optional script that adds the time check and submits in place. */
|
|
930
|
-
script_url:
|
|
1273
|
+
script_url: z7.string().url(),
|
|
931
1274
|
/** Where a browser fetches a fresh `form_token` from (GET, public, CORS for `allowed_origins`). */
|
|
932
|
-
token_url:
|
|
1275
|
+
token_url: z7.string().url()
|
|
933
1276
|
});
|
|
934
|
-
var SignupSubmission =
|
|
1277
|
+
var SignupSubmission = z7.object({
|
|
935
1278
|
email: Email,
|
|
936
|
-
first_name:
|
|
937
|
-
last_name:
|
|
938
|
-
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(),
|
|
939
1282
|
/** Honeypot: must be empty. */
|
|
940
|
-
website:
|
|
1283
|
+
website: z7.string().max(0).optional(),
|
|
941
1284
|
/**
|
|
942
1285
|
* The signed render time from the form's token URL: a submission sooner than a
|
|
943
1286
|
* human could fill the form, or long after, is refused.
|
|
944
1287
|
*/
|
|
945
|
-
form_token:
|
|
1288
|
+
form_token: z7.string().min(1).max(512)
|
|
946
1289
|
});
|
|
947
1290
|
var IMPORT_STATUSES = ["uploaded", "validating", "validated", "committing", "completed", "failed", "cancelled"];
|
|
948
|
-
var ImportStatus =
|
|
1291
|
+
var ImportStatus = z7.enum(IMPORT_STATUSES);
|
|
949
1292
|
var IMPORT_TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
950
|
-
var ImportColumnMapping =
|
|
951
|
-
|
|
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)$/)
|
|
952
1295
|
);
|
|
953
1296
|
var IMPORT_FILE_FIELD = "file";
|
|
954
1297
|
var MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
|
955
1298
|
var MAX_IMPORT_ROWS = 2e5;
|
|
956
|
-
var ImportMappingRequest =
|
|
1299
|
+
var ImportMappingRequest = z7.object({
|
|
957
1300
|
/** CSV header -> contact field. Exactly one column maps to `email`. */
|
|
958
1301
|
mapping: ImportColumnMapping.refine(
|
|
959
1302
|
(m) => Object.values(m).filter((v) => v === "email").length === 1,
|
|
960
1303
|
"exactly one column maps to email"
|
|
961
1304
|
),
|
|
962
|
-
topics:
|
|
963
|
-
tags:
|
|
1305
|
+
topics: z7.array(Slug).max(100),
|
|
1306
|
+
tags: z7.array(Slug).max(100),
|
|
964
1307
|
/** Existing contacts get the mapped fields overwritten; false only adds topics, tags and missing fields. */
|
|
965
|
-
update_existing:
|
|
1308
|
+
update_existing: z7.boolean().optional(),
|
|
966
1309
|
/** The importer states the people consented; the service records who said so and when. */
|
|
967
|
-
consent_confirmed:
|
|
1310
|
+
consent_confirmed: z7.literal(true)
|
|
968
1311
|
});
|
|
969
|
-
var ImportCommitRequest =
|
|
1312
|
+
var ImportCommitRequest = z7.object({ mapping_version: z7.number().int().min(1) });
|
|
970
1313
|
var IMPORT_ROW_OUTCOMES = ["created", "updated", "unchanged", "suppressed", "skipped"];
|
|
971
|
-
var ImportRowOutcome =
|
|
1314
|
+
var ImportRowOutcome = z7.enum(IMPORT_ROW_OUTCOMES);
|
|
972
1315
|
var IMPORT_SKIP_REASONS = [
|
|
973
1316
|
"missing_email",
|
|
974
1317
|
"invalid_email",
|
|
@@ -977,90 +1320,90 @@ var IMPORT_SKIP_REASONS = [
|
|
|
977
1320
|
"external_id_conflict",
|
|
978
1321
|
"wrong_column_count"
|
|
979
1322
|
];
|
|
980
|
-
var ImportSkipReason =
|
|
981
|
-
var ImportReport =
|
|
982
|
-
created:
|
|
983
|
-
updated:
|
|
984
|
-
unchanged:
|
|
985
|
-
suppressed:
|
|
986
|
-
skipped:
|
|
987
|
-
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)),
|
|
988
1331
|
/** Rows whose contact was subscribed without some topic, because the address is blocked for it. */
|
|
989
|
-
topics_withheld:
|
|
1332
|
+
topics_withheld: z7.number().int().min(0)
|
|
990
1333
|
});
|
|
991
|
-
var ImportJob =
|
|
1334
|
+
var ImportJob = z7.object({
|
|
992
1335
|
id: Id,
|
|
993
1336
|
status: ImportStatus,
|
|
994
|
-
file_name:
|
|
995
|
-
file_bytes:
|
|
996
|
-
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),
|
|
997
1340
|
/** The header row, as in the file. */
|
|
998
|
-
columns:
|
|
1341
|
+
columns: z7.array(z7.string()),
|
|
999
1342
|
/** The first rows, for the mapping screen. */
|
|
1000
|
-
sample:
|
|
1343
|
+
sample: z7.array(z7.array(z7.string())).max(5),
|
|
1001
1344
|
/** A mapping guessed from the headers, for the mapping screen to start from. */
|
|
1002
1345
|
suggested_mapping: ImportColumnMapping,
|
|
1003
1346
|
mapping: ImportColumnMapping.nullable(),
|
|
1004
|
-
mapping_version:
|
|
1005
|
-
topics:
|
|
1006
|
-
tags:
|
|
1007
|
-
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(),
|
|
1008
1351
|
/** The dry run of the current mapping; null before one finished. */
|
|
1009
1352
|
dry_run: ImportReport.nullable(),
|
|
1010
1353
|
/** What the commit has written so far; null before it started. */
|
|
1011
1354
|
result: ImportReport.nullable(),
|
|
1012
1355
|
/** Rows the current phase (dry run or commit) has processed. */
|
|
1013
|
-
processed_rows:
|
|
1356
|
+
processed_rows: z7.number().int().min(0),
|
|
1014
1357
|
/** Why a `failed` import failed. */
|
|
1015
|
-
error:
|
|
1358
|
+
error: z7.string().nullable(),
|
|
1016
1359
|
created_at: Timestamp,
|
|
1017
1360
|
updated_at: Timestamp,
|
|
1018
1361
|
finished_at: Timestamp.nullable()
|
|
1019
1362
|
});
|
|
1020
|
-
var ImportRow =
|
|
1363
|
+
var ImportRow = z7.object({
|
|
1021
1364
|
/** The line in the file, 1 being the first line after the header. */
|
|
1022
|
-
row:
|
|
1023
|
-
email:
|
|
1365
|
+
row: z7.number().int().min(1),
|
|
1366
|
+
email: z7.string().nullable(),
|
|
1024
1367
|
outcome: ImportRowOutcome,
|
|
1025
1368
|
reason: ImportSkipReason.nullable(),
|
|
1026
1369
|
/** A human explanation for a skipped row (which value, which column). */
|
|
1027
|
-
message:
|
|
1370
|
+
message: z7.string().nullable(),
|
|
1028
1371
|
contact_id: Id.nullable()
|
|
1029
1372
|
});
|
|
1030
1373
|
var ImportRowListQuery = PageQuery.extend({ outcome: ImportRowOutcome.optional() });
|
|
1031
|
-
var MailingScheduleRequest =
|
|
1032
|
-
var TrackingSettings =
|
|
1033
|
-
var VariantAnalytics =
|
|
1034
|
-
key:
|
|
1035
|
-
sent:
|
|
1036
|
-
unique_opens:
|
|
1037
|
-
unique_clicks:
|
|
1038
|
-
});
|
|
1039
|
-
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({
|
|
1040
1383
|
mailing_id: Id,
|
|
1041
1384
|
counts: MailingCounts,
|
|
1042
1385
|
/** What was tracked for this mailing, fixed when it started; null before it started. */
|
|
1043
1386
|
tracking: TrackingSettings.nullable(),
|
|
1044
1387
|
/** Unique recipients with a human open. Null when opens were not tracked. */
|
|
1045
|
-
unique_opens:
|
|
1388
|
+
unique_opens: z7.number().int().min(0).nullable(),
|
|
1046
1389
|
/**
|
|
1047
1390
|
* Recipients whose only opens came from Apple Mail Privacy Protection, which
|
|
1048
1391
|
* loads every image on delivery: counted apart, never as human opens.
|
|
1049
1392
|
*/
|
|
1050
|
-
apple_mpp_opens:
|
|
1393
|
+
apple_mpp_opens: z7.number().int().min(0).nullable(),
|
|
1051
1394
|
/** Opens and clicks from security scanners and prefetchers, filtered out of the unique counts. */
|
|
1052
|
-
machine_events:
|
|
1053
|
-
unique_clicks:
|
|
1054
|
-
unsubscribes:
|
|
1055
|
-
bounces:
|
|
1056
|
-
complaints:
|
|
1057
|
-
links:
|
|
1058
|
-
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()
|
|
1059
1402
|
});
|
|
1060
1403
|
|
|
1061
1404
|
// src/automations.ts
|
|
1062
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})$/;
|
|
1063
|
-
var AutomationFilterField =
|
|
1406
|
+
var AutomationFilterField = z8.string().regex(AUTOMATION_FILTER_FIELD_PATTERN, "unknown filter field");
|
|
1064
1407
|
var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
1065
1408
|
...FILTER_FIELD_OPERATORS,
|
|
1066
1409
|
segment: ["eq", "neq", "in", "not_in"],
|
|
@@ -1070,18 +1413,18 @@ var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
|
1070
1413
|
step: ["eq", "neq"]
|
|
1071
1414
|
};
|
|
1072
1415
|
var automationFilterFamily = filterFieldFamily;
|
|
1073
|
-
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), {
|
|
1074
1417
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
1075
1418
|
path: ["value"]
|
|
1076
1419
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
1077
1420
|
message: "in and not_in take an array",
|
|
1078
1421
|
path: ["value"]
|
|
1079
1422
|
});
|
|
1080
|
-
var AutomationFilter =
|
|
1081
|
-
() =>
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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(),
|
|
1085
1428
|
AutomationFilterCondition
|
|
1086
1429
|
])
|
|
1087
1430
|
);
|
|
@@ -1094,20 +1437,20 @@ function filterConditions(filter) {
|
|
|
1094
1437
|
if ("not" in filter) return filterConditions(filter.not);
|
|
1095
1438
|
return [filter];
|
|
1096
1439
|
}
|
|
1097
|
-
var StepId =
|
|
1098
|
-
var TimeOfDay =
|
|
1099
|
-
var Weekday =
|
|
1100
|
-
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");
|
|
1101
1444
|
var MAX_DELAY_SECONDS = 365 * 24 * 60 * 60;
|
|
1102
|
-
var DateOnly =
|
|
1445
|
+
var DateOnly = z8.string().regex(/^\d{4}-\d{2}-\d{2}$/, "a calendar date such as 2026-09-20").refine((v) => {
|
|
1103
1446
|
const [y, m, d] = v.split("-").map(Number);
|
|
1104
1447
|
const t = new Date(Date.UTC(y, m - 1, d));
|
|
1105
1448
|
return t.getUTCFullYear() === y && t.getUTCMonth() + 1 === m && t.getUTCDate() === d;
|
|
1106
1449
|
}, "a real calendar date");
|
|
1107
|
-
var OffsetDays =
|
|
1450
|
+
var OffsetDays = z8.number().int().min(-365).max(365);
|
|
1108
1451
|
var DATE_PASSED_ANSWERS = ["skip", "stop", "continue"];
|
|
1109
|
-
var DatePassedAnswer =
|
|
1110
|
-
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");
|
|
1111
1454
|
var AUTOMATION_TRIGGER_KINDS = [
|
|
1112
1455
|
"topic_subscribed",
|
|
1113
1456
|
"form_confirmed",
|
|
@@ -1120,17 +1463,17 @@ var AUTOMATION_TRIGGER_KINDS = [
|
|
|
1120
1463
|
"segment_entered",
|
|
1121
1464
|
"link_clicked"
|
|
1122
1465
|
];
|
|
1123
|
-
var AutomationTriggerKind =
|
|
1466
|
+
var AutomationTriggerKind = z8.enum(AUTOMATION_TRIGGER_KINDS);
|
|
1124
1467
|
var exclude = { exclude: BoundedAutomationFilter.nullish() };
|
|
1125
|
-
var AutomationTrigger =
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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(),
|
|
1129
1472
|
/** `to` given: only a change that lands on this value fires. Absent: any change of the key. */
|
|
1130
|
-
|
|
1473
|
+
z8.object({ kind: z8.literal("property_changed"), key: ContactPropertyKey, to: JsonValue.optional(), ...exclude }).strict(),
|
|
1131
1474
|
/** `where` filters the event's own data, so only `event:<key>` leaves belong in it. */
|
|
1132
|
-
|
|
1133
|
-
|
|
1475
|
+
z8.object({ kind: z8.literal("event"), name: EventName, where: BoundedAutomationFilter.nullish(), ...exclude }).strict(),
|
|
1476
|
+
z8.object({ kind: z8.literal("manual"), ...exclude }).strict(),
|
|
1134
1477
|
/**
|
|
1135
1478
|
* Every year on a date the contact carries: a birthday, a joining date, a
|
|
1136
1479
|
* renewal. `offset_days` moves it ("three days before"), `at` is the wall
|
|
@@ -1140,8 +1483,8 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1140
1483
|
* A 29 February date fires on 1 March in a year that has no 29 February, so
|
|
1141
1484
|
* nobody is skipped every fourth year.
|
|
1142
1485
|
*/
|
|
1143
|
-
|
|
1144
|
-
kind:
|
|
1486
|
+
z8.object({
|
|
1487
|
+
kind: z8.literal("date_anniversary"),
|
|
1145
1488
|
key: ContactPropertyKey,
|
|
1146
1489
|
offset_days: OffsetDays,
|
|
1147
1490
|
at: TimeOfDay,
|
|
@@ -1153,8 +1496,8 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1153
1496
|
* not ask about a segment or about an event's data: there is no event, and a
|
|
1154
1497
|
* segment is its own trigger kind.
|
|
1155
1498
|
*/
|
|
1156
|
-
|
|
1157
|
-
kind:
|
|
1499
|
+
z8.object({
|
|
1500
|
+
kind: z8.literal("exact_date"),
|
|
1158
1501
|
on: DateOnly,
|
|
1159
1502
|
at: TimeOfDay,
|
|
1160
1503
|
where: BoundedAutomationFilter.nullish(),
|
|
@@ -1173,7 +1516,7 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1173
1516
|
* already exists. Entering those people as well is the publish dialog's
|
|
1174
1517
|
* backfill question, answered by a person rather than by the sweep.
|
|
1175
1518
|
*/
|
|
1176
|
-
|
|
1519
|
+
z8.object({ kind: z8.literal("segment_entered"), segment_id: Id, ...exclude }).strict(),
|
|
1177
1520
|
/**
|
|
1178
1521
|
* S4. When somebody clicks a link in a mailing that has already gone out.
|
|
1179
1522
|
*
|
|
@@ -1183,7 +1526,7 @@ var AutomationTrigger = z7.discriminatedUnion("kind", [
|
|
|
1183
1526
|
* tracking off there is no click to hear about, so the validator refuses it
|
|
1184
1527
|
* rather than leaving an automation that can never fire.
|
|
1185
1528
|
*/
|
|
1186
|
-
|
|
1529
|
+
z8.object({ kind: z8.literal("link_clicked"), mailing_id: Id, url: z8.string().min(1).max(2048).optional(), ...exclude }).strict()
|
|
1187
1530
|
]);
|
|
1188
1531
|
var AUTOMATION_DATE_TRIGGER_KINDS = ["date_anniversary", "exact_date"];
|
|
1189
1532
|
var MAX_TRIGGERS = 10;
|
|
@@ -1202,41 +1545,45 @@ var AUTOMATION_STEP_KINDS = [
|
|
|
1202
1545
|
"webhook",
|
|
1203
1546
|
"notify"
|
|
1204
1547
|
];
|
|
1205
|
-
var AutomationStepKind =
|
|
1548
|
+
var AutomationStepKind = z8.enum(AUTOMATION_STEP_KINDS);
|
|
1206
1549
|
var next = { next: StepId.nullable() };
|
|
1207
|
-
var Unchosen =
|
|
1208
|
-
var DraftSlug =
|
|
1209
|
-
var DraftPropertyKey =
|
|
1210
|
-
var label = { label:
|
|
1211
|
-
var EmailStepLanguage =
|
|
1212
|
-
subject:
|
|
1213
|
-
preheader:
|
|
1214
|
-
document: TemplateDocument
|
|
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()
|
|
1215
1560
|
}).strict();
|
|
1216
|
-
var EmailStep =
|
|
1217
|
-
kind:
|
|
1561
|
+
var EmailStep = z8.object({
|
|
1562
|
+
kind: z8.literal("email"),
|
|
1218
1563
|
...label,
|
|
1219
1564
|
...next,
|
|
1220
1565
|
template_id: Id.nullish(),
|
|
1221
1566
|
document: TemplateDocument.nullish(),
|
|
1222
1567
|
/** Empty while the step is being written, or when its template has a subject; else `not_chosen` refuses to publish it. */
|
|
1223
|
-
subject:
|
|
1224
|
-
preheader:
|
|
1568
|
+
subject: z8.string().max(998),
|
|
1569
|
+
preheader: z8.string().max(500).nullish(),
|
|
1225
1570
|
locale: LocaleTag.nullish(),
|
|
1226
|
-
languages:
|
|
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()
|
|
1227
1574
|
}).strict();
|
|
1228
|
-
var DelayStep =
|
|
1229
|
-
kind:
|
|
1575
|
+
var DelayStep = z8.object({
|
|
1576
|
+
kind: z8.literal("delay"),
|
|
1230
1577
|
...label,
|
|
1231
1578
|
...next,
|
|
1232
|
-
wait:
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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(),
|
|
1236
1583
|
/** A fixed day has no next occurrence, so `continue` is not one of its answers. */
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
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"),
|
|
1240
1587
|
key: ContactPropertyKey,
|
|
1241
1588
|
offset_days: OffsetDays,
|
|
1242
1589
|
at: TimeOfDay,
|
|
@@ -1244,8 +1591,8 @@ var DelayStep = z7.object({
|
|
|
1244
1591
|
}).strict()
|
|
1245
1592
|
])
|
|
1246
1593
|
}).strict();
|
|
1247
|
-
var ConditionStep =
|
|
1248
|
-
kind:
|
|
1594
|
+
var ConditionStep = z8.object({
|
|
1595
|
+
kind: z8.literal("condition"),
|
|
1249
1596
|
...label,
|
|
1250
1597
|
filter: BoundedAutomationFilter,
|
|
1251
1598
|
yes: StepId.nullable(),
|
|
@@ -1256,49 +1603,49 @@ var ConditionStep = z7.object({
|
|
|
1256
1603
|
* neither half means anything without the other: a window with no answer
|
|
1257
1604
|
* for the end of it would be a silent decision.
|
|
1258
1605
|
*/
|
|
1259
|
-
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()
|
|
1260
1607
|
}).strict();
|
|
1261
|
-
var SplitBranchKey =
|
|
1608
|
+
var SplitBranchKey = z8.string().regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, underscore and hyphen, 1 to 32");
|
|
1262
1609
|
var MIN_SPLIT_BRANCHES = 2;
|
|
1263
1610
|
var MAX_SPLIT_BRANCHES = 4;
|
|
1264
|
-
var SplitStep =
|
|
1265
|
-
kind:
|
|
1611
|
+
var SplitStep = z8.object({
|
|
1612
|
+
kind: z8.literal("split"),
|
|
1266
1613
|
...label,
|
|
1267
|
-
branches:
|
|
1268
|
-
|
|
1614
|
+
branches: z8.array(
|
|
1615
|
+
z8.object({
|
|
1269
1616
|
key: SplitBranchKey,
|
|
1270
1617
|
/** A whole percentage. Zero is allowed: a branch kept in the flow but switched off. */
|
|
1271
|
-
weight:
|
|
1618
|
+
weight: z8.number().int().min(0).max(100),
|
|
1272
1619
|
next: StepId.nullable()
|
|
1273
1620
|
}).strict()
|
|
1274
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")
|
|
1275
1622
|
}).strict();
|
|
1276
|
-
var GotoStep =
|
|
1623
|
+
var GotoStep = z8.object({ kind: z8.literal("goto"), ...label, target: StepId.nullable() }).strict();
|
|
1277
1624
|
var MAX_STEP_VISITS = 20;
|
|
1278
|
-
var WebhookStep =
|
|
1279
|
-
var NotifyRecipients =
|
|
1280
|
-
var NotifyStep =
|
|
1281
|
-
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"),
|
|
1282
1629
|
...label,
|
|
1283
1630
|
...next,
|
|
1284
1631
|
to: NotifyRecipients,
|
|
1285
1632
|
/** Empty while the step is being written; `not_chosen` refuses to publish it. */
|
|
1286
|
-
subject:
|
|
1287
|
-
body:
|
|
1633
|
+
subject: z8.string().max(998),
|
|
1634
|
+
body: z8.string().max(1e4)
|
|
1288
1635
|
}).strict();
|
|
1289
|
-
var AutomationStep =
|
|
1636
|
+
var AutomationStep = z8.discriminatedUnion("kind", [
|
|
1290
1637
|
EmailStep,
|
|
1291
1638
|
DelayStep,
|
|
1292
1639
|
ConditionStep,
|
|
1293
|
-
|
|
1294
|
-
|
|
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(),
|
|
1295
1642
|
/** `value` null clears the property, as a contact upsert does. */
|
|
1296
|
-
|
|
1643
|
+
z8.object({ kind: z8.literal("set_property"), ...label, ...next, key: DraftPropertyKey, value: JsonValue }).strict(),
|
|
1297
1644
|
/** Subscribing never lifts a suppression: the service checks that, not the flow. */
|
|
1298
|
-
|
|
1645
|
+
z8.object({ kind: z8.literal("subscribe_topic"), ...label, ...next, topic: DraftSlug }).strict(),
|
|
1299
1646
|
/** `topic` unsubscribes from the automation's own topic, `all` from every topic. */
|
|
1300
|
-
|
|
1301
|
-
|
|
1647
|
+
z8.object({ kind: z8.literal("unsubscribe"), ...label, ...next, scope: z8.enum(["topic", "all"]) }).strict(),
|
|
1648
|
+
z8.object({ kind: z8.literal("exit"), ...label }).strict(),
|
|
1302
1649
|
SplitStep,
|
|
1303
1650
|
GotoStep,
|
|
1304
1651
|
WebhookStep,
|
|
@@ -1322,25 +1669,25 @@ function stepTargets(step) {
|
|
|
1322
1669
|
}
|
|
1323
1670
|
}
|
|
1324
1671
|
var MAX_STEPS = 200;
|
|
1325
|
-
var AutomationFlow =
|
|
1326
|
-
triggers:
|
|
1672
|
+
var AutomationFlow = z8.object({
|
|
1673
|
+
triggers: z8.array(AutomationTrigger).max(MAX_TRIGGERS),
|
|
1327
1674
|
/** The first step. `null` is an empty flow. */
|
|
1328
1675
|
entry: StepId.nullable(),
|
|
1329
|
-
steps:
|
|
1676
|
+
steps: z8.record(StepId, AutomationStep)
|
|
1330
1677
|
}).strict();
|
|
1331
1678
|
var emptyFlow = () => ({ triggers: [], entry: null, steps: {} });
|
|
1332
|
-
var QuietHours =
|
|
1679
|
+
var QuietHours = z8.object({
|
|
1333
1680
|
start: TimeOfDay,
|
|
1334
1681
|
end: TimeOfDay,
|
|
1335
|
-
weekdays:
|
|
1682
|
+
weekdays: z8.array(Weekday).min(1).max(7).nullish()
|
|
1336
1683
|
}).strict();
|
|
1337
1684
|
var RE_ENTRY_MODES = ["off", "after_exit"];
|
|
1338
|
-
var ReEntryMode =
|
|
1685
|
+
var ReEntryMode = z8.enum(RE_ENTRY_MODES);
|
|
1339
1686
|
var MAX_RE_ENTRY_COOLDOWN_SECONDS = 365 * 24 * 60 * 60;
|
|
1340
|
-
var AutomationSettings =
|
|
1687
|
+
var AutomationSettings = z8.object({
|
|
1341
1688
|
re_entry: ReEntryMode,
|
|
1342
1689
|
/** Seconds a person must wait after leaving before entering again. Only with `after_exit`. */
|
|
1343
|
-
re_entry_cooldown_s:
|
|
1690
|
+
re_entry_cooldown_s: z8.number().int().min(0).max(MAX_RE_ENTRY_COOLDOWN_SECONDS).nullish(),
|
|
1344
1691
|
quiet_hours: QuietHours.nullish(),
|
|
1345
1692
|
/** Checked before every step; when it matches, the run ends. */
|
|
1346
1693
|
exit_filter: BoundedAutomationFilter.nullish()
|
|
@@ -1398,18 +1745,18 @@ var AUTOMATION_ISSUE_CODES = [
|
|
|
1398
1745
|
/** A one-off date that is already over everywhere on earth, so it would fire for nobody. */
|
|
1399
1746
|
"date_in_the_past"
|
|
1400
1747
|
];
|
|
1401
|
-
var AutomationIssueCode =
|
|
1402
|
-
var AutomationIssue =
|
|
1748
|
+
var AutomationIssueCode = z8.enum(AUTOMATION_ISSUE_CODES);
|
|
1749
|
+
var AutomationIssue = z8.object({
|
|
1403
1750
|
code: AutomationIssueCode,
|
|
1404
1751
|
/** The step the issue is about, when it is about one. */
|
|
1405
1752
|
step_id: StepId.nullable(),
|
|
1406
1753
|
/** Where inside the document, for the canvas to point at a field. */
|
|
1407
|
-
path:
|
|
1408
|
-
message:
|
|
1754
|
+
path: z8.array(z8.union([z8.string(), z8.number()])),
|
|
1755
|
+
message: z8.string()
|
|
1409
1756
|
});
|
|
1410
|
-
var AutomationValidation =
|
|
1411
|
-
ok:
|
|
1412
|
-
issues:
|
|
1757
|
+
var AutomationValidation = z8.object({
|
|
1758
|
+
ok: z8.boolean(),
|
|
1759
|
+
issues: z8.array(AutomationIssue)
|
|
1413
1760
|
});
|
|
1414
1761
|
function validateAutomation(flow, settings, ctx) {
|
|
1415
1762
|
const issues = [];
|
|
@@ -1800,7 +2147,7 @@ function cycles(flow) {
|
|
|
1800
2147
|
return out.sort();
|
|
1801
2148
|
}
|
|
1802
2149
|
var AUTOMATION_STATUSES = ["draft", "active", "paused", "archived"];
|
|
1803
|
-
var AutomationStatus =
|
|
2150
|
+
var AutomationStatus = z8.enum(AUTOMATION_STATUSES);
|
|
1804
2151
|
var AUTOMATION_END_REASONS = [
|
|
1805
2152
|
"completed",
|
|
1806
2153
|
"exit_filter",
|
|
@@ -1819,9 +2166,9 @@ var AUTOMATION_END_REASONS = [
|
|
|
1819
2166
|
"ended_by_hand",
|
|
1820
2167
|
"failed"
|
|
1821
2168
|
];
|
|
1822
|
-
var AutomationEndReason =
|
|
2169
|
+
var AutomationEndReason = z8.enum(AUTOMATION_END_REASONS);
|
|
1823
2170
|
var AUTOMATION_WAIT_REASONS = ["delay", "quiet_hours", "plan_limit", "sending", "condition"];
|
|
1824
|
-
var AutomationWaitReason =
|
|
2171
|
+
var AutomationWaitReason = z8.enum(AUTOMATION_WAIT_REASONS);
|
|
1825
2172
|
var AUTOMATION_ENTRY_REFUSALS = [
|
|
1826
2173
|
"already_inside",
|
|
1827
2174
|
"re_entry_off",
|
|
@@ -1832,26 +2179,26 @@ var AUTOMATION_ENTRY_REFUSALS = [
|
|
|
1832
2179
|
"not_active",
|
|
1833
2180
|
"unknown_contact"
|
|
1834
2181
|
];
|
|
1835
|
-
var AutomationEntryRefusal =
|
|
1836
|
-
var AutomationCounts =
|
|
1837
|
-
started:
|
|
1838
|
-
in_progress:
|
|
1839
|
-
completed:
|
|
1840
|
-
exited:
|
|
1841
|
-
failed:
|
|
1842
|
-
});
|
|
1843
|
-
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({
|
|
1844
2191
|
id: Id,
|
|
1845
|
-
name:
|
|
2192
|
+
name: z8.string().min(1).max(200),
|
|
1846
2193
|
status: AutomationStatus,
|
|
1847
2194
|
/** Every automation sends under one topic; there are no transactional automations. */
|
|
1848
2195
|
topic: Slug,
|
|
1849
2196
|
provider_id: Id.nullable(),
|
|
1850
2197
|
settings: AutomationSettings,
|
|
1851
2198
|
/** The flow people are running through, null while the automation has never been published. */
|
|
1852
|
-
published_version:
|
|
2199
|
+
published_version: z8.number().int().min(1).nullable(),
|
|
1853
2200
|
/** The flow being edited. Increments on every save, like a template's version. */
|
|
1854
|
-
draft_version:
|
|
2201
|
+
draft_version: z8.number().int().min(1),
|
|
1855
2202
|
counts: AutomationCounts,
|
|
1856
2203
|
created_at: Timestamp,
|
|
1857
2204
|
updated_at: Timestamp,
|
|
@@ -1864,16 +2211,16 @@ var AutomationWithFlow = Automation.extend({
|
|
|
1864
2211
|
published_flow: AutomationFlow.nullable()
|
|
1865
2212
|
});
|
|
1866
2213
|
var AutomationSummary = Automation;
|
|
1867
|
-
var AutomationCreate =
|
|
1868
|
-
name:
|
|
2214
|
+
var AutomationCreate = z8.object({
|
|
2215
|
+
name: z8.string().min(1).max(200),
|
|
1869
2216
|
topic: Slug,
|
|
1870
2217
|
provider_id: Id.optional(),
|
|
1871
2218
|
flow: AutomationFlow.optional(),
|
|
1872
2219
|
settings: AutomationSettings.partial().optional()
|
|
1873
2220
|
});
|
|
1874
|
-
var AutomationUpdate =
|
|
1875
|
-
base_version:
|
|
1876
|
-
name:
|
|
2221
|
+
var AutomationUpdate = z8.object({
|
|
2222
|
+
base_version: z8.number().int().min(1),
|
|
2223
|
+
name: z8.string().min(1).max(200).optional(),
|
|
1877
2224
|
topic: Slug.optional(),
|
|
1878
2225
|
provider_id: Id.optional(),
|
|
1879
2226
|
flow: AutomationFlow.optional(),
|
|
@@ -1886,42 +2233,42 @@ var AutomationListQuery = PageQuery.extend({
|
|
|
1886
2233
|
status: AutomationStatus.optional(),
|
|
1887
2234
|
topic: Slug.optional()
|
|
1888
2235
|
});
|
|
1889
|
-
var AutomationPublishRequest =
|
|
2236
|
+
var AutomationPublishRequest = z8.object({
|
|
1890
2237
|
/**
|
|
1891
2238
|
* Answer what this publish would do and change nothing, whatever the flow
|
|
1892
2239
|
* looks like. Without it a publish that has no issues and moves nobody
|
|
1893
2240
|
* simply goes ahead, which is right for an API caller and wrong for a
|
|
1894
2241
|
* dialog that is only showing somebody what they are about to do.
|
|
1895
2242
|
*/
|
|
1896
|
-
dry_run:
|
|
1897
|
-
confirm:
|
|
2243
|
+
dry_run: z8.boolean().optional(),
|
|
2244
|
+
confirm: z8.boolean().optional(),
|
|
1898
2245
|
/** Per removed step: the step its waiting runs move to, or null to end them. */
|
|
1899
|
-
moves:
|
|
2246
|
+
moves: z8.record(StepId, StepId.nullable()).optional(),
|
|
1900
2247
|
/** Also enter everyone who matches a trigger's condition right now. */
|
|
1901
|
-
backfill:
|
|
2248
|
+
backfill: z8.boolean().optional()
|
|
1902
2249
|
}).strict();
|
|
1903
|
-
var AutomationPublishMove =
|
|
2250
|
+
var AutomationPublishMove = z8.object({
|
|
1904
2251
|
step_id: StepId,
|
|
1905
2252
|
/** Why the step's people have to move: it is gone, or its delay changed. */
|
|
1906
|
-
change:
|
|
1907
|
-
waiting:
|
|
2253
|
+
change: z8.enum(["removed", "delay_changed", "kind_changed"]),
|
|
2254
|
+
waiting: z8.number().int().min(0),
|
|
1908
2255
|
/** Where they go: a step id, or null to end their runs with `step_removed`. */
|
|
1909
2256
|
destination: StepId.nullable()
|
|
1910
2257
|
});
|
|
1911
|
-
var AutomationPublishResult =
|
|
1912
|
-
published:
|
|
2258
|
+
var AutomationPublishResult = z8.object({
|
|
2259
|
+
published: z8.boolean(),
|
|
1913
2260
|
automation: Automation,
|
|
1914
2261
|
/** Empty when nothing has to move; non-empty and `published` false asks for a confirmation. */
|
|
1915
|
-
moves:
|
|
2262
|
+
moves: z8.array(AutomationPublishMove),
|
|
1916
2263
|
/** How many people the backfill entered, or would enter when it is not confirmed. */
|
|
1917
|
-
backfill:
|
|
1918
|
-
issues:
|
|
1919
|
-
});
|
|
1920
|
-
var AutomationDuplicateRequest =
|
|
1921
|
-
var AutomationEnrollRequest =
|
|
1922
|
-
contact_ids:
|
|
1923
|
-
external_ids:
|
|
1924
|
-
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(),
|
|
1925
2272
|
segment_id: Id.optional(),
|
|
1926
2273
|
/** Where to start them; the flow's entry step by default. */
|
|
1927
2274
|
step_id: StepId.optional()
|
|
@@ -1929,34 +2276,34 @@ var AutomationEnrollRequest = z7.object({
|
|
|
1929
2276
|
(v) => [v.contact_ids, v.external_ids, v.emails, v.segment_id].filter((x) => x !== void 0).length === 1,
|
|
1930
2277
|
"name the people exactly one way: contact_ids, external_ids, emails or segment_id"
|
|
1931
2278
|
);
|
|
1932
|
-
var AutomationEnrollment =
|
|
2279
|
+
var AutomationEnrollment = z8.object({
|
|
1933
2280
|
contact_id: Id.nullable(),
|
|
1934
2281
|
email: Email.nullable(),
|
|
1935
|
-
external_id:
|
|
1936
|
-
entered:
|
|
2282
|
+
external_id: z8.string().nullable(),
|
|
2283
|
+
entered: z8.boolean(),
|
|
1937
2284
|
run_id: Id.nullable(),
|
|
1938
2285
|
reason: AutomationEntryRefusal.nullable()
|
|
1939
2286
|
});
|
|
1940
|
-
var AutomationEnrollResult =
|
|
1941
|
-
entered:
|
|
1942
|
-
refused:
|
|
1943
|
-
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)
|
|
1944
2291
|
});
|
|
1945
|
-
var AutomationRunStatus =
|
|
1946
|
-
var AutomationRun =
|
|
2292
|
+
var AutomationRunStatus = z8.enum(["active", "waiting", "completed", "exited", "failed"]);
|
|
2293
|
+
var AutomationRun = z8.object({
|
|
1947
2294
|
id: Id,
|
|
1948
2295
|
automation_id: Id,
|
|
1949
2296
|
contact_id: Id,
|
|
1950
2297
|
email: Email,
|
|
1951
|
-
external_id:
|
|
1952
|
-
version:
|
|
2298
|
+
external_id: z8.string().nullable(),
|
|
2299
|
+
version: z8.number().int().min(1),
|
|
1953
2300
|
status: AutomationRunStatus,
|
|
1954
2301
|
current_step_id: StepId.nullable(),
|
|
1955
2302
|
next_action_at: Timestamp.nullable(),
|
|
1956
2303
|
wait_reason: AutomationWaitReason.nullable(),
|
|
1957
|
-
end_reason:
|
|
2304
|
+
end_reason: z8.string().nullable(),
|
|
1958
2305
|
/** What went wrong the last time this run tried to take its step, null otherwise. */
|
|
1959
|
-
last_error:
|
|
2306
|
+
last_error: z8.string().nullable(),
|
|
1960
2307
|
entered_at: Timestamp,
|
|
1961
2308
|
ended_at: Timestamp.nullable()
|
|
1962
2309
|
});
|
|
@@ -1966,13 +2313,13 @@ var AutomationRunListQuery = PageQuery.extend({
|
|
|
1966
2313
|
contact_id: Id.optional()
|
|
1967
2314
|
});
|
|
1968
2315
|
var ContactAutomationRun = AutomationRun.extend({
|
|
1969
|
-
automation:
|
|
2316
|
+
automation: z8.object({ id: Id, name: z8.string(), status: AutomationStatus })
|
|
1970
2317
|
});
|
|
1971
2318
|
var ContactAutomationListQuery = PageQuery.extend({ status: AutomationRunStatus.optional() });
|
|
1972
|
-
var AutomationStepExecution =
|
|
2319
|
+
var AutomationStepExecution = z8.object({
|
|
1973
2320
|
step_id: StepId,
|
|
1974
2321
|
step_kind: AutomationStepKind,
|
|
1975
|
-
visit:
|
|
2322
|
+
visit: z8.number().int().min(1),
|
|
1976
2323
|
started_at: Timestamp,
|
|
1977
2324
|
finished_at: Timestamp.nullable(),
|
|
1978
2325
|
/**
|
|
@@ -1984,48 +2331,48 @@ var AutomationStepExecution = z7.object({
|
|
|
1984
2331
|
*/
|
|
1985
2332
|
outcome: Properties
|
|
1986
2333
|
});
|
|
1987
|
-
var AutomationJourney =
|
|
2334
|
+
var AutomationJourney = z8.object({
|
|
1988
2335
|
run: AutomationRun,
|
|
1989
|
-
steps:
|
|
2336
|
+
steps: z8.array(AutomationStepExecution)
|
|
1990
2337
|
});
|
|
1991
|
-
var AutomationStepReport =
|
|
2338
|
+
var AutomationStepReport = z8.object({
|
|
1992
2339
|
step_id: StepId,
|
|
1993
2340
|
step_kind: AutomationStepKind,
|
|
1994
|
-
reached:
|
|
1995
|
-
waiting:
|
|
1996
|
-
passed:
|
|
2341
|
+
reached: z8.number().int().min(0),
|
|
2342
|
+
waiting: z8.number().int().min(0),
|
|
2343
|
+
passed: z8.number().int().min(0),
|
|
1997
2344
|
/** Per branch, for a condition: how many took `yes` and how many `no`. */
|
|
1998
|
-
branches:
|
|
2345
|
+
branches: z8.record(z8.number().int().min(0)),
|
|
1999
2346
|
/** The step mailing's id, for an email step of the published version. */
|
|
2000
2347
|
mailing_id: Id.nullable()
|
|
2001
2348
|
});
|
|
2002
|
-
var AutomationReport =
|
|
2349
|
+
var AutomationReport = z8.object({
|
|
2003
2350
|
counts: AutomationCounts,
|
|
2004
2351
|
/** Why the people who left did: `AutomationEndReason` to a count. */
|
|
2005
|
-
end_reasons:
|
|
2352
|
+
end_reasons: z8.record(z8.number().int().min(0)),
|
|
2006
2353
|
/**
|
|
2007
2354
|
* Why people a trigger named did not enter: `AutomationEntryRefusal` to a
|
|
2008
2355
|
* count. Without it a pause, an exclusion or a missing subscription would
|
|
2009
2356
|
* turn people away invisibly.
|
|
2010
2357
|
*/
|
|
2011
|
-
refusals:
|
|
2012
|
-
steps:
|
|
2013
|
-
});
|
|
2014
|
-
var AutomationExitRunRequest =
|
|
2015
|
-
var AutomationRunParams =
|
|
2016
|
-
var AutomationStepParams =
|
|
2017
|
-
var AutomationTestEmailRequest =
|
|
2018
|
-
var AutomationValidateRequest =
|
|
2019
|
-
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({
|
|
2020
2367
|
name: EventName,
|
|
2021
|
-
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"),
|
|
2022
2369
|
data: Properties.optional(),
|
|
2023
2370
|
occurred_at: Timestamp.optional(),
|
|
2024
|
-
id:
|
|
2371
|
+
id: z8.string().min(1).max(128).optional()
|
|
2025
2372
|
}).strict();
|
|
2026
|
-
var Event =
|
|
2373
|
+
var Event = z8.object({
|
|
2027
2374
|
id: Id,
|
|
2028
|
-
client_event_id:
|
|
2375
|
+
client_event_id: z8.string().nullable(),
|
|
2029
2376
|
name: EventName,
|
|
2030
2377
|
contact_id: Id,
|
|
2031
2378
|
data: Properties,
|
|
@@ -2036,53 +2383,27 @@ var Event = z7.object({
|
|
|
2036
2383
|
* hand. Each is then admitted or refused by the runtime on its own rules
|
|
2037
2384
|
* (consent, exclusions, re-entry), which `automations.report` counts.
|
|
2038
2385
|
*/
|
|
2039
|
-
entered:
|
|
2386
|
+
entered: z8.number().int().min(0)
|
|
2040
2387
|
});
|
|
2041
2388
|
var EventListQuery = PageQuery.extend({
|
|
2042
2389
|
name: EventName.optional(),
|
|
2043
2390
|
contact_id: Id.optional()
|
|
2044
2391
|
});
|
|
2045
2392
|
|
|
2046
|
-
// src/unsubscribe.ts
|
|
2047
|
-
var UNSUBSCRIBE_PATH_PREFIX = "/u/";
|
|
2048
|
-
var LIST_UNSUBSCRIBE_POST_VALUE = "List-Unsubscribe=One-Click";
|
|
2049
|
-
var RESERVED_MERGE_FIELDS = {
|
|
2050
|
-
/** From the recipient's merge values, else the contact. Supports a fallback. */
|
|
2051
|
-
first_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
2052
|
-
last_name: { source: "contact", fallback: true, required_for_broadcast: false },
|
|
2053
|
-
email: { source: "contact", fallback: false, required_for_broadcast: false },
|
|
2054
|
-
/** The hosted unsubscribe page for this recipient. A broadcast without it is refused. */
|
|
2055
|
-
unsubscribe_url: { source: "service", fallback: false, required_for_broadcast: true }
|
|
2056
|
-
};
|
|
2057
|
-
var MERGE_FIELD_PATTERN = /\{\{\s*([a-z][a-z0-9_]*)\s*(?:\|([^}]*))?\}\}/g;
|
|
2058
|
-
function findMergeFields(html) {
|
|
2059
|
-
const uses = [];
|
|
2060
|
-
for (const match of html.matchAll(MERGE_FIELD_PATTERN)) {
|
|
2061
|
-
uses.push({ name: match[1], fallback: match[2] === void 0 ? null : match[2].trim() });
|
|
2062
|
-
}
|
|
2063
|
-
return uses;
|
|
2064
|
-
}
|
|
2065
|
-
function missingRequiredMergeFields(html) {
|
|
2066
|
-
const used = new Set(findMergeFields(html).map((u) => u.name));
|
|
2067
|
-
return Object.keys(RESERVED_MERGE_FIELDS).filter(
|
|
2068
|
-
(name) => RESERVED_MERGE_FIELDS[name].required_for_broadcast && !used.has(name)
|
|
2069
|
-
);
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
2393
|
// src/workspace.ts
|
|
2073
|
-
var CompanyId =
|
|
2394
|
+
var CompanyId = z9.string().min(1).max(64);
|
|
2074
2395
|
var ASSET_POLICIES = ["any", "service_only"];
|
|
2075
|
-
var AssetPolicy =
|
|
2396
|
+
var AssetPolicy = z9.enum(ASSET_POLICIES);
|
|
2076
2397
|
var MAX_ALLOWED_ASSET_HOSTS = 20;
|
|
2077
|
-
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");
|
|
2078
2399
|
var MAX_MERGE_DEFAULTS = 50;
|
|
2079
2400
|
var MAX_MERGE_DEFAULT_LENGTH = 2e3;
|
|
2080
|
-
var MergeFieldName =
|
|
2081
|
-
var MergeDefaultValue =
|
|
2082
|
-
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), {
|
|
2083
2404
|
message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
|
|
2084
2405
|
});
|
|
2085
|
-
var WorkspaceSettings =
|
|
2406
|
+
var WorkspaceSettings = z9.object({
|
|
2086
2407
|
/**
|
|
2087
2408
|
* The workspace's main language (a {@link LocaleTag}, e.g. "de"): the main
|
|
2088
2409
|
* language of a new template, and of the hosted pages when nothing else
|
|
@@ -2095,9 +2416,9 @@ var WorkspaceSettings = z8.object({
|
|
|
2095
2416
|
* each; the hosted pages are offered in those of them they support (English,
|
|
2096
2417
|
* German, French, Italian, Spanish). The service stores canonical casing.
|
|
2097
2418
|
*/
|
|
2098
|
-
locales:
|
|
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"),
|
|
2099
2420
|
/** Open and click tracking. Off by default, opt-in per workspace. */
|
|
2100
|
-
tracking_enabled:
|
|
2421
|
+
tracking_enabled: z9.boolean(),
|
|
2101
2422
|
/** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
|
|
2102
2423
|
asset_policy: AssetPolicy,
|
|
2103
2424
|
/**
|
|
@@ -2105,7 +2426,7 @@ var WorkspaceSettings = z8.object({
|
|
|
2105
2426
|
* the workspace's website, typically. Empty by default. Ignored under `any`,
|
|
2106
2427
|
* which allows everything already.
|
|
2107
2428
|
*/
|
|
2108
|
-
allowed_asset_hosts:
|
|
2429
|
+
allowed_asset_hosts: z9.array(AssetHost).max(MAX_ALLOWED_ASSET_HOSTS),
|
|
2109
2430
|
/** Merge values every mail of the workspace can use (see `MergeDefaults`). Empty by default. */
|
|
2110
2431
|
merge_defaults: MergeDefaults,
|
|
2111
2432
|
/**
|
|
@@ -2114,10 +2435,10 @@ var WorkspaceSettings = z8.object({
|
|
|
2114
2435
|
*/
|
|
2115
2436
|
default_timezone: TimezoneName.nullable()
|
|
2116
2437
|
});
|
|
2117
|
-
var Workspace =
|
|
2438
|
+
var Workspace = z9.object({
|
|
2118
2439
|
id: Id,
|
|
2119
2440
|
slug: Slug,
|
|
2120
|
-
name:
|
|
2441
|
+
name: z9.string().min(1).max(120),
|
|
2121
2442
|
settings: WorkspaceSettings,
|
|
2122
2443
|
/**
|
|
2123
2444
|
* The auth-brain company (tenant) the workspace was created for, or null for
|
|
@@ -2128,13 +2449,13 @@ var Workspace = z8.object({
|
|
|
2128
2449
|
created_at: Timestamp,
|
|
2129
2450
|
updated_at: Timestamp
|
|
2130
2451
|
});
|
|
2131
|
-
var WorkspaceCreate =
|
|
2452
|
+
var WorkspaceCreate = z9.object({
|
|
2132
2453
|
slug: Slug,
|
|
2133
|
-
name:
|
|
2454
|
+
name: z9.string().min(1).max(120),
|
|
2134
2455
|
settings: WorkspaceSettings.partial().optional(),
|
|
2135
|
-
owner:
|
|
2456
|
+
owner: z9.object({
|
|
2136
2457
|
email: Email,
|
|
2137
|
-
name:
|
|
2458
|
+
name: z9.string().max(200).nullable().optional()
|
|
2138
2459
|
}),
|
|
2139
2460
|
/**
|
|
2140
2461
|
* The signed-in person's active auth-brain company. The dashboard always
|
|
@@ -2144,49 +2465,49 @@ var WorkspaceCreate = z8.object({
|
|
|
2144
2465
|
*/
|
|
2145
2466
|
company_id: CompanyId.optional()
|
|
2146
2467
|
});
|
|
2147
|
-
var WorkspaceUpdate =
|
|
2148
|
-
name:
|
|
2468
|
+
var WorkspaceUpdate = z9.object({
|
|
2469
|
+
name: z9.string().min(1).max(120),
|
|
2149
2470
|
settings: WorkspaceSettings.partial()
|
|
2150
2471
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2151
|
-
var WorkspaceMove =
|
|
2472
|
+
var WorkspaceMove = z9.object({ company_id: CompanyId });
|
|
2152
2473
|
var MEMBER_ROLES = ["owner", "admin", "editor", "viewer"];
|
|
2153
|
-
var MemberRole =
|
|
2474
|
+
var MemberRole = z9.enum(MEMBER_ROLES);
|
|
2154
2475
|
var WorkspaceMembership = Workspace.extend({ role: MemberRole });
|
|
2155
|
-
var Member =
|
|
2476
|
+
var Member = z9.object({
|
|
2156
2477
|
id: Id,
|
|
2157
2478
|
/** The person's auth-brain subject. */
|
|
2158
|
-
subject:
|
|
2479
|
+
subject: z9.string().min(1).max(255),
|
|
2159
2480
|
email: Email,
|
|
2160
|
-
name:
|
|
2481
|
+
name: z9.string().max(200).nullable(),
|
|
2161
2482
|
role: MemberRole,
|
|
2162
2483
|
created_at: Timestamp
|
|
2163
2484
|
});
|
|
2164
|
-
var MemberCreate =
|
|
2165
|
-
subject:
|
|
2485
|
+
var MemberCreate = z9.object({
|
|
2486
|
+
subject: z9.string().min(1).max(255),
|
|
2166
2487
|
email: Email,
|
|
2167
|
-
name:
|
|
2488
|
+
name: z9.string().max(200).nullable().optional(),
|
|
2168
2489
|
role: MemberRole
|
|
2169
2490
|
});
|
|
2170
|
-
var MemberUpdate =
|
|
2491
|
+
var MemberUpdate = z9.object({ role: MemberRole });
|
|
2171
2492
|
var API_KEY_SCOPES = ["full", "read", "send"];
|
|
2172
|
-
var ApiKeyScope =
|
|
2173
|
-
var ApiKey =
|
|
2493
|
+
var ApiKeyScope = z9.enum(API_KEY_SCOPES);
|
|
2494
|
+
var ApiKey = z9.object({
|
|
2174
2495
|
id: Id,
|
|
2175
|
-
name:
|
|
2496
|
+
name: z9.string().min(1).max(120),
|
|
2176
2497
|
/** The first characters of the key, for recognising it in a list. */
|
|
2177
|
-
prefix:
|
|
2498
|
+
prefix: z9.string().min(4).max(32),
|
|
2178
2499
|
scope: ApiKeyScope,
|
|
2179
2500
|
last_used_at: Timestamp.nullable(),
|
|
2180
2501
|
revoked_at: Timestamp.nullable(),
|
|
2181
2502
|
created_at: Timestamp
|
|
2182
2503
|
});
|
|
2183
|
-
var ApiKeyCreate =
|
|
2184
|
-
name:
|
|
2504
|
+
var ApiKeyCreate = z9.object({
|
|
2505
|
+
name: z9.string().min(1).max(120),
|
|
2185
2506
|
/** Omitted means `full`. */
|
|
2186
2507
|
scope: ApiKeyScope.optional()
|
|
2187
2508
|
});
|
|
2188
|
-
var ApiKeyCreated =
|
|
2189
|
-
key:
|
|
2509
|
+
var ApiKeyCreated = z9.object({
|
|
2510
|
+
key: z9.string().min(20),
|
|
2190
2511
|
api_key: ApiKey
|
|
2191
2512
|
});
|
|
2192
2513
|
var AUDIT_ACTIONS = [
|
|
@@ -2274,29 +2595,29 @@ var AUDIT_ACTIONS = [
|
|
|
2274
2595
|
"automation.run_ended",
|
|
2275
2596
|
"event.recorded"
|
|
2276
2597
|
];
|
|
2277
|
-
var AuditAction =
|
|
2278
|
-
var AuditActor =
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
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"),
|
|
2282
2603
|
api_key_id: Id,
|
|
2283
2604
|
/**
|
|
2284
2605
|
* Who the client's application said it was acting for, from
|
|
2285
2606
|
* `ON_BEHALF_OF_HEADER`: reported by that application, never verified.
|
|
2286
2607
|
* Absent when the call carried no label.
|
|
2287
2608
|
*/
|
|
2288
|
-
on_behalf_of:
|
|
2609
|
+
on_behalf_of: z9.string().min(1).max(ON_BEHALF_OF_MAX_LENGTH).optional()
|
|
2289
2610
|
}),
|
|
2290
2611
|
/** The service itself (the worker, a bounce, the hosted unsubscribe page). */
|
|
2291
|
-
|
|
2612
|
+
z9.object({ type: z9.literal("system"), reason: z9.string().min(1).max(200) })
|
|
2292
2613
|
]);
|
|
2293
|
-
var AuditEntry =
|
|
2614
|
+
var AuditEntry = z9.object({
|
|
2294
2615
|
id: Id,
|
|
2295
2616
|
action: AuditAction,
|
|
2296
2617
|
actor: AuditActor,
|
|
2297
|
-
target_type:
|
|
2618
|
+
target_type: z9.string().min(1).max(64),
|
|
2298
2619
|
target_id: Id.nullable(),
|
|
2299
|
-
details:
|
|
2620
|
+
details: z9.record(z9.unknown()),
|
|
2300
2621
|
created_at: Timestamp
|
|
2301
2622
|
});
|
|
2302
2623
|
var AuditQuery = PageQuery.extend({
|
|
@@ -2305,12 +2626,12 @@ var AuditQuery = PageQuery.extend({
|
|
|
2305
2626
|
});
|
|
2306
2627
|
|
|
2307
2628
|
// src/invites.ts
|
|
2308
|
-
import { z as
|
|
2629
|
+
import { z as z10 } from "zod";
|
|
2309
2630
|
var INVITE_STATUSES = ["pending", "accepted", "revoked", "expired"];
|
|
2310
|
-
var InviteStatus =
|
|
2631
|
+
var InviteStatus = z10.enum(INVITE_STATUSES);
|
|
2311
2632
|
var DEFAULT_INVITE_TTL_DAYS = 7;
|
|
2312
2633
|
var MAX_INVITE_TTL_DAYS = 30;
|
|
2313
|
-
var Invite =
|
|
2634
|
+
var Invite = z10.object({
|
|
2314
2635
|
id: Id,
|
|
2315
2636
|
/** Stored lowercased; only a person signed in with this address can accept. */
|
|
2316
2637
|
email: Email,
|
|
@@ -2318,32 +2639,32 @@ var Invite = z9.object({
|
|
|
2318
2639
|
/** Derived from the timestamps below, never stored. */
|
|
2319
2640
|
status: InviteStatus,
|
|
2320
2641
|
/** The member who invited, while they are still one (null after they left). */
|
|
2321
|
-
invited_by:
|
|
2642
|
+
invited_by: z10.object({ member_id: Id.nullable(), email: Email.nullable() }),
|
|
2322
2643
|
expires_at: Timestamp,
|
|
2323
2644
|
accepted_at: Timestamp.nullable(),
|
|
2324
2645
|
revoked_at: Timestamp.nullable(),
|
|
2325
2646
|
created_at: Timestamp
|
|
2326
2647
|
});
|
|
2327
|
-
var InviteCreate =
|
|
2648
|
+
var InviteCreate = z10.object({
|
|
2328
2649
|
email: Email,
|
|
2329
2650
|
role: MemberRole,
|
|
2330
|
-
expires_in_days:
|
|
2651
|
+
expires_in_days: z10.number().int().min(1).max(MAX_INVITE_TTL_DAYS).optional()
|
|
2331
2652
|
});
|
|
2332
|
-
var InviteCreated =
|
|
2653
|
+
var InviteCreated = z10.object({
|
|
2333
2654
|
invite: Invite,
|
|
2334
|
-
token:
|
|
2655
|
+
token: z10.string().min(32).max(128)
|
|
2335
2656
|
});
|
|
2336
2657
|
var InviteListQuery = PageQuery.extend({
|
|
2337
2658
|
status: InviteStatus.optional()
|
|
2338
2659
|
});
|
|
2339
|
-
var InviteAccept =
|
|
2340
|
-
token:
|
|
2660
|
+
var InviteAccept = z10.object({
|
|
2661
|
+
token: z10.string().min(32).max(128),
|
|
2341
2662
|
email: Email,
|
|
2342
|
-
name:
|
|
2663
|
+
name: z10.string().max(200).nullable().optional()
|
|
2343
2664
|
});
|
|
2344
|
-
var InviteAccepted =
|
|
2665
|
+
var InviteAccepted = z10.object({
|
|
2345
2666
|
workspace: WorkspaceMembership,
|
|
2346
|
-
already_member:
|
|
2667
|
+
already_member: z10.boolean()
|
|
2347
2668
|
});
|
|
2348
2669
|
var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
|
|
2349
2670
|
|
|
@@ -2385,6 +2706,7 @@ function storedTemplateLine(value) {
|
|
|
2385
2706
|
function buildTemplatePatch(args) {
|
|
2386
2707
|
const { loaded, main, documentChanged, translations, changed, removed } = args;
|
|
2387
2708
|
const out = {};
|
|
2709
|
+
if (args.insertions !== void 0 && !sameJson(args.insertions, loaded.insertions ?? {})) out.insertions = args.insertions;
|
|
2388
2710
|
const subject = storedTemplateLine(main.subject);
|
|
2389
2711
|
if (subject !== storedTemplateLine(loaded.subject)) out.subject = subject;
|
|
2390
2712
|
const preheader = storedTemplateLine(main.preheader);
|
|
@@ -2394,7 +2716,13 @@ function buildTemplatePatch(args) {
|
|
|
2394
2716
|
for (const tag of changed) {
|
|
2395
2717
|
const version = translations[tag];
|
|
2396
2718
|
if (version && !removed.has(tag)) {
|
|
2397
|
-
patch[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
|
+
};
|
|
2398
2726
|
}
|
|
2399
2727
|
}
|
|
2400
2728
|
for (const tag of removed) {
|
|
@@ -2403,6 +2731,10 @@ function buildTemplatePatch(args) {
|
|
|
2403
2731
|
if (Object.keys(patch).length > 0) out.translations = patch;
|
|
2404
2732
|
return out;
|
|
2405
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
|
+
}
|
|
2406
2738
|
function restorePatch(old, current, workspaceLocales) {
|
|
2407
2739
|
const oldMain = canonicalLocale(old.locale) ?? old.locale;
|
|
2408
2740
|
const listed = workspaceLocales ? new Set(workspaceLocales.map((t) => canonicalLocale(t) ?? t)) : null;
|
|
@@ -2417,7 +2749,7 @@ function restorePatch(old, current, workspaceLocales) {
|
|
|
2417
2749
|
untouched.add(canonical);
|
|
2418
2750
|
continue;
|
|
2419
2751
|
}
|
|
2420
|
-
patch[canonical] = version;
|
|
2752
|
+
patch[canonical] = { ...version, insertions: version.insertions ?? {} };
|
|
2421
2753
|
}
|
|
2422
2754
|
for (const tag of Object.keys(current.translations)) {
|
|
2423
2755
|
const canonical = canonicalLocale(tag) ?? tag;
|
|
@@ -2429,15 +2761,20 @@ function restorePatch(old, current, workspaceLocales) {
|
|
|
2429
2761
|
subject: old.subject,
|
|
2430
2762
|
preheader: old.preheader,
|
|
2431
2763
|
document: old.document,
|
|
2764
|
+
insertions: old.insertions ?? {},
|
|
2432
2765
|
...Object.keys(patch).length > 0 ? { translations: patch } : {}
|
|
2433
2766
|
};
|
|
2434
2767
|
return { update, skipped: skipped.sort() };
|
|
2435
2768
|
}
|
|
2436
2769
|
function draftsFromTemplate(template) {
|
|
2770
|
+
const insertions = template.insertions ?? {};
|
|
2437
2771
|
const translations = {};
|
|
2438
|
-
for (const [tag, version] of Object.entries(template.translations))
|
|
2772
|
+
for (const [tag, version] of Object.entries(template.translations)) {
|
|
2773
|
+
translations[canonicalLocale(tag) ?? tag] = { ...version, insertions: alignTranslationInsertions(insertions, version.insertions) };
|
|
2774
|
+
}
|
|
2439
2775
|
return {
|
|
2440
2776
|
main: { subject: template.subject ?? "", preheader: template.preheader ?? "", document: template.document },
|
|
2777
|
+
insertions,
|
|
2441
2778
|
translations,
|
|
2442
2779
|
mainDocumentChanged: false,
|
|
2443
2780
|
changed: [],
|
|
@@ -2461,13 +2798,19 @@ function addLanguageDraft(drafts, locale) {
|
|
|
2461
2798
|
subject: drafts.main.subject,
|
|
2462
2799
|
preheader: drafts.main.preheader,
|
|
2463
2800
|
document: structuredClone(drafts.main.document),
|
|
2464
|
-
status: "draft"
|
|
2801
|
+
status: "draft",
|
|
2802
|
+
insertions: copyInsertionTexts(drafts.insertions)
|
|
2465
2803
|
}
|
|
2466
2804
|
},
|
|
2467
2805
|
changed: withChanged(drafts.changed, tag),
|
|
2468
2806
|
removed: without(drafts.removed, tag)
|
|
2469
2807
|
};
|
|
2470
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
|
+
}
|
|
2471
2814
|
function removeLanguageDraft(drafts, locale) {
|
|
2472
2815
|
const tag = canonicalLocale(locale) ?? locale;
|
|
2473
2816
|
const { [tag]: _gone, ...rest } = drafts.translations;
|
|
@@ -2500,6 +2843,7 @@ function setLanguageStatus(drafts, locale, status) {
|
|
|
2500
2843
|
function draftsPatch(drafts, loaded) {
|
|
2501
2844
|
return buildTemplatePatch({
|
|
2502
2845
|
loaded,
|
|
2846
|
+
insertions: drafts.insertions,
|
|
2503
2847
|
main: drafts.main,
|
|
2504
2848
|
documentChanged: drafts.mainDocumentChanged,
|
|
2505
2849
|
translations: drafts.translations,
|
|
@@ -2516,6 +2860,7 @@ function readyWithoutSubject(drafts) {
|
|
|
2516
2860
|
function rebaseDrafts(sent, now, saved) {
|
|
2517
2861
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2518
2862
|
let out = draftsFromTemplate(saved);
|
|
2863
|
+
if (!sameJson(now.insertions, sent.insertions)) out = { ...out, insertions: now.insertions };
|
|
2519
2864
|
if (now.main.subject !== sent.main.subject) out = editLanguageDraft(out, null, { subject: now.main.subject });
|
|
2520
2865
|
if (now.main.preheader !== sent.main.preheader) out = editLanguageDraft(out, null, { preheader: now.main.preheader });
|
|
2521
2866
|
out = same(now.main.document, sent.main.document) ? keepLanguageDocument(out, null, now.main.document) : editLanguageDraft(out, null, { document: now.main.document });
|
|
@@ -2538,17 +2883,321 @@ function rebaseDrafts(sent, now, saved) {
|
|
|
2538
2883
|
removed: without(out.removed, tag)
|
|
2539
2884
|
};
|
|
2540
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;
|
|
2541
2901
|
return out;
|
|
2542
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
|
+
}
|
|
2543
3192
|
|
|
2544
3193
|
// src/providers.ts
|
|
2545
|
-
import { z as
|
|
3194
|
+
import { z as z11 } from "zod";
|
|
2546
3195
|
var PROVIDER_KINDS = ["smtp", "resend"];
|
|
2547
|
-
var ProviderKind =
|
|
2548
|
-
var ProviderPolicy =
|
|
2549
|
-
daily_recipient_budget:
|
|
2550
|
-
min_interval_ms:
|
|
2551
|
-
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)
|
|
2552
3201
|
});
|
|
2553
3202
|
var ICLOUD_SMTP_POLICY = {
|
|
2554
3203
|
daily_recipient_budget: 800,
|
|
@@ -2556,17 +3205,17 @@ var ICLOUD_SMTP_POLICY = {
|
|
|
2556
3205
|
max_recipients_per_message: 1
|
|
2557
3206
|
};
|
|
2558
3207
|
var SMTP_SECURITY = ["tls", "starttls"];
|
|
2559
|
-
var SmtpConfigBase =
|
|
2560
|
-
host:
|
|
2561
|
-
port:
|
|
3208
|
+
var SmtpConfigBase = z11.object({
|
|
3209
|
+
host: z11.string().min(1).max(255),
|
|
3210
|
+
port: z11.number().int().min(1).max(65535),
|
|
2562
3211
|
/** `tls` connects encrypted (465), `starttls` upgrades (587). Never plaintext. */
|
|
2563
|
-
security:
|
|
2564
|
-
username:
|
|
3212
|
+
security: z11.enum(SMTP_SECURITY),
|
|
3213
|
+
username: z11.string().min(1).max(255)
|
|
2565
3214
|
});
|
|
2566
|
-
var ResendConfigBase =
|
|
2567
|
-
var ProviderRejections =
|
|
2568
|
-
count:
|
|
2569
|
-
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(),
|
|
2570
3219
|
last_at: Timestamp.nullable(),
|
|
2571
3220
|
/**
|
|
2572
3221
|
* The last time the bounce circuit breaker tripped on one of this provider's
|
|
@@ -2576,59 +3225,59 @@ var ProviderRejections = z10.object({
|
|
|
2576
3225
|
* run's bounce blocks were undone and the mailing paused. `sample` is the
|
|
2577
3226
|
* reply that tripped it.
|
|
2578
3227
|
*/
|
|
2579
|
-
anomaly:
|
|
3228
|
+
anomaly: z11.object({
|
|
2580
3229
|
at: Timestamp,
|
|
2581
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. */
|
|
2582
|
-
scope:
|
|
3231
|
+
scope: z11.enum(["mailing", "provider"]),
|
|
2583
3232
|
/**
|
|
2584
3233
|
* True while the provider-wide breaker is open: no bounce blocks through
|
|
2585
3234
|
* this provider, test sends refused and mailings not started or resumed
|
|
2586
3235
|
* (`provider_anomaly`), until an admin clears it (`providers.clearAnomaly`).
|
|
2587
3236
|
*/
|
|
2588
|
-
blocking:
|
|
3237
|
+
blocking: z11.boolean(),
|
|
2589
3238
|
mailing_id: Id.nullable(),
|
|
2590
|
-
reason:
|
|
2591
|
-
sample:
|
|
3239
|
+
reason: z11.string(),
|
|
3240
|
+
sample: z11.string()
|
|
2592
3241
|
}).nullable()
|
|
2593
3242
|
});
|
|
2594
|
-
var ProviderEvents =
|
|
2595
|
-
status:
|
|
2596
|
-
source:
|
|
2597
|
-
url:
|
|
2598
|
-
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(),
|
|
2599
3248
|
/**
|
|
2600
3249
|
* Events that named an email this provider never sent through the service
|
|
2601
3250
|
* (another workspace or system sharing the Resend account): counted, never
|
|
2602
3251
|
* acted on.
|
|
2603
3252
|
*/
|
|
2604
|
-
unmatched:
|
|
3253
|
+
unmatched: z11.number().int().min(0)
|
|
2605
3254
|
});
|
|
2606
3255
|
var RESEND_EVENT_TYPES = ["email.bounced", "email.complained", "email.delivery_delayed"];
|
|
2607
|
-
var ResendSigningSecret =
|
|
2608
|
-
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 });
|
|
2609
3258
|
var ProviderFields = {
|
|
2610
|
-
name:
|
|
2611
|
-
from_name:
|
|
3259
|
+
name: z11.string().min(1).max(120),
|
|
3260
|
+
from_name: z11.string().min(1).max(120),
|
|
2612
3261
|
from_email: Email,
|
|
2613
3262
|
reply_to: Email.nullable(),
|
|
2614
3263
|
policy: ProviderPolicy
|
|
2615
3264
|
};
|
|
2616
|
-
var Provider =
|
|
2617
|
-
|
|
3265
|
+
var Provider = z11.discriminatedUnion("kind", [
|
|
3266
|
+
z11.object({
|
|
2618
3267
|
id: Id,
|
|
2619
|
-
kind:
|
|
3268
|
+
kind: z11.literal("smtp"),
|
|
2620
3269
|
config: SmtpConfigBase,
|
|
2621
|
-
has_secret:
|
|
3270
|
+
has_secret: z11.boolean(),
|
|
2622
3271
|
...ProviderFields,
|
|
2623
3272
|
rejections: ProviderRejections,
|
|
2624
3273
|
created_at: Timestamp,
|
|
2625
3274
|
updated_at: Timestamp
|
|
2626
3275
|
}),
|
|
2627
|
-
|
|
3276
|
+
z11.object({
|
|
2628
3277
|
id: Id,
|
|
2629
|
-
kind:
|
|
3278
|
+
kind: z11.literal("resend"),
|
|
2630
3279
|
config: ResendConfigBase,
|
|
2631
|
-
has_secret:
|
|
3280
|
+
has_secret: z11.boolean(),
|
|
2632
3281
|
...ProviderFields,
|
|
2633
3282
|
rejections: ProviderRejections,
|
|
2634
3283
|
events: ProviderEvents,
|
|
@@ -2636,31 +3285,31 @@ var Provider = z10.discriminatedUnion("kind", [
|
|
|
2636
3285
|
updated_at: Timestamp
|
|
2637
3286
|
})
|
|
2638
3287
|
]);
|
|
2639
|
-
var ProviderCreate =
|
|
2640
|
-
|
|
2641
|
-
kind:
|
|
2642
|
-
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) }),
|
|
2643
3292
|
...ProviderFields
|
|
2644
3293
|
}),
|
|
2645
|
-
|
|
2646
|
-
kind:
|
|
2647
|
-
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) }),
|
|
2648
3297
|
...ProviderFields
|
|
2649
3298
|
})
|
|
2650
3299
|
]);
|
|
2651
|
-
var ProviderUpdate =
|
|
2652
|
-
|
|
2653
|
-
kind:
|
|
2654
|
-
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(),
|
|
2655
3304
|
name: ProviderFields.name.optional(),
|
|
2656
3305
|
from_name: ProviderFields.from_name.optional(),
|
|
2657
3306
|
from_email: ProviderFields.from_email.optional(),
|
|
2658
3307
|
reply_to: ProviderFields.reply_to.optional(),
|
|
2659
3308
|
policy: ProviderPolicy.partial().optional()
|
|
2660
3309
|
}),
|
|
2661
|
-
|
|
2662
|
-
kind:
|
|
2663
|
-
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(),
|
|
2664
3313
|
name: ProviderFields.name.optional(),
|
|
2665
3314
|
from_name: ProviderFields.from_name.optional(),
|
|
2666
3315
|
from_email: ProviderFields.from_email.optional(),
|
|
@@ -2668,48 +3317,48 @@ var ProviderUpdate = z10.discriminatedUnion("kind", [
|
|
|
2668
3317
|
policy: ProviderPolicy.partial().optional()
|
|
2669
3318
|
})
|
|
2670
3319
|
]);
|
|
2671
|
-
var ProviderUsage =
|
|
3320
|
+
var ProviderUsage = z11.object({
|
|
2672
3321
|
provider_id: Id,
|
|
2673
|
-
recipients_last_24h:
|
|
2674
|
-
remaining_budget:
|
|
3322
|
+
recipients_last_24h: z11.number().int().min(0),
|
|
3323
|
+
remaining_budget: z11.number().int().min(0),
|
|
2675
3324
|
/** When enough of the rolling window frees up to send again, if exhausted. */
|
|
2676
3325
|
next_capacity_at: Timestamp.nullable()
|
|
2677
3326
|
});
|
|
2678
|
-
var ProviderVerifyResult =
|
|
2679
|
-
ok:
|
|
2680
|
-
error:
|
|
3327
|
+
var ProviderVerifyResult = z11.object({
|
|
3328
|
+
ok: z11.boolean(),
|
|
3329
|
+
error: z11.string().nullable()
|
|
2681
3330
|
});
|
|
2682
3331
|
|
|
2683
3332
|
// src/contacts.ts
|
|
2684
|
-
import { z as
|
|
2685
|
-
var Topic =
|
|
3333
|
+
import { z as z12 } from "zod";
|
|
3334
|
+
var Topic = z12.object({
|
|
2686
3335
|
id: Id,
|
|
2687
3336
|
slug: Slug,
|
|
2688
|
-
name:
|
|
3337
|
+
name: z12.string().min(1).max(120),
|
|
2689
3338
|
/** Shown on the hosted preference page. */
|
|
2690
|
-
description:
|
|
3339
|
+
description: z12.string().max(1e3).nullable(),
|
|
2691
3340
|
/** Translations of `name` and `description` per locale, for the hosted page. */
|
|
2692
|
-
translations:
|
|
2693
|
-
|
|
3341
|
+
translations: z12.record(
|
|
3342
|
+
z12.object({ name: z12.string().min(1).max(120), description: z12.string().max(1e3).nullable() })
|
|
2694
3343
|
),
|
|
2695
3344
|
created_at: Timestamp,
|
|
2696
3345
|
updated_at: Timestamp
|
|
2697
3346
|
});
|
|
2698
|
-
var TopicCreate =
|
|
3347
|
+
var TopicCreate = z12.object({
|
|
2699
3348
|
slug: Slug,
|
|
2700
|
-
name:
|
|
2701
|
-
description:
|
|
3349
|
+
name: z12.string().min(1).max(120),
|
|
3350
|
+
description: z12.string().max(1e3).optional(),
|
|
2702
3351
|
translations: Topic.shape.translations.optional()
|
|
2703
3352
|
});
|
|
2704
3353
|
var TopicUpdate = TopicCreate.omit({ slug: true }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2705
|
-
var Contact =
|
|
3354
|
+
var Contact = z12.object({
|
|
2706
3355
|
id: Id,
|
|
2707
|
-
external_id:
|
|
3356
|
+
external_id: z12.string().min(1).max(255).nullable(),
|
|
2708
3357
|
/** Stored lowercased, unique per workspace. */
|
|
2709
3358
|
email: Email,
|
|
2710
|
-
first_name:
|
|
2711
|
-
last_name:
|
|
2712
|
-
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(),
|
|
2713
3362
|
/**
|
|
2714
3363
|
* The person's own time zone, an IANA name such as `Europe/Berlin` (A1).
|
|
2715
3364
|
* Every wall clock an automation waits for is read in it, falling back to the
|
|
@@ -2717,65 +3366,65 @@ var Contact = z11.object({
|
|
|
2717
3366
|
*/
|
|
2718
3367
|
timezone: TimezoneName.nullable(),
|
|
2719
3368
|
properties: Properties,
|
|
2720
|
-
topics:
|
|
3369
|
+
topics: z12.array(Slug),
|
|
2721
3370
|
/** The slugs of the contact's tags (S4). */
|
|
2722
|
-
tags:
|
|
3371
|
+
tags: z12.array(Slug),
|
|
2723
3372
|
created_at: Timestamp,
|
|
2724
3373
|
updated_at: Timestamp
|
|
2725
3374
|
});
|
|
2726
|
-
var ContactUpsert =
|
|
2727
|
-
external_id:
|
|
3375
|
+
var ContactUpsert = z12.object({
|
|
3376
|
+
external_id: z12.string().min(1).max(255).optional(),
|
|
2728
3377
|
email: Email.optional(),
|
|
2729
|
-
first_name:
|
|
2730
|
-
last_name:
|
|
2731
|
-
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(),
|
|
2732
3381
|
timezone: TimezoneName.nullable().optional(),
|
|
2733
3382
|
properties: Properties.optional(),
|
|
2734
|
-
topics:
|
|
3383
|
+
topics: z12.array(Slug).max(100).optional()
|
|
2735
3384
|
}).refine((v) => v.external_id !== void 0 || v.email !== void 0, {
|
|
2736
3385
|
message: "external_id or email is required",
|
|
2737
3386
|
path: ["email"]
|
|
2738
3387
|
});
|
|
2739
|
-
var ContactUpsertResult =
|
|
3388
|
+
var ContactUpsertResult = z12.object({
|
|
2740
3389
|
contact: Contact,
|
|
2741
|
-
created:
|
|
3390
|
+
created: z12.boolean()
|
|
2742
3391
|
});
|
|
2743
3392
|
var ContactListQuery = PageQuery.extend({
|
|
2744
|
-
email:
|
|
2745
|
-
external_id:
|
|
3393
|
+
email: z12.string().min(1).max(254).optional(),
|
|
3394
|
+
external_id: z12.string().min(1).max(255).optional(),
|
|
2746
3395
|
topic: Slug.optional()
|
|
2747
3396
|
});
|
|
2748
|
-
var ContactErased =
|
|
2749
|
-
ok:
|
|
2750
|
-
erased_messages:
|
|
2751
|
-
erased_recipients:
|
|
2752
|
-
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)
|
|
2753
3402
|
});
|
|
2754
3403
|
var SUPPRESSION_REASONS = ["unsubscribed", "bounced", "complained", "manual"];
|
|
2755
|
-
var SuppressionReason =
|
|
2756
|
-
var Suppression =
|
|
3404
|
+
var SuppressionReason = z12.enum(SUPPRESSION_REASONS);
|
|
3405
|
+
var Suppression = z12.object({
|
|
2757
3406
|
id: Id,
|
|
2758
3407
|
email: Email,
|
|
2759
3408
|
reason: SuppressionReason,
|
|
2760
3409
|
topic: Slug.nullable(),
|
|
2761
3410
|
source_message_id: Id.nullable(),
|
|
2762
|
-
note:
|
|
3411
|
+
note: z12.string().max(1e3).nullable(),
|
|
2763
3412
|
created_at: Timestamp
|
|
2764
3413
|
});
|
|
2765
|
-
var SuppressionCreate =
|
|
3414
|
+
var SuppressionCreate = z12.object({
|
|
2766
3415
|
email: Email,
|
|
2767
|
-
reason:
|
|
3416
|
+
reason: z12.enum(["manual", "unsubscribed"]),
|
|
2768
3417
|
topic: Slug.nullable().optional(),
|
|
2769
|
-
note:
|
|
3418
|
+
note: z12.string().max(1e3).optional()
|
|
2770
3419
|
});
|
|
2771
3420
|
var SuppressionListQuery = PageQuery.extend({
|
|
2772
|
-
email:
|
|
3421
|
+
email: z12.string().min(1).max(254).optional(),
|
|
2773
3422
|
reason: SuppressionReason.optional(),
|
|
2774
3423
|
topic: Slug.optional()
|
|
2775
3424
|
});
|
|
2776
3425
|
|
|
2777
3426
|
// src/webhooks.ts
|
|
2778
|
-
import { z as
|
|
3427
|
+
import { z as z13 } from "zod";
|
|
2779
3428
|
var WEBHOOK_EVENT_TYPES = [
|
|
2780
3429
|
"message.sent",
|
|
2781
3430
|
"message.failed",
|
|
@@ -2819,40 +3468,47 @@ var WEBHOOK_EVENT_TYPES = [
|
|
|
2819
3468
|
"template.updated",
|
|
2820
3469
|
"template.deleted"
|
|
2821
3470
|
];
|
|
2822
|
-
var WebhookEventType =
|
|
2823
|
-
var MessageEventBase =
|
|
3471
|
+
var WebhookEventType = z13.enum(WEBHOOK_EVENT_TYPES);
|
|
3472
|
+
var MessageEventBase = z13.object({
|
|
2824
3473
|
message_id: Id,
|
|
2825
3474
|
mailing_id: Id.nullable(),
|
|
2826
3475
|
mailing_metadata: MailingMetadata.nullable(),
|
|
2827
3476
|
recipient_id: Id.nullable(),
|
|
2828
3477
|
contact_id: Id.nullable(),
|
|
2829
|
-
external_id:
|
|
3478
|
+
external_id: z13.string().nullable(),
|
|
2830
3479
|
email: Email,
|
|
2831
|
-
subject:
|
|
3480
|
+
subject: z13.string(),
|
|
2832
3481
|
topic: Slug.nullable(),
|
|
2833
|
-
provider_message_id:
|
|
2834
|
-
is_test:
|
|
3482
|
+
provider_message_id: z13.string().nullable(),
|
|
3483
|
+
is_test: z13.boolean()
|
|
2835
3484
|
});
|
|
2836
3485
|
var MessageSentData = MessageEventBase.extend({
|
|
2837
3486
|
/** The final HTML as sent, so the client can archive it beside its own records. */
|
|
2838
|
-
html:
|
|
2839
|
-
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()
|
|
2840
3496
|
});
|
|
2841
3497
|
var MessageFailedData = MessageEventBase.extend({
|
|
2842
|
-
error:
|
|
3498
|
+
error: z13.string(),
|
|
2843
3499
|
/** False when the provider rejected permanently; the recipient will not be retried. */
|
|
2844
|
-
retryable:
|
|
3500
|
+
retryable: z13.boolean(),
|
|
2845
3501
|
failed_at: Timestamp
|
|
2846
3502
|
});
|
|
2847
3503
|
var UNSUBSCRIBE_SOURCES = ["hosted_page", "one_click", "api", "dashboard", "signup_form"];
|
|
2848
|
-
var ContactUnsubscribedData =
|
|
3504
|
+
var ContactUnsubscribedData = z13.object({
|
|
2849
3505
|
contact_id: Id.nullable(),
|
|
2850
|
-
external_id:
|
|
3506
|
+
external_id: z13.string().nullable(),
|
|
2851
3507
|
email: Email,
|
|
2852
3508
|
/** The topic unsubscribed from; null means every topic. */
|
|
2853
3509
|
topic: Slug.nullable(),
|
|
2854
3510
|
mailing_id: Id.nullable(),
|
|
2855
|
-
source:
|
|
3511
|
+
source: z13.enum(UNSUBSCRIBE_SOURCES),
|
|
2856
3512
|
unsubscribed_at: Timestamp
|
|
2857
3513
|
});
|
|
2858
3514
|
var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: true }).extend({
|
|
@@ -2861,142 +3517,142 @@ var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: tr
|
|
|
2861
3517
|
* breaker undid a `bounced` block it judged false (a `contact.bounced` for the
|
|
2862
3518
|
* same address and mailing went out before); `topic` is null.
|
|
2863
3519
|
*/
|
|
2864
|
-
source:
|
|
3520
|
+
source: z13.enum([...UNSUBSCRIBE_SOURCES, "bounce_reverted"]),
|
|
2865
3521
|
resubscribed_at: Timestamp
|
|
2866
3522
|
});
|
|
2867
|
-
var ContactBouncedData =
|
|
3523
|
+
var ContactBouncedData = z13.object({
|
|
2868
3524
|
contact_id: Id.nullable(),
|
|
2869
|
-
external_id:
|
|
3525
|
+
external_id: z13.string().nullable(),
|
|
2870
3526
|
email: Email,
|
|
2871
3527
|
/** `bounced` for a hard bounce, `complained` for a spam complaint. */
|
|
2872
|
-
reason:
|
|
3528
|
+
reason: z13.enum(["bounced", "complained"]),
|
|
2873
3529
|
message_id: Id.nullable(),
|
|
2874
|
-
diagnostic:
|
|
3530
|
+
diagnostic: z13.string().nullable(),
|
|
2875
3531
|
bounced_at: Timestamp
|
|
2876
3532
|
});
|
|
2877
|
-
var MailingFinishedData =
|
|
3533
|
+
var MailingFinishedData = z13.object({
|
|
2878
3534
|
mailing_id: Id,
|
|
2879
3535
|
mailing_metadata: MailingMetadata,
|
|
2880
|
-
status:
|
|
3536
|
+
status: z13.enum(["sent", "partially_failed", "cancelled"]),
|
|
2881
3537
|
counts: MailingCounts,
|
|
2882
3538
|
finished_at: Timestamp
|
|
2883
3539
|
});
|
|
2884
|
-
var ContactSubscribedData =
|
|
3540
|
+
var ContactSubscribedData = z13.object({
|
|
2885
3541
|
contact_id: Id,
|
|
2886
|
-
external_id:
|
|
3542
|
+
external_id: z13.string().nullable(),
|
|
2887
3543
|
email: Email,
|
|
2888
|
-
topics:
|
|
2889
|
-
source:
|
|
3544
|
+
topics: z13.array(Slug),
|
|
3545
|
+
source: z13.enum(["signup_form", "api", "dashboard", "automation"]),
|
|
2890
3546
|
signup_form_id: Id.nullable(),
|
|
2891
|
-
signup_form_version:
|
|
3547
|
+
signup_form_version: z13.number().int().min(1).nullable(),
|
|
2892
3548
|
subscribed_at: Timestamp
|
|
2893
3549
|
});
|
|
2894
|
-
var ImportFinishedData =
|
|
3550
|
+
var ImportFinishedData = z13.object({
|
|
2895
3551
|
import_id: Id,
|
|
2896
|
-
status:
|
|
3552
|
+
status: z13.enum(["completed", "cancelled", "failed"]),
|
|
2897
3553
|
result: ImportReport.nullable(),
|
|
2898
|
-
error:
|
|
3554
|
+
error: z13.string().nullable(),
|
|
2899
3555
|
finished_at: Timestamp
|
|
2900
3556
|
});
|
|
2901
|
-
var MailingScheduledData =
|
|
3557
|
+
var MailingScheduledData = z13.object({
|
|
2902
3558
|
mailing_id: Id,
|
|
2903
3559
|
mailing_metadata: MailingMetadata,
|
|
2904
3560
|
scheduled_at: Timestamp.nullable()
|
|
2905
3561
|
});
|
|
2906
|
-
var MailingStartedData =
|
|
3562
|
+
var MailingStartedData = z13.object({
|
|
2907
3563
|
mailing_id: Id,
|
|
2908
3564
|
mailing_metadata: MailingMetadata,
|
|
2909
|
-
trigger:
|
|
2910
|
-
recipients:
|
|
3565
|
+
trigger: z13.enum(["send", "schedule"]),
|
|
3566
|
+
recipients: z13.number().int().min(0),
|
|
2911
3567
|
started_at: Timestamp
|
|
2912
3568
|
});
|
|
2913
|
-
var MailingScheduleFailedData =
|
|
3569
|
+
var MailingScheduleFailedData = z13.object({
|
|
2914
3570
|
mailing_id: Id,
|
|
2915
3571
|
mailing_metadata: MailingMetadata,
|
|
2916
|
-
code:
|
|
2917
|
-
message:
|
|
3572
|
+
code: z13.string(),
|
|
3573
|
+
message: z13.string(),
|
|
2918
3574
|
failed_at: Timestamp
|
|
2919
3575
|
});
|
|
2920
|
-
var MailingAbWinnerSelectedData =
|
|
3576
|
+
var MailingAbWinnerSelectedData = z13.object({
|
|
2921
3577
|
mailing_id: Id,
|
|
2922
3578
|
mailing_metadata: MailingMetadata,
|
|
2923
|
-
winner:
|
|
2924
|
-
decided_by:
|
|
2925
|
-
variants:
|
|
3579
|
+
winner: z13.string(),
|
|
3580
|
+
decided_by: z13.enum(["metric", "manual"]),
|
|
3581
|
+
variants: z13.array(VariantAnalytics),
|
|
2926
3582
|
decided_at: Timestamp
|
|
2927
3583
|
});
|
|
2928
|
-
var ContactTaggedData =
|
|
3584
|
+
var ContactTaggedData = z13.object({
|
|
2929
3585
|
contact_id: Id,
|
|
2930
|
-
external_id:
|
|
3586
|
+
external_id: z13.string().nullable(),
|
|
2931
3587
|
email: Email,
|
|
2932
3588
|
/** Only the tags the contact did not already have. */
|
|
2933
|
-
tags:
|
|
2934
|
-
source:
|
|
3589
|
+
tags: z13.array(Slug).min(1),
|
|
3590
|
+
source: z13.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
2935
3591
|
tagged_at: Timestamp
|
|
2936
3592
|
});
|
|
2937
|
-
var ContactUpdatedData =
|
|
3593
|
+
var ContactUpdatedData = z13.object({
|
|
2938
3594
|
contact_id: Id,
|
|
2939
|
-
external_id:
|
|
3595
|
+
external_id: z13.string().nullable(),
|
|
2940
3596
|
email: Email,
|
|
2941
|
-
changed:
|
|
3597
|
+
changed: z13.array(z13.string()).min(1),
|
|
2942
3598
|
properties: Properties,
|
|
2943
|
-
source:
|
|
3599
|
+
source: z13.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
2944
3600
|
updated_at: Timestamp
|
|
2945
3601
|
});
|
|
2946
|
-
var AutomationRunEventBase =
|
|
3602
|
+
var AutomationRunEventBase = z13.object({
|
|
2947
3603
|
automation_id: Id,
|
|
2948
|
-
automation_name:
|
|
3604
|
+
automation_name: z13.string(),
|
|
2949
3605
|
run_id: Id,
|
|
2950
|
-
version:
|
|
3606
|
+
version: z13.number().int().min(1),
|
|
2951
3607
|
contact_id: Id,
|
|
2952
|
-
external_id:
|
|
3608
|
+
external_id: z13.string().nullable(),
|
|
2953
3609
|
email: Email
|
|
2954
3610
|
});
|
|
2955
3611
|
var AutomationRunStartedData = AutomationRunEventBase.extend({
|
|
2956
3612
|
/** What let them in: a trigger kind, or `manual` for an explicit enrolment. */
|
|
2957
|
-
trigger_kind:
|
|
3613
|
+
trigger_kind: z13.string(),
|
|
2958
3614
|
entered_at: Timestamp
|
|
2959
3615
|
});
|
|
2960
3616
|
var AutomationRunCompletedData = AutomationRunEventBase.extend({
|
|
2961
|
-
last_step_id:
|
|
3617
|
+
last_step_id: z13.string().nullable(),
|
|
2962
3618
|
completed_at: Timestamp
|
|
2963
3619
|
});
|
|
2964
3620
|
var AutomationRunExitedData = AutomationRunEventBase.extend({
|
|
2965
3621
|
reason: AutomationEndReason,
|
|
2966
|
-
step_id:
|
|
3622
|
+
step_id: z13.string().nullable(),
|
|
2967
3623
|
exited_at: Timestamp
|
|
2968
3624
|
});
|
|
2969
3625
|
var AutomationRunFailedData = AutomationRunEventBase.extend({
|
|
2970
|
-
reason:
|
|
2971
|
-
step_id:
|
|
3626
|
+
reason: z13.string(),
|
|
3627
|
+
step_id: z13.string().nullable(),
|
|
2972
3628
|
failed_at: Timestamp
|
|
2973
3629
|
});
|
|
2974
3630
|
var AutomationWebhookData = AutomationRunEventBase.extend({
|
|
2975
|
-
step_id:
|
|
3631
|
+
step_id: z13.string(),
|
|
2976
3632
|
/** The step's own label, when it was given one, so a receiver can branch on a name. */
|
|
2977
|
-
step_label:
|
|
3633
|
+
step_label: z13.string().nullable(),
|
|
2978
3634
|
/** Whatever the step carries, verbatim. Null when it carries nothing. */
|
|
2979
3635
|
payload: JsonValue.nullable(),
|
|
2980
3636
|
fired_at: Timestamp
|
|
2981
3637
|
});
|
|
2982
|
-
var TemplateEventBase =
|
|
3638
|
+
var TemplateEventBase = z13.object({
|
|
2983
3639
|
template_id: Id,
|
|
2984
|
-
name:
|
|
2985
|
-
version:
|
|
2986
|
-
source:
|
|
3640
|
+
name: z13.string().min(1).max(200),
|
|
3641
|
+
version: z13.number().int().min(1),
|
|
3642
|
+
source: z13.enum(["api", "dashboard", "mjml_import"])
|
|
2987
3643
|
});
|
|
2988
3644
|
var TemplateCreatedData = TemplateEventBase.extend({ created_at: Timestamp });
|
|
2989
|
-
var TemplateUpdatedData = TemplateEventBase.extend({ archived:
|
|
3645
|
+
var TemplateUpdatedData = TemplateEventBase.extend({ archived: z13.boolean(), updated_at: Timestamp });
|
|
2990
3646
|
var TemplateDeletedData = TemplateEventBase.extend({ deleted_at: Timestamp });
|
|
2991
|
-
var envelope = (type, data) =>
|
|
3647
|
+
var envelope = (type, data) => z13.object({
|
|
2992
3648
|
/** Unique per event: a receiver deduplicates on it (deliveries may repeat). */
|
|
2993
3649
|
id: Id,
|
|
2994
|
-
type:
|
|
3650
|
+
type: z13.literal(type),
|
|
2995
3651
|
created_at: Timestamp,
|
|
2996
3652
|
workspace_id: Id,
|
|
2997
3653
|
data
|
|
2998
3654
|
});
|
|
2999
|
-
var WebhookEvent =
|
|
3655
|
+
var WebhookEvent = z13.discriminatedUnion("type", [
|
|
3000
3656
|
envelope("message.sent", MessageSentData),
|
|
3001
3657
|
envelope("message.failed", MessageFailedData),
|
|
3002
3658
|
envelope("contact.unsubscribed", ContactUnsubscribedData),
|
|
@@ -3020,16 +3676,16 @@ var WebhookEvent = z12.discriminatedUnion("type", [
|
|
|
3020
3676
|
envelope("template.updated", TemplateUpdatedData),
|
|
3021
3677
|
envelope("template.deleted", TemplateDeletedData)
|
|
3022
3678
|
]);
|
|
3023
|
-
var WebhookEndpoint =
|
|
3679
|
+
var WebhookEndpoint = z13.object({
|
|
3024
3680
|
id: Id,
|
|
3025
|
-
url:
|
|
3026
|
-
description:
|
|
3027
|
-
events:
|
|
3028
|
-
enabled:
|
|
3681
|
+
url: z13.string().url(),
|
|
3682
|
+
description: z13.string().max(500).nullable(),
|
|
3683
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3684
|
+
enabled: z13.boolean(),
|
|
3029
3685
|
created_at: Timestamp,
|
|
3030
3686
|
updated_at: Timestamp
|
|
3031
3687
|
});
|
|
3032
|
-
var WebhookUrl =
|
|
3688
|
+
var WebhookUrl = z13.string().url().refine((u) => {
|
|
3033
3689
|
try {
|
|
3034
3690
|
const url = new URL(u);
|
|
3035
3691
|
if (url.protocol === "https:") return true;
|
|
@@ -3038,35 +3694,35 @@ var WebhookUrl = z12.string().url().refine((u) => {
|
|
|
3038
3694
|
return false;
|
|
3039
3695
|
}
|
|
3040
3696
|
}, "must be https (http only for localhost)");
|
|
3041
|
-
var WebhookEndpointCreate =
|
|
3697
|
+
var WebhookEndpointCreate = z13.object({
|
|
3042
3698
|
url: WebhookUrl,
|
|
3043
|
-
description:
|
|
3044
|
-
events:
|
|
3045
|
-
enabled:
|
|
3699
|
+
description: z13.string().max(500).optional(),
|
|
3700
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3701
|
+
enabled: z13.boolean().optional()
|
|
3046
3702
|
});
|
|
3047
|
-
var WebhookEndpointUpdate =
|
|
3703
|
+
var WebhookEndpointUpdate = z13.object({
|
|
3048
3704
|
url: WebhookUrl,
|
|
3049
|
-
description:
|
|
3050
|
-
events:
|
|
3051
|
-
enabled:
|
|
3705
|
+
description: z13.string().max(500).nullable(),
|
|
3706
|
+
events: z13.array(WebhookEventType).min(1),
|
|
3707
|
+
enabled: z13.boolean()
|
|
3052
3708
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
3053
|
-
var WebhookEndpointWithSecret =
|
|
3709
|
+
var WebhookEndpointWithSecret = z13.object({
|
|
3054
3710
|
endpoint: WebhookEndpoint,
|
|
3055
|
-
secret:
|
|
3711
|
+
secret: z13.string().min(32)
|
|
3056
3712
|
});
|
|
3057
3713
|
var WEBHOOK_DELIVERY_STATUSES = ["pending", "succeeded", "failed"];
|
|
3058
|
-
var WebhookDeliveryStatus =
|
|
3714
|
+
var WebhookDeliveryStatus = z13.enum(WEBHOOK_DELIVERY_STATUSES);
|
|
3059
3715
|
var WEBHOOK_MAX_ATTEMPTS = 8;
|
|
3060
3716
|
var WEBHOOK_RETRY_DELAYS_SECONDS = [30, 120, 600, 1800, 3600, 7200, 21600];
|
|
3061
|
-
var WebhookDelivery =
|
|
3717
|
+
var WebhookDelivery = z13.object({
|
|
3062
3718
|
id: Id,
|
|
3063
3719
|
endpoint_id: Id,
|
|
3064
3720
|
event_id: Id,
|
|
3065
3721
|
event_type: WebhookEventType,
|
|
3066
3722
|
status: WebhookDeliveryStatus,
|
|
3067
|
-
attempts:
|
|
3068
|
-
last_status_code:
|
|
3069
|
-
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(),
|
|
3070
3726
|
next_attempt_at: Timestamp.nullable(),
|
|
3071
3727
|
delivered_at: Timestamp.nullable(),
|
|
3072
3728
|
created_at: Timestamp
|
|
@@ -3075,7 +3731,7 @@ var WebhookDeliveryListQuery = PageQuery.extend({
|
|
|
3075
3731
|
status: WebhookDeliveryStatus.optional(),
|
|
3076
3732
|
event_type: WebhookEventType.optional()
|
|
3077
3733
|
});
|
|
3078
|
-
var WebhookDeliveryParams =
|
|
3734
|
+
var WebhookDeliveryParams = z13.object({ id: Id, delivery_id: Id });
|
|
3079
3735
|
|
|
3080
3736
|
// src/webhook-signing.ts
|
|
3081
3737
|
var WEBHOOK_SIGNATURE_HEADER = "x-mail-signature";
|
|
@@ -3142,79 +3798,79 @@ async function verifyWebhook(input) {
|
|
|
3142
3798
|
}
|
|
3143
3799
|
|
|
3144
3800
|
// src/billing.ts
|
|
3145
|
-
import { z as
|
|
3801
|
+
import { z as z14 } from "zod";
|
|
3146
3802
|
var PLAN_IDS = ["free", "starter", "growth", "design_partner"];
|
|
3147
|
-
var PlanId =
|
|
3148
|
-
var PlanLimits =
|
|
3149
|
-
monthly_messages:
|
|
3150
|
-
contacts:
|
|
3151
|
-
members:
|
|
3152
|
-
providers:
|
|
3153
|
-
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(),
|
|
3154
3810
|
/** Automations that may be live (active or paused) at once (A1). */
|
|
3155
|
-
active_automations:
|
|
3811
|
+
active_automations: z14.number().int().min(0).nullable(),
|
|
3156
3812
|
/** Steps one automation may hold. Null is the flow's own cap, not unlimited. */
|
|
3157
|
-
automation_steps:
|
|
3813
|
+
automation_steps: z14.number().int().min(1).nullable()
|
|
3158
3814
|
});
|
|
3159
|
-
var PlanFeatures =
|
|
3815
|
+
var PlanFeatures = z14.object({
|
|
3160
3816
|
/** A/B tests of subject and content on a mailing (S4). */
|
|
3161
|
-
ab_testing:
|
|
3817
|
+
ab_testing: z14.boolean(),
|
|
3162
3818
|
/** Open and click tracking, still off until a workspace turns it on (`settings.tracking_enabled`). */
|
|
3163
|
-
tracking:
|
|
3819
|
+
tracking: z14.boolean(),
|
|
3164
3820
|
/** Sending and hosting pages on the workspace's own domain (a later phase). */
|
|
3165
|
-
custom_domains:
|
|
3821
|
+
custom_domains: z14.boolean(),
|
|
3166
3822
|
/** Automations: flows a person walks through step by step (A1). */
|
|
3167
|
-
automations:
|
|
3823
|
+
automations: z14.boolean()
|
|
3168
3824
|
});
|
|
3169
3825
|
var PLAN_FEATURES = ["ab_testing", "tracking", "custom_domains", "automations"];
|
|
3170
|
-
var Plan =
|
|
3826
|
+
var Plan = z14.object({
|
|
3171
3827
|
id: PlanId,
|
|
3172
|
-
name:
|
|
3828
|
+
name: z14.string().min(1),
|
|
3173
3829
|
/** Price in the smallest currency unit per month; null for plans not sold. */
|
|
3174
|
-
monthly_price_cents:
|
|
3175
|
-
currency:
|
|
3830
|
+
monthly_price_cents: z14.number().int().min(0).nullable(),
|
|
3831
|
+
currency: z14.string().length(3),
|
|
3176
3832
|
limits: PlanLimits,
|
|
3177
3833
|
features: PlanFeatures
|
|
3178
3834
|
});
|
|
3179
|
-
var CatalogPlan = Plan.extend({ sellable:
|
|
3180
|
-
var Subscription =
|
|
3835
|
+
var CatalogPlan = Plan.extend({ sellable: z14.boolean() });
|
|
3836
|
+
var Subscription = z14.object({
|
|
3181
3837
|
workspace_id: Id,
|
|
3182
3838
|
plan: PlanId,
|
|
3183
|
-
status:
|
|
3839
|
+
status: z14.enum(["active", "trialing", "past_due", "cancelled"]),
|
|
3184
3840
|
current_period_start: Timestamp,
|
|
3185
3841
|
current_period_end: Timestamp,
|
|
3186
|
-
cancel_at_period_end:
|
|
3842
|
+
cancel_at_period_end: z14.boolean(),
|
|
3187
3843
|
/**
|
|
3188
3844
|
* A design partner outside billing (plan `design_partner`, no limits, no
|
|
3189
3845
|
* checkout). Set only by an operator, never through the API.
|
|
3190
3846
|
*/
|
|
3191
|
-
billing_exempt:
|
|
3847
|
+
billing_exempt: z14.boolean()
|
|
3192
3848
|
});
|
|
3193
3849
|
var USAGE_METRICS = ["messages", "contacts", "members", "providers", "webhook_endpoints", "active_automations"];
|
|
3194
|
-
var UsageMetric =
|
|
3850
|
+
var UsageMetric = z14.enum(USAGE_METRICS);
|
|
3195
3851
|
var USAGE_WARNING_RATIO = 0.8;
|
|
3196
|
-
var UsageWarning =
|
|
3852
|
+
var UsageWarning = z14.object({
|
|
3197
3853
|
metric: UsageMetric,
|
|
3198
|
-
used:
|
|
3199
|
-
limit:
|
|
3854
|
+
used: z14.number().int().min(0),
|
|
3855
|
+
limit: z14.number().int().min(0),
|
|
3200
3856
|
/** `approaching`: at or above USAGE_WARNING_RATIO of the limit; `reached`: at or above the limit. */
|
|
3201
|
-
level:
|
|
3857
|
+
level: z14.enum(["approaching", "reached"])
|
|
3202
3858
|
});
|
|
3203
|
-
var Usage =
|
|
3859
|
+
var Usage = z14.object({
|
|
3204
3860
|
plan: PlanId,
|
|
3205
3861
|
period_start: Timestamp,
|
|
3206
3862
|
period_end: Timestamp,
|
|
3207
|
-
metrics:
|
|
3208
|
-
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)
|
|
3209
3865
|
});
|
|
3210
|
-
var CheckoutRequest =
|
|
3211
|
-
plan:
|
|
3212
|
-
success_url:
|
|
3213
|
-
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()
|
|
3214
3870
|
});
|
|
3215
|
-
var CheckoutSession =
|
|
3216
|
-
var PortalRequest =
|
|
3217
|
-
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() });
|
|
3218
3874
|
var USAGE_WARNING_HEADER = "x-mail-usage-warning";
|
|
3219
3875
|
function formatUsageWarningHeader(warnings) {
|
|
3220
3876
|
if (warnings.length === 0) return null;
|
|
@@ -3234,7 +3890,7 @@ function parseUsageWarningHeader(value) {
|
|
|
3234
3890
|
}
|
|
3235
3891
|
|
|
3236
3892
|
// src/routes.ts
|
|
3237
|
-
import { z as
|
|
3893
|
+
import { z as z15 } from "zod";
|
|
3238
3894
|
var list = (item) => page(item);
|
|
3239
3895
|
var foundationRoutes = {
|
|
3240
3896
|
"workspaces.create": {
|
|
@@ -3442,7 +4098,7 @@ var templateRoutes = {
|
|
|
3442
4098
|
path: "/v1/templates/:id/export",
|
|
3443
4099
|
params: IdParams,
|
|
3444
4100
|
query: TemplateExportQuery,
|
|
3445
|
-
response:
|
|
4101
|
+
response: z15.string(),
|
|
3446
4102
|
responseType: "text",
|
|
3447
4103
|
status: 200,
|
|
3448
4104
|
access: "read",
|
|
@@ -3766,7 +4422,7 @@ var sendingRoutes = {
|
|
|
3766
4422
|
path: "/v1/mailings/:id/export",
|
|
3767
4423
|
params: IdParams,
|
|
3768
4424
|
query: MailingExportQuery,
|
|
3769
|
-
response:
|
|
4425
|
+
response: z15.string(),
|
|
3770
4426
|
responseType: "text",
|
|
3771
4427
|
status: 200,
|
|
3772
4428
|
access: "read",
|
|
@@ -3999,7 +4655,7 @@ var platformRoutes = {
|
|
|
3999
4655
|
"contactProperties.list": {
|
|
4000
4656
|
method: "GET",
|
|
4001
4657
|
path: "/v1/contact-properties",
|
|
4002
|
-
response:
|
|
4658
|
+
response: z15.object({ data: z15.array(ContactPropertyDefinition) }),
|
|
4003
4659
|
status: 200,
|
|
4004
4660
|
access: "read",
|
|
4005
4661
|
phase: "S4"
|
|
@@ -4298,7 +4954,7 @@ var billingRoutes = {
|
|
|
4298
4954
|
"billing.plans": {
|
|
4299
4955
|
method: "GET",
|
|
4300
4956
|
path: "/v1/billing/plans",
|
|
4301
|
-
response:
|
|
4957
|
+
response: z15.object({ data: z15.array(CatalogPlan) }),
|
|
4302
4958
|
status: 200,
|
|
4303
4959
|
access: "read",
|
|
4304
4960
|
phase: "S5"
|
|
@@ -4440,7 +5096,7 @@ var automationRoutes = {
|
|
|
4440
5096
|
method: "POST",
|
|
4441
5097
|
path: "/v1/automations/:id/pause",
|
|
4442
5098
|
params: IdParams,
|
|
4443
|
-
body:
|
|
5099
|
+
body: z15.object({}).strict(),
|
|
4444
5100
|
response: Automation,
|
|
4445
5101
|
status: 200,
|
|
4446
5102
|
access: "write",
|
|
@@ -4450,7 +5106,7 @@ var automationRoutes = {
|
|
|
4450
5106
|
method: "POST",
|
|
4451
5107
|
path: "/v1/automations/:id/resume",
|
|
4452
5108
|
params: IdParams,
|
|
4453
|
-
body:
|
|
5109
|
+
body: z15.object({}).strict(),
|
|
4454
5110
|
response: Automation,
|
|
4455
5111
|
status: 200,
|
|
4456
5112
|
access: "write",
|
|
@@ -4460,7 +5116,7 @@ var automationRoutes = {
|
|
|
4460
5116
|
method: "POST",
|
|
4461
5117
|
path: "/v1/automations/:id/archive",
|
|
4462
5118
|
params: IdParams,
|
|
4463
|
-
body:
|
|
5119
|
+
body: z15.object({}).strict(),
|
|
4464
5120
|
response: Automation,
|
|
4465
5121
|
status: 200,
|
|
4466
5122
|
access: "write",
|
|
@@ -4594,13 +5250,13 @@ function matchRoute(method, pathname) {
|
|
|
4594
5250
|
for (const [id, route] of Object.entries(routes)) {
|
|
4595
5251
|
if (route.method !== method) continue;
|
|
4596
5252
|
const names = [];
|
|
4597
|
-
const
|
|
5253
|
+
const pattern2 = new RegExp(
|
|
4598
5254
|
"^" + route.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-z_]+)/g, (_, name) => {
|
|
4599
5255
|
names.push(name);
|
|
4600
5256
|
return "([^/]+)";
|
|
4601
5257
|
}) + "$"
|
|
4602
5258
|
);
|
|
4603
|
-
const match =
|
|
5259
|
+
const match = pattern2.exec(pathname);
|
|
4604
5260
|
if (!match) continue;
|
|
4605
5261
|
const params = {};
|
|
4606
5262
|
names.forEach((name, i) => {
|
|
@@ -4766,6 +5422,8 @@ export {
|
|
|
4766
5422
|
IMPORT_SKIP_REASONS,
|
|
4767
5423
|
IMPORT_STATUSES,
|
|
4768
5424
|
IMPORT_TERMINAL_STATUSES,
|
|
5425
|
+
INSERTION_PROBLEM_REASONS,
|
|
5426
|
+
INSERTION_TEXT_SLOTS,
|
|
4769
5427
|
INVITE_REFUSAL_REASONS,
|
|
4770
5428
|
INVITE_STATUSES,
|
|
4771
5429
|
Id,
|
|
@@ -4783,6 +5441,13 @@ export {
|
|
|
4783
5441
|
ImportStatus,
|
|
4784
5442
|
ImportWarning,
|
|
4785
5443
|
ImportedAsset,
|
|
5444
|
+
Insertion,
|
|
5445
|
+
InsertionChoice,
|
|
5446
|
+
InsertionChoiceKey,
|
|
5447
|
+
InsertionChoices,
|
|
5448
|
+
InsertionName,
|
|
5449
|
+
InsertionText,
|
|
5450
|
+
Insertions,
|
|
4786
5451
|
Invite,
|
|
4787
5452
|
InviteAccept,
|
|
4788
5453
|
InviteAccepted,
|
|
@@ -4810,6 +5475,9 @@ export {
|
|
|
4810
5475
|
MAX_IMPORT_BYTES,
|
|
4811
5476
|
MAX_IMPORT_ROWS,
|
|
4812
5477
|
MAX_IMPORT_URL_LENGTH,
|
|
5478
|
+
MAX_INSERTIONS,
|
|
5479
|
+
MAX_INSERTION_CHOICES,
|
|
5480
|
+
MAX_INSERTION_TEXT_LENGTH,
|
|
4813
5481
|
MAX_INVITE_TTL_DAYS,
|
|
4814
5482
|
MAX_IN_LIST,
|
|
4815
5483
|
MAX_LETTER_RECIPIENTS,
|
|
@@ -4882,6 +5550,8 @@ export {
|
|
|
4882
5550
|
Ok,
|
|
4883
5551
|
PLAN_FEATURES,
|
|
4884
5552
|
PLAN_IDS,
|
|
5553
|
+
PREVIEW_EMPTY,
|
|
5554
|
+
PREVIEW_FILLED,
|
|
4885
5555
|
PROVIDER_KINDS,
|
|
4886
5556
|
PageQuery,
|
|
4887
5557
|
Plan,
|
|
@@ -4982,6 +5652,8 @@ export {
|
|
|
4982
5652
|
TopicCreate,
|
|
4983
5653
|
TopicUpdate,
|
|
4984
5654
|
TrackingSettings,
|
|
5655
|
+
TranslationInsertion,
|
|
5656
|
+
TranslationInsertions,
|
|
4985
5657
|
UNSUBSCRIBE_PATH_PREFIX,
|
|
4986
5658
|
UNSUBSCRIBE_SOURCES,
|
|
4987
5659
|
USAGE_METRICS,
|
|
@@ -5023,7 +5695,10 @@ export {
|
|
|
5023
5695
|
WorkspaceSettings,
|
|
5024
5696
|
WorkspaceUpdate,
|
|
5025
5697
|
acceptsIdempotencyKey,
|
|
5698
|
+
addInsertion,
|
|
5699
|
+
addInsertionChoice,
|
|
5026
5700
|
addLanguageDraft,
|
|
5701
|
+
alignTranslationInsertions,
|
|
5027
5702
|
alwaysWaits,
|
|
5028
5703
|
automationFilterFamily,
|
|
5029
5704
|
automationRoutes,
|
|
@@ -5032,15 +5707,27 @@ export {
|
|
|
5032
5707
|
buildTemplatePatch,
|
|
5033
5708
|
canTransition,
|
|
5034
5709
|
canonicalLocale,
|
|
5710
|
+
choiceKeyProblem,
|
|
5711
|
+
chooseInsertion,
|
|
5035
5712
|
cycles,
|
|
5713
|
+
defaultPreviewPick,
|
|
5036
5714
|
defaultSettings,
|
|
5715
|
+
demoteForSave,
|
|
5716
|
+
demotedBySave,
|
|
5717
|
+
describeMissingTexts,
|
|
5037
5718
|
draftDocument,
|
|
5719
|
+
draftInsertionProblems,
|
|
5038
5720
|
draftsDirty,
|
|
5039
5721
|
draftsFromTemplate,
|
|
5040
5722
|
draftsPatch,
|
|
5723
|
+
editInsertionText,
|
|
5041
5724
|
editLanguageDraft,
|
|
5725
|
+
editMainInsertionText,
|
|
5726
|
+
editMainInsertions,
|
|
5727
|
+
effectivePreviewPicks,
|
|
5042
5728
|
emptyFlow,
|
|
5043
5729
|
errorBody,
|
|
5730
|
+
escapeMergeHtml,
|
|
5044
5731
|
exportFilename,
|
|
5045
5732
|
filterConditions,
|
|
5046
5733
|
filterDepth,
|
|
@@ -5050,30 +5737,64 @@ export {
|
|
|
5050
5737
|
formatExportWarningsHeader,
|
|
5051
5738
|
formatUsageWarningHeader,
|
|
5052
5739
|
foundationRoutes,
|
|
5740
|
+
insertionByProblem,
|
|
5741
|
+
insertionChoices,
|
|
5742
|
+
insertionCoverage,
|
|
5743
|
+
insertionInputs,
|
|
5744
|
+
insertionNameProblem,
|
|
5745
|
+
insertionProblems,
|
|
5746
|
+
insertionRefusalHint,
|
|
5747
|
+
insertionSlotLabel,
|
|
5748
|
+
insertionTextSlots,
|
|
5749
|
+
insertionTokens,
|
|
5750
|
+
insertionValue,
|
|
5053
5751
|
inviteRoutes,
|
|
5054
5752
|
isDayEntirelyPast,
|
|
5055
5753
|
isPublishable,
|
|
5056
5754
|
isRetryableError,
|
|
5057
5755
|
keepLanguageDocument,
|
|
5756
|
+
languageInsertions,
|
|
5058
5757
|
languageName,
|
|
5059
5758
|
languageTabs,
|
|
5759
|
+
mainInsertionText,
|
|
5060
5760
|
matchLocale,
|
|
5061
5761
|
matchRoute,
|
|
5762
|
+
missingInsertionTexts,
|
|
5062
5763
|
missingRequiredMergeFields,
|
|
5764
|
+
normalizeChoiceKey,
|
|
5063
5765
|
nowSeconds,
|
|
5064
5766
|
operationsWithAccess,
|
|
5767
|
+
ownEntry,
|
|
5065
5768
|
page,
|
|
5066
5769
|
parseExportWarningsHeader,
|
|
5067
5770
|
parseUsageWarningHeader,
|
|
5771
|
+
personalizeHtml,
|
|
5772
|
+
personalizeText,
|
|
5773
|
+
placementProblems,
|
|
5068
5774
|
platformRoutes,
|
|
5775
|
+
previewControls,
|
|
5776
|
+
previewInsertions,
|
|
5777
|
+
previewPersonalization,
|
|
5069
5778
|
primaryLanguage,
|
|
5070
5779
|
reachable,
|
|
5780
|
+
readyWithoutInsertions,
|
|
5071
5781
|
readyWithoutSubject,
|
|
5072
5782
|
rebaseDrafts,
|
|
5783
|
+
removeInsertion,
|
|
5784
|
+
removeInsertionChoice,
|
|
5073
5785
|
removeLanguageDraft,
|
|
5786
|
+
renameInsertion,
|
|
5787
|
+
renameInsertionChecked,
|
|
5788
|
+
renameInsertionChoice,
|
|
5789
|
+
renameInsertionChoiceChecked,
|
|
5790
|
+
resolveInsertions,
|
|
5074
5791
|
restorePatch,
|
|
5075
5792
|
routes,
|
|
5793
|
+
sampleMergeValue,
|
|
5794
|
+
saveDemotions,
|
|
5076
5795
|
sendingRoutes,
|
|
5796
|
+
setInsertionBy,
|
|
5797
|
+
setInsertionFilled,
|
|
5077
5798
|
setLanguageStatus,
|
|
5078
5799
|
signWebhook,
|
|
5079
5800
|
statusForError,
|
|
@@ -5084,7 +5805,10 @@ export {
|
|
|
5084
5805
|
testGroupSize,
|
|
5085
5806
|
tightLoops,
|
|
5086
5807
|
timingSafeEqualString,
|
|
5808
|
+
translationInsertionProblems,
|
|
5809
|
+
translationTextFromInput,
|
|
5087
5810
|
triggerKey,
|
|
5811
|
+
unplacedInsertions,
|
|
5088
5812
|
validateAutomation,
|
|
5089
5813
|
verifyWebhook
|
|
5090
5814
|
};
|