@marlinjai/mail-contract 0.2.0 → 0.4.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 CHANGED
@@ -114,6 +114,8 @@ var IDEMPOTENCY_KEY_MAX_LENGTH = 255;
114
114
  var IDEMPOTENCY_KEY_RETENTION_HOURS = 24;
115
115
  var SUBJECT_HEADER = "x-mail-subject";
116
116
  var WORKSPACE_HEADER = "x-mail-workspace";
117
+ var ON_BEHALF_OF_HEADER = "x-mail-on-behalf-of";
118
+ var ON_BEHALF_OF_MAX_LENGTH = 200;
117
119
  var REQUEST_ID_HEADER = "x-request-id";
118
120
  var RETRY_AFTER_HEADER = "retry-after";
119
121
  var API_VERSION_PREFIX = "/v1";
@@ -437,7 +439,12 @@ var Mailing = z5.object({
437
439
  /** The template the document was taken from, if any. The snapshot is what is sent. */
438
440
  template_id: Id.nullable(),
439
441
  document: TemplateDocument,
440
- topic: Slug,
442
+ /**
443
+ * The topic the mailing is sent under. Null for a letter (a one-to-one
444
+ * mailing with at most one recipient, which belongs to no topic and whose
445
+ * unsubscribe link offers every topic) and for an automation's notification.
446
+ */
447
+ topic: Slug.nullable(),
441
448
  provider_id: Id,
442
449
  status: MailingStatus,
443
450
  counts: MailingCounts,
@@ -462,7 +469,13 @@ var MailingContent = {
462
469
  name: z5.string().max(200).optional(),
463
470
  subject: z5.string().min(1).max(998),
464
471
  preheader: z5.string().max(500).optional(),
465
- topic: Slug,
472
+ /**
473
+ * Omitted or null makes the mailing a letter: one recipient at most, sent to
474
+ * anybody not blocked on every topic, with an unsubscribe link that offers
475
+ * every topic and no List-Unsubscribe headers. See "Letters" in the
476
+ * contract's documentation.
477
+ */
478
+ topic: Slug.nullable().optional(),
466
479
  provider_id: Id,
467
480
  metadata: MailingMetadata.optional()
468
481
  };
@@ -478,7 +491,8 @@ var MailingUpdate = z5.object({
478
491
  name: z5.string().max(200).nullable().optional(),
479
492
  subject: MailingContent.subject.optional(),
480
493
  preheader: z5.string().max(500).nullable().optional(),
481
- topic: Slug.optional(),
494
+ /** Null turns a draft into a letter, which it can only be with at most one recipient. */
495
+ topic: Slug.nullable().optional(),
482
496
  provider_id: Id.optional(),
483
497
  metadata: MailingMetadata.optional(),
484
498
  document: TemplateDocument.optional()
@@ -526,6 +540,8 @@ var Recipient = z5.object({
526
540
  updated_at: Timestamp
527
541
  });
528
542
  var MAX_RECIPIENTS_PER_BATCH = 1e3;
543
+ var MAX_LETTER_RECIPIENTS = 1;
544
+ var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
529
545
  var RecipientInput = z5.object({
530
546
  contact_id: Id.optional(),
531
547
  external_id: z5.string().min(1).max(255).optional(),
@@ -1906,10 +1922,45 @@ var EventListQuery = PageQuery.extend({
1906
1922
  contact_id: Id.optional()
1907
1923
  });
1908
1924
 
1925
+ // src/unsubscribe.ts
1926
+ var UNSUBSCRIBE_PATH_PREFIX = "/u/";
1927
+ var LIST_UNSUBSCRIBE_POST_VALUE = "List-Unsubscribe=One-Click";
1928
+ var RESERVED_MERGE_FIELDS = {
1929
+ /** From the recipient's merge values, else the contact. Supports a fallback. */
1930
+ first_name: { source: "contact", fallback: true, required_for_broadcast: false },
1931
+ last_name: { source: "contact", fallback: true, required_for_broadcast: false },
1932
+ email: { source: "contact", fallback: false, required_for_broadcast: false },
1933
+ /** The hosted unsubscribe page for this recipient. A broadcast without it is refused. */
1934
+ unsubscribe_url: { source: "service", fallback: false, required_for_broadcast: true }
1935
+ };
1936
+ var MERGE_FIELD_PATTERN = /\{\{\s*([a-z][a-z0-9_]*)\s*(?:\|([^}]*))?\}\}/g;
1937
+ function findMergeFields(html) {
1938
+ const uses = [];
1939
+ for (const match of html.matchAll(MERGE_FIELD_PATTERN)) {
1940
+ uses.push({ name: match[1], fallback: match[2] === void 0 ? null : match[2].trim() });
1941
+ }
1942
+ return uses;
1943
+ }
1944
+ function missingRequiredMergeFields(html) {
1945
+ const used = new Set(findMergeFields(html).map((u) => u.name));
1946
+ return Object.keys(RESERVED_MERGE_FIELDS).filter(
1947
+ (name) => RESERVED_MERGE_FIELDS[name].required_for_broadcast && !used.has(name)
1948
+ );
1949
+ }
1950
+
1909
1951
  // src/workspace.ts
1910
1952
  var CompanyId = z8.string().min(1).max(64);
1911
1953
  var ASSET_POLICIES = ["any", "service_only"];
1912
1954
  var AssetPolicy = z8.enum(ASSET_POLICIES);
1955
+ var MAX_ALLOWED_ASSET_HOSTS = 20;
1956
+ var AssetHost = z8.string().trim().toLowerCase().min(1).max(253).regex(/^(?=.{1,253}(?::\d{1,5})?$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+(:\d{1,5})?$/, "a host name such as example.com");
1957
+ var MAX_MERGE_DEFAULTS = 50;
1958
+ var MAX_MERGE_DEFAULT_LENGTH = 2e3;
1959
+ var MergeFieldName = z8.string().regex(/^[a-z][a-z0-9_]*$/, "a merge-field name: lowercase letters, digits and underscores, starting with a letter").max(64);
1960
+ var MergeDefaultValue = z8.union([z8.string().max(MAX_MERGE_DEFAULT_LENGTH), z8.number().finite(), z8.boolean()]);
1961
+ var MergeDefaults = z8.record(MergeFieldName, MergeDefaultValue).refine((m) => Object.keys(m).length <= MAX_MERGE_DEFAULTS, `at most ${MAX_MERGE_DEFAULTS} merge defaults`).refine((m) => !Object.keys(m).some((k) => k in RESERVED_MERGE_FIELDS), {
1962
+ message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
1963
+ });
1913
1964
  var WorkspaceSettings = z8.object({
1914
1965
  /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
1915
1966
  default_locale: z8.string().min(2).max(35),
@@ -1919,6 +1970,14 @@ var WorkspaceSettings = z8.object({
1919
1970
  tracking_enabled: z8.boolean(),
1920
1971
  /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
1921
1972
  asset_policy: AssetPolicy,
1973
+ /**
1974
+ * Hosts a `service_only` workspace also allows, besides the service's own:
1975
+ * the workspace's website, typically. Empty by default. Ignored under `any`,
1976
+ * which allows everything already.
1977
+ */
1978
+ allowed_asset_hosts: z8.array(AssetHost).max(MAX_ALLOWED_ASSET_HOSTS),
1979
+ /** Merge values every mail of the workspace can use (see `MergeDefaults`). Empty by default. */
1980
+ merge_defaults: MergeDefaults,
1922
1981
  /**
1923
1982
  * The time zone an automation reads a wall clock in for a contact who has
1924
1983
  * none of their own (A1). Null falls back to UTC.
@@ -2020,6 +2079,7 @@ var AUDIT_ACTIONS = [
2020
2079
  "provider.anomaly_cleared",
2021
2080
  "topic.created",
2022
2081
  "topic.updated",
2082
+ "topic.deleted",
2023
2083
  "template.created",
2024
2084
  "template.updated",
2025
2085
  "template.deleted",
@@ -2087,7 +2147,16 @@ var AUDIT_ACTIONS = [
2087
2147
  var AuditAction = z8.enum(AUDIT_ACTIONS);
2088
2148
  var AuditActor = z8.discriminatedUnion("type", [
2089
2149
  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 }),
2150
+ z8.object({
2151
+ type: z8.literal("api_key"),
2152
+ api_key_id: Id,
2153
+ /**
2154
+ * Who the client's application said it was acting for, from
2155
+ * `ON_BEHALF_OF_HEADER`: reported by that application, never verified.
2156
+ * Absent when the call carried no label.
2157
+ */
2158
+ on_behalf_of: z8.string().min(1).max(ON_BEHALF_OF_MAX_LENGTH).optional()
2159
+ }),
2091
2160
  /** The service itself (the worker, a bounce, the hosted unsubscribe page). */
2092
2161
  z8.object({ type: z8.literal("system"), reason: z8.string().min(1).max(200) })
2093
2162
  ]);
@@ -2415,7 +2484,16 @@ var WEBHOOK_EVENT_TYPES = [
2415
2484
  * `webhook_events.type` is checked against it, not because an endpoint can ask
2416
2485
  * to receive everybody's.
2417
2486
  */
2418
- "automation.webhook"
2487
+ "automation.webhook",
2488
+ /**
2489
+ * E4 of the embeddable-mail plan: a template changed, so a client that
2490
+ * caches or mirrors templates (an app embedding the editor beside the
2491
+ * hosted dashboard) can invalidate. The service stays the source of truth:
2492
+ * the event carries the new version, never the document.
2493
+ */
2494
+ "template.created",
2495
+ "template.updated",
2496
+ "template.deleted"
2419
2497
  ];
2420
2498
  var WebhookEventType = z12.enum(WEBHOOK_EVENT_TYPES);
2421
2499
  var MessageEventBase = z12.object({
@@ -2577,6 +2655,15 @@ var AutomationWebhookData = AutomationRunEventBase.extend({
2577
2655
  payload: JsonValue.nullable(),
2578
2656
  fired_at: Timestamp
2579
2657
  });
2658
+ var TemplateEventBase = z12.object({
2659
+ template_id: Id,
2660
+ name: z12.string().min(1).max(200),
2661
+ version: z12.number().int().min(1),
2662
+ source: z12.enum(["api", "dashboard", "mjml_import"])
2663
+ });
2664
+ var TemplateCreatedData = TemplateEventBase.extend({ created_at: Timestamp });
2665
+ var TemplateUpdatedData = TemplateEventBase.extend({ archived: z12.boolean(), updated_at: Timestamp });
2666
+ var TemplateDeletedData = TemplateEventBase.extend({ deleted_at: Timestamp });
2580
2667
  var envelope = (type, data) => z12.object({
2581
2668
  /** Unique per event: a receiver deduplicates on it (deliveries may repeat). */
2582
2669
  id: Id,
@@ -2604,7 +2691,10 @@ var WebhookEvent = z12.discriminatedUnion("type", [
2604
2691
  envelope("automation.run_completed", AutomationRunCompletedData),
2605
2692
  envelope("automation.run_exited", AutomationRunExitedData),
2606
2693
  envelope("automation.run_failed", AutomationRunFailedData),
2607
- envelope("automation.webhook", AutomationWebhookData)
2694
+ envelope("automation.webhook", AutomationWebhookData),
2695
+ envelope("template.created", TemplateCreatedData),
2696
+ envelope("template.updated", TemplateUpdatedData),
2697
+ envelope("template.deleted", TemplateDeletedData)
2608
2698
  ]);
2609
2699
  var WebhookEndpoint = z12.object({
2610
2700
  id: Id,
@@ -2727,32 +2817,6 @@ async function verifyWebhook(input) {
2727
2817
  return matched ? { ok: true, timestamp } : { ok: false, reason: "signature_mismatch" };
2728
2818
  }
2729
2819
 
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
2820
  // src/billing.ts
2757
2821
  import { z as z13 } from "zod";
2758
2822
  var PLAN_IDS = ["free", "starter", "growth", "design_partner"];
@@ -3263,6 +3327,16 @@ var sendingRoutes = {
3263
3327
  access: "admin",
3264
3328
  phase: "S2"
3265
3329
  },
3330
+ /** Refused with `conflict` while a mailing or an automation step still uses the topic; subscriptions and suppressions on it go with it. */
3331
+ "topics.delete": {
3332
+ method: "DELETE",
3333
+ path: "/v1/topics/:id",
3334
+ params: IdParams,
3335
+ response: Ok,
3336
+ status: 200,
3337
+ access: "admin",
3338
+ phase: "S2"
3339
+ },
3266
3340
  "contacts.upsert": {
3267
3341
  method: "POST",
3268
3342
  path: "/v1/contacts",
@@ -4180,6 +4254,9 @@ function buildPath(path, params = {}) {
4180
4254
  return encodeURIComponent(String(value));
4181
4255
  });
4182
4256
  }
4257
+ function operationsWithAccess(access) {
4258
+ return Object.entries(routes).filter(([, route]) => route.access === access).map(([id]) => id).sort();
4259
+ }
4183
4260
  function matchRoute(method, pathname) {
4184
4261
  for (const [id, route] of Object.entries(routes)) {
4185
4262
  if (route.method !== method) continue;
@@ -4230,6 +4307,7 @@ export {
4230
4307
  ApiKeyScope,
4231
4308
  Asset,
4232
4309
  AssetContentType,
4310
+ AssetHost,
4233
4311
  AssetImport,
4234
4312
  AssetPolicy,
4235
4313
  AuditAction,
@@ -4379,11 +4457,13 @@ export {
4379
4457
  InviteListQuery,
4380
4458
  InviteStatus,
4381
4459
  JsonValue,
4460
+ LETTER_RECIPIENT_REFUSAL,
4382
4461
  LIST_UNSUBSCRIBE_POST_VALUE,
4383
4462
  MAILING_ACTIONS,
4384
4463
  MAILING_KINDS,
4385
4464
  MAILING_STATUSES,
4386
4465
  MAILING_TRANSITIONS,
4466
+ MAX_ALLOWED_ASSET_HOSTS,
4387
4467
  MAX_ASSET_BYTES,
4388
4468
  MAX_DELAY_SECONDS,
4389
4469
  MAX_DOCUMENT_BYTES,
@@ -4396,6 +4476,9 @@ export {
4396
4476
  MAX_IMPORT_URL_LENGTH,
4397
4477
  MAX_INVITE_TTL_DAYS,
4398
4478
  MAX_IN_LIST,
4479
+ MAX_LETTER_RECIPIENTS,
4480
+ MAX_MERGE_DEFAULTS,
4481
+ MAX_MERGE_DEFAULT_LENGTH,
4399
4482
  MAX_MJML_DEPTH,
4400
4483
  MAX_MJML_ELEMENTS,
4401
4484
  MAX_MJML_IMPORT_BYTES,
@@ -4440,6 +4523,9 @@ export {
4440
4523
  MemberCreate,
4441
4524
  MemberRole,
4442
4525
  MemberUpdate,
4526
+ MergeDefaultValue,
4527
+ MergeDefaults,
4528
+ MergeFieldName,
4443
4529
  Message,
4444
4530
  MessageFailedData,
4445
4531
  MessageListQuery,
@@ -4448,6 +4534,8 @@ export {
4448
4534
  MessageSummary,
4449
4535
  NotifyRecipients,
4450
4536
  NotifyStep,
4537
+ ON_BEHALF_OF_HEADER,
4538
+ ON_BEHALF_OF_MAX_LENGTH,
4451
4539
  OffsetDays,
4452
4540
  Ok,
4453
4541
  PLAN_FEATURES,
@@ -4525,6 +4613,8 @@ export {
4525
4613
  Template,
4526
4614
  TemplateCompileRequest,
4527
4615
  TemplateCreate,
4616
+ TemplateCreatedData,
4617
+ TemplateDeletedData,
4528
4618
  TemplateDocument,
4529
4619
  TemplateExportQuery,
4530
4620
  TemplateImport,
@@ -4534,6 +4624,7 @@ export {
4534
4624
  TemplateListQuery,
4535
4625
  TemplateSummary,
4536
4626
  TemplateUpdate,
4627
+ TemplateUpdatedData,
4537
4628
  TemplateVersion,
4538
4629
  TemplateVersionParams,
4539
4630
  TimeOfDay,
@@ -4610,6 +4701,7 @@ export {
4610
4701
  matchRoute,
4611
4702
  missingRequiredMergeFields,
4612
4703
  nowSeconds,
4704
+ operationsWithAccess,
4613
4705
  page,
4614
4706
  parseExportWarningsHeader,
4615
4707
  parseUsageWarningHeader,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.0",
6
+ "version": "0.4.0",
7
7
  "description": "The v1 API contract of the mail service: zod schemas and types for every request, response, error and webhook, the route table, and webhook signing helpers",
8
8
  "license": "MIT",
9
9
  "repository": {