riverbankcms 0.6.0 → 0.7.1

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.
@@ -79,7 +79,6 @@ const unsafeAsBookingSessionId = (id) => id;
79
79
  const unsafeAsDatedOfferingCapacityHoldId = (value) => value;
80
80
  const unsafeAsDatedOfferingCapacityHoldAuditRef = (value) => value;
81
81
  const unsafeAsDatedOfferingRevocationAuditEventId = (value) => value;
82
- const unsafeAsDatedOfferingNonCashProjectionClaimToken = (value) => value;
83
82
  const unsafeAsDatedOfferingWaitlistCapacityEffectId = (value) => value;
84
83
  const unsafeAsDatedOfferingWaitlistCapacityEffectClaimToken = (value) => value;
85
84
  const unsafeAsDatedOfferingRunCancellationOperationId = (value) => value;
@@ -103,7 +102,6 @@ const unsafeAsCheckoutLineId = (id) => id;
103
102
  const unsafeAsAttendeeId = (id) => id;
104
103
  const unsafeAsEventRegistrationCommandId = (id) => id;
105
104
  const unsafeAsEventRegistrationCommandRowId = (id) => id;
106
- const unsafeAsEventRegistrationProjectionClaimToken = (id) => id;
107
105
  const unsafeAsCustomerPassId = (id) => id;
108
106
  const unsafeAsCustomerMembershipId = (id) => id;
109
107
  const unsafeAsAppointmentId = (id) => id;
@@ -149,7 +147,6 @@ const unsafeAsParticipantId = (id) => id;
149
147
  const unsafeAsParticipantParticipationId = (id) => id;
150
148
  const unsafeAsParticipantRosterAssertionJobId = (id) => id;
151
149
  const unsafeAsParticipantBookingQuestionsTaskId = (id) => id;
152
- const unsafeAsEventAttendeeGuestId = (id) => id;
153
150
  const unsafeAsPersonRecordId = (id) => id;
154
151
  const unsafeAsEncounterRecordId = (id) => id;
155
152
  const unsafeAsRecordEntryId = (id) => id;
@@ -186,11 +183,9 @@ const parseAppointmentReservationRunId = (value) => parseUuidBrand(value, unsafe
186
183
  const parseAttendeeId = (value) => parseUuidBrand(value, unsafeAsAttendeeId);
187
184
  const parseEventRegistrationCommandId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandId);
188
185
  const parseEventRegistrationCommandRowId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandRowId);
189
- const parseEventRegistrationProjectionClaimToken = (value) => parseUuidBrand(value, unsafeAsEventRegistrationProjectionClaimToken);
190
186
  const parseDatedOfferingCapacityHoldId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldId);
191
187
  const parseDatedOfferingCapacityHoldAuditRef = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldAuditRef);
192
188
  const parseDatedOfferingRevocationAuditEventId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRevocationAuditEventId);
193
- const parseDatedOfferingNonCashProjectionClaimToken = (value) => parseUuidBrand(value, unsafeAsDatedOfferingNonCashProjectionClaimToken);
194
189
  const parseDatedOfferingWaitlistCapacityEffectId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingWaitlistCapacityEffectId);
195
190
  const parseDatedOfferingWaitlistCapacityEffectClaimToken = (value) => parseUuidBrand(value, unsafeAsDatedOfferingWaitlistCapacityEffectClaimToken);
196
191
  const parseDatedOfferingRunCancellationOperationId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRunCancellationOperationId);
