@marlinjai/mail-contract 0.4.0 → 0.6.0

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