@marlinjai/mail-contract 0.2.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.mjs ADDED
@@ -0,0 +1,4631 @@
1
+ // src/common.ts
2
+ import { z } from "zod";
3
+ var Id = z.string().min(1).max(64);
4
+ var Timestamp = z.string().datetime({ offset: true });
5
+ var Email = z.string().trim().min(3).max(254).email();
6
+ var Slug = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "lowercase letters, digits and single hyphens");
7
+ var JsonValue = z.lazy(
8
+ () => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(JsonValue)])
9
+ );
10
+ var Properties = z.record(JsonValue);
11
+ var DEFAULT_PAGE_LIMIT = 50;
12
+ var MAX_PAGE_LIMIT = 100;
13
+ var PageQuery = z.object({
14
+ cursor: z.string().min(1).max(512).optional(),
15
+ limit: z.coerce.number().int().min(1).max(MAX_PAGE_LIMIT).optional()
16
+ });
17
+ function page(item) {
18
+ return z.object({
19
+ data: z.array(item),
20
+ next_cursor: z.string().nullable()
21
+ });
22
+ }
23
+ var IdParams = z.object({ id: Id });
24
+ var Ok = z.object({ ok: z.literal(true) });
25
+
26
+ // src/errors.ts
27
+ import { z as z2 } from "zod";
28
+ var ERROR_STATUS = {
29
+ // 400: the request is malformed or fails validation
30
+ invalid_request: 400,
31
+ validation_failed: 400,
32
+ invalid_cursor: 400,
33
+ // 401 and 403: who is calling and whether they may
34
+ unauthenticated: 401,
35
+ invalid_api_key: 401,
36
+ api_key_revoked: 401,
37
+ forbidden: 403,
38
+ insufficient_role: 403,
39
+ // 404
40
+ not_found: 404,
41
+ // 409: the resource is in a state that does not allow the operation
42
+ conflict: 409,
43
+ already_exists: 409,
44
+ mailing_invalid_state: 409,
45
+ idempotency_key_reused: 409,
46
+ last_owner: 409,
47
+ /** The provider's bounce circuit breaker is open: an admin clears it with providers.clearAnomaly. */
48
+ provider_anomaly: 409,
49
+ // 413
50
+ payload_too_large: 413,
51
+ // 422: well-formed, but the content cannot be used
52
+ compile_failed: 422,
53
+ /** MJML that cannot be imported; `details.reason` is one of MJML_IMPORT_REFUSALS, with `line` and `column`. */
54
+ invalid_mjml: 422,
55
+ missing_unsubscribe_url: 422,
56
+ mailing_not_ready: 422,
57
+ unknown_topic: 422,
58
+ unknown_provider: 422,
59
+ /**
60
+ * A1: an automation names a tag, topic, template, segment or signup form the
61
+ * workspace no longer has. `details` carries which.
62
+ */
63
+ unknown_reference: 422,
64
+ /** A1: the automation is not in a state this call can act on (publishing an archived one, enrolling into a draft). */
65
+ automation_invalid_state: 422,
66
+ recipient_suppressed: 422,
67
+ unsupported_media_type: 422,
68
+ /** Opens and clicks are not tracked in this workspace (an A/B metric, an engagement filter). */
69
+ tracking_disabled: 422,
70
+ // 429
71
+ rate_limited: 429,
72
+ daily_budget_exhausted: 429,
73
+ plan_limit_reached: 429,
74
+ // 5xx
75
+ provider_error: 502,
76
+ internal_error: 500,
77
+ service_unavailable: 503
78
+ };
79
+ var ERROR_CODES = Object.keys(ERROR_STATUS);
80
+ var ErrorCodeSchema = z2.enum(ERROR_CODES);
81
+ var ErrorBody = z2.object({
82
+ error: z2.object({
83
+ code: ErrorCodeSchema,
84
+ message: z2.string().min(1),
85
+ details: z2.record(z2.unknown()).optional()
86
+ })
87
+ });
88
+ var ValidationIssue = z2.object({
89
+ path: z2.array(z2.union([z2.string(), z2.number()])),
90
+ message: z2.string()
91
+ });
92
+ function statusForError(code) {
93
+ return ERROR_STATUS[code];
94
+ }
95
+ function errorBody(code, message, details) {
96
+ return details === void 0 ? { error: { code, message } } : { error: { code, message, details } };
97
+ }
98
+ var RETRYABLE_ERRORS = [
99
+ "rate_limited",
100
+ "provider_error",
101
+ "internal_error",
102
+ "service_unavailable"
103
+ ];
104
+ var BILLING_NOT_CONFIGURED_REASON = "billing_not_configured";
105
+ function isRetryableError(code, details) {
106
+ return RETRYABLE_ERRORS.includes(code) && details?.reason !== BILLING_NOT_CONFIGURED_REASON;
107
+ }
108
+
109
+ // src/headers.ts
110
+ var AUTHORIZATION_HEADER = "authorization";
111
+ var BEARER_PREFIX = "Bearer ";
112
+ var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
113
+ var IDEMPOTENCY_KEY_MAX_LENGTH = 255;
114
+ var IDEMPOTENCY_KEY_RETENTION_HOURS = 24;
115
+ var SUBJECT_HEADER = "x-mail-subject";
116
+ var WORKSPACE_HEADER = "x-mail-workspace";
117
+ var REQUEST_ID_HEADER = "x-request-id";
118
+ var RETRY_AFTER_HEADER = "retry-after";
119
+ var API_VERSION_PREFIX = "/v1";
120
+ var HEALTH_PATH = "/healthz";
121
+
122
+ // src/workspace.ts
123
+ import { z as z8 } from "zod";
124
+
125
+ // src/automations.ts
126
+ import { z as z7 } from "zod";
127
+
128
+ // src/templates.ts
129
+ import { z as z3 } from "zod";
130
+ var DOCUMENT_SCHEMA_VERSIONS = ["1.0", "1.1"];
131
+ var DocumentSchemaVersion = z3.enum(DOCUMENT_SCHEMA_VERSIONS);
132
+ var TemplateDocument = z3.object({
133
+ version: DocumentSchemaVersion,
134
+ metadata: z3.record(z3.unknown()),
135
+ sections: z3.array(z3.record(z3.unknown()))
136
+ }).passthrough();
137
+ var MAX_DOCUMENT_BYTES = 1e6;
138
+ var Template = z3.object({
139
+ id: Id,
140
+ name: z3.string().min(1).max(200),
141
+ description: z3.string().max(2e3).nullable(),
142
+ document: TemplateDocument,
143
+ /** Increments on every saved change. */
144
+ version: z3.number().int().min(1),
145
+ thumbnail_url: z3.string().url().nullable(),
146
+ archived_at: Timestamp.nullable(),
147
+ created_at: Timestamp,
148
+ updated_at: Timestamp
149
+ });
150
+ var TemplateSummary = Template.omit({ document: true });
151
+ var TemplateCreate = z3.object({
152
+ name: z3.string().min(1).max(200),
153
+ description: z3.string().max(2e3).optional(),
154
+ document: TemplateDocument
155
+ });
156
+ var TemplateUpdate = z3.object({
157
+ base_version: z3.number().int().min(1),
158
+ name: z3.string().min(1).max(200).optional(),
159
+ description: z3.string().max(2e3).nullable().optional(),
160
+ document: TemplateDocument.optional(),
161
+ archived: z3.boolean().optional()
162
+ }).refine(
163
+ (v) => v.name !== void 0 || v.description !== void 0 || v.document !== void 0 || v.archived !== void 0,
164
+ "at least one field besides base_version"
165
+ );
166
+ var SavedSectionBody = z3.record(z3.unknown());
167
+ var MAX_SAVED_SECTIONS = 50;
168
+ var SavedSection = z3.object({
169
+ id: Id,
170
+ name: z3.string().min(1).max(200),
171
+ description: z3.string().max(2e3).nullable(),
172
+ section: SavedSectionBody,
173
+ created_by: z3.string().nullable(),
174
+ created_at: Timestamp,
175
+ updated_at: Timestamp
176
+ });
177
+ var SavedSectionCreate = z3.object({
178
+ name: z3.string().min(1).max(200),
179
+ description: z3.string().max(2e3).optional(),
180
+ section: SavedSectionBody
181
+ });
182
+ var SavedSectionUpdate = z3.object({
183
+ name: z3.string().min(1).max(200).optional(),
184
+ description: z3.string().max(2e3).nullable().optional(),
185
+ section: SavedSectionBody.optional()
186
+ }).refine((v) => v.name !== void 0 || v.description !== void 0 || v.section !== void 0, "at least one field");
187
+ var TemplateListQuery = PageQuery.extend({
188
+ archived: z3.enum(["true", "false"]).optional()
189
+ });
190
+ var TemplateVersion = z3.object({
191
+ template_id: Id,
192
+ version: z3.number().int().min(1),
193
+ document: TemplateDocument,
194
+ created_by: z3.string().nullable(),
195
+ created_at: Timestamp
196
+ });
197
+ var TemplateVersionParams = z3.object({
198
+ id: Id,
199
+ version: z3.coerce.number().int().min(1)
200
+ });
201
+ var CompileMessage = z3.object({
202
+ message: z3.string().min(1),
203
+ /** Where in the document or the MJML the message points, when known. */
204
+ path: z3.string().optional(),
205
+ line: z3.number().int().min(1).optional()
206
+ });
207
+ var CompileResult = z3.object({
208
+ mjml: z3.string(),
209
+ html: z3.string(),
210
+ warnings: z3.array(CompileMessage),
211
+ errors: z3.array(CompileMessage)
212
+ });
213
+ var TemplateCompileRequest = z3.object({
214
+ version: z3.number().int().min(1).optional()
215
+ });
216
+ var CompileRequest = z3.object({
217
+ document: TemplateDocument
218
+ });
219
+ var ASSET_CONTENT_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
220
+ var AssetContentType = z3.enum(ASSET_CONTENT_TYPES);
221
+ var MAX_ASSET_BYTES = 10 * 1024 * 1024;
222
+ var Asset = z3.object({
223
+ id: Id,
224
+ url: z3.string().url(),
225
+ content_type: AssetContentType,
226
+ size_bytes: z3.number().int().min(1).max(MAX_ASSET_BYTES),
227
+ width: z3.number().int().min(1).nullable(),
228
+ height: z3.number().int().min(1).nullable(),
229
+ filename: z3.string().min(1).max(255),
230
+ created_at: Timestamp
231
+ });
232
+ var ASSET_UPLOAD_FIELD = "file";
233
+ var MAX_IMPORT_URL_LENGTH = 2048;
234
+ var AssetImport = z3.object({
235
+ url: z3.string().max(MAX_IMPORT_URL_LENGTH).url().refine((u) => {
236
+ try {
237
+ const url = new URL(u);
238
+ if (url.protocol === "https:") return true;
239
+ return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
240
+ } catch {
241
+ return false;
242
+ }
243
+ }, "must be https (http only for localhost)"),
244
+ /** The stored file name; derived from the address when omitted. */
245
+ filename: z3.string().min(1).max(255).optional()
246
+ });
247
+ var MAX_MJML_IMPORT_BYTES = 512 * 1024;
248
+ var MAX_MJML_DEPTH = 32;
249
+ var MAX_MJML_ELEMENTS = 5e3;
250
+ var MAX_IMPORTED_REMOTE_IMAGES = 50;
251
+ var MJML_IMPORT_REFUSALS = [
252
+ "invalid_xml",
253
+ "not_mjml",
254
+ "include_not_supported",
255
+ "too_deep",
256
+ "too_many_elements",
257
+ "too_complex",
258
+ "invalid_document"
259
+ ];
260
+ var ImportWarning = z3.object({
261
+ severity: z3.enum(["info", "warning"]),
262
+ /** Stable kind, e.g. `kept_as_html`, `unknown_component`, `remote_image_not_imported`. */
263
+ code: z3.string().min(1).max(64),
264
+ /** Where in the source, e.g. `mj-body > mj-section[2] > mj-column[1] > mj-social[1]`. */
265
+ path: z3.string(),
266
+ line: z3.number().int().min(1).optional(),
267
+ message: z3.string().min(1),
268
+ fragment: z3.string().optional()
269
+ });
270
+ var RemoteImage = z3.object({
271
+ url: z3.string().min(1),
272
+ /** The element and attribute it is loaded from, e.g. `<img src>`. */
273
+ where: z3.string()
274
+ });
275
+ var ImportedAsset = z3.object({
276
+ source_url: z3.string().min(1),
277
+ asset_id: Id,
278
+ url: z3.string().url()
279
+ });
280
+ var mjmlSource = z3.string().min(1, "paste or upload the MJML");
281
+ var TemplateImportPreviewRequest = z3.object({ mjml: mjmlSource });
282
+ var TemplateImportPreview = z3.object({
283
+ document: TemplateDocument,
284
+ warnings: z3.array(ImportWarning),
285
+ /** The document compiled under the workspace's asset policy, exactly as a send would. */
286
+ compiled: CompileResult,
287
+ /** Images loaded from outside the service; under `service_only` each is also a compile error. */
288
+ remote_images: z3.array(RemoteImage),
289
+ asset_policy: z3.enum(["any", "service_only"])
290
+ });
291
+ var TemplateImport = z3.object({
292
+ name: z3.string().min(1).max(200),
293
+ description: z3.string().max(2e3).optional(),
294
+ mjml: mjmlSource,
295
+ import_remote_assets: z3.boolean().optional()
296
+ });
297
+ var TemplateImportResult = z3.object({
298
+ template: Template,
299
+ warnings: z3.array(ImportWarning),
300
+ imported_assets: z3.array(ImportedAsset)
301
+ });
302
+ var EXPORT_FORMATS = ["mjml", "html"];
303
+ var ExportFormat = z3.enum(EXPORT_FORMATS);
304
+ var TemplateExportQuery = z3.object({
305
+ format: ExportFormat,
306
+ version: z3.coerce.number().int().min(1).optional()
307
+ });
308
+ var MailingExportQuery = z3.object({ format: ExportFormat });
309
+ var EXPORT_CONTENT_TYPES = {
310
+ mjml: "text/plain; charset=utf-8",
311
+ html: "text/html; charset=utf-8"
312
+ };
313
+ var EXPORT_WARNINGS_HEADER = "x-mail-export-warnings";
314
+ var EXPORT_WARNING_COUNT_HEADER = "x-mail-export-warning-count";
315
+ var MAX_EXPORT_WARNINGS_HEADER_LENGTH = 6e3;
316
+ function formatExportWarningsHeader(messages) {
317
+ const kept = [];
318
+ for (const m of messages) {
319
+ if (encodeURIComponent(JSON.stringify([...kept, m])).length > MAX_EXPORT_WARNINGS_HEADER_LENGTH) break;
320
+ kept.push(m);
321
+ }
322
+ return encodeURIComponent(JSON.stringify(kept));
323
+ }
324
+ function parseExportWarningsHeader(value) {
325
+ if (!value) return [];
326
+ try {
327
+ const parsed = z3.array(CompileMessage).safeParse(JSON.parse(decodeURIComponent(value)));
328
+ return parsed.success ? parsed.data : [];
329
+ } catch {
330
+ return [];
331
+ }
332
+ }
333
+ function exportFilename(name, format) {
334
+ const stem = name.replace(/ß/g, "ss").normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
335
+ return `${stem || "export"}.${format}`;
336
+ }
337
+
338
+ // src/platform.ts
339
+ import { z as z6 } from "zod";
340
+
341
+ // src/mailings.ts
342
+ import { z as z5 } from "zod";
343
+
344
+ // src/ab-test.ts
345
+ import { z as z4 } from "zod";
346
+ var AbVariant = z4.object({
347
+ key: z4.string().regex(/^[a-z]$/, "a single lowercase letter"),
348
+ subject: z4.string().min(1).max(998).optional(),
349
+ /** Replaces the mailing's document for this variant. */
350
+ document: z4.record(z4.unknown()).optional(),
351
+ /**
352
+ * Keeps the document this variant key already has in the mailing's current
353
+ * test, so a test can be changed without sending every document again. The
354
+ * service refuses it (`validation_failed`) when that variant has none.
355
+ */
356
+ keep_document: z4.literal(true).optional()
357
+ });
358
+ var AB_WINNER_METRICS = ["opens", "clicks", "manual"];
359
+ var AbTestConfig = z4.object({
360
+ variants: z4.array(AbVariant).min(2).max(5),
361
+ /** Share of recipients in the test group, split evenly across variants; the rest get the winner. */
362
+ test_fraction: z4.number().gt(0).lte(1),
363
+ winner_metric: z4.enum(AB_WINNER_METRICS),
364
+ decide_after_minutes: z4.number().int().min(15).max(10080).optional()
365
+ }).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), {
366
+ message: "every variant changes the subject, the document or both",
367
+ path: ["variants"]
368
+ }).refine((c) => c.variants.every((v) => !(v.document !== void 0 && v.keep_document === true)), {
369
+ message: "a variant sends a new document or keeps its current one, not both",
370
+ path: ["variants"]
371
+ }).refine((c) => c.winner_metric === "manual" === (c.decide_after_minutes === void 0), {
372
+ message: "opens and clicks need decide_after_minutes; manual takes none",
373
+ path: ["decide_after_minutes"]
374
+ });
375
+ var AB_TEST_STATUSES = ["pending", "testing", "awaiting_pick", "decided"];
376
+ var AbTestState = z4.object({
377
+ variants: z4.array(z4.object({ key: z4.string(), subject: z4.string().nullable(), has_document: z4.boolean() })),
378
+ test_fraction: z4.number(),
379
+ winner_metric: z4.enum(AB_WINNER_METRICS),
380
+ decide_after_minutes: z4.number().int().nullable(),
381
+ status: z4.enum(AB_TEST_STATUSES),
382
+ /** When the metric decides; null for manual or before the start. */
383
+ decide_at: Timestamp.nullable(),
384
+ winner: z4.string().nullable(),
385
+ decided_by: z4.enum(["metric", "manual"]).nullable(),
386
+ decided_at: Timestamp.nullable()
387
+ });
388
+ var AbWinnerRequest = z4.object({ variant: z4.string().regex(/^[a-z]$/) });
389
+ function testGroupSize(total, fraction, variants) {
390
+ return Math.min(total, Math.max(variants, Math.ceil(total * fraction)));
391
+ }
392
+
393
+ // src/mailings.ts
394
+ var MAILING_STATUSES = [
395
+ "draft",
396
+ "scheduled",
397
+ "sending",
398
+ "paused",
399
+ "sent",
400
+ "partially_failed",
401
+ "cancelled"
402
+ ];
403
+ var MailingStatus = z5.enum(MAILING_STATUSES);
404
+ var MAILING_ACTIONS = ["send", "schedule", "unschedule", "pause", "resume", "cancel", "retry-failed"];
405
+ var MailingAction = z5.enum(MAILING_ACTIONS);
406
+ var MAILING_TRANSITIONS = {
407
+ draft: ["send", "schedule", "cancel"],
408
+ scheduled: ["send", "schedule", "unschedule", "cancel"],
409
+ sending: ["pause", "cancel"],
410
+ paused: ["resume", "cancel"],
411
+ sent: [],
412
+ partially_failed: ["retry-failed"],
413
+ cancelled: []
414
+ };
415
+ function canTransition(status, action) {
416
+ return MAILING_TRANSITIONS[status].includes(action);
417
+ }
418
+ var EDITABLE_MAILING_STATUSES = ["draft", "scheduled"];
419
+ var TERMINAL_MAILING_STATUSES = ["sent", "cancelled"];
420
+ var MailingMetadata = z5.record(z5.string().max(500)).refine((v) => Object.keys(v).length <= 20, "at most 20 keys").refine((v) => Object.keys(v).every((k) => k.length >= 1 && k.length <= 64), "keys are 1 to 64 characters");
421
+ var MailingCounts = z5.object({
422
+ total: z5.number().int().min(0),
423
+ queued: z5.number().int().min(0),
424
+ sending: z5.number().int().min(0),
425
+ sent: z5.number().int().min(0),
426
+ failed: z5.number().int().min(0),
427
+ skipped: z5.number().int().min(0)
428
+ });
429
+ var MAILING_KINDS = ["broadcast", "automation", "notification"];
430
+ var MailingKind = z5.enum(MAILING_KINDS);
431
+ var Mailing = z5.object({
432
+ id: Id,
433
+ kind: MailingKind,
434
+ name: z5.string().max(200).nullable(),
435
+ subject: z5.string().min(1).max(998),
436
+ preheader: z5.string().max(500).nullable(),
437
+ /** The template the document was taken from, if any. The snapshot is what is sent. */
438
+ template_id: Id.nullable(),
439
+ document: TemplateDocument,
440
+ topic: Slug,
441
+ provider_id: Id,
442
+ status: MailingStatus,
443
+ counts: MailingCounts,
444
+ metadata: MailingMetadata,
445
+ scheduled_at: Timestamp.nullable(),
446
+ /** The A/B test on subject or content (S4), null when there is none. */
447
+ ab_test: AbTestState.nullable(),
448
+ started_at: Timestamp.nullable(),
449
+ finished_at: Timestamp.nullable(),
450
+ /**
451
+ * Why the service paused the mailing itself, null when a person paused it or
452
+ * it is not paused. Today only the bounce circuit breaker does: too many
453
+ * recipients refused as dead addresses in one run (see the provider's
454
+ * `rejections.anomaly`). Resuming clears it and starts a new run.
455
+ */
456
+ pause_reason: z5.string().nullable().default(null),
457
+ created_at: Timestamp,
458
+ updated_at: Timestamp
459
+ });
460
+ var MailingSummary = Mailing.omit({ document: true });
461
+ var MailingContent = {
462
+ name: z5.string().max(200).optional(),
463
+ subject: z5.string().min(1).max(998),
464
+ preheader: z5.string().max(500).optional(),
465
+ topic: Slug,
466
+ provider_id: Id,
467
+ metadata: MailingMetadata.optional()
468
+ };
469
+ var MailingCreate = z5.object({
470
+ ...MailingContent,
471
+ document: TemplateDocument.optional(),
472
+ template_id: Id.optional()
473
+ }).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
474
+ message: "exactly one of document or template_id",
475
+ path: ["document"]
476
+ });
477
+ var MailingUpdate = z5.object({
478
+ name: z5.string().max(200).nullable().optional(),
479
+ subject: MailingContent.subject.optional(),
480
+ preheader: z5.string().max(500).nullable().optional(),
481
+ topic: Slug.optional(),
482
+ provider_id: Id.optional(),
483
+ metadata: MailingMetadata.optional(),
484
+ document: TemplateDocument.optional()
485
+ }).refine((v) => Object.keys(v).length > 0, "at least one field");
486
+ var MailingListQuery = PageQuery.extend({
487
+ status: MailingStatus.optional(),
488
+ topic: Slug.optional(),
489
+ kind: MailingKind.optional(),
490
+ automation_id: Id.optional()
491
+ });
492
+ var MailingSendRequest = z5.object({}).strict();
493
+ var MailingRetryFailedRequest = z5.object({
494
+ /**
495
+ * Also requeue recipients whose outcome is unknown after a crash. They may
496
+ * already have received the message, so this is a human's decision, never a
497
+ * default.
498
+ */
499
+ include_outcome_unknown: z5.boolean().optional()
500
+ }).strict();
501
+ var MailingActionRequest = z5.object({}).strict();
502
+ var MailingTestRequest = z5.object({
503
+ to: Email,
504
+ merge: Properties.optional()
505
+ });
506
+ var MailingTestResult = z5.object({
507
+ message_id: Id,
508
+ provider_message_id: z5.string().nullable()
509
+ });
510
+ var RECIPIENT_STATUSES = ["queued", "sending", "sent", "failed", "skipped"];
511
+ var RecipientStatus = z5.enum(RECIPIENT_STATUSES);
512
+ var SKIP_REASONS = ["suppressed", "not_subscribed", "contact_erased", "cancelled", "outcome_unknown"];
513
+ var SkipReason = z5.enum(SKIP_REASONS);
514
+ var Recipient = z5.object({
515
+ id: Id,
516
+ mailing_id: Id,
517
+ contact_id: Id.nullable(),
518
+ email: Email,
519
+ merge: Properties,
520
+ status: RecipientStatus,
521
+ skip_reason: SkipReason.nullable(),
522
+ attempts: z5.number().int().min(0),
523
+ message_id: Id.nullable(),
524
+ last_error: z5.string().nullable(),
525
+ created_at: Timestamp,
526
+ updated_at: Timestamp
527
+ });
528
+ var MAX_RECIPIENTS_PER_BATCH = 1e3;
529
+ var RecipientInput = z5.object({
530
+ contact_id: Id.optional(),
531
+ external_id: z5.string().min(1).max(255).optional(),
532
+ email: Email.optional(),
533
+ merge: Properties.optional()
534
+ }).refine((v) => v.contact_id !== void 0 || v.external_id !== void 0 || v.email !== void 0, {
535
+ message: "contact_id, external_id or email is required",
536
+ path: ["email"]
537
+ });
538
+ var RecipientBatch = z5.object({
539
+ recipients: z5.array(RecipientInput).min(1).max(MAX_RECIPIENTS_PER_BATCH)
540
+ });
541
+ var RECIPIENT_REJECTIONS = ["unknown_contact", "duplicate_in_batch"];
542
+ var RecipientBatchResult = z5.object({
543
+ /** Newly queued. */
544
+ added: z5.number().int().min(0),
545
+ /** Already on the mailing from an earlier batch: left unchanged. */
546
+ already_present: z5.number().int().min(0),
547
+ /** Items not added, by their index in the request. */
548
+ rejected: z5.array(z5.object({ index: z5.number().int().min(0), reason: z5.enum(RECIPIENT_REJECTIONS) }))
549
+ });
550
+ var RecipientListQuery = PageQuery.extend({
551
+ status: RecipientStatus.optional(),
552
+ skip_reason: SkipReason.optional()
553
+ });
554
+ var MESSAGE_OUTCOMES = ["sent", "failed"];
555
+ var MessageOutcome = z5.enum(MESSAGE_OUTCOMES);
556
+ var MessageSummary = z5.object({
557
+ id: Id,
558
+ mailing_id: Id.nullable(),
559
+ recipient_id: Id.nullable(),
560
+ contact_id: Id.nullable(),
561
+ to: Email,
562
+ subject: z5.string(),
563
+ provider_id: Id,
564
+ provider_message_id: z5.string().nullable(),
565
+ outcome: MessageOutcome,
566
+ error: z5.string().nullable(),
567
+ is_test: z5.boolean(),
568
+ recipient_count: z5.number().int().min(1),
569
+ created_at: Timestamp
570
+ });
571
+ var Message = MessageSummary.extend({ html: z5.string() });
572
+ var MessageListQuery = PageQuery.extend({
573
+ mailing_id: Id.optional(),
574
+ outcome: MessageOutcome.optional()
575
+ });
576
+
577
+ // src/platform.ts
578
+ var Tag = z6.object({
579
+ id: Id,
580
+ slug: Slug,
581
+ name: z6.string().min(1).max(120),
582
+ contact_count: z6.number().int().min(0),
583
+ created_at: Timestamp
584
+ });
585
+ var TagCreate = z6.object({ slug: Slug, name: z6.string().min(1).max(120) });
586
+ var TagAssignment = z6.object({
587
+ contact_ids: z6.array(Id).min(1).max(1e3)
588
+ });
589
+ var CONTACT_PROPERTY_TYPES = ["string", "number", "boolean", "date"];
590
+ var ContactPropertyType = z6.enum(CONTACT_PROPERTY_TYPES);
591
+ var ContactPropertyKey = z6.string().regex(/^[A-Za-z0-9_.-]{1,64}$/, "letters, digits, dot, underscore and hyphen, 1 to 64");
592
+ var ContactPropertyDefinition = z6.object({
593
+ key: ContactPropertyKey,
594
+ label: z6.string().min(1).max(120),
595
+ type: ContactPropertyType
596
+ });
597
+ var ContactPropertyParams = z6.object({ key: ContactPropertyKey });
598
+ var FILTER_OPERATORS = [
599
+ "eq",
600
+ "neq",
601
+ "contains",
602
+ "not_contains",
603
+ "starts_with",
604
+ "gt",
605
+ "gte",
606
+ "lt",
607
+ "lte",
608
+ "exists",
609
+ "not_exists",
610
+ "in",
611
+ "not_in"
612
+ ];
613
+ var FilterOperator = z6.enum(FILTER_OPERATORS);
614
+ var FilterField = z6.string().regex(
615
+ /^(email|first_name|last_name|locale|created_at|tag|topic|engagement:(opened|clicked)|property:[A-Za-z0-9_.-]{1,64})$/,
616
+ "unknown filter field"
617
+ );
618
+ var FILTER_FIELD_OPERATORS = {
619
+ email: ["eq", "neq", "contains", "not_contains", "starts_with", "in", "not_in"],
620
+ first_name: ["eq", "neq", "contains", "not_contains", "starts_with", "exists", "not_exists", "in", "not_in"],
621
+ last_name: ["eq", "neq", "contains", "not_contains", "starts_with", "exists", "not_exists", "in", "not_in"],
622
+ locale: ["eq", "neq", "starts_with", "exists", "not_exists", "in", "not_in"],
623
+ created_at: ["gt", "gte", "lt", "lte"],
624
+ tag: ["eq", "neq", "in", "not_in"],
625
+ topic: ["eq", "neq", "in", "not_in"],
626
+ engagement: ["lte", "gt"],
627
+ property: FILTER_OPERATORS
628
+ };
629
+ var MAX_FILTER_DEPTH = 5;
630
+ var FilterCondition = z6.object({ field: FilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
631
+ message: "exists and not_exists take no value; every other operator needs one",
632
+ path: ["value"]
633
+ }).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
634
+ message: "in and not_in take an array",
635
+ path: ["value"]
636
+ });
637
+ var SegmentFilter = z6.lazy(
638
+ () => z6.union([
639
+ z6.object({ and: z6.array(SegmentFilter).min(1).max(50) }).strict(),
640
+ z6.object({ or: z6.array(SegmentFilter).min(1).max(50) }).strict(),
641
+ z6.object({ not: SegmentFilter }).strict(),
642
+ FilterCondition
643
+ ])
644
+ );
645
+ function filterDepth(filter) {
646
+ if ("and" in filter) return 1 + Math.max(...filter.and.map(filterDepth));
647
+ if ("or" in filter) return 1 + Math.max(...filter.or.map(filterDepth));
648
+ if ("not" in filter) return 1 + filterDepth(filter.not);
649
+ return 1;
650
+ }
651
+ var BoundedSegmentFilter = SegmentFilter.refine((f) => filterDepth(f) <= MAX_FILTER_DEPTH, {
652
+ message: `nested deeper than ${MAX_FILTER_DEPTH} levels`
653
+ });
654
+ var MAX_IN_LIST = 500;
655
+ var MAX_ENGAGEMENT_DAYS = 3650;
656
+ function filterFieldFamily(field) {
657
+ const colon = field.indexOf(":");
658
+ return colon === -1 ? field : field.slice(0, colon);
659
+ }
660
+ var isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
661
+ var isFilterString = (v) => typeof v === "string";
662
+ var isFilterNumber = (v) => typeof v === "number" && Number.isFinite(v);
663
+ var DATE_VALUE_PREFIX = /^\d{4}-\d{2}-\d{2}/;
664
+ function filterLeafProblem(c, types, operators = FILTER_FIELD_OPERATORS) {
665
+ const fam = filterFieldFamily(c.field);
666
+ const allowed = operators[fam];
667
+ if (!allowed || !allowed.includes(c.op)) {
668
+ return { at: "op", message: `${c.field} does not take ${c.op}; it takes ${(allowed ?? []).join(", ")}` };
669
+ }
670
+ if (c.op === "exists" || c.op === "not_exists") return null;
671
+ const list2 = c.op === "in" || c.op === "not_in";
672
+ const values = list2 ? c.value : [c.value];
673
+ if (list2 && (values.length === 0 || values.length > MAX_IN_LIST)) {
674
+ return { at: "value", message: `in and not_in take 1 to ${MAX_IN_LIST} values` };
675
+ }
676
+ const every = (ok, what) => values.every(ok) ? null : { at: "value", message: `${c.field} ${c.op} needs ${what}` };
677
+ switch (fam) {
678
+ case "email":
679
+ case "first_name":
680
+ case "last_name":
681
+ case "locale":
682
+ return every((v) => isFilterString(v) && v.length >= 1 && v.length <= 254, "strings of 1 to 254 characters");
683
+ case "created_at":
684
+ return every((v) => isFilterString(v) && Timestamp.safeParse(v).success, "an ISO 8601 timestamp with an offset");
685
+ case "tag":
686
+ case "topic":
687
+ return every((v) => Slug.safeParse(v).success, "slugs");
688
+ case "segment":
689
+ return every((v) => Id.safeParse(v).success, "segment ids");
690
+ case "engagement":
691
+ return every(
692
+ (v) => Number.isInteger(v) && v >= 1 && v <= MAX_ENGAGEMENT_DAYS,
693
+ `a whole number of days from 1 to ${MAX_ENGAGEMENT_DAYS}`
694
+ );
695
+ // An event's data is untyped, like a property with no definition.
696
+ case "event":
697
+ return untypedValueProblem(c, every);
698
+ case "property": {
699
+ const type = types.get(c.field.slice("property:".length));
700
+ const textOp = c.op === "contains" || c.op === "not_contains" || c.op === "starts_with";
701
+ const orderOp = c.op === "gt" || c.op === "gte" || c.op === "lt" || c.op === "lte";
702
+ switch (type) {
703
+ case "number":
704
+ if (textOp) return { at: "op", message: `${c.field} is a number; ${c.op} compares text` };
705
+ return every(isFilterNumber, "numbers");
706
+ case "boolean":
707
+ if (textOp || orderOp || list2) return { at: "op", message: `${c.field} is a boolean; it takes eq, neq, exists and not_exists` };
708
+ return every((v) => typeof v === "boolean", "true or false");
709
+ case "date":
710
+ if (textOp) return { at: "op", message: `${c.field} is a date; ${c.op} compares text` };
711
+ return every((v) => isFilterString(v) && DATE_VALUE_PREFIX.test(v) && !Number.isNaN(Date.parse(v.slice(0, 10))), "ISO 8601 dates");
712
+ case "string":
713
+ if (orderOp) return { at: "op", message: `${c.field} is a string; it cannot be ordered` };
714
+ return every((v) => isFilterString(v) && v.length <= 1e3, "strings of up to 1000 characters");
715
+ case void 0:
716
+ return untypedValueProblem(c, every);
717
+ }
718
+ }
719
+ }
720
+ return { at: "op", message: `unknown field ${c.field}` };
721
+ }
722
+ function untypedValueProblem(c, every) {
723
+ const textOp = c.op === "contains" || c.op === "not_contains" || c.op === "starts_with";
724
+ const orderOp = c.op === "gt" || c.op === "gte" || c.op === "lt" || c.op === "lte";
725
+ if (textOp) return every((v) => isFilterString(v) && v.length <= 1e3, "strings of up to 1000 characters");
726
+ if (orderOp) return every(isFilterNumber, "numbers (define the property to compare dates)");
727
+ return every((v) => isScalar(v) || v === null, "strings, numbers, booleans or null");
728
+ }
729
+ var Segment = z6.object({
730
+ id: Id,
731
+ name: z6.string().min(1).max(120),
732
+ filter: SegmentFilter,
733
+ /** Computed on read. */
734
+ contact_count: z6.number().int().min(0),
735
+ created_at: Timestamp,
736
+ updated_at: Timestamp
737
+ });
738
+ var SegmentCreate = z6.object({ name: z6.string().min(1).max(120), filter: BoundedSegmentFilter });
739
+ var SegmentPreviewRequest = z6.object({ filter: BoundedSegmentFilter });
740
+ var SegmentPreview = z6.object({
741
+ contact_count: z6.number().int().min(0),
742
+ /** Up to five matching contacts, for a human to sanity-check the filter. */
743
+ sample: z6.array(z6.object({ id: Id, email: Email })).max(5)
744
+ });
745
+ var MailingAudienceFromSegment = z6.object({ segment_id: Id });
746
+ var SIGNUP_FORM_FIELDS = ["first_name", "last_name"];
747
+ var HOSTED_PAGE_LOCALES = ["en", "de", "it", "fr", "es"];
748
+ var SignupFormTranslation = z6.object({
749
+ title: z6.string().min(1).max(120),
750
+ consent_text: z6.string().min(1).max(2e3)
751
+ });
752
+ var SignupForm = z6.object({
753
+ id: Id,
754
+ name: z6.string().min(1).max(120),
755
+ /** The heading of the hosted page. */
756
+ title: z6.string().min(1).max(120),
757
+ /** What the person agrees to, shown above the button and recorded with the confirmation. */
758
+ consent_text: z6.string().min(1).max(2e3),
759
+ translations: z6.record(SignupFormTranslation),
760
+ topics: z6.array(Slug).min(1),
761
+ tags: z6.array(Slug),
762
+ /** Fields shown besides email. */
763
+ fields: z6.array(z6.enum(SIGNUP_FORM_FIELDS)),
764
+ double_opt_in: z6.literal(true),
765
+ /** The provider the confirmation mail is sent through. */
766
+ provider_id: Id,
767
+ /**
768
+ * A saved template for the confirmation mail; it must contain
769
+ * `{{confirm_url}}`. Null sends the built-in confirmation mail, localised.
770
+ */
771
+ confirmation_template_id: Id.nullable(),
772
+ /** Where the person lands after confirming. Null shows the hosted confirmation page. */
773
+ redirect_url: z6.string().url().nullable(),
774
+ /** Origins allowed to embed the form and call the submission route from a browser. */
775
+ allowed_origins: z6.array(z6.string().url()).max(20),
776
+ /** Bumped on every change; recorded with each confirmation. */
777
+ version: z6.number().int().min(1),
778
+ created_at: Timestamp,
779
+ updated_at: Timestamp
780
+ });
781
+ var SignupFormCreate = SignupForm.omit({
782
+ id: true,
783
+ created_at: true,
784
+ updated_at: true,
785
+ double_opt_in: true,
786
+ version: true
787
+ }).extend({
788
+ translations: SignupForm.shape.translations.optional(),
789
+ tags: SignupForm.shape.tags.optional(),
790
+ fields: SignupForm.shape.fields.optional(),
791
+ confirmation_template_id: Id.nullable().optional(),
792
+ redirect_url: z6.string().url().nullable().optional(),
793
+ allowed_origins: SignupForm.shape.allowed_origins.optional()
794
+ });
795
+ var SignupFormEmbed = z6.object({
796
+ hosted_url: z6.string().url(),
797
+ /** A plain HTML form posting to the hosted page; works without JavaScript. */
798
+ html: z6.string(),
799
+ /** A small optional script that adds the time check and submits in place. */
800
+ script_url: z6.string().url(),
801
+ /** Where a browser fetches a fresh `form_token` from (GET, public, CORS for `allowed_origins`). */
802
+ token_url: z6.string().url()
803
+ });
804
+ var SignupSubmission = z6.object({
805
+ email: Email,
806
+ first_name: z6.string().max(200).optional(),
807
+ last_name: z6.string().max(200).optional(),
808
+ locale: z6.string().min(2).max(35).optional(),
809
+ /** Honeypot: must be empty. */
810
+ website: z6.string().max(0).optional(),
811
+ /**
812
+ * The signed render time from the form's token URL: a submission sooner than a
813
+ * human could fill the form, or long after, is refused.
814
+ */
815
+ form_token: z6.string().min(1).max(512)
816
+ });
817
+ var IMPORT_STATUSES = ["uploaded", "validating", "validated", "committing", "completed", "failed", "cancelled"];
818
+ var ImportStatus = z6.enum(IMPORT_STATUSES);
819
+ var IMPORT_TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
820
+ var ImportColumnMapping = z6.record(
821
+ z6.string().regex(/^(email|external_id|first_name|last_name|locale|property:[A-Za-z0-9_.-]{1,64}|ignore)$/)
822
+ );
823
+ var IMPORT_FILE_FIELD = "file";
824
+ var MAX_IMPORT_BYTES = 50 * 1024 * 1024;
825
+ var MAX_IMPORT_ROWS = 2e5;
826
+ var ImportMappingRequest = z6.object({
827
+ /** CSV header -> contact field. Exactly one column maps to `email`. */
828
+ mapping: ImportColumnMapping.refine(
829
+ (m) => Object.values(m).filter((v) => v === "email").length === 1,
830
+ "exactly one column maps to email"
831
+ ),
832
+ topics: z6.array(Slug).max(100),
833
+ tags: z6.array(Slug).max(100),
834
+ /** Existing contacts get the mapped fields overwritten; false only adds topics, tags and missing fields. */
835
+ update_existing: z6.boolean().optional(),
836
+ /** The importer states the people consented; the service records who said so and when. */
837
+ consent_confirmed: z6.literal(true)
838
+ });
839
+ var ImportCommitRequest = z6.object({ mapping_version: z6.number().int().min(1) });
840
+ var IMPORT_ROW_OUTCOMES = ["created", "updated", "unchanged", "suppressed", "skipped"];
841
+ var ImportRowOutcome = z6.enum(IMPORT_ROW_OUTCOMES);
842
+ var IMPORT_SKIP_REASONS = [
843
+ "missing_email",
844
+ "invalid_email",
845
+ "duplicate_in_file",
846
+ "invalid_value",
847
+ "external_id_conflict",
848
+ "wrong_column_count"
849
+ ];
850
+ var ImportSkipReason = z6.enum(IMPORT_SKIP_REASONS);
851
+ var ImportReport = z6.object({
852
+ created: z6.number().int().min(0),
853
+ updated: z6.number().int().min(0),
854
+ unchanged: z6.number().int().min(0),
855
+ suppressed: z6.number().int().min(0),
856
+ skipped: z6.number().int().min(0),
857
+ skipped_by_reason: z6.record(ImportSkipReason, z6.number().int().min(0)),
858
+ /** Rows whose contact was subscribed without some topic, because the address is blocked for it. */
859
+ topics_withheld: z6.number().int().min(0)
860
+ });
861
+ var ImportJob = z6.object({
862
+ id: Id,
863
+ status: ImportStatus,
864
+ file_name: z6.string().max(255).nullable(),
865
+ file_bytes: z6.number().int().min(0),
866
+ total_rows: z6.number().int().min(0),
867
+ /** The header row, as in the file. */
868
+ columns: z6.array(z6.string()),
869
+ /** The first rows, for the mapping screen. */
870
+ sample: z6.array(z6.array(z6.string())).max(5),
871
+ /** A mapping guessed from the headers, for the mapping screen to start from. */
872
+ suggested_mapping: ImportColumnMapping,
873
+ mapping: ImportColumnMapping.nullable(),
874
+ mapping_version: z6.number().int().min(0),
875
+ topics: z6.array(Slug),
876
+ tags: z6.array(Slug),
877
+ update_existing: z6.boolean(),
878
+ /** The dry run of the current mapping; null before one finished. */
879
+ dry_run: ImportReport.nullable(),
880
+ /** What the commit has written so far; null before it started. */
881
+ result: ImportReport.nullable(),
882
+ /** Rows the current phase (dry run or commit) has processed. */
883
+ processed_rows: z6.number().int().min(0),
884
+ /** Why a `failed` import failed. */
885
+ error: z6.string().nullable(),
886
+ created_at: Timestamp,
887
+ updated_at: Timestamp,
888
+ finished_at: Timestamp.nullable()
889
+ });
890
+ var ImportRow = z6.object({
891
+ /** The line in the file, 1 being the first line after the header. */
892
+ row: z6.number().int().min(1),
893
+ email: z6.string().nullable(),
894
+ outcome: ImportRowOutcome,
895
+ reason: ImportSkipReason.nullable(),
896
+ /** A human explanation for a skipped row (which value, which column). */
897
+ message: z6.string().nullable(),
898
+ contact_id: Id.nullable()
899
+ });
900
+ var ImportRowListQuery = PageQuery.extend({ outcome: ImportRowOutcome.optional() });
901
+ var MailingScheduleRequest = z6.object({ send_at: Timestamp });
902
+ var TrackingSettings = z6.object({ opens: z6.boolean(), clicks: z6.boolean() });
903
+ var VariantAnalytics = z6.object({
904
+ key: z6.string(),
905
+ sent: z6.number().int().min(0),
906
+ unique_opens: z6.number().int().min(0).nullable(),
907
+ unique_clicks: z6.number().int().min(0).nullable()
908
+ });
909
+ var MailingAnalytics = z6.object({
910
+ mailing_id: Id,
911
+ counts: MailingCounts,
912
+ /** What was tracked for this mailing, fixed when it started; null before it started. */
913
+ tracking: TrackingSettings.nullable(),
914
+ /** Unique recipients with a human open. Null when opens were not tracked. */
915
+ unique_opens: z6.number().int().min(0).nullable(),
916
+ /**
917
+ * Recipients whose only opens came from Apple Mail Privacy Protection, which
918
+ * loads every image on delivery: counted apart, never as human opens.
919
+ */
920
+ apple_mpp_opens: z6.number().int().min(0).nullable(),
921
+ /** Opens and clicks from security scanners and prefetchers, filtered out of the unique counts. */
922
+ machine_events: z6.number().int().min(0).nullable(),
923
+ unique_clicks: z6.number().int().min(0).nullable(),
924
+ unsubscribes: z6.number().int().min(0),
925
+ bounces: z6.number().int().min(0),
926
+ complaints: z6.number().int().min(0),
927
+ links: z6.array(z6.object({ url: z6.string(), unique_clicks: z6.number().int().min(0) })).nullable(),
928
+ variants: z6.array(VariantAnalytics).nullable()
929
+ });
930
+
931
+ // src/automations.ts
932
+ 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})$/;
933
+ var AutomationFilterField = z7.string().regex(AUTOMATION_FILTER_FIELD_PATTERN, "unknown filter field");
934
+ var AUTOMATION_FILTER_FIELD_OPERATORS = {
935
+ ...FILTER_FIELD_OPERATORS,
936
+ segment: ["eq", "neq", "in", "not_in"],
937
+ event: FILTER_FIELD_OPERATORS.property,
938
+ // Did they, or did they not. No `in`: naming several steps at once would be
939
+ // an "any of these" that the filter's own `or` already says more plainly.
940
+ step: ["eq", "neq"]
941
+ };
942
+ var automationFilterFamily = filterFieldFamily;
943
+ var AutomationFilterCondition = z7.object({ field: AutomationFilterField, op: FilterOperator, value: JsonValue.optional() }).strict().refine((c) => (c.op === "exists" || c.op === "not_exists") === (c.value === void 0), {
944
+ message: "exists and not_exists take no value; every other operator needs one",
945
+ path: ["value"]
946
+ }).refine((c) => c.op !== "in" && c.op !== "not_in" || Array.isArray(c.value), {
947
+ message: "in and not_in take an array",
948
+ path: ["value"]
949
+ });
950
+ var AutomationFilter = z7.lazy(
951
+ () => z7.union([
952
+ z7.object({ and: z7.array(AutomationFilter).min(1).max(50) }).strict(),
953
+ z7.object({ or: z7.array(AutomationFilter).min(1).max(50) }).strict(),
954
+ z7.object({ not: AutomationFilter }).strict(),
955
+ AutomationFilterCondition
956
+ ])
957
+ );
958
+ var BoundedAutomationFilter = AutomationFilter.refine((f) => filterDepth(f) <= MAX_FILTER_DEPTH, {
959
+ message: `nested deeper than ${MAX_FILTER_DEPTH} levels`
960
+ });
961
+ function filterConditions(filter) {
962
+ if ("and" in filter) return filter.and.flatMap(filterConditions);
963
+ if ("or" in filter) return filter.or.flatMap(filterConditions);
964
+ if ("not" in filter) return filterConditions(filter.not);
965
+ return [filter];
966
+ }
967
+ var StepId = z7.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "letters, digits, underscore and hyphen, 1 to 64");
968
+ var TimeOfDay = z7.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "HH:MM in 24 hours");
969
+ var Weekday = z7.number().int().min(0).max(6);
970
+ var TimezoneName = z7.string().min(1).max(64).regex(/^(UTC|[A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+)+)$/, "an IANA time zone name such as Europe/Berlin");
971
+ var MAX_DELAY_SECONDS = 365 * 24 * 60 * 60;
972
+ var DateOnly = z7.string().regex(/^\d{4}-\d{2}-\d{2}$/, "a calendar date such as 2026-09-20").refine((v) => {
973
+ const [y, m, d] = v.split("-").map(Number);
974
+ const t = new Date(Date.UTC(y, m - 1, d));
975
+ return t.getUTCFullYear() === y && t.getUTCMonth() + 1 === m && t.getUTCDate() === d;
976
+ }, "a real calendar date");
977
+ var OffsetDays = z7.number().int().min(-365).max(365);
978
+ var DATE_PASSED_ANSWERS = ["skip", "stop", "continue"];
979
+ var DatePassedAnswer = z7.enum(DATE_PASSED_ANSWERS);
980
+ var EventName = z7.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/, "letters, digits, dot, colon, underscore and hyphen, 1 to 80");
981
+ var AUTOMATION_TRIGGER_KINDS = [
982
+ "topic_subscribed",
983
+ "form_confirmed",
984
+ "tag_added",
985
+ "property_changed",
986
+ "event",
987
+ "manual",
988
+ "date_anniversary",
989
+ "exact_date",
990
+ "segment_entered",
991
+ "link_clicked"
992
+ ];
993
+ var AutomationTriggerKind = z7.enum(AUTOMATION_TRIGGER_KINDS);
994
+ var exclude = { exclude: BoundedAutomationFilter.nullish() };
995
+ var AutomationTrigger = z7.discriminatedUnion("kind", [
996
+ z7.object({ kind: z7.literal("topic_subscribed"), topic: Slug, ...exclude }).strict(),
997
+ z7.object({ kind: z7.literal("form_confirmed"), form_id: Id, ...exclude }).strict(),
998
+ z7.object({ kind: z7.literal("tag_added"), tag: Slug, ...exclude }).strict(),
999
+ /** `to` given: only a change that lands on this value fires. Absent: any change of the key. */
1000
+ z7.object({ kind: z7.literal("property_changed"), key: ContactPropertyKey, to: JsonValue.optional(), ...exclude }).strict(),
1001
+ /** `where` filters the event's own data, so only `event:<key>` leaves belong in it. */
1002
+ z7.object({ kind: z7.literal("event"), name: EventName, where: BoundedAutomationFilter.nullish(), ...exclude }).strict(),
1003
+ z7.object({ kind: z7.literal("manual"), ...exclude }).strict(),
1004
+ /**
1005
+ * Every year on a date the contact carries: a birthday, a joining date, a
1006
+ * renewal. `offset_days` moves it ("three days before"), `at` is the wall
1007
+ * clock in the contact's own time zone. The property must be defined and of
1008
+ * type `date`, which the validator checks.
1009
+ *
1010
+ * A 29 February date fires on 1 March in a year that has no 29 February, so
1011
+ * nobody is skipped every fourth year.
1012
+ */
1013
+ z7.object({
1014
+ kind: z7.literal("date_anniversary"),
1015
+ key: ContactPropertyKey,
1016
+ offset_days: OffsetDays,
1017
+ at: TimeOfDay,
1018
+ ...exclude
1019
+ }).strict(),
1020
+ /**
1021
+ * Once, on one calendar day, for everyone `where` matches. `where` is read as
1022
+ * a scan over the workspace's contacts rather than inside a run, so it may
1023
+ * not ask about a segment or about an event's data: there is no event, and a
1024
+ * segment is its own trigger kind.
1025
+ */
1026
+ z7.object({
1027
+ kind: z7.literal("exact_date"),
1028
+ on: DateOnly,
1029
+ at: TimeOfDay,
1030
+ where: BoundedAutomationFilter.nullish(),
1031
+ ...exclude
1032
+ }).strict(),
1033
+ /**
1034
+ * S4. When somebody starts matching a segment.
1035
+ *
1036
+ * The one trigger with no event behind it: a segment is evaluated when it is
1037
+ * read, so nothing happens at the moment a contact begins to match one. A
1038
+ * sweep keeps a membership snapshot per trigger and enters the difference, so
1039
+ * "entered" means "was not a member when we last looked, and is now".
1040
+ *
1041
+ * That snapshot is seeded when the automation is published, which is what
1042
+ * keeps publishing "when somebody joins VIPs" from mailing every VIP who
1043
+ * already exists. Entering those people as well is the publish dialog's
1044
+ * backfill question, answered by a person rather than by the sweep.
1045
+ */
1046
+ z7.object({ kind: z7.literal("segment_entered"), segment_id: Id, ...exclude }).strict(),
1047
+ /**
1048
+ * S4. When somebody clicks a link in a mailing that has already gone out.
1049
+ *
1050
+ * `url` narrows it to one link, matched exactly as the mailing stored it, and
1051
+ * left out means any link in that mailing. It needs the workspace to record
1052
+ * clicks, because it rides on the very rows that setting writes: with click
1053
+ * tracking off there is no click to hear about, so the validator refuses it
1054
+ * rather than leaving an automation that can never fire.
1055
+ */
1056
+ z7.object({ kind: z7.literal("link_clicked"), mailing_id: Id, url: z7.string().min(1).max(2048).optional(), ...exclude }).strict()
1057
+ ]);
1058
+ var AUTOMATION_DATE_TRIGGER_KINDS = ["date_anniversary", "exact_date"];
1059
+ var MAX_TRIGGERS = 10;
1060
+ var AUTOMATION_STEP_KINDS = [
1061
+ "email",
1062
+ "delay",
1063
+ "condition",
1064
+ "add_tag",
1065
+ "remove_tag",
1066
+ "set_property",
1067
+ "subscribe_topic",
1068
+ "unsubscribe",
1069
+ "exit",
1070
+ "split",
1071
+ "goto",
1072
+ "webhook",
1073
+ "notify"
1074
+ ];
1075
+ var AutomationStepKind = z7.enum(AUTOMATION_STEP_KINDS);
1076
+ var next = { next: StepId.nullable() };
1077
+ var Unchosen = z7.literal("");
1078
+ var DraftSlug = z7.union([Slug, Unchosen]);
1079
+ var DraftPropertyKey = z7.union([ContactPropertyKey, Unchosen]);
1080
+ var label = { label: z7.string().max(120).nullish() };
1081
+ var EmailStep = z7.object({
1082
+ kind: z7.literal("email"),
1083
+ ...label,
1084
+ ...next,
1085
+ template_id: Id.nullish(),
1086
+ document: TemplateDocument.nullish(),
1087
+ /** Empty while the step is being written; `not_chosen` refuses to publish it. */
1088
+ subject: z7.string().max(998),
1089
+ preheader: z7.string().max(500).nullish()
1090
+ }).strict();
1091
+ var DelayStep = z7.object({
1092
+ kind: z7.literal("delay"),
1093
+ ...label,
1094
+ ...next,
1095
+ wait: z7.discriminatedUnion("mode", [
1096
+ z7.object({ mode: z7.literal("duration"), seconds: z7.number().int().min(1).max(MAX_DELAY_SECONDS) }).strict(),
1097
+ z7.object({ mode: z7.literal("time_of_day"), at: TimeOfDay }).strict(),
1098
+ z7.object({ mode: z7.literal("weekday"), weekday: Weekday, at: TimeOfDay }).strict(),
1099
+ /** A fixed day has no next occurrence, so `continue` is not one of its answers. */
1100
+ z7.object({ mode: z7.literal("date"), on: DateOnly, at: TimeOfDay, passed: z7.enum(["skip", "stop"]) }).strict(),
1101
+ z7.object({
1102
+ mode: z7.literal("date_property"),
1103
+ key: ContactPropertyKey,
1104
+ offset_days: OffsetDays,
1105
+ at: TimeOfDay,
1106
+ passed: DatePassedAnswer
1107
+ }).strict()
1108
+ ])
1109
+ }).strict();
1110
+ var ConditionStep = z7.object({
1111
+ kind: z7.literal("condition"),
1112
+ ...label,
1113
+ filter: BoundedAutomationFilter,
1114
+ yes: StepId.nullable(),
1115
+ no: StepId.nullable(),
1116
+ /**
1117
+ * How long to hold the run while the filter is false, and which branch it
1118
+ * takes when that is up. One object rather than two loose fields, because
1119
+ * neither half means anything without the other: a window with no answer
1120
+ * for the end of it would be a silent decision.
1121
+ */
1122
+ wait_for: z7.object({ up_to_s: z7.number().int().min(1).max(MAX_DELAY_SECONDS), on_timeout: z7.enum(["yes", "no"]) }).strict().nullish()
1123
+ }).strict();
1124
+ var SplitBranchKey = z7.string().regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, underscore and hyphen, 1 to 32");
1125
+ var MIN_SPLIT_BRANCHES = 2;
1126
+ var MAX_SPLIT_BRANCHES = 4;
1127
+ var SplitStep = z7.object({
1128
+ kind: z7.literal("split"),
1129
+ ...label,
1130
+ branches: z7.array(
1131
+ z7.object({
1132
+ key: SplitBranchKey,
1133
+ /** A whole percentage. Zero is allowed: a branch kept in the flow but switched off. */
1134
+ weight: z7.number().int().min(0).max(100),
1135
+ next: StepId.nullable()
1136
+ }).strict()
1137
+ ).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")
1138
+ }).strict();
1139
+ var GotoStep = z7.object({ kind: z7.literal("goto"), ...label, target: StepId.nullable() }).strict();
1140
+ var MAX_STEP_VISITS = 20;
1141
+ var WebhookStep = z7.object({ kind: z7.literal("webhook"), ...label, ...next, endpoint_id: Id.nullable(), payload: JsonValue.nullish() }).strict();
1142
+ var NotifyRecipients = z7.union([z7.literal("owner"), z7.literal("admins"), z7.object({ member_id: Id }).strict()]);
1143
+ var NotifyStep = z7.object({
1144
+ kind: z7.literal("notify"),
1145
+ ...label,
1146
+ ...next,
1147
+ to: NotifyRecipients,
1148
+ /** Empty while the step is being written; `not_chosen` refuses to publish it. */
1149
+ subject: z7.string().max(998),
1150
+ body: z7.string().max(1e4)
1151
+ }).strict();
1152
+ var AutomationStep = z7.discriminatedUnion("kind", [
1153
+ EmailStep,
1154
+ DelayStep,
1155
+ ConditionStep,
1156
+ z7.object({ kind: z7.literal("add_tag"), ...label, ...next, tag: DraftSlug }).strict(),
1157
+ z7.object({ kind: z7.literal("remove_tag"), ...label, ...next, tag: DraftSlug }).strict(),
1158
+ /** `value` null clears the property, as a contact upsert does. */
1159
+ z7.object({ kind: z7.literal("set_property"), ...label, ...next, key: DraftPropertyKey, value: JsonValue }).strict(),
1160
+ /** Subscribing never lifts a suppression: the service checks that, not the flow. */
1161
+ z7.object({ kind: z7.literal("subscribe_topic"), ...label, ...next, topic: DraftSlug }).strict(),
1162
+ /** `topic` unsubscribes from the automation's own topic, `all` from every topic. */
1163
+ z7.object({ kind: z7.literal("unsubscribe"), ...label, ...next, scope: z7.enum(["topic", "all"]) }).strict(),
1164
+ z7.object({ kind: z7.literal("exit"), ...label }).strict(),
1165
+ SplitStep,
1166
+ GotoStep,
1167
+ WebhookStep,
1168
+ NotifyStep
1169
+ ]);
1170
+ function stepTargets(step) {
1171
+ switch (step.kind) {
1172
+ case "exit":
1173
+ return [];
1174
+ case "condition":
1175
+ return [
1176
+ { to: step.yes, branch: "yes" },
1177
+ { to: step.no, branch: "no" }
1178
+ ];
1179
+ case "split":
1180
+ return step.branches.map((b) => ({ to: b.next, branch: b.key }));
1181
+ case "goto":
1182
+ return [{ to: step.target, jump: true }];
1183
+ default:
1184
+ return [{ to: step.next }];
1185
+ }
1186
+ }
1187
+ var MAX_STEPS = 200;
1188
+ var AutomationFlow = z7.object({
1189
+ triggers: z7.array(AutomationTrigger).max(MAX_TRIGGERS),
1190
+ /** The first step. `null` is an empty flow. */
1191
+ entry: StepId.nullable(),
1192
+ steps: z7.record(StepId, AutomationStep)
1193
+ }).strict();
1194
+ var emptyFlow = () => ({ triggers: [], entry: null, steps: {} });
1195
+ var QuietHours = z7.object({
1196
+ start: TimeOfDay,
1197
+ end: TimeOfDay,
1198
+ weekdays: z7.array(Weekday).min(1).max(7).nullish()
1199
+ }).strict();
1200
+ var RE_ENTRY_MODES = ["off", "after_exit"];
1201
+ var ReEntryMode = z7.enum(RE_ENTRY_MODES);
1202
+ var MAX_RE_ENTRY_COOLDOWN_SECONDS = 365 * 24 * 60 * 60;
1203
+ var AutomationSettings = z7.object({
1204
+ re_entry: ReEntryMode,
1205
+ /** Seconds a person must wait after leaving before entering again. Only with `after_exit`. */
1206
+ re_entry_cooldown_s: z7.number().int().min(0).max(MAX_RE_ENTRY_COOLDOWN_SECONDS).nullish(),
1207
+ quiet_hours: QuietHours.nullish(),
1208
+ /** Checked before every step; when it matches, the run ends. */
1209
+ exit_filter: BoundedAutomationFilter.nullish()
1210
+ }).strict();
1211
+ var defaultSettings = () => ({
1212
+ re_entry: "off",
1213
+ re_entry_cooldown_s: null,
1214
+ quiet_hours: null,
1215
+ exit_filter: null
1216
+ });
1217
+ var AUTOMATION_ISSUE_CODES = [
1218
+ /** No trigger at all: nothing would ever enter. */
1219
+ "missing_trigger",
1220
+ /** Two triggers that would fire on exactly the same thing. */
1221
+ "duplicate_trigger",
1222
+ /** No entry step, or an entry step that is not in `steps`. */
1223
+ "empty_flow",
1224
+ /** A `next`, `yes` or `no` naming a step that does not exist. */
1225
+ "dangling_next",
1226
+ /** A step no run can reach from the entry. */
1227
+ "unreachable_step",
1228
+ /**
1229
+ * A loop with nothing in it that waits. Loops themselves are legal once a
1230
+ * flow has a `goto`; a loop that can go round without ever waiting is not,
1231
+ * because it would spend a worker's whole tick budget on one person.
1232
+ */
1233
+ "tight_loop",
1234
+ /** An email step with neither a template nor a document. */
1235
+ "empty_email",
1236
+ /**
1237
+ * A step still missing something it needs before it can run: a subject, a
1238
+ * tag, a topic, a property. A draft is allowed to hold one; publishing is not.
1239
+ */
1240
+ "not_chosen",
1241
+ /** An engagement field while the workspace has tracking off. */
1242
+ "tracking_disabled",
1243
+ /** A template, tag, topic, segment, form or property the workspace does not have. */
1244
+ "unknown_reference",
1245
+ /** More steps than the plan or the flow allows. */
1246
+ "too_many_steps",
1247
+ /** A filter nested deeper than the service evaluates. */
1248
+ "filter_too_deep",
1249
+ /** A field that does not belong where it is used (`event:` outside a run, `segment` in an event filter). */
1250
+ "unsupported_field",
1251
+ /** An operator the field does not take. */
1252
+ "unsupported_operator",
1253
+ /** A value the field's type cannot hold: a tag compared to a number, a date that is not one. */
1254
+ "invalid_value",
1255
+ /** A cool-down with re-entry off, which would never apply. */
1256
+ "cooldown_without_reentry",
1257
+ /** A window that is always or never open. */
1258
+ "invalid_quiet_hours",
1259
+ /** A date trigger or a date delay reading a property that is not defined as a date. */
1260
+ "not_a_date_property",
1261
+ /** A one-off date that is already over everywhere on earth, so it would fire for nobody. */
1262
+ "date_in_the_past"
1263
+ ];
1264
+ var AutomationIssueCode = z7.enum(AUTOMATION_ISSUE_CODES);
1265
+ var AutomationIssue = z7.object({
1266
+ code: AutomationIssueCode,
1267
+ /** The step the issue is about, when it is about one. */
1268
+ step_id: StepId.nullable(),
1269
+ /** Where inside the document, for the canvas to point at a field. */
1270
+ path: z7.array(z7.union([z7.string(), z7.number()])),
1271
+ message: z7.string()
1272
+ });
1273
+ var AutomationValidation = z7.object({
1274
+ ok: z7.boolean(),
1275
+ issues: z7.array(AutomationIssue)
1276
+ });
1277
+ function validateAutomation(flow, settings, ctx) {
1278
+ const issues = [];
1279
+ const add = (i) => issues.push(i);
1280
+ const known = ctx.known ?? {};
1281
+ const emailSteps = new Set(
1282
+ Object.entries(flow.steps).filter(([, step]) => step.kind === "email").map(([id]) => id)
1283
+ );
1284
+ if (settings.re_entry === "off" && settings.re_entry_cooldown_s != null && settings.re_entry_cooldown_s > 0) {
1285
+ add({
1286
+ code: "cooldown_without_reentry",
1287
+ path: ["settings", "re_entry_cooldown_s"],
1288
+ message: "A cool-down only applies when re-entry is on."
1289
+ });
1290
+ }
1291
+ if (settings.quiet_hours && settings.quiet_hours.start === settings.quiet_hours.end) {
1292
+ add({
1293
+ code: "invalid_quiet_hours",
1294
+ path: ["settings", "quiet_hours"],
1295
+ message: "A window that starts and ends at the same time is either always or never open."
1296
+ });
1297
+ }
1298
+ if (settings.exit_filter) checkFilter(settings.exit_filter, "contact", ["settings", "exit_filter"], null, ctx, known, emailSteps, add);
1299
+ if (flow.triggers.length === 0) {
1300
+ add({ code: "missing_trigger", path: ["triggers"], message: "An automation needs at least one trigger." });
1301
+ }
1302
+ const seen = /* @__PURE__ */ new Set();
1303
+ flow.triggers.forEach((trigger, i) => {
1304
+ const key = triggerKey(trigger);
1305
+ if (seen.has(key)) {
1306
+ add({ code: "duplicate_trigger", path: ["triggers", i], message: "Another trigger already fires on exactly this." });
1307
+ }
1308
+ seen.add(key);
1309
+ if (trigger.kind === "topic_subscribed") checkRef(known.topics, trigger.topic, "topic", ["triggers", i, "topic"], add);
1310
+ if (trigger.kind === "tag_added") checkRef(known.tags, trigger.tag, "tag", ["triggers", i, "tag"], add);
1311
+ if (trigger.kind === "form_confirmed") checkRef(known.forms, trigger.form_id, "signup form", ["triggers", i, "form_id"], add);
1312
+ if (trigger.kind === "event" && trigger.where) {
1313
+ checkFilter(trigger.where, "event", ["triggers", i, "where"], null, ctx, known, emailSteps, add);
1314
+ }
1315
+ if (trigger.kind === "date_anniversary") {
1316
+ checkDateProperty(ctx, trigger.key, ["triggers", i, "key"], null, add);
1317
+ }
1318
+ if (trigger.kind === "exact_date") {
1319
+ if (trigger.where) checkFilter(trigger.where, "scan", ["triggers", i, "where"], null, ctx, known, emailSteps, add);
1320
+ if (ctx.now && isDayEntirelyPast(trigger.on, ctx.now)) {
1321
+ add({
1322
+ code: "date_in_the_past",
1323
+ path: ["triggers", i, "on"],
1324
+ message: `${trigger.on} is already over everywhere, so this trigger would never fire. Pick a day still to come.`
1325
+ });
1326
+ }
1327
+ }
1328
+ if (trigger.kind === "segment_entered") {
1329
+ checkRef(known.segments, trigger.segment_id, "segment", ["triggers", i, "segment_id"], add);
1330
+ }
1331
+ if (trigger.kind === "link_clicked") {
1332
+ checkRef(known.mailings, trigger.mailing_id, "sent mailing", ["triggers", i, "mailing_id"], add);
1333
+ if (!ctx.tracking.clicks) {
1334
+ add({
1335
+ code: "tracking_disabled",
1336
+ path: ["triggers", i],
1337
+ message: "This workspace does not record clicks, so nothing would ever tell this automation somebody clicked."
1338
+ });
1339
+ }
1340
+ }
1341
+ if (trigger.exclude) checkFilter(trigger.exclude, "entry", ["triggers", i, "exclude"], null, ctx, known, emailSteps, add);
1342
+ });
1343
+ const ids = Object.keys(flow.steps);
1344
+ const cap = ctx.max_steps == null ? MAX_STEPS : Math.min(ctx.max_steps, MAX_STEPS);
1345
+ if (ids.length > cap) {
1346
+ add({ code: "too_many_steps", path: ["steps"], message: `This automation may have at most ${cap} steps; it has ${ids.length}.` });
1347
+ }
1348
+ if (flow.entry === null) {
1349
+ add({ code: "empty_flow", path: ["entry"], message: "An automation needs a first step." });
1350
+ } else if (!flow.steps[flow.entry]) {
1351
+ add({ code: "empty_flow", path: ["entry"], message: `The first step "${flow.entry}" is not in the flow.` });
1352
+ }
1353
+ const order = reachable(flow);
1354
+ for (const id of ids) {
1355
+ if (!order.includes(id)) {
1356
+ add({ code: "unreachable_step", step_id: id, path: ["steps", id], message: "No run can reach this step." });
1357
+ }
1358
+ }
1359
+ for (const id of tightLoops(flow)) {
1360
+ add({
1361
+ code: "tight_loop",
1362
+ step_id: id,
1363
+ path: ["steps", id],
1364
+ message: "This step is in a loop that never waits, so a run would go round it without stopping. Put a wait in the loop."
1365
+ });
1366
+ }
1367
+ for (const id of [...order, ...ids.filter((i) => !order.includes(i))]) {
1368
+ const step = flow.steps[id];
1369
+ if (!step) continue;
1370
+ const path = ["steps", id];
1371
+ for (const { to, branch, jump } of stepTargets(step)) {
1372
+ if (to === null && jump) {
1373
+ add({
1374
+ code: "dangling_next",
1375
+ step_id: id,
1376
+ path: [...path, "target"],
1377
+ message: "This step has nowhere to move the run to. Choose the step it should go to."
1378
+ });
1379
+ continue;
1380
+ }
1381
+ if (to !== null && !flow.steps[to]) {
1382
+ add({
1383
+ code: "dangling_next",
1384
+ step_id: id,
1385
+ path: [...path, jump ? "target" : branch ?? "next"],
1386
+ message: `This step points at "${to}", which is not in the flow.`
1387
+ });
1388
+ }
1389
+ }
1390
+ switch (step.kind) {
1391
+ case "email":
1392
+ if (!step.template_id && !step.document) {
1393
+ add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
1394
+ }
1395
+ if (step.subject.trim() === "") {
1396
+ add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
1397
+ }
1398
+ if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
1399
+ break;
1400
+ case "delay":
1401
+ if (step.wait.mode === "date_property") checkDateProperty(ctx, step.wait.key, [...path, "wait", "key"], id, add);
1402
+ break;
1403
+ case "condition":
1404
+ checkFilter(step.filter, "contact", [...path, "filter"], id, ctx, known, emailSteps, add);
1405
+ break;
1406
+ case "add_tag":
1407
+ case "remove_tag":
1408
+ if (!chosen(step.tag, "tag", [...path, "tag"], id, add)) checkRef(known.tags, step.tag, "tag", [...path, "tag"], add, id);
1409
+ break;
1410
+ case "subscribe_topic":
1411
+ if (!chosen(step.topic, "topic", [...path, "topic"], id, add)) checkRef(known.topics, step.topic, "topic", [...path, "topic"], add, id);
1412
+ break;
1413
+ case "set_property":
1414
+ chosen(step.key, "property", [...path, "key"], id, add);
1415
+ break;
1416
+ case "webhook":
1417
+ if (step.endpoint_id === null) {
1418
+ add({ code: "not_chosen", step_id: id, path: [...path, "endpoint_id"], message: "This step needs a webhook endpoint. Choose one." });
1419
+ } else {
1420
+ checkRef(known.webhook_endpoints, step.endpoint_id, "webhook endpoint", [...path, "endpoint_id"], add, id);
1421
+ }
1422
+ break;
1423
+ case "notify":
1424
+ if (step.subject.trim() === "") {
1425
+ add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This notification has no subject line." });
1426
+ }
1427
+ if (step.body.trim() === "") {
1428
+ add({ code: "not_chosen", step_id: id, path: [...path, "body"], message: "This notification has nothing to say. Write the note." });
1429
+ }
1430
+ if (typeof step.to === "object") checkRef(known.members, step.to.member_id, "member", [...path, "to"], add, id);
1431
+ break;
1432
+ default:
1433
+ break;
1434
+ }
1435
+ }
1436
+ return issues.map((i) => ({ code: i.code, step_id: i.step_id ?? null, path: i.path, message: i.message }));
1437
+ }
1438
+ function isPublishable(flow, settings, ctx) {
1439
+ return validateAutomation(flow, settings, ctx).length === 0;
1440
+ }
1441
+ function isDayEntirelyPast(day, now) {
1442
+ const [y, m, d] = day.split("-").map(Number);
1443
+ return now.getTime() > Date.UTC(y, m - 1, d) + 36 * 60 * 60 * 1e3;
1444
+ }
1445
+ function checkDateProperty(ctx, key, path, stepId, add) {
1446
+ if (!ctx.property_types) return;
1447
+ const type = ctx.property_types.get(key);
1448
+ if (type === "date") return;
1449
+ add({
1450
+ code: "not_a_date_property",
1451
+ step_id: stepId,
1452
+ path,
1453
+ message: type === void 0 ? `This workspace has no property "${key}". A date needs one defined as a date.` : `"${key}" is a ${type}, and a date is needed here. Change the property's type or pick another one.`
1454
+ });
1455
+ }
1456
+ function triggerKey(trigger) {
1457
+ switch (trigger.kind) {
1458
+ case "topic_subscribed":
1459
+ return `topic_subscribed:${trigger.topic}`;
1460
+ case "form_confirmed":
1461
+ return `form_confirmed:${trigger.form_id}`;
1462
+ case "tag_added":
1463
+ return `tag_added:${trigger.tag}`;
1464
+ case "property_changed":
1465
+ return `property_changed:${trigger.key}:${JSON.stringify(trigger.to ?? null)}`;
1466
+ case "event":
1467
+ return `event:${trigger.name}`;
1468
+ case "manual":
1469
+ return "manual";
1470
+ case "date_anniversary":
1471
+ return `date_anniversary:${trigger.key}:${trigger.offset_days}:${trigger.at}`;
1472
+ case "exact_date":
1473
+ return `exact_date:${trigger.on}:${trigger.at}:${JSON.stringify(trigger.where ?? null)}`;
1474
+ case "segment_entered":
1475
+ return `segment_entered:${trigger.segment_id}`;
1476
+ // The url belongs in the identity. Two triggers on the same mailing and
1477
+ // different links are two different things to wait for, and the click path
1478
+ // finds them by mailing alone, so this is what tells them apart afterwards.
1479
+ case "link_clicked":
1480
+ return `link_clicked:${trigger.mailing_id}:${trigger.url ?? ""}`;
1481
+ }
1482
+ }
1483
+ function chosen(value, what, path, stepId, add) {
1484
+ if (value !== "") return false;
1485
+ add({ code: "not_chosen", step_id: stepId, path, message: `This step needs a ${what}. Choose one.` });
1486
+ return true;
1487
+ }
1488
+ function checkRef(known, value, what, path, add, stepId) {
1489
+ if (!known || known.has(value)) return;
1490
+ add({ code: "unknown_reference", step_id: stepId ?? null, path, message: `This workspace has no ${what} "${value}".` });
1491
+ }
1492
+ function checkFilter(filter, place, path, stepId, ctx, known, emailSteps, add) {
1493
+ if (filterDepth(filter) > MAX_FILTER_DEPTH) {
1494
+ add({ code: "filter_too_deep", step_id: stepId, path, message: `This filter is nested deeper than ${MAX_FILTER_DEPTH} levels.` });
1495
+ }
1496
+ for (const leaf of filterConditions(filter)) {
1497
+ const family = automationFilterFamily(leaf.field);
1498
+ if (place === "event" && family !== "event") {
1499
+ add({ code: "unsupported_field", step_id: stepId, path, message: `An event filter compares the event's own data; "${leaf.field}" is not part of it.` });
1500
+ continue;
1501
+ }
1502
+ if (place === "scan" && (family === "event" || family === "segment")) {
1503
+ add({
1504
+ code: "unsupported_field",
1505
+ step_id: stepId,
1506
+ path,
1507
+ message: family === "event" ? `Nothing happened here to compare against, so "${leaf.field}" cannot be used. A date trigger fires from the calendar.` : "This is read as a sweep over your contacts, which cannot ask about a segment. Compare the properties, tags or topics the segment is built from instead."
1508
+ });
1509
+ continue;
1510
+ }
1511
+ if (family === "step" && place !== "contact") {
1512
+ add({
1513
+ code: "unsupported_field",
1514
+ step_id: stepId,
1515
+ path,
1516
+ message: place === "entry" ? `"${leaf.field}" asks about a mail this automation sent, and nobody has been through it yet at this point. Ask it in a condition instead.` : `"${leaf.field}" asks about a mail this automation sent, which only exists inside a run.`
1517
+ });
1518
+ continue;
1519
+ }
1520
+ if (!AUTOMATION_FILTER_FIELD_OPERATORS[family]) {
1521
+ add({ code: "unsupported_field", step_id: stepId, path, message: `Unknown field "${leaf.field}".` });
1522
+ continue;
1523
+ }
1524
+ const problem = filterLeafProblem(leaf, ctx.property_types ?? /* @__PURE__ */ new Map(), AUTOMATION_FILTER_FIELD_OPERATORS);
1525
+ if (problem) {
1526
+ add({
1527
+ code: problem.at === "op" ? "unsupported_operator" : "invalid_value",
1528
+ step_id: stepId,
1529
+ path,
1530
+ message: `${problem.message}.`
1531
+ });
1532
+ }
1533
+ if (family === "engagement" || family === "step") {
1534
+ const asks = leaf.field.endsWith(":clicked") ? "clicks" : "opens";
1535
+ if (!ctx.tracking[asks]) {
1536
+ add({
1537
+ code: "tracking_disabled",
1538
+ step_id: stepId,
1539
+ path,
1540
+ message: `This workspace does not record ${asks}, so a filter cannot ask about them.`
1541
+ });
1542
+ }
1543
+ }
1544
+ if (family === "step" && typeof leaf.value === "string") {
1545
+ if (leaf.value === stepId) {
1546
+ add({ code: "invalid_value", step_id: stepId, path, message: "A step cannot ask whether its own mail was opened." });
1547
+ } else if (!emailSteps.has(leaf.value)) {
1548
+ add({
1549
+ code: "unknown_reference",
1550
+ step_id: stepId,
1551
+ path,
1552
+ message: "This names a step that is not an email in this automation."
1553
+ });
1554
+ }
1555
+ }
1556
+ if (family === "segment" && typeof leaf.value === "string") {
1557
+ checkRef(known.segments, leaf.value, "segment", path, add, stepId ?? void 0);
1558
+ }
1559
+ if (family === "tag" && typeof leaf.value === "string") checkRef(known.tags, leaf.value, "tag", path, add, stepId ?? void 0);
1560
+ if (family === "topic" && typeof leaf.value === "string") checkRef(known.topics, leaf.value, "topic", path, add, stepId ?? void 0);
1561
+ }
1562
+ }
1563
+ function reachable(flow) {
1564
+ const out = [];
1565
+ if (flow.entry === null || !flow.steps[flow.entry]) return out;
1566
+ const queue = [flow.entry];
1567
+ const seen = new Set(queue);
1568
+ while (queue.length > 0) {
1569
+ const id = queue.shift();
1570
+ out.push(id);
1571
+ const step = flow.steps[id];
1572
+ if (!step) continue;
1573
+ for (const { to } of stepTargets(step)) {
1574
+ if (to === null || seen.has(to) || !flow.steps[to]) continue;
1575
+ seen.add(to);
1576
+ queue.push(to);
1577
+ }
1578
+ }
1579
+ return out;
1580
+ }
1581
+ function alwaysWaits(step) {
1582
+ if (step.kind === "email") return true;
1583
+ if (step.kind === "condition") return step.wait_for != null;
1584
+ if (step.kind !== "delay") return false;
1585
+ return step.wait.mode === "duration" || step.wait.mode === "time_of_day" || step.wait.mode === "weekday";
1586
+ }
1587
+ function tightLoops(flow) {
1588
+ const out = [];
1589
+ for (const component of stronglyConnected(flow)) {
1590
+ const loops = component.length > 1 || stepTargets(flow.steps[component[0]]).some((t) => t.to === component[0]);
1591
+ if (!loops) continue;
1592
+ if (component.some((id) => alwaysWaits(flow.steps[id]))) continue;
1593
+ out.push(...component);
1594
+ }
1595
+ return out.sort();
1596
+ }
1597
+ function stronglyConnected(flow) {
1598
+ const index = /* @__PURE__ */ new Map();
1599
+ const low = /* @__PURE__ */ new Map();
1600
+ const onStack = /* @__PURE__ */ new Set();
1601
+ const stack = [];
1602
+ const out = [];
1603
+ let next2 = 0;
1604
+ for (const root of Object.keys(flow.steps)) {
1605
+ if (index.has(root)) continue;
1606
+ const work = [{ id: root, edge: 0 }];
1607
+ index.set(root, next2);
1608
+ low.set(root, next2);
1609
+ next2 += 1;
1610
+ stack.push(root);
1611
+ onStack.add(root);
1612
+ while (work.length > 0) {
1613
+ const frame = work[work.length - 1];
1614
+ const targets = stepTargets(flow.steps[frame.id]).filter((t) => t.to !== null && flow.steps[t.to]);
1615
+ if (frame.edge < targets.length) {
1616
+ const to = targets[frame.edge].to;
1617
+ frame.edge += 1;
1618
+ if (!index.has(to)) {
1619
+ index.set(to, next2);
1620
+ low.set(to, next2);
1621
+ next2 += 1;
1622
+ stack.push(to);
1623
+ onStack.add(to);
1624
+ work.push({ id: to, edge: 0 });
1625
+ } else if (onStack.has(to)) {
1626
+ low.set(frame.id, Math.min(low.get(frame.id), index.get(to)));
1627
+ }
1628
+ continue;
1629
+ }
1630
+ work.pop();
1631
+ const parent = work[work.length - 1];
1632
+ if (parent) low.set(parent.id, Math.min(low.get(parent.id), low.get(frame.id)));
1633
+ if (low.get(frame.id) === index.get(frame.id)) {
1634
+ const component = [];
1635
+ for (; ; ) {
1636
+ const id = stack.pop();
1637
+ onStack.delete(id);
1638
+ component.push(id);
1639
+ if (id === frame.id) break;
1640
+ }
1641
+ out.push(component);
1642
+ }
1643
+ }
1644
+ }
1645
+ return out;
1646
+ }
1647
+ function cycles(flow) {
1648
+ const ids = Object.keys(flow.steps);
1649
+ const out = [];
1650
+ for (const id of ids) {
1651
+ const seen = /* @__PURE__ */ new Set();
1652
+ const queue = stepTargets(flow.steps[id]).map((t) => t.to);
1653
+ while (queue.length > 0) {
1654
+ const to = queue.shift();
1655
+ if (to === null || seen.has(to)) continue;
1656
+ seen.add(to);
1657
+ const step = flow.steps[to];
1658
+ if (!step) continue;
1659
+ for (const t of stepTargets(step)) queue.push(t.to);
1660
+ }
1661
+ if (seen.has(id)) out.push(id);
1662
+ }
1663
+ return out.sort();
1664
+ }
1665
+ var AUTOMATION_STATUSES = ["draft", "active", "paused", "archived"];
1666
+ var AutomationStatus = z7.enum(AUTOMATION_STATUSES);
1667
+ var AUTOMATION_END_REASONS = [
1668
+ "completed",
1669
+ "exit_filter",
1670
+ "exit_step",
1671
+ "unsubscribed",
1672
+ /** The send worker found the person not subscribed to the automation's topic. */
1673
+ "not_subscribed",
1674
+ "suppressed",
1675
+ "erased",
1676
+ "step_removed",
1677
+ /** A date delay's date was already behind the run, and its answer was to stop. */
1678
+ "date_passed",
1679
+ /** The run reached one step more times than `MAX_STEP_VISITS` allows: a loop that never let go. */
1680
+ "loop_guard",
1681
+ "automation_archived",
1682
+ "ended_by_hand",
1683
+ "failed"
1684
+ ];
1685
+ var AutomationEndReason = z7.enum(AUTOMATION_END_REASONS);
1686
+ var AUTOMATION_WAIT_REASONS = ["delay", "quiet_hours", "plan_limit", "sending", "condition"];
1687
+ var AutomationWaitReason = z7.enum(AUTOMATION_WAIT_REASONS);
1688
+ var AUTOMATION_ENTRY_REFUSALS = [
1689
+ "already_inside",
1690
+ "re_entry_off",
1691
+ "in_cooldown",
1692
+ "excluded",
1693
+ "not_subscribed",
1694
+ "suppressed",
1695
+ "not_active",
1696
+ "unknown_contact"
1697
+ ];
1698
+ var AutomationEntryRefusal = z7.enum(AUTOMATION_ENTRY_REFUSALS);
1699
+ var AutomationCounts = z7.object({
1700
+ started: z7.number().int().min(0),
1701
+ in_progress: z7.number().int().min(0),
1702
+ completed: z7.number().int().min(0),
1703
+ exited: z7.number().int().min(0),
1704
+ failed: z7.number().int().min(0)
1705
+ });
1706
+ var Automation = z7.object({
1707
+ id: Id,
1708
+ name: z7.string().min(1).max(200),
1709
+ status: AutomationStatus,
1710
+ /** Every automation sends under one topic; there are no transactional automations. */
1711
+ topic: Slug,
1712
+ provider_id: Id.nullable(),
1713
+ settings: AutomationSettings,
1714
+ /** The flow people are running through, null while the automation has never been published. */
1715
+ published_version: z7.number().int().min(1).nullable(),
1716
+ /** The flow being edited. Increments on every save, like a template's version. */
1717
+ draft_version: z7.number().int().min(1),
1718
+ counts: AutomationCounts,
1719
+ created_at: Timestamp,
1720
+ updated_at: Timestamp,
1721
+ published_at: Timestamp.nullable()
1722
+ });
1723
+ var AutomationWithFlow = Automation.extend({
1724
+ /** The draft, always present: a new automation starts with an empty one. */
1725
+ flow: AutomationFlow,
1726
+ /** The flow people are running through, null until the first publish. */
1727
+ published_flow: AutomationFlow.nullable()
1728
+ });
1729
+ var AutomationSummary = Automation;
1730
+ var AutomationCreate = z7.object({
1731
+ name: z7.string().min(1).max(200),
1732
+ topic: Slug,
1733
+ provider_id: Id.optional(),
1734
+ flow: AutomationFlow.optional(),
1735
+ settings: AutomationSettings.partial().optional()
1736
+ });
1737
+ var AutomationUpdate = z7.object({
1738
+ base_version: z7.number().int().min(1),
1739
+ name: z7.string().min(1).max(200).optional(),
1740
+ topic: Slug.optional(),
1741
+ provider_id: Id.optional(),
1742
+ flow: AutomationFlow.optional(),
1743
+ settings: AutomationSettings.partial().optional()
1744
+ }).refine(
1745
+ (v) => v.name !== void 0 || v.topic !== void 0 || v.provider_id !== void 0 || v.flow !== void 0 || v.settings !== void 0,
1746
+ "at least one field besides base_version"
1747
+ );
1748
+ var AutomationListQuery = PageQuery.extend({
1749
+ status: AutomationStatus.optional(),
1750
+ topic: Slug.optional()
1751
+ });
1752
+ var AutomationPublishRequest = z7.object({
1753
+ /**
1754
+ * Answer what this publish would do and change nothing, whatever the flow
1755
+ * looks like. Without it a publish that has no issues and moves nobody
1756
+ * simply goes ahead, which is right for an API caller and wrong for a
1757
+ * dialog that is only showing somebody what they are about to do.
1758
+ */
1759
+ dry_run: z7.boolean().optional(),
1760
+ confirm: z7.boolean().optional(),
1761
+ /** Per removed step: the step its waiting runs move to, or null to end them. */
1762
+ moves: z7.record(StepId, StepId.nullable()).optional(),
1763
+ /** Also enter everyone who matches a trigger's condition right now. */
1764
+ backfill: z7.boolean().optional()
1765
+ }).strict();
1766
+ var AutomationPublishMove = z7.object({
1767
+ step_id: StepId,
1768
+ /** Why the step's people have to move: it is gone, or its delay changed. */
1769
+ change: z7.enum(["removed", "delay_changed", "kind_changed"]),
1770
+ waiting: z7.number().int().min(0),
1771
+ /** Where they go: a step id, or null to end their runs with `step_removed`. */
1772
+ destination: StepId.nullable()
1773
+ });
1774
+ var AutomationPublishResult = z7.object({
1775
+ published: z7.boolean(),
1776
+ automation: Automation,
1777
+ /** Empty when nothing has to move; non-empty and `published` false asks for a confirmation. */
1778
+ moves: z7.array(AutomationPublishMove),
1779
+ /** How many people the backfill entered, or would enter when it is not confirmed. */
1780
+ backfill: z7.number().int().min(0),
1781
+ issues: z7.array(AutomationIssue)
1782
+ });
1783
+ var AutomationDuplicateRequest = z7.object({ name: z7.string().min(1).max(200).optional() }).strict();
1784
+ var AutomationEnrollRequest = z7.object({
1785
+ contact_ids: z7.array(Id).min(1).max(1e3).optional(),
1786
+ external_ids: z7.array(z7.string().min(1).max(255)).min(1).max(1e3).optional(),
1787
+ emails: z7.array(Email).min(1).max(1e3).optional(),
1788
+ segment_id: Id.optional(),
1789
+ /** Where to start them; the flow's entry step by default. */
1790
+ step_id: StepId.optional()
1791
+ }).strict().refine(
1792
+ (v) => [v.contact_ids, v.external_ids, v.emails, v.segment_id].filter((x) => x !== void 0).length === 1,
1793
+ "name the people exactly one way: contact_ids, external_ids, emails or segment_id"
1794
+ );
1795
+ var AutomationEnrollment = z7.object({
1796
+ contact_id: Id.nullable(),
1797
+ email: Email.nullable(),
1798
+ external_id: z7.string().nullable(),
1799
+ entered: z7.boolean(),
1800
+ run_id: Id.nullable(),
1801
+ reason: AutomationEntryRefusal.nullable()
1802
+ });
1803
+ var AutomationEnrollResult = z7.object({
1804
+ entered: z7.number().int().min(0),
1805
+ refused: z7.number().int().min(0),
1806
+ results: z7.array(AutomationEnrollment)
1807
+ });
1808
+ var AutomationRunStatus = z7.enum(["active", "waiting", "completed", "exited", "failed"]);
1809
+ var AutomationRun = z7.object({
1810
+ id: Id,
1811
+ automation_id: Id,
1812
+ contact_id: Id,
1813
+ email: Email,
1814
+ external_id: z7.string().nullable(),
1815
+ version: z7.number().int().min(1),
1816
+ status: AutomationRunStatus,
1817
+ current_step_id: StepId.nullable(),
1818
+ next_action_at: Timestamp.nullable(),
1819
+ wait_reason: AutomationWaitReason.nullable(),
1820
+ end_reason: z7.string().nullable(),
1821
+ /** What went wrong the last time this run tried to take its step, null otherwise. */
1822
+ last_error: z7.string().nullable(),
1823
+ entered_at: Timestamp,
1824
+ ended_at: Timestamp.nullable()
1825
+ });
1826
+ var AutomationRunListQuery = PageQuery.extend({
1827
+ status: AutomationRunStatus.optional(),
1828
+ step_id: StepId.optional(),
1829
+ contact_id: Id.optional()
1830
+ });
1831
+ var ContactAutomationRun = AutomationRun.extend({
1832
+ automation: z7.object({ id: Id, name: z7.string(), status: AutomationStatus })
1833
+ });
1834
+ var ContactAutomationListQuery = PageQuery.extend({ status: AutomationRunStatus.optional() });
1835
+ var AutomationStepExecution = z7.object({
1836
+ step_id: StepId,
1837
+ step_kind: AutomationStepKind,
1838
+ visit: z7.number().int().min(1),
1839
+ started_at: Timestamp,
1840
+ finished_at: Timestamp.nullable(),
1841
+ /**
1842
+ * What happened, by step kind: `branch` for a condition, `recipient_id`,
1843
+ * `recipient_status` and `skip_reason` for an email, `reason` for a step that
1844
+ * did nothing, and `skipped` for a wait that was not waited (`no_date` when
1845
+ * the contact carries no date, `date_passed` when the date was already behind
1846
+ * them). The journey renders each of these in words.
1847
+ */
1848
+ outcome: Properties
1849
+ });
1850
+ var AutomationJourney = z7.object({
1851
+ run: AutomationRun,
1852
+ steps: z7.array(AutomationStepExecution)
1853
+ });
1854
+ var AutomationStepReport = z7.object({
1855
+ step_id: StepId,
1856
+ step_kind: AutomationStepKind,
1857
+ reached: z7.number().int().min(0),
1858
+ waiting: z7.number().int().min(0),
1859
+ passed: z7.number().int().min(0),
1860
+ /** Per branch, for a condition: how many took `yes` and how many `no`. */
1861
+ branches: z7.record(z7.number().int().min(0)),
1862
+ /** The step mailing's id, for an email step of the published version. */
1863
+ mailing_id: Id.nullable()
1864
+ });
1865
+ var AutomationReport = z7.object({
1866
+ counts: AutomationCounts,
1867
+ /** Why the people who left did: `AutomationEndReason` to a count. */
1868
+ end_reasons: z7.record(z7.number().int().min(0)),
1869
+ /**
1870
+ * Why people a trigger named did not enter: `AutomationEntryRefusal` to a
1871
+ * count. Without it a pause, an exclusion or a missing subscription would
1872
+ * turn people away invisibly.
1873
+ */
1874
+ refusals: z7.record(z7.number().int().min(0)),
1875
+ steps: z7.array(AutomationStepReport)
1876
+ });
1877
+ var AutomationExitRunRequest = z7.object({ reason: z7.string().max(200).optional() }).strict();
1878
+ var AutomationRunParams = z7.object({ id: Id, run_id: Id });
1879
+ var AutomationStepParams = z7.object({ id: Id, step_id: StepId });
1880
+ var AutomationTestEmailRequest = z7.object({ to: Email, merge: Properties.optional() }).strict();
1881
+ var AutomationValidateRequest = z7.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
1882
+ var EventCreate = z7.object({
1883
+ name: EventName,
1884
+ contact: z7.object({ external_id: z7.string().min(1).max(255).optional(), email: Email.optional(), id: Id.optional() }).refine((c) => [c.external_id, c.email, c.id].filter((x) => x !== void 0).length === 1, "name the contact exactly one way"),
1885
+ data: Properties.optional(),
1886
+ occurred_at: Timestamp.optional(),
1887
+ id: z7.string().min(1).max(128).optional()
1888
+ }).strict();
1889
+ var Event = z7.object({
1890
+ id: Id,
1891
+ client_event_id: z7.string().nullable(),
1892
+ name: EventName,
1893
+ contact_id: Id,
1894
+ data: Properties,
1895
+ occurred_at: Timestamp,
1896
+ received_at: Timestamp,
1897
+ /**
1898
+ * How many automations were waiting for this event and took the person in
1899
+ * hand. Each is then admitted or refused by the runtime on its own rules
1900
+ * (consent, exclusions, re-entry), which `automations.report` counts.
1901
+ */
1902
+ entered: z7.number().int().min(0)
1903
+ });
1904
+ var EventListQuery = PageQuery.extend({
1905
+ name: EventName.optional(),
1906
+ contact_id: Id.optional()
1907
+ });
1908
+
1909
+ // src/workspace.ts
1910
+ var CompanyId = z8.string().min(1).max(64);
1911
+ var ASSET_POLICIES = ["any", "service_only"];
1912
+ var AssetPolicy = z8.enum(ASSET_POLICIES);
1913
+ var WorkspaceSettings = z8.object({
1914
+ /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
1915
+ default_locale: z8.string().min(2).max(35),
1916
+ /** Languages the hosted pages are offered in. Includes `default_locale`. */
1917
+ locales: z8.array(z8.string().min(2).max(35)).min(1).max(20),
1918
+ /** Open and click tracking. Off by default, opt-in per workspace. */
1919
+ tracking_enabled: z8.boolean(),
1920
+ /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
1921
+ asset_policy: AssetPolicy,
1922
+ /**
1923
+ * The time zone an automation reads a wall clock in for a contact who has
1924
+ * none of their own (A1). Null falls back to UTC.
1925
+ */
1926
+ default_timezone: TimezoneName.nullable()
1927
+ });
1928
+ var Workspace = z8.object({
1929
+ id: Id,
1930
+ slug: Slug,
1931
+ name: z8.string().min(1).max(120),
1932
+ settings: WorkspaceSettings,
1933
+ /**
1934
+ * The auth-brain company (tenant) the workspace was created for, or null for
1935
+ * a workspace created without one. When auth-brain erases that company, every
1936
+ * workspace carrying its id is erased with it.
1937
+ */
1938
+ company_id: CompanyId.nullable(),
1939
+ created_at: Timestamp,
1940
+ updated_at: Timestamp
1941
+ });
1942
+ var WorkspaceCreate = z8.object({
1943
+ slug: Slug,
1944
+ name: z8.string().min(1).max(120),
1945
+ settings: WorkspaceSettings.partial().optional(),
1946
+ owner: z8.object({
1947
+ email: Email,
1948
+ name: z8.string().max(200).nullable().optional()
1949
+ }),
1950
+ /**
1951
+ * The signed-in person's active auth-brain company. The dashboard always
1952
+ * sends it, which is what links the workspace to auth-brain's company
1953
+ * erasure; omitted, the workspace belongs to no company and no company
1954
+ * erasure reaches it.
1955
+ */
1956
+ company_id: CompanyId.optional()
1957
+ });
1958
+ var WorkspaceUpdate = z8.object({
1959
+ name: z8.string().min(1).max(120),
1960
+ settings: WorkspaceSettings.partial()
1961
+ }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
1962
+ var WorkspaceMove = z8.object({ company_id: CompanyId });
1963
+ var MEMBER_ROLES = ["owner", "admin", "editor", "viewer"];
1964
+ var MemberRole = z8.enum(MEMBER_ROLES);
1965
+ var WorkspaceMembership = Workspace.extend({ role: MemberRole });
1966
+ var Member = z8.object({
1967
+ id: Id,
1968
+ /** The person's auth-brain subject. */
1969
+ subject: z8.string().min(1).max(255),
1970
+ email: Email,
1971
+ name: z8.string().max(200).nullable(),
1972
+ role: MemberRole,
1973
+ created_at: Timestamp
1974
+ });
1975
+ var MemberCreate = z8.object({
1976
+ subject: z8.string().min(1).max(255),
1977
+ email: Email,
1978
+ name: z8.string().max(200).nullable().optional(),
1979
+ role: MemberRole
1980
+ });
1981
+ var MemberUpdate = z8.object({ role: MemberRole });
1982
+ var API_KEY_SCOPES = ["full", "read", "send"];
1983
+ var ApiKeyScope = z8.enum(API_KEY_SCOPES);
1984
+ var ApiKey = z8.object({
1985
+ id: Id,
1986
+ name: z8.string().min(1).max(120),
1987
+ /** The first characters of the key, for recognising it in a list. */
1988
+ prefix: z8.string().min(4).max(32),
1989
+ scope: ApiKeyScope,
1990
+ last_used_at: Timestamp.nullable(),
1991
+ revoked_at: Timestamp.nullable(),
1992
+ created_at: Timestamp
1993
+ });
1994
+ var ApiKeyCreate = z8.object({
1995
+ name: z8.string().min(1).max(120),
1996
+ /** Omitted means `full`. */
1997
+ scope: ApiKeyScope.optional()
1998
+ });
1999
+ var ApiKeyCreated = z8.object({
2000
+ key: z8.string().min(20),
2001
+ api_key: ApiKey
2002
+ });
2003
+ var AUDIT_ACTIONS = [
2004
+ "workspace.created",
2005
+ "workspace.updated",
2006
+ "workspace.company_changed",
2007
+ "member.added",
2008
+ "member.role_changed",
2009
+ "member.removed",
2010
+ "member.invited",
2011
+ "invite.revoked",
2012
+ "api_key.created",
2013
+ "api_key.revoked",
2014
+ "provider.created",
2015
+ "provider.updated",
2016
+ "provider.deleted",
2017
+ "provider.events_registered",
2018
+ "provider.events_secret_set",
2019
+ "provider.bounces_halted",
2020
+ "provider.anomaly_cleared",
2021
+ "topic.created",
2022
+ "topic.updated",
2023
+ "template.created",
2024
+ "template.updated",
2025
+ "template.deleted",
2026
+ "saved_section.created",
2027
+ "saved_section.updated",
2028
+ "saved_section.deleted",
2029
+ "asset.uploaded",
2030
+ "asset.imported",
2031
+ "contact.erased",
2032
+ "suppression.created",
2033
+ "suppression.deleted",
2034
+ "mailing.created",
2035
+ "mailing.sent",
2036
+ "mailing.paused",
2037
+ "mailing.resumed",
2038
+ "mailing.cancelled",
2039
+ "mailing.retried",
2040
+ "webhook.created",
2041
+ "webhook.updated",
2042
+ "webhook.deleted",
2043
+ "webhook.secret_rotated",
2044
+ "webhook.redelivered",
2045
+ "contact.unsubscribed",
2046
+ // S4: the platform features
2047
+ "tag.created",
2048
+ "tag.deleted",
2049
+ "tag.assigned",
2050
+ "tag.unassigned",
2051
+ "contact_property.created",
2052
+ "contact_property.deleted",
2053
+ "segment.created",
2054
+ "segment.updated",
2055
+ "segment.deleted",
2056
+ "signup_form.created",
2057
+ "signup_form.updated",
2058
+ "signup_form.deleted",
2059
+ "contact.subscribed",
2060
+ "import.created",
2061
+ "import.mapped",
2062
+ "import.committed",
2063
+ "import.cancelled",
2064
+ "import.finished",
2065
+ "mailing.scheduled",
2066
+ "mailing.unscheduled",
2067
+ "mailing.schedule_failed",
2068
+ "mailing.ab_test_updated",
2069
+ "mailing.ab_winner_selected",
2070
+ "tracking.updated",
2071
+ "billing.checkout_started",
2072
+ "billing.subscription_changed",
2073
+ "billing.exemption_changed",
2074
+ // A1: automations
2075
+ "automation.created",
2076
+ "automation.updated",
2077
+ "automation.published",
2078
+ "automation.paused",
2079
+ "automation.resumed",
2080
+ "automation.archived",
2081
+ "automation.deleted",
2082
+ "automation.duplicated",
2083
+ "automation.enrolled",
2084
+ "automation.run_ended",
2085
+ "event.recorded"
2086
+ ];
2087
+ var AuditAction = z8.enum(AUDIT_ACTIONS);
2088
+ var AuditActor = z8.discriminatedUnion("type", [
2089
+ z8.object({ type: z8.literal("member"), member_id: Id, subject: z8.string().min(1) }),
2090
+ z8.object({ type: z8.literal("api_key"), api_key_id: Id }),
2091
+ /** The service itself (the worker, a bounce, the hosted unsubscribe page). */
2092
+ z8.object({ type: z8.literal("system"), reason: z8.string().min(1).max(200) })
2093
+ ]);
2094
+ var AuditEntry = z8.object({
2095
+ id: Id,
2096
+ action: AuditAction,
2097
+ actor: AuditActor,
2098
+ target_type: z8.string().min(1).max(64),
2099
+ target_id: Id.nullable(),
2100
+ details: z8.record(z8.unknown()),
2101
+ created_at: Timestamp
2102
+ });
2103
+ var AuditQuery = PageQuery.extend({
2104
+ action: AuditAction.optional(),
2105
+ target_id: Id.optional()
2106
+ });
2107
+
2108
+ // src/invites.ts
2109
+ import { z as z9 } from "zod";
2110
+ var INVITE_STATUSES = ["pending", "accepted", "revoked", "expired"];
2111
+ var InviteStatus = z9.enum(INVITE_STATUSES);
2112
+ var DEFAULT_INVITE_TTL_DAYS = 7;
2113
+ var MAX_INVITE_TTL_DAYS = 30;
2114
+ var Invite = z9.object({
2115
+ id: Id,
2116
+ /** Stored lowercased; only a person signed in with this address can accept. */
2117
+ email: Email,
2118
+ role: MemberRole,
2119
+ /** Derived from the timestamps below, never stored. */
2120
+ status: InviteStatus,
2121
+ /** The member who invited, while they are still one (null after they left). */
2122
+ invited_by: z9.object({ member_id: Id.nullable(), email: Email.nullable() }),
2123
+ expires_at: Timestamp,
2124
+ accepted_at: Timestamp.nullable(),
2125
+ revoked_at: Timestamp.nullable(),
2126
+ created_at: Timestamp
2127
+ });
2128
+ var InviteCreate = z9.object({
2129
+ email: Email,
2130
+ role: MemberRole,
2131
+ expires_in_days: z9.number().int().min(1).max(MAX_INVITE_TTL_DAYS).optional()
2132
+ });
2133
+ var InviteCreated = z9.object({
2134
+ invite: Invite,
2135
+ token: z9.string().min(32).max(128)
2136
+ });
2137
+ var InviteListQuery = PageQuery.extend({
2138
+ status: InviteStatus.optional()
2139
+ });
2140
+ var InviteAccept = z9.object({
2141
+ token: z9.string().min(32).max(128),
2142
+ email: Email,
2143
+ name: z9.string().max(200).nullable().optional()
2144
+ });
2145
+ var InviteAccepted = z9.object({
2146
+ workspace: WorkspaceMembership,
2147
+ already_member: z9.boolean()
2148
+ });
2149
+ var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
2150
+
2151
+ // src/providers.ts
2152
+ import { z as z10 } from "zod";
2153
+ var PROVIDER_KINDS = ["smtp", "resend"];
2154
+ var ProviderKind = z10.enum(PROVIDER_KINDS);
2155
+ var ProviderPolicy = z10.object({
2156
+ daily_recipient_budget: z10.number().int().min(1).max(1e7),
2157
+ min_interval_ms: z10.number().int().min(0).max(36e5),
2158
+ max_recipients_per_message: z10.number().int().min(1).max(1e3)
2159
+ });
2160
+ var ICLOUD_SMTP_POLICY = {
2161
+ daily_recipient_budget: 800,
2162
+ min_interval_ms: 3e3,
2163
+ max_recipients_per_message: 1
2164
+ };
2165
+ var SMTP_SECURITY = ["tls", "starttls"];
2166
+ var SmtpConfigBase = z10.object({
2167
+ host: z10.string().min(1).max(255),
2168
+ port: z10.number().int().min(1).max(65535),
2169
+ /** `tls` connects encrypted (465), `starttls` upgrades (587). Never plaintext. */
2170
+ security: z10.enum(SMTP_SECURITY),
2171
+ username: z10.string().min(1).max(255)
2172
+ });
2173
+ var ResendConfigBase = z10.object({});
2174
+ var ProviderRejections = z10.object({
2175
+ count: z10.number().int().min(0),
2176
+ last_error: z10.string().nullable(),
2177
+ last_at: Timestamp.nullable(),
2178
+ /**
2179
+ * The last time the bounce circuit breaker tripped on one of this provider's
2180
+ * mailings: too many recipients of one run were refused as dead addresses
2181
+ * (five in a row with the same reply, or more than 20 percent of the first
2182
+ * 50), which points at the provider or the setup rather than the list. The
2183
+ * run's bounce blocks were undone and the mailing paused. `sample` is the
2184
+ * reply that tripped it.
2185
+ */
2186
+ anomaly: z10.object({
2187
+ at: Timestamp,
2188
+ /** `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. */
2189
+ scope: z10.enum(["mailing", "provider"]),
2190
+ /**
2191
+ * True while the provider-wide breaker is open: no bounce blocks through
2192
+ * this provider, test sends refused and mailings not started or resumed
2193
+ * (`provider_anomaly`), until an admin clears it (`providers.clearAnomaly`).
2194
+ */
2195
+ blocking: z10.boolean(),
2196
+ mailing_id: Id.nullable(),
2197
+ reason: z10.string(),
2198
+ sample: z10.string()
2199
+ }).nullable()
2200
+ });
2201
+ var ProviderEvents = z10.object({
2202
+ status: z10.enum(["active", "needs_secret"]),
2203
+ source: z10.enum(["automatic", "manual"]).nullable(),
2204
+ url: z10.string().url(),
2205
+ error: z10.string().nullable(),
2206
+ /**
2207
+ * Events that named an email this provider never sent through the service
2208
+ * (another workspace or system sharing the Resend account): counted, never
2209
+ * acted on.
2210
+ */
2211
+ unmatched: z10.number().int().min(0)
2212
+ });
2213
+ var RESEND_EVENT_TYPES = ["email.bounced", "email.complained", "email.delivery_delayed"];
2214
+ var ResendSigningSecret = z10.string().max(200).regex(/^whsec_[A-Za-z0-9+/]{16,}={0,2}$/, "must be the signing secret Resend shows, starting with whsec_");
2215
+ var ProviderEventsSecret = z10.object({ signing_secret: ResendSigningSecret });
2216
+ var ProviderFields = {
2217
+ name: z10.string().min(1).max(120),
2218
+ from_name: z10.string().min(1).max(120),
2219
+ from_email: Email,
2220
+ reply_to: Email.nullable(),
2221
+ policy: ProviderPolicy
2222
+ };
2223
+ var Provider = z10.discriminatedUnion("kind", [
2224
+ z10.object({
2225
+ id: Id,
2226
+ kind: z10.literal("smtp"),
2227
+ config: SmtpConfigBase,
2228
+ has_secret: z10.boolean(),
2229
+ ...ProviderFields,
2230
+ rejections: ProviderRejections,
2231
+ created_at: Timestamp,
2232
+ updated_at: Timestamp
2233
+ }),
2234
+ z10.object({
2235
+ id: Id,
2236
+ kind: z10.literal("resend"),
2237
+ config: ResendConfigBase,
2238
+ has_secret: z10.boolean(),
2239
+ ...ProviderFields,
2240
+ rejections: ProviderRejections,
2241
+ events: ProviderEvents,
2242
+ created_at: Timestamp,
2243
+ updated_at: Timestamp
2244
+ })
2245
+ ]);
2246
+ var ProviderCreate = z10.discriminatedUnion("kind", [
2247
+ z10.object({
2248
+ kind: z10.literal("smtp"),
2249
+ config: SmtpConfigBase.extend({ password: z10.string().min(1).max(1024) }),
2250
+ ...ProviderFields
2251
+ }),
2252
+ z10.object({
2253
+ kind: z10.literal("resend"),
2254
+ config: ResendConfigBase.extend({ api_key: z10.string().min(1).max(1024) }),
2255
+ ...ProviderFields
2256
+ })
2257
+ ]);
2258
+ var ProviderUpdate = z10.discriminatedUnion("kind", [
2259
+ z10.object({
2260
+ kind: z10.literal("smtp"),
2261
+ config: SmtpConfigBase.extend({ password: z10.string().min(1).max(1024).optional() }).partial().optional(),
2262
+ name: ProviderFields.name.optional(),
2263
+ from_name: ProviderFields.from_name.optional(),
2264
+ from_email: ProviderFields.from_email.optional(),
2265
+ reply_to: ProviderFields.reply_to.optional(),
2266
+ policy: ProviderPolicy.partial().optional()
2267
+ }),
2268
+ z10.object({
2269
+ kind: z10.literal("resend"),
2270
+ config: ResendConfigBase.extend({ api_key: z10.string().min(1).max(1024).optional() }).optional(),
2271
+ name: ProviderFields.name.optional(),
2272
+ from_name: ProviderFields.from_name.optional(),
2273
+ from_email: ProviderFields.from_email.optional(),
2274
+ reply_to: ProviderFields.reply_to.optional(),
2275
+ policy: ProviderPolicy.partial().optional()
2276
+ })
2277
+ ]);
2278
+ var ProviderUsage = z10.object({
2279
+ provider_id: Id,
2280
+ recipients_last_24h: z10.number().int().min(0),
2281
+ remaining_budget: z10.number().int().min(0),
2282
+ /** When enough of the rolling window frees up to send again, if exhausted. */
2283
+ next_capacity_at: Timestamp.nullable()
2284
+ });
2285
+ var ProviderVerifyResult = z10.object({
2286
+ ok: z10.boolean(),
2287
+ error: z10.string().nullable()
2288
+ });
2289
+
2290
+ // src/contacts.ts
2291
+ import { z as z11 } from "zod";
2292
+ var Topic = z11.object({
2293
+ id: Id,
2294
+ slug: Slug,
2295
+ name: z11.string().min(1).max(120),
2296
+ /** Shown on the hosted preference page. */
2297
+ description: z11.string().max(1e3).nullable(),
2298
+ /** Translations of `name` and `description` per locale, for the hosted page. */
2299
+ translations: z11.record(
2300
+ z11.object({ name: z11.string().min(1).max(120), description: z11.string().max(1e3).nullable() })
2301
+ ),
2302
+ created_at: Timestamp,
2303
+ updated_at: Timestamp
2304
+ });
2305
+ var TopicCreate = z11.object({
2306
+ slug: Slug,
2307
+ name: z11.string().min(1).max(120),
2308
+ description: z11.string().max(1e3).optional(),
2309
+ translations: Topic.shape.translations.optional()
2310
+ });
2311
+ var TopicUpdate = TopicCreate.omit({ slug: true }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
2312
+ var Contact = z11.object({
2313
+ id: Id,
2314
+ external_id: z11.string().min(1).max(255).nullable(),
2315
+ /** Stored lowercased, unique per workspace. */
2316
+ email: Email,
2317
+ first_name: z11.string().max(200).nullable(),
2318
+ last_name: z11.string().max(200).nullable(),
2319
+ locale: z11.string().min(2).max(35).nullable(),
2320
+ /**
2321
+ * The person's own time zone, an IANA name such as `Europe/Berlin` (A1).
2322
+ * Every wall clock an automation waits for is read in it, falling back to the
2323
+ * workspace's `settings.default_timezone` and then to UTC.
2324
+ */
2325
+ timezone: TimezoneName.nullable(),
2326
+ properties: Properties,
2327
+ topics: z11.array(Slug),
2328
+ /** The slugs of the contact's tags (S4). */
2329
+ tags: z11.array(Slug),
2330
+ created_at: Timestamp,
2331
+ updated_at: Timestamp
2332
+ });
2333
+ var ContactUpsert = z11.object({
2334
+ external_id: z11.string().min(1).max(255).optional(),
2335
+ email: Email.optional(),
2336
+ first_name: z11.string().max(200).nullable().optional(),
2337
+ last_name: z11.string().max(200).nullable().optional(),
2338
+ locale: z11.string().min(2).max(35).nullable().optional(),
2339
+ timezone: TimezoneName.nullable().optional(),
2340
+ properties: Properties.optional(),
2341
+ topics: z11.array(Slug).max(100).optional()
2342
+ }).refine((v) => v.external_id !== void 0 || v.email !== void 0, {
2343
+ message: "external_id or email is required",
2344
+ path: ["email"]
2345
+ });
2346
+ var ContactUpsertResult = z11.object({
2347
+ contact: Contact,
2348
+ created: z11.boolean()
2349
+ });
2350
+ var ContactListQuery = PageQuery.extend({
2351
+ email: z11.string().min(1).max(254).optional(),
2352
+ external_id: z11.string().min(1).max(255).optional(),
2353
+ topic: Slug.optional()
2354
+ });
2355
+ var ContactErased = z11.object({
2356
+ ok: z11.literal(true),
2357
+ erased_messages: z11.number().int().min(0),
2358
+ erased_recipients: z11.number().int().min(0),
2359
+ suppressions_kept: z11.number().int().min(0)
2360
+ });
2361
+ var SUPPRESSION_REASONS = ["unsubscribed", "bounced", "complained", "manual"];
2362
+ var SuppressionReason = z11.enum(SUPPRESSION_REASONS);
2363
+ var Suppression = z11.object({
2364
+ id: Id,
2365
+ email: Email,
2366
+ reason: SuppressionReason,
2367
+ topic: Slug.nullable(),
2368
+ source_message_id: Id.nullable(),
2369
+ note: z11.string().max(1e3).nullable(),
2370
+ created_at: Timestamp
2371
+ });
2372
+ var SuppressionCreate = z11.object({
2373
+ email: Email,
2374
+ reason: z11.enum(["manual", "unsubscribed"]),
2375
+ topic: Slug.nullable().optional(),
2376
+ note: z11.string().max(1e3).optional()
2377
+ });
2378
+ var SuppressionListQuery = PageQuery.extend({
2379
+ email: z11.string().min(1).max(254).optional(),
2380
+ reason: SuppressionReason.optional(),
2381
+ topic: Slug.optional()
2382
+ });
2383
+
2384
+ // src/webhooks.ts
2385
+ import { z as z12 } from "zod";
2386
+ var WEBHOOK_EVENT_TYPES = [
2387
+ "message.sent",
2388
+ "message.failed",
2389
+ "contact.unsubscribed",
2390
+ "contact.resubscribed",
2391
+ "contact.bounced",
2392
+ "mailing.finished",
2393
+ // S4: the platform features
2394
+ "contact.subscribed",
2395
+ "import.finished",
2396
+ "mailing.scheduled",
2397
+ "mailing.started",
2398
+ "mailing.schedule_failed",
2399
+ "mailing.ab_winner_selected",
2400
+ // A1: automations. The first two are what the `tag_added` and
2401
+ // `property_changed` triggers hang on, so a client sees the same change the
2402
+ // automations do.
2403
+ "contact.tagged",
2404
+ "contact.updated",
2405
+ "automation.run_started",
2406
+ "automation.run_completed",
2407
+ "automation.run_exited",
2408
+ "automation.run_failed",
2409
+ /**
2410
+ * A3 S3: a flow's webhook step fired.
2411
+ *
2412
+ * Never fanned out. Every other type here goes to every enabled endpoint that
2413
+ * subscribes to it; this one goes only to the endpoint the step names, because
2414
+ * the step means that endpoint and no other. It is in this list because
2415
+ * `webhook_events.type` is checked against it, not because an endpoint can ask
2416
+ * to receive everybody's.
2417
+ */
2418
+ "automation.webhook"
2419
+ ];
2420
+ var WebhookEventType = z12.enum(WEBHOOK_EVENT_TYPES);
2421
+ var MessageEventBase = z12.object({
2422
+ message_id: Id,
2423
+ mailing_id: Id.nullable(),
2424
+ mailing_metadata: MailingMetadata.nullable(),
2425
+ recipient_id: Id.nullable(),
2426
+ contact_id: Id.nullable(),
2427
+ external_id: z12.string().nullable(),
2428
+ email: Email,
2429
+ subject: z12.string(),
2430
+ topic: Slug.nullable(),
2431
+ provider_message_id: z12.string().nullable(),
2432
+ is_test: z12.boolean()
2433
+ });
2434
+ var MessageSentData = MessageEventBase.extend({
2435
+ /** The final HTML as sent, so the client can archive it beside its own records. */
2436
+ html: z12.string(),
2437
+ sent_at: Timestamp
2438
+ });
2439
+ var MessageFailedData = MessageEventBase.extend({
2440
+ error: z12.string(),
2441
+ /** False when the provider rejected permanently; the recipient will not be retried. */
2442
+ retryable: z12.boolean(),
2443
+ failed_at: Timestamp
2444
+ });
2445
+ var UNSUBSCRIBE_SOURCES = ["hosted_page", "one_click", "api", "dashboard", "signup_form"];
2446
+ var ContactUnsubscribedData = z12.object({
2447
+ contact_id: Id.nullable(),
2448
+ external_id: z12.string().nullable(),
2449
+ email: Email,
2450
+ /** The topic unsubscribed from; null means every topic. */
2451
+ topic: Slug.nullable(),
2452
+ mailing_id: Id.nullable(),
2453
+ source: z12.enum(UNSUBSCRIBE_SOURCES),
2454
+ unsubscribed_at: Timestamp
2455
+ });
2456
+ var ContactResubscribedData = ContactUnsubscribedData.omit({ unsubscribed_at: true }).extend({
2457
+ /**
2458
+ * As on `contact.unsubscribed`, plus `bounce_reverted`: the bounce circuit
2459
+ * breaker undid a `bounced` block it judged false (a `contact.bounced` for the
2460
+ * same address and mailing went out before); `topic` is null.
2461
+ */
2462
+ source: z12.enum([...UNSUBSCRIBE_SOURCES, "bounce_reverted"]),
2463
+ resubscribed_at: Timestamp
2464
+ });
2465
+ var ContactBouncedData = z12.object({
2466
+ contact_id: Id.nullable(),
2467
+ external_id: z12.string().nullable(),
2468
+ email: Email,
2469
+ /** `bounced` for a hard bounce, `complained` for a spam complaint. */
2470
+ reason: z12.enum(["bounced", "complained"]),
2471
+ message_id: Id.nullable(),
2472
+ diagnostic: z12.string().nullable(),
2473
+ bounced_at: Timestamp
2474
+ });
2475
+ var MailingFinishedData = z12.object({
2476
+ mailing_id: Id,
2477
+ mailing_metadata: MailingMetadata,
2478
+ status: z12.enum(["sent", "partially_failed", "cancelled"]),
2479
+ counts: MailingCounts,
2480
+ finished_at: Timestamp
2481
+ });
2482
+ var ContactSubscribedData = z12.object({
2483
+ contact_id: Id,
2484
+ external_id: z12.string().nullable(),
2485
+ email: Email,
2486
+ topics: z12.array(Slug),
2487
+ source: z12.enum(["signup_form", "api", "dashboard", "automation"]),
2488
+ signup_form_id: Id.nullable(),
2489
+ signup_form_version: z12.number().int().min(1).nullable(),
2490
+ subscribed_at: Timestamp
2491
+ });
2492
+ var ImportFinishedData = z12.object({
2493
+ import_id: Id,
2494
+ status: z12.enum(["completed", "cancelled", "failed"]),
2495
+ result: ImportReport.nullable(),
2496
+ error: z12.string().nullable(),
2497
+ finished_at: Timestamp
2498
+ });
2499
+ var MailingScheduledData = z12.object({
2500
+ mailing_id: Id,
2501
+ mailing_metadata: MailingMetadata,
2502
+ scheduled_at: Timestamp.nullable()
2503
+ });
2504
+ var MailingStartedData = z12.object({
2505
+ mailing_id: Id,
2506
+ mailing_metadata: MailingMetadata,
2507
+ trigger: z12.enum(["send", "schedule"]),
2508
+ recipients: z12.number().int().min(0),
2509
+ started_at: Timestamp
2510
+ });
2511
+ var MailingScheduleFailedData = z12.object({
2512
+ mailing_id: Id,
2513
+ mailing_metadata: MailingMetadata,
2514
+ code: z12.string(),
2515
+ message: z12.string(),
2516
+ failed_at: Timestamp
2517
+ });
2518
+ var MailingAbWinnerSelectedData = z12.object({
2519
+ mailing_id: Id,
2520
+ mailing_metadata: MailingMetadata,
2521
+ winner: z12.string(),
2522
+ decided_by: z12.enum(["metric", "manual"]),
2523
+ variants: z12.array(VariantAnalytics),
2524
+ decided_at: Timestamp
2525
+ });
2526
+ var ContactTaggedData = z12.object({
2527
+ contact_id: Id,
2528
+ external_id: z12.string().nullable(),
2529
+ email: Email,
2530
+ /** Only the tags the contact did not already have. */
2531
+ tags: z12.array(Slug).min(1),
2532
+ source: z12.enum(["api", "dashboard", "signup_form", "import", "automation"]),
2533
+ tagged_at: Timestamp
2534
+ });
2535
+ var ContactUpdatedData = z12.object({
2536
+ contact_id: Id,
2537
+ external_id: z12.string().nullable(),
2538
+ email: Email,
2539
+ changed: z12.array(z12.string()).min(1),
2540
+ properties: Properties,
2541
+ source: z12.enum(["api", "dashboard", "signup_form", "import", "automation"]),
2542
+ updated_at: Timestamp
2543
+ });
2544
+ var AutomationRunEventBase = z12.object({
2545
+ automation_id: Id,
2546
+ automation_name: z12.string(),
2547
+ run_id: Id,
2548
+ version: z12.number().int().min(1),
2549
+ contact_id: Id,
2550
+ external_id: z12.string().nullable(),
2551
+ email: Email
2552
+ });
2553
+ var AutomationRunStartedData = AutomationRunEventBase.extend({
2554
+ /** What let them in: a trigger kind, or `manual` for an explicit enrolment. */
2555
+ trigger_kind: z12.string(),
2556
+ entered_at: Timestamp
2557
+ });
2558
+ var AutomationRunCompletedData = AutomationRunEventBase.extend({
2559
+ last_step_id: z12.string().nullable(),
2560
+ completed_at: Timestamp
2561
+ });
2562
+ var AutomationRunExitedData = AutomationRunEventBase.extend({
2563
+ reason: AutomationEndReason,
2564
+ step_id: z12.string().nullable(),
2565
+ exited_at: Timestamp
2566
+ });
2567
+ var AutomationRunFailedData = AutomationRunEventBase.extend({
2568
+ reason: z12.string(),
2569
+ step_id: z12.string().nullable(),
2570
+ failed_at: Timestamp
2571
+ });
2572
+ var AutomationWebhookData = AutomationRunEventBase.extend({
2573
+ step_id: z12.string(),
2574
+ /** The step's own label, when it was given one, so a receiver can branch on a name. */
2575
+ step_label: z12.string().nullable(),
2576
+ /** Whatever the step carries, verbatim. Null when it carries nothing. */
2577
+ payload: JsonValue.nullable(),
2578
+ fired_at: Timestamp
2579
+ });
2580
+ var envelope = (type, data) => z12.object({
2581
+ /** Unique per event: a receiver deduplicates on it (deliveries may repeat). */
2582
+ id: Id,
2583
+ type: z12.literal(type),
2584
+ created_at: Timestamp,
2585
+ workspace_id: Id,
2586
+ data
2587
+ });
2588
+ var WebhookEvent = z12.discriminatedUnion("type", [
2589
+ envelope("message.sent", MessageSentData),
2590
+ envelope("message.failed", MessageFailedData),
2591
+ envelope("contact.unsubscribed", ContactUnsubscribedData),
2592
+ envelope("contact.resubscribed", ContactResubscribedData),
2593
+ envelope("contact.bounced", ContactBouncedData),
2594
+ envelope("mailing.finished", MailingFinishedData),
2595
+ envelope("contact.subscribed", ContactSubscribedData),
2596
+ envelope("import.finished", ImportFinishedData),
2597
+ envelope("mailing.scheduled", MailingScheduledData),
2598
+ envelope("mailing.started", MailingStartedData),
2599
+ envelope("mailing.schedule_failed", MailingScheduleFailedData),
2600
+ envelope("mailing.ab_winner_selected", MailingAbWinnerSelectedData),
2601
+ envelope("contact.tagged", ContactTaggedData),
2602
+ envelope("contact.updated", ContactUpdatedData),
2603
+ envelope("automation.run_started", AutomationRunStartedData),
2604
+ envelope("automation.run_completed", AutomationRunCompletedData),
2605
+ envelope("automation.run_exited", AutomationRunExitedData),
2606
+ envelope("automation.run_failed", AutomationRunFailedData),
2607
+ envelope("automation.webhook", AutomationWebhookData)
2608
+ ]);
2609
+ var WebhookEndpoint = z12.object({
2610
+ id: Id,
2611
+ url: z12.string().url(),
2612
+ description: z12.string().max(500).nullable(),
2613
+ events: z12.array(WebhookEventType).min(1),
2614
+ enabled: z12.boolean(),
2615
+ created_at: Timestamp,
2616
+ updated_at: Timestamp
2617
+ });
2618
+ var WebhookUrl = z12.string().url().refine((u) => {
2619
+ try {
2620
+ const url = new URL(u);
2621
+ if (url.protocol === "https:") return true;
2622
+ return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
2623
+ } catch {
2624
+ return false;
2625
+ }
2626
+ }, "must be https (http only for localhost)");
2627
+ var WebhookEndpointCreate = z12.object({
2628
+ url: WebhookUrl,
2629
+ description: z12.string().max(500).optional(),
2630
+ events: z12.array(WebhookEventType).min(1),
2631
+ enabled: z12.boolean().optional()
2632
+ });
2633
+ var WebhookEndpointUpdate = z12.object({
2634
+ url: WebhookUrl,
2635
+ description: z12.string().max(500).nullable(),
2636
+ events: z12.array(WebhookEventType).min(1),
2637
+ enabled: z12.boolean()
2638
+ }).partial().refine((v) => Object.keys(v).length > 0, "at least one field");
2639
+ var WebhookEndpointWithSecret = z12.object({
2640
+ endpoint: WebhookEndpoint,
2641
+ secret: z12.string().min(32)
2642
+ });
2643
+ var WEBHOOK_DELIVERY_STATUSES = ["pending", "succeeded", "failed"];
2644
+ var WebhookDeliveryStatus = z12.enum(WEBHOOK_DELIVERY_STATUSES);
2645
+ var WEBHOOK_MAX_ATTEMPTS = 8;
2646
+ var WEBHOOK_RETRY_DELAYS_SECONDS = [30, 120, 600, 1800, 3600, 7200, 21600];
2647
+ var WebhookDelivery = z12.object({
2648
+ id: Id,
2649
+ endpoint_id: Id,
2650
+ event_id: Id,
2651
+ event_type: WebhookEventType,
2652
+ status: WebhookDeliveryStatus,
2653
+ attempts: z12.number().int().min(0),
2654
+ last_status_code: z12.number().int().min(100).max(599).nullable(),
2655
+ last_error: z12.string().nullable(),
2656
+ next_attempt_at: Timestamp.nullable(),
2657
+ delivered_at: Timestamp.nullable(),
2658
+ created_at: Timestamp
2659
+ });
2660
+ var WebhookDeliveryListQuery = PageQuery.extend({
2661
+ status: WebhookDeliveryStatus.optional(),
2662
+ event_type: WebhookEventType.optional()
2663
+ });
2664
+ var WebhookDeliveryParams = z12.object({ id: Id, delivery_id: Id });
2665
+
2666
+ // src/webhook-signing.ts
2667
+ var WEBHOOK_SIGNATURE_HEADER = "x-mail-signature";
2668
+ var WEBHOOK_TIMESTAMP_HEADER = "x-mail-timestamp";
2669
+ var WEBHOOK_EVENT_ID_HEADER = "x-mail-event-id";
2670
+ var WEBHOOK_SIGNATURE_VERSION = "v1";
2671
+ var WEBHOOK_TOLERANCE_SECONDS = 300;
2672
+ var WEBHOOK_SECRET_PREFIX = "whsec_";
2673
+ var encoder = new TextEncoder();
2674
+ function subtle() {
2675
+ const c = globalThis.crypto;
2676
+ if (!c?.subtle) {
2677
+ throw new Error("Web Crypto (globalThis.crypto.subtle) is not available in this runtime");
2678
+ }
2679
+ return c.subtle;
2680
+ }
2681
+ function toHex(buffer) {
2682
+ return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
2683
+ }
2684
+ async function hmacHex(secret, message) {
2685
+ if (secret.length === 0) throw new Error("webhook secret must not be empty");
2686
+ const key = await subtle().importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
2687
+ "sign"
2688
+ ]);
2689
+ return toHex(await subtle().sign("HMAC", key, encoder.encode(message)));
2690
+ }
2691
+ function timingSafeEqualString(a, b) {
2692
+ const ab = encoder.encode(a);
2693
+ const bb = encoder.encode(b);
2694
+ let diff = ab.length ^ bb.length;
2695
+ const n = Math.max(ab.length, bb.length);
2696
+ for (let i = 0; i < n; i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
2697
+ return diff === 0;
2698
+ }
2699
+ function nowSeconds() {
2700
+ return Math.floor(Date.now() / 1e3);
2701
+ }
2702
+ async function signWebhook(secret, rawBody, timestamp = nowSeconds()) {
2703
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0) throw new Error("timestamp must be a non-negative integer");
2704
+ const hex = await hmacHex(secret, `${timestamp}.${rawBody}`);
2705
+ return { signature: `${WEBHOOK_SIGNATURE_VERSION}=${hex}`, timestamp: String(timestamp) };
2706
+ }
2707
+ async function verifyWebhook(input) {
2708
+ const { rawBody, signatureHeader, timestampHeader } = input;
2709
+ if (!signatureHeader) return { ok: false, reason: "missing_signature" };
2710
+ if (!timestampHeader) return { ok: false, reason: "missing_timestamp" };
2711
+ if (!/^\d{1,12}$/.test(timestampHeader)) return { ok: false, reason: "invalid_timestamp" };
2712
+ const timestamp = Number(timestampHeader);
2713
+ const now = input.now ?? nowSeconds();
2714
+ const tolerance = input.toleranceSeconds ?? WEBHOOK_TOLERANCE_SECONDS;
2715
+ if (Math.abs(now - timestamp) > tolerance) return { ok: false, reason: "timestamp_out_of_tolerance" };
2716
+ const candidates = signatureHeader.split(",").map((part) => part.trim()).filter((part) => part.startsWith(`${WEBHOOK_SIGNATURE_VERSION}=`)).map((part) => part.slice(WEBHOOK_SIGNATURE_VERSION.length + 1));
2717
+ if (candidates.length === 0) return { ok: false, reason: "missing_signature" };
2718
+ const secrets = typeof input.secret === "string" ? [input.secret] : input.secret;
2719
+ let matched = false;
2720
+ for (const secret of secrets) {
2721
+ if (secret.length === 0) continue;
2722
+ const expected = await hmacHex(secret, `${timestampHeader}.${rawBody}`);
2723
+ for (const candidate of candidates) {
2724
+ if (timingSafeEqualString(expected, candidate)) matched = true;
2725
+ }
2726
+ }
2727
+ return matched ? { ok: true, timestamp } : { ok: false, reason: "signature_mismatch" };
2728
+ }
2729
+
2730
+ // src/unsubscribe.ts
2731
+ var UNSUBSCRIBE_PATH_PREFIX = "/u/";
2732
+ var LIST_UNSUBSCRIBE_POST_VALUE = "List-Unsubscribe=One-Click";
2733
+ var RESERVED_MERGE_FIELDS = {
2734
+ /** From the recipient's merge values, else the contact. Supports a fallback. */
2735
+ first_name: { source: "contact", fallback: true, required_for_broadcast: false },
2736
+ last_name: { source: "contact", fallback: true, required_for_broadcast: false },
2737
+ email: { source: "contact", fallback: false, required_for_broadcast: false },
2738
+ /** The hosted unsubscribe page for this recipient. A broadcast without it is refused. */
2739
+ unsubscribe_url: { source: "service", fallback: false, required_for_broadcast: true }
2740
+ };
2741
+ var MERGE_FIELD_PATTERN = /\{\{\s*([a-z][a-z0-9_]*)\s*(?:\|([^}]*))?\}\}/g;
2742
+ function findMergeFields(html) {
2743
+ const uses = [];
2744
+ for (const match of html.matchAll(MERGE_FIELD_PATTERN)) {
2745
+ uses.push({ name: match[1], fallback: match[2] === void 0 ? null : match[2].trim() });
2746
+ }
2747
+ return uses;
2748
+ }
2749
+ function missingRequiredMergeFields(html) {
2750
+ const used = new Set(findMergeFields(html).map((u) => u.name));
2751
+ return Object.keys(RESERVED_MERGE_FIELDS).filter(
2752
+ (name) => RESERVED_MERGE_FIELDS[name].required_for_broadcast && !used.has(name)
2753
+ );
2754
+ }
2755
+
2756
+ // src/billing.ts
2757
+ import { z as z13 } from "zod";
2758
+ var PLAN_IDS = ["free", "starter", "growth", "design_partner"];
2759
+ var PlanId = z13.enum(PLAN_IDS);
2760
+ var PlanLimits = z13.object({
2761
+ monthly_messages: z13.number().int().min(0).nullable(),
2762
+ contacts: z13.number().int().min(0).nullable(),
2763
+ members: z13.number().int().min(1).nullable(),
2764
+ providers: z13.number().int().min(1).nullable(),
2765
+ webhook_endpoints: z13.number().int().min(0).nullable(),
2766
+ /** Automations that may be live (active or paused) at once (A1). */
2767
+ active_automations: z13.number().int().min(0).nullable(),
2768
+ /** Steps one automation may hold. Null is the flow's own cap, not unlimited. */
2769
+ automation_steps: z13.number().int().min(1).nullable()
2770
+ });
2771
+ var PlanFeatures = z13.object({
2772
+ /** A/B tests of subject and content on a mailing (S4). */
2773
+ ab_testing: z13.boolean(),
2774
+ /** Open and click tracking, still off until a workspace turns it on (`settings.tracking_enabled`). */
2775
+ tracking: z13.boolean(),
2776
+ /** Sending and hosting pages on the workspace's own domain (a later phase). */
2777
+ custom_domains: z13.boolean(),
2778
+ /** Automations: flows a person walks through step by step (A1). */
2779
+ automations: z13.boolean()
2780
+ });
2781
+ var PLAN_FEATURES = ["ab_testing", "tracking", "custom_domains", "automations"];
2782
+ var Plan = z13.object({
2783
+ id: PlanId,
2784
+ name: z13.string().min(1),
2785
+ /** Price in the smallest currency unit per month; null for plans not sold. */
2786
+ monthly_price_cents: z13.number().int().min(0).nullable(),
2787
+ currency: z13.string().length(3),
2788
+ limits: PlanLimits,
2789
+ features: PlanFeatures
2790
+ });
2791
+ var CatalogPlan = Plan.extend({ sellable: z13.boolean() });
2792
+ var Subscription = z13.object({
2793
+ workspace_id: Id,
2794
+ plan: PlanId,
2795
+ status: z13.enum(["active", "trialing", "past_due", "cancelled"]),
2796
+ current_period_start: Timestamp,
2797
+ current_period_end: Timestamp,
2798
+ cancel_at_period_end: z13.boolean(),
2799
+ /**
2800
+ * A design partner outside billing (plan `design_partner`, no limits, no
2801
+ * checkout). Set only by an operator, never through the API.
2802
+ */
2803
+ billing_exempt: z13.boolean()
2804
+ });
2805
+ var USAGE_METRICS = ["messages", "contacts", "members", "providers", "webhook_endpoints", "active_automations"];
2806
+ var UsageMetric = z13.enum(USAGE_METRICS);
2807
+ var USAGE_WARNING_RATIO = 0.8;
2808
+ var UsageWarning = z13.object({
2809
+ metric: UsageMetric,
2810
+ used: z13.number().int().min(0),
2811
+ limit: z13.number().int().min(0),
2812
+ /** `approaching`: at or above USAGE_WARNING_RATIO of the limit; `reached`: at or above the limit. */
2813
+ level: z13.enum(["approaching", "reached"])
2814
+ });
2815
+ var Usage = z13.object({
2816
+ plan: PlanId,
2817
+ period_start: Timestamp,
2818
+ period_end: Timestamp,
2819
+ metrics: z13.record(UsageMetric, z13.object({ used: z13.number().int().min(0), limit: z13.number().int().min(0).nullable() })),
2820
+ warnings: z13.array(UsageWarning)
2821
+ });
2822
+ var CheckoutRequest = z13.object({
2823
+ plan: z13.enum(["starter", "growth"]),
2824
+ success_url: z13.string().url(),
2825
+ cancel_url: z13.string().url()
2826
+ });
2827
+ var CheckoutSession = z13.object({ url: z13.string().url() });
2828
+ var PortalRequest = z13.object({ return_url: z13.string().url() });
2829
+ var PortalSession = z13.object({ url: z13.string().url() });
2830
+ var USAGE_WARNING_HEADER = "x-mail-usage-warning";
2831
+ function formatUsageWarningHeader(warnings) {
2832
+ if (warnings.length === 0) return null;
2833
+ return warnings.map((w) => `${w.metric}=${w.used}/${w.limit}`).join(",");
2834
+ }
2835
+ function parseUsageWarningHeader(value) {
2836
+ if (!value) return [];
2837
+ const out = [];
2838
+ for (const part of value.split(",")) {
2839
+ const match = /^\s*([a-z_]+)=(\d+)\/(\d+)\s*$/.exec(part);
2840
+ if (!match) continue;
2841
+ const metric = UsageMetric.safeParse(match[1]);
2842
+ if (!metric.success) continue;
2843
+ out.push({ metric: metric.data, used: Number(match[2]), limit: Number(match[3]) });
2844
+ }
2845
+ return out;
2846
+ }
2847
+
2848
+ // src/routes.ts
2849
+ import { z as z14 } from "zod";
2850
+ var list = (item) => page(item);
2851
+ var foundationRoutes = {
2852
+ "workspaces.create": {
2853
+ method: "POST",
2854
+ path: "/v1/workspaces",
2855
+ body: WorkspaceCreate,
2856
+ response: Workspace,
2857
+ status: 201,
2858
+ access: "dashboard",
2859
+ phase: "S0"
2860
+ },
2861
+ "workspaces.list": {
2862
+ method: "GET",
2863
+ path: "/v1/workspaces",
2864
+ query: PageQuery,
2865
+ response: list(WorkspaceMembership),
2866
+ status: 200,
2867
+ access: "dashboard",
2868
+ phase: "S0"
2869
+ },
2870
+ "workspace.get": { method: "GET", path: "/v1/workspace", response: Workspace, status: 200, access: "read", phase: "S0" },
2871
+ "workspace.update": {
2872
+ method: "PATCH",
2873
+ path: "/v1/workspace",
2874
+ body: WorkspaceUpdate,
2875
+ response: Workspace,
2876
+ status: 200,
2877
+ access: "admin",
2878
+ phase: "S0"
2879
+ },
2880
+ /**
2881
+ * Hands the workspace to another auth-brain company. Its own route rather
2882
+ * than a field on `workspace.update`, because that one is `admin` and this is
2883
+ * not an ordinary setting: it decides whose erasure removes the workspace.
2884
+ */
2885
+ "workspace.move": {
2886
+ method: "POST",
2887
+ path: "/v1/workspace/company",
2888
+ body: WorkspaceMove,
2889
+ response: Workspace,
2890
+ status: 200,
2891
+ access: "owner",
2892
+ phase: "S0"
2893
+ },
2894
+ "members.list": {
2895
+ method: "GET",
2896
+ path: "/v1/members",
2897
+ query: PageQuery,
2898
+ response: list(Member),
2899
+ status: 200,
2900
+ access: "read",
2901
+ phase: "S0"
2902
+ },
2903
+ "members.add": {
2904
+ method: "POST",
2905
+ path: "/v1/members",
2906
+ body: MemberCreate,
2907
+ response: Member,
2908
+ status: 201,
2909
+ access: "admin",
2910
+ phase: "S0"
2911
+ },
2912
+ "members.update": {
2913
+ method: "PATCH",
2914
+ path: "/v1/members/:id",
2915
+ params: IdParams,
2916
+ body: MemberUpdate,
2917
+ response: Member,
2918
+ status: 200,
2919
+ access: "admin",
2920
+ phase: "S0"
2921
+ },
2922
+ "members.remove": {
2923
+ method: "DELETE",
2924
+ path: "/v1/members/:id",
2925
+ params: IdParams,
2926
+ response: Ok,
2927
+ status: 200,
2928
+ access: "admin",
2929
+ phase: "S0"
2930
+ },
2931
+ "apiKeys.list": {
2932
+ method: "GET",
2933
+ path: "/v1/api-keys",
2934
+ query: PageQuery,
2935
+ response: list(ApiKey),
2936
+ status: 200,
2937
+ access: "admin",
2938
+ phase: "S0"
2939
+ },
2940
+ "apiKeys.create": {
2941
+ method: "POST",
2942
+ path: "/v1/api-keys",
2943
+ body: ApiKeyCreate,
2944
+ response: ApiKeyCreated,
2945
+ status: 201,
2946
+ access: "admin",
2947
+ phase: "S0"
2948
+ },
2949
+ "apiKeys.revoke": {
2950
+ method: "DELETE",
2951
+ path: "/v1/api-keys/:id",
2952
+ params: IdParams,
2953
+ response: ApiKey,
2954
+ status: 200,
2955
+ access: "admin",
2956
+ phase: "S0"
2957
+ },
2958
+ "audit.list": {
2959
+ method: "GET",
2960
+ path: "/v1/audit-log",
2961
+ query: AuditQuery,
2962
+ response: list(AuditEntry),
2963
+ status: 200,
2964
+ access: "admin",
2965
+ phase: "S0"
2966
+ }
2967
+ };
2968
+ var templateRoutes = {
2969
+ "templates.list": {
2970
+ method: "GET",
2971
+ path: "/v1/templates",
2972
+ query: TemplateListQuery,
2973
+ response: list(TemplateSummary),
2974
+ status: 200,
2975
+ access: "read",
2976
+ phase: "S1"
2977
+ },
2978
+ "templates.create": {
2979
+ method: "POST",
2980
+ path: "/v1/templates",
2981
+ body: TemplateCreate,
2982
+ response: Template,
2983
+ status: 201,
2984
+ access: "write",
2985
+ phase: "S1"
2986
+ },
2987
+ "templates.get": {
2988
+ method: "GET",
2989
+ path: "/v1/templates/:id",
2990
+ params: IdParams,
2991
+ response: Template,
2992
+ status: 200,
2993
+ access: "read",
2994
+ phase: "S1"
2995
+ },
2996
+ "templates.update": {
2997
+ method: "PUT",
2998
+ path: "/v1/templates/:id",
2999
+ params: IdParams,
3000
+ body: TemplateUpdate,
3001
+ response: Template,
3002
+ status: 200,
3003
+ access: "write",
3004
+ phase: "S1"
3005
+ },
3006
+ "templates.delete": {
3007
+ method: "DELETE",
3008
+ path: "/v1/templates/:id",
3009
+ params: IdParams,
3010
+ response: Ok,
3011
+ status: 200,
3012
+ access: "write",
3013
+ phase: "S1"
3014
+ },
3015
+ "templates.versions": {
3016
+ method: "GET",
3017
+ path: "/v1/templates/:id/versions",
3018
+ params: IdParams,
3019
+ query: PageQuery,
3020
+ response: list(TemplateVersion),
3021
+ status: 200,
3022
+ access: "read",
3023
+ phase: "S1"
3024
+ },
3025
+ "templates.version": {
3026
+ method: "GET",
3027
+ path: "/v1/templates/:id/versions/:version",
3028
+ params: TemplateVersionParams,
3029
+ response: TemplateVersion,
3030
+ status: 200,
3031
+ access: "read",
3032
+ phase: "S1"
3033
+ },
3034
+ "templates.import": {
3035
+ method: "POST",
3036
+ path: "/v1/templates/import",
3037
+ body: TemplateImport,
3038
+ response: TemplateImportResult,
3039
+ status: 201,
3040
+ access: "write",
3041
+ phase: "S1"
3042
+ },
3043
+ "templates.importPreview": {
3044
+ method: "POST",
3045
+ path: "/v1/templates/import/preview",
3046
+ body: TemplateImportPreviewRequest,
3047
+ response: TemplateImportPreview,
3048
+ status: 200,
3049
+ access: "read",
3050
+ phase: "S1"
3051
+ },
3052
+ "templates.export": {
3053
+ method: "GET",
3054
+ path: "/v1/templates/:id/export",
3055
+ params: IdParams,
3056
+ query: TemplateExportQuery,
3057
+ response: z14.string(),
3058
+ responseType: "text",
3059
+ status: 200,
3060
+ access: "read",
3061
+ phase: "S1"
3062
+ },
3063
+ "templates.compile": {
3064
+ method: "POST",
3065
+ path: "/v1/templates/:id/compile",
3066
+ params: IdParams,
3067
+ body: TemplateCompileRequest,
3068
+ response: CompileResult,
3069
+ status: 200,
3070
+ access: "read",
3071
+ phase: "S1"
3072
+ },
3073
+ compile: {
3074
+ method: "POST",
3075
+ path: "/v1/compile",
3076
+ body: CompileRequest,
3077
+ response: CompileResult,
3078
+ status: 200,
3079
+ access: "read",
3080
+ phase: "S1"
3081
+ },
3082
+ "assets.upload": {
3083
+ method: "POST",
3084
+ path: "/v1/assets",
3085
+ multipart: { fileField: "file" },
3086
+ response: Asset,
3087
+ status: 201,
3088
+ access: "write",
3089
+ phase: "S1"
3090
+ },
3091
+ "assets.import": {
3092
+ method: "POST",
3093
+ path: "/v1/assets/import",
3094
+ body: AssetImport,
3095
+ response: Asset,
3096
+ status: 201,
3097
+ access: "write",
3098
+ phase: "S1"
3099
+ },
3100
+ "assets.get": {
3101
+ method: "GET",
3102
+ path: "/v1/assets/:id",
3103
+ params: IdParams,
3104
+ response: Asset,
3105
+ status: 200,
3106
+ access: "read",
3107
+ phase: "S1"
3108
+ },
3109
+ // Saved sections: one section of a document, kept under a name so it can be
3110
+ // dropped into any other template of the same workspace.
3111
+ //
3112
+ // `write` rather than `admin`: saving a footer is ordinary editing work, and
3113
+ // the person who builds templates is the person who has it. Phase `S1`, with
3114
+ // the rest of the editor and templates area: the plan calls this its own
3115
+ // slice four, which is its numbering, not the service's phase.
3116
+ "savedSections.list": {
3117
+ method: "GET",
3118
+ path: "/v1/saved-sections",
3119
+ query: PageQuery,
3120
+ response: list(SavedSection),
3121
+ status: 200,
3122
+ access: "read",
3123
+ phase: "S1"
3124
+ },
3125
+ "savedSections.create": {
3126
+ method: "POST",
3127
+ path: "/v1/saved-sections",
3128
+ body: SavedSectionCreate,
3129
+ response: SavedSection,
3130
+ status: 201,
3131
+ access: "write",
3132
+ phase: "S1"
3133
+ },
3134
+ "savedSections.update": {
3135
+ method: "PATCH",
3136
+ path: "/v1/saved-sections/:id",
3137
+ params: IdParams,
3138
+ body: SavedSectionUpdate,
3139
+ response: SavedSection,
3140
+ status: 200,
3141
+ access: "write",
3142
+ phase: "S1"
3143
+ },
3144
+ "savedSections.delete": {
3145
+ method: "DELETE",
3146
+ path: "/v1/saved-sections/:id",
3147
+ params: IdParams,
3148
+ response: Ok,
3149
+ status: 200,
3150
+ access: "write",
3151
+ phase: "S1"
3152
+ }
3153
+ };
3154
+ var sendingRoutes = {
3155
+ "providers.list": {
3156
+ method: "GET",
3157
+ path: "/v1/providers",
3158
+ query: PageQuery,
3159
+ response: list(Provider),
3160
+ status: 200,
3161
+ access: "read",
3162
+ phase: "S2"
3163
+ },
3164
+ "providers.create": {
3165
+ method: "POST",
3166
+ path: "/v1/providers",
3167
+ body: ProviderCreate,
3168
+ response: Provider,
3169
+ status: 201,
3170
+ access: "admin",
3171
+ phase: "S2"
3172
+ },
3173
+ "providers.get": {
3174
+ method: "GET",
3175
+ path: "/v1/providers/:id",
3176
+ params: IdParams,
3177
+ response: Provider,
3178
+ status: 200,
3179
+ access: "read",
3180
+ phase: "S2"
3181
+ },
3182
+ "providers.update": {
3183
+ method: "PATCH",
3184
+ path: "/v1/providers/:id",
3185
+ params: IdParams,
3186
+ body: ProviderUpdate,
3187
+ response: Provider,
3188
+ status: 200,
3189
+ access: "admin",
3190
+ phase: "S2"
3191
+ },
3192
+ "providers.delete": {
3193
+ method: "DELETE",
3194
+ path: "/v1/providers/:id",
3195
+ params: IdParams,
3196
+ response: Ok,
3197
+ status: 200,
3198
+ access: "admin",
3199
+ phase: "S2"
3200
+ },
3201
+ "providers.verify": {
3202
+ method: "POST",
3203
+ path: "/v1/providers/:id/verify",
3204
+ params: IdParams,
3205
+ response: ProviderVerifyResult,
3206
+ status: 200,
3207
+ access: "admin",
3208
+ phase: "S2"
3209
+ },
3210
+ "providers.setEventsSecret": {
3211
+ method: "PUT",
3212
+ path: "/v1/providers/:id/events-secret",
3213
+ params: IdParams,
3214
+ body: ProviderEventsSecret,
3215
+ response: Provider,
3216
+ status: 200,
3217
+ access: "admin",
3218
+ phase: "S2"
3219
+ },
3220
+ "providers.clearAnomaly": {
3221
+ method: "POST",
3222
+ path: "/v1/providers/:id/clear-anomaly",
3223
+ params: IdParams,
3224
+ response: Provider,
3225
+ status: 200,
3226
+ access: "admin",
3227
+ phase: "S2"
3228
+ },
3229
+ "providers.usage": {
3230
+ method: "GET",
3231
+ path: "/v1/providers/:id/usage",
3232
+ params: IdParams,
3233
+ response: ProviderUsage,
3234
+ status: 200,
3235
+ access: "read",
3236
+ phase: "S2"
3237
+ },
3238
+ "topics.list": {
3239
+ method: "GET",
3240
+ path: "/v1/topics",
3241
+ query: PageQuery,
3242
+ response: list(Topic),
3243
+ status: 200,
3244
+ access: "read",
3245
+ phase: "S2"
3246
+ },
3247
+ "topics.create": {
3248
+ method: "POST",
3249
+ path: "/v1/topics",
3250
+ body: TopicCreate,
3251
+ response: Topic,
3252
+ status: 201,
3253
+ access: "admin",
3254
+ phase: "S2"
3255
+ },
3256
+ "topics.update": {
3257
+ method: "PATCH",
3258
+ path: "/v1/topics/:id",
3259
+ params: IdParams,
3260
+ body: TopicUpdate,
3261
+ response: Topic,
3262
+ status: 200,
3263
+ access: "admin",
3264
+ phase: "S2"
3265
+ },
3266
+ "contacts.upsert": {
3267
+ method: "POST",
3268
+ path: "/v1/contacts",
3269
+ body: ContactUpsert,
3270
+ response: ContactUpsertResult,
3271
+ status: 200,
3272
+ access: "write",
3273
+ phase: "S2"
3274
+ },
3275
+ "contacts.list": {
3276
+ method: "GET",
3277
+ path: "/v1/contacts",
3278
+ query: ContactListQuery,
3279
+ response: list(Contact),
3280
+ status: 200,
3281
+ access: "read",
3282
+ phase: "S2"
3283
+ },
3284
+ "contacts.get": {
3285
+ method: "GET",
3286
+ path: "/v1/contacts/:id",
3287
+ params: IdParams,
3288
+ response: Contact,
3289
+ status: 200,
3290
+ access: "read",
3291
+ phase: "S2"
3292
+ },
3293
+ "contacts.erase": {
3294
+ method: "DELETE",
3295
+ path: "/v1/contacts/:id",
3296
+ params: IdParams,
3297
+ response: ContactErased,
3298
+ status: 200,
3299
+ access: "write",
3300
+ phase: "S2"
3301
+ },
3302
+ "contacts.messages": {
3303
+ method: "GET",
3304
+ path: "/v1/contacts/:id/messages",
3305
+ params: IdParams,
3306
+ query: PageQuery,
3307
+ response: list(MessageSummary),
3308
+ status: 200,
3309
+ access: "read",
3310
+ phase: "S2"
3311
+ },
3312
+ "suppressions.list": {
3313
+ method: "GET",
3314
+ path: "/v1/suppressions",
3315
+ query: SuppressionListQuery,
3316
+ response: list(Suppression),
3317
+ status: 200,
3318
+ access: "read",
3319
+ phase: "S2"
3320
+ },
3321
+ "suppressions.create": {
3322
+ method: "POST",
3323
+ path: "/v1/suppressions",
3324
+ body: SuppressionCreate,
3325
+ response: Suppression,
3326
+ status: 201,
3327
+ access: "write",
3328
+ phase: "S2"
3329
+ },
3330
+ "suppressions.delete": {
3331
+ method: "DELETE",
3332
+ path: "/v1/suppressions/:id",
3333
+ params: IdParams,
3334
+ response: Ok,
3335
+ status: 200,
3336
+ access: "admin",
3337
+ phase: "S2"
3338
+ },
3339
+ "mailings.list": {
3340
+ method: "GET",
3341
+ path: "/v1/mailings",
3342
+ query: MailingListQuery,
3343
+ response: list(MailingSummary),
3344
+ status: 200,
3345
+ access: "read",
3346
+ phase: "S2"
3347
+ },
3348
+ "mailings.create": {
3349
+ method: "POST",
3350
+ path: "/v1/mailings",
3351
+ body: MailingCreate,
3352
+ response: Mailing,
3353
+ status: 201,
3354
+ access: "write",
3355
+ phase: "S2"
3356
+ },
3357
+ "mailings.get": {
3358
+ method: "GET",
3359
+ path: "/v1/mailings/:id",
3360
+ params: IdParams,
3361
+ response: Mailing,
3362
+ status: 200,
3363
+ access: "read",
3364
+ phase: "S2"
3365
+ },
3366
+ "mailings.export": {
3367
+ method: "GET",
3368
+ path: "/v1/mailings/:id/export",
3369
+ params: IdParams,
3370
+ query: MailingExportQuery,
3371
+ response: z14.string(),
3372
+ responseType: "text",
3373
+ status: 200,
3374
+ access: "read",
3375
+ phase: "S2"
3376
+ },
3377
+ "mailings.update": {
3378
+ method: "PATCH",
3379
+ path: "/v1/mailings/:id",
3380
+ params: IdParams,
3381
+ body: MailingUpdate,
3382
+ response: Mailing,
3383
+ status: 200,
3384
+ access: "write",
3385
+ phase: "S2"
3386
+ },
3387
+ "mailings.addRecipients": {
3388
+ method: "POST",
3389
+ path: "/v1/mailings/:id/recipients",
3390
+ params: IdParams,
3391
+ body: RecipientBatch,
3392
+ response: RecipientBatchResult,
3393
+ status: 200,
3394
+ access: "write",
3395
+ phase: "S2"
3396
+ },
3397
+ "mailings.listRecipients": {
3398
+ method: "GET",
3399
+ path: "/v1/mailings/:id/recipients",
3400
+ params: IdParams,
3401
+ query: RecipientListQuery,
3402
+ response: list(Recipient),
3403
+ status: 200,
3404
+ access: "read",
3405
+ phase: "S2"
3406
+ },
3407
+ "mailings.test": {
3408
+ method: "POST",
3409
+ path: "/v1/mailings/:id/test",
3410
+ params: IdParams,
3411
+ body: MailingTestRequest,
3412
+ response: MailingTestResult,
3413
+ status: 200,
3414
+ access: "write",
3415
+ phase: "S2"
3416
+ },
3417
+ "mailings.send": {
3418
+ method: "POST",
3419
+ path: "/v1/mailings/:id/send",
3420
+ params: IdParams,
3421
+ body: MailingSendRequest,
3422
+ response: Mailing,
3423
+ status: 202,
3424
+ access: "write",
3425
+ phase: "S2"
3426
+ },
3427
+ "mailings.pause": {
3428
+ method: "POST",
3429
+ path: "/v1/mailings/:id/pause",
3430
+ params: IdParams,
3431
+ body: MailingActionRequest,
3432
+ response: Mailing,
3433
+ status: 200,
3434
+ access: "write",
3435
+ phase: "S2"
3436
+ },
3437
+ "mailings.resume": {
3438
+ method: "POST",
3439
+ path: "/v1/mailings/:id/resume",
3440
+ params: IdParams,
3441
+ body: MailingActionRequest,
3442
+ response: Mailing,
3443
+ status: 202,
3444
+ access: "write",
3445
+ phase: "S2"
3446
+ },
3447
+ "mailings.cancel": {
3448
+ method: "POST",
3449
+ path: "/v1/mailings/:id/cancel",
3450
+ params: IdParams,
3451
+ body: MailingActionRequest,
3452
+ response: Mailing,
3453
+ status: 200,
3454
+ access: "write",
3455
+ phase: "S2"
3456
+ },
3457
+ "mailings.retryFailed": {
3458
+ method: "POST",
3459
+ path: "/v1/mailings/:id/retry-failed",
3460
+ params: IdParams,
3461
+ body: MailingRetryFailedRequest,
3462
+ response: Mailing,
3463
+ status: 202,
3464
+ access: "write",
3465
+ phase: "S2"
3466
+ },
3467
+ /**
3468
+ * Copies any mailing, in any state, into a new `draft` with the same content,
3469
+ * topic and provider (no recipients). An A/B test's definition is copied
3470
+ * (variants, test fraction, winner metric and wait) with its run reset: the
3471
+ * copy's test is `pending`, with no winner and no results.
3472
+ */
3473
+ "mailings.duplicate": {
3474
+ method: "POST",
3475
+ path: "/v1/mailings/:id/duplicate",
3476
+ params: IdParams,
3477
+ body: MailingActionRequest,
3478
+ response: Mailing,
3479
+ status: 201,
3480
+ access: "write",
3481
+ phase: "S2"
3482
+ },
3483
+ "messages.list": {
3484
+ method: "GET",
3485
+ path: "/v1/messages",
3486
+ query: MessageListQuery,
3487
+ response: list(MessageSummary),
3488
+ status: 200,
3489
+ access: "read",
3490
+ phase: "S2"
3491
+ },
3492
+ "messages.get": {
3493
+ method: "GET",
3494
+ path: "/v1/messages/:id",
3495
+ params: IdParams,
3496
+ response: Message,
3497
+ status: 200,
3498
+ access: "read",
3499
+ phase: "S2"
3500
+ },
3501
+ "webhooks.list": {
3502
+ method: "GET",
3503
+ path: "/v1/webhooks",
3504
+ query: PageQuery,
3505
+ response: list(WebhookEndpoint),
3506
+ status: 200,
3507
+ access: "admin",
3508
+ phase: "S2"
3509
+ },
3510
+ "webhooks.create": {
3511
+ method: "POST",
3512
+ path: "/v1/webhooks",
3513
+ body: WebhookEndpointCreate,
3514
+ response: WebhookEndpointWithSecret,
3515
+ status: 201,
3516
+ access: "admin",
3517
+ phase: "S2"
3518
+ },
3519
+ "webhooks.get": {
3520
+ method: "GET",
3521
+ path: "/v1/webhooks/:id",
3522
+ params: IdParams,
3523
+ response: WebhookEndpoint,
3524
+ status: 200,
3525
+ access: "admin",
3526
+ phase: "S2"
3527
+ },
3528
+ "webhooks.update": {
3529
+ method: "PATCH",
3530
+ path: "/v1/webhooks/:id",
3531
+ params: IdParams,
3532
+ body: WebhookEndpointUpdate,
3533
+ response: WebhookEndpoint,
3534
+ status: 200,
3535
+ access: "admin",
3536
+ phase: "S2"
3537
+ },
3538
+ "webhooks.delete": {
3539
+ method: "DELETE",
3540
+ path: "/v1/webhooks/:id",
3541
+ params: IdParams,
3542
+ response: Ok,
3543
+ status: 200,
3544
+ access: "admin",
3545
+ phase: "S2"
3546
+ },
3547
+ "webhooks.rotateSecret": {
3548
+ method: "POST",
3549
+ path: "/v1/webhooks/:id/rotate-secret",
3550
+ params: IdParams,
3551
+ response: WebhookEndpointWithSecret,
3552
+ status: 200,
3553
+ access: "admin",
3554
+ phase: "S2"
3555
+ },
3556
+ "webhooks.deliveries": {
3557
+ method: "GET",
3558
+ path: "/v1/webhooks/:id/deliveries",
3559
+ params: IdParams,
3560
+ query: WebhookDeliveryListQuery,
3561
+ response: list(WebhookDelivery),
3562
+ status: 200,
3563
+ access: "admin",
3564
+ phase: "S2"
3565
+ },
3566
+ "webhooks.redeliver": {
3567
+ method: "POST",
3568
+ path: "/v1/webhooks/:id/deliveries/:delivery_id/redeliver",
3569
+ params: WebhookDeliveryParams,
3570
+ response: WebhookDelivery,
3571
+ status: 202,
3572
+ access: "admin",
3573
+ phase: "S2"
3574
+ }
3575
+ };
3576
+ var platformRoutes = {
3577
+ "tags.list": { method: "GET", path: "/v1/tags", query: PageQuery, response: list(Tag), status: 200, access: "read", phase: "S4" },
3578
+ "tags.create": { method: "POST", path: "/v1/tags", body: TagCreate, response: Tag, status: 201, access: "write", phase: "S4" },
3579
+ /** Deleting a tag takes it off every contact; segments that name it then match nobody for it. */
3580
+ "tags.delete": { method: "DELETE", path: "/v1/tags/:id", params: IdParams, response: Ok, status: 200, access: "write", phase: "S4" },
3581
+ "tags.assign": {
3582
+ method: "POST",
3583
+ path: "/v1/tags/:id/contacts",
3584
+ params: IdParams,
3585
+ body: TagAssignment,
3586
+ response: Tag,
3587
+ status: 200,
3588
+ access: "write",
3589
+ phase: "S4"
3590
+ },
3591
+ "tags.unassign": {
3592
+ method: "DELETE",
3593
+ path: "/v1/tags/:id/contacts",
3594
+ params: IdParams,
3595
+ body: TagAssignment,
3596
+ response: Tag,
3597
+ status: 200,
3598
+ access: "write",
3599
+ phase: "S4"
3600
+ },
3601
+ "contactProperties.list": {
3602
+ method: "GET",
3603
+ path: "/v1/contact-properties",
3604
+ response: z14.object({ data: z14.array(ContactPropertyDefinition) }),
3605
+ status: 200,
3606
+ access: "read",
3607
+ phase: "S4"
3608
+ },
3609
+ /** Defining a key refuses when a stored value of an existing contact has another type (`conflict`, with examples). */
3610
+ "contactProperties.create": {
3611
+ method: "POST",
3612
+ path: "/v1/contact-properties",
3613
+ body: ContactPropertyDefinition,
3614
+ response: ContactPropertyDefinition,
3615
+ status: 201,
3616
+ access: "admin",
3617
+ phase: "S4"
3618
+ },
3619
+ /** Removes the definition only; stored values stay and become free-form again. */
3620
+ "contactProperties.delete": {
3621
+ method: "DELETE",
3622
+ path: "/v1/contact-properties/:key",
3623
+ params: ContactPropertyParams,
3624
+ response: Ok,
3625
+ status: 200,
3626
+ access: "admin",
3627
+ phase: "S4"
3628
+ },
3629
+ "segments.list": {
3630
+ method: "GET",
3631
+ path: "/v1/segments",
3632
+ query: PageQuery,
3633
+ response: list(Segment),
3634
+ status: 200,
3635
+ access: "read",
3636
+ phase: "S4"
3637
+ },
3638
+ "segments.create": {
3639
+ method: "POST",
3640
+ path: "/v1/segments",
3641
+ body: SegmentCreate,
3642
+ response: Segment,
3643
+ status: 201,
3644
+ access: "write",
3645
+ phase: "S4"
3646
+ },
3647
+ /** Counts what a filter matches without saving it. */
3648
+ "segments.preview": {
3649
+ method: "POST",
3650
+ path: "/v1/segments/preview",
3651
+ body: SegmentPreviewRequest,
3652
+ response: SegmentPreview,
3653
+ status: 200,
3654
+ access: "read",
3655
+ phase: "S4"
3656
+ },
3657
+ "segments.get": { method: "GET", path: "/v1/segments/:id", params: IdParams, response: Segment, status: 200, access: "read", phase: "S4" },
3658
+ "segments.update": {
3659
+ method: "PUT",
3660
+ path: "/v1/segments/:id",
3661
+ params: IdParams,
3662
+ body: SegmentCreate,
3663
+ response: Segment,
3664
+ status: 200,
3665
+ access: "write",
3666
+ phase: "S4"
3667
+ },
3668
+ "segments.delete": { method: "DELETE", path: "/v1/segments/:id", params: IdParams, response: Ok, status: 200, access: "write", phase: "S4" },
3669
+ "mailings.addSegment": {
3670
+ method: "POST",
3671
+ path: "/v1/mailings/:id/recipients/segment",
3672
+ params: IdParams,
3673
+ body: MailingAudienceFromSegment,
3674
+ response: RecipientBatchResult,
3675
+ status: 200,
3676
+ access: "write",
3677
+ phase: "S4"
3678
+ },
3679
+ "mailings.schedule": {
3680
+ method: "POST",
3681
+ path: "/v1/mailings/:id/schedule",
3682
+ params: IdParams,
3683
+ body: MailingScheduleRequest,
3684
+ response: Mailing,
3685
+ status: 200,
3686
+ access: "write",
3687
+ phase: "S4"
3688
+ },
3689
+ /** A `scheduled` mailing back to `draft`. */
3690
+ "mailings.unschedule": {
3691
+ method: "POST",
3692
+ path: "/v1/mailings/:id/unschedule",
3693
+ params: IdParams,
3694
+ body: MailingActionRequest,
3695
+ response: Mailing,
3696
+ status: 200,
3697
+ access: "write",
3698
+ phase: "S4"
3699
+ },
3700
+ /** Sets or replaces the A/B test of a `draft` or `scheduled` mailing. */
3701
+ "mailings.setAbTest": {
3702
+ method: "PUT",
3703
+ path: "/v1/mailings/:id/ab-test",
3704
+ params: IdParams,
3705
+ body: AbTestConfig,
3706
+ response: Mailing,
3707
+ status: 200,
3708
+ access: "write",
3709
+ phase: "S4"
3710
+ },
3711
+ "mailings.clearAbTest": {
3712
+ method: "DELETE",
3713
+ path: "/v1/mailings/:id/ab-test",
3714
+ params: IdParams,
3715
+ response: Mailing,
3716
+ status: 200,
3717
+ access: "write",
3718
+ phase: "S4"
3719
+ },
3720
+ /** Picks the winner of a running test by hand; the held recipients get it. */
3721
+ "mailings.pickAbWinner": {
3722
+ method: "POST",
3723
+ path: "/v1/mailings/:id/ab-test/winner",
3724
+ params: IdParams,
3725
+ body: AbWinnerRequest,
3726
+ response: Mailing,
3727
+ status: 200,
3728
+ access: "write",
3729
+ phase: "S4"
3730
+ },
3731
+ "mailings.analytics": {
3732
+ method: "GET",
3733
+ path: "/v1/mailings/:id/analytics",
3734
+ params: IdParams,
3735
+ response: MailingAnalytics,
3736
+ status: 200,
3737
+ access: "read",
3738
+ phase: "S4"
3739
+ },
3740
+ "signupForms.list": {
3741
+ method: "GET",
3742
+ path: "/v1/signup-forms",
3743
+ query: PageQuery,
3744
+ response: list(SignupForm),
3745
+ status: 200,
3746
+ access: "read",
3747
+ phase: "S4"
3748
+ },
3749
+ "signupForms.create": {
3750
+ method: "POST",
3751
+ path: "/v1/signup-forms",
3752
+ body: SignupFormCreate,
3753
+ response: SignupForm,
3754
+ status: 201,
3755
+ access: "admin",
3756
+ phase: "S4"
3757
+ },
3758
+ "signupForms.get": {
3759
+ method: "GET",
3760
+ path: "/v1/signup-forms/:id",
3761
+ params: IdParams,
3762
+ response: SignupForm,
3763
+ status: 200,
3764
+ access: "read",
3765
+ phase: "S4"
3766
+ },
3767
+ "signupForms.update": {
3768
+ method: "PUT",
3769
+ path: "/v1/signup-forms/:id",
3770
+ params: IdParams,
3771
+ body: SignupFormCreate,
3772
+ response: SignupForm,
3773
+ status: 200,
3774
+ access: "admin",
3775
+ phase: "S4"
3776
+ },
3777
+ /** Pending confirmations of a deleted form stop working; confirmed subscriptions stay. */
3778
+ "signupForms.delete": {
3779
+ method: "DELETE",
3780
+ path: "/v1/signup-forms/:id",
3781
+ params: IdParams,
3782
+ response: Ok,
3783
+ status: 200,
3784
+ access: "admin",
3785
+ phase: "S4"
3786
+ },
3787
+ "signupForms.embed": {
3788
+ method: "GET",
3789
+ path: "/v1/signup-forms/:id/embed",
3790
+ params: IdParams,
3791
+ response: SignupFormEmbed,
3792
+ status: 200,
3793
+ access: "read",
3794
+ phase: "S4"
3795
+ },
3796
+ /**
3797
+ * The public submission, as JSON (the optional embed script and custom
3798
+ * frontends). Answers 202 for every accepted-looking submission, whatever
3799
+ * happens next, so it discloses nobody's membership.
3800
+ */
3801
+ "signupForms.submit": {
3802
+ method: "POST",
3803
+ path: "/v1/signup-forms/:id/submit",
3804
+ params: IdParams,
3805
+ body: SignupSubmission,
3806
+ response: Ok,
3807
+ status: 202,
3808
+ access: "public",
3809
+ phase: "S4"
3810
+ },
3811
+ /** Uploads the CSV (`file` field, UTF-8, comma or semicolon separated, a header row). */
3812
+ "imports.create": {
3813
+ method: "POST",
3814
+ path: "/v1/imports",
3815
+ multipart: { fileField: "file" },
3816
+ response: ImportJob,
3817
+ status: 201,
3818
+ access: "write",
3819
+ phase: "S4"
3820
+ },
3821
+ "imports.list": {
3822
+ method: "GET",
3823
+ path: "/v1/imports",
3824
+ query: PageQuery,
3825
+ response: list(ImportJob),
3826
+ status: 200,
3827
+ access: "read",
3828
+ phase: "S4"
3829
+ },
3830
+ "imports.get": { method: "GET", path: "/v1/imports/:id", params: IdParams, response: ImportJob, status: 200, access: "read", phase: "S4" },
3831
+ /** Sets or revises the mapping and starts its dry run (a revision discards the previous dry run). */
3832
+ "imports.setMapping": {
3833
+ method: "PUT",
3834
+ path: "/v1/imports/:id/mapping",
3835
+ params: IdParams,
3836
+ body: ImportMappingRequest,
3837
+ response: ImportJob,
3838
+ status: 202,
3839
+ access: "write",
3840
+ phase: "S4"
3841
+ },
3842
+ "imports.commit": {
3843
+ method: "POST",
3844
+ path: "/v1/imports/:id/commit",
3845
+ params: IdParams,
3846
+ body: ImportCommitRequest,
3847
+ response: ImportJob,
3848
+ status: 202,
3849
+ access: "write",
3850
+ phase: "S4"
3851
+ },
3852
+ "imports.cancel": {
3853
+ method: "POST",
3854
+ path: "/v1/imports/:id/cancel",
3855
+ params: IdParams,
3856
+ body: MailingActionRequest,
3857
+ response: ImportJob,
3858
+ status: 200,
3859
+ access: "write",
3860
+ phase: "S4"
3861
+ },
3862
+ "imports.rows": {
3863
+ method: "GET",
3864
+ path: "/v1/imports/:id/rows",
3865
+ params: IdParams,
3866
+ query: ImportRowListQuery,
3867
+ response: list(ImportRow),
3868
+ status: 200,
3869
+ access: "read",
3870
+ phase: "S4"
3871
+ },
3872
+ "tracking.get": {
3873
+ method: "GET",
3874
+ path: "/v1/workspace/tracking",
3875
+ response: TrackingSettings,
3876
+ status: 200,
3877
+ access: "read",
3878
+ phase: "S4"
3879
+ },
3880
+ "tracking.update": {
3881
+ method: "PUT",
3882
+ path: "/v1/workspace/tracking",
3883
+ body: TrackingSettings,
3884
+ response: TrackingSettings,
3885
+ status: 200,
3886
+ access: "admin",
3887
+ phase: "S4"
3888
+ }
3889
+ };
3890
+ var billingRoutes = {
3891
+ "billing.plans": {
3892
+ method: "GET",
3893
+ path: "/v1/billing/plans",
3894
+ response: z14.object({ data: z14.array(CatalogPlan) }),
3895
+ status: 200,
3896
+ access: "read",
3897
+ phase: "S5"
3898
+ },
3899
+ "billing.subscription": {
3900
+ method: "GET",
3901
+ path: "/v1/billing/subscription",
3902
+ response: Subscription,
3903
+ status: 200,
3904
+ access: "admin",
3905
+ phase: "S5"
3906
+ },
3907
+ "billing.usage": { method: "GET", path: "/v1/billing/usage", response: Usage, status: 200, access: "read", phase: "S5" },
3908
+ "billing.checkout": {
3909
+ method: "POST",
3910
+ path: "/v1/billing/checkout",
3911
+ body: CheckoutRequest,
3912
+ response: CheckoutSession,
3913
+ status: 200,
3914
+ access: "admin",
3915
+ phase: "S5"
3916
+ },
3917
+ "billing.portal": {
3918
+ method: "POST",
3919
+ path: "/v1/billing/portal",
3920
+ body: PortalRequest,
3921
+ response: PortalSession,
3922
+ status: 200,
3923
+ access: "admin",
3924
+ phase: "S5"
3925
+ }
3926
+ };
3927
+ var inviteRoutes = {
3928
+ "invites.create": {
3929
+ method: "POST",
3930
+ path: "/v1/invites",
3931
+ body: InviteCreate,
3932
+ response: InviteCreated,
3933
+ status: 201,
3934
+ access: "admin",
3935
+ phase: "S3"
3936
+ },
3937
+ "invites.list": {
3938
+ method: "GET",
3939
+ path: "/v1/invites",
3940
+ query: InviteListQuery,
3941
+ response: list(Invite),
3942
+ status: 200,
3943
+ access: "admin",
3944
+ phase: "S3"
3945
+ },
3946
+ "invites.revoke": {
3947
+ method: "DELETE",
3948
+ path: "/v1/invites/:id",
3949
+ params: IdParams,
3950
+ response: Invite,
3951
+ status: 200,
3952
+ access: "admin",
3953
+ phase: "S3"
3954
+ },
3955
+ "invites.accept": {
3956
+ method: "POST",
3957
+ path: "/v1/invites/accept",
3958
+ body: InviteAccept,
3959
+ response: InviteAccepted,
3960
+ status: 200,
3961
+ access: "dashboard",
3962
+ phase: "S3"
3963
+ }
3964
+ };
3965
+ var automationRoutes = {
3966
+ "automations.list": {
3967
+ method: "GET",
3968
+ path: "/v1/automations",
3969
+ query: AutomationListQuery,
3970
+ response: list(AutomationSummary),
3971
+ status: 200,
3972
+ access: "read",
3973
+ phase: "A1"
3974
+ },
3975
+ "automations.create": {
3976
+ method: "POST",
3977
+ path: "/v1/automations",
3978
+ body: AutomationCreate,
3979
+ response: AutomationWithFlow,
3980
+ status: 201,
3981
+ access: "write",
3982
+ phase: "A1"
3983
+ },
3984
+ "automations.get": {
3985
+ method: "GET",
3986
+ path: "/v1/automations/:id",
3987
+ params: IdParams,
3988
+ response: AutomationWithFlow,
3989
+ status: 200,
3990
+ access: "read",
3991
+ phase: "A1"
3992
+ },
3993
+ "automations.update": {
3994
+ method: "PATCH",
3995
+ path: "/v1/automations/:id",
3996
+ params: IdParams,
3997
+ body: AutomationUpdate,
3998
+ response: AutomationWithFlow,
3999
+ status: 200,
4000
+ access: "write",
4001
+ phase: "A1"
4002
+ },
4003
+ "automations.delete": {
4004
+ method: "DELETE",
4005
+ path: "/v1/automations/:id",
4006
+ params: IdParams,
4007
+ response: Ok,
4008
+ status: 200,
4009
+ access: "write",
4010
+ phase: "A1"
4011
+ },
4012
+ "automations.validate": {
4013
+ method: "POST",
4014
+ path: "/v1/automations/:id/validate",
4015
+ params: IdParams,
4016
+ body: AutomationValidateRequest,
4017
+ response: AutomationValidation,
4018
+ status: 200,
4019
+ access: "read",
4020
+ phase: "A1"
4021
+ },
4022
+ "automations.publish": {
4023
+ method: "POST",
4024
+ path: "/v1/automations/:id/publish",
4025
+ params: IdParams,
4026
+ body: AutomationPublishRequest,
4027
+ response: AutomationPublishResult,
4028
+ status: 200,
4029
+ access: "write",
4030
+ phase: "A1"
4031
+ },
4032
+ "automations.pause": {
4033
+ method: "POST",
4034
+ path: "/v1/automations/:id/pause",
4035
+ params: IdParams,
4036
+ body: z14.object({}).strict(),
4037
+ response: Automation,
4038
+ status: 200,
4039
+ access: "write",
4040
+ phase: "A1"
4041
+ },
4042
+ "automations.resume": {
4043
+ method: "POST",
4044
+ path: "/v1/automations/:id/resume",
4045
+ params: IdParams,
4046
+ body: z14.object({}).strict(),
4047
+ response: Automation,
4048
+ status: 200,
4049
+ access: "write",
4050
+ phase: "A1"
4051
+ },
4052
+ "automations.archive": {
4053
+ method: "POST",
4054
+ path: "/v1/automations/:id/archive",
4055
+ params: IdParams,
4056
+ body: z14.object({}).strict(),
4057
+ response: Automation,
4058
+ status: 200,
4059
+ access: "write",
4060
+ phase: "A1"
4061
+ },
4062
+ "automations.duplicate": {
4063
+ method: "POST",
4064
+ path: "/v1/automations/:id/duplicate",
4065
+ params: IdParams,
4066
+ body: AutomationDuplicateRequest,
4067
+ response: AutomationWithFlow,
4068
+ status: 201,
4069
+ access: "write",
4070
+ phase: "A1"
4071
+ },
4072
+ "automations.enroll": {
4073
+ method: "POST",
4074
+ path: "/v1/automations/:id/enroll",
4075
+ params: IdParams,
4076
+ body: AutomationEnrollRequest,
4077
+ response: AutomationEnrollResult,
4078
+ status: 200,
4079
+ access: "write",
4080
+ phase: "A1"
4081
+ },
4082
+ "automations.testEmail": {
4083
+ method: "POST",
4084
+ path: "/v1/automations/:id/steps/:step_id/test",
4085
+ params: AutomationStepParams,
4086
+ body: AutomationTestEmailRequest,
4087
+ response: MailingTestResult,
4088
+ status: 200,
4089
+ access: "write",
4090
+ phase: "A1"
4091
+ },
4092
+ "automations.runs": {
4093
+ method: "GET",
4094
+ path: "/v1/automations/:id/runs",
4095
+ params: IdParams,
4096
+ query: AutomationRunListQuery,
4097
+ response: list(AutomationRun),
4098
+ status: 200,
4099
+ access: "read",
4100
+ phase: "A1"
4101
+ },
4102
+ "automations.run": {
4103
+ method: "GET",
4104
+ path: "/v1/automations/:id/runs/:run_id",
4105
+ params: AutomationRunParams,
4106
+ response: AutomationJourney,
4107
+ status: 200,
4108
+ access: "read",
4109
+ phase: "A1"
4110
+ },
4111
+ "automations.exitRun": {
4112
+ method: "POST",
4113
+ path: "/v1/automations/:id/runs/:run_id/exit",
4114
+ params: AutomationRunParams,
4115
+ body: AutomationExitRunRequest,
4116
+ response: AutomationRun,
4117
+ status: 200,
4118
+ access: "write",
4119
+ phase: "A1"
4120
+ },
4121
+ "automations.report": {
4122
+ method: "GET",
4123
+ path: "/v1/automations/:id/report",
4124
+ params: IdParams,
4125
+ response: AutomationReport,
4126
+ status: 200,
4127
+ access: "read",
4128
+ phase: "A1"
4129
+ },
4130
+ /**
4131
+ * Every automation one contact has been through, newest first. It sits with
4132
+ * the automations because that is the surface it belongs to, though its path
4133
+ * hangs off the contact, which is where a person asks the question.
4134
+ */
4135
+ "contacts.automations": {
4136
+ method: "GET",
4137
+ path: "/v1/contacts/:id/automations",
4138
+ params: IdParams,
4139
+ query: ContactAutomationListQuery,
4140
+ response: list(ContactAutomationRun),
4141
+ status: 200,
4142
+ access: "read",
4143
+ phase: "A1"
4144
+ },
4145
+ "events.create": {
4146
+ method: "POST",
4147
+ path: "/v1/events",
4148
+ body: EventCreate,
4149
+ response: Event,
4150
+ status: 201,
4151
+ access: "write",
4152
+ phase: "A1"
4153
+ },
4154
+ "events.list": {
4155
+ method: "GET",
4156
+ path: "/v1/events",
4157
+ query: EventListQuery,
4158
+ response: list(Event),
4159
+ status: 200,
4160
+ access: "read",
4161
+ phase: "A1"
4162
+ }
4163
+ };
4164
+ var routes = {
4165
+ ...foundationRoutes,
4166
+ ...templateRoutes,
4167
+ ...sendingRoutes,
4168
+ ...inviteRoutes,
4169
+ ...platformRoutes,
4170
+ ...billingRoutes,
4171
+ ...automationRoutes
4172
+ };
4173
+ function acceptsIdempotencyKey(route) {
4174
+ return route.method !== "GET";
4175
+ }
4176
+ function buildPath(path, params = {}) {
4177
+ return path.replace(/:([a-z_]+)/g, (_, name) => {
4178
+ const value = params[name];
4179
+ if (value === void 0 || value === "") throw new Error(`missing path parameter "${name}" for ${path}`);
4180
+ return encodeURIComponent(String(value));
4181
+ });
4182
+ }
4183
+ function matchRoute(method, pathname) {
4184
+ for (const [id, route] of Object.entries(routes)) {
4185
+ if (route.method !== method) continue;
4186
+ const names = [];
4187
+ const pattern = new RegExp(
4188
+ "^" + route.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-z_]+)/g, (_, name) => {
4189
+ names.push(name);
4190
+ return "([^/]+)";
4191
+ }) + "$"
4192
+ );
4193
+ const match = pattern.exec(pathname);
4194
+ if (!match) continue;
4195
+ const params = {};
4196
+ names.forEach((name, i) => {
4197
+ params[name] = decodeURIComponent(match[i + 1]);
4198
+ });
4199
+ return { id, params };
4200
+ }
4201
+ return null;
4202
+ }
4203
+ export {
4204
+ AB_TEST_STATUSES,
4205
+ AB_WINNER_METRICS,
4206
+ API_KEY_SCOPES,
4207
+ API_VERSION_PREFIX,
4208
+ ASSET_CONTENT_TYPES,
4209
+ ASSET_POLICIES,
4210
+ ASSET_UPLOAD_FIELD,
4211
+ AUDIT_ACTIONS,
4212
+ AUTHORIZATION_HEADER,
4213
+ AUTOMATION_DATE_TRIGGER_KINDS,
4214
+ AUTOMATION_END_REASONS,
4215
+ AUTOMATION_ENTRY_REFUSALS,
4216
+ AUTOMATION_FILTER_FIELD_OPERATORS,
4217
+ AUTOMATION_FILTER_FIELD_PATTERN,
4218
+ AUTOMATION_ISSUE_CODES,
4219
+ AUTOMATION_STATUSES,
4220
+ AUTOMATION_STEP_KINDS,
4221
+ AUTOMATION_TRIGGER_KINDS,
4222
+ AUTOMATION_WAIT_REASONS,
4223
+ AbTestConfig,
4224
+ AbTestState,
4225
+ AbVariant,
4226
+ AbWinnerRequest,
4227
+ ApiKey,
4228
+ ApiKeyCreate,
4229
+ ApiKeyCreated,
4230
+ ApiKeyScope,
4231
+ Asset,
4232
+ AssetContentType,
4233
+ AssetImport,
4234
+ AssetPolicy,
4235
+ AuditAction,
4236
+ AuditActor,
4237
+ AuditEntry,
4238
+ AuditQuery,
4239
+ Automation,
4240
+ AutomationCounts,
4241
+ AutomationCreate,
4242
+ AutomationDuplicateRequest,
4243
+ AutomationEndReason,
4244
+ AutomationEnrollRequest,
4245
+ AutomationEnrollResult,
4246
+ AutomationEnrollment,
4247
+ AutomationEntryRefusal,
4248
+ AutomationExitRunRequest,
4249
+ AutomationFilter,
4250
+ AutomationFilterCondition,
4251
+ AutomationFilterField,
4252
+ AutomationFlow,
4253
+ AutomationIssue,
4254
+ AutomationIssueCode,
4255
+ AutomationJourney,
4256
+ AutomationListQuery,
4257
+ AutomationPublishMove,
4258
+ AutomationPublishRequest,
4259
+ AutomationPublishResult,
4260
+ AutomationReport,
4261
+ AutomationRun,
4262
+ AutomationRunCompletedData,
4263
+ AutomationRunExitedData,
4264
+ AutomationRunFailedData,
4265
+ AutomationRunListQuery,
4266
+ AutomationRunParams,
4267
+ AutomationRunStartedData,
4268
+ AutomationRunStatus,
4269
+ AutomationSettings,
4270
+ AutomationStatus,
4271
+ AutomationStep,
4272
+ AutomationStepExecution,
4273
+ AutomationStepKind,
4274
+ AutomationStepParams,
4275
+ AutomationStepReport,
4276
+ AutomationSummary,
4277
+ AutomationTestEmailRequest,
4278
+ AutomationTrigger,
4279
+ AutomationTriggerKind,
4280
+ AutomationUpdate,
4281
+ AutomationValidateRequest,
4282
+ AutomationValidation,
4283
+ AutomationWaitReason,
4284
+ AutomationWebhookData,
4285
+ AutomationWithFlow,
4286
+ BEARER_PREFIX,
4287
+ BILLING_NOT_CONFIGURED_REASON,
4288
+ BoundedAutomationFilter,
4289
+ BoundedSegmentFilter,
4290
+ CONTACT_PROPERTY_TYPES,
4291
+ CatalogPlan,
4292
+ CheckoutRequest,
4293
+ CheckoutSession,
4294
+ CompanyId,
4295
+ CompileMessage,
4296
+ CompileRequest,
4297
+ CompileResult,
4298
+ ConditionStep,
4299
+ Contact,
4300
+ ContactAutomationListQuery,
4301
+ ContactAutomationRun,
4302
+ ContactBouncedData,
4303
+ ContactErased,
4304
+ ContactListQuery,
4305
+ ContactPropertyDefinition,
4306
+ ContactPropertyKey,
4307
+ ContactPropertyParams,
4308
+ ContactPropertyType,
4309
+ ContactResubscribedData,
4310
+ ContactSubscribedData,
4311
+ ContactTaggedData,
4312
+ ContactUnsubscribedData,
4313
+ ContactUpdatedData,
4314
+ ContactUpsert,
4315
+ ContactUpsertResult,
4316
+ DATE_PASSED_ANSWERS,
4317
+ DEFAULT_INVITE_TTL_DAYS,
4318
+ DEFAULT_PAGE_LIMIT,
4319
+ DOCUMENT_SCHEMA_VERSIONS,
4320
+ DateOnly,
4321
+ DatePassedAnswer,
4322
+ DelayStep,
4323
+ DocumentSchemaVersion,
4324
+ EDITABLE_MAILING_STATUSES,
4325
+ ERROR_CODES,
4326
+ ERROR_STATUS,
4327
+ EXPORT_CONTENT_TYPES,
4328
+ EXPORT_FORMATS,
4329
+ EXPORT_WARNINGS_HEADER,
4330
+ EXPORT_WARNING_COUNT_HEADER,
4331
+ Email,
4332
+ EmailStep,
4333
+ ErrorBody,
4334
+ ErrorCodeSchema,
4335
+ Event,
4336
+ EventCreate,
4337
+ EventListQuery,
4338
+ EventName,
4339
+ ExportFormat,
4340
+ FILTER_FIELD_OPERATORS,
4341
+ FILTER_OPERATORS,
4342
+ FilterCondition,
4343
+ FilterField,
4344
+ FilterOperator,
4345
+ GotoStep,
4346
+ HEALTH_PATH,
4347
+ HOSTED_PAGE_LOCALES,
4348
+ ICLOUD_SMTP_POLICY,
4349
+ IDEMPOTENCY_KEY_HEADER,
4350
+ IDEMPOTENCY_KEY_MAX_LENGTH,
4351
+ IDEMPOTENCY_KEY_RETENTION_HOURS,
4352
+ IMPORT_FILE_FIELD,
4353
+ IMPORT_ROW_OUTCOMES,
4354
+ IMPORT_SKIP_REASONS,
4355
+ IMPORT_STATUSES,
4356
+ IMPORT_TERMINAL_STATUSES,
4357
+ INVITE_REFUSAL_REASONS,
4358
+ INVITE_STATUSES,
4359
+ Id,
4360
+ IdParams,
4361
+ ImportColumnMapping,
4362
+ ImportCommitRequest,
4363
+ ImportFinishedData,
4364
+ ImportJob,
4365
+ ImportMappingRequest,
4366
+ ImportReport,
4367
+ ImportRow,
4368
+ ImportRowListQuery,
4369
+ ImportRowOutcome,
4370
+ ImportSkipReason,
4371
+ ImportStatus,
4372
+ ImportWarning,
4373
+ ImportedAsset,
4374
+ Invite,
4375
+ InviteAccept,
4376
+ InviteAccepted,
4377
+ InviteCreate,
4378
+ InviteCreated,
4379
+ InviteListQuery,
4380
+ InviteStatus,
4381
+ JsonValue,
4382
+ LIST_UNSUBSCRIBE_POST_VALUE,
4383
+ MAILING_ACTIONS,
4384
+ MAILING_KINDS,
4385
+ MAILING_STATUSES,
4386
+ MAILING_TRANSITIONS,
4387
+ MAX_ASSET_BYTES,
4388
+ MAX_DELAY_SECONDS,
4389
+ MAX_DOCUMENT_BYTES,
4390
+ MAX_ENGAGEMENT_DAYS,
4391
+ MAX_EXPORT_WARNINGS_HEADER_LENGTH,
4392
+ MAX_FILTER_DEPTH,
4393
+ MAX_IMPORTED_REMOTE_IMAGES,
4394
+ MAX_IMPORT_BYTES,
4395
+ MAX_IMPORT_ROWS,
4396
+ MAX_IMPORT_URL_LENGTH,
4397
+ MAX_INVITE_TTL_DAYS,
4398
+ MAX_IN_LIST,
4399
+ MAX_MJML_DEPTH,
4400
+ MAX_MJML_ELEMENTS,
4401
+ MAX_MJML_IMPORT_BYTES,
4402
+ MAX_PAGE_LIMIT,
4403
+ MAX_RECIPIENTS_PER_BATCH,
4404
+ MAX_RE_ENTRY_COOLDOWN_SECONDS,
4405
+ MAX_SAVED_SECTIONS,
4406
+ MAX_SPLIT_BRANCHES,
4407
+ MAX_STEPS,
4408
+ MAX_STEP_VISITS,
4409
+ MAX_TRIGGERS,
4410
+ MEMBER_ROLES,
4411
+ MERGE_FIELD_PATTERN,
4412
+ MESSAGE_OUTCOMES,
4413
+ MIN_SPLIT_BRANCHES,
4414
+ MJML_IMPORT_REFUSALS,
4415
+ Mailing,
4416
+ MailingAbWinnerSelectedData,
4417
+ MailingAction,
4418
+ MailingActionRequest,
4419
+ MailingAnalytics,
4420
+ MailingAudienceFromSegment,
4421
+ MailingCounts,
4422
+ MailingCreate,
4423
+ MailingExportQuery,
4424
+ MailingFinishedData,
4425
+ MailingKind,
4426
+ MailingListQuery,
4427
+ MailingMetadata,
4428
+ MailingRetryFailedRequest,
4429
+ MailingScheduleFailedData,
4430
+ MailingScheduleRequest,
4431
+ MailingScheduledData,
4432
+ MailingSendRequest,
4433
+ MailingStartedData,
4434
+ MailingStatus,
4435
+ MailingSummary,
4436
+ MailingTestRequest,
4437
+ MailingTestResult,
4438
+ MailingUpdate,
4439
+ Member,
4440
+ MemberCreate,
4441
+ MemberRole,
4442
+ MemberUpdate,
4443
+ Message,
4444
+ MessageFailedData,
4445
+ MessageListQuery,
4446
+ MessageOutcome,
4447
+ MessageSentData,
4448
+ MessageSummary,
4449
+ NotifyRecipients,
4450
+ NotifyStep,
4451
+ OffsetDays,
4452
+ Ok,
4453
+ PLAN_FEATURES,
4454
+ PLAN_IDS,
4455
+ PROVIDER_KINDS,
4456
+ PageQuery,
4457
+ Plan,
4458
+ PlanFeatures,
4459
+ PlanId,
4460
+ PlanLimits,
4461
+ PortalRequest,
4462
+ PortalSession,
4463
+ Properties,
4464
+ Provider,
4465
+ ProviderCreate,
4466
+ ProviderEvents,
4467
+ ProviderEventsSecret,
4468
+ ProviderKind,
4469
+ ProviderPolicy,
4470
+ ProviderRejections,
4471
+ ProviderUpdate,
4472
+ ProviderUsage,
4473
+ ProviderVerifyResult,
4474
+ QuietHours,
4475
+ RECIPIENT_REJECTIONS,
4476
+ RECIPIENT_STATUSES,
4477
+ REQUEST_ID_HEADER,
4478
+ RESEND_EVENT_TYPES,
4479
+ RESERVED_MERGE_FIELDS,
4480
+ RETRYABLE_ERRORS,
4481
+ RETRY_AFTER_HEADER,
4482
+ RE_ENTRY_MODES,
4483
+ ReEntryMode,
4484
+ Recipient,
4485
+ RecipientBatch,
4486
+ RecipientBatchResult,
4487
+ RecipientInput,
4488
+ RecipientListQuery,
4489
+ RecipientStatus,
4490
+ RemoteImage,
4491
+ ResendSigningSecret,
4492
+ SIGNUP_FORM_FIELDS,
4493
+ SKIP_REASONS,
4494
+ SMTP_SECURITY,
4495
+ SUBJECT_HEADER,
4496
+ SUPPRESSION_REASONS,
4497
+ SavedSection,
4498
+ SavedSectionBody,
4499
+ SavedSectionCreate,
4500
+ SavedSectionUpdate,
4501
+ Segment,
4502
+ SegmentCreate,
4503
+ SegmentFilter,
4504
+ SegmentPreview,
4505
+ SegmentPreviewRequest,
4506
+ SignupForm,
4507
+ SignupFormCreate,
4508
+ SignupFormEmbed,
4509
+ SignupFormTranslation,
4510
+ SignupSubmission,
4511
+ SkipReason,
4512
+ Slug,
4513
+ SplitBranchKey,
4514
+ SplitStep,
4515
+ StepId,
4516
+ Subscription,
4517
+ Suppression,
4518
+ SuppressionCreate,
4519
+ SuppressionListQuery,
4520
+ SuppressionReason,
4521
+ TERMINAL_MAILING_STATUSES,
4522
+ Tag,
4523
+ TagAssignment,
4524
+ TagCreate,
4525
+ Template,
4526
+ TemplateCompileRequest,
4527
+ TemplateCreate,
4528
+ TemplateDocument,
4529
+ TemplateExportQuery,
4530
+ TemplateImport,
4531
+ TemplateImportPreview,
4532
+ TemplateImportPreviewRequest,
4533
+ TemplateImportResult,
4534
+ TemplateListQuery,
4535
+ TemplateSummary,
4536
+ TemplateUpdate,
4537
+ TemplateVersion,
4538
+ TemplateVersionParams,
4539
+ TimeOfDay,
4540
+ Timestamp,
4541
+ TimezoneName,
4542
+ Topic,
4543
+ TopicCreate,
4544
+ TopicUpdate,
4545
+ TrackingSettings,
4546
+ UNSUBSCRIBE_PATH_PREFIX,
4547
+ UNSUBSCRIBE_SOURCES,
4548
+ USAGE_METRICS,
4549
+ USAGE_WARNING_HEADER,
4550
+ USAGE_WARNING_RATIO,
4551
+ Usage,
4552
+ UsageMetric,
4553
+ UsageWarning,
4554
+ ValidationIssue,
4555
+ VariantAnalytics,
4556
+ WEBHOOK_DELIVERY_STATUSES,
4557
+ WEBHOOK_EVENT_ID_HEADER,
4558
+ WEBHOOK_EVENT_TYPES,
4559
+ WEBHOOK_MAX_ATTEMPTS,
4560
+ WEBHOOK_RETRY_DELAYS_SECONDS,
4561
+ WEBHOOK_SECRET_PREFIX,
4562
+ WEBHOOK_SIGNATURE_HEADER,
4563
+ WEBHOOK_SIGNATURE_VERSION,
4564
+ WEBHOOK_TIMESTAMP_HEADER,
4565
+ WEBHOOK_TOLERANCE_SECONDS,
4566
+ WORKSPACE_HEADER,
4567
+ WebhookDelivery,
4568
+ WebhookDeliveryListQuery,
4569
+ WebhookDeliveryParams,
4570
+ WebhookDeliveryStatus,
4571
+ WebhookEndpoint,
4572
+ WebhookEndpointCreate,
4573
+ WebhookEndpointUpdate,
4574
+ WebhookEndpointWithSecret,
4575
+ WebhookEvent,
4576
+ WebhookEventType,
4577
+ WebhookStep,
4578
+ WebhookUrl,
4579
+ Weekday,
4580
+ Workspace,
4581
+ WorkspaceCreate,
4582
+ WorkspaceMembership,
4583
+ WorkspaceMove,
4584
+ WorkspaceSettings,
4585
+ WorkspaceUpdate,
4586
+ acceptsIdempotencyKey,
4587
+ alwaysWaits,
4588
+ automationFilterFamily,
4589
+ automationRoutes,
4590
+ billingRoutes,
4591
+ buildPath,
4592
+ canTransition,
4593
+ cycles,
4594
+ defaultSettings,
4595
+ emptyFlow,
4596
+ errorBody,
4597
+ exportFilename,
4598
+ filterConditions,
4599
+ filterDepth,
4600
+ filterFieldFamily,
4601
+ filterLeafProblem,
4602
+ findMergeFields,
4603
+ formatExportWarningsHeader,
4604
+ formatUsageWarningHeader,
4605
+ foundationRoutes,
4606
+ inviteRoutes,
4607
+ isDayEntirelyPast,
4608
+ isPublishable,
4609
+ isRetryableError,
4610
+ matchRoute,
4611
+ missingRequiredMergeFields,
4612
+ nowSeconds,
4613
+ page,
4614
+ parseExportWarningsHeader,
4615
+ parseUsageWarningHeader,
4616
+ platformRoutes,
4617
+ reachable,
4618
+ routes,
4619
+ sendingRoutes,
4620
+ signWebhook,
4621
+ statusForError,
4622
+ stepTargets,
4623
+ stronglyConnected,
4624
+ templateRoutes,
4625
+ testGroupSize,
4626
+ tightLoops,
4627
+ timingSafeEqualString,
4628
+ triggerKey,
4629
+ validateAutomation,
4630
+ verifyWebhook
4631
+ };