@@ -814,20 +809,20 @@ function parseOptionAvailability(input) {
814
809
  function parseEventPricingOption(input) {
815
810
  const identity = parseOptionIdentity(input);
816
811
  const commercials = parseOptionCommercials(input);
817
- const availability = parseOptionAvailability(input);
812
+ const availability$1 = parseOptionAvailability(input);
818
813
  const issues = [
819
814
  identity,
820
815
  commercials,
821
- availability
816
+ availability$1
822
817
  ].flatMap((result) => result.ok ? [] : [result.error]);
823
- if (!identity.ok || !commercials.ok || !availability.ok) {
818
+ if (!identity.ok || !commercials.ok || !availability$1.ok) {
824
819
  const nonEmptyIssues = fromReadonlyArray(issues);
825
820
  return err(nonEmptyIssues.ok ? nonEmptyIssues.value : oneIssue({ kind: "invalid_object" }));
826
821
  }
827
822
  return ok({
828
823
  ...identity.value,
829
824
  ...commercials.value,
830
- ...availability.value
825
+ ...availability$1.value
831
826
  });
832
827
  }
833
828
  function unexpectedValues(input, fields) {
@@ -1303,7 +1298,7 @@ function parseEventPageSurfaceDestinationMode(value) {
1303
1298
  return value === "cms-managed" || value === "system-managed" ? value : undefined;
1304
1299
  }
1305
1300
  function parseEventPageSurfaceSubject(value) {
1306
- if (!isRecord$6(value)) return err({ kind: "notObject" });
1301
+ if (!isRecord$9(value)) return err({ kind: "notObject" });
1307
1302
  switch (value.kind) {
1308
1303
  case "eventSeries": {
1309
1304
  const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
@@ -1373,7 +1368,7 @@ function parseEventSeriesIdList(values) {
1373
1368
  }
1374
1369
  return ok(parsed);
1375
1370
  }
1376
- function isRecord$6(value) {
1371
+ function isRecord$9(value) {
1377
1372
  return typeof value === "object" && value !== null && !Array.isArray(value);
1378
1373
  }
1379
1374
 
@@ -2575,41 +2570,14 @@ function parseParticipantCount(value) {
2575
2570
  }
2576
2571
  return ok(unsafeAsParticipantCount(value));
2577
2572
  }
2578
- function provisionalEventGuestParticipantIdentity(input) {
2579
- return {
2580
- kind: "event_guest",
2581
- eventAttendeeGuestId: input.eventAttendeeGuestId,
2582
- slotKey: input.slotKey
2583
- };
2584
- }
2585
- const provisionalParticipantIdentityEncoders = { event_guest: (identity) => ({
2586
- kind: identity.kind,
2587
- source: "event_guest_backfill",
2588
- eventAttendeeGuestId: identity.eventAttendeeGuestId,
2589
- slotKey: identity.slotKey
2590
- }) };
2591
- function encodeProvisionalParticipantIdentity(identity) {
2592
- return provisionalParticipantIdentityEncoders[identity.kind](identity);
2593
- }
2594
- function parseProvisionalParticipantIdentity(value) {
2595
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2596
- return err({ kind: "not_an_object" });
2597
- }
2598
- const metadata = value;
2599
- const kind = metadata.kind ?? metadata.source;
2600
- if (kind !== "event_guest" && kind !== "event_guest_backfill") {
2601
- return err({ kind: "unsupported_identity_kind" });
2602
- }
2603
- if (typeof metadata.eventAttendeeGuestId !== "string") {
2604
- return err({ kind: "missing_event_attendee_guest_id" });
2605
- }
2606
- if (typeof metadata.slotKey !== "string") {
2607
- return err({ kind: "missing_slot_key" });
2608
- }
2609
- return ok(provisionalEventGuestParticipantIdentity({
2610
- eventAttendeeGuestId: unsafeAsEventAttendeeGuestId(metadata.eventAttendeeGuestId),
2611
- slotKey: unsafeAsParticipantSlotKey(metadata.slotKey)
2612
- }));
2573
+ /**
2574
+ * The in-memory planning key for one guest place on an occurrence registration.
2575
+ *
2576
+ * Persistence uses the participation row and its explicit guest ordinal. This
2577
+ * key only lets pure roster planning correlate issues and resolutions.
2578
+ */
2579
+ function eventGuestRosterSlotKey(input) {
2580
+ return unsafeAsParticipantSlotKey(`event_guest:${input.attendeeId}:${input.guestIndex}`);
2613
2581
  }
2614
2582
  function deriveParticipantDisplayLabel(input) {
2615
2583
  const displayName = trimmedStringOrNull(input.displayName);
@@ -2735,12 +2703,15 @@ function projectEventRegistrationParticipantRoster(input) {
2735
2703
  }
2736
2704
  });
2737
2705
  const guestSlots = input.guests.map((guest) => buildSlot({
2738
- slotKey: unsafeAsParticipantSlotKey(`event_guest:${guest.guestId}`),
2706
+ slotKey: eventGuestRosterSlotKey({
2707
+ attendeeId: input.attendeeId,
2708
+ guestIndex: guest.guestIndex
2709
+ }),
2739
2710
  contact: guest,
2740
2711
  participation: {
2741
2712
  kind: "event_guest",
2742
2713
  attendeeId: input.attendeeId,
2743
- guestId: guest.guestId
2714
+ guestIndex: guest.guestIndex
2744
2715
  }
2745
2716
  }));
2746
2717
  const slots = [bookerSlot, ...guestSlots];
@@ -3239,7 +3210,7 @@ const knownSocialPlatforms = [
3239
3210
  "x",
3240
3211
  "youtube"
3241
3212
  ];
3242
- function isRecord$5(value) {
3213
+ function isRecord$8(value) {
3243
3214
  return typeof value === "object" && value !== null && !Array.isArray(value);
3244
3215
  }
3245
3216
  function trimmedOptionalString(value) {
@@ -3249,7 +3220,7 @@ function trimmedOptionalString(value) {
3249
3220
  return trimmed.length > 0 ? trimmed : null;
3250
3221
  }
3251
3222
  function parsePublicContactDetails(value) {
3252
- if (!isRecord$5(value)) {
3223
+ if (!isRecord$8(value)) {
3253
3224
  return err({
3254
3225
  code: "invalid_shape",
3255
3226
  path: "contact",
@@ -3345,7 +3316,7 @@ function minutesSinceMidnight(value) {
3345
3316
  }
3346
3317
  function parseOpeningHoursDay(value, expectedWeekday, index) {
3347
3318
  const path = `openingHours.days.${index}`;
3348
- if (!isRecord$5(value) || value.weekday !== expectedWeekday) {
3319
+ if (!isRecord$8(value) || value.weekday !== expectedWeekday) {
3349
3320
  return err({
3350
3321
  code: "invalid_weekdays",
3351
3322
  path,
@@ -3366,7 +3337,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
3366
3337
  const intervals = [];
3367
3338
  for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
3368
3339
  const intervalPath = `${path}.intervals.${intervalIndex}`;
3369
- if (!isRecord$5(rawInterval)) {
3340
+ if (!isRecord$8(rawInterval)) {
3370
3341
  return err({
3371
3342
  code: "invalid_interval",
3372
3343
  path: intervalPath,
@@ -3416,7 +3387,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
3416
3387
  });
3417
3388
  }
3418
3389
  function parseWeeklyOpeningHours(value) {
3419
- if (!isRecord$5(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
3390
+ if (!isRecord$8(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
3420
3391
  return err({
3421
3392
  code: "invalid_weekdays",
3422
3393
  path: "openingHours.days",
@@ -3496,7 +3467,7 @@ function parsePublicSocialLinks(value) {
3496
3467
  const ids = new Set();
3497
3468
  for (const [index, rawLink] of value.entries()) {
3498
3469
  const path = `socialLinks.${index}`;
3499
- if (!isRecord$5(rawLink)) {
3470
+ if (!isRecord$8(rawLink)) {
3500
3471
  return err({
3501
3472
  code: "invalid_social_link",
3502
3473
  path,
@@ -3543,7 +3514,7 @@ function parsePublicSocialLinks(value) {
3543
3514
  return ok(links);
3544
3515
  }
3545
3516
  function parsePublicProfile(value) {
3546
- if (!isRecord$5(value)) {
3517
+ if (!isRecord$8(value)) {
3547
3518
  return err({
3548
3519
  code: "invalid_shape",
3549
3520
  path: "",
@@ -3556,23 +3527,105 @@ function parsePublicProfile(value) {
3556
3527
  if (!openingHours.ok) return openingHours;
3557
3528
  const socialLinks = parsePublicSocialLinks(value.socialLinks);
3558
3529
  if (!socialLinks.ok) return socialLinks;
3530
+ const addressVisibility = parseAddressVisibility(value.addressVisibility);
3531
+ if (!addressVisibility.ok) return addressVisibility;
3532
+ const contactVisibility = parseContactVisibility(value.contactVisibility);
3533
+ if (!contactVisibility.ok) return contactVisibility;
3534
+ const premises = parsePremises(value.premises);
3535
+ if (!premises.ok) return premises;
3559
3536
  return ok({
3560
3537
  contact: contact.value,
3538
+ contactVisibility: contactVisibility.value,
3561
3539
  openingHours: openingHours.value,
3562
- socialLinks: socialLinks.value
3540
+ socialLinks: socialLinks.value,
3541
+ addressVisibility: addressVisibility.value,
3542
+ premises: premises.value
3543
+ });
3544
+ }
3545
+ function parseFieldVisibility(value, path) {
3546
+ if (value === undefined || value === null) return ok("shown");
3547
+ if (value === "shown" || value === "hidden") return ok(value);
3548
+ return err({
3549
+ code: "invalid_contact_visibility",
3550
+ path,
3551
+ message: "Contact visibility must be shown or hidden."
3552
+ });
3553
+ }
3554
+ function parseContactVisibility(value) {
3555
+ if (value === undefined || value === null) {
3556
+ return ok({
3557
+ phone: "shown",
3558
+ email: "shown"
3559
+ });
3560
+ }
3561
+ if (!isRecord$8(value)) {
3562
+ return err({
3563
+ code: "invalid_contact_visibility",
3564
+ path: "contactVisibility",
3565
+ message: "Contact visibility must be an object."
3566
+ });
3567
+ }
3568
+ const phone = parseFieldVisibility(value.phone, "contactVisibility.phone");
3569
+ if (!phone.ok) return phone;
3570
+ const email = parseFieldVisibility(value.email, "contactVisibility.email");
3571
+ if (!email.ok) return email;
3572
+ return ok({
3573
+ phone: phone.value,
3574
+ email: email.value
3575
+ });
3576
+ }
3577
+ function parsePremises(value) {
3578
+ if (value === undefined || value === null) return ok(null);
3579
+ if (typeof value === "boolean") return ok(value);
3580
+ return err({
3581
+ code: "invalid_premises",
3582
+ path: "premises",
3583
+ message: "Premises must be true or false."
3584
+ });
3585
+ }
3586
+ /**
3587
+ * Resolves whether a site has premises, falling back to whether it already
3588
+ * holds a business address for every profile saved before the switch existed.
3589
+ */
3590
+ function resolvePublicProfilePremises(profile, hasBusinessAddress) {
3591
+ return profile.premises ?? hasBusinessAddress;
3592
+ }
3593
+ function parseAddressVisibility(value) {
3594
+ if (value === undefined || value === null) return ok("shown");
3595
+ if (value === "shown" || value === "hidden") return ok(value);
3596
+ return err({
3597
+ code: "invalid_address_visibility",
3598
+ path: "addressVisibility",
3599
+ message: "Address visibility must be shown or hidden."
3563
3600
  });
3564
3601
  }
3565
3602
  function emptyPublicProfile() {
3566
3603
  return {
3567
3604
  contact: null,
3605
+ contactVisibility: {
3606
+ phone: "shown",
3607
+ email: "shown"
3608
+ },
3568
3609
  openingHours: null,
3569
- socialLinks: []
3610
+ socialLinks: [],
3611
+ addressVisibility: "shown",
3612
+ premises: null
3570
3613
  };
3571
3614
  }
3572
3615
 
3616
+ //#endregion
3617
+ //#region ../core/src/public-experience.ts
3618
+ const PUBLIC_EXPERIENCES = ["website", "hosted_booking"];
3619
+ function isPublicExperience(value) {
3620
+ return typeof value === "string" && PUBLIC_EXPERIENCES.includes(value);
3621
+ }
3622
+ function parsePublicExperience(value) {
3623
+ return isPublicExperience(value) ? value : null;
3624
+ }
3625
+
3573
3626
  //#endregion
3574
3627
  //#region ../core/src/rich-text.ts
3575
- function isRecord$4(value) {
3628
+ function isRecord$7(value) {
3576
3629
  return typeof value === "object" && value !== null && !Array.isArray(value);
3577
3630
  }
3578
3631
  function hasOnlyKeys(value, allowed) {
@@ -3594,10 +3647,10 @@ function isDenseArrayOf(value, predicate) {
3594
3647
  return true;
3595
3648
  }
3596
3649
  function isTipTapMark(value) {
3597
- return isRecord$4(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$4(value.attrs));
3650
+ return isRecord$7(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$7(value.attrs));
3598
3651
  }
3599
3652
  function isTipTapNode(value) {
3600
- if (!isRecord$4(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$4(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3653
+ if (!isRecord$7(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$7(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3601
3654
  return false;
3602
3655
  }
3603
3656
  if (value.type === "text") {
@@ -3606,89 +3659,222 @@ function isTipTapNode(value) {
3606
3659
  return value.text === undefined;
3607
3660
  }
3608
3661
  function isCanonicalRichTextDoc(value) {
3609
- return isRecord$4(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3662
+ return isRecord$7(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3610
3663
  }
3611
3664
 
3612
3665
  //#endregion
3613
- //#region ../core/src/newsletter-signup-destination.ts
3666
+ //#region ../core/src/mailing-list-identifiers.ts
3614
3667
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
3615
- const MAILCHIMP_AUDIENCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3668
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3616
3669
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
3617
- function isRecord$3(value) {
3618
- return typeof value === "object" && value !== null && !Array.isArray(value);
3619
- }
3620
- function parseTrimmedString(value) {
3670
+ function parseTrimmedMatchingString(value, pattern) {
3621
3671
  if (typeof value !== "string") return null;
3622
3672
  const trimmed = value.trim();
3623
- return trimmed.length > 0 ? trimmed : null;
3673
+ return pattern.test(trimmed) ? trimmed : null;
3674
+ }
3675
+ function parsePositiveInteger(value) {
3676
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
3624
3677
  }
3625
3678
  function parseNewsletterListId(value) {
3626
- const trimmed = parseTrimmedString(value);
3627
- return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3679
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3628
3680
  }
3629
3681
  function parseMailchimpConnectionId(value) {
3630
- const trimmed = parseTrimmedString(value);
3631
- return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3682
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3683
+ }
3684
+ function parseMailchimpAccountId(value) {
3685
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3632
3686
  }
3633
3687
  function parseMailchimpAudienceId(value) {
3634
- const trimmed = parseTrimmedString(value);
3635
- return trimmed !== null && MAILCHIMP_AUDIENCE_ID_PATTERN.test(trimmed) ? trimmed : null;
3688
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3689
+ }
3690
+ function parseMailchimpInterestId(value) {
3691
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3636
3692
  }
3637
3693
  function parseMailchimpTagName(value) {
3638
- const trimmed = parseTrimmedString(value);
3639
- return trimmed !== null && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
3640
- }
3641
- function parseNewsletterSignupDestinationProvider(value) {
3642
- return value === "riverbank" || value === "mailchimp" ? value : null;
3643
- }
3644
- function parseNewsletterSignupAuthoringDestinationValue(value) {
3645
- if (!isRecord$3(value) || typeof value.tag !== "string") {
3646
- return err({ kind: "invalid-destination-shape" });
3647
- }
3648
- switch (value.tag) {
3649
- case "riverbank_list": {
3650
- const listId = parseNewsletterListId(value.listId);
3651
- return listId === null ? err({ kind: "invalid-newsletter-list-id" }) : ok({
3652
- tag: "riverbank_list",
3653
- listId
3694
+ if (typeof value !== "string") return null;
3695
+ const trimmed = value.trim();
3696
+ return trimmed.length > 0 && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
3697
+ }
3698
+ function parseBrevoConnectionId(value) {
3699
+ return parseTrimmedMatchingString(value, UUID_PATTERN);
3700
+ }
3701
+ function parseBrevoAccountId(value) {
3702
+ return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
3703
+ }
3704
+ function parseBrevoListId(value) {
3705
+ return parsePositiveInteger(value);
3706
+ }
3707
+ function parseBrevoConfirmationTemplateId(value) {
3708
+ return parsePositiveInteger(value);
3709
+ }
3710
+
3711
+ //#endregion
3712
+ //#region ../core/src/mailing-list-targeting-parser.ts
3713
+ function parseMailingListPlacementTargeting(value) {
3714
+ if (!isRecord$6(value)) return targetingError("shape");
3715
+ if (value.mode === "default") return ok({ mode: "default" });
3716
+ if (value.mode !== "custom") return targetingError("mode");
3717
+ if (!isRecord$6(value.selection)) return targetingError("selection");
3718
+ switch (value.provider) {
3719
+ case "native": return parseNativeTargeting(value.selection);
3720
+ case "brevo": return parseBrevoTargeting(value.selection);
3721
+ case "mailchimp": return parseMailchimpTargeting(value, value.selection);
3722
+ default: return targetingError("provider");
3723
+ }
3724
+ }
3725
+ function parseMailingListVisitorSelection(value) {
3726
+ if (!isRecord$6(value)) return visitorSelectionError("shape");
3727
+ switch (value.provider) {
3728
+ case "native": {
3729
+ const topicIds = parseVisitorIds(value.topicIds, parseNewsletterListId);
3730
+ return topicIds === null ? visitorSelectionError("targets") : ok({
3731
+ provider: "native",
3732
+ topicIds
3654
3733
  });
3655
3734
  }
3656
- case "mailchimp_audience": {
3657
- const connectionId = parseMailchimpConnectionId(value.connectionId);
3658
- if (connectionId === null) {
3659
- return err({ kind: "invalid-mailchimp-connection-id" });
3735
+ case "brevo": {
3736
+ const listIds = parseVisitorIds(value.listIds, parseBrevoListId);
3737
+ return listIds === null ? visitorSelectionError("targets") : ok({
3738
+ provider: "brevo",
3739
+ listIds
3740
+ });
3741
+ }
3742
+ case "mailchimp": {
3743
+ const interestIds = parseVisitorIds(value.interestIds, parseMailchimpInterestId);
3744
+ return interestIds === null ? visitorSelectionError("targets") : ok({
3745
+ provider: "mailchimp",
3746
+ interestIds
3747
+ });
3748
+ }
3749
+ default: return visitorSelectionError("provider");
3750
+ }
3751
+ }
3752
+ function parseNativeTargeting(selection) {
3753
+ if (selection.tag === "fixed") {
3754
+ const topicIds = parseNonEmptyUniqueIds(selection.topicIds, parseNewsletterListId);
3755
+ return topicIds.ok ? ok({
3756
+ mode: "custom",
3757
+ provider: "native",
3758
+ selection: {
3759
+ tag: "fixed",
3760
+ topicIds: topicIds.value
3660
3761
  }
3661
- const audienceId = parseMailchimpAudienceId(value.audienceId);
3662
- if (audienceId === null) {
3663
- return err({ kind: "invalid-mailchimp-audience-id" });
3762
+ }) : topicIds;
3763
+ }
3764
+ if (selection.tag === "visitor_choice") {
3765
+ const offeredTopicIds = parseNonEmptyUniqueIds(selection.offeredTopicIds, parseNewsletterListId);
3766
+ return offeredTopicIds.ok ? ok({
3767
+ mode: "custom",
3768
+ provider: "native",
3769
+ selection: {
3770
+ tag: "visitor_choice",
3771
+ offeredTopicIds: offeredTopicIds.value
3664
3772
  }
3665
- const rawTags = value.tags ?? [];
3666
- if (!Array.isArray(rawTags)) {
3667
- return err({ kind: "invalid-mailchimp-tags" });
3773
+ }) : offeredTopicIds;
3774
+ }
3775
+ return targetingError("selection");
3776
+ }
3777
+ function parseBrevoTargeting(selection) {
3778
+ if (selection.tag === "fixed") {
3779
+ const listIds = parseNonEmptyUniqueIds(selection.listIds, parseBrevoListId);
3780
+ return listIds.ok ? ok({
3781
+ mode: "custom",
3782
+ provider: "brevo",
3783
+ selection: {
3784
+ tag: "fixed",
3785
+ listIds: listIds.value
3668
3786
  }
3669
- const tags = [];
3670
- for (const valueTag of rawTags) {
3671
- const tag = parseMailchimpTagName(valueTag);
3672
- if (tag === null) return err({ kind: "invalid-mailchimp-tags" });
3673
- tags.push(tag);
3787
+ }) : listIds;
3788
+ }
3789
+ if (selection.tag === "visitor_choice") {
3790
+ const offeredListIds = parseNonEmptyUniqueIds(selection.offeredListIds, parseBrevoListId);
3791
+ return offeredListIds.ok ? ok({
3792
+ mode: "custom",
3793
+ provider: "brevo",
3794
+ selection: {
3795
+ tag: "visitor_choice",
3796
+ offeredListIds: offeredListIds.value
3674
3797
  }
3675
- return ok({
3676
- tag: "mailchimp_audience",
3677
- connectionId,
3678
- audienceId,
3679
- tags
3680
- });
3681
- }
3682
- default: return err({
3683
- kind: "invalid-destination-tag",
3684
- value: value.tag
3685
- });
3798
+ }) : offeredListIds;
3799
+ }
3800
+ return targetingError("selection");
3801
+ }
3802
+ function parseMailchimpTargeting(value, selection) {
3803
+ const audienceId = parseMailchimpAudienceId(value.audienceId);
3804
+ if (audienceId === null) return targetingError("audienceId");
3805
+ if (selection.tag === "fixed") {
3806
+ const interestIds = parseUniqueIds(selection.interestIds, parseMailchimpInterestId);
3807
+ return interestIds.ok ? ok({
3808
+ mode: "custom",
3809
+ provider: "mailchimp",
3810
+ audienceId,
3811
+ selection: {
3812
+ tag: "fixed",
3813
+ interestIds: interestIds.value
3814
+ }
3815
+ }) : interestIds;
3816
+ }
3817
+ if (selection.tag === "visitor_choice") {
3818
+ const offeredInterestIds = parseNonEmptyUniqueIds(selection.offeredInterestIds, parseMailchimpInterestId);
3819
+ return offeredInterestIds.ok ? ok({
3820
+ mode: "custom",
3821
+ provider: "mailchimp",
3822
+ audienceId,
3823
+ selection: {
3824
+ tag: "visitor_choice",
3825
+ offeredInterestIds: offeredInterestIds.value
3826
+ }
3827
+ }) : offeredInterestIds;
3828
+ }
3829
+ return targetingError("selection");
3830
+ }
3831
+ function parseNonEmptyUniqueIds(value, parseId) {
3832
+ const parsed = parseUniqueIds(value, parseId);
3833
+ if (!parsed.ok) return parsed;
3834
+ const [first, ...rest] = parsed.value;
3835
+ return first === undefined ? targetingError("targets") : ok([first, ...rest]);
3836
+ }
3837
+ function parseUniqueIds(value, parseId) {
3838
+ if (!Array.isArray(value)) return targetingError("targets");
3839
+ const parsed = [];
3840
+ for (const item of value) {
3841
+ const id = parseId(item);
3842
+ if (id === null) return targetingError("targets");
3843
+ if (parsed.includes(id)) return targetingError("duplicate_target");
3844
+ parsed.push(id);
3686
3845
  }
3846
+ return ok(parsed);
3847
+ }
3848
+ function parseVisitorIds(value, parseId) {
3849
+ if (!Array.isArray(value) || value.length === 0) return null;
3850
+ const parsed = [];
3851
+ for (const item of value) {
3852
+ const id = parseId(item);
3853
+ if (id === null) return null;
3854
+ if (parsed.includes(id)) return null;
3855
+ parsed.push(id);
3856
+ }
3857
+ return parsed;
3858
+ }
3859
+ function isRecord$6(value) {
3860
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3861
+ }
3862
+ function targetingError(field) {
3863
+ return err({
3864
+ tag: "invalid_mailing_list_targeting",
3865
+ field
3866
+ });
3867
+ }
3868
+ function visitorSelectionError(field) {
3869
+ return err({
3870
+ tag: "invalid_mailing_list_visitor_selection",
3871
+ field
3872
+ });
3687
3873
  }
3688
3874
 
3689
3875
  //#endregion
3690
3876
  //#region ../core/src/footer-engagement.ts
3691
- function isRecord$2(value) {
3877
+ function isRecord$5(value) {
3692
3878
  return typeof value === "object" && value !== null && !Array.isArray(value);
3693
3879
  }
3694
3880
  function invalid(path, message) {
@@ -3722,7 +3908,7 @@ function parseEngagementBase(value, base, path) {
3722
3908
  }
3723
3909
  const treatment = parseTreatment(value.treatment, `${path}.treatment`);
3724
3910
  if (!treatment.ok) return treatment;
3725
- if (!isRecord$2(value.source)) {
3911
+ if (!isRecord$5(value.source)) {
3726
3912
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3727
3913
  }
3728
3914
  const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
@@ -3751,36 +3937,17 @@ function parseStandardContact(source, base, path) {
3751
3937
  }
3752
3938
  function parseMailingList(source, base, path) {
3753
3939
  const { intro, ...section } = base;
3754
- const destination = parseNewsletterSignupAuthoringDestinationValue(source.destination);
3755
- if (!destination.ok) {
3756
- return err(invalid(`${path}.destination`, "Footer mailing-list destination is invalid."));
3757
- }
3758
- const buttonLabel = parseBoundedText(source.buttonLabel, `${path}.buttonLabel`, { maxLength: 40 });
3759
- if (!buttonLabel.ok) return buttonLabel;
3760
- const successMessage = parseBoundedText(source.successMessage, `${path}.successMessage`, { maxLength: 160 });
3761
- if (!successMessage.ok) return successMessage;
3762
- const privacyNote = parseBoundedText(source.privacyNote, `${path}.privacyNote`, {
3763
- maxLength: 240,
3764
- optional: true
3765
- });
3766
- if (!privacyNote.ok) return privacyNote;
3767
- if (buttonLabel.value === null || successMessage.value === null) {
3768
- return err(invalid(path, "Footer mailing-list copy cannot be empty."));
3769
- }
3770
- if (source.fieldPreset !== "emailOnly") {
3771
- return err(invalid(`${path}.fieldPreset`, "Footer mailing-list signup must be email-only."));
3940
+ const targeting = parseMailingListPlacementTargeting(source.targeting);
3941
+ if (!targeting.ok) {
3942
+ return err(invalid(`${path}.targeting`, "Footer mailing-list targeting is invalid."));
3772
3943
  }
3773
3944
  return ok({
3774
3945
  ...section,
3775
3946
  kind: "engagement",
3776
3947
  source: {
3777
3948
  kind: "mailing-list",
3778
- destination: destination.value,
3779
- intro,
3780
- buttonLabel: buttonLabel.value,
3781
- successMessage: successMessage.value,
3782
- privacyNote: privacyNote.value,
3783
- fieldPreset: "emailOnly"
3949
+ targeting: targeting.value,
3950
+ intro
3784
3951
  }
3785
3952
  });
3786
3953
  }
@@ -3788,7 +3955,7 @@ function parseFooterEngagementSectionValue(value, sectionBase, path) {
3788
3955
  const base = parseEngagementBase(value, sectionBase, path);
3789
3956
  if (!base.ok) return base;
3790
3957
  const source = value.source;
3791
- if (!isRecord$2(source)) {
3958
+ if (!isRecord$5(source)) {
3792
3959
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3793
3960
  }
3794
3961
  switch (source.kind) {
@@ -3903,7 +4070,7 @@ function unsafeAsFooterModuleId(value) {
3903
4070
  function parseFooterModuleId(value) {
3904
4071
  return parseSectionId(value, "sectionId");
3905
4072
  }
3906
- function isRecord$1(value) {
4073
+ function isRecord$4(value) {
3907
4074
  return typeof value === "object" && value !== null && !Array.isArray(value);
3908
4075
  }
3909
4076
  function isRichText(value) {
@@ -3933,7 +4100,7 @@ function parseHeading(value, path) {
3933
4100
  }
3934
4101
  function parseFooterSection(value, index) {
3935
4102
  const path = `sections.${index}`;
3936
- if (!isRecord$1(value)) {
4103
+ if (!isRecord$4(value)) {
3937
4104
  return err({
3938
4105
  code: "invalid_section",
3939
4106
  path,
@@ -3968,7 +4135,7 @@ function parseFooterSection(value, index) {
3968
4135
  }
3969
4136
  break;
3970
4137
  case "navigation":
3971
- if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
4138
+ if (isRecord$4(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
3972
4139
  return ok({
3973
4140
  ...base,
3974
4141
  kind: "navigation",
@@ -3981,7 +4148,7 @@ function parseFooterSection(value, index) {
3981
4148
  }
3982
4149
  break;
3983
4150
  case "legal-navigation":
3984
- if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
4151
+ if (isRecord$4(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
3985
4152
  return ok({
3986
4153
  ...base,
3987
4154
  kind: "legal-navigation",
@@ -3994,7 +4161,7 @@ function parseFooterSection(value, index) {
3994
4161
  }
3995
4162
  break;
3996
4163
  case "business-address":
3997
- if (isRecord$1(value.source) && value.source.kind === "business-address") {
4164
+ if (isRecord$4(value.source) && value.source.kind === "business-address") {
3998
4165
  return ok({
3999
4166
  ...base,
4000
4167
  kind: "business-address",
@@ -4006,7 +4173,7 @@ function parseFooterSection(value, index) {
4006
4173
  case "contact":
4007
4174
  case "opening-hours":
4008
4175
  case "social-links":
4009
- if (isRecord$1(value.source) && value.source.kind === "public-profile") {
4176
+ if (isRecord$4(value.source) && value.source.kind === "public-profile") {
4010
4177
  return ok({
4011
4178
  ...base,
4012
4179
  kind: value.kind,
@@ -4058,7 +4225,7 @@ function validateFooterComposition(composition) {
4058
4225
  return ok(composition);
4059
4226
  }
4060
4227
  function parseFooterCompositionV1(value) {
4061
- if (!isRecord$1(value)) {
4228
+ if (!isRecord$4(value)) {
4062
4229
  return err({
4063
4230
  code: "invalid_shape",
4064
4231
  path: "",
@@ -4091,7 +4258,7 @@ function parseFooterCompositionV1(value) {
4091
4258
  });
4092
4259
  }
4093
4260
  function parseLegacyPlacementSection(value, index, selectedIds) {
4094
- if (!isRecord$1(value)) {
4261
+ if (!isRecord$4(value)) {
4095
4262
  return err({
4096
4263
  code: "invalid_section",
4097
4264
  path: `modules.${index}`,
@@ -4099,7 +4266,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
4099
4266
  });
4100
4267
  }
4101
4268
  const selected = typeof value.id === "string" && selectedIds.has(value.id);
4102
- const source = isRecord$1(value.source) ? value.source : null;
4269
+ const source = isRecord$4(value.source) ? value.source : null;
4103
4270
  const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
4104
4271
  ...value,
4105
4272
  source: {
@@ -4118,7 +4285,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
4118
4285
  * It is intentionally not called by the runtime parser.
4119
4286
  */
4120
4287
  function upgradePlacementFooterCompositionV1(value) {
4121
- if (!isRecord$1(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
4288
+ if (!isRecord$4(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
4122
4289
  return err({
4123
4290
  code: "invalid_shape",
4124
4291
  path: "",
@@ -4127,7 +4294,7 @@ function upgradePlacementFooterCompositionV1(value) {
4127
4294
  }
4128
4295
  const selectedIds = new Set();
4129
4296
  for (const [index, placement] of value.placements.entries()) {
4130
- if (!isRecord$1(placement) || typeof placement.moduleId !== "string") {
4297
+ if (!isRecord$4(placement) || typeof placement.moduleId !== "string") {
4131
4298
  return err({
4132
4299
  code: "invalid_shape",
4133
4300
  path: `placements.${index}`,
@@ -4230,8 +4397,8 @@ function repairTargetFor(section) {
4230
4397
  };
4231
4398
  }
4232
4399
  }
4233
- function resolveFooterSections(composition, availability) {
4234
- return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability) ? {
4400
+ function resolveFooterSections(composition, availability$1) {
4401
+ return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability$1) ? {
4235
4402
  kind: "renderable",
4236
4403
  section
4237
4404
  } : {
@@ -4240,16 +4407,16 @@ function resolveFooterSections(composition, availability) {
4240
4407
  repairTarget: repairTargetFor(section)
4241
4408
  });
4242
4409
  }
4243
- function isSectionRenderable(section, availability) {
4410
+ function isSectionRenderable(section, availability$1) {
4244
4411
  switch (section.kind) {
4245
- case "brand": return section.showLogo && availability.hasLogo || section.showTitle;
4246
- case "navigation": return availability.hasMainNavigation;
4247
- case "legal-navigation": return availability.hasLegalPolicyNavigation;
4248
- case "business-address": return availability.hasBusinessAddress;
4249
- case "contact": return availability.hasContact;
4250
- case "opening-hours": return availability.hasOpeningHours;
4251
- case "social-links": return availability.hasSocialLinks;
4252
- case "engagement": return availability.hasEngagementSource;
4412
+ case "brand": return section.showLogo && availability$1.hasLogo || section.showTitle;
4413
+ case "navigation": return availability$1.hasMainNavigation;
4414
+ case "legal-navigation": return availability$1.hasLegalPolicyNavigation;
4415
+ case "business-address": return availability$1.hasBusinessAddress;
4416
+ case "contact": return availability$1.hasContact;
4417
+ case "opening-hours": return availability$1.hasOpeningHours;
4418
+ case "social-links": return availability$1.hasSocialLinks;
4419
+ case "engagement": return availability$1.hasEngagementSource;
4253
4420
  case "supporting-text":
4254
4421
  case "legal": return section.content !== null;
4255
4422
  }
@@ -4342,7 +4509,7 @@ const defaultSections = (legacy, options) => {
4342
4509
  ];
4343
4510
  };
4344
4511
  function normalizeLegacyFooterContentToV1(value, options = {}) {
4345
- if (!isRecord$1(value)) {
4512
+ if (!isRecord$4(value)) {
4346
4513
  return err({
4347
4514
  code: "invalid_shape",
4348
4515
  path: "",
@@ -4363,6 +4530,773 @@ function normalizeFooterNavigationLayout(value) {
4363
4530
  return value === "stack" ? "stack" : defaultFooterNavigationLayout;
4364
4531
  }
4365
4532
 
4533
+ //#endregion
4534
+ //#region ../core/src/mailing-list-setup.ts
4535
+ function isRecord$3(value) {
4536
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4537
+ }
4538
+ /** The name shown to people for each provider; the native one is the product brand. */
4539
+ function mailingListProviderName(provider) {
4540
+ switch (provider) {
4541
+ case "native": return "Macadamia";
4542
+ case "mailchimp": return "Mailchimp";
4543
+ case "brevo": return "Brevo";
4544
+ }
4545
+ }
4546
+ const DEFAULT_MAILING_LIST_SUCCESS_MESSAGE = "Thanks — you're on the list.";
4547
+ /**
4548
+ * Seeded into a new setup so every signup form carries a preferences/unsubscribe
4549
+ * reassurance out of the box. Administrators can edit or clear it.
4550
+ */
4551
+ const DEFAULT_MAILING_LIST_PRIVACY_NOTE = "Change your preferences or unsubscribe at any time.";
4552
+ function planMailingListFieldCompatibility(input) {
4553
+ const collectedFields = resolveCollectedGlobalFields(input.form);
4554
+ const requiredFields = resolveRequiredGlobalFields(input.form.fields);
4555
+ const issues = [];
4556
+ for (const destination of input.destinations) {
4557
+ for (const field of destination.requiredFields) {
4558
+ if (!collectedFields.includes(field)) {
4559
+ issues.push({
4560
+ tag: "destination_requires_uncollected_field",
4561
+ destinationKey: destination.destinationKey,
4562
+ field
4563
+ });
4564
+ } else if (!requiredFields.includes(field)) {
4565
+ issues.push({
4566
+ tag: "destination_requires_site_required_field",
4567
+ destinationKey: destination.destinationKey,
4568
+ field
4569
+ });
4570
+ }
4571
+ }
4572
+ for (const field of collectedFields) {
4573
+ if (!destination.acceptedFields.includes(field)) {
4574
+ issues.push({
4575
+ tag: requiredFields.includes(field) ? "destination_rejects_required_site_field" : "destination_rejects_collected_site_field",
4576
+ destinationKey: destination.destinationKey,
4577
+ field
4578
+ });
4579
+ }
4580
+ }
4581
+ }
4582
+ const [first, ...rest] = issues;
4583
+ return first === undefined ? { tag: "compatible" } : {
4584
+ tag: "blocked",
4585
+ issues: [first, ...rest]
4586
+ };
4587
+ }
4588
+ function resolveCollectedGlobalFields(form) {
4589
+ const phone = form.includeOptionalPhone ? ["phone"] : [];
4590
+ switch (form.fields.preset) {
4591
+ case "emailOnly": return phone;
4592
+ case "emailName": return ["name", ...phone];
4593
+ case "emailFirstLast": return [
4594
+ "first_name",
4595
+ "last_name",
4596
+ ...phone
4597
+ ];
4598
+ case "emailNameCompany": return [
4599
+ "name",
4600
+ "company",
4601
+ ...phone
4602
+ ];
4603
+ }
4604
+ }
4605
+ function resolveRequiredGlobalFields(fields) {
4606
+ switch (fields.preset) {
4607
+ case "emailOnly": return [];
4608
+ case "emailName": return fields.name === "required" ? ["name"] : [];
4609
+ case "emailFirstLast": return [...fields.firstName === "required" ? ["first_name"] : [], ...fields.lastName === "required" ? ["last_name"] : []];
4610
+ case "emailNameCompany": return [...fields.name === "required" ? ["name"] : [], ...fields.company === "required" ? ["company"] : []];
4611
+ }
4612
+ }
4613
+ function planMailingListSetupSwitch(input) {
4614
+ if (input.previousScope === null) {
4615
+ return {
4616
+ tag: "activate",
4617
+ affectedPlacementCount: 0
4618
+ };
4619
+ }
4620
+ if (sameMailingListProviderScope(input.previousScope, input.nextScope)) {
4621
+ return {
4622
+ tag: "update_same_scope",
4623
+ affectedPlacementCount: input.defaultChanged ? countDefaultMailingListPlacements(input.placementModes) : 0
4624
+ };
4625
+ }
4626
+ return {
4627
+ tag: "replace_scope",
4628
+ affectedPlacementCount: input.placementModes.length
4629
+ };
4630
+ }
4631
+ function mailingListProviderScopeFromConfigured(provider) {
4632
+ switch (provider.provider) {
4633
+ case "native": return { provider: "native" };
4634
+ case "brevo": return {
4635
+ provider: "brevo",
4636
+ accountId: provider.accountId
4637
+ };
4638
+ case "mailchimp": return {
4639
+ provider: "mailchimp",
4640
+ accountId: provider.accountId
4641
+ };
4642
+ }
4643
+ }
4644
+ function parseMailingListProviderScope(value) {
4645
+ if (!isRecord$3(value)) return err("invalid_mailing_list_provider_scope");
4646
+ if (value.provider === "native") return ok({ provider: "native" });
4647
+ if (value.provider === "brevo") {
4648
+ const accountId = parseBrevoAccountId(value.accountId);
4649
+ return accountId !== null ? ok({
4650
+ provider: "brevo",
4651
+ accountId
4652
+ }) : err("invalid_mailing_list_provider_scope");
4653
+ }
4654
+ if (value.provider === "mailchimp") {
4655
+ const accountId = parseMailchimpAccountId(value.accountId);
4656
+ return accountId !== null ? ok({
4657
+ provider: "mailchimp",
4658
+ accountId
4659
+ }) : err("invalid_mailing_list_provider_scope");
4660
+ }
4661
+ return err("invalid_mailing_list_provider_scope");
4662
+ }
4663
+ function sameMailingListProviderScope(previous, next) {
4664
+ if (previous.provider !== next.provider) return false;
4665
+ switch (previous.provider) {
4666
+ case "native": return true;
4667
+ case "mailchimp": return next.provider === "mailchimp" && previous.accountId === next.accountId;
4668
+ case "brevo": return next.provider === "brevo" && previous.accountId === next.accountId;
4669
+ }
4670
+ }
4671
+ function countDefaultMailingListPlacements(placementModes) {
4672
+ return placementModes.filter((mode) => mode === "default").length;
4673
+ }
4674
+
4675
+ //#endregion
4676
+ //#region ../core/src/mailing-list-setup-parser.ts
4677
+ function parseMailingListGlobalForm(value) {
4678
+ if (!isRecord$2(value)) return formError("shape");
4679
+ const fields = parseGlobalFields(value.fields);
4680
+ if (fields === null) return formError("fields");
4681
+ if (typeof value.includeOptionalPhone !== "boolean") {
4682
+ return formError("includeOptionalPhone");
4683
+ }
4684
+ const buttonLabel = parseRequiredString(value.buttonLabel);
4685
+ if (buttonLabel === null) return formError("buttonLabel");
4686
+ const successMessage = parseRequiredString(value.successMessage);
4687
+ if (successMessage === null) return formError("successMessage");
4688
+ const privacyNote = parseOptionalString(value.privacyNote);
4689
+ if (privacyNote === undefined) return formError("privacyNote");
4690
+ return ok({
4691
+ fields,
4692
+ includeOptionalPhone: value.includeOptionalPhone,
4693
+ buttonLabel,
4694
+ successMessage,
4695
+ privacyNote
4696
+ });
4697
+ }
4698
+ function parseMailingListSiteSetup(value) {
4699
+ if (!isRecord$2(value)) return setupError("shape");
4700
+ const siteId = parseSiteId(value.siteId);
4701
+ if (!siteId.ok) return setupError("siteId");
4702
+ if (value.tag === "not_configured") {
4703
+ return ok({
4704
+ tag: "not_configured",
4705
+ siteId: siteId.value
4706
+ });
4707
+ }
4708
+ if (value.tag !== "configured") return setupError("tag");
4709
+ const revision = parseMailingListSetupRevision(value.revision);
4710
+ if (revision === null) return setupError("revision");
4711
+ const form = parseMailingListGlobalForm(value.form);
4712
+ if (!form.ok) return setupError("form");
4713
+ const provider = parseConfiguredMailingListProvider(value.provider);
4714
+ if (!provider.ok) return provider;
4715
+ return ok({
4716
+ tag: "configured",
4717
+ siteId: siteId.value,
4718
+ revision,
4719
+ form: form.value,
4720
+ provider: provider.value
4721
+ });
4722
+ }
4723
+ function parseConfiguredMailingListProvider(value) {
4724
+ if (!isRecord$2(value) || typeof value.provider !== "string") {
4725
+ return setupError("provider");
4726
+ }
4727
+ switch (value.provider) {
4728
+ case "native": {
4729
+ const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
4730
+ if (defaultTopicId === null) return setupError("defaultTargetId");
4731
+ const confirmation = parseToggleConfirmation(value.confirmation);
4732
+ return confirmation === null ? setupError("confirmation") : ok({
4733
+ provider: "native",
4734
+ defaultTopicId,
4735
+ confirmation
4736
+ });
4737
+ }
4738
+ case "mailchimp": {
4739
+ const connectionId = parseMailchimpConnectionId(value.connectionId);
4740
+ if (connectionId === null) return setupError("connectionId");
4741
+ const accountId = parseMailchimpAccountId(value.accountId);
4742
+ if (accountId === null) return setupError("accountId");
4743
+ const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
4744
+ if (defaultAudienceId === null) return setupError("defaultTargetId");
4745
+ const confirmation = parseToggleConfirmation(value.confirmation);
4746
+ return confirmation === null ? setupError("confirmation") : ok({
4747
+ provider: "mailchimp",
4748
+ connectionId,
4749
+ accountId,
4750
+ defaultAudienceId,
4751
+ confirmation
4752
+ });
4753
+ }
4754
+ case "brevo": {
4755
+ const connectionId = parseBrevoConnectionId(value.connectionId);
4756
+ if (connectionId === null) return setupError("connectionId");
4757
+ const accountId = parseBrevoAccountId(value.accountId);
4758
+ if (accountId === null) return setupError("accountId");
4759
+ const defaultListId = parseBrevoListId(value.defaultListId);
4760
+ if (defaultListId === null) return setupError("defaultTargetId");
4761
+ const confirmation = parseBrevoConfirmation(value.confirmation);
4762
+ return confirmation === null ? setupError("confirmation") : ok({
4763
+ provider: "brevo",
4764
+ connectionId,
4765
+ accountId,
4766
+ defaultListId,
4767
+ confirmation
4768
+ });
4769
+ }
4770
+ default: return setupError("provider");
4771
+ }
4772
+ }
4773
+ function parseGlobalFields(value) {
4774
+ if (!isRecord$2(value) || typeof value.preset !== "string") return null;
4775
+ switch (value.preset) {
4776
+ case "emailOnly": return { preset: "emailOnly" };
4777
+ case "emailName": return isRequirement(value.name) ? {
4778
+ preset: "emailName",
4779
+ name: value.name
4780
+ } : null;
4781
+ case "emailFirstLast": return isRequirement(value.firstName) && isRequirement(value.lastName) ? {
4782
+ preset: "emailFirstLast",
4783
+ firstName: value.firstName,
4784
+ lastName: value.lastName
4785
+ } : null;
4786
+ case "emailNameCompany": return isRequirement(value.name) && isRequirement(value.company) ? {
4787
+ preset: "emailNameCompany",
4788
+ name: value.name,
4789
+ company: value.company
4790
+ } : null;
4791
+ default: return null;
4792
+ }
4793
+ }
4794
+ function parseToggleConfirmation(value) {
4795
+ if (!isRecord$2(value)) return null;
4796
+ return value.tag === "on" || value.tag === "off" ? { tag: value.tag } : null;
4797
+ }
4798
+ function parseBrevoConfirmation(value) {
4799
+ if (!isRecord$2(value)) return null;
4800
+ if (value.tag === "off") return { tag: "off" };
4801
+ if (value.tag !== "on") return null;
4802
+ const templateId = parseBrevoConfirmationTemplateId(value.templateId);
4803
+ const redirectUrl = parseHttpUrl(value.redirectUrl);
4804
+ return templateId === null || redirectUrl === null ? null : {
4805
+ tag: "on",
4806
+ templateId,
4807
+ redirectUrl
4808
+ };
4809
+ }
4810
+ function parseMailingListSetupRevision(value) {
4811
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
4812
+ }
4813
+ function parseHttpUrl(value) {
4814
+ if (typeof value !== "string") return null;
4815
+ try {
4816
+ const url = new URL(value);
4817
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
4818
+ } catch {
4819
+ return null;
4820
+ }
4821
+ }
4822
+ function parseRequiredString(value) {
4823
+ if (typeof value !== "string") return null;
4824
+ const trimmed = value.trim();
4825
+ return trimmed.length > 0 ? trimmed : null;
4826
+ }
4827
+ function parseOptionalString(value) {
4828
+ if (value === null || value === undefined) return null;
4829
+ if (typeof value !== "string") return undefined;
4830
+ const trimmed = value.trim();
4831
+ return trimmed.length > 0 ? trimmed : null;
4832
+ }
4833
+ function isRequirement(value) {
4834
+ return value === "required" || value === "optional";
4835
+ }
4836
+ function isRecord$2(value) {
4837
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4838
+ }
4839
+ function formError(field) {
4840
+ return err({
4841
+ tag: "invalid_mailing_list_global_form",
4842
+ field
4843
+ });
4844
+ }
4845
+ function setupError(field) {
4846
+ return err({
4847
+ tag: "invalid_mailing_list_setup",
4848
+ field
4849
+ });
4850
+ }
4851
+
4852
+ //#endregion
4853
+ //#region ../core/src/mailing-list-targeting.ts
4854
+ function sameMailingListPlacementTargeting(left, right) {
4855
+ if (left.mode !== right.mode) return false;
4856
+ if (left.mode === "default" || right.mode === "default") return true;
4857
+ if (left.provider !== right.provider || left.selection.tag !== right.selection.tag) return false;
4858
+ if (left.provider === "mailchimp" && (right.provider !== "mailchimp" || left.audienceId !== right.audienceId)) return false;
4859
+ return sameIds(targetingSelectionIds(left), targetingSelectionIds(right));
4860
+ }
4861
+ function targetingSelectionIds(targeting) {
4862
+ switch (targeting.provider) {
4863
+ case "native": return targeting.selection.tag === "fixed" ? targeting.selection.topicIds : targeting.selection.offeredTopicIds;
4864
+ case "brevo": return targeting.selection.tag === "fixed" ? targeting.selection.listIds : targeting.selection.offeredListIds;
4865
+ case "mailchimp": return targeting.selection.tag === "fixed" ? targeting.selection.interestIds : targeting.selection.offeredInterestIds;
4866
+ }
4867
+ }
4868
+ function sameIds(left, right) {
4869
+ const leftIds = new Set(left);
4870
+ const rightIds = new Set(right);
4871
+ return leftIds.size === left.length && rightIds.size === right.length && leftIds.size === rightIds.size && [...leftIds].every((id) => rightIds.has(id));
4872
+ }
4873
+ function resolveMailingListTarget(input) {
4874
+ if (input.targeting.mode === "default") {
4875
+ if (input.visitorSelection !== undefined) {
4876
+ return err({ tag: "visitor_selection_not_allowed" });
4877
+ }
4878
+ return ok(resolveDefaultTarget(input.setup));
4879
+ }
4880
+ if (input.targeting.provider !== input.setup.provider) {
4881
+ return err({
4882
+ tag: "targeting_provider_mismatch",
4883
+ setupProvider: input.setup.provider,
4884
+ targetingProvider: input.targeting.provider
4885
+ });
4886
+ }
4887
+ return resolveCustomTarget(input.targeting, input.visitorSelection);
4888
+ }
4889
+ function resolveDefaultTarget(setup) {
4890
+ switch (setup.provider) {
4891
+ case "native": return {
4892
+ provider: "native",
4893
+ topicIds: [setup.defaultTopicId]
4894
+ };
4895
+ case "brevo": return {
4896
+ provider: "brevo",
4897
+ listIds: [setup.defaultListId]
4898
+ };
4899
+ case "mailchimp": return {
4900
+ provider: "mailchimp",
4901
+ audienceId: setup.defaultAudienceId,
4902
+ interestIds: []
4903
+ };
4904
+ }
4905
+ }
4906
+ function resolveCustomTarget(targeting, visitorSelection) {
4907
+ switch (targeting.provider) {
4908
+ case "native": return resolveNativeTarget(targeting, visitorSelection);
4909
+ case "brevo": return resolveBrevoTarget(targeting, visitorSelection);
4910
+ case "mailchimp": return resolveMailchimpTarget(targeting, visitorSelection);
4911
+ }
4912
+ }
4913
+ function resolveNativeTarget(targeting, selection) {
4914
+ if (targeting.selection.tag === "fixed") {
4915
+ return selection === undefined ? ok({
4916
+ provider: "native",
4917
+ topicIds: targeting.selection.topicIds
4918
+ }) : err({ tag: "visitor_selection_not_allowed" });
4919
+ }
4920
+ if (selection === undefined) {
4921
+ return targeting.selection.offeredTopicIds.length === 1 ? ok({
4922
+ provider: "native",
4923
+ topicIds: targeting.selection.offeredTopicIds
4924
+ }) : err({ tag: "visitor_selection_required" });
4925
+ }
4926
+ if (selection.provider !== "native") {
4927
+ return err({ tag: "visitor_selection_provider_mismatch" });
4928
+ }
4929
+ const selected = selectedNonEmptySubset(selection.topicIds, targeting.selection.offeredTopicIds);
4930
+ return selected.ok ? ok({
4931
+ provider: "native",
4932
+ topicIds: selected.value
4933
+ }) : err({ tag: "visitor_selection_outside_offer" });
4934
+ }
4935
+ function resolveBrevoTarget(targeting, selection) {
4936
+ if (targeting.selection.tag === "fixed") {
4937
+ return selection === undefined ? ok({
4938
+ provider: "brevo",
4939
+ listIds: targeting.selection.listIds
4940
+ }) : err({ tag: "visitor_selection_not_allowed" });
4941
+ }
4942
+ if (selection === undefined) {
4943
+ return targeting.selection.offeredListIds.length === 1 ? ok({
4944
+ provider: "brevo",
4945
+ listIds: targeting.selection.offeredListIds
4946
+ }) : err({ tag: "visitor_selection_required" });
4947
+ }
4948
+ if (selection.provider !== "brevo") {
4949
+ return err({ tag: "visitor_selection_provider_mismatch" });
4950
+ }
4951
+ const selected = selectedNonEmptySubset(selection.listIds, targeting.selection.offeredListIds);
4952
+ return selected.ok ? ok({
4953
+ provider: "brevo",
4954
+ listIds: selected.value
4955
+ }) : err({ tag: "visitor_selection_outside_offer" });
4956
+ }
4957
+ function resolveMailchimpTarget(targeting, selection) {
4958
+ if (targeting.selection.tag === "fixed") {
4959
+ return selection === undefined ? ok({
4960
+ provider: "mailchimp",
4961
+ audienceId: targeting.audienceId,
4962
+ interestIds: targeting.selection.interestIds
4963
+ }) : err({ tag: "visitor_selection_not_allowed" });
4964
+ }
4965
+ if (selection === undefined) {
4966
+ return targeting.selection.offeredInterestIds.length === 1 ? ok({
4967
+ provider: "mailchimp",
4968
+ audienceId: targeting.audienceId,
4969
+ interestIds: targeting.selection.offeredInterestIds
4970
+ }) : err({ tag: "visitor_selection_required" });
4971
+ }
4972
+ if (selection.provider !== "mailchimp") {
4973
+ return err({ tag: "visitor_selection_provider_mismatch" });
4974
+ }
4975
+ const subset = selectedNonEmptySubset(selection.interestIds, targeting.selection.offeredInterestIds);
4976
+ return subset.ok ? ok({
4977
+ provider: "mailchimp",
4978
+ audienceId: targeting.audienceId,
4979
+ interestIds: subset.value
4980
+ }) : err({ tag: "visitor_selection_outside_offer" });
4981
+ }
4982
+ function selectedNonEmptySubset(selected, offered) {
4983
+ const normalized = [...new Set(selected)];
4984
+ const [first, ...rest] = normalized;
4985
+ if (first === undefined || normalized.some((item) => !offered.includes(item))) {
4986
+ return err("invalid_selection");
4987
+ }
4988
+ return ok([first, ...rest]);
4989
+ }
4990
+ function planMailingListChoicePresentation(targeting) {
4991
+ if (targeting.mode === "default" || targeting.selection.tag === "fixed") {
4992
+ return { tag: "hidden" };
4993
+ }
4994
+ switch (targeting.provider) {
4995
+ case "native": return targeting.selection.offeredTopicIds.length === 1 ? {
4996
+ tag: "automatic",
4997
+ selection: {
4998
+ provider: "native",
4999
+ topicIds: targeting.selection.offeredTopicIds
5000
+ }
5001
+ } : {
5002
+ tag: "visitor_choice",
5003
+ provider: "native",
5004
+ optionCount: targeting.selection.offeredTopicIds.length
5005
+ };
5006
+ case "brevo": return targeting.selection.offeredListIds.length === 1 ? {
5007
+ tag: "automatic",
5008
+ selection: {
5009
+ provider: "brevo",
5010
+ listIds: targeting.selection.offeredListIds
5011
+ }
5012
+ } : {
5013
+ tag: "visitor_choice",
5014
+ provider: "brevo",
5015
+ optionCount: targeting.selection.offeredListIds.length
5016
+ };
5017
+ case "mailchimp": return targeting.selection.offeredInterestIds.length === 1 ? {
5018
+ tag: "automatic",
5019
+ selection: {
5020
+ provider: "mailchimp",
5021
+ interestIds: targeting.selection.offeredInterestIds
5022
+ }
5023
+ } : {
5024
+ tag: "visitor_choice",
5025
+ provider: "mailchimp",
5026
+ optionCount: targeting.selection.offeredInterestIds.length
5027
+ };
5028
+ }
5029
+ }
5030
+
5031
+ //#endregion
5032
+ //#region ../core/src/mailing-list-runtime.ts
5033
+ function parseMailingListRuntimeContext(value) {
5034
+ if (!isRecord$1(value)) return invalidRuntimeContext();
5035
+ if (value.tag === "not_configured") return ok({ tag: "not_configured" });
5036
+ if (value.tag !== "configured") return invalidRuntimeContext();
5037
+ const revision = parseMailingListSetupRevision(value.revision);
5038
+ const form = parseMailingListGlobalForm(value.form);
5039
+ const provider = parseRuntimeProvider(value.provider);
5040
+ if (revision === null || !form.ok || provider === null) {
5041
+ return invalidRuntimeContext();
5042
+ }
5043
+ return ok({
5044
+ tag: "configured",
5045
+ revision,
5046
+ form: form.value,
5047
+ provider
5048
+ });
5049
+ }
5050
+ function parseRuntimeProvider(value) {
5051
+ if (!isRecord$1(value)) return null;
5052
+ switch (value.provider) {
5053
+ case "native": return parseNativeRuntimeProvider(value);
5054
+ case "brevo": return parseBrevoRuntimeProvider(value);
5055
+ case "mailchimp": return parseMailchimpRuntimeProvider(value);
5056
+ default: return null;
5057
+ }
5058
+ }
5059
+ function parseNativeRuntimeProvider(value) {
5060
+ const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
5061
+ const topics = parseNamedItems(value.topics, parseNewsletterListId);
5062
+ return defaultTopicId !== null && topics !== null ? {
5063
+ provider: "native",
5064
+ defaultTopicId,
5065
+ topics
5066
+ } : null;
5067
+ }
5068
+ function parseBrevoRuntimeProvider(value) {
5069
+ const defaultListId = parseBrevoListId(value.defaultListId);
5070
+ const lists = parseNamedItems(value.lists, parseBrevoListId);
5071
+ return defaultListId !== null && lists !== null ? {
5072
+ provider: "brevo",
5073
+ defaultListId,
5074
+ lists
5075
+ } : null;
5076
+ }
5077
+ function parseMailchimpRuntimeProvider(value) {
5078
+ if (!Array.isArray(value.audiences)) return null;
5079
+ const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
5080
+ if (defaultAudienceId === null) return null;
5081
+ const audiences = [];
5082
+ for (const audience of value.audiences) {
5083
+ if (!isRecord$1(audience)) return null;
5084
+ const id = parseMailchimpAudienceId(audience.id);
5085
+ const name = parseName(audience.name);
5086
+ if (id === null || name === null || !Array.isArray(audience.interests)) {
5087
+ return null;
5088
+ }
5089
+ const interests = parseMailchimpInterests(audience.interests);
5090
+ if (interests === null) return null;
5091
+ audiences.push({
5092
+ id,
5093
+ name,
5094
+ interests
5095
+ });
5096
+ }
5097
+ return {
5098
+ provider: "mailchimp",
5099
+ defaultAudienceId,
5100
+ audiences
5101
+ };
5102
+ }
5103
+ function parseMailchimpInterests(value) {
5104
+ const interests = [];
5105
+ for (const interest of value) {
5106
+ if (!isRecord$1(interest)) return null;
5107
+ const id = parseMailchimpInterestId(interest.id);
5108
+ const name = parseName(interest.name);
5109
+ const categoryName = parseName(interest.categoryName);
5110
+ if (id === null || name === null || categoryName === null) return null;
5111
+ interests.push({
5112
+ id,
5113
+ name,
5114
+ categoryName
5115
+ });
5116
+ }
5117
+ return interests;
5118
+ }
5119
+ function parseNamedItems(value, parseId) {
5120
+ if (!Array.isArray(value)) return null;
5121
+ const items = [];
5122
+ for (const item of value) {
5123
+ if (!isRecord$1(item)) return null;
5124
+ const id = parseId(item.id);
5125
+ const name = parseName(item.name);
5126
+ if (id === null || name === null) return null;
5127
+ items.push({
5128
+ id,
5129
+ name
5130
+ });
5131
+ }
5132
+ return items;
5133
+ }
5134
+ function parseName(value) {
5135
+ if (typeof value !== "string") return null;
5136
+ const name = value.trim();
5137
+ return name.length > 0 ? name : null;
5138
+ }
5139
+ function isRecord$1(value) {
5140
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5141
+ }
5142
+ function invalidRuntimeContext() {
5143
+ return err({ tag: "invalid_mailing_list_runtime_context" });
5144
+ }
5145
+
5146
+ //#endregion
5147
+ //#region ../core/src/mailing-list-submission.ts
5148
+ const MAILING_LIST_SUBMISSION_BUDGET_MS = 1e4;
5149
+ function parseMailingListBlockId(value) {
5150
+ const parsed = typeof value === "string" ? value.trim() : "";
5151
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed) ? parsed : null;
5152
+ }
5153
+ function parseMailingListSignedSourceToken(value) {
5154
+ const parsed = typeof value === "string" ? value.trim() : "";
5155
+ return /^[A-Za-z0-9._~-]{16,1000}$/u.test(parsed) ? parsed : null;
5156
+ }
5157
+ function parseMailingListSdkFooterBlockId(value) {
5158
+ const parsed = typeof value === "string" ? value.trim() : "";
5159
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(parsed) ? parsed : null;
5160
+ }
5161
+ function parseMailingListSdkContentHash(value) {
5162
+ const parsed = typeof value === "string" ? value.trim() : "";
5163
+ return /^[a-f0-9]{16}$/iu.test(parsed) ? parsed : null;
5164
+ }
5165
+ function parseMailingListContentVersionId(value) {
5166
+ const parsed = parseContentVersionId(value);
5167
+ return parsed.ok ? parsed.value : null;
5168
+ }
5169
+ function parseMailingListFooterRevision(value) {
5170
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
5171
+ }
5172
+ function buildMailingListSignupBlockSourceRef(blockId, contentVersionId) {
5173
+ const parsedBlockId = parseMailingListBlockId(blockId);
5174
+ const parsedContentVersionId = parseMailingListContentVersionId(contentVersionId);
5175
+ return parsedBlockId !== null && parsedContentVersionId !== null ? {
5176
+ tag: "block_ref",
5177
+ blockId: parsedBlockId,
5178
+ contentVersionId: parsedContentVersionId
5179
+ } : null;
5180
+ }
5181
+ function parseMailingListSignupProfile(form, input) {
5182
+ const email = normalizeSubscriberEmail(input.email);
5183
+ if (email === null) return {
5184
+ ok: false,
5185
+ error: { tag: "invalid_email" }
5186
+ };
5187
+ const phone = form.includeOptionalPhone ? {
5188
+ tag: "collected",
5189
+ value: normalizeOptionalValue(input.phone)
5190
+ } : { tag: "not_collected" };
5191
+ switch (form.fields.preset) {
5192
+ case "emailOnly": return {
5193
+ ok: true,
5194
+ value: {
5195
+ preset: "emailOnly",
5196
+ email,
5197
+ phone
5198
+ }
5199
+ };
5200
+ case "emailName": {
5201
+ const name = resolveProfileField("name", form.fields.name, input.name);
5202
+ return name.ok ? {
5203
+ ok: true,
5204
+ value: {
5205
+ preset: "emailName",
5206
+ email,
5207
+ phone,
5208
+ name: name.value
5209
+ }
5210
+ } : name;
5211
+ }
5212
+ case "emailFirstLast": {
5213
+ const firstName = resolveProfileField("first_name", form.fields.firstName, input.firstName);
5214
+ if (!firstName.ok) return firstName;
5215
+ const lastName = resolveProfileField("last_name", form.fields.lastName, input.lastName);
5216
+ return lastName.ok ? {
5217
+ ok: true,
5218
+ value: {
5219
+ preset: "emailFirstLast",
5220
+ email,
5221
+ phone,
5222
+ firstName: firstName.value,
5223
+ lastName: lastName.value
5224
+ }
5225
+ } : lastName;
5226
+ }
5227
+ case "emailNameCompany": {
5228
+ const name = resolveProfileField("name", form.fields.name, input.name);
5229
+ if (!name.ok) return name;
5230
+ const company = resolveProfileField("company", form.fields.company, input.company);
5231
+ return company.ok ? {
5232
+ ok: true,
5233
+ value: {
5234
+ preset: "emailNameCompany",
5235
+ email,
5236
+ phone,
5237
+ name: name.value,
5238
+ company: company.value
5239
+ }
5240
+ } : company;
5241
+ }
5242
+ }
5243
+ }
5244
+ function resolveProfileField(field, requirement, input) {
5245
+ const value = normalizeOptionalValue(input);
5246
+ if (requirement === "required") {
5247
+ return value === null ? {
5248
+ ok: false,
5249
+ error: {
5250
+ tag: "required_field_missing",
5251
+ field
5252
+ }
5253
+ } : {
5254
+ ok: true,
5255
+ value: {
5256
+ requirement: "required",
5257
+ value
5258
+ }
5259
+ };
5260
+ }
5261
+ return {
5262
+ ok: true,
5263
+ value: {
5264
+ requirement: "optional",
5265
+ value
5266
+ }
5267
+ };
5268
+ }
5269
+ function normalizeSubscriberEmail(value) {
5270
+ const normalized = value.trim().toLowerCase();
5271
+ const at = normalized.indexOf("@");
5272
+ return normalized.length <= 320 && at > 0 && at === normalized.lastIndexOf("@") && at < normalized.length - 1 ? normalized : null;
5273
+ }
5274
+ function normalizeOptionalValue(value) {
5275
+ const normalized = value?.trim() ?? "";
5276
+ return normalized.length > 0 ? normalized : null;
5277
+ }
5278
+ function toMailingListPublicSignupResponse(result) {
5279
+ if (result.ok) {
5280
+ return result.acknowledgement.tag === "subscribed" ? { tag: "subscribed" } : { tag: "confirmation_required" };
5281
+ }
5282
+ if (result.error.tag === "profile_rejected") {
5283
+ return {
5284
+ tag: "invalid_input",
5285
+ reason: "profile_rejected"
5286
+ };
5287
+ }
5288
+ if (result.error.tag === "subscriber_suppressed") {
5289
+ return { tag: "confirmation_required" };
5290
+ }
5291
+ return result.error.tag === "uncertain" ? {
5292
+ tag: "uncertain",
5293
+ reason: result.error.reason
5294
+ } : {
5295
+ tag: "unavailable",
5296
+ reason: result.error.tag
5297
+ };
5298
+ }
5299
+
4366
5300
  //#endregion
4367
5301
  //#region ../core/src/number-input.ts
4368
5302
  /**
@@ -5110,7 +6044,8 @@ function failInvalidSecret() {
5110
6044
  const SITE_ROUTE_NODE_TYPES = [
5111
6045
  "page",
5112
6046
  "content_entry",
5113
- "page_surface"
6047
+ "page_surface",
6048
+ "appointment_service"
5114
6049
  ];
5115
6050
  function parseSiteRouteTarget(input) {
5116
6051
  if (typeof input.nodeType !== "string") {
@@ -5150,6 +6085,16 @@ function parseSiteRouteTarget(input) {
5150
6085
  error: surfaceId.error
5151
6086
  });
5152
6087
  }
6088
+ case "appointment_service": {
6089
+ const serviceId = parseAppointmentServiceId(input.nodeId);
6090
+ return serviceId.ok ? ok({
6091
+ kind: "appointmentService",
6092
+ serviceId: serviceId.value
6093
+ }) : err({
6094
+ kind: "invalid-appointment-service-id",
6095
+ error: serviceId.error
6096
+ });
6097
+ }
5153
6098
  default: return err({
5154
6099
  kind: "unsupported-node-type",
5155
6100
  nodeType: input.nodeType
@@ -5161,6 +6106,7 @@ function siteRouteTargetNodeType(target) {
5161
6106
  case "page": return "page";
5162
6107
  case "contentEntry": return "content_entry";
5163
6108
  case "pageSurface": return "page_surface";
6109
+ case "appointmentService": return "appointment_service";
5164
6110
  default: return assertNever(target);
5165
6111
  }
5166
6112
  }
@@ -5169,6 +6115,7 @@ function foldSiteRouteTarget(target, cases) {
5169
6115
  case "page": return cases.page(target.pageId);
5170
6116
  case "contentEntry": return cases.contentEntry(target.entryId);
5171
6117
  case "pageSurface": return cases.pageSurface(target.surfaceId);
6118
+ case "appointmentService": return cases.appointmentService(target.serviceId);
5172
6119
  default: return assertNever(target);
5173
6120
  }
5174
6121
  }
@@ -5274,6 +6221,63 @@ function resolveWholeSeriesBookingPhase(run, now) {
5274
6221
  }
5275
6222
  }
5276
6223
 
6224
+ //#endregion
6225
+ //#region ../core/src/public-read-cache-contract.ts
6226
+ const PUBLIC_AVAILABILITY_FRESHNESS_SECONDS = 60;
6227
+ const PUBLIC_CONTENT_FRESHNESS_SECONDS = 3600;
6228
+ function availability(transport) {
6229
+ return {
6230
+ kind: "live",
6231
+ reason: "booking_correctness",
6232
+ freshness: "availability",
6233
+ transport
6234
+ };
6235
+ }
6236
+ function content(tags, transport = "direct") {
6237
+ return {
6238
+ kind: "tagged",
6239
+ tags,
6240
+ freshness: "content",
6241
+ transport
6242
+ };
6243
+ }
6244
+ /** The single classification authority for cacheable public reads. */
6245
+ const PUBLIC_READ_CACHE_POLICIES = {
6246
+ listPublishedEntries: content(["published_content"]),
6247
+ getPublishedEntryPreview: content(["published_content"]),
6248
+ listPublicEvents: availability("direct"),
6249
+ listPublicEventCalendarWindow: availability("direct"),
6250
+ listPublicRecurringSchedule: availability("batch_only"),
6251
+ getPublicEventDetails: availability("batch_only"),
6252
+ resolveEventSubroute: availability("direct"),
6253
+ listPublicRouteCandidates: content(["published_content"], "batch_only"),
6254
+ listPublicTeamMembers: content(["public_team_members"]),
6255
+ listPublicProducts: availability("direct"),
6256
+ listPublicPasses: availability("direct"),
6257
+ listPublicMemberships: availability("direct"),
6258
+ getPublicFormById: content(["site_forms"]),
6259
+ getPublicBookingServices: availability("direct"),
6260
+ getPublicBookingService: availability("direct"),
6261
+ getPublicBookingResourcesReference: availability("direct"),
6262
+ getPublicBookingAvailableDates: availability("direct"),
6263
+ getPublicBookingAvailableSlots: availability("direct"),
6264
+ getPublicWeeklySlotAvailability: availability("direct"),
6265
+ listPublicForms: content(["site_forms"]),
6266
+ checkRedirect: content([]),
6267
+ getPublicRoutableContent: content(["published_content"]),
6268
+ getPublicSitemapRoutes: content(["published_content"])
6269
+ };
6270
+ const PUBLIC_READ_ENDPOINT_SET = new Set(Object.keys(PUBLIC_READ_CACHE_POLICIES));
6271
+ function isPublicReadEndpoint(endpoint) {
6272
+ return PUBLIC_READ_ENDPOINT_SET.has(endpoint);
6273
+ }
6274
+ function getPublicReadCachePolicy(endpoint) {
6275
+ return PUBLIC_READ_CACHE_POLICIES[endpoint];
6276
+ }
6277
+ function publicReadFreshnessSeconds(endpoint) {
6278
+ return getPublicReadCachePolicy(endpoint).freshness === "availability" ? PUBLIC_AVAILABILITY_FRESHNESS_SECONDS : PUBLIC_CONTENT_FRESHNESS_SECONDS;
6279
+ }
6280
+
5277
6281
  //#endregion
5278
6282
  //#region ../core/src/event-series-behaviour.ts
5279
6283
  const EVENT_PRESET_VALUES = [
@@ -5357,13 +6361,13 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
5357
6361
  memberships: "off"
5358
6362
  }
5359
6363
  };
5360
- function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability = "onboarding") {
6364
+ function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
5361
6365
  return {
5362
6366
  id,
5363
6367
  verticalId,
5364
6368
  canonicalBehaviour,
5365
6369
  engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
5366
- availability,
6370
+ availability: availability$1,
5367
6371
  label,
5368
6372
  pluralLabel,
5369
6373
  description
@@ -5717,12 +6721,12 @@ const VERTICAL_PRESET_AVAILABILITIES = [
5717
6721
  "future"
5718
6722
  ];
5719
6723
  function getVerticalPresetsForVertical(verticalId, options) {
5720
- const availability = options?.availability;
6724
+ const availability$1 = options?.availability;
5721
6725
  return VERTICAL_BOOKING_PRESETS.filter((presetDefinition) => {
5722
6726
  if (presetDefinition.verticalId !== verticalId) return false;
5723
- if (!availability) return true;
5724
- if (availability === "selectable") return presetDefinition.availability !== "future";
5725
- return presetDefinition.availability === availability;
6727
+ if (!availability$1) return true;
6728
+ if (availability$1 === "selectable") return presetDefinition.availability !== "future";
6729
+ return presetDefinition.availability === availability$1;
5726
6730
  });
5727
6731
  }
5728
6732
  function getOnboardingPresetsForVertical(verticalId) {
@@ -5946,4 +6950,62 @@ function orderPresetIdsForVertical(verticalId, presetIds) {
5946
6950
  }
5947
6951
 
5948
6952
  //#endregion
5949
- export { fromArray as $, normalizeLocationAddressText as A, unsafeAsStorageBucketName as At, getLocalScheduleTime as B, parseMailchimpTagName as C, unsafeAsPositiveSeconds as Ct, isCanonicalRichTextDoc as D, unsafeAsSiteId as Dt, parseNewsletterSignupDestinationProvider as E, unsafeAsSiteFontId as Et, formatBookingTime as F, ok as Ft, parseBookingTimeZone as G, toBookingTimeZone as H, formatLocalScheduleDate as I, formatEventPricing as J, parseEventPageSurfaceSubject as K, formatLocalScheduleTime as L, buildBookingUtcDateWindow as M, unsafeAsUploadSessionId as Mt, formatBookingDate as N, unsafeAsUserId as Nt, assertAllowedPublicUrl as O, unsafeAsSiteScopedStoragePath as Ot, formatBookingDateParts as P, err as Pt, publicEventPricingType as Q, getBookingLocalDateKey as R, parseMailchimpAudienceId as S, unsafeAsPositiveByteCount as St, parseNewsletterSignupAuthoringDestinationValue as T, unsafeAsSafeDownloadFilename as Tt, toScheduledBookingDateRange as U, parseUtcIsoDateTimeString as V, toScheduledBookingDateTime as W, publicEventPricingCurrency as X, isPublicEventPricingOptionDisplayable as Y, publicEventPricingDisplay as Z, parseManagementKey as _, unsafeAsEventSeriesId as _t, parseVerticalPresetId as a, parseMediaAssetId as at, parseFooterCompositionV1 as b, unsafeAsOccurrenceId as bt, EVENT_PRESET_VALUES as c, parseUserId as ct, WHOLE_SERIES_EVENT_PRESET_VALUES as d, unsafeAsBookingSessionId as dt, mapNonEmptyArray as et, CANCELLED_SESSION_STATUS as f, unsafeAsDeclaredMimeType as ft, exposeSecret as g, unsafeAsEventOfferingId as gt, createSecret as h, unsafeAsEntitlementId as ht, parseVerticalId as i, parseEventSeriesId as it, toLocationCoordinates as j, unsafeAsStoragePath as jt, isPresent as k, unsafeAsStaffMemberId as kt, ORDINARY_EVENT_PRESET_VALUES as l, unsafeAsAppointmentResourceId as lt, resolveWholeSeriesBookingPhase as m, unsafeAsDetectedMimeType as mt, canonicalizeConfiguredBookingVerticalConfig as n, assertNever as nt, resolveSiteVerticalBookingConfig as o, parsePageId as ot, orderedLiveRunSessions as p, unsafeAsDemoCapability as pt, formatBookingPriceDisplay as q, isEventSeriesVerticalPresetDefinition as r, parseApiKeyId as rt, DEFAULT_EVENT_TYPE_PRESETS as s, parseSiteId as st, areConfiguredBookingVerticalConfigsEqual as t, formatCurrencyAmount as tt, SURFACE_EVENT_PRESET_VALUES as u, unsafeAsBookingRequirementId as ut, stableJsonStringify as v, unsafeAsFileChecksumSha256 as vt, parseNewsletterListId as w, unsafeAsProductId as wt, parseFooterModuleId as x, unsafeAsOriginalFilename as xt, footerNavigationLayouts as y, unsafeAsMediaAssetId as yt, getLocalScheduleDate as z };
6953
+ //#region ../core/src/navigation-locations.ts
6954
+ /** The only places a site's navigation can be displayed. */
6955
+ const NAVIGATION_LOCATIONS = [
6956
+ "main",
6957
+ "footer",
6958
+ "legal"
6959
+ ];
6960
+ const NAVIGATION_LOCATION_DESCRIPTORS = {
6961
+ main: {
6962
+ location: "main",
6963
+ label: "Main navigation",
6964
+ description: "Links shown in the site header when header navigation is enabled."
6965
+ },
6966
+ footer: {
6967
+ location: "footer",
6968
+ label: "Footer links",
6969
+ description: "Links shown in the main footer links section when it is enabled."
6970
+ },
6971
+ legal: {
6972
+ location: "legal",
6973
+ label: "Legal links",
6974
+ description: "Smaller footer links for privacy, terms and other policies."
6975
+ }
6976
+ };
6977
+ function isNavigationLocation(value) {
6978
+ return typeof value === "string" && NAVIGATION_LOCATIONS.includes(value);
6979
+ }
6980
+ /** Parse all rows for one site into a complete fixed-location record. */
6981
+ function parseSiteNavigationLocations(rows) {
6982
+ const locations = {};
6983
+ for (const [index, row] of rows.entries()) {
6984
+ if (!isNavigationLocation(row.location)) {
6985
+ return err({
6986
+ kind: "invalid_location",
6987
+ index,
6988
+ value: row.location
6989
+ });
6990
+ }
6991
+ if (Object.prototype.hasOwnProperty.call(locations, row.location)) {
6992
+ return err({
6993
+ kind: "duplicate_location",
6994
+ location: row.location
6995
+ });
6996
+ }
6997
+ locations[row.location] = row.value;
6998
+ }
6999
+ for (const location of NAVIGATION_LOCATIONS) {
7000
+ if (!Object.prototype.hasOwnProperty.call(locations, location)) {
7001
+ return err({
7002
+ kind: "missing_location",
7003
+ location
7004
+ });
7005
+ }
7006
+ }
7007
+ return ok(locations);
7008
+ }
7009
+
7010
+ //#endregion
7011
+ export { formatEventPricing as $, parseFooterCompositionV1 as A, unsafeAsPositiveSeconds as At, formatBookingTime as B, unsafeAsUserId as Bt, parseMailingListContentVersionId as C, unsafeAsEventOfferingId as Ct, DEFAULT_MAILING_LIST_SUCCESS_MESSAGE as D, unsafeAsOccurrenceId as Dt, planMailingListChoicePresentation as E, unsafeAsMediaAssetId as Et, normalizeLocationAddressText as F, unsafeAsSiteScopedStoragePath as Ft, getLocalScheduleTime as G, formatLocalScheduleTime as H, ok as Ht, toLocationCoordinates as I, unsafeAsStaffMemberId as It, toScheduledBookingDateRange as J, parseUtcIsoDateTimeString as K, buildBookingUtcDateWindow as L, unsafeAsStorageBucketName as Lt, isCanonicalRichTextDoc as M, unsafeAsSafeDownloadFilename as Mt, assertAllowedPublicUrl as N, unsafeAsSiteFontId as Nt, parseMailingListProviderScope as O, unsafeAsOriginalFilename as Ot, isPresent as P, unsafeAsSiteId as Pt, formatBookingPriceDisplay as Q, formatBookingDate as R, unsafeAsStoragePath as Rt, parseMailingListBlockId as S, unsafeAsEntitlementId as St, parseMailingListRuntimeContext as T, unsafeAsFileChecksumSha256 as Tt, getBookingLocalDateKey as U, formatLocalScheduleDate as V, err as Vt, getLocalScheduleDate as W, parseBookingTimeZone as X, toScheduledBookingDateTime as Y, parseEventPageSurfaceSubject as Z, createSecret as _, unsafeAsBookingRequirementId as _t, parseVerticalId as a, resolvePricingOptionsPriceDisplay as at, stableJsonStringify as b, unsafeAsDemoCapability as bt, DEFAULT_EVENT_TYPE_PRESETS as c, formatCurrencyAmount as ct, SURFACE_EVENT_PRESET_VALUES as d, parseEventSeriesId as dt, isPublicEventPricingOptionDisplayable as et, WHOLE_SERIES_EVENT_PRESET_VALUES as f, parseMediaAssetId as ft, resolveWholeSeriesBookingPhase as g, unsafeAsAppointmentResourceId as gt, orderedLiveRunSessions as h, parseUserId as ht, isEventSeriesVerticalPresetDefinition as i, resolveExactOrFreePriceDisplay as it, parseMailingListPlacementTargeting as j, unsafeAsProductId as jt, footerNavigationLayouts as k, unsafeAsPositiveByteCount as kt, EVENT_PRESET_VALUES as l, assertNever as lt, CANCELLED_SESSION_STATUS as m, parseSiteId as mt, areConfiguredBookingVerticalConfigsEqual as n, publicEventPricingDisplay as nt, parseVerticalPresetId as o, fromArray as ot, publicReadFreshnessSeconds as p, parsePageId as pt, toBookingTimeZone as q, canonicalizeConfiguredBookingVerticalConfig as r, publicEventPricingType as rt, resolveSiteVerticalBookingConfig as s, mapNonEmptyArray as st, NAVIGATION_LOCATIONS as t, publicEventPricingCurrency as tt, ORDINARY_EVENT_PRESET_VALUES as u, parseApiKeyId as ut, exposeSecret as v, unsafeAsBookingSessionId as vt, parseMailingListSignedSourceToken as w, unsafeAsEventSeriesId as wt, buildMailingListSignupBlockSourceRef as x, unsafeAsDetectedMimeType as xt, parseManagementKey as y, unsafeAsDeclaredMimeType as yt, formatBookingDateParts as z, unsafeAsUploadSessionId as zt };