@marlinjai/mail-contract 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +16526 -2594
- package/dist/index.d.ts +16526 -2594
- package/dist/index.js +1044 -763
- package/dist/index.mjs +1017 -763
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -145,13 +145,13 @@ var API_VERSION_PREFIX = "/v1";
|
|
|
145
145
|
var HEALTH_PATH = "/healthz";
|
|
146
146
|
|
|
147
147
|
// src/workspace.ts
|
|
148
|
-
import { z as
|
|
148
|
+
import { z as z10 } from "zod";
|
|
149
149
|
|
|
150
150
|
// src/automations.ts
|
|
151
|
-
import { z as
|
|
151
|
+
import { z as z9 } from "zod";
|
|
152
152
|
|
|
153
153
|
// src/templates.ts
|
|
154
|
-
import { z as
|
|
154
|
+
import { z as z5 } from "zod";
|
|
155
155
|
|
|
156
156
|
// src/insertions.ts
|
|
157
157
|
import { z as z3 } from "zod";
|
|
@@ -442,22 +442,133 @@ function sampleMergeValue(values) {
|
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
// src/template-inputs.ts
|
|
446
|
+
import { z as z4 } from "zod";
|
|
447
|
+
var MAX_TEMPLATE_INPUTS = 50;
|
|
448
|
+
var MAX_TEMPLATE_INPUT_VALUES = 30;
|
|
449
|
+
var MAX_TEMPLATE_INPUT_LABEL_LENGTH = 80;
|
|
450
|
+
var MAX_TEMPLATE_INPUT_NOTE_LENGTH = 300;
|
|
451
|
+
var MAX_TEMPLATE_INPUT_EXAMPLE_LENGTH = 500;
|
|
452
|
+
var MAX_TEMPLATE_INPUTS_APP_LENGTH = 80;
|
|
453
|
+
var MAX_TEMPLATE_INPUTS_SEEN = 200;
|
|
454
|
+
var SERVICE_FILLED_MERGE_FIELDS = ["email", "unsubscribe_url"];
|
|
455
|
+
var line = (max) => z4.string().min(1).max(max).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters").refine((s) => s.trim() !== "", "not blank");
|
|
456
|
+
var label = line(MAX_TEMPLATE_INPUT_LABEL_LENGTH);
|
|
457
|
+
var note = line(MAX_TEMPLATE_INPUT_NOTE_LENGTH);
|
|
458
|
+
var TemplateInputName = InsertionName;
|
|
459
|
+
var TemplateInputSource = z4.discriminatedUnion("kind", [
|
|
460
|
+
z4.object({ kind: z4.literal("form_field"), form: label, field: label, note: note.optional() }),
|
|
461
|
+
z4.object({ kind: z4.literal("derived"), note })
|
|
462
|
+
]);
|
|
463
|
+
var TemplateInputValue = z4.object({
|
|
464
|
+
/** The key the app sends, in an insertion choice's syntax, so a row "When it is venue" matches it exactly. */
|
|
465
|
+
value: InsertionChoiceKey,
|
|
466
|
+
label
|
|
467
|
+
});
|
|
468
|
+
var HttpUrl = z4.string().max(MAX_TEMPLATE_INPUT_EXAMPLE_LENGTH).url().refine((u) => {
|
|
469
|
+
try {
|
|
470
|
+
const { protocol } = new URL(u);
|
|
471
|
+
return protocol === "https:" || protocol === "http:";
|
|
472
|
+
} catch {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}, "an http or https address");
|
|
476
|
+
var common = {
|
|
477
|
+
/** What an author sees: "Contact reason", "First name". */
|
|
478
|
+
label,
|
|
479
|
+
source: TemplateInputSource,
|
|
480
|
+
/** The app may send it empty or not at all (a name nobody typed). Absent means it is always sent. */
|
|
481
|
+
optional: z4.boolean().optional()
|
|
482
|
+
};
|
|
483
|
+
var TemplateInput = z4.discriminatedUnion("kind", [
|
|
484
|
+
z4.object({ kind: z4.literal("text"), ...common, example: z4.string().max(MAX_TEMPLATE_INPUT_EXAMPLE_LENGTH).optional() }),
|
|
485
|
+
z4.object({ kind: z4.literal("url"), ...common, example: HttpUrl.optional() }),
|
|
486
|
+
z4.object({ kind: z4.literal("boolean"), ...common, example: z4.boolean().optional() }),
|
|
487
|
+
z4.object({
|
|
488
|
+
kind: z4.literal("one_of"),
|
|
489
|
+
...common,
|
|
490
|
+
values: z4.array(TemplateInputValue).min(1).max(MAX_TEMPLATE_INPUT_VALUES),
|
|
491
|
+
example: InsertionChoiceKey.optional()
|
|
492
|
+
})
|
|
493
|
+
]);
|
|
494
|
+
var TEMPLATE_INPUT_KINDS = ["text", "url", "boolean", "one_of"];
|
|
495
|
+
var TemplateInputMap = z4.record(TemplateInputName, TemplateInput).refine((m) => Object.keys(m).length <= MAX_TEMPLATE_INPUTS, `at most ${MAX_TEMPLATE_INPUTS} inputs`);
|
|
496
|
+
var TemplateInputsDeclaration = z4.object({
|
|
497
|
+
app: line(MAX_TEMPLATE_INPUTS_APP_LENGTH),
|
|
498
|
+
app_version: line(MAX_TEMPLATE_INPUTS_APP_LENGTH).optional(),
|
|
499
|
+
inputs: TemplateInputMap
|
|
500
|
+
});
|
|
501
|
+
var TemplateInputs = z4.object({
|
|
502
|
+
app: z4.string(),
|
|
503
|
+
app_version: z4.string().nullable(),
|
|
504
|
+
inputs: TemplateInputMap,
|
|
505
|
+
declared_by: z4.object({ api_key_id: Id, api_key_name: z4.string() }),
|
|
506
|
+
declared_at: Timestamp
|
|
507
|
+
});
|
|
508
|
+
var TemplateInputSeen = z4.object({
|
|
509
|
+
count: z4.number().int().min(1),
|
|
510
|
+
first_seen_at: Timestamp,
|
|
511
|
+
last_seen_at: Timestamp
|
|
512
|
+
});
|
|
513
|
+
var TemplateInputsSeen = z4.record(TemplateInputName, TemplateInputSeen);
|
|
514
|
+
var TemplateInputsDeclared = z4.object({
|
|
515
|
+
template_id: Id,
|
|
516
|
+
inputs: TemplateInputs,
|
|
517
|
+
/** False when the stored declaration already said exactly this (a redeploy of the same commit). */
|
|
518
|
+
changed: z4.boolean()
|
|
519
|
+
});
|
|
520
|
+
var TEMPLATE_INPUT_PROBLEM_REASONS = [
|
|
521
|
+
/** `email` or `unsubscribe_url`: the service fills it, a client's value is never read. */
|
|
522
|
+
"input_reserved",
|
|
523
|
+
/** A `one_of` input lists the same value twice. */
|
|
524
|
+
"input_values",
|
|
525
|
+
/** A `one_of` input's example is not one of its values. */
|
|
526
|
+
"input_example"
|
|
527
|
+
];
|
|
528
|
+
function templateInputProblems(inputs) {
|
|
529
|
+
const problems = [];
|
|
530
|
+
for (const [name, input] of Object.entries(inputs)) {
|
|
531
|
+
if (SERVICE_FILLED_MERGE_FIELDS.includes(name)) {
|
|
532
|
+
problems.push({ reason: "input_reserved", path: name, message: `"${name}" is filled by the service, never by the app; leave it out of the declaration` });
|
|
533
|
+
}
|
|
534
|
+
if (input.kind !== "one_of") continue;
|
|
535
|
+
const seen = /* @__PURE__ */ new Set();
|
|
536
|
+
input.values.forEach((v, i) => {
|
|
537
|
+
if (seen.has(v.value)) {
|
|
538
|
+
problems.push({ reason: "input_values", path: `${name}.values.${i}`, message: `"${v.value}" is listed twice` });
|
|
539
|
+
}
|
|
540
|
+
seen.add(v.value);
|
|
541
|
+
});
|
|
542
|
+
if (input.example !== void 0 && !seen.has(input.example)) {
|
|
543
|
+
problems.push({ reason: "input_example", path: `${name}.example`, message: `the example "${input.example}" is not one of the values` });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return problems;
|
|
547
|
+
}
|
|
548
|
+
function templateInputDrift(inputs, seen) {
|
|
549
|
+
const declared = inputs ? Object.keys(inputs.inputs) : [];
|
|
550
|
+
return {
|
|
551
|
+
declared_not_seen: declared.filter((name) => ownEntry(seen, name) === void 0).sort(),
|
|
552
|
+
seen_not_declared: Object.keys(seen).filter((name) => ownEntry(inputs?.inputs, name) === void 0).sort()
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
445
556
|
// src/templates.ts
|
|
446
557
|
var DOCUMENT_SCHEMA_VERSIONS = ["1.0", "1.1"];
|
|
447
|
-
var DocumentSchemaVersion =
|
|
448
|
-
var TemplateDocument =
|
|
558
|
+
var DocumentSchemaVersion = z5.enum(DOCUMENT_SCHEMA_VERSIONS);
|
|
559
|
+
var TemplateDocument = z5.object({
|
|
449
560
|
version: DocumentSchemaVersion,
|
|
450
|
-
metadata:
|
|
451
|
-
sections:
|
|
561
|
+
metadata: z5.record(z5.unknown()),
|
|
562
|
+
sections: z5.array(z5.record(z5.unknown()))
|
|
452
563
|
}).passthrough();
|
|
453
564
|
var MAX_DOCUMENT_BYTES = 1e6;
|
|
454
565
|
var MAX_SUBJECT_LENGTH = 998;
|
|
455
566
|
var MAX_PREHEADER_LENGTH = 500;
|
|
456
|
-
var TemplateSubject =
|
|
457
|
-
var TemplatePreheader =
|
|
567
|
+
var TemplateSubject = z5.string().max(MAX_SUBJECT_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
|
|
568
|
+
var TemplatePreheader = z5.string().max(MAX_PREHEADER_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
|
|
458
569
|
var TEMPLATE_LANGUAGE_STATUSES = ["draft", "ready"];
|
|
459
|
-
var TemplateLanguageStatus =
|
|
460
|
-
var TemplateTranslation =
|
|
570
|
+
var TemplateLanguageStatus = z5.enum(TEMPLATE_LANGUAGE_STATUSES);
|
|
571
|
+
var TemplateTranslation = z5.object({
|
|
461
572
|
subject: TemplateSubject.nullable(),
|
|
462
573
|
preheader: TemplatePreheader.nullable(),
|
|
463
574
|
document: TemplateDocument,
|
|
@@ -475,11 +586,11 @@ var TemplateTranslation = z4.object({
|
|
|
475
586
|
insertions: TranslationInsertions.optional()
|
|
476
587
|
});
|
|
477
588
|
var MAX_TEMPLATE_TRANSLATIONS = 19;
|
|
478
|
-
var TemplateTranslations =
|
|
479
|
-
var Template =
|
|
589
|
+
var TemplateTranslations = z5.record(LocaleTag, TemplateTranslation).refine((t) => Object.keys(t).length <= MAX_TEMPLATE_TRANSLATIONS, `at most ${MAX_TEMPLATE_TRANSLATIONS} language versions`);
|
|
590
|
+
var Template = z5.object({
|
|
480
591
|
id: Id,
|
|
481
|
-
name:
|
|
482
|
-
description:
|
|
592
|
+
name: z5.string().min(1).max(200),
|
|
593
|
+
description: z5.string().max(2e3).nullable(),
|
|
483
594
|
/**
|
|
484
595
|
* The main language: what `subject`, `preheader` and `document` are written
|
|
485
596
|
* in, and what every recipient without a ready version of their own gets.
|
|
@@ -503,21 +614,33 @@ var Template = z4.object({
|
|
|
503
614
|
* texts in `translations[tag].insertions`.
|
|
504
615
|
*/
|
|
505
616
|
insertions: Insertions,
|
|
617
|
+
/**
|
|
618
|
+
* What the sending app declared this template receives
|
|
619
|
+
* (`templates.declareInputs`); null until an app declares. Not part of a
|
|
620
|
+
* version: declaring never changes `version` or `updated_at`.
|
|
621
|
+
*/
|
|
622
|
+
inputs: TemplateInputs.nullable(),
|
|
623
|
+
/**
|
|
624
|
+
* The merge names real sends of this template carried, with a count and
|
|
625
|
+
* when first and last seen. Names only, never values. `{}` before the
|
|
626
|
+
* first send.
|
|
627
|
+
*/
|
|
628
|
+
inputs_seen: TemplateInputsSeen,
|
|
506
629
|
/** Increments on every saved change, of any language. */
|
|
507
|
-
version:
|
|
508
|
-
thumbnail_url:
|
|
630
|
+
version: z5.number().int().min(1),
|
|
631
|
+
thumbnail_url: z5.string().url().nullable(),
|
|
509
632
|
archived_at: Timestamp.nullable(),
|
|
510
633
|
created_at: Timestamp,
|
|
511
634
|
updated_at: Timestamp
|
|
512
635
|
});
|
|
513
|
-
var TemplateSummary = Template.omit({ document: true, translations: true, insertions: true }).extend({
|
|
514
|
-
languages:
|
|
636
|
+
var TemplateSummary = Template.omit({ document: true, translations: true, insertions: true, inputs: true, inputs_seen: true }).extend({
|
|
637
|
+
languages: z5.record(LocaleTag, TemplateLanguageStatus),
|
|
515
638
|
/** The names of the template's insertions, sorted alphabetically. */
|
|
516
|
-
insertion_names:
|
|
639
|
+
insertion_names: z5.array(z5.string())
|
|
517
640
|
});
|
|
518
|
-
var TemplateCreate =
|
|
519
|
-
name:
|
|
520
|
-
description:
|
|
641
|
+
var TemplateCreate = z5.object({
|
|
642
|
+
name: z5.string().min(1).max(200),
|
|
643
|
+
description: z5.string().max(2e3).optional(),
|
|
521
644
|
locale: LocaleTag.optional(),
|
|
522
645
|
subject: TemplateSubject.nullable().optional(),
|
|
523
646
|
preheader: TemplatePreheader.nullable().optional(),
|
|
@@ -525,15 +648,15 @@ var TemplateCreate = z4.object({
|
|
|
525
648
|
translations: TemplateTranslations.optional(),
|
|
526
649
|
insertions: Insertions.optional()
|
|
527
650
|
});
|
|
528
|
-
var TemplateUpdate =
|
|
529
|
-
base_version:
|
|
530
|
-
name:
|
|
531
|
-
description:
|
|
651
|
+
var TemplateUpdate = z5.object({
|
|
652
|
+
base_version: z5.number().int().min(1),
|
|
653
|
+
name: z5.string().min(1).max(200).optional(),
|
|
654
|
+
description: z5.string().max(2e3).nullable().optional(),
|
|
532
655
|
locale: LocaleTag.optional(),
|
|
533
656
|
subject: TemplateSubject.nullable().optional(),
|
|
534
657
|
preheader: TemplatePreheader.nullable().optional(),
|
|
535
658
|
document: TemplateDocument.optional(),
|
|
536
|
-
translations:
|
|
659
|
+
translations: z5.record(LocaleTag, TemplateTranslation.nullable()).optional(),
|
|
537
660
|
/**
|
|
538
661
|
* The main language's insertions, whole: what is left out is removed, from
|
|
539
662
|
* every language in the same save. A rename is a removal and an addition,
|
|
@@ -541,87 +664,87 @@ var TemplateUpdate = z4.object({
|
|
|
541
664
|
* save (it sends those languages in `translations`).
|
|
542
665
|
*/
|
|
543
666
|
insertions: Insertions.optional(),
|
|
544
|
-
archived:
|
|
667
|
+
archived: z5.boolean().optional()
|
|
545
668
|
}).refine(
|
|
546
669
|
(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,
|
|
547
670
|
"at least one field besides base_version"
|
|
548
671
|
);
|
|
549
|
-
var SavedSectionBody =
|
|
672
|
+
var SavedSectionBody = z5.record(z5.unknown());
|
|
550
673
|
var MAX_SAVED_SECTIONS = 50;
|
|
551
|
-
var SavedSection =
|
|
674
|
+
var SavedSection = z5.object({
|
|
552
675
|
id: Id,
|
|
553
|
-
name:
|
|
554
|
-
description:
|
|
676
|
+
name: z5.string().min(1).max(200),
|
|
677
|
+
description: z5.string().max(2e3).nullable(),
|
|
555
678
|
section: SavedSectionBody,
|
|
556
|
-
created_by:
|
|
679
|
+
created_by: z5.string().nullable(),
|
|
557
680
|
created_at: Timestamp,
|
|
558
681
|
updated_at: Timestamp
|
|
559
682
|
});
|
|
560
|
-
var SavedSectionCreate =
|
|
561
|
-
name:
|
|
562
|
-
description:
|
|
683
|
+
var SavedSectionCreate = z5.object({
|
|
684
|
+
name: z5.string().min(1).max(200),
|
|
685
|
+
description: z5.string().max(2e3).optional(),
|
|
563
686
|
section: SavedSectionBody
|
|
564
687
|
});
|
|
565
|
-
var SavedSectionUpdate =
|
|
566
|
-
name:
|
|
567
|
-
description:
|
|
688
|
+
var SavedSectionUpdate = z5.object({
|
|
689
|
+
name: z5.string().min(1).max(200).optional(),
|
|
690
|
+
description: z5.string().max(2e3).nullable().optional(),
|
|
568
691
|
section: SavedSectionBody.optional()
|
|
569
692
|
}).refine((v) => v.name !== void 0 || v.description !== void 0 || v.section !== void 0, "at least one field");
|
|
570
693
|
var TemplateListQuery = PageQuery.extend({
|
|
571
|
-
archived:
|
|
694
|
+
archived: z5.enum(["true", "false"]).optional()
|
|
572
695
|
});
|
|
573
|
-
var TemplateVersion =
|
|
696
|
+
var TemplateVersion = z5.object({
|
|
574
697
|
template_id: Id,
|
|
575
|
-
version:
|
|
698
|
+
version: z5.number().int().min(1),
|
|
576
699
|
locale: LocaleTag,
|
|
577
700
|
subject: TemplateSubject.nullable(),
|
|
578
701
|
preheader: TemplatePreheader.nullable(),
|
|
579
702
|
document: TemplateDocument,
|
|
580
703
|
translations: TemplateTranslations,
|
|
581
704
|
insertions: Insertions,
|
|
582
|
-
created_by:
|
|
705
|
+
created_by: z5.string().nullable(),
|
|
583
706
|
created_at: Timestamp
|
|
584
707
|
});
|
|
585
|
-
var TemplateVersionParams =
|
|
708
|
+
var TemplateVersionParams = z5.object({
|
|
586
709
|
id: Id,
|
|
587
|
-
version:
|
|
710
|
+
version: z5.coerce.number().int().min(1)
|
|
588
711
|
});
|
|
589
|
-
var CompileMessage =
|
|
590
|
-
message:
|
|
712
|
+
var CompileMessage = z5.object({
|
|
713
|
+
message: z5.string().min(1),
|
|
591
714
|
/** Where in the document or the MJML the message points, when known. */
|
|
592
|
-
path:
|
|
593
|
-
line:
|
|
715
|
+
path: z5.string().optional(),
|
|
716
|
+
line: z5.number().int().min(1).optional()
|
|
594
717
|
});
|
|
595
|
-
var CompileResult =
|
|
596
|
-
mjml:
|
|
597
|
-
html:
|
|
598
|
-
warnings:
|
|
599
|
-
errors:
|
|
718
|
+
var CompileResult = z5.object({
|
|
719
|
+
mjml: z5.string(),
|
|
720
|
+
html: z5.string(),
|
|
721
|
+
warnings: z5.array(CompileMessage),
|
|
722
|
+
errors: z5.array(CompileMessage)
|
|
600
723
|
});
|
|
601
|
-
var TemplateCompileRequest =
|
|
602
|
-
version:
|
|
724
|
+
var TemplateCompileRequest = z5.object({
|
|
725
|
+
version: z5.number().int().min(1).optional(),
|
|
603
726
|
locale: LocaleTag.optional()
|
|
604
727
|
});
|
|
605
|
-
var CompileRequest =
|
|
728
|
+
var CompileRequest = z5.object({
|
|
606
729
|
document: TemplateDocument
|
|
607
730
|
});
|
|
608
731
|
var ASSET_CONTENT_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
609
|
-
var AssetContentType =
|
|
732
|
+
var AssetContentType = z5.enum(ASSET_CONTENT_TYPES);
|
|
610
733
|
var MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
611
|
-
var Asset =
|
|
734
|
+
var Asset = z5.object({
|
|
612
735
|
id: Id,
|
|
613
|
-
url:
|
|
736
|
+
url: z5.string().url(),
|
|
614
737
|
content_type: AssetContentType,
|
|
615
|
-
size_bytes:
|
|
616
|
-
width:
|
|
617
|
-
height:
|
|
618
|
-
filename:
|
|
738
|
+
size_bytes: z5.number().int().min(1).max(MAX_ASSET_BYTES),
|
|
739
|
+
width: z5.number().int().min(1).nullable(),
|
|
740
|
+
height: z5.number().int().min(1).nullable(),
|
|
741
|
+
filename: z5.string().min(1).max(255),
|
|
619
742
|
created_at: Timestamp
|
|
620
743
|
});
|
|
621
744
|
var ASSET_UPLOAD_FIELD = "file";
|
|
622
745
|
var MAX_IMPORT_URL_LENGTH = 2048;
|
|
623
|
-
var AssetImport =
|
|
624
|
-
url:
|
|
746
|
+
var AssetImport = z5.object({
|
|
747
|
+
url: z5.string().max(MAX_IMPORT_URL_LENGTH).url().refine((u) => {
|
|
625
748
|
try {
|
|
626
749
|
const url = new URL(u);
|
|
627
750
|
if (url.protocol === "https:") return true;
|
|
@@ -631,7 +754,7 @@ var AssetImport = z4.object({
|
|
|
631
754
|
}
|
|
632
755
|
}, "must be https (http only for localhost)"),
|
|
633
756
|
/** The stored file name; derived from the address when omitted. */
|
|
634
|
-
filename:
|
|
757
|
+
filename: z5.string().min(1).max(255).optional()
|
|
635
758
|
});
|
|
636
759
|
var MAX_MJML_IMPORT_BYTES = 512 * 1024;
|
|
637
760
|
var MAX_MJML_DEPTH = 32;
|
|
@@ -646,56 +769,56 @@ var MJML_IMPORT_REFUSALS = [
|
|
|
646
769
|
"too_complex",
|
|
647
770
|
"invalid_document"
|
|
648
771
|
];
|
|
649
|
-
var ImportWarning =
|
|
650
|
-
severity:
|
|
772
|
+
var ImportWarning = z5.object({
|
|
773
|
+
severity: z5.enum(["info", "warning"]),
|
|
651
774
|
/** Stable kind, e.g. `kept_as_html`, `unknown_component`, `remote_image_not_imported`. */
|
|
652
|
-
code:
|
|
775
|
+
code: z5.string().min(1).max(64),
|
|
653
776
|
/** Where in the source, e.g. `mj-body > mj-section[2] > mj-column[1] > mj-social[1]`. */
|
|
654
|
-
path:
|
|
655
|
-
line:
|
|
656
|
-
message:
|
|
657
|
-
fragment:
|
|
777
|
+
path: z5.string(),
|
|
778
|
+
line: z5.number().int().min(1).optional(),
|
|
779
|
+
message: z5.string().min(1),
|
|
780
|
+
fragment: z5.string().optional()
|
|
658
781
|
});
|
|
659
|
-
var RemoteImage =
|
|
660
|
-
url:
|
|
782
|
+
var RemoteImage = z5.object({
|
|
783
|
+
url: z5.string().min(1),
|
|
661
784
|
/** The element and attribute it is loaded from, e.g. `<img src>`. */
|
|
662
|
-
where:
|
|
785
|
+
where: z5.string()
|
|
663
786
|
});
|
|
664
|
-
var ImportedAsset =
|
|
665
|
-
source_url:
|
|
787
|
+
var ImportedAsset = z5.object({
|
|
788
|
+
source_url: z5.string().min(1),
|
|
666
789
|
asset_id: Id,
|
|
667
|
-
url:
|
|
790
|
+
url: z5.string().url()
|
|
668
791
|
});
|
|
669
|
-
var mjmlSource =
|
|
670
|
-
var TemplateImportPreviewRequest =
|
|
671
|
-
var TemplateImportPreview =
|
|
792
|
+
var mjmlSource = z5.string().min(1, "paste or upload the MJML");
|
|
793
|
+
var TemplateImportPreviewRequest = z5.object({ mjml: mjmlSource });
|
|
794
|
+
var TemplateImportPreview = z5.object({
|
|
672
795
|
document: TemplateDocument,
|
|
673
|
-
warnings:
|
|
796
|
+
warnings: z5.array(ImportWarning),
|
|
674
797
|
/** The document compiled under the workspace's asset policy, exactly as a send would. */
|
|
675
798
|
compiled: CompileResult,
|
|
676
799
|
/** Images loaded from outside the service; under `service_only` each is also a compile error. */
|
|
677
|
-
remote_images:
|
|
678
|
-
asset_policy:
|
|
800
|
+
remote_images: z5.array(RemoteImage),
|
|
801
|
+
asset_policy: z5.enum(["any", "service_only"])
|
|
679
802
|
});
|
|
680
|
-
var TemplateImport =
|
|
681
|
-
name:
|
|
682
|
-
description:
|
|
803
|
+
var TemplateImport = z5.object({
|
|
804
|
+
name: z5.string().min(1).max(200),
|
|
805
|
+
description: z5.string().max(2e3).optional(),
|
|
683
806
|
mjml: mjmlSource,
|
|
684
|
-
import_remote_assets:
|
|
807
|
+
import_remote_assets: z5.boolean().optional()
|
|
685
808
|
});
|
|
686
|
-
var TemplateImportResult =
|
|
809
|
+
var TemplateImportResult = z5.object({
|
|
687
810
|
template: Template,
|
|
688
|
-
warnings:
|
|
689
|
-
imported_assets:
|
|
811
|
+
warnings: z5.array(ImportWarning),
|
|
812
|
+
imported_assets: z5.array(ImportedAsset)
|
|
690
813
|
});
|
|
691
814
|
var EXPORT_FORMATS = ["mjml", "html"];
|
|
692
|
-
var ExportFormat =
|
|
693
|
-
var TemplateExportQuery =
|
|
815
|
+
var ExportFormat = z5.enum(EXPORT_FORMATS);
|
|
816
|
+
var TemplateExportQuery = z5.object({
|
|
694
817
|
format: ExportFormat,
|
|
695
|
-
version:
|
|
818
|
+
version: z5.coerce.number().int().min(1).optional(),
|
|
696
819
|
locale: LocaleTag.optional()
|
|
697
820
|
});
|
|
698
|
-
var MailingExportQuery =
|
|
821
|
+
var MailingExportQuery = z5.object({ format: ExportFormat });
|
|
699
822
|
var EXPORT_CONTENT_TYPES = {
|
|
700
823
|
mjml: "text/plain; charset=utf-8",
|
|
701
824
|
html: "text/html; charset=utf-8"
|
|
@@ -714,7 +837,7 @@ function formatExportWarningsHeader(messages) {
|
|
|
714
837
|
function parseExportWarningsHeader(value) {
|
|
715
838
|
if (!value) return [];
|
|
716
839
|
try {
|
|
717
|
-
const parsed =
|
|
840
|
+
const parsed = z5.array(CompileMessage).safeParse(JSON.parse(decodeURIComponent(value)));
|
|
718
841
|
return parsed.success ? parsed.data : [];
|
|
719
842
|
} catch {
|
|
720
843
|
return [];
|
|
@@ -726,32 +849,32 @@ function exportFilename(name, format) {
|
|
|
726
849
|
}
|
|
727
850
|
|
|
728
851
|
// src/platform.ts
|
|
729
|
-
import { z as
|
|
852
|
+
import { z as z8 } from "zod";
|
|
730
853
|
|
|
731
854
|
// src/mailings.ts
|
|
732
|
-
import { z as
|
|
855
|
+
import { z as z7 } from "zod";
|
|
733
856
|
|
|
734
857
|
// src/ab-test.ts
|
|
735
|
-
import { z as
|
|
736
|
-
var AbVariant =
|
|
737
|
-
key:
|
|
738
|
-
subject:
|
|
858
|
+
import { z as z6 } from "zod";
|
|
859
|
+
var AbVariant = z6.object({
|
|
860
|
+
key: z6.string().regex(/^[a-z]$/, "a single lowercase letter"),
|
|
861
|
+
subject: z6.string().min(1).max(998).optional(),
|
|
739
862
|
/** Replaces the mailing's document for this variant. */
|
|
740
|
-
document:
|
|
863
|
+
document: z6.record(z6.unknown()).optional(),
|
|
741
864
|
/**
|
|
742
865
|
* Keeps the document this variant key already has in the mailing's current
|
|
743
866
|
* test, so a test can be changed without sending every document again. The
|
|
744
867
|
* service refuses it (`validation_failed`) when that variant has none.
|
|
745
868
|
*/
|
|
746
|
-
keep_document:
|
|
869
|
+
keep_document: z6.literal(true).optional()
|
|
747
870
|
});
|
|
748
871
|
var AB_WINNER_METRICS = ["opens", "clicks", "manual"];
|
|
749
|
-
var AbTestConfig =
|
|
750
|
-
variants:
|
|
872
|
+
var AbTestConfig = z6.object({
|
|
873
|
+
variants: z6.array(AbVariant).min(2).max(5),
|
|
751
874
|
/** Share of recipients in the test group, split evenly across variants; the rest get the winner. */
|
|
752
|
-
test_fraction:
|
|
753
|
-
winner_metric:
|
|
754
|
-
decide_after_minutes:
|
|
875
|
+
test_fraction: z6.number().gt(0).lte(1),
|
|
876
|
+
winner_metric: z6.enum(AB_WINNER_METRICS),
|
|
877
|
+
decide_after_minutes: z6.number().int().min(15).max(10080).optional()
|
|
755
878
|
}).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), {
|
|
756
879
|
message: "every variant changes the subject, the document or both",
|
|
757
880
|
path: ["variants"]
|
|
@@ -763,19 +886,19 @@ var AbTestConfig = z5.object({
|
|
|
763
886
|
path: ["decide_after_minutes"]
|
|
764
887
|
});
|
|
765
888
|
var AB_TEST_STATUSES = ["pending", "testing", "awaiting_pick", "decided"];
|
|
766
|
-
var AbTestState =
|
|
767
|
-
variants:
|
|
768
|
-
test_fraction:
|
|
769
|
-
winner_metric:
|
|
770
|
-
decide_after_minutes:
|
|
771
|
-
status:
|
|
889
|
+
var AbTestState = z6.object({
|
|
890
|
+
variants: z6.array(z6.object({ key: z6.string(), subject: z6.string().nullable(), has_document: z6.boolean() })),
|
|
891
|
+
test_fraction: z6.number(),
|
|
892
|
+
winner_metric: z6.enum(AB_WINNER_METRICS),
|
|
893
|
+
decide_after_minutes: z6.number().int().nullable(),
|
|
894
|
+
status: z6.enum(AB_TEST_STATUSES),
|
|
772
895
|
/** When the metric decides; null for manual or before the start. */
|
|
773
896
|
decide_at: Timestamp.nullable(),
|
|
774
|
-
winner:
|
|
775
|
-
decided_by:
|
|
897
|
+
winner: z6.string().nullable(),
|
|
898
|
+
decided_by: z6.enum(["metric", "manual"]).nullable(),
|
|
776
899
|
decided_at: Timestamp.nullable()
|
|
777
900
|
});
|
|
778
|
-
var AbWinnerRequest =
|
|
901
|
+
var AbWinnerRequest = z6.object({ variant: z6.string().regex(/^[a-z]$/) });
|
|
779
902
|
function testGroupSize(total, fraction, variants) {
|
|
780
903
|
return Math.min(total, Math.max(variants, Math.ceil(total * fraction)));
|
|
781
904
|
}
|
|
@@ -790,9 +913,9 @@ var MAILING_STATUSES = [
|
|
|
790
913
|
"partially_failed",
|
|
791
914
|
"cancelled"
|
|
792
915
|
];
|
|
793
|
-
var MailingStatus =
|
|
916
|
+
var MailingStatus = z7.enum(MAILING_STATUSES);
|
|
794
917
|
var MAILING_ACTIONS = ["send", "schedule", "unschedule", "pause", "resume", "cancel", "retry-failed"];
|
|
795
|
-
var MailingAction =
|
|
918
|
+
var MailingAction = z7.enum(MAILING_ACTIONS);
|
|
796
919
|
var MAILING_TRANSITIONS = {
|
|
797
920
|
draft: ["send", "schedule", "cancel"],
|
|
798
921
|
scheduled: ["send", "schedule", "unschedule", "cancel"],
|
|
@@ -807,23 +930,23 @@ function canTransition(status, action) {
|
|
|
807
930
|
}
|
|
808
931
|
var EDITABLE_MAILING_STATUSES = ["draft", "scheduled"];
|
|
809
932
|
var TERMINAL_MAILING_STATUSES = ["sent", "cancelled"];
|
|
810
|
-
var MailingMetadata =
|
|
811
|
-
var MailingCounts =
|
|
812
|
-
total:
|
|
813
|
-
queued:
|
|
814
|
-
sending:
|
|
815
|
-
sent:
|
|
816
|
-
failed:
|
|
817
|
-
skipped:
|
|
933
|
+
var MailingMetadata = z7.record(z7.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");
|
|
934
|
+
var MailingCounts = z7.object({
|
|
935
|
+
total: z7.number().int().min(0),
|
|
936
|
+
queued: z7.number().int().min(0),
|
|
937
|
+
sending: z7.number().int().min(0),
|
|
938
|
+
sent: z7.number().int().min(0),
|
|
939
|
+
failed: z7.number().int().min(0),
|
|
940
|
+
skipped: z7.number().int().min(0)
|
|
818
941
|
});
|
|
819
942
|
var MAILING_KINDS = ["broadcast", "automation", "notification"];
|
|
820
|
-
var MailingKind =
|
|
821
|
-
var Mailing =
|
|
943
|
+
var MailingKind = z7.enum(MAILING_KINDS);
|
|
944
|
+
var Mailing = z7.object({
|
|
822
945
|
id: Id,
|
|
823
946
|
kind: MailingKind,
|
|
824
|
-
name:
|
|
825
|
-
subject:
|
|
826
|
-
preheader:
|
|
947
|
+
name: z7.string().max(200).nullable(),
|
|
948
|
+
subject: z7.string().min(1).max(998),
|
|
949
|
+
preheader: z7.string().max(500).nullable(),
|
|
827
950
|
/**
|
|
828
951
|
* The template the document was taken from, if any. The snapshot is what is
|
|
829
952
|
* sent in the main language; the template's other language versions are
|
|
@@ -868,15 +991,15 @@ var Mailing = z6.object({
|
|
|
868
991
|
* recipients refused as dead addresses in one run (see the provider's
|
|
869
992
|
* `rejections.anomaly`). Resuming clears it and starts a new run.
|
|
870
993
|
*/
|
|
871
|
-
pause_reason:
|
|
994
|
+
pause_reason: z7.string().nullable().default(null),
|
|
872
995
|
created_at: Timestamp,
|
|
873
996
|
updated_at: Timestamp
|
|
874
997
|
});
|
|
875
998
|
var MailingSummary = Mailing.omit({ document: true });
|
|
876
999
|
var MailingContent = {
|
|
877
|
-
name:
|
|
878
|
-
subject:
|
|
879
|
-
preheader:
|
|
1000
|
+
name: z7.string().max(200).optional(),
|
|
1001
|
+
subject: z7.string().min(1).max(998),
|
|
1002
|
+
preheader: z7.string().max(500).optional(),
|
|
880
1003
|
/** See `Mailing.send_locale`. */
|
|
881
1004
|
send_locale: LocaleTag.nullable().optional(),
|
|
882
1005
|
/**
|
|
@@ -889,7 +1012,7 @@ var MailingContent = {
|
|
|
889
1012
|
provider_id: Id,
|
|
890
1013
|
metadata: MailingMetadata.optional()
|
|
891
1014
|
};
|
|
892
|
-
var MailingCreate =
|
|
1015
|
+
var MailingCreate = z7.object({
|
|
893
1016
|
...MailingContent,
|
|
894
1017
|
subject: MailingContent.subject.optional(),
|
|
895
1018
|
document: TemplateDocument.optional(),
|
|
@@ -901,10 +1024,10 @@ var MailingCreate = z6.object({
|
|
|
901
1024
|
message: "a mailing made from a document needs a subject",
|
|
902
1025
|
path: ["subject"]
|
|
903
1026
|
});
|
|
904
|
-
var MailingUpdate =
|
|
905
|
-
name:
|
|
1027
|
+
var MailingUpdate = z7.object({
|
|
1028
|
+
name: z7.string().max(200).nullable().optional(),
|
|
906
1029
|
subject: MailingContent.subject.optional(),
|
|
907
|
-
preheader:
|
|
1030
|
+
preheader: z7.string().max(500).nullable().optional(),
|
|
908
1031
|
/** Null turns a draft into a letter, which it can only be with at most one recipient. */
|
|
909
1032
|
topic: Slug.nullable().optional(),
|
|
910
1033
|
send_locale: LocaleTag.nullable().optional(),
|
|
@@ -918,30 +1041,30 @@ var MailingListQuery = PageQuery.extend({
|
|
|
918
1041
|
kind: MailingKind.optional(),
|
|
919
1042
|
automation_id: Id.optional()
|
|
920
1043
|
});
|
|
921
|
-
var MailingSendRequest =
|
|
922
|
-
var MailingRetryFailedRequest =
|
|
1044
|
+
var MailingSendRequest = z7.object({}).strict();
|
|
1045
|
+
var MailingRetryFailedRequest = z7.object({
|
|
923
1046
|
/**
|
|
924
1047
|
* Also requeue recipients whose outcome is unknown after a crash. They may
|
|
925
1048
|
* already have received the message, so this is a human's decision, never a
|
|
926
1049
|
* default.
|
|
927
1050
|
*/
|
|
928
|
-
include_outcome_unknown:
|
|
1051
|
+
include_outcome_unknown: z7.boolean().optional()
|
|
929
1052
|
}).strict();
|
|
930
|
-
var MailingActionRequest =
|
|
931
|
-
var MailingTestRequest =
|
|
1053
|
+
var MailingActionRequest = z7.object({}).strict();
|
|
1054
|
+
var MailingTestRequest = z7.object({
|
|
932
1055
|
to: Email,
|
|
933
1056
|
merge: Properties.optional(),
|
|
934
1057
|
locale: LocaleTag.optional()
|
|
935
1058
|
});
|
|
936
|
-
var MailingTestResult =
|
|
1059
|
+
var MailingTestResult = z7.object({
|
|
937
1060
|
message_id: Id,
|
|
938
|
-
provider_message_id:
|
|
1061
|
+
provider_message_id: z7.string().nullable()
|
|
939
1062
|
});
|
|
940
1063
|
var RECIPIENT_STATUSES = ["queued", "sending", "sent", "failed", "skipped"];
|
|
941
|
-
var RecipientStatus =
|
|
1064
|
+
var RecipientStatus = z7.enum(RECIPIENT_STATUSES);
|
|
942
1065
|
var SKIP_REASONS = ["suppressed", "not_subscribed", "contact_erased", "cancelled", "outcome_unknown"];
|
|
943
|
-
var SkipReason =
|
|
944
|
-
var Recipient =
|
|
1066
|
+
var SkipReason = z7.enum(SKIP_REASONS);
|
|
1067
|
+
var Recipient = z7.object({
|
|
945
1068
|
id: Id,
|
|
946
1069
|
mailing_id: Id,
|
|
947
1070
|
contact_id: Id.nullable(),
|
|
@@ -949,55 +1072,55 @@ var Recipient = z6.object({
|
|
|
949
1072
|
merge: Properties,
|
|
950
1073
|
status: RecipientStatus,
|
|
951
1074
|
skip_reason: SkipReason.nullable(),
|
|
952
|
-
attempts:
|
|
1075
|
+
attempts: z7.number().int().min(0),
|
|
953
1076
|
message_id: Id.nullable(),
|
|
954
|
-
last_error:
|
|
1077
|
+
last_error: z7.string().nullable(),
|
|
955
1078
|
created_at: Timestamp,
|
|
956
1079
|
updated_at: Timestamp
|
|
957
1080
|
});
|
|
958
1081
|
var MAX_RECIPIENTS_PER_BATCH = 1e3;
|
|
959
1082
|
var MAX_LETTER_RECIPIENTS = 1;
|
|
960
1083
|
var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
|
|
961
|
-
var RecipientInput =
|
|
1084
|
+
var RecipientInput = z7.object({
|
|
962
1085
|
contact_id: Id.optional(),
|
|
963
|
-
external_id:
|
|
1086
|
+
external_id: z7.string().min(1).max(255).optional(),
|
|
964
1087
|
email: Email.optional(),
|
|
965
1088
|
merge: Properties.optional()
|
|
966
1089
|
}).refine((v) => v.contact_id !== void 0 || v.external_id !== void 0 || v.email !== void 0, {
|
|
967
1090
|
message: "contact_id, external_id or email is required",
|
|
968
1091
|
path: ["email"]
|
|
969
1092
|
});
|
|
970
|
-
var RecipientBatch =
|
|
971
|
-
recipients:
|
|
1093
|
+
var RecipientBatch = z7.object({
|
|
1094
|
+
recipients: z7.array(RecipientInput).min(1).max(MAX_RECIPIENTS_PER_BATCH)
|
|
972
1095
|
});
|
|
973
1096
|
var RECIPIENT_REJECTIONS = ["unknown_contact", "duplicate_in_batch"];
|
|
974
|
-
var RecipientBatchResult =
|
|
1097
|
+
var RecipientBatchResult = z7.object({
|
|
975
1098
|
/** Newly queued. */
|
|
976
|
-
added:
|
|
1099
|
+
added: z7.number().int().min(0),
|
|
977
1100
|
/** Already on the mailing from an earlier batch: left unchanged. */
|
|
978
|
-
already_present:
|
|
1101
|
+
already_present: z7.number().int().min(0),
|
|
979
1102
|
/** Items not added, by their index in the request. */
|
|
980
|
-
rejected:
|
|
1103
|
+
rejected: z7.array(z7.object({ index: z7.number().int().min(0), reason: z7.enum(RECIPIENT_REJECTIONS) }))
|
|
981
1104
|
});
|
|
982
1105
|
var RecipientListQuery = PageQuery.extend({
|
|
983
1106
|
status: RecipientStatus.optional(),
|
|
984
1107
|
skip_reason: SkipReason.optional()
|
|
985
1108
|
});
|
|
986
1109
|
var MESSAGE_OUTCOMES = ["sent", "failed"];
|
|
987
|
-
var MessageOutcome =
|
|
988
|
-
var MessageSummary =
|
|
1110
|
+
var MessageOutcome = z7.enum(MESSAGE_OUTCOMES);
|
|
1111
|
+
var MessageSummary = z7.object({
|
|
989
1112
|
id: Id,
|
|
990
1113
|
mailing_id: Id.nullable(),
|
|
991
1114
|
recipient_id: Id.nullable(),
|
|
992
1115
|
contact_id: Id.nullable(),
|
|
993
1116
|
to: Email,
|
|
994
|
-
subject:
|
|
1117
|
+
subject: z7.string(),
|
|
995
1118
|
provider_id: Id,
|
|
996
|
-
provider_message_id:
|
|
1119
|
+
provider_message_id: z7.string().nullable(),
|
|
997
1120
|
outcome: MessageOutcome,
|
|
998
|
-
error:
|
|
999
|
-
is_test:
|
|
1000
|
-
recipient_count:
|
|
1121
|
+
error: z7.string().nullable(),
|
|
1122
|
+
is_test: z7.boolean(),
|
|
1123
|
+
recipient_count: z7.number().int().min(1),
|
|
1001
1124
|
/** The language version the message was sent in; null for messages archived before languages existed. */
|
|
1002
1125
|
locale: LocaleTag.nullable(),
|
|
1003
1126
|
/**
|
|
@@ -1009,22 +1132,22 @@ var MessageSummary = z6.object({
|
|
|
1009
1132
|
insertions: InsertionChoices.nullable(),
|
|
1010
1133
|
created_at: Timestamp
|
|
1011
1134
|
});
|
|
1012
|
-
var Message = MessageSummary.extend({ html:
|
|
1013
|
-
var MailingLanguageStatus =
|
|
1014
|
-
var MailingLanguage =
|
|
1135
|
+
var Message = MessageSummary.extend({ html: z7.string() });
|
|
1136
|
+
var MailingLanguageStatus = z7.enum(["main", "ready", "draft", "not_a_workspace_language"]);
|
|
1137
|
+
var MailingLanguage = z7.object({
|
|
1015
1138
|
locale: LocaleTag,
|
|
1016
1139
|
status: MailingLanguageStatus,
|
|
1017
1140
|
/** Queued recipients who will get this version. */
|
|
1018
|
-
planned:
|
|
1141
|
+
planned: z7.number().int().min(0),
|
|
1019
1142
|
/** Messages already sent in this language (tests excluded). */
|
|
1020
|
-
sent:
|
|
1143
|
+
sent: z7.number().int().min(0)
|
|
1021
1144
|
});
|
|
1022
|
-
var MailingLanguageReport =
|
|
1145
|
+
var MailingLanguageReport = z7.object({
|
|
1023
1146
|
main_locale: LocaleTag,
|
|
1024
1147
|
send_locale: LocaleTag.nullable(),
|
|
1025
|
-
languages:
|
|
1026
|
-
fallback:
|
|
1027
|
-
no_language:
|
|
1148
|
+
languages: z7.array(MailingLanguage),
|
|
1149
|
+
fallback: z7.array(z7.object({ wanted: z7.string(), planned: z7.number().int().min(1) })),
|
|
1150
|
+
no_language: z7.number().int().min(0),
|
|
1028
1151
|
/**
|
|
1029
1152
|
* Values that no choice of an insertion takes, so the "otherwise" text goes
|
|
1030
1153
|
* out (docs/plans/2026-09-25-translated-insertions.md, decision 3): per
|
|
@@ -1033,12 +1156,12 @@ var MailingLanguageReport = z6.object({
|
|
|
1033
1156
|
* template's keys never produces one; a non-empty list means the client and
|
|
1034
1157
|
* the template have drifted apart.
|
|
1035
1158
|
*/
|
|
1036
|
-
insertion_misses:
|
|
1037
|
-
|
|
1038
|
-
insertion:
|
|
1039
|
-
value:
|
|
1040
|
-
planned:
|
|
1041
|
-
sent:
|
|
1159
|
+
insertion_misses: z7.array(
|
|
1160
|
+
z7.object({
|
|
1161
|
+
insertion: z7.string(),
|
|
1162
|
+
value: z7.string(),
|
|
1163
|
+
planned: z7.number().int().min(0),
|
|
1164
|
+
sent: z7.number().int().min(0)
|
|
1042
1165
|
})
|
|
1043
1166
|
)
|
|
1044
1167
|
});
|
|
@@ -1048,26 +1171,26 @@ var MessageListQuery = PageQuery.extend({
|
|
|
1048
1171
|
});
|
|
1049
1172
|
|
|
1050
1173
|
// src/platform.ts
|
|
1051
|
-
var Tag =
|
|
1174
|
+
var Tag = z8.object({
|
|
1052
1175
|
id: Id,
|
|
1053
1176
|
slug: Slug,
|
|
1054
|
-
name:
|
|
1055
|
-
contact_count:
|
|
1177
|
+
name: z8.string().min(1).max(120),
|
|
1178
|
+
contact_count: z8.number().int().min(0),
|
|
1056
1179
|
created_at: Timestamp
|
|
1057
1180
|
});
|
|
1058
|
-
var TagCreate =
|
|
1059
|
-
var TagAssignment =
|
|
1060
|
-
contact_ids:
|
|
1181
|
+
var TagCreate = z8.object({ slug: Slug, name: z8.string().min(1).max(120) });
|
|
1182
|
+
var TagAssignment = z8.object({
|
|
1183
|
+
contact_ids: z8.array(Id).min(1).max(1e3)
|
|
1061
1184
|
});
|
|
1062
1185
|
var CONTACT_PROPERTY_TYPES = ["string", "number", "boolean", "date"];
|
|
1063
|
-
var ContactPropertyType =
|
|
1064
|
-
var ContactPropertyKey =
|
|
1065
|
-
var ContactPropertyDefinition =
|
|
1186
|
+
var ContactPropertyType = z8.enum(CONTACT_PROPERTY_TYPES);
|
|
1187
|
+
var ContactPropertyKey = z8.string().regex(/^[A-Za-z0-9_.-]{1,64}$/, "letters, digits, dot, underscore and hyphen, 1 to 64");
|
|
1188
|
+
var ContactPropertyDefinition = z8.object({
|
|
1066
1189
|
key: ContactPropertyKey,
|
|
1067
|
-
label:
|
|
1190
|
+
label: z8.string().min(1).max(120),
|
|
1068
1191
|
type: ContactPropertyType
|
|
1069
1192
|
});
|
|
1070
|
-
var ContactPropertyParams =
|
|
1193
|
+
var ContactPropertyParams = z8.object({ key: ContactPropertyKey });
|
|
1071
1194
|
var FILTER_OPERATORS = [
|
|
1072
1195
|
"eq",
|
|
1073
1196
|
"neq",
|
|
@@ -1083,8 +1206,8 @@ var FILTER_OPERATORS = [
|
|
|
1083
1206
|
"in",
|
|
1084
1207
|
"not_in"
|
|
1085
1208
|
];
|
|
1086
|
-
var FilterOperator =
|
|
1087
|
-
var FilterField =
|
|
1209
|
+
var FilterOperator = z8.enum(FILTER_OPERATORS);
|
|
1210
|
+
var FilterField = z8.string().regex(
|
|
1088
1211
|
/^(email|first_name|last_name|locale|created_at|tag|topic|engagement:(opened|clicked)|property:[A-Za-z0-9_.-]{1,64})$/,
|
|
1089
1212
|
"unknown filter field"
|
|
1090
1213
|
);
|
|
@@ -1100,18 +1223,18 @@ var FILTER_FIELD_OPERATORS = {
|
|
|
1100
1223
|
property: FILTER_OPERATORS
|
|
1101
1224
|
};
|
|
1102
1225
|
var MAX_FILTER_DEPTH = 5;
|
|
1103
|
-
var FilterCondition =
|
|
1226
|
+
var FilterCondition = z8.object({ field: FilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
|
|
1104
1227
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
1105
1228
|
path: ["value"]
|
|
1106
1229
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
1107
1230
|
message: "in and not_in take an array",
|
|
1108
1231
|
path: ["value"]
|
|
1109
1232
|
});
|
|
1110
|
-
var SegmentFilter =
|
|
1111
|
-
() =>
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1233
|
+
var SegmentFilter = z8.lazy(
|
|
1234
|
+
() => z8.union([
|
|
1235
|
+
z8.object({ and: z8.array(SegmentFilter).min(1).max(50) }).strict(),
|
|
1236
|
+
z8.object({ or: z8.array(SegmentFilter).min(1).max(50) }).strict(),
|
|
1237
|
+
z8.object({ not: SegmentFilter }).strict(),
|
|
1115
1238
|
FilterCondition
|
|
1116
1239
|
])
|
|
1117
1240
|
);
|
|
@@ -1199,42 +1322,42 @@ function untypedValueProblem(c, every) {
|
|
|
1199
1322
|
if (orderOp) return every(isFilterNumber, "numbers (define the property to compare dates)");
|
|
1200
1323
|
return every((v) => isScalar(v) || v === null, "strings, numbers, booleans or null");
|
|
1201
1324
|
}
|
|
1202
|
-
var Segment =
|
|
1325
|
+
var Segment = z8.object({
|
|
1203
1326
|
id: Id,
|
|
1204
|
-
name:
|
|
1327
|
+
name: z8.string().min(1).max(120),
|
|
1205
1328
|
filter: SegmentFilter,
|
|
1206
1329
|
/** Computed on read. */
|
|
1207
|
-
contact_count:
|
|
1330
|
+
contact_count: z8.number().int().min(0),
|
|
1208
1331
|
created_at: Timestamp,
|
|
1209
1332
|
updated_at: Timestamp
|
|
1210
1333
|
});
|
|
1211
|
-
var SegmentCreate =
|
|
1212
|
-
var SegmentPreviewRequest =
|
|
1213
|
-
var SegmentPreview =
|
|
1214
|
-
contact_count:
|
|
1334
|
+
var SegmentCreate = z8.object({ name: z8.string().min(1).max(120), filter: BoundedSegmentFilter });
|
|
1335
|
+
var SegmentPreviewRequest = z8.object({ filter: BoundedSegmentFilter });
|
|
1336
|
+
var SegmentPreview = z8.object({
|
|
1337
|
+
contact_count: z8.number().int().min(0),
|
|
1215
1338
|
/** Up to five matching contacts, for a human to sanity-check the filter. */
|
|
1216
|
-
sample:
|
|
1339
|
+
sample: z8.array(z8.object({ id: Id, email: Email })).max(5)
|
|
1217
1340
|
});
|
|
1218
|
-
var MailingAudienceFromSegment =
|
|
1341
|
+
var MailingAudienceFromSegment = z8.object({ segment_id: Id });
|
|
1219
1342
|
var SIGNUP_FORM_FIELDS = ["first_name", "last_name"];
|
|
1220
1343
|
var HOSTED_PAGE_LOCALES = ["en", "de", "it", "fr", "es"];
|
|
1221
|
-
var SignupFormTranslation =
|
|
1222
|
-
title:
|
|
1223
|
-
consent_text:
|
|
1344
|
+
var SignupFormTranslation = z8.object({
|
|
1345
|
+
title: z8.string().min(1).max(120),
|
|
1346
|
+
consent_text: z8.string().min(1).max(2e3)
|
|
1224
1347
|
});
|
|
1225
|
-
var SignupForm =
|
|
1348
|
+
var SignupForm = z8.object({
|
|
1226
1349
|
id: Id,
|
|
1227
|
-
name:
|
|
1350
|
+
name: z8.string().min(1).max(120),
|
|
1228
1351
|
/** The heading of the hosted page. */
|
|
1229
|
-
title:
|
|
1352
|
+
title: z8.string().min(1).max(120),
|
|
1230
1353
|
/** What the person agrees to, shown above the button and recorded with the confirmation. */
|
|
1231
|
-
consent_text:
|
|
1232
|
-
translations:
|
|
1233
|
-
topics:
|
|
1234
|
-
tags:
|
|
1354
|
+
consent_text: z8.string().min(1).max(2e3),
|
|
1355
|
+
translations: z8.record(SignupFormTranslation),
|
|
1356
|
+
topics: z8.array(Slug).min(1),
|
|
1357
|
+
tags: z8.array(Slug),
|
|
1235
1358
|
/** Fields shown besides email. */
|
|
1236
|
-
fields:
|
|
1237
|
-
double_opt_in:
|
|
1359
|
+
fields: z8.array(z8.enum(SIGNUP_FORM_FIELDS)),
|
|
1360
|
+
double_opt_in: z8.literal(true),
|
|
1238
1361
|
/** The provider the confirmation mail is sent through. */
|
|
1239
1362
|
provider_id: Id,
|
|
1240
1363
|
/**
|
|
@@ -1243,11 +1366,11 @@ var SignupForm = z7.object({
|
|
|
1243
1366
|
*/
|
|
1244
1367
|
confirmation_template_id: Id.nullable(),
|
|
1245
1368
|
/** Where the person lands after confirming. Null shows the hosted confirmation page. */
|
|
1246
|
-
redirect_url:
|
|
1369
|
+
redirect_url: z8.string().url().nullable(),
|
|
1247
1370
|
/** Origins allowed to embed the form and call the submission route from a browser. */
|
|
1248
|
-
allowed_origins:
|
|
1371
|
+
allowed_origins: z8.array(z8.string().url()).max(20),
|
|
1249
1372
|
/** Bumped on every change; recorded with each confirmation. */
|
|
1250
|
-
version:
|
|
1373
|
+
version: z8.number().int().min(1),
|
|
1251
1374
|
created_at: Timestamp,
|
|
1252
1375
|
updated_at: Timestamp
|
|
1253
1376
|
});
|
|
@@ -1262,56 +1385,56 @@ var SignupFormCreate = SignupForm.omit({
|
|
|
1262
1385
|
tags: SignupForm.shape.tags.optional(),
|
|
1263
1386
|
fields: SignupForm.shape.fields.optional(),
|
|
1264
1387
|
confirmation_template_id: Id.nullable().optional(),
|
|
1265
|
-
redirect_url:
|
|
1388
|
+
redirect_url: z8.string().url().nullable().optional(),
|
|
1266
1389
|
allowed_origins: SignupForm.shape.allowed_origins.optional()
|
|
1267
1390
|
});
|
|
1268
|
-
var SignupFormEmbed =
|
|
1269
|
-
hosted_url:
|
|
1391
|
+
var SignupFormEmbed = z8.object({
|
|
1392
|
+
hosted_url: z8.string().url(),
|
|
1270
1393
|
/** A plain HTML form posting to the hosted page; works without JavaScript. */
|
|
1271
|
-
html:
|
|
1394
|
+
html: z8.string(),
|
|
1272
1395
|
/** A small optional script that adds the time check and submits in place. */
|
|
1273
|
-
script_url:
|
|
1396
|
+
script_url: z8.string().url(),
|
|
1274
1397
|
/** Where a browser fetches a fresh `form_token` from (GET, public, CORS for `allowed_origins`). */
|
|
1275
|
-
token_url:
|
|
1398
|
+
token_url: z8.string().url()
|
|
1276
1399
|
});
|
|
1277
|
-
var SignupSubmission =
|
|
1400
|
+
var SignupSubmission = z8.object({
|
|
1278
1401
|
email: Email,
|
|
1279
|
-
first_name:
|
|
1280
|
-
last_name:
|
|
1281
|
-
locale:
|
|
1402
|
+
first_name: z8.string().max(200).optional(),
|
|
1403
|
+
last_name: z8.string().max(200).optional(),
|
|
1404
|
+
locale: z8.string().min(2).max(35).optional(),
|
|
1282
1405
|
/** Honeypot: must be empty. */
|
|
1283
|
-
website:
|
|
1406
|
+
website: z8.string().max(0).optional(),
|
|
1284
1407
|
/**
|
|
1285
1408
|
* The signed render time from the form's token URL: a submission sooner than a
|
|
1286
1409
|
* human could fill the form, or long after, is refused.
|
|
1287
1410
|
*/
|
|
1288
|
-
form_token:
|
|
1411
|
+
form_token: z8.string().min(1).max(512)
|
|
1289
1412
|
});
|
|
1290
1413
|
var IMPORT_STATUSES = ["uploaded", "validating", "validated", "committing", "completed", "failed", "cancelled"];
|
|
1291
|
-
var ImportStatus =
|
|
1414
|
+
var ImportStatus = z8.enum(IMPORT_STATUSES);
|
|
1292
1415
|
var IMPORT_TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
1293
|
-
var ImportColumnMapping =
|
|
1294
|
-
|
|
1416
|
+
var ImportColumnMapping = z8.record(
|
|
1417
|
+
z8.string().regex(/^(email|external_id|first_name|last_name|locale|property:[A-Za-z0-9_.-]{1,64}|ignore)$/)
|
|
1295
1418
|
);
|
|
1296
1419
|
var IMPORT_FILE_FIELD = "file";
|
|
1297
1420
|
var MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
|
1298
1421
|
var MAX_IMPORT_ROWS = 2e5;
|
|
1299
|
-
var ImportMappingRequest =
|
|
1422
|
+
var ImportMappingRequest = z8.object({
|
|
1300
1423
|
/** CSV header -> contact field. Exactly one column maps to `email`. */
|
|
1301
1424
|
mapping: ImportColumnMapping.refine(
|
|
1302
1425
|
(m) => Object.values(m).filter((v) => v === "email").length === 1,
|
|
1303
1426
|
"exactly one column maps to email"
|
|
1304
1427
|
),
|
|
1305
|
-
topics:
|
|
1306
|
-
tags:
|
|
1428
|
+
topics: z8.array(Slug).max(100),
|
|
1429
|
+
tags: z8.array(Slug).max(100),
|
|
1307
1430
|
/** Existing contacts get the mapped fields overwritten; false only adds topics, tags and missing fields. */
|
|
1308
|
-
update_existing:
|
|
1431
|
+
update_existing: z8.boolean().optional(),
|
|
1309
1432
|
/** The importer states the people consented; the service records who said so and when. */
|
|
1310
|
-
consent_confirmed:
|
|
1433
|
+
consent_confirmed: z8.literal(true)
|
|
1311
1434
|
});
|
|
1312
|
-
var ImportCommitRequest =
|
|
1435
|
+
var ImportCommitRequest = z8.object({ mapping_version: z8.number().int().min(1) });
|
|
1313
1436
|
var IMPORT_ROW_OUTCOMES = ["created", "updated", "unchanged", "suppressed", "skipped"];
|
|
1314
|
-
var ImportRowOutcome =
|
|
1437
|
+
var ImportRowOutcome = z8.enum(IMPORT_ROW_OUTCOMES);
|
|
1315
1438
|
var IMPORT_SKIP_REASONS = [
|
|
1316
1439
|
"missing_email",
|
|
1317
1440
|
"invalid_email",
|
|
@@ -1320,90 +1443,90 @@ var IMPORT_SKIP_REASONS = [
|
|
|
1320
1443
|
"external_id_conflict",
|
|
1321
1444
|
"wrong_column_count"
|
|
1322
1445
|
];
|
|
1323
|
-
var ImportSkipReason =
|
|
1324
|
-
var ImportReport =
|
|
1325
|
-
created:
|
|
1326
|
-
updated:
|
|
1327
|
-
unchanged:
|
|
1328
|
-
suppressed:
|
|
1329
|
-
skipped:
|
|
1330
|
-
skipped_by_reason:
|
|
1446
|
+
var ImportSkipReason = z8.enum(IMPORT_SKIP_REASONS);
|
|
1447
|
+
var ImportReport = z8.object({
|
|
1448
|
+
created: z8.number().int().min(0),
|
|
1449
|
+
updated: z8.number().int().min(0),
|
|
1450
|
+
unchanged: z8.number().int().min(0),
|
|
1451
|
+
suppressed: z8.number().int().min(0),
|
|
1452
|
+
skipped: z8.number().int().min(0),
|
|
1453
|
+
skipped_by_reason: z8.record(ImportSkipReason, z8.number().int().min(0)),
|
|
1331
1454
|
/** Rows whose contact was subscribed without some topic, because the address is blocked for it. */
|
|
1332
|
-
topics_withheld:
|
|
1455
|
+
topics_withheld: z8.number().int().min(0)
|
|
1333
1456
|
});
|
|
1334
|
-
var ImportJob =
|
|
1457
|
+
var ImportJob = z8.object({
|
|
1335
1458
|
id: Id,
|
|
1336
1459
|
status: ImportStatus,
|
|
1337
|
-
file_name:
|
|
1338
|
-
file_bytes:
|
|
1339
|
-
total_rows:
|
|
1460
|
+
file_name: z8.string().max(255).nullable(),
|
|
1461
|
+
file_bytes: z8.number().int().min(0),
|
|
1462
|
+
total_rows: z8.number().int().min(0),
|
|
1340
1463
|
/** The header row, as in the file. */
|
|
1341
|
-
columns:
|
|
1464
|
+
columns: z8.array(z8.string()),
|
|
1342
1465
|
/** The first rows, for the mapping screen. */
|
|
1343
|
-
sample:
|
|
1466
|
+
sample: z8.array(z8.array(z8.string())).max(5),
|
|
1344
1467
|
/** A mapping guessed from the headers, for the mapping screen to start from. */
|
|
1345
1468
|
suggested_mapping: ImportColumnMapping,
|
|
1346
1469
|
mapping: ImportColumnMapping.nullable(),
|
|
1347
|
-
mapping_version:
|
|
1348
|
-
topics:
|
|
1349
|
-
tags:
|
|
1350
|
-
update_existing:
|
|
1470
|
+
mapping_version: z8.number().int().min(0),
|
|
1471
|
+
topics: z8.array(Slug),
|
|
1472
|
+
tags: z8.array(Slug),
|
|
1473
|
+
update_existing: z8.boolean(),
|
|
1351
1474
|
/** The dry run of the current mapping; null before one finished. */
|
|
1352
1475
|
dry_run: ImportReport.nullable(),
|
|
1353
1476
|
/** What the commit has written so far; null before it started. */
|
|
1354
1477
|
result: ImportReport.nullable(),
|
|
1355
1478
|
/** Rows the current phase (dry run or commit) has processed. */
|
|
1356
|
-
processed_rows:
|
|
1479
|
+
processed_rows: z8.number().int().min(0),
|
|
1357
1480
|
/** Why a `failed` import failed. */
|
|
1358
|
-
error:
|
|
1481
|
+
error: z8.string().nullable(),
|
|
1359
1482
|
created_at: Timestamp,
|
|
1360
1483
|
updated_at: Timestamp,
|
|
1361
1484
|
finished_at: Timestamp.nullable()
|
|
1362
1485
|
});
|
|
1363
|
-
var ImportRow =
|
|
1486
|
+
var ImportRow = z8.object({
|
|
1364
1487
|
/** The line in the file, 1 being the first line after the header. */
|
|
1365
|
-
row:
|
|
1366
|
-
email:
|
|
1488
|
+
row: z8.number().int().min(1),
|
|
1489
|
+
email: z8.string().nullable(),
|
|
1367
1490
|
outcome: ImportRowOutcome,
|
|
1368
1491
|
reason: ImportSkipReason.nullable(),
|
|
1369
1492
|
/** A human explanation for a skipped row (which value, which column). */
|
|
1370
|
-
message:
|
|
1493
|
+
message: z8.string().nullable(),
|
|
1371
1494
|
contact_id: Id.nullable()
|
|
1372
1495
|
});
|
|
1373
1496
|
var ImportRowListQuery = PageQuery.extend({ outcome: ImportRowOutcome.optional() });
|
|
1374
|
-
var MailingScheduleRequest =
|
|
1375
|
-
var TrackingSettings =
|
|
1376
|
-
var VariantAnalytics =
|
|
1377
|
-
key:
|
|
1378
|
-
sent:
|
|
1379
|
-
unique_opens:
|
|
1380
|
-
unique_clicks:
|
|
1381
|
-
});
|
|
1382
|
-
var MailingAnalytics =
|
|
1497
|
+
var MailingScheduleRequest = z8.object({ send_at: Timestamp });
|
|
1498
|
+
var TrackingSettings = z8.object({ opens: z8.boolean(), clicks: z8.boolean() });
|
|
1499
|
+
var VariantAnalytics = z8.object({
|
|
1500
|
+
key: z8.string(),
|
|
1501
|
+
sent: z8.number().int().min(0),
|
|
1502
|
+
unique_opens: z8.number().int().min(0).nullable(),
|
|
1503
|
+
unique_clicks: z8.number().int().min(0).nullable()
|
|
1504
|
+
});
|
|
1505
|
+
var MailingAnalytics = z8.object({
|
|
1383
1506
|
mailing_id: Id,
|
|
1384
1507
|
counts: MailingCounts,
|
|
1385
1508
|
/** What was tracked for this mailing, fixed when it started; null before it started. */
|
|
1386
1509
|
tracking: TrackingSettings.nullable(),
|
|
1387
1510
|
/** Unique recipients with a human open. Null when opens were not tracked. */
|
|
1388
|
-
unique_opens:
|
|
1511
|
+
unique_opens: z8.number().int().min(0).nullable(),
|
|
1389
1512
|
/**
|
|
1390
1513
|
* Recipients whose only opens came from Apple Mail Privacy Protection, which
|
|
1391
1514
|
* loads every image on delivery: counted apart, never as human opens.
|
|
1392
1515
|
*/
|
|
1393
|
-
apple_mpp_opens:
|
|
1516
|
+
apple_mpp_opens: z8.number().int().min(0).nullable(),
|
|
1394
1517
|
/** Opens and clicks from security scanners and prefetchers, filtered out of the unique counts. */
|
|
1395
|
-
machine_events:
|
|
1396
|
-
unique_clicks:
|
|
1397
|
-
unsubscribes:
|
|
1398
|
-
bounces:
|
|
1399
|
-
complaints:
|
|
1400
|
-
links:
|
|
1401
|
-
variants:
|
|
1518
|
+
machine_events: z8.number().int().min(0).nullable(),
|
|
1519
|
+
unique_clicks: z8.number().int().min(0).nullable(),
|
|
1520
|
+
unsubscribes: z8.number().int().min(0),
|
|
1521
|
+
bounces: z8.number().int().min(0),
|
|
1522
|
+
complaints: z8.number().int().min(0),
|
|
1523
|
+
links: z8.array(z8.object({ url: z8.string(), unique_clicks: z8.number().int().min(0) })).nullable(),
|
|
1524
|
+
variants: z8.array(VariantAnalytics).nullable()
|
|
1402
1525
|
});
|
|
1403
1526
|
|
|
1404
1527
|
// src/automations.ts
|
|
1405
1528
|
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})$/;
|
|
1406
|
-
var AutomationFilterField =
|
|
1529
|
+
var AutomationFilterField = z9.string().regex(AUTOMATION_FILTER_FIELD_PATTERN, "unknown filter field");
|
|
1407
1530
|
var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
1408
1531
|
...FILTER_FIELD_OPERATORS,
|
|
1409
1532
|
segment: ["eq", "neq", "in", "not_in"],
|
|
@@ -1413,18 +1536,18 @@ var AUTOMATION_FILTER_FIELD_OPERATORS = {
|
|
|
1413
1536
|
step: ["eq", "neq"]
|
|
1414
1537
|
};
|
|
1415
1538
|
var automationFilterFamily = filterFieldFamily;
|
|
1416
|
-
var AutomationFilterCondition =
|
|
1539
|
+
var AutomationFilterCondition = z9.object({ field: AutomationFilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
|
|
1417
1540
|
message: "exists and not_exists take no value; every other operator needs one",
|
|
1418
1541
|
path: ["value"]
|
|
1419
1542
|
}).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
|
|
1420
1543
|
message: "in and not_in take an array",
|
|
1421
1544
|
path: ["value"]
|
|
1422
1545
|
});
|
|
1423
|
-
var AutomationFilter =
|
|
1424
|
-
() =>
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1546
|
+
var AutomationFilter = z9.lazy(
|
|
1547
|
+
() => z9.union([
|
|
1548
|
+
z9.object({ and: z9.array(AutomationFilter).min(1).max(50) }).strict(),
|
|
1549
|
+
z9.object({ or: z9.array(AutomationFilter).min(1).max(50) }).strict(),
|
|
1550
|
+
z9.object({ not: AutomationFilter }).strict(),
|
|
1428
1551
|
AutomationFilterCondition
|
|
1429
1552
|
])
|
|
1430
1553
|
);
|
|
@@ -1437,20 +1560,20 @@ function filterConditions(filter) {
|
|
|
1437
1560
|
if ("not" in filter) return filterConditions(filter.not);
|
|
1438
1561
|
return [filter];
|
|
1439
1562
|
}
|
|
1440
|
-
var StepId =
|
|
1441
|
-
var TimeOfDay =
|
|
1442
|
-
var Weekday =
|
|
1443
|
-
var TimezoneName =
|
|
1563
|
+
var StepId = z9.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "letters, digits, underscore and hyphen, 1 to 64");
|
|
1564
|
+
var TimeOfDay = z9.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "HH:MM in 24 hours");
|
|
1565
|
+
var Weekday = z9.number().int().min(0).max(6);
|
|
1566
|
+
var TimezoneName = z9.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");
|
|
1444
1567
|
var MAX_DELAY_SECONDS = 365 * 24 * 60 * 60;
|
|
1445
|
-
var DateOnly =
|
|
1568
|
+
var DateOnly = z9.string().regex(/^\d{4}-\d{2}-\d{2}$/, "a calendar date such as 2026-09-20").refine((v) => {
|
|
1446
1569
|
const [y, m, d] = v.split("-").map(Number);
|
|
1447
1570
|
const t = new Date(Date.UTC(y, m - 1, d));
|
|
1448
1571
|
return t.getUTCFullYear() === y && t.getUTCMonth() + 1 === m && t.getUTCDate() === d;
|
|
1449
1572
|
}, "a real calendar date");
|
|
1450
|
-
var OffsetDays =
|
|
1573
|
+
var OffsetDays = z9.number().int().min(-365).max(365);
|
|
1451
1574
|
var DATE_PASSED_ANSWERS = ["skip", "stop", "continue"];
|
|
1452
|
-
var DatePassedAnswer =
|
|
1453
|
-
var EventName =
|
|
1575
|
+
var DatePassedAnswer = z9.enum(DATE_PASSED_ANSWERS);
|
|
1576
|
+
var EventName = z9.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/, "letters, digits, dot, colon, underscore and hyphen, 1 to 80");
|
|
1454
1577
|
var AUTOMATION_TRIGGER_KINDS = [
|
|
1455
1578
|
"topic_subscribed",
|
|
1456
1579
|
"form_confirmed",
|
|
@@ -1463,17 +1586,17 @@ var AUTOMATION_TRIGGER_KINDS = [
|
|
|
1463
1586
|
"segment_entered",
|
|
1464
1587
|
"link_clicked"
|
|
1465
1588
|
];
|
|
1466
|
-
var AutomationTriggerKind =
|
|
1589
|
+
var AutomationTriggerKind = z9.enum(AUTOMATION_TRIGGER_KINDS);
|
|
1467
1590
|
var exclude = { exclude: BoundedAutomationFilter.nullish() };
|
|
1468
|
-
var AutomationTrigger =
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1591
|
+
var AutomationTrigger = z9.discriminatedUnion("kind", [
|
|
1592
|
+
z9.object({ kind: z9.literal("topic_subscribed"), topic: Slug, ...exclude }).strict(),
|
|
1593
|
+
z9.object({ kind: z9.literal("form_confirmed"), form_id: Id, ...exclude }).strict(),
|
|
1594
|
+
z9.object({ kind: z9.literal("tag_added"), tag: Slug, ...exclude }).strict(),
|
|
1472
1595
|
/** `to` given: only a change that lands on this value fires. Absent: any change of the key. */
|
|
1473
|
-
|
|
1596
|
+
z9.object({ kind: z9.literal("property_changed"), key: ContactPropertyKey, to: JsonValue.optional(), ...exclude }).strict(),
|
|
1474
1597
|
/** `where` filters the event's own data, so only `event:<key>` leaves belong in it. */
|
|
1475
|
-
|
|
1476
|
-
|
|
1598
|
+
z9.object({ kind: z9.literal("event"), name: EventName, where: BoundedAutomationFilter.nullish(), ...exclude }).strict(),
|
|
1599
|
+
z9.object({ kind: z9.literal("manual"), ...exclude }).strict(),
|
|
1477
1600
|
/**
|
|
1478
1601
|
* Every year on a date the contact carries: a birthday, a joining date, a
|
|
1479
1602
|
* renewal. `offset_days` moves it ("three days before"), `at` is the wall
|
|
@@ -1483,8 +1606,8 @@ var AutomationTrigger = z8.discriminatedUnion("kind", [
|
|
|
1483
1606
|
* A 29 February date fires on 1 March in a year that has no 29 February, so
|
|
1484
1607
|
* nobody is skipped every fourth year.
|
|
1485
1608
|
*/
|
|
1486
|
-
|
|
1487
|
-
kind:
|
|
1609
|
+
z9.object({
|
|
1610
|
+
kind: z9.literal("date_anniversary"),
|
|
1488
1611
|
key: ContactPropertyKey,
|
|
1489
1612
|
offset_days: OffsetDays,
|
|
1490
1613
|
at: TimeOfDay,
|
|
@@ -1496,8 +1619,8 @@ var AutomationTrigger = z8.discriminatedUnion("kind", [
|
|
|
1496
1619
|
* not ask about a segment or about an event's data: there is no event, and a
|
|
1497
1620
|
* segment is its own trigger kind.
|
|
1498
1621
|
*/
|
|
1499
|
-
|
|
1500
|
-
kind:
|
|
1622
|
+
z9.object({
|
|
1623
|
+
kind: z9.literal("exact_date"),
|
|
1501
1624
|
on: DateOnly,
|
|
1502
1625
|
at: TimeOfDay,
|
|
1503
1626
|
where: BoundedAutomationFilter.nullish(),
|
|
@@ -1516,7 +1639,7 @@ var AutomationTrigger = z8.discriminatedUnion("kind", [
|
|
|
1516
1639
|
* already exists. Entering those people as well is the publish dialog's
|
|
1517
1640
|
* backfill question, answered by a person rather than by the sweep.
|
|
1518
1641
|
*/
|
|
1519
|
-
|
|
1642
|
+
z9.object({ kind: z9.literal("segment_entered"), segment_id: Id, ...exclude }).strict(),
|
|
1520
1643
|
/**
|
|
1521
1644
|
* S4. When somebody clicks a link in a mailing that has already gone out.
|
|
1522
1645
|
*
|
|
@@ -1526,7 +1649,7 @@ var AutomationTrigger = z8.discriminatedUnion("kind", [
|
|
|
1526
1649
|
* tracking off there is no click to hear about, so the validator refuses it
|
|
1527
1650
|
* rather than leaving an automation that can never fire.
|
|
1528
1651
|
*/
|
|
1529
|
-
|
|
1652
|
+
z9.object({ kind: z9.literal("link_clicked"), mailing_id: Id, url: z9.string().min(1).max(2048).optional(), ...exclude }).strict()
|
|
1530
1653
|
]);
|
|
1531
1654
|
var AUTOMATION_DATE_TRIGGER_KINDS = ["date_anniversary", "exact_date"];
|
|
1532
1655
|
var MAX_TRIGGERS = 10;
|
|
@@ -1545,45 +1668,45 @@ var AUTOMATION_STEP_KINDS = [
|
|
|
1545
1668
|
"webhook",
|
|
1546
1669
|
"notify"
|
|
1547
1670
|
];
|
|
1548
|
-
var AutomationStepKind =
|
|
1671
|
+
var AutomationStepKind = z9.enum(AUTOMATION_STEP_KINDS);
|
|
1549
1672
|
var next = { next: StepId.nullable() };
|
|
1550
|
-
var Unchosen =
|
|
1551
|
-
var DraftSlug =
|
|
1552
|
-
var DraftPropertyKey =
|
|
1553
|
-
var
|
|
1554
|
-
var EmailStepLanguage =
|
|
1555
|
-
subject:
|
|
1556
|
-
preheader:
|
|
1673
|
+
var Unchosen = z9.literal("");
|
|
1674
|
+
var DraftSlug = z9.union([Slug, Unchosen]);
|
|
1675
|
+
var DraftPropertyKey = z9.union([ContactPropertyKey, Unchosen]);
|
|
1676
|
+
var label2 = { label: z9.string().max(120).nullish() };
|
|
1677
|
+
var EmailStepLanguage = z9.object({
|
|
1678
|
+
subject: z9.string().min(1).max(998),
|
|
1679
|
+
preheader: z9.string().max(500).nullable(),
|
|
1557
1680
|
document: TemplateDocument,
|
|
1558
1681
|
/** This language's insertions, its texts over the template's structure (every text present). */
|
|
1559
1682
|
insertions: Insertions.nullish()
|
|
1560
1683
|
}).strict();
|
|
1561
|
-
var EmailStep =
|
|
1562
|
-
kind:
|
|
1563
|
-
...
|
|
1684
|
+
var EmailStep = z9.object({
|
|
1685
|
+
kind: z9.literal("email"),
|
|
1686
|
+
...label2,
|
|
1564
1687
|
...next,
|
|
1565
1688
|
template_id: Id.nullish(),
|
|
1566
1689
|
document: TemplateDocument.nullish(),
|
|
1567
1690
|
/** Empty while the step is being written, or when its template has a subject; else `not_chosen` refuses to publish it. */
|
|
1568
|
-
subject:
|
|
1569
|
-
preheader:
|
|
1691
|
+
subject: z9.string().max(998),
|
|
1692
|
+
preheader: z9.string().max(500).nullish(),
|
|
1570
1693
|
locale: LocaleTag.nullish(),
|
|
1571
|
-
languages:
|
|
1694
|
+
languages: z9.record(LocaleTag, EmailStepLanguage).nullish(),
|
|
1572
1695
|
/** The main language's insertions, snapshot from the template when the automation is published. */
|
|
1573
1696
|
insertions: Insertions.nullish()
|
|
1574
1697
|
}).strict();
|
|
1575
|
-
var DelayStep =
|
|
1576
|
-
kind:
|
|
1577
|
-
...
|
|
1698
|
+
var DelayStep = z9.object({
|
|
1699
|
+
kind: z9.literal("delay"),
|
|
1700
|
+
...label2,
|
|
1578
1701
|
...next,
|
|
1579
|
-
wait:
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1702
|
+
wait: z9.discriminatedUnion("mode", [
|
|
1703
|
+
z9.object({ mode: z9.literal("duration"), seconds: z9.number().int().min(1).max(MAX_DELAY_SECONDS) }).strict(),
|
|
1704
|
+
z9.object({ mode: z9.literal("time_of_day"), at: TimeOfDay }).strict(),
|
|
1705
|
+
z9.object({ mode: z9.literal("weekday"), weekday: Weekday, at: TimeOfDay }).strict(),
|
|
1583
1706
|
/** A fixed day has no next occurrence, so `continue` is not one of its answers. */
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
mode:
|
|
1707
|
+
z9.object({ mode: z9.literal("date"), on: DateOnly, at: TimeOfDay, passed: z9.enum(["skip", "stop"]) }).strict(),
|
|
1708
|
+
z9.object({
|
|
1709
|
+
mode: z9.literal("date_property"),
|
|
1587
1710
|
key: ContactPropertyKey,
|
|
1588
1711
|
offset_days: OffsetDays,
|
|
1589
1712
|
at: TimeOfDay,
|
|
@@ -1591,9 +1714,9 @@ var DelayStep = z8.object({
|
|
|
1591
1714
|
}).strict()
|
|
1592
1715
|
])
|
|
1593
1716
|
}).strict();
|
|
1594
|
-
var ConditionStep =
|
|
1595
|
-
kind:
|
|
1596
|
-
...
|
|
1717
|
+
var ConditionStep = z9.object({
|
|
1718
|
+
kind: z9.literal("condition"),
|
|
1719
|
+
...label2,
|
|
1597
1720
|
filter: BoundedAutomationFilter,
|
|
1598
1721
|
yes: StepId.nullable(),
|
|
1599
1722
|
no: StepId.nullable(),
|
|
@@ -1603,49 +1726,49 @@ var ConditionStep = z8.object({
|
|
|
1603
1726
|
* neither half means anything without the other: a window with no answer
|
|
1604
1727
|
* for the end of it would be a silent decision.
|
|
1605
1728
|
*/
|
|
1606
|
-
wait_for:
|
|
1729
|
+
wait_for: z9.object({ up_to_s: z9.number().int().min(1).max(MAX_DELAY_SECONDS), on_timeout: z9.enum(["yes", "no"]) }).strict().nullish()
|
|
1607
1730
|
}).strict();
|
|
1608
|
-
var SplitBranchKey =
|
|
1731
|
+
var SplitBranchKey = z9.string().regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, underscore and hyphen, 1 to 32");
|
|
1609
1732
|
var MIN_SPLIT_BRANCHES = 2;
|
|
1610
1733
|
var MAX_SPLIT_BRANCHES = 4;
|
|
1611
|
-
var SplitStep =
|
|
1612
|
-
kind:
|
|
1613
|
-
...
|
|
1614
|
-
branches:
|
|
1615
|
-
|
|
1734
|
+
var SplitStep = z9.object({
|
|
1735
|
+
kind: z9.literal("split"),
|
|
1736
|
+
...label2,
|
|
1737
|
+
branches: z9.array(
|
|
1738
|
+
z9.object({
|
|
1616
1739
|
key: SplitBranchKey,
|
|
1617
1740
|
/** A whole percentage. Zero is allowed: a branch kept in the flow but switched off. */
|
|
1618
|
-
weight:
|
|
1741
|
+
weight: z9.number().int().min(0).max(100),
|
|
1619
1742
|
next: StepId.nullable()
|
|
1620
1743
|
}).strict()
|
|
1621
1744
|
).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")
|
|
1622
1745
|
}).strict();
|
|
1623
|
-
var GotoStep =
|
|
1746
|
+
var GotoStep = z9.object({ kind: z9.literal("goto"), ...label2, target: StepId.nullable() }).strict();
|
|
1624
1747
|
var MAX_STEP_VISITS = 20;
|
|
1625
|
-
var WebhookStep =
|
|
1626
|
-
var NotifyRecipients =
|
|
1627
|
-
var NotifyStep =
|
|
1628
|
-
kind:
|
|
1629
|
-
...
|
|
1748
|
+
var WebhookStep = z9.object({ kind: z9.literal("webhook"), ...label2, ...next, endpoint_id: Id.nullable(), payload: JsonValue.nullish() }).strict();
|
|
1749
|
+
var NotifyRecipients = z9.union([z9.literal("owner"), z9.literal("admins"), z9.object({ member_id: Id }).strict()]);
|
|
1750
|
+
var NotifyStep = z9.object({
|
|
1751
|
+
kind: z9.literal("notify"),
|
|
1752
|
+
...label2,
|
|
1630
1753
|
...next,
|
|
1631
1754
|
to: NotifyRecipients,
|
|
1632
1755
|
/** Empty while the step is being written; `not_chosen` refuses to publish it. */
|
|
1633
|
-
subject:
|
|
1634
|
-
body:
|
|
1756
|
+
subject: z9.string().max(998),
|
|
1757
|
+
body: z9.string().max(1e4)
|
|
1635
1758
|
}).strict();
|
|
1636
|
-
var AutomationStep =
|
|
1759
|
+
var AutomationStep = z9.discriminatedUnion("kind", [
|
|
1637
1760
|
EmailStep,
|
|
1638
1761
|
DelayStep,
|
|
1639
1762
|
ConditionStep,
|
|
1640
|
-
|
|
1641
|
-
|
|
1763
|
+
z9.object({ kind: z9.literal("add_tag"), ...label2, ...next, tag: DraftSlug }).strict(),
|
|
1764
|
+
z9.object({ kind: z9.literal("remove_tag"), ...label2, ...next, tag: DraftSlug }).strict(),
|
|
1642
1765
|
/** `value` null clears the property, as a contact upsert does. */
|
|
1643
|
-
|
|
1766
|
+
z9.object({ kind: z9.literal("set_property"), ...label2, ...next, key: DraftPropertyKey, value: JsonValue }).strict(),
|
|
1644
1767
|
/** Subscribing never lifts a suppression: the service checks that, not the flow. */
|
|
1645
|
-
|
|
1768
|
+
z9.object({ kind: z9.literal("subscribe_topic"), ...label2, ...next, topic: DraftSlug }).strict(),
|
|
1646
1769
|
/** `topic` unsubscribes from the automation's own topic, `all` from every topic. */
|
|
1647
|
-
|
|
1648
|
-
|
|
1770
|
+
z9.object({ kind: z9.literal("unsubscribe"), ...label2, ...next, scope: z9.enum(["topic", "all"]) }).strict(),
|
|
1771
|
+
z9.object({ kind: z9.literal("exit"), ...label2 }).strict(),
|
|
1649
1772
|
SplitStep,
|
|
1650
1773
|
GotoStep,
|
|
1651
1774
|
WebhookStep,
|
|
@@ -1669,25 +1792,25 @@ function stepTargets(step) {
|
|
|
1669
1792
|
}
|
|
1670
1793
|
}
|
|
1671
1794
|
var MAX_STEPS = 200;
|
|
1672
|
-
var AutomationFlow =
|
|
1673
|
-
triggers:
|
|
1795
|
+
var AutomationFlow = z9.object({
|
|
1796
|
+
triggers: z9.array(AutomationTrigger).max(MAX_TRIGGERS),
|
|
1674
1797
|
/** The first step. `null` is an empty flow. */
|
|
1675
1798
|
entry: StepId.nullable(),
|
|
1676
|
-
steps:
|
|
1799
|
+
steps: z9.record(StepId, AutomationStep)
|
|
1677
1800
|
}).strict();
|
|
1678
1801
|
var emptyFlow = () => ({ triggers: [], entry: null, steps: {} });
|
|
1679
|
-
var QuietHours =
|
|
1802
|
+
var QuietHours = z9.object({
|
|
1680
1803
|
start: TimeOfDay,
|
|
1681
1804
|
end: TimeOfDay,
|
|
1682
|
-
weekdays:
|
|
1805
|
+
weekdays: z9.array(Weekday).min(1).max(7).nullish()
|
|
1683
1806
|
}).strict();
|
|
1684
1807
|
var RE_ENTRY_MODES = ["off", "after_exit"];
|
|
1685
|
-
var ReEntryMode =
|
|
1808
|
+
var ReEntryMode = z9.enum(RE_ENTRY_MODES);
|
|
1686
1809
|
var MAX_RE_ENTRY_COOLDOWN_SECONDS = 365 * 24 * 60 * 60;
|
|
1687
|
-
var AutomationSettings =
|
|
1810
|
+
var AutomationSettings = z9.object({
|
|
1688
1811
|
re_entry: ReEntryMode,
|
|
1689
1812
|
/** Seconds a person must wait after leaving before entering again. Only with `after_exit`. */
|
|
1690
|
-
re_entry_cooldown_s:
|
|
1813
|
+
re_entry_cooldown_s: z9.number().int().min(0).max(MAX_RE_ENTRY_COOLDOWN_SECONDS).nullish(),
|
|
1691
1814
|
quiet_hours: QuietHours.nullish(),
|
|
1692
1815
|
/** Checked before every step; when it matches, the run ends. */
|
|
1693
1816
|
exit_filter: BoundedAutomationFilter.nullish()
|
|
@@ -1745,18 +1868,18 @@ var AUTOMATION_ISSUE_CODES = [
|
|
|
1745
1868
|
/** A one-off date that is already over everywhere on earth, so it would fire for nobody. */
|
|
1746
1869
|
"date_in_the_past"
|
|
1747
1870
|
];
|
|
1748
|
-
var AutomationIssueCode =
|
|
1749
|
-
var AutomationIssue =
|
|
1871
|
+
var AutomationIssueCode = z9.enum(AUTOMATION_ISSUE_CODES);
|
|
1872
|
+
var AutomationIssue = z9.object({
|
|
1750
1873
|
code: AutomationIssueCode,
|
|
1751
1874
|
/** The step the issue is about, when it is about one. */
|
|
1752
1875
|
step_id: StepId.nullable(),
|
|
1753
1876
|
/** Where inside the document, for the canvas to point at a field. */
|
|
1754
|
-
path:
|
|
1755
|
-
message:
|
|
1877
|
+
path: z9.array(z9.union([z9.string(), z9.number()])),
|
|
1878
|
+
message: z9.string()
|
|
1756
1879
|
});
|
|
1757
|
-
var AutomationValidation =
|
|
1758
|
-
ok:
|
|
1759
|
-
issues:
|
|
1880
|
+
var AutomationValidation = z9.object({
|
|
1881
|
+
ok: z9.boolean(),
|
|
1882
|
+
issues: z9.array(AutomationIssue)
|
|
1760
1883
|
});
|
|
1761
1884
|
function validateAutomation(flow, settings, ctx) {
|
|
1762
1885
|
const issues = [];
|
|
@@ -2147,7 +2270,7 @@ function cycles(flow) {
|
|
|
2147
2270
|
return out.sort();
|
|
2148
2271
|
}
|
|
2149
2272
|
var AUTOMATION_STATUSES = ["draft", "active", "paused", "archived"];
|
|
2150
|
-
var AutomationStatus =
|
|
2273
|
+
var AutomationStatus = z9.enum(AUTOMATION_STATUSES);
|
|
2151
2274
|
var AUTOMATION_END_REASONS = [
|
|
2152
2275
|
"completed",
|
|
2153
2276
|
"exit_filter",
|
|
@@ -2166,9 +2289,9 @@ var AUTOMATION_END_REASONS = [
|
|
|
2166
2289
|
"ended_by_hand",
|
|
2167
2290
|
"failed"
|
|
2168
2291
|
];
|
|
2169
|
-
var AutomationEndReason =
|
|
2292
|
+
var AutomationEndReason = z9.enum(AUTOMATION_END_REASONS);
|
|
2170
2293
|
var AUTOMATION_WAIT_REASONS = ["delay", "quiet_hours", "plan_limit", "sending", "condition"];
|
|
2171
|
-
var AutomationWaitReason =
|
|
2294
|
+
var AutomationWaitReason = z9.enum(AUTOMATION_WAIT_REASONS);
|
|
2172
2295
|
var AUTOMATION_ENTRY_REFUSALS = [
|
|
2173
2296
|
"already_inside",
|
|
2174
2297
|
"re_entry_off",
|
|
@@ -2179,26 +2302,26 @@ var AUTOMATION_ENTRY_REFUSALS = [
|
|
|
2179
2302
|
"not_active",
|
|
2180
2303
|
"unknown_contact"
|
|
2181
2304
|
];
|
|
2182
|
-
var AutomationEntryRefusal =
|
|
2183
|
-
var AutomationCounts =
|
|
2184
|
-
started:
|
|
2185
|
-
in_progress:
|
|
2186
|
-
completed:
|
|
2187
|
-
exited:
|
|
2188
|
-
failed:
|
|
2189
|
-
});
|
|
2190
|
-
var Automation =
|
|
2305
|
+
var AutomationEntryRefusal = z9.enum(AUTOMATION_ENTRY_REFUSALS);
|
|
2306
|
+
var AutomationCounts = z9.object({
|
|
2307
|
+
started: z9.number().int().min(0),
|
|
2308
|
+
in_progress: z9.number().int().min(0),
|
|
2309
|
+
completed: z9.number().int().min(0),
|
|
2310
|
+
exited: z9.number().int().min(0),
|
|
2311
|
+
failed: z9.number().int().min(0)
|
|
2312
|
+
});
|
|
2313
|
+
var Automation = z9.object({
|
|
2191
2314
|
id: Id,
|
|
2192
|
-
name:
|
|
2315
|
+
name: z9.string().min(1).max(200),
|
|
2193
2316
|
status: AutomationStatus,
|
|
2194
2317
|
/** Every automation sends under one topic; there are no transactional automations. */
|
|
2195
2318
|
topic: Slug,
|
|
2196
2319
|
provider_id: Id.nullable(),
|
|
2197
2320
|
settings: AutomationSettings,
|
|
2198
2321
|
/** The flow people are running through, null while the automation has never been published. */
|
|
2199
|
-
published_version:
|
|
2322
|
+
published_version: z9.number().int().min(1).nullable(),
|
|
2200
2323
|
/** The flow being edited. Increments on every save, like a template's version. */
|
|
2201
|
-
draft_version:
|
|
2324
|
+
draft_version: z9.number().int().min(1),
|
|
2202
2325
|
counts: AutomationCounts,
|
|
2203
2326
|
created_at: Timestamp,
|
|
2204
2327
|
updated_at: Timestamp,
|
|
@@ -2211,16 +2334,16 @@ var AutomationWithFlow = Automation.extend({
|
|
|
2211
2334
|
published_flow: AutomationFlow.nullable()
|
|
2212
2335
|
});
|
|
2213
2336
|
var AutomationSummary = Automation;
|
|
2214
|
-
var AutomationCreate =
|
|
2215
|
-
name:
|
|
2337
|
+
var AutomationCreate = z9.object({
|
|
2338
|
+
name: z9.string().min(1).max(200),
|
|
2216
2339
|
topic: Slug,
|
|
2217
2340
|
provider_id: Id.optional(),
|
|
2218
2341
|
flow: AutomationFlow.optional(),
|
|
2219
2342
|
settings: AutomationSettings.partial().optional()
|
|
2220
2343
|
});
|
|
2221
|
-
var AutomationUpdate =
|
|
2222
|
-
base_version:
|
|
2223
|
-
name:
|
|
2344
|
+
var AutomationUpdate = z9.object({
|
|
2345
|
+
base_version: z9.number().int().min(1),
|
|
2346
|
+
name: z9.string().min(1).max(200).optional(),
|
|
2224
2347
|
topic: Slug.optional(),
|
|
2225
2348
|
provider_id: Id.optional(),
|
|
2226
2349
|
flow: AutomationFlow.optional(),
|
|
@@ -2233,42 +2356,42 @@ var AutomationListQuery = PageQuery.extend({
|
|
|
2233
2356
|
status: AutomationStatus.optional(),
|
|
2234
2357
|
topic: Slug.optional()
|
|
2235
2358
|
});
|
|
2236
|
-
var AutomationPublishRequest =
|
|
2359
|
+
var AutomationPublishRequest = z9.object({
|
|
2237
2360
|
/**
|
|
2238
2361
|
* Answer what this publish would do and change nothing, whatever the flow
|
|
2239
2362
|
* looks like. Without it a publish that has no issues and moves nobody
|
|
2240
2363
|
* simply goes ahead, which is right for an API caller and wrong for a
|
|
2241
2364
|
* dialog that is only showing somebody what they are about to do.
|
|
2242
2365
|
*/
|
|
2243
|
-
dry_run:
|
|
2244
|
-
confirm:
|
|
2366
|
+
dry_run: z9.boolean().optional(),
|
|
2367
|
+
confirm: z9.boolean().optional(),
|
|
2245
2368
|
/** Per removed step: the step its waiting runs move to, or null to end them. */
|
|
2246
|
-
moves:
|
|
2369
|
+
moves: z9.record(StepId, StepId.nullable()).optional(),
|
|
2247
2370
|
/** Also enter everyone who matches a trigger's condition right now. */
|
|
2248
|
-
backfill:
|
|
2371
|
+
backfill: z9.boolean().optional()
|
|
2249
2372
|
}).strict();
|
|
2250
|
-
var AutomationPublishMove =
|
|
2373
|
+
var AutomationPublishMove = z9.object({
|
|
2251
2374
|
step_id: StepId,
|
|
2252
2375
|
/** Why the step's people have to move: it is gone, or its delay changed. */
|
|
2253
|
-
change:
|
|
2254
|
-
waiting:
|
|
2376
|
+
change: z9.enum(["removed", "delay_changed", "kind_changed"]),
|
|
2377
|
+
waiting: z9.number().int().min(0),
|
|
2255
2378
|
/** Where they go: a step id, or null to end their runs with `step_removed`. */
|
|
2256
2379
|
destination: StepId.nullable()
|
|
2257
2380
|
});
|
|
2258
|
-
var AutomationPublishResult =
|
|
2259
|
-
published:
|
|
2381
|
+
var AutomationPublishResult = z9.object({
|
|
2382
|
+
published: z9.boolean(),
|
|
2260
2383
|
automation: Automation,
|
|
2261
2384
|
/** Empty when nothing has to move; non-empty and `published` false asks for a confirmation. */
|
|
2262
|
-
moves:
|
|
2385
|
+
moves: z9.array(AutomationPublishMove),
|
|
2263
2386
|
/** How many people the backfill entered, or would enter when it is not confirmed. */
|
|
2264
|
-
backfill:
|
|
2265
|
-
issues:
|
|
2266
|
-
});
|
|
2267
|
-
var AutomationDuplicateRequest =
|
|
2268
|
-
var AutomationEnrollRequest =
|
|
2269
|
-
contact_ids:
|
|
2270
|
-
external_ids:
|
|
2271
|
-
emails:
|
|
2387
|
+
backfill: z9.number().int().min(0),
|
|
2388
|
+
issues: z9.array(AutomationIssue)
|
|
2389
|
+
});
|
|
2390
|
+
var AutomationDuplicateRequest = z9.object({ name: z9.string().min(1).max(200).optional() }).strict();
|
|
2391
|
+
var AutomationEnrollRequest = z9.object({
|
|
2392
|
+
contact_ids: z9.array(Id).min(1).max(1e3).optional(),
|
|
2393
|
+
external_ids: z9.array(z9.string().min(1).max(255)).min(1).max(1e3).optional(),
|
|
2394
|
+
emails: z9.array(Email).min(1).max(1e3).optional(),
|
|
2272
2395
|
segment_id: Id.optional(),
|
|
2273
2396
|
/** Where to start them; the flow's entry step by default. */
|
|
2274
2397
|
step_id: StepId.optional()
|
|
@@ -2276,34 +2399,34 @@ var AutomationEnrollRequest = z8.object({
|
|
|
2276
2399
|
(v) => [v.contact_ids, v.external_ids, v.emails, v.segment_id].filter((x) => x !== void 0).length === 1,
|
|
2277
2400
|
"name the people exactly one way: contact_ids, external_ids, emails or segment_id"
|
|
2278
2401
|
);
|
|
2279
|
-
var AutomationEnrollment =
|
|
2402
|
+
var AutomationEnrollment = z9.object({
|
|
2280
2403
|
contact_id: Id.nullable(),
|
|
2281
2404
|
email: Email.nullable(),
|
|
2282
|
-
external_id:
|
|
2283
|
-
entered:
|
|
2405
|
+
external_id: z9.string().nullable(),
|
|
2406
|
+
entered: z9.boolean(),
|
|
2284
2407
|
run_id: Id.nullable(),
|
|
2285
2408
|
reason: AutomationEntryRefusal.nullable()
|
|
2286
2409
|
});
|
|
2287
|
-
var AutomationEnrollResult =
|
|
2288
|
-
entered:
|
|
2289
|
-
refused:
|
|
2290
|
-
results:
|
|
2410
|
+
var AutomationEnrollResult = z9.object({
|
|
2411
|
+
entered: z9.number().int().min(0),
|
|
2412
|
+
refused: z9.number().int().min(0),
|
|
2413
|
+
results: z9.array(AutomationEnrollment)
|
|
2291
2414
|
});
|
|
2292
|
-
var AutomationRunStatus =
|
|
2293
|
-
var AutomationRun =
|
|
2415
|
+
var AutomationRunStatus = z9.enum(["active", "waiting", "completed", "exited", "failed"]);
|
|
2416
|
+
var AutomationRun = z9.object({
|
|
2294
2417
|
id: Id,
|
|
2295
2418
|
automation_id: Id,
|
|
2296
2419
|
contact_id: Id,
|
|
2297
2420
|
email: Email,
|
|
2298
|
-
external_id:
|
|
2299
|
-
version:
|
|
2421
|
+
external_id: z9.string().nullable(),
|
|
2422
|
+
version: z9.number().int().min(1),
|
|
2300
2423
|
status: AutomationRunStatus,
|
|
2301
2424
|
current_step_id: StepId.nullable(),
|
|
2302
2425
|
next_action_at: Timestamp.nullable(),
|
|
2303
2426
|
wait_reason: AutomationWaitReason.nullable(),
|
|
2304
|
-
end_reason:
|
|
2427
|
+
end_reason: z9.string().nullable(),
|
|
2305
2428
|
/** What went wrong the last time this run tried to take its step, null otherwise. */
|
|
2306
|
-
last_error:
|
|
2429
|
+
last_error: z9.string().nullable(),
|
|
2307
2430
|
entered_at: Timestamp,
|
|
2308
2431
|
ended_at: Timestamp.nullable()
|
|
2309
2432
|
});
|
|
@@ -2313,13 +2436,13 @@ var AutomationRunListQuery = PageQuery.extend({
|
|
|
2313
2436
|
contact_id: Id.optional()
|
|
2314
2437
|
});
|
|
2315
2438
|
var ContactAutomationRun = AutomationRun.extend({
|
|
2316
|
-
automation:
|
|
2439
|
+
automation: z9.object({ id: Id, name: z9.string(), status: AutomationStatus })
|
|
2317
2440
|
});
|
|
2318
2441
|
var ContactAutomationListQuery = PageQuery.extend({ status: AutomationRunStatus.optional() });
|
|
2319
|
-
var AutomationStepExecution =
|
|
2442
|
+
var AutomationStepExecution = z9.object({
|
|
2320
2443
|
step_id: StepId,
|
|
2321
2444
|
step_kind: AutomationStepKind,
|
|
2322
|
-
visit:
|
|
2445
|
+
visit: z9.number().int().min(1),
|
|
2323
2446
|
started_at: Timestamp,
|
|
2324
2447
|
finished_at: Timestamp.nullable(),
|
|
2325
2448
|
/**
|
|
@@ -2331,48 +2454,48 @@ var AutomationStepExecution = z8.object({
|
|
|
2331
2454
|
*/
|
|
2332
2455
|
outcome: Properties
|
|
2333
2456
|
});
|
|
2334
|
-
var AutomationJourney =
|
|
2457
|
+
var AutomationJourney = z9.object({
|
|
2335
2458
|
run: AutomationRun,
|
|
2336
|
-
steps:
|
|
2459
|
+
steps: z9.array(AutomationStepExecution)
|
|
2337
2460
|
});
|
|
2338
|
-
var AutomationStepReport =
|
|
2461
|
+
var AutomationStepReport = z9.object({
|
|
2339
2462
|
step_id: StepId,
|
|
2340
2463
|
step_kind: AutomationStepKind,
|
|
2341
|
-
reached:
|
|
2342
|
-
waiting:
|
|
2343
|
-
passed:
|
|
2464
|
+
reached: z9.number().int().min(0),
|
|
2465
|
+
waiting: z9.number().int().min(0),
|
|
2466
|
+
passed: z9.number().int().min(0),
|
|
2344
2467
|
/** Per branch, for a condition: how many took `yes` and how many `no`. */
|
|
2345
|
-
branches:
|
|
2468
|
+
branches: z9.record(z9.number().int().min(0)),
|
|
2346
2469
|
/** The step mailing's id, for an email step of the published version. */
|
|
2347
2470
|
mailing_id: Id.nullable()
|
|
2348
2471
|
});
|
|
2349
|
-
var AutomationReport =
|
|
2472
|
+
var AutomationReport = z9.object({
|
|
2350
2473
|
counts: AutomationCounts,
|
|
2351
2474
|
/** Why the people who left did: `AutomationEndReason` to a count. */
|
|
2352
|
-
end_reasons:
|
|
2475
|
+
end_reasons: z9.record(z9.number().int().min(0)),
|
|
2353
2476
|
/**
|
|
2354
2477
|
* Why people a trigger named did not enter: `AutomationEntryRefusal` to a
|
|
2355
2478
|
* count. Without it a pause, an exclusion or a missing subscription would
|
|
2356
2479
|
* turn people away invisibly.
|
|
2357
2480
|
*/
|
|
2358
|
-
refusals:
|
|
2359
|
-
steps:
|
|
2360
|
-
});
|
|
2361
|
-
var AutomationExitRunRequest =
|
|
2362
|
-
var AutomationRunParams =
|
|
2363
|
-
var AutomationStepParams =
|
|
2364
|
-
var AutomationTestEmailRequest =
|
|
2365
|
-
var AutomationValidateRequest =
|
|
2366
|
-
var EventCreate =
|
|
2481
|
+
refusals: z9.record(z9.number().int().min(0)),
|
|
2482
|
+
steps: z9.array(AutomationStepReport)
|
|
2483
|
+
});
|
|
2484
|
+
var AutomationExitRunRequest = z9.object({ reason: z9.string().max(200).optional() }).strict();
|
|
2485
|
+
var AutomationRunParams = z9.object({ id: Id, run_id: Id });
|
|
2486
|
+
var AutomationStepParams = z9.object({ id: Id, step_id: StepId });
|
|
2487
|
+
var AutomationTestEmailRequest = z9.object({ to: Email, merge: Properties.optional(), locale: LocaleTag.optional() }).strict();
|
|
2488
|
+
var AutomationValidateRequest = z9.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
|
|
2489
|
+
var EventCreate = z9.object({
|
|
2367
2490
|
name: EventName,
|
|
2368
|
-
contact:
|
|
2491
|
+
contact: z9.object({ external_id: z9.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"),
|
|
2369
2492
|
data: Properties.optional(),
|
|
2370
2493
|
occurred_at: Timestamp.optional(),
|
|
2371
|
-
id:
|
|
2494
|
+
id: z9.string().min(1).max(128).optional()
|
|
2372
2495
|
}).strict();
|
|
2373
|
-
var Event =
|
|
2496
|
+
var Event = z9.object({
|
|
2374
2497
|
id: Id,
|
|
2375
|
-
client_event_id:
|
|
2498
|
+
client_event_id: z9.string().nullable(),
|
|
2376
2499
|
name: EventName,
|
|
2377
2500
|
contact_id: Id,
|
|
2378
2501
|
data: Properties,
|
|
@@ -2383,7 +2506,7 @@ var Event = z8.object({
|
|
|
2383
2506
|
* hand. Each is then admitted or refused by the runtime on its own rules
|
|
2384
2507
|
* (consent, exclusions, re-entry), which `automations.report` counts.
|
|
2385
2508
|
*/
|
|
2386
|
-
entered:
|
|
2509
|
+
entered: z9.number().int().min(0)
|
|
2387
2510
|
});
|
|
2388
2511
|
var EventListQuery = PageQuery.extend({
|
|
2389
2512
|
name: EventName.optional(),
|
|
@@ -2391,19 +2514,19 @@ var EventListQuery = PageQuery.extend({
|
|
|
2391
2514
|
});
|
|
2392
2515
|
|
|
2393
2516
|
// src/workspace.ts
|
|
2394
|
-
var CompanyId =
|
|
2517
|
+
var CompanyId = z10.string().min(1).max(64);
|
|
2395
2518
|
var ASSET_POLICIES = ["any", "service_only"];
|
|
2396
|
-
var AssetPolicy =
|
|
2519
|
+
var AssetPolicy = z10.enum(ASSET_POLICIES);
|
|
2397
2520
|
var MAX_ALLOWED_ASSET_HOSTS = 20;
|
|
2398
|
-
var AssetHost =
|
|
2521
|
+
var AssetHost = z10.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");
|
|
2399
2522
|
var MAX_MERGE_DEFAULTS = 50;
|
|
2400
2523
|
var MAX_MERGE_DEFAULT_LENGTH = 2e3;
|
|
2401
|
-
var MergeFieldName =
|
|
2402
|
-
var MergeDefaultValue =
|
|
2403
|
-
var MergeDefaults =
|
|
2524
|
+
var MergeFieldName = z10.string().regex(/^[a-z][a-z0-9_]*$/, "a merge-field name: lowercase letters, digits and underscores, starting with a letter").max(64);
|
|
2525
|
+
var MergeDefaultValue = z10.union([z10.string().max(MAX_MERGE_DEFAULT_LENGTH), z10.number().finite(), z10.boolean()]);
|
|
2526
|
+
var MergeDefaults = z10.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), {
|
|
2404
2527
|
message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
|
|
2405
2528
|
});
|
|
2406
|
-
var WorkspaceSettings =
|
|
2529
|
+
var WorkspaceSettings = z10.object({
|
|
2407
2530
|
/**
|
|
2408
2531
|
* The workspace's main language (a {@link LocaleTag}, e.g. "de"): the main
|
|
2409
2532
|
* language of a new template, and of the hosted pages when nothing else
|
|
@@ -2416,9 +2539,9 @@ var WorkspaceSettings = z9.object({
|
|
|
2416
2539
|
* each; the hosted pages are offered in those of them they support (English,
|
|
2417
2540
|
* German, French, Italian, Spanish). The service stores canonical casing.
|
|
2418
2541
|
*/
|
|
2419
|
-
locales:
|
|
2542
|
+
locales: z10.array(LocaleTag).min(1).max(20).refine((l) => new Set(l.map((t) => t.toLowerCase())).size === l.length, "a language appears twice"),
|
|
2420
2543
|
/** Open and click tracking. Off by default, opt-in per workspace. */
|
|
2421
|
-
tracking_enabled:
|
|
2544
|
+
tracking_enabled: z10.boolean(),
|
|
2422
2545
|
/** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
|
|
2423
2546
|
asset_policy: AssetPolicy,
|
|
2424
2547
|
/**
|
|
@@ -2426,7 +2549,7 @@ var WorkspaceSettings = z9.object({
|
|
|
2426
2549
|
* the workspace's website, typically. Empty by default. Ignored under `any`,
|
|
2427
2550
|
* which allows everything already.
|
|
2428
2551
|
*/
|
|
2429
|
-
allowed_asset_hosts:
|
|
2552
|
+
allowed_asset_hosts: z10.array(AssetHost).max(MAX_ALLOWED_ASSET_HOSTS),
|
|
2430
2553
|
/** Merge values every mail of the workspace can use (see `MergeDefaults`). Empty by default. */
|
|
2431
2554
|
merge_defaults: MergeDefaults,
|
|
2432
2555
|
/**
|
|
@@ -2435,10 +2558,10 @@ var WorkspaceSettings = z9.object({
|
|
|
2435
2558
|
*/
|
|
2436
2559
|
default_timezone: TimezoneName.nullable()
|
|
2437
2560
|
});
|
|
2438
|
-
var Workspace =
|
|
2561
|
+
var Workspace = z10.object({
|
|
2439
2562
|
id: Id,
|
|
2440
2563
|
slug: Slug,
|
|
2441
|
-
name:
|
|
2564
|
+
name: z10.string().min(1).max(120),
|
|
2442
2565
|
settings: WorkspaceSettings,
|
|
2443
2566
|
/**
|
|
2444
2567
|
* The auth-brain company (tenant) the workspace was created for, or null for
|
|
@@ -2449,13 +2572,13 @@ var Workspace = z9.object({
|
|
|
2449
2572
|
created_at: Timestamp,
|
|
2450
2573
|
updated_at: Timestamp
|
|
2451
2574
|
});
|
|
2452
|
-
var WorkspaceCreate =
|
|
2575
|
+
var WorkspaceCreate = z10.object({
|
|
2453
2576
|
slug: Slug,
|
|
2454
|
-
name:
|
|
2577
|
+
name: z10.string().min(1).max(120),
|
|
2455
2578
|
settings: WorkspaceSettings.partial().optional(),
|
|
2456
|
-
owner:
|
|
2579
|
+
owner: z10.object({
|
|
2457
2580
|
email: Email,
|
|
2458
|
-
name:
|
|
2581
|
+
name: z10.string().max(200).nullable().optional()
|
|
2459
2582
|
}),
|
|
2460
2583
|
/**
|
|
2461
2584
|
* The signed-in person's active auth-brain company. The dashboard always
|
|
@@ -2465,49 +2588,49 @@ var WorkspaceCreate = z9.object({
|
|
|
2465
2588
|
*/
|
|
2466
2589
|
company_id: CompanyId.optional()
|
|
2467
2590
|
});
|
|
2468
|
-
var WorkspaceUpdate =
|
|
2469
|
-
name:
|
|
2591
|
+
var WorkspaceUpdate = z10.object({
|
|
2592
|
+
name: z10.string().min(1).max(120),
|
|
2470
2593
|
settings: WorkspaceSettings.partial()
|
|
2471
2594
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
2472
|
-
var WorkspaceMove =
|
|
2595
|
+
var WorkspaceMove = z10.object({ company_id: CompanyId });
|
|
2473
2596
|
var MEMBER_ROLES = ["owner", "admin", "editor", "viewer"];
|
|
2474
|
-
var MemberRole =
|
|
2597
|
+
var MemberRole = z10.enum(MEMBER_ROLES);
|
|
2475
2598
|
var WorkspaceMembership = Workspace.extend({ role: MemberRole });
|
|
2476
|
-
var Member =
|
|
2599
|
+
var Member = z10.object({
|
|
2477
2600
|
id: Id,
|
|
2478
2601
|
/** The person's auth-brain subject. */
|
|
2479
|
-
subject:
|
|
2602
|
+
subject: z10.string().min(1).max(255),
|
|
2480
2603
|
email: Email,
|
|
2481
|
-
name:
|
|
2604
|
+
name: z10.string().max(200).nullable(),
|
|
2482
2605
|
role: MemberRole,
|
|
2483
2606
|
created_at: Timestamp
|
|
2484
2607
|
});
|
|
2485
|
-
var MemberCreate =
|
|
2486
|
-
subject:
|
|
2608
|
+
var MemberCreate = z10.object({
|
|
2609
|
+
subject: z10.string().min(1).max(255),
|
|
2487
2610
|
email: Email,
|
|
2488
|
-
name:
|
|
2611
|
+
name: z10.string().max(200).nullable().optional(),
|
|
2489
2612
|
role: MemberRole
|
|
2490
2613
|
});
|
|
2491
|
-
var MemberUpdate =
|
|
2614
|
+
var MemberUpdate = z10.object({ role: MemberRole });
|
|
2492
2615
|
var API_KEY_SCOPES = ["full", "read", "send"];
|
|
2493
|
-
var ApiKeyScope =
|
|
2494
|
-
var ApiKey =
|
|
2616
|
+
var ApiKeyScope = z10.enum(API_KEY_SCOPES);
|
|
2617
|
+
var ApiKey = z10.object({
|
|
2495
2618
|
id: Id,
|
|
2496
|
-
name:
|
|
2619
|
+
name: z10.string().min(1).max(120),
|
|
2497
2620
|
/** The first characters of the key, for recognising it in a list. */
|
|
2498
|
-
prefix:
|
|
2621
|
+
prefix: z10.string().min(4).max(32),
|
|
2499
2622
|
scope: ApiKeyScope,
|
|
2500
2623
|
last_used_at: Timestamp.nullable(),
|
|
2501
2624
|
revoked_at: Timestamp.nullable(),
|
|
2502
2625
|
created_at: Timestamp
|
|
2503
2626
|
});
|
|
2504
|
-
var ApiKeyCreate =
|
|
2505
|
-
name:
|
|
2627
|
+
var ApiKeyCreate = z10.object({
|
|
2628
|
+
name: z10.string().min(1).max(120),
|
|
2506
2629
|
/** Omitted means `full`. */
|
|
2507
2630
|
scope: ApiKeyScope.optional()
|
|
2508
2631
|
});
|
|
2509
|
-
var ApiKeyCreated =
|
|
2510
|
-
key:
|
|
2632
|
+
var ApiKeyCreated = z10.object({
|
|
2633
|
+
key: z10.string().min(20),
|
|
2511
2634
|
api_key: ApiKey
|
|
2512
2635
|
});
|
|
2513
2636
|
var AUDIT_ACTIONS = [
|
|
@@ -2534,6 +2657,7 @@ var AUDIT_ACTIONS = [
|
|
|
2534
2657
|
"template.created",
|
|
2535
2658
|
"template.updated",
|
|
2536
2659
|
"template.deleted",
|
|
2660
|
+
"template.inputs_declared",
|
|
2537
2661
|
"saved_section.created",
|
|
2538
2662
|
"saved_section.updated",
|
|
2539
2663
|
"saved_section.deleted",
|
|
@@ -2595,29 +2719,29 @@ var AUDIT_ACTIONS = [
|
|
|
2595
2719
|
"automation.run_ended",
|
|
2596
2720
|
"event.recorded"
|
|
2597
2721
|
];
|
|
2598
|
-
var AuditAction =
|
|
2599
|
-
var AuditActor =
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
type:
|
|
2722
|
+
var AuditAction = z10.enum(AUDIT_ACTIONS);
|
|
2723
|
+
var AuditActor = z10.discriminatedUnion("type", [
|
|
2724
|
+
z10.object({ type: z10.literal("member"), member_id: Id, subject: z10.string().min(1) }),
|
|
2725
|
+
z10.object({
|
|
2726
|
+
type: z10.literal("api_key"),
|
|
2603
2727
|
api_key_id: Id,
|
|
2604
2728
|
/**
|
|
2605
2729
|
* Who the client's application said it was acting for, from
|
|
2606
2730
|
* `ON_BEHALF_OF_HEADER`: reported by that application, never verified.
|
|
2607
2731
|
* Absent when the call carried no label.
|
|
2608
2732
|
*/
|
|
2609
|
-
on_behalf_of:
|
|
2733
|
+
on_behalf_of: z10.string().min(1).max(ON_BEHALF_OF_MAX_LENGTH).optional()
|
|
2610
2734
|
}),
|
|
2611
2735
|
/** The service itself (the worker, a bounce, the hosted unsubscribe page). */
|
|
2612
|
-
|
|
2736
|
+
z10.object({ type: z10.literal("system"), reason: z10.string().min(1).max(200) })
|
|
2613
2737
|
]);
|
|
2614
|
-
var AuditEntry =
|
|
2738
|
+
var AuditEntry = z10.object({
|
|
2615
2739
|
id: Id,
|
|
2616
2740
|
action: AuditAction,
|
|
2617
2741
|
actor: AuditActor,
|
|
2618
|
-
target_type:
|
|
2742
|
+
target_type: z10.string().min(1).max(64),
|
|
2619
2743
|
target_id: Id.nullable(),
|
|
2620
|
-
details:
|
|
2744
|
+
details: z10.record(z10.unknown()),
|
|
2621
2745
|
created_at: Timestamp
|
|
2622
2746
|
});
|
|
2623
2747
|
var AuditQuery = PageQuery.extend({
|
|
@@ -2626,12 +2750,12 @@ var AuditQuery = PageQuery.extend({
|
|
|
2626
2750
|
});
|
|
2627
2751
|
|
|
2628
2752
|
// src/invites.ts
|
|
2629
|
-
import { z as
|
|
2753
|
+
import { z as z11 } from "zod";
|
|
2630
2754
|
var INVITE_STATUSES = ["pending", "accepted", "revoked", "expired"];
|
|
2631
|
-
var InviteStatus =
|
|
2755
|
+
var InviteStatus = z11.enum(INVITE_STATUSES);
|
|
2632
2756
|
var DEFAULT_INVITE_TTL_DAYS = 7;
|
|
2633
2757
|
var MAX_INVITE_TTL_DAYS = 30;
|
|
2634
|
-
var Invite =
|
|
2758
|
+
var Invite = z11.object({
|
|
2635
2759
|
id: Id,
|
|
2636
2760
|
/** Stored lowercased; only a person signed in with this address can accept. */
|
|
2637
2761
|
email: Email,
|
|
@@ -2639,32 +2763,32 @@ var Invite = z10.object({
|
|
|
2639
2763
|
/** Derived from the timestamps below, never stored. */
|
|
2640
2764
|
status: InviteStatus,
|
|
2641
2765
|
/** The member who invited, while they are still one (null after they left). */
|
|
2642
|
-
invited_by:
|
|
2766
|
+
invited_by: z11.object({ member_id: Id.nullable(), email: Email.nullable() }),
|
|
2643
2767
|
expires_at: Timestamp,
|
|
2644
2768
|
accepted_at: Timestamp.nullable(),
|
|
2645
2769
|
revoked_at: Timestamp.nullable(),
|
|
2646
2770
|
created_at: Timestamp
|
|
2647
2771
|
});
|
|
2648
|
-
var InviteCreate =
|
|
2772
|
+
var InviteCreate = z11.object({
|
|
2649
2773
|
email: Email,
|
|
2650
2774
|
role: MemberRole,
|
|
2651
|
-
expires_in_days:
|
|
2775
|
+
expires_in_days: z11.number().int().min(1).max(MAX_INVITE_TTL_DAYS).optional()
|
|
2652
2776
|
});
|
|
2653
|
-
var InviteCreated =
|
|
2777
|
+
var InviteCreated = z11.object({
|
|
2654
2778
|
invite: Invite,
|
|
2655
|
-
token:
|
|
2779
|
+
token: z11.string().min(32).max(128)
|
|
2656
2780
|
});
|
|
2657
2781
|
var InviteListQuery = PageQuery.extend({
|
|
2658
2782
|
status: InviteStatus.optional()
|
|
2659
2783
|
});
|
|
2660
|
-
var InviteAccept =
|
|
2661
|
-
token:
|
|
2784
|
+
var InviteAccept = z11.object({
|
|
2785
|
+
token: z11.string().min(32).max(128),
|
|
2662
2786
|
email: Email,
|
|
2663
|
-
name:
|
|
2787
|
+
name: z11.string().max(200).nullable().optional()
|
|
2664
2788
|
});
|
|
2665
|
-
var InviteAccepted =
|
|
2789
|
+
var InviteAccepted = z11.object({
|
|
2666
2790
|
workspace: WorkspaceMembership,
|
|
2667
|
-
already_member:
|
|
2791
|
+
already_member: z11.boolean()
|
|
2668
2792
|
});
|
|
2669
2793
|
var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
|
|
2670
2794
|
|
|
@@ -2971,14 +3095,14 @@ function insertionNameProblem(insertions, name, current) {
|
|
|
2971
3095
|
if (has(RESERVED_MERGE_FIELDS, name)) return `"${name}" is a merge field the service fills itself. Choose another name.`;
|
|
2972
3096
|
if (name !== current && has(insertions, name)) return `There is already an insertion called "${name}".`;
|
|
2973
3097
|
const chooser = Object.entries(insertions).find(([other, def]) => def.by === name && other !== current);
|
|
2974
|
-
if (chooser) return `"${name}" is the value
|
|
2975
|
-
if (current !== void 0 && ownEntry(insertions, current)?.by === name) return `"${name}" is the value
|
|
3098
|
+
if (chooser) return `"${name}" is the value "${chooser[0]}" depends on. Choose another name.`;
|
|
3099
|
+
if (current !== void 0 && ownEntry(insertions, current)?.by === name) return `"${name}" is the value this insertion depends on. Choose another name.`;
|
|
2976
3100
|
return null;
|
|
2977
3101
|
}
|
|
2978
3102
|
function insertionByProblem(insertions, by, self) {
|
|
2979
|
-
if (!InsertionName.safeParse(by).success) return "The value
|
|
2980
|
-
if (by === "unsubscribe_url") return "
|
|
2981
|
-
if (by === self || has(insertions, by)) return `"${by}" is an insertion, not a value. Use a value the
|
|
3103
|
+
if (!InsertionName.safeParse(by).success) return "The value it depends on is a merge-field name: lowercase letters, digits and underscores (like reason).";
|
|
3104
|
+
if (by === "unsubscribe_url") return "An insertion cannot depend on the unsubscribe link. Use a value the app or the contact provides.";
|
|
3105
|
+
if (by === self || has(insertions, by)) return `"${by}" is an insertion, not a value. Use a value the app or the contact provides.`;
|
|
2982
3106
|
return null;
|
|
2983
3107
|
}
|
|
2984
3108
|
function normalizeChoiceKey(raw) {
|
|
@@ -2986,10 +3110,10 @@ function normalizeChoiceKey(raw) {
|
|
|
2986
3110
|
}
|
|
2987
3111
|
function choiceKeyProblem(insertion, key, current) {
|
|
2988
3112
|
if (INSERTION_TEXT_SLOTS.includes(key)) {
|
|
2989
|
-
return `"${key}"
|
|
3113
|
+
return `"${key}" is kept for the insertion's ${key === "filled" ? '"any other value"' : '"empty"'} sentence, so it cannot be a value. Choose another value.`;
|
|
2990
3114
|
}
|
|
2991
|
-
if (!InsertionChoiceKey.safeParse(key).success) return "A
|
|
2992
|
-
if (key !== current && has(insertion.choices, key)) return `
|
|
3115
|
+
if (!InsertionChoiceKey.safeParse(key).success) return "A value is written exactly as the app sends it: lowercase letters, digits, hyphens and underscores (like venue).";
|
|
3116
|
+
if (key !== current && has(insertion.choices, key)) return `"${key}" already has its own sentence.`;
|
|
2993
3117
|
return null;
|
|
2994
3118
|
}
|
|
2995
3119
|
function addInsertion(drafts, rawName, rawBy) {
|
|
@@ -2998,7 +3122,7 @@ function addInsertion(drafts, rawName, rawBy) {
|
|
|
2998
3122
|
if (Object.keys(drafts.insertions).length >= MAX_INSERTIONS) return { ok: false, message: `A template holds at most ${MAX_INSERTIONS} insertions.` };
|
|
2999
3123
|
const nameProblem = insertionNameProblem(drafts.insertions, name);
|
|
3000
3124
|
if (nameProblem) return { ok: false, message: nameProblem };
|
|
3001
|
-
if (name === by) return { ok: false, message: "An insertion cannot
|
|
3125
|
+
if (name === by) return { ok: false, message: "An insertion cannot depend on its own name. Use the value the app sends (like reason)." };
|
|
3002
3126
|
const byProblem = insertionByProblem(drafts.insertions, by);
|
|
3003
3127
|
if (byProblem) return { ok: false, message: byProblem };
|
|
3004
3128
|
return { ok: true, drafts: editMainInsertions(drafts, { ...drafts.insertions, [name]: { by, choices: {}, filled: null, otherwise: "" } }) };
|
|
@@ -3189,15 +3313,99 @@ function insertionRefusalHint(details) {
|
|
|
3189
3313
|
const missing = Array.isArray(details?.missing) ? details.missing.filter((m) => typeof m === "object" && m !== null && typeof m.insertion === "string" && typeof m.choice === "string") : [];
|
|
3190
3314
|
return missing.length > 0 ? `The language has ${describeMissingTexts(missing)}. ${hint}` : hint;
|
|
3191
3315
|
}
|
|
3316
|
+
function tokenOf(name) {
|
|
3317
|
+
return new RegExp(String.raw`\{\{(\s*)${name}(\s*)(\|[^}]*)?\}\}`, "g");
|
|
3318
|
+
}
|
|
3319
|
+
function renameTokenInText(text, from, to) {
|
|
3320
|
+
let count = 0;
|
|
3321
|
+
const out = text.replace(tokenOf(from), (_m, before, after, fallback) => {
|
|
3322
|
+
count++;
|
|
3323
|
+
return `{{${before}${to}${after}${fallback ?? ""}}}`;
|
|
3324
|
+
});
|
|
3325
|
+
return { text: count > 0 ? out : text, count };
|
|
3326
|
+
}
|
|
3327
|
+
function renameTokenInDocument(document, from, to) {
|
|
3328
|
+
let count = 0;
|
|
3329
|
+
const walk = (value) => {
|
|
3330
|
+
if (typeof value === "string") {
|
|
3331
|
+
const r = renameTokenInText(value, from, to);
|
|
3332
|
+
count += r.count;
|
|
3333
|
+
return r.text;
|
|
3334
|
+
}
|
|
3335
|
+
if (Array.isArray(value)) {
|
|
3336
|
+
let changed = false;
|
|
3337
|
+
const next2 = value.map((v) => {
|
|
3338
|
+
const w = walk(v);
|
|
3339
|
+
if (w !== v) changed = true;
|
|
3340
|
+
return w;
|
|
3341
|
+
});
|
|
3342
|
+
return changed ? next2 : value;
|
|
3343
|
+
}
|
|
3344
|
+
if (value && typeof value === "object") {
|
|
3345
|
+
let changed = false;
|
|
3346
|
+
const next2 = {};
|
|
3347
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3348
|
+
const w = walk(v);
|
|
3349
|
+
if (w !== v) changed = true;
|
|
3350
|
+
next2[k] = w;
|
|
3351
|
+
}
|
|
3352
|
+
return changed ? next2 : value;
|
|
3353
|
+
}
|
|
3354
|
+
return value;
|
|
3355
|
+
};
|
|
3356
|
+
const renamed = walk(document);
|
|
3357
|
+
return { document: renamed, count };
|
|
3358
|
+
}
|
|
3359
|
+
function renameInsertionPlacements(drafts, from, to) {
|
|
3360
|
+
if (from === to) return { drafts, places: [] };
|
|
3361
|
+
const places = [];
|
|
3362
|
+
const note2 = (locale, where, count) => {
|
|
3363
|
+
if (count > 0) places.push({ locale, where, count });
|
|
3364
|
+
};
|
|
3365
|
+
const body = renameTokenInDocument(drafts.main.document, from, to);
|
|
3366
|
+
const subject = renameTokenInText(drafts.main.subject, from, to);
|
|
3367
|
+
const preheader = renameTokenInText(drafts.main.preheader, from, to);
|
|
3368
|
+
note2(null, "body", body.count);
|
|
3369
|
+
note2(null, "subject", subject.count);
|
|
3370
|
+
note2(null, "preheader", preheader.count);
|
|
3371
|
+
let out = {
|
|
3372
|
+
...drafts,
|
|
3373
|
+
main: { subject: subject.text, preheader: preheader.text, document: body.document },
|
|
3374
|
+
mainDocumentChanged: drafts.mainDocumentChanged || body.count > 0
|
|
3375
|
+
};
|
|
3376
|
+
for (const [tag, version] of Object.entries(drafts.translations)) {
|
|
3377
|
+
const b = renameTokenInDocument(version.document, from, to);
|
|
3378
|
+
const s = version.subject === null ? { text: null, count: 0 } : renameTokenInText(version.subject, from, to);
|
|
3379
|
+
const p = version.preheader === null ? { text: null, count: 0 } : renameTokenInText(version.preheader, from, to);
|
|
3380
|
+
note2(tag, "body", b.count);
|
|
3381
|
+
note2(tag, "subject", s.count);
|
|
3382
|
+
note2(tag, "preheader", p.count);
|
|
3383
|
+
if (b.count + s.count + p.count === 0) continue;
|
|
3384
|
+
out = {
|
|
3385
|
+
...out,
|
|
3386
|
+
translations: { ...out.translations, [tag]: { ...version, document: b.document, subject: s.text, preheader: p.text } },
|
|
3387
|
+
changed: out.changed.includes(tag) ? out.changed : [...out.changed, tag]
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
return { drafts: out, places };
|
|
3391
|
+
}
|
|
3392
|
+
function renameInsertionEverywhere(drafts, from, rawTo) {
|
|
3393
|
+
const renamed = renameInsertionChecked(drafts, from, rawTo);
|
|
3394
|
+
if (!renamed.ok) return renamed;
|
|
3395
|
+
const to = rawTo.trim();
|
|
3396
|
+
if (to === from) return { ok: true, drafts, places: [] };
|
|
3397
|
+
const placed = renameInsertionPlacements(renamed.drafts, from, to);
|
|
3398
|
+
return { ok: true, drafts: placed.drafts, places: placed.places };
|
|
3399
|
+
}
|
|
3192
3400
|
|
|
3193
3401
|
// src/providers.ts
|
|
3194
|
-
import { z as
|
|
3402
|
+
import { z as z12 } from "zod";
|
|
3195
3403
|
var PROVIDER_KINDS = ["smtp", "resend"];
|
|
3196
|
-
var ProviderKind =
|
|
3197
|
-
var ProviderPolicy =
|
|
3198
|
-
daily_recipient_budget:
|
|
3199
|
-
min_interval_ms:
|
|
3200
|
-
max_recipients_per_message:
|
|
3404
|
+
var ProviderKind = z12.enum(PROVIDER_KINDS);
|
|
3405
|
+
var ProviderPolicy = z12.object({
|
|
3406
|
+
daily_recipient_budget: z12.number().int().min(1).max(1e7),
|
|
3407
|
+
min_interval_ms: z12.number().int().min(0).max(36e5),
|
|
3408
|
+
max_recipients_per_message: z12.number().int().min(1).max(1e3)
|
|
3201
3409
|
});
|
|
3202
3410
|
var ICLOUD_SMTP_POLICY = {
|
|
3203
3411
|
daily_recipient_budget: 800,
|
|
@@ -3205,17 +3413,17 @@ var ICLOUD_SMTP_POLICY = {
|
|
|
3205
3413
|
max_recipients_per_message: 1
|
|
3206
3414
|
};
|
|
3207
3415
|
var SMTP_SECURITY = ["tls", "starttls"];
|
|
3208
|
-
var SmtpConfigBase =
|
|
3209
|
-
host:
|
|
3210
|
-
port:
|
|
3416
|
+
var SmtpConfigBase = z12.object({
|
|
3417
|
+
host: z12.string().min(1).max(255),
|
|
3418
|
+
port: z12.number().int().min(1).max(65535),
|
|
3211
3419
|
/** `tls` connects encrypted (465), `starttls` upgrades (587). Never plaintext. */
|
|
3212
|
-
security:
|
|
3213
|
-
username:
|
|
3420
|
+
security: z12.enum(SMTP_SECURITY),
|
|
3421
|
+
username: z12.string().min(1).max(255)
|
|
3214
3422
|
});
|
|
3215
|
-
var ResendConfigBase =
|
|
3216
|
-
var ProviderRejections =
|
|
3217
|
-
count:
|
|
3218
|
-
last_error:
|
|
3423
|
+
var ResendConfigBase = z12.object({});
|
|
3424
|
+
var ProviderRejections = z12.object({
|
|
3425
|
+
count: z12.number().int().min(0),
|
|
3426
|
+
last_error: z12.string().nullable(),
|
|
3219
3427
|
last_at: Timestamp.nullable(),
|
|
3220
3428
|
/**
|
|
3221
3429
|
* The last time the bounce circuit breaker tripped on one of this provider's
|
|
@@ -3225,59 +3433,59 @@ var ProviderRejections = z11.object({
|
|
|
3225
3433
|
* run's bounce blocks were undone and the mailing paused. `sample` is the
|
|
3226
3434
|
* reply that tripped it.
|
|
3227
3435
|
*/
|
|
3228
|
-
anomaly:
|
|
3436
|
+
anomaly: z12.object({
|
|
3229
3437
|
at: Timestamp,
|
|
3230
3438
|
/** `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. */
|
|
3231
|
-
scope:
|
|
3439
|
+
scope: z12.enum(["mailing", "provider"]),
|
|
3232
3440
|
/**
|
|
3233
3441
|
* True while the provider-wide breaker is open: no bounce blocks through
|
|
3234
3442
|
* this provider, test sends refused and mailings not started or resumed
|
|
3235
3443
|
* (`provider_anomaly`), until an admin clears it (`providers.clearAnomaly`).
|
|
3236
3444
|
*/
|
|
3237
|
-
blocking:
|
|
3445
|
+
blocking: z12.boolean(),
|
|
3238
3446
|
mailing_id: Id.nullable(),
|
|
3239
|
-
reason:
|
|
3240
|
-
sample:
|
|
3447
|
+
reason: z12.string(),
|
|
3448
|
+
sample: z12.string()
|
|
3241
3449
|
}).nullable()
|
|
3242
3450
|
});
|
|
3243
|
-
var ProviderEvents =
|
|
3244
|
-
status:
|
|
3245
|
-
source:
|
|
3246
|
-
url:
|
|
3247
|
-
error:
|
|
3451
|
+
var ProviderEvents = z12.object({
|
|
3452
|
+
status: z12.enum(["active", "needs_secret"]),
|
|
3453
|
+
source: z12.enum(["automatic", "manual"]).nullable(),
|
|
3454
|
+
url: z12.string().url(),
|
|
3455
|
+
error: z12.string().nullable(),
|
|
3248
3456
|
/**
|
|
3249
3457
|
* Events that named an email this provider never sent through the service
|
|
3250
3458
|
* (another workspace or system sharing the Resend account): counted, never
|
|
3251
3459
|
* acted on.
|
|
3252
3460
|
*/
|
|
3253
|
-
unmatched:
|
|
3461
|
+
unmatched: z12.number().int().min(0)
|
|
3254
3462
|
});
|
|
3255
3463
|
var RESEND_EVENT_TYPES = ["email.bounced", "email.complained", "email.delivery_delayed"];
|
|
3256
|
-
var ResendSigningSecret =
|
|
3257
|
-
var ProviderEventsSecret =
|
|
3464
|
+
var ResendSigningSecret = z12.string().max(200).regex(/^whsec_[A-Za-z0-9+/]{16,}={0,2}$/, "must be the signing secret Resend shows, starting with whsec_");
|
|
3465
|
+
var ProviderEventsSecret = z12.object({ signing_secret: ResendSigningSecret });
|
|
3258
3466
|
var ProviderFields = {
|
|
3259
|
-
name:
|
|
3260
|
-
from_name:
|
|
3467
|
+
name: z12.string().min(1).max(120),
|
|
3468
|
+
from_name: z12.string().min(1).max(120),
|
|
3261
3469
|
from_email: Email,
|
|
3262
3470
|
reply_to: Email.nullable(),
|
|
3263
3471
|
policy: ProviderPolicy
|
|
3264
3472
|
};
|
|
3265
|
-
var Provider =
|
|
3266
|
-
|
|
3473
|
+
var Provider = z12.discriminatedUnion("kind", [
|
|
3474
|
+
z12.object({
|
|
3267
3475
|
id: Id,
|
|
3268
|
-
kind:
|
|
3476
|
+
kind: z12.literal("smtp"),
|
|
3269
3477
|
config: SmtpConfigBase,
|
|
3270
|
-
has_secret:
|
|
3478
|
+
has_secret: z12.boolean(),
|
|
3271
3479
|
...ProviderFields,
|
|
3272
3480
|
rejections: ProviderRejections,
|
|
3273
3481
|
created_at: Timestamp,
|
|
3274
3482
|
updated_at: Timestamp
|
|
3275
3483
|
}),
|
|
3276
|
-
|
|
3484
|
+
z12.object({
|
|
3277
3485
|
id: Id,
|
|
3278
|
-
kind:
|
|
3486
|
+
kind: z12.literal("resend"),
|
|
3279
3487
|
config: ResendConfigBase,
|
|
3280
|
-
has_secret:
|
|
3488
|
+
has_secret: z12.boolean(),
|
|
3281
3489
|
...ProviderFields,
|
|
3282
3490
|
rejections: ProviderRejections,
|
|
3283
3491
|
events: ProviderEvents,
|
|
@@ -3285,31 +3493,31 @@ var Provider = z11.discriminatedUnion("kind", [
|
|
|
3285
3493
|
updated_at: Timestamp
|
|
3286
3494
|
})
|
|
3287
3495
|
]);
|
|
3288
|
-
var ProviderCreate =
|
|
3289
|
-
|
|
3290
|
-
kind:
|
|
3291
|
-
config: SmtpConfigBase.extend({ password:
|
|
3496
|
+
var ProviderCreate = z12.discriminatedUnion("kind", [
|
|
3497
|
+
z12.object({
|
|
3498
|
+
kind: z12.literal("smtp"),
|
|
3499
|
+
config: SmtpConfigBase.extend({ password: z12.string().min(1).max(1024) }),
|
|
3292
3500
|
...ProviderFields
|
|
3293
3501
|
}),
|
|
3294
|
-
|
|
3295
|
-
kind:
|
|
3296
|
-
config: ResendConfigBase.extend({ api_key:
|
|
3502
|
+
z12.object({
|
|
3503
|
+
kind: z12.literal("resend"),
|
|
3504
|
+
config: ResendConfigBase.extend({ api_key: z12.string().min(1).max(1024) }),
|
|
3297
3505
|
...ProviderFields
|
|
3298
3506
|
})
|
|
3299
3507
|
]);
|
|
3300
|
-
var ProviderUpdate =
|
|
3301
|
-
|
|
3302
|
-
kind:
|
|
3303
|
-
config: SmtpConfigBase.extend({ password:
|
|
3508
|
+
var ProviderUpdate = z12.discriminatedUnion("kind", [
|
|
3509
|
+
z12.object({
|
|
3510
|
+
kind: z12.literal("smtp"),
|
|
3511
|
+
config: SmtpConfigBase.extend({ password: z12.string().min(1).max(1024).optional() }).partial().optional(),
|
|
3304
3512
|
name: ProviderFields.name.optional(),
|
|
3305
3513
|
from_name: ProviderFields.from_name.optional(),
|
|
3306
3514
|
from_email: ProviderFields.from_email.optional(),
|
|
3307
3515
|
reply_to: ProviderFields.reply_to.optional(),
|
|
3308
3516
|
policy: ProviderPolicy.partial().optional()
|
|
3309
3517
|
}),
|
|
3310
|
-
|
|
3311
|
-
kind:
|
|
3312
|
-
config: ResendConfigBase.extend({ api_key:
|
|
3518
|
+
z12.object({
|
|
3519
|
+
kind: z12.literal("resend"),
|
|
3520
|
+
config: ResendConfigBase.extend({ api_key: z12.string().min(1).max(1024).optional() }).optional(),
|
|
3313
3521
|
name: ProviderFields.name.optional(),
|
|
3314
3522
|
from_name: ProviderFields.from_name.optional(),
|
|
3315
3523
|
from_email: ProviderFields.from_email.optional(),
|
|
@@ -3317,48 +3525,48 @@ var ProviderUpdate = z11.discriminatedUnion("kind", [
|
|
|
3317
3525
|
policy: ProviderPolicy.partial().optional()
|
|
3318
3526
|
})
|
|
3319
3527
|
]);
|
|
3320
|
-
var ProviderUsage =
|
|
3528
|
+
var ProviderUsage = z12.object({
|
|
3321
3529
|
provider_id: Id,
|
|
3322
|
-
recipients_last_24h:
|
|
3323
|
-
remaining_budget:
|
|
3530
|
+
recipients_last_24h: z12.number().int().min(0),
|
|
3531
|
+
remaining_budget: z12.number().int().min(0),
|
|
3324
3532
|
/** When enough of the rolling window frees up to send again, if exhausted. */
|
|
3325
3533
|
next_capacity_at: Timestamp.nullable()
|
|
3326
3534
|
});
|
|
3327
|
-
var ProviderVerifyResult =
|
|
3328
|
-
ok:
|
|
3329
|
-
error:
|
|
3535
|
+
var ProviderVerifyResult = z12.object({
|
|
3536
|
+
ok: z12.boolean(),
|
|
3537
|
+
error: z12.string().nullable()
|
|
3330
3538
|
});
|
|
3331
3539
|
|
|
3332
3540
|
// src/contacts.ts
|
|
3333
|
-
import { z as
|
|
3334
|
-
var Topic =
|
|
3541
|
+
import { z as z13 } from "zod";
|
|
3542
|
+
var Topic = z13.object({
|
|
3335
3543
|
id: Id,
|
|
3336
3544
|
slug: Slug,
|
|
3337
|
-
name:
|
|
3545
|
+
name: z13.string().min(1).max(120),
|
|
3338
3546
|
/** Shown on the hosted preference page. */
|
|
3339
|
-
description:
|
|
3547
|
+
description: z13.string().max(1e3).nullable(),
|
|
3340
3548
|
/** Translations of `name` and `description` per locale, for the hosted page. */
|
|
3341
|
-
translations:
|
|
3342
|
-
|
|
3549
|
+
translations: z13.record(
|
|
3550
|
+
z13.object({ name: z13.string().min(1).max(120), description: z13.string().max(1e3).nullable() })
|
|
3343
3551
|
),
|
|
3344
3552
|
created_at: Timestamp,
|
|
3345
3553
|
updated_at: Timestamp
|
|
3346
3554
|
});
|
|
3347
|
-
var TopicCreate =
|
|
3555
|
+
var TopicCreate = z13.object({
|
|
3348
3556
|
slug: Slug,
|
|
3349
|
-
name:
|
|
3350
|
-
description:
|
|
3557
|
+
name: z13.string().min(1).max(120),
|
|
3558
|
+
description: z13.string().max(1e3).optional(),
|
|
3351
3559
|
translations: Topic.shape.translations.optional()
|
|
3352
3560
|
});
|
|
3353
3561
|
var TopicUpdate = TopicCreate.omit({ slug: true }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
3354
|
-
var Contact =
|
|
3562
|
+
var Contact = z13.object({
|
|
3355
3563
|
id: Id,
|
|
3356
|
-
external_id:
|
|
3564
|
+
external_id: z13.string().min(1).max(255).nullable(),
|
|
3357
3565
|
/** Stored lowercased, unique per workspace. */
|
|
3358
3566
|
email: Email,
|
|
3359
|
-
first_name:
|
|
3360
|
-
last_name:
|
|
3361
|
-
locale:
|
|
3567
|
+
first_name: z13.string().max(200).nullable(),
|
|
3568
|
+
last_name: z13.string().max(200).nullable(),
|
|
3569
|
+
locale: z13.string().min(2).max(35).nullable(),
|
|
3362
3570
|
/**
|
|
3363
3571
|
* The person's own time zone, an IANA name such as `Europe/Berlin` (A1).
|
|
3364
3572
|
* Every wall clock an automation waits for is read in it, falling back to the
|
|
@@ -3366,65 +3574,65 @@ var Contact = z12.object({
|
|
|
3366
3574
|
*/
|
|
3367
3575
|
timezone: TimezoneName.nullable(),
|
|
3368
3576
|
properties: Properties,
|
|
3369
|
-
topics:
|
|
3577
|
+
topics: z13.array(Slug),
|
|
3370
3578
|
/** The slugs of the contact's tags (S4). */
|
|
3371
|
-
tags:
|
|
3579
|
+
tags: z13.array(Slug),
|
|
3372
3580
|
created_at: Timestamp,
|
|
3373
3581
|
updated_at: Timestamp
|
|
3374
3582
|
});
|
|
3375
|
-
var ContactUpsert =
|
|
3376
|
-
external_id:
|
|
3583
|
+
var ContactUpsert = z13.object({
|
|
3584
|
+
external_id: z13.string().min(1).max(255).optional(),
|
|
3377
3585
|
email: Email.optional(),
|
|
3378
|
-
first_name:
|
|
3379
|
-
last_name:
|
|
3380
|
-
locale:
|
|
3586
|
+
first_name: z13.string().max(200).nullable().optional(),
|
|
3587
|
+
last_name: z13.string().max(200).nullable().optional(),
|
|
3588
|
+
locale: z13.string().min(2).max(35).nullable().optional(),
|
|
3381
3589
|
timezone: TimezoneName.nullable().optional(),
|
|
3382
3590
|
properties: Properties.optional(),
|
|
3383
|
-
topics:
|
|
3591
|
+
topics: z13.array(Slug).max(100).optional()
|
|
3384
3592
|
}).refine((v) => v.external_id !== void 0 || v.email !== void 0, {
|
|
3385
3593
|
message: "external_id or email is required",
|
|
3386
3594
|
path: ["email"]
|
|
3387
3595
|
});
|
|
3388
|
-
var ContactUpsertResult =
|
|
3596
|
+
var ContactUpsertResult = z13.object({
|
|
3389
3597
|
contact: Contact,
|
|
3390
|
-
created:
|
|
3598
|
+
created: z13.boolean()
|
|
3391
3599
|
});
|
|
3392
3600
|
var ContactListQuery = PageQuery.extend({
|
|
3393
|
-
email:
|
|
3394
|
-
external_id:
|
|
3601
|
+
email: z13.string().min(1).max(254).optional(),
|
|
3602
|
+
external_id: z13.string().min(1).max(255).optional(),
|
|
3395
3603
|
topic: Slug.optional()
|
|
3396
3604
|
});
|
|
3397
|
-
var ContactErased =
|
|
3398
|
-
ok:
|
|
3399
|
-
erased_messages:
|
|
3400
|
-
erased_recipients:
|
|
3401
|
-
suppressions_kept:
|
|
3605
|
+
var ContactErased = z13.object({
|
|
3606
|
+
ok: z13.literal(true),
|
|
3607
|
+
erased_messages: z13.number().int().min(0),
|
|
3608
|
+
erased_recipients: z13.number().int().min(0),
|
|
3609
|
+
suppressions_kept: z13.number().int().min(0)
|
|
3402
3610
|
});
|
|
3403
3611
|
var SUPPRESSION_REASONS = ["unsubscribed", "bounced", "complained", "manual"];
|
|
3404
|
-
var SuppressionReason =
|
|
3405
|
-
var Suppression =
|
|
3612
|
+
var SuppressionReason = z13.enum(SUPPRESSION_REASONS);
|
|
3613
|
+
var Suppression = z13.object({
|
|
3406
3614
|
id: Id,
|
|
3407
3615
|
email: Email,
|
|
3408
3616
|
reason: SuppressionReason,
|
|
3409
3617
|
topic: Slug.nullable(),
|
|
3410
3618
|
source_message_id: Id.nullable(),
|
|
3411
|
-
note:
|
|
3619
|
+
note: z13.string().max(1e3).nullable(),
|
|
3412
3620
|
created_at: Timestamp
|
|
3413
3621
|
});
|
|
3414
|
-
var SuppressionCreate =
|
|
3622
|
+
var SuppressionCreate = z13.object({
|
|
3415
3623
|
email: Email,
|
|
3416
|
-
reason:
|
|
3624
|
+
reason: z13.enum(["manual", "unsubscribed"]),
|
|
3417
3625
|
topic: Slug.nullable().optional(),
|
|
3418
|
-
note:
|
|
3626
|
+
note: z13.string().max(1e3).optional()
|
|
3419
3627
|
});
|
|
3420
3628
|
var SuppressionListQuery = PageQuery.extend({
|
|
3421
|
-
email:
|
|
3629
|
+
email: z13.string().min(1).max(254).optional(),
|
|
3422
3630
|
reason: SuppressionReason.optional(),
|
|
3423
3631
|
topic: Slug.optional()
|
|
3424
3632
|
});
|
|
3425
3633
|
|
|
3426
3634
|
// src/webhooks.ts
|
|
3427
|
-
import { z as
|
|
3635
|
+
import { z as z14 } from "zod";
|
|
3428
3636
|
var WEBHOOK_EVENT_TYPES = [
|
|
3429
3637
|
"message.sent",
|
|
3430
3638
|
"message.failed",
|
|
@@ -3468,23 +3676,23 @@ var WEBHOOK_EVENT_TYPES = [
|
|
|
3468
3676
|
"template.updated",
|
|
3469
3677
|
"template.deleted"
|
|
3470
3678
|
];
|
|
3471
|
-
var WebhookEventType =
|
|
3472
|
-
var MessageEventBase =
|
|
3679
|
+
var WebhookEventType = z14.enum(WEBHOOK_EVENT_TYPES);
|
|
3680
|
+
var MessageEventBase = z14.object({
|
|
3473
3681
|
message_id: Id,
|
|
3474
3682
|
mailing_id: Id.nullable(),
|
|
3475
3683
|
mailing_metadata: MailingMetadata.nullable(),
|
|
3476
3684
|
recipient_id: Id.nullable(),
|
|
3477
3685
|
contact_id: Id.nullable(),
|
|
3478
|
-
external_id:
|
|
3686
|
+
external_id: z14.string().nullable(),
|
|
3479
3687
|
email: Email,
|
|
3480
|
-
subject:
|
|
3688
|
+
subject: z14.string(),
|
|
3481
3689
|
topic: Slug.nullable(),
|
|
3482
|
-
provider_message_id:
|
|
3483
|
-
is_test:
|
|
3690
|
+
provider_message_id: z14.string().nullable(),
|
|
3691
|
+
is_test: z14.boolean()
|
|
3484
3692
|
});
|
|
3485
3693
|
var MessageSentData = MessageEventBase.extend({
|
|
3486
3694
|
/** The final HTML as sent, so the client can archive it beside its own records. */
|
|
3487
|
-
html:
|
|
3695
|
+
html: z14.string(),
|
|
3488
3696
|
sent_at: Timestamp,
|
|
3489
3697
|
/**
|
|
3490
3698
|
* The choice each insertion made (see `Message.insertions`); absent or null
|
|
@@ -3495,20 +3703,20 @@ var MessageSentData = MessageEventBase.extend({
|
|
|
3495
3703
|
insertions: InsertionChoices.nullish()
|
|
3496
3704
|
});
|
|
3497
3705
|
var MessageFailedData = MessageEventBase.extend({
|
|
3498
|
-
error:
|
|
3706
|
+
error: z14.string(),
|
|
3499
3707
|
/** False when the provider rejected permanently; the recipient will not be retried. */
|
|
3500
|
-
retryable:
|
|
3708
|
+
retryable: z14.boolean(),
|
|
3501
3709
|
failed_at: Timestamp
|
|
3502
3710
|
});
|
|
3503
3711
|
var UNSUBSCRIBE_SOURCES = ["hosted_page", "one_click", "api", "dashboard", "signup_form"];
|
|
3504
|
-
var ContactUnsubscribedData =
|
|
3712
|
+
var ContactUnsubscribedData = z14.object({
|
|
3505
3713
|
contact_id: Id.nullable(),
|
|
3506
|
-
external_id:
|
|
3714
|
+
external_id: z14.string().nullable(),
|
|
3507
3715
|
email: Email,
|
|
3508
3716
|
/** The topic unsubscribed from; null means every topic. */
|
|
3509
3717
|
topic: Slug.nullable(),
|
|
3510
3718
|
mailing_id: Id.nullable(),
|
|
3511
|
-
source:
|
|
3719
|
+
source: z14.enum(UNSUBSCRIBE_SOURCES),
|
|
3512
3720
|
unsubscribed_at: Timestamp
|
|
3513
3721
|
});
|
|
3514
3722
|
var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: true }).extend({
|
|
@@ -3517,142 +3725,142 @@ var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: tr
|
|
|
3517
3725
|
* breaker undid a `bounced` block it judged false (a `contact.bounced` for the
|
|
3518
3726
|
* same address and mailing went out before); `topic` is null.
|
|
3519
3727
|
*/
|
|
3520
|
-
source:
|
|
3728
|
+
source: z14.enum([...UNSUBSCRIBE_SOURCES, "bounce_reverted"]),
|
|
3521
3729
|
resubscribed_at: Timestamp
|
|
3522
3730
|
});
|
|
3523
|
-
var ContactBouncedData =
|
|
3731
|
+
var ContactBouncedData = z14.object({
|
|
3524
3732
|
contact_id: Id.nullable(),
|
|
3525
|
-
external_id:
|
|
3733
|
+
external_id: z14.string().nullable(),
|
|
3526
3734
|
email: Email,
|
|
3527
3735
|
/** `bounced` for a hard bounce, `complained` for a spam complaint. */
|
|
3528
|
-
reason:
|
|
3736
|
+
reason: z14.enum(["bounced", "complained"]),
|
|
3529
3737
|
message_id: Id.nullable(),
|
|
3530
|
-
diagnostic:
|
|
3738
|
+
diagnostic: z14.string().nullable(),
|
|
3531
3739
|
bounced_at: Timestamp
|
|
3532
3740
|
});
|
|
3533
|
-
var MailingFinishedData =
|
|
3741
|
+
var MailingFinishedData = z14.object({
|
|
3534
3742
|
mailing_id: Id,
|
|
3535
3743
|
mailing_metadata: MailingMetadata,
|
|
3536
|
-
status:
|
|
3744
|
+
status: z14.enum(["sent", "partially_failed", "cancelled"]),
|
|
3537
3745
|
counts: MailingCounts,
|
|
3538
3746
|
finished_at: Timestamp
|
|
3539
3747
|
});
|
|
3540
|
-
var ContactSubscribedData =
|
|
3748
|
+
var ContactSubscribedData = z14.object({
|
|
3541
3749
|
contact_id: Id,
|
|
3542
|
-
external_id:
|
|
3750
|
+
external_id: z14.string().nullable(),
|
|
3543
3751
|
email: Email,
|
|
3544
|
-
topics:
|
|
3545
|
-
source:
|
|
3752
|
+
topics: z14.array(Slug),
|
|
3753
|
+
source: z14.enum(["signup_form", "api", "dashboard", "automation"]),
|
|
3546
3754
|
signup_form_id: Id.nullable(),
|
|
3547
|
-
signup_form_version:
|
|
3755
|
+
signup_form_version: z14.number().int().min(1).nullable(),
|
|
3548
3756
|
subscribed_at: Timestamp
|
|
3549
3757
|
});
|
|
3550
|
-
var ImportFinishedData =
|
|
3758
|
+
var ImportFinishedData = z14.object({
|
|
3551
3759
|
import_id: Id,
|
|
3552
|
-
status:
|
|
3760
|
+
status: z14.enum(["completed", "cancelled", "failed"]),
|
|
3553
3761
|
result: ImportReport.nullable(),
|
|
3554
|
-
error:
|
|
3762
|
+
error: z14.string().nullable(),
|
|
3555
3763
|
finished_at: Timestamp
|
|
3556
3764
|
});
|
|
3557
|
-
var MailingScheduledData =
|
|
3765
|
+
var MailingScheduledData = z14.object({
|
|
3558
3766
|
mailing_id: Id,
|
|
3559
3767
|
mailing_metadata: MailingMetadata,
|
|
3560
3768
|
scheduled_at: Timestamp.nullable()
|
|
3561
3769
|
});
|
|
3562
|
-
var MailingStartedData =
|
|
3770
|
+
var MailingStartedData = z14.object({
|
|
3563
3771
|
mailing_id: Id,
|
|
3564
3772
|
mailing_metadata: MailingMetadata,
|
|
3565
|
-
trigger:
|
|
3566
|
-
recipients:
|
|
3773
|
+
trigger: z14.enum(["send", "schedule"]),
|
|
3774
|
+
recipients: z14.number().int().min(0),
|
|
3567
3775
|
started_at: Timestamp
|
|
3568
3776
|
});
|
|
3569
|
-
var MailingScheduleFailedData =
|
|
3777
|
+
var MailingScheduleFailedData = z14.object({
|
|
3570
3778
|
mailing_id: Id,
|
|
3571
3779
|
mailing_metadata: MailingMetadata,
|
|
3572
|
-
code:
|
|
3573
|
-
message:
|
|
3780
|
+
code: z14.string(),
|
|
3781
|
+
message: z14.string(),
|
|
3574
3782
|
failed_at: Timestamp
|
|
3575
3783
|
});
|
|
3576
|
-
var MailingAbWinnerSelectedData =
|
|
3784
|
+
var MailingAbWinnerSelectedData = z14.object({
|
|
3577
3785
|
mailing_id: Id,
|
|
3578
3786
|
mailing_metadata: MailingMetadata,
|
|
3579
|
-
winner:
|
|
3580
|
-
decided_by:
|
|
3581
|
-
variants:
|
|
3787
|
+
winner: z14.string(),
|
|
3788
|
+
decided_by: z14.enum(["metric", "manual"]),
|
|
3789
|
+
variants: z14.array(VariantAnalytics),
|
|
3582
3790
|
decided_at: Timestamp
|
|
3583
3791
|
});
|
|
3584
|
-
var ContactTaggedData =
|
|
3792
|
+
var ContactTaggedData = z14.object({
|
|
3585
3793
|
contact_id: Id,
|
|
3586
|
-
external_id:
|
|
3794
|
+
external_id: z14.string().nullable(),
|
|
3587
3795
|
email: Email,
|
|
3588
3796
|
/** Only the tags the contact did not already have. */
|
|
3589
|
-
tags:
|
|
3590
|
-
source:
|
|
3797
|
+
tags: z14.array(Slug).min(1),
|
|
3798
|
+
source: z14.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
3591
3799
|
tagged_at: Timestamp
|
|
3592
3800
|
});
|
|
3593
|
-
var ContactUpdatedData =
|
|
3801
|
+
var ContactUpdatedData = z14.object({
|
|
3594
3802
|
contact_id: Id,
|
|
3595
|
-
external_id:
|
|
3803
|
+
external_id: z14.string().nullable(),
|
|
3596
3804
|
email: Email,
|
|
3597
|
-
changed:
|
|
3805
|
+
changed: z14.array(z14.string()).min(1),
|
|
3598
3806
|
properties: Properties,
|
|
3599
|
-
source:
|
|
3807
|
+
source: z14.enum(["api", "dashboard", "signup_form", "import", "automation"]),
|
|
3600
3808
|
updated_at: Timestamp
|
|
3601
3809
|
});
|
|
3602
|
-
var AutomationRunEventBase =
|
|
3810
|
+
var AutomationRunEventBase = z14.object({
|
|
3603
3811
|
automation_id: Id,
|
|
3604
|
-
automation_name:
|
|
3812
|
+
automation_name: z14.string(),
|
|
3605
3813
|
run_id: Id,
|
|
3606
|
-
version:
|
|
3814
|
+
version: z14.number().int().min(1),
|
|
3607
3815
|
contact_id: Id,
|
|
3608
|
-
external_id:
|
|
3816
|
+
external_id: z14.string().nullable(),
|
|
3609
3817
|
email: Email
|
|
3610
3818
|
});
|
|
3611
3819
|
var AutomationRunStartedData = AutomationRunEventBase.extend({
|
|
3612
3820
|
/** What let them in: a trigger kind, or `manual` for an explicit enrolment. */
|
|
3613
|
-
trigger_kind:
|
|
3821
|
+
trigger_kind: z14.string(),
|
|
3614
3822
|
entered_at: Timestamp
|
|
3615
3823
|
});
|
|
3616
3824
|
var AutomationRunCompletedData = AutomationRunEventBase.extend({
|
|
3617
|
-
last_step_id:
|
|
3825
|
+
last_step_id: z14.string().nullable(),
|
|
3618
3826
|
completed_at: Timestamp
|
|
3619
3827
|
});
|
|
3620
3828
|
var AutomationRunExitedData = AutomationRunEventBase.extend({
|
|
3621
3829
|
reason: AutomationEndReason,
|
|
3622
|
-
step_id:
|
|
3830
|
+
step_id: z14.string().nullable(),
|
|
3623
3831
|
exited_at: Timestamp
|
|
3624
3832
|
});
|
|
3625
3833
|
var AutomationRunFailedData = AutomationRunEventBase.extend({
|
|
3626
|
-
reason:
|
|
3627
|
-
step_id:
|
|
3834
|
+
reason: z14.string(),
|
|
3835
|
+
step_id: z14.string().nullable(),
|
|
3628
3836
|
failed_at: Timestamp
|
|
3629
3837
|
});
|
|
3630
3838
|
var AutomationWebhookData = AutomationRunEventBase.extend({
|
|
3631
|
-
step_id:
|
|
3839
|
+
step_id: z14.string(),
|
|
3632
3840
|
/** The step's own label, when it was given one, so a receiver can branch on a name. */
|
|
3633
|
-
step_label:
|
|
3841
|
+
step_label: z14.string().nullable(),
|
|
3634
3842
|
/** Whatever the step carries, verbatim. Null when it carries nothing. */
|
|
3635
3843
|
payload: JsonValue.nullable(),
|
|
3636
3844
|
fired_at: Timestamp
|
|
3637
3845
|
});
|
|
3638
|
-
var TemplateEventBase =
|
|
3846
|
+
var TemplateEventBase = z14.object({
|
|
3639
3847
|
template_id: Id,
|
|
3640
|
-
name:
|
|
3641
|
-
version:
|
|
3642
|
-
source:
|
|
3848
|
+
name: z14.string().min(1).max(200),
|
|
3849
|
+
version: z14.number().int().min(1),
|
|
3850
|
+
source: z14.enum(["api", "dashboard", "mjml_import"])
|
|
3643
3851
|
});
|
|
3644
3852
|
var TemplateCreatedData = TemplateEventBase.extend({ created_at: Timestamp });
|
|
3645
|
-
var TemplateUpdatedData = TemplateEventBase.extend({ archived:
|
|
3853
|
+
var TemplateUpdatedData = TemplateEventBase.extend({ archived: z14.boolean(), updated_at: Timestamp });
|
|
3646
3854
|
var TemplateDeletedData = TemplateEventBase.extend({ deleted_at: Timestamp });
|
|
3647
|
-
var envelope = (type, data) =>
|
|
3855
|
+
var envelope = (type, data) => z14.object({
|
|
3648
3856
|
/** Unique per event: a receiver deduplicates on it (deliveries may repeat). */
|
|
3649
3857
|
id: Id,
|
|
3650
|
-
type:
|
|
3858
|
+
type: z14.literal(type),
|
|
3651
3859
|
created_at: Timestamp,
|
|
3652
3860
|
workspace_id: Id,
|
|
3653
3861
|
data
|
|
3654
3862
|
});
|
|
3655
|
-
var WebhookEvent =
|
|
3863
|
+
var WebhookEvent = z14.discriminatedUnion("type", [
|
|
3656
3864
|
envelope("message.sent", MessageSentData),
|
|
3657
3865
|
envelope("message.failed", MessageFailedData),
|
|
3658
3866
|
envelope("contact.unsubscribed", ContactUnsubscribedData),
|
|
@@ -3676,16 +3884,16 @@ var WebhookEvent = z13.discriminatedUnion("type", [
|
|
|
3676
3884
|
envelope("template.updated", TemplateUpdatedData),
|
|
3677
3885
|
envelope("template.deleted", TemplateDeletedData)
|
|
3678
3886
|
]);
|
|
3679
|
-
var WebhookEndpoint =
|
|
3887
|
+
var WebhookEndpoint = z14.object({
|
|
3680
3888
|
id: Id,
|
|
3681
|
-
url:
|
|
3682
|
-
description:
|
|
3683
|
-
events:
|
|
3684
|
-
enabled:
|
|
3889
|
+
url: z14.string().url(),
|
|
3890
|
+
description: z14.string().max(500).nullable(),
|
|
3891
|
+
events: z14.array(WebhookEventType).min(1),
|
|
3892
|
+
enabled: z14.boolean(),
|
|
3685
3893
|
created_at: Timestamp,
|
|
3686
3894
|
updated_at: Timestamp
|
|
3687
3895
|
});
|
|
3688
|
-
var WebhookUrl =
|
|
3896
|
+
var WebhookUrl = z14.string().url().refine((u) => {
|
|
3689
3897
|
try {
|
|
3690
3898
|
const url = new URL(u);
|
|
3691
3899
|
if (url.protocol === "https:") return true;
|
|
@@ -3694,35 +3902,35 @@ var WebhookUrl = z13.string().url().refine((u) => {
|
|
|
3694
3902
|
return false;
|
|
3695
3903
|
}
|
|
3696
3904
|
}, "must be https (http only for localhost)");
|
|
3697
|
-
var WebhookEndpointCreate =
|
|
3905
|
+
var WebhookEndpointCreate = z14.object({
|
|
3698
3906
|
url: WebhookUrl,
|
|
3699
|
-
description:
|
|
3700
|
-
events:
|
|
3701
|
-
enabled:
|
|
3907
|
+
description: z14.string().max(500).optional(),
|
|
3908
|
+
events: z14.array(WebhookEventType).min(1),
|
|
3909
|
+
enabled: z14.boolean().optional()
|
|
3702
3910
|
});
|
|
3703
|
-
var WebhookEndpointUpdate =
|
|
3911
|
+
var WebhookEndpointUpdate = z14.object({
|
|
3704
3912
|
url: WebhookUrl,
|
|
3705
|
-
description:
|
|
3706
|
-
events:
|
|
3707
|
-
enabled:
|
|
3913
|
+
description: z14.string().max(500).nullable(),
|
|
3914
|
+
events: z14.array(WebhookEventType).min(1),
|
|
3915
|
+
enabled: z14.boolean()
|
|
3708
3916
|
}).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
|
|
3709
|
-
var WebhookEndpointWithSecret =
|
|
3917
|
+
var WebhookEndpointWithSecret = z14.object({
|
|
3710
3918
|
endpoint: WebhookEndpoint,
|
|
3711
|
-
secret:
|
|
3919
|
+
secret: z14.string().min(32)
|
|
3712
3920
|
});
|
|
3713
3921
|
var WEBHOOK_DELIVERY_STATUSES = ["pending", "succeeded", "failed"];
|
|
3714
|
-
var WebhookDeliveryStatus =
|
|
3922
|
+
var WebhookDeliveryStatus = z14.enum(WEBHOOK_DELIVERY_STATUSES);
|
|
3715
3923
|
var WEBHOOK_MAX_ATTEMPTS = 8;
|
|
3716
3924
|
var WEBHOOK_RETRY_DELAYS_SECONDS = [30, 120, 600, 1800, 3600, 7200, 21600];
|
|
3717
|
-
var WebhookDelivery =
|
|
3925
|
+
var WebhookDelivery = z14.object({
|
|
3718
3926
|
id: Id,
|
|
3719
3927
|
endpoint_id: Id,
|
|
3720
3928
|
event_id: Id,
|
|
3721
3929
|
event_type: WebhookEventType,
|
|
3722
3930
|
status: WebhookDeliveryStatus,
|
|
3723
|
-
attempts:
|
|
3724
|
-
last_status_code:
|
|
3725
|
-
last_error:
|
|
3931
|
+
attempts: z14.number().int().min(0),
|
|
3932
|
+
last_status_code: z14.number().int().min(100).max(599).nullable(),
|
|
3933
|
+
last_error: z14.string().nullable(),
|
|
3726
3934
|
next_attempt_at: Timestamp.nullable(),
|
|
3727
3935
|
delivered_at: Timestamp.nullable(),
|
|
3728
3936
|
created_at: Timestamp
|
|
@@ -3731,7 +3939,7 @@ var WebhookDeliveryListQuery = PageQuery.extend({
|
|
|
3731
3939
|
status: WebhookDeliveryStatus.optional(),
|
|
3732
3940
|
event_type: WebhookEventType.optional()
|
|
3733
3941
|
});
|
|
3734
|
-
var WebhookDeliveryParams =
|
|
3942
|
+
var WebhookDeliveryParams = z14.object({ id: Id, delivery_id: Id });
|
|
3735
3943
|
|
|
3736
3944
|
// src/webhook-signing.ts
|
|
3737
3945
|
var WEBHOOK_SIGNATURE_HEADER = "x-mail-signature";
|
|
@@ -3798,79 +4006,79 @@ async function verifyWebhook(input) {
|
|
|
3798
4006
|
}
|
|
3799
4007
|
|
|
3800
4008
|
// src/billing.ts
|
|
3801
|
-
import { z as
|
|
4009
|
+
import { z as z15 } from "zod";
|
|
3802
4010
|
var PLAN_IDS = ["free", "starter", "growth", "design_partner"];
|
|
3803
|
-
var PlanId =
|
|
3804
|
-
var PlanLimits =
|
|
3805
|
-
monthly_messages:
|
|
3806
|
-
contacts:
|
|
3807
|
-
members:
|
|
3808
|
-
providers:
|
|
3809
|
-
webhook_endpoints:
|
|
4011
|
+
var PlanId = z15.enum(PLAN_IDS);
|
|
4012
|
+
var PlanLimits = z15.object({
|
|
4013
|
+
monthly_messages: z15.number().int().min(0).nullable(),
|
|
4014
|
+
contacts: z15.number().int().min(0).nullable(),
|
|
4015
|
+
members: z15.number().int().min(1).nullable(),
|
|
4016
|
+
providers: z15.number().int().min(1).nullable(),
|
|
4017
|
+
webhook_endpoints: z15.number().int().min(0).nullable(),
|
|
3810
4018
|
/** Automations that may be live (active or paused) at once (A1). */
|
|
3811
|
-
active_automations:
|
|
4019
|
+
active_automations: z15.number().int().min(0).nullable(),
|
|
3812
4020
|
/** Steps one automation may hold. Null is the flow's own cap, not unlimited. */
|
|
3813
|
-
automation_steps:
|
|
4021
|
+
automation_steps: z15.number().int().min(1).nullable()
|
|
3814
4022
|
});
|
|
3815
|
-
var PlanFeatures =
|
|
4023
|
+
var PlanFeatures = z15.object({
|
|
3816
4024
|
/** A/B tests of subject and content on a mailing (S4). */
|
|
3817
|
-
ab_testing:
|
|
4025
|
+
ab_testing: z15.boolean(),
|
|
3818
4026
|
/** Open and click tracking, still off until a workspace turns it on (`settings.tracking_enabled`). */
|
|
3819
|
-
tracking:
|
|
4027
|
+
tracking: z15.boolean(),
|
|
3820
4028
|
/** Sending and hosting pages on the workspace's own domain (a later phase). */
|
|
3821
|
-
custom_domains:
|
|
4029
|
+
custom_domains: z15.boolean(),
|
|
3822
4030
|
/** Automations: flows a person walks through step by step (A1). */
|
|
3823
|
-
automations:
|
|
4031
|
+
automations: z15.boolean()
|
|
3824
4032
|
});
|
|
3825
4033
|
var PLAN_FEATURES = ["ab_testing", "tracking", "custom_domains", "automations"];
|
|
3826
|
-
var Plan =
|
|
4034
|
+
var Plan = z15.object({
|
|
3827
4035
|
id: PlanId,
|
|
3828
|
-
name:
|
|
4036
|
+
name: z15.string().min(1),
|
|
3829
4037
|
/** Price in the smallest currency unit per month; null for plans not sold. */
|
|
3830
|
-
monthly_price_cents:
|
|
3831
|
-
currency:
|
|
4038
|
+
monthly_price_cents: z15.number().int().min(0).nullable(),
|
|
4039
|
+
currency: z15.string().length(3),
|
|
3832
4040
|
limits: PlanLimits,
|
|
3833
4041
|
features: PlanFeatures
|
|
3834
4042
|
});
|
|
3835
|
-
var CatalogPlan = Plan.extend({ sellable:
|
|
3836
|
-
var Subscription =
|
|
4043
|
+
var CatalogPlan = Plan.extend({ sellable: z15.boolean() });
|
|
4044
|
+
var Subscription = z15.object({
|
|
3837
4045
|
workspace_id: Id,
|
|
3838
4046
|
plan: PlanId,
|
|
3839
|
-
status:
|
|
4047
|
+
status: z15.enum(["active", "trialing", "past_due", "cancelled"]),
|
|
3840
4048
|
current_period_start: Timestamp,
|
|
3841
4049
|
current_period_end: Timestamp,
|
|
3842
|
-
cancel_at_period_end:
|
|
4050
|
+
cancel_at_period_end: z15.boolean(),
|
|
3843
4051
|
/**
|
|
3844
4052
|
* A design partner outside billing (plan `design_partner`, no limits, no
|
|
3845
4053
|
* checkout). Set only by an operator, never through the API.
|
|
3846
4054
|
*/
|
|
3847
|
-
billing_exempt:
|
|
4055
|
+
billing_exempt: z15.boolean()
|
|
3848
4056
|
});
|
|
3849
4057
|
var USAGE_METRICS = ["messages", "contacts", "members", "providers", "webhook_endpoints", "active_automations"];
|
|
3850
|
-
var UsageMetric =
|
|
4058
|
+
var UsageMetric = z15.enum(USAGE_METRICS);
|
|
3851
4059
|
var USAGE_WARNING_RATIO = 0.8;
|
|
3852
|
-
var UsageWarning =
|
|
4060
|
+
var UsageWarning = z15.object({
|
|
3853
4061
|
metric: UsageMetric,
|
|
3854
|
-
used:
|
|
3855
|
-
limit:
|
|
4062
|
+
used: z15.number().int().min(0),
|
|
4063
|
+
limit: z15.number().int().min(0),
|
|
3856
4064
|
/** `approaching`: at or above USAGE_WARNING_RATIO of the limit; `reached`: at or above the limit. */
|
|
3857
|
-
level:
|
|
4065
|
+
level: z15.enum(["approaching", "reached"])
|
|
3858
4066
|
});
|
|
3859
|
-
var Usage =
|
|
4067
|
+
var Usage = z15.object({
|
|
3860
4068
|
plan: PlanId,
|
|
3861
4069
|
period_start: Timestamp,
|
|
3862
4070
|
period_end: Timestamp,
|
|
3863
|
-
metrics:
|
|
3864
|
-
warnings:
|
|
4071
|
+
metrics: z15.record(UsageMetric, z15.object({ used: z15.number().int().min(0), limit: z15.number().int().min(0).nullable() })),
|
|
4072
|
+
warnings: z15.array(UsageWarning)
|
|
3865
4073
|
});
|
|
3866
|
-
var CheckoutRequest =
|
|
3867
|
-
plan:
|
|
3868
|
-
success_url:
|
|
3869
|
-
cancel_url:
|
|
4074
|
+
var CheckoutRequest = z15.object({
|
|
4075
|
+
plan: z15.enum(["starter", "growth"]),
|
|
4076
|
+
success_url: z15.string().url(),
|
|
4077
|
+
cancel_url: z15.string().url()
|
|
3870
4078
|
});
|
|
3871
|
-
var CheckoutSession =
|
|
3872
|
-
var PortalRequest =
|
|
3873
|
-
var PortalSession =
|
|
4079
|
+
var CheckoutSession = z15.object({ url: z15.string().url() });
|
|
4080
|
+
var PortalRequest = z15.object({ return_url: z15.string().url() });
|
|
4081
|
+
var PortalSession = z15.object({ url: z15.string().url() });
|
|
3874
4082
|
var USAGE_WARNING_HEADER = "x-mail-usage-warning";
|
|
3875
4083
|
function formatUsageWarningHeader(warnings) {
|
|
3876
4084
|
if (warnings.length === 0) return null;
|
|
@@ -3890,7 +4098,7 @@ function parseUsageWarningHeader(value) {
|
|
|
3890
4098
|
}
|
|
3891
4099
|
|
|
3892
4100
|
// src/routes.ts
|
|
3893
|
-
import { z as
|
|
4101
|
+
import { z as z16 } from "zod";
|
|
3894
4102
|
var list = (item) => page(item);
|
|
3895
4103
|
var foundationRoutes = {
|
|
3896
4104
|
"workspaces.create": {
|
|
@@ -4047,6 +4255,25 @@ var templateRoutes = {
|
|
|
4047
4255
|
access: "write",
|
|
4048
4256
|
phase: "S1"
|
|
4049
4257
|
},
|
|
4258
|
+
/**
|
|
4259
|
+
* The sending app declares what the template receives (see
|
|
4260
|
+
* `template-inputs.ts`). Replaces the stored declaration whole; the same
|
|
4261
|
+
* declaration again is a no-op (`changed: false`). Creates no template
|
|
4262
|
+
* version and takes no `base_version`, so it never conflicts with an edit.
|
|
4263
|
+
* An API key only (`send` or `full`): the declaration is the app's statement
|
|
4264
|
+
* about its own code, so a dashboard member is refused with `forbidden`, a
|
|
4265
|
+
* rule the access levels cannot express and the service enforces itself.
|
|
4266
|
+
*/
|
|
4267
|
+
"templates.declareInputs": {
|
|
4268
|
+
method: "PUT",
|
|
4269
|
+
path: "/v1/templates/:id/inputs",
|
|
4270
|
+
params: IdParams,
|
|
4271
|
+
body: TemplateInputsDeclaration,
|
|
4272
|
+
response: TemplateInputsDeclared,
|
|
4273
|
+
status: 200,
|
|
4274
|
+
access: "write",
|
|
4275
|
+
phase: "S1"
|
|
4276
|
+
},
|
|
4050
4277
|
"templates.delete": {
|
|
4051
4278
|
method: "DELETE",
|
|
4052
4279
|
path: "/v1/templates/:id",
|
|
@@ -4098,7 +4325,7 @@ var templateRoutes = {
|
|
|
4098
4325
|
path: "/v1/templates/:id/export",
|
|
4099
4326
|
params: IdParams,
|
|
4100
4327
|
query: TemplateExportQuery,
|
|
4101
|
-
response:
|
|
4328
|
+
response: z16.string(),
|
|
4102
4329
|
responseType: "text",
|
|
4103
4330
|
status: 200,
|
|
4104
4331
|
access: "read",
|
|
@@ -4422,7 +4649,7 @@ var sendingRoutes = {
|
|
|
4422
4649
|
path: "/v1/mailings/:id/export",
|
|
4423
4650
|
params: IdParams,
|
|
4424
4651
|
query: MailingExportQuery,
|
|
4425
|
-
response:
|
|
4652
|
+
response: z16.string(),
|
|
4426
4653
|
responseType: "text",
|
|
4427
4654
|
status: 200,
|
|
4428
4655
|
access: "read",
|
|
@@ -4655,7 +4882,7 @@ var platformRoutes = {
|
|
|
4655
4882
|
"contactProperties.list": {
|
|
4656
4883
|
method: "GET",
|
|
4657
4884
|
path: "/v1/contact-properties",
|
|
4658
|
-
response:
|
|
4885
|
+
response: z16.object({ data: z16.array(ContactPropertyDefinition) }),
|
|
4659
4886
|
status: 200,
|
|
4660
4887
|
access: "read",
|
|
4661
4888
|
phase: "S4"
|
|
@@ -4954,7 +5181,7 @@ var billingRoutes = {
|
|
|
4954
5181
|
"billing.plans": {
|
|
4955
5182
|
method: "GET",
|
|
4956
5183
|
path: "/v1/billing/plans",
|
|
4957
|
-
response:
|
|
5184
|
+
response: z16.object({ data: z16.array(CatalogPlan) }),
|
|
4958
5185
|
status: 200,
|
|
4959
5186
|
access: "read",
|
|
4960
5187
|
phase: "S5"
|
|
@@ -5096,7 +5323,7 @@ var automationRoutes = {
|
|
|
5096
5323
|
method: "POST",
|
|
5097
5324
|
path: "/v1/automations/:id/pause",
|
|
5098
5325
|
params: IdParams,
|
|
5099
|
-
body:
|
|
5326
|
+
body: z16.object({}).strict(),
|
|
5100
5327
|
response: Automation,
|
|
5101
5328
|
status: 200,
|
|
5102
5329
|
access: "write",
|
|
@@ -5106,7 +5333,7 @@ var automationRoutes = {
|
|
|
5106
5333
|
method: "POST",
|
|
5107
5334
|
path: "/v1/automations/:id/resume",
|
|
5108
5335
|
params: IdParams,
|
|
5109
|
-
body:
|
|
5336
|
+
body: z16.object({}).strict(),
|
|
5110
5337
|
response: Automation,
|
|
5111
5338
|
status: 200,
|
|
5112
5339
|
access: "write",
|
|
@@ -5116,7 +5343,7 @@ var automationRoutes = {
|
|
|
5116
5343
|
method: "POST",
|
|
5117
5344
|
path: "/v1/automations/:id/archive",
|
|
5118
5345
|
params: IdParams,
|
|
5119
|
-
body:
|
|
5346
|
+
body: z16.object({}).strict(),
|
|
5120
5347
|
response: Automation,
|
|
5121
5348
|
status: 200,
|
|
5122
5349
|
access: "write",
|
|
@@ -5495,6 +5722,13 @@ export {
|
|
|
5495
5722
|
MAX_STEPS,
|
|
5496
5723
|
MAX_STEP_VISITS,
|
|
5497
5724
|
MAX_SUBJECT_LENGTH,
|
|
5725
|
+
MAX_TEMPLATE_INPUTS,
|
|
5726
|
+
MAX_TEMPLATE_INPUTS_APP_LENGTH,
|
|
5727
|
+
MAX_TEMPLATE_INPUTS_SEEN,
|
|
5728
|
+
MAX_TEMPLATE_INPUT_EXAMPLE_LENGTH,
|
|
5729
|
+
MAX_TEMPLATE_INPUT_LABEL_LENGTH,
|
|
5730
|
+
MAX_TEMPLATE_INPUT_NOTE_LENGTH,
|
|
5731
|
+
MAX_TEMPLATE_INPUT_VALUES,
|
|
5498
5732
|
MAX_TEMPLATE_TRANSLATIONS,
|
|
5499
5733
|
MAX_TRIGGERS,
|
|
5500
5734
|
MEMBER_ROLES,
|
|
@@ -5589,6 +5823,7 @@ export {
|
|
|
5589
5823
|
RecipientStatus,
|
|
5590
5824
|
RemoteImage,
|
|
5591
5825
|
ResendSigningSecret,
|
|
5826
|
+
SERVICE_FILLED_MERGE_FIELDS,
|
|
5592
5827
|
SIGNUP_FORM_FIELDS,
|
|
5593
5828
|
SKIP_REASONS,
|
|
5594
5829
|
SMTP_SECURITY,
|
|
@@ -5618,6 +5853,8 @@ export {
|
|
|
5618
5853
|
SuppressionCreate,
|
|
5619
5854
|
SuppressionListQuery,
|
|
5620
5855
|
SuppressionReason,
|
|
5856
|
+
TEMPLATE_INPUT_KINDS,
|
|
5857
|
+
TEMPLATE_INPUT_PROBLEM_REASONS,
|
|
5621
5858
|
TEMPLATE_LANGUAGE_STATUSES,
|
|
5622
5859
|
TERMINAL_MAILING_STATUSES,
|
|
5623
5860
|
Tag,
|
|
@@ -5634,6 +5871,16 @@ export {
|
|
|
5634
5871
|
TemplateImportPreview,
|
|
5635
5872
|
TemplateImportPreviewRequest,
|
|
5636
5873
|
TemplateImportResult,
|
|
5874
|
+
TemplateInput,
|
|
5875
|
+
TemplateInputMap,
|
|
5876
|
+
TemplateInputName,
|
|
5877
|
+
TemplateInputSeen,
|
|
5878
|
+
TemplateInputSource,
|
|
5879
|
+
TemplateInputValue,
|
|
5880
|
+
TemplateInputs,
|
|
5881
|
+
TemplateInputsDeclaration,
|
|
5882
|
+
TemplateInputsDeclared,
|
|
5883
|
+
TemplateInputsSeen,
|
|
5637
5884
|
TemplateLanguageStatus,
|
|
5638
5885
|
TemplateListQuery,
|
|
5639
5886
|
TemplatePreheader,
|
|
@@ -5787,9 +6034,14 @@ export {
|
|
|
5787
6034
|
renameInsertionChecked,
|
|
5788
6035
|
renameInsertionChoice,
|
|
5789
6036
|
renameInsertionChoiceChecked,
|
|
6037
|
+
renameInsertionEverywhere,
|
|
6038
|
+
renameInsertionPlacements,
|
|
6039
|
+
renameTokenInDocument,
|
|
6040
|
+
renameTokenInText,
|
|
5790
6041
|
resolveInsertions,
|
|
5791
6042
|
restorePatch,
|
|
5792
6043
|
routes,
|
|
6044
|
+
sameJson,
|
|
5793
6045
|
sampleMergeValue,
|
|
5794
6046
|
saveDemotions,
|
|
5795
6047
|
sendingRoutes,
|
|
@@ -5801,6 +6053,8 @@ export {
|
|
|
5801
6053
|
stepTargets,
|
|
5802
6054
|
storedTemplateLine,
|
|
5803
6055
|
stronglyConnected,
|
|
6056
|
+
templateInputDrift,
|
|
6057
|
+
templateInputProblems,
|
|
5804
6058
|
templateRoutes,
|
|
5805
6059
|
testGroupSize,
|
|
5806
6060
|
tightLoops,
|