riverbankcms 0.11.0 → 0.13.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.
@@ -48,6 +48,8 @@ const unsafeAsApiKeyId = (id) => id;
48
48
  const unsafeAsSiteRouteId = (id) => id;
49
49
  const unsafeAsPageId = (id) => id;
50
50
  const unsafeAsPageSurfaceId = (id) => id;
51
+ const unsafeAsEditLeaseToken = (id) => id;
52
+ const unsafeAsEditorClientId = (id) => id;
51
53
  const unsafeAsOccurrenceId = (id) => id;
52
54
  const unsafeAsEventOfferingId = (id) => id;
53
55
  const unsafeAsEventSeriesId = (id) => id;
@@ -120,6 +122,8 @@ const unsafeAsContentBlockId = (id) => id;
120
122
  const unsafeAsContentVersionId = (id) => id;
121
123
  const unsafeAsEntryContentHash = (value) => value;
122
124
  const unsafeAsStripeInvoiceId = (id) => id;
125
+ const unsafeAsStripeCustomerId = (id) => id;
126
+ const unsafeAsStripeSubscriptionId = (id) => id;
123
127
  const unsafeAsNewsletterSubscriberId = (id) => id;
124
128
  const unsafeAsNavigationMenuId = (id) => id;
125
129
  const unsafeAsNavigationItemId = (id) => id;
@@ -156,6 +160,8 @@ const parseApiKeyId = (value) => parseUuidBrand(value, unsafeAsApiKeyId);
156
160
  const parseSiteRouteId = (value) => parseUuidBrand(value, unsafeAsSiteRouteId);
157
161
  const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
158
162
  const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
163
+ const parseEditLeaseToken = (value) => parseUuidBrand(value, unsafeAsEditLeaseToken);
164
+ const parseEditorClientId = (value) => parseUuidBrand(value, unsafeAsEditorClientId);
159
165
  const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
160
166
  const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
161
167
  const parseEventAttendeeTicketLineId = (value) => parseUuidBrand(value, unsafeAsEventAttendeeTicketLineId);
@@ -223,6 +229,29 @@ function subtractDaysFromIso(isoString, dayCount) {
223
229
  return addDaysToIso(isoString, -dayCount);
224
230
  }
225
231
 
232
+ //#endregion
233
+ //#region ../core/src/html-entities.ts
234
+ const NAMED_ENTITIES = {
235
+ amp: "&",
236
+ lt: "<",
237
+ gt: ">",
238
+ quot: "\"",
239
+ apos: "'",
240
+ nbsp: "\xA0"
241
+ };
242
+ /**
243
+ * Decodes the HTML entities rendered pages commonly contain (the basic named
244
+ * set plus decimal and hex numeric references), leaving anything else as is.
245
+ * For reading text back out of server-rendered HTML, not for sanitising input.
246
+ */
247
+ function decodeHtmlEntities(text) {
248
+ return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, body) => {
249
+ if (body[0] !== "#") return NAMED_ENTITIES[body.toLowerCase()] ?? entity;
250
+ const codePoint = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
251
+ return Number.isInteger(codePoint) && codePoint > 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : entity;
252
+ });
253
+ }
254
+
226
255
  //#endregion
227
256
  //#region ../core/src/duration.ts
228
257
  const DURATION_UNIT_MINUTES = {
@@ -357,7 +386,7 @@ const demoInstanceStatusValues = [
357
386
  "available",
358
387
  "claimed",
359
388
  "expired",
360
- "recycling",
389
+ "showcase",
361
390
  "failed"
362
391
  ];
363
392
  const demoClaimStatusValues = [
@@ -365,7 +394,7 @@ const demoClaimStatusValues = [
365
394
  "expired",
366
395
  "released"
367
396
  ];
368
- const demoBlueprintArtifactKindValues = ["site_backup", "manual_snapshot"];
397
+ const demoBlueprintArtifactKindValues = ["recipe"];
369
398
  function narrowDemoInstanceStatus(value) {
370
399
  if (demoInstanceStatusValues.includes(value)) {
371
400
  return value;
@@ -385,6 +414,28 @@ function narrowDemoBlueprintArtifactKind(value) {
385
414
  throw new Error(`Unexpected demo blueprint artifact kind: ${value}`);
386
415
  }
387
416
 
417
+ //#endregion
418
+ //#region ../core/src/demo-warm-pool-target.ts
419
+ const DEFAULT_DEMO_WARM_POOL_TARGET = 0;
420
+ const MAX_DEMO_WARM_POOL_TARGET = 20;
421
+ function parseDemoWarmPoolTarget(value) {
422
+ if (value === undefined || value === null || value.trim() === "") {
423
+ return ok(DEFAULT_DEMO_WARM_POOL_TARGET);
424
+ }
425
+ const normalized = value.trim();
426
+ if (!/^(0|[1-9]\d*)$/u.test(normalized)) {
427
+ return err(invalidTargetError());
428
+ }
429
+ const target = Number(normalized);
430
+ return Number.isSafeInteger(target) && target <= MAX_DEMO_WARM_POOL_TARGET ? ok(target) : err(invalidTargetError());
431
+ }
432
+ function invalidTargetError() {
433
+ return {
434
+ kind: "invalid_demo_warm_pool_target",
435
+ message: "DEMO_WARM_POOL_TARGET must be a whole number from 0 to 20."
436
+ };
437
+ }
438
+
388
439
  //#endregion
389
440
  //#region ../core/src/demo-runtime.ts
390
441
  const RIVERBANK_RUNTIME_ENV_VAR = "RIVERBANK_RUNTIME_ENV";
@@ -544,6 +595,19 @@ const CURRENCY_LOCALE_BY_CODE = {
544
595
  function resolveCurrencyLocale(currencyCode, override) {
545
596
  return override ?? CURRENCY_LOCALE_BY_CODE[currencyCode] ?? "en";
546
597
  }
598
+ function fractionDigitsFor(decimals, exactFormatter) {
599
+ switch (decimals) {
600
+ case "exact": return {};
601
+ case "auto": return {
602
+ minimumFractionDigits: 0,
603
+ maximumFractionDigits: exactFormatter.resolvedOptions().maximumFractionDigits
604
+ };
605
+ case "whole": return {
606
+ minimumFractionDigits: 0,
607
+ maximumFractionDigits: 0
608
+ };
609
+ }
610
+ }
547
611
  /**
548
612
  * The single currency-amount formatter for the product: minor units in,
549
613
  * localized string out.
@@ -554,6 +618,13 @@ function resolveCurrencyLocale(currencyCode, override) {
554
618
  * own `Intl.NumberFormat`.
555
619
  */
556
620
  function formatCurrencyAmount(amountCents, currency, options) {
621
+ return formatCurrencyMajorUnits(amountCents / 100, currency, options);
622
+ }
623
+ /**
624
+ * `formatCurrencyAmount` for an amount already in MAJOR units (29.99 = £29.99),
625
+ * such as a registrar price. Prefer minor units everywhere else.
626
+ */
627
+ function formatCurrencyMajorUnits(amount, currency, options) {
557
628
  const currencyCode = currency.toUpperCase();
558
629
  const locale = resolveCurrencyLocale(currencyCode, options?.locale);
559
630
  try {
@@ -561,17 +632,13 @@ function formatCurrencyAmount(amountCents, currency, options) {
561
632
  style: "currency",
562
633
  currency: currencyCode
563
634
  });
564
- const fractionDigits = options?.decimals === "auto" ? {
565
- minimumFractionDigits: 0,
566
- maximumFractionDigits: exactFormatter.resolvedOptions().maximumFractionDigits
567
- } : {};
568
635
  return new Intl.NumberFormat(locale, {
569
636
  style: "currency",
570
637
  currency: currencyCode,
571
- ...fractionDigits
572
- }).format(amountCents / 100);
638
+ ...fractionDigitsFor(options?.decimals ?? "exact", exactFormatter)
639
+ }).format(amount);
573
640
  } catch {
574
- return `${(amountCents / 100).toFixed(2)} ${currencyCode}`;
641
+ return `${amount.toFixed(2)} ${currencyCode}`;
575
642
  }
576
643
  }
577
644
 
@@ -2446,6 +2513,18 @@ const unsafeAsEditorSiteId = (value) => value;
2446
2513
  const unsafeAsEditorPageId = (value) => value;
2447
2514
  const unsafeAsEditorEntryId = (value) => value;
2448
2515
  const unsafeAsEditorBlockId = (value) => value;
2516
+ function editorBlockCollectionOwnerKey(owner) {
2517
+ switch (owner.kind) {
2518
+ case "page": return `page:${owner.pageId}`;
2519
+ case "entry-slot": return `entry-slot:${owner.type}:${owner.entryId}:${owner.slotKey}`;
2520
+ case "surface-slot": return `surface-slot:${owner.pageSurfaceId}:${owner.slotKey}`;
2521
+ case "site-footer": return `site-footer:${owner.siteId}`;
2522
+ }
2523
+ }
2524
+ function editorBlockCollectionOwnersEqual(left, right) {
2525
+ if (left === right) return true;
2526
+ return left !== null && right !== null && editorBlockCollectionOwnerKey(left) === editorBlockCollectionOwnerKey(right);
2527
+ }
2449
2528
  const unsafeAsEditorMetadataValues = (value) => value;
2450
2529
  const unsafeAsEditorContentValues = (value) => value;
2451
2530
  const unsafeAsEditorBlockValues = (value) => value;
@@ -2470,6 +2549,151 @@ function createEditorReorderBlocksCommand(blockIds) {
2470
2549
  };
2471
2550
  }
2472
2551
 
2552
+ //#endregion
2553
+ //#region ../core/src/editor-value-sync.ts
2554
+ const unsafeAsSendId = (value) => value;
2555
+ const UNKNOWN_REVISION = { kind: "unknown" };
2556
+ function knownRevision(value) {
2557
+ return {
2558
+ kind: "known",
2559
+ value
2560
+ };
2561
+ }
2562
+ /** A pristine value at the given baseline. */
2563
+ function pristineValueSync(values, revision) {
2564
+ return {
2565
+ baseline: {
2566
+ values,
2567
+ revision
2568
+ },
2569
+ local: {
2570
+ kind: "pristine",
2571
+ request: { kind: "idle" }
2572
+ }
2573
+ };
2574
+ }
2575
+ /** What the person sees: their edits, or the baseline when there are none. */
2576
+ function displayedValues(sync) {
2577
+ return sync.local.kind === "edited" ? sync.local.values : sync.baseline.values;
2578
+ }
2579
+ /** Pristine counts as saved even with an abandoned request: nothing the person wants is unsaved. */
2580
+ function valueStatus(sync) {
2581
+ if (sync.local.kind === "pristine") return "saved";
2582
+ switch (sync.local.request.kind) {
2583
+ case "idle":
2584
+ case "abandoned": return "unsaved";
2585
+ case "in-flight": return "saving";
2586
+ case "failed": return "failed";
2587
+ default: return assertNever(sync.local.request);
2588
+ }
2589
+ }
2590
+ /** Projects the model onto the presentation sync state: unsaved and saving are both `pending`. */
2591
+ function syncStateOf(sync) {
2592
+ if (sync.local.kind === "edited" && sync.local.request.kind === "failed") {
2593
+ return {
2594
+ kind: "failed",
2595
+ error: sync.local.request.error
2596
+ };
2597
+ }
2598
+ return valueStatus(sync) === "saved" ? { kind: "baseline" } : { kind: "pending" };
2599
+ }
2600
+
2601
+ //#endregion
2602
+ //#region ../core/src/edit-lease.ts
2603
+ function parseEditLeaseScope(input) {
2604
+ switch (input.nodeType) {
2605
+ case "page": {
2606
+ const id = parsePageId(input.nodeId);
2607
+ return id.ok ? {
2608
+ nodeType: "page",
2609
+ nodeId: id.value
2610
+ } : null;
2611
+ }
2612
+ case "content_entry": {
2613
+ const id = parseContentEntryId(input.nodeId);
2614
+ return id.ok ? {
2615
+ nodeType: "content_entry",
2616
+ nodeId: id.value
2617
+ } : null;
2618
+ }
2619
+ case "page_surface": {
2620
+ const id = parsePageSurfaceId(input.nodeId);
2621
+ return id.ok ? {
2622
+ nodeType: "page_surface",
2623
+ nodeId: id.value
2624
+ } : null;
2625
+ }
2626
+ }
2627
+ }
2628
+
2629
+ //#endregion
2630
+ //#region ../core/src/block-content-identity.ts
2631
+ function readContentVersionId(value) {
2632
+ const parsed = parseContentVersionId(value);
2633
+ return parsed.ok ? parsed.value : null;
2634
+ }
2635
+ /** The one pairing of a published id with its content. */
2636
+ function readPublishedBlockPlacement(id, content$1) {
2637
+ if (id == null && content$1 == null) return null;
2638
+ const versionId = readContentVersionId(id);
2639
+ return versionId !== null && content$1 != null ? {
2640
+ kind: "published",
2641
+ id: versionId,
2642
+ content: content$1
2643
+ } : {
2644
+ kind: "incomplete",
2645
+ id: id ?? null,
2646
+ content: content$1 ?? null
2647
+ };
2648
+ }
2649
+ const NO_BLOCK_VERSIONS = {
2650
+ draftContentId: null,
2651
+ publishedPlacement: null
2652
+ };
2653
+ /** The one conversion from wire identity to editor identity. */
2654
+ function readEditorBlockVersions(wire) {
2655
+ return {
2656
+ draftContentId: readContentVersionId(wire.draftContentId),
2657
+ publishedPlacement: readPublishedBlockPlacement(wire.publishedContentId, wire.publishedContent)
2658
+ };
2659
+ }
2660
+ /** Editor identity from a render block, whose published placement is already paired. */
2661
+ function readEditorBlockVersionsFromPlacement(draftContentId, publishedPlacement) {
2662
+ return {
2663
+ draftContentId: readContentVersionId(draftContentId),
2664
+ publishedPlacement
2665
+ };
2666
+ }
2667
+ /** The published version a block can cite, if it has a complete one. */
2668
+ function publishedVersionOf(versions) {
2669
+ const placement = versions?.publishedPlacement ?? null;
2670
+ return placement?.kind === "published" ? placement : null;
2671
+ }
2672
+ /** The content version a block renders: its draft if it has one, else its published version. */
2673
+ function selectedContentVersionId(versions) {
2674
+ return versions.draftContentId ?? publishedVersionOf(versions)?.id ?? null;
2675
+ }
2676
+ /** Published content the editor treats as its dirty-state baseline. */
2677
+ function publishedContentOf(versions) {
2678
+ return publishedVersionOf(versions)?.content ?? null;
2679
+ }
2680
+ function readContentRecord(value) {
2681
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2682
+ }
2683
+ /** A block's versions after the server acknowledges one content save. */
2684
+ function applySavedBlockContentVersion(versions, saved) {
2685
+ switch (saved.stage) {
2686
+ case "draft": return {
2687
+ ...versions,
2688
+ draftContentId: readContentVersionId(saved.contentId)
2689
+ };
2690
+ case "published": return {
2691
+ draftContentId: null,
2692
+ publishedPlacement: readPublishedBlockPlacement(saved.contentId, readContentRecord(saved.data))
2693
+ };
2694
+ }
2695
+ }
2696
+
2473
2697
  //#endregion
2474
2698
  //#region ../core/src/sensitive-collection.ts
2475
2699
  const SENSITIVE_COLLECTION_MODES = ["standard", "sensitive"];
@@ -2770,16 +2994,6 @@ function projectSingleSubjectParticipantSlot(input) {
2770
2994
  default: return assertNever$1(input.target);
2771
2995
  }
2772
2996
  }
2773
- function projectEventSeriesSessionAttendanceParticipantSlot(input) {
2774
- return {
2775
- slotKey: unsafeAsParticipantSlotKey(`event_series_session_attendance:${input.attendeeId}`),
2776
- participation: {
2777
- kind: "event_series_session_attendance",
2778
- attendeeId: input.attendeeId,
2779
- registrationId: input.registrationId
2780
- }
2781
- };
2782
- }
2783
2997
  function projectEventRegistrationParticipantRoster(input) {
2784
2998
  const bookerSlot = buildSlot({
2785
2999
  slotKey: unsafeAsParticipantSlotKey(`event_registration_booker:${input.attendeeId}`),
@@ -2829,43 +3043,6 @@ function reconcileParticipantRoster(input) {
2829
3043
  issues.push(...findDuplicateParticipantEmails(input.slots));
2830
3044
  return issues;
2831
3045
  }
2832
- function planParticipantSubjectResolution(input) {
2833
- const existingParticipantIdByEmail = new Map(input.existingParticipantsByEmail.map((participant) => [participant.email, participant.participantId]));
2834
- const duplicateSlotKeysByEmail = new Map(input.projection.issues.filter((issue) => issue.kind === "duplicate_email_in_booking_context").map((issue) => [issue.email, issue.slotKeys]));
2835
- return input.projection.slots.map((slot) => {
2836
- if (slot.email === null) {
2837
- return {
2838
- kind: "staff_managed_slot",
2839
- slotKey: slot.slotKey
2840
- };
2841
- }
2842
- const duplicateSlotKeys = duplicateSlotKeysByEmail.get(slot.email);
2843
- if (duplicateSlotKeys !== undefined) {
2844
- return {
2845
- kind: "blocked_duplicate_participant_in_booking",
2846
- slotKey: slot.slotKey,
2847
- email: slot.email,
2848
- conflictingSlotKeys: duplicateSlotKeys
2849
- };
2850
- }
2851
- const participantId = existingParticipantIdByEmail.get(slot.email);
2852
- if (participantId !== undefined) {
2853
- return {
2854
- kind: "reuse_participant_by_site_email",
2855
- slotKey: slot.slotKey,
2856
- email: slot.email,
2857
- participantId
2858
- };
2859
- }
2860
- return {
2861
- kind: "create_participant",
2862
- slotKey: slot.slotKey,
2863
- email: slot.email,
2864
- displayName: slot.displayName,
2865
- phone: slot.phone
2866
- };
2867
- });
2868
- }
2869
3046
  function buildSlot(input) {
2870
3047
  const email = normalizeParticipantEmail(input.contact.email);
2871
3048
  return {
@@ -2904,213 +3081,6 @@ function assertNever$1(value) {
2904
3081
  throw new Error(`Unexpected participant variant: ${JSON.stringify(value)}`);
2905
3082
  }
2906
3083
 
2907
- //#endregion
2908
- //#region ../core/src/participant-identity.ts
2909
- function planPersonResolution(input) {
2910
- switch (input.kind) {
2911
- case "with_email":
2912
- if (input.existingParticipantIdByEmail !== null) {
2913
- return {
2914
- kind: "reuse_existing_person_by_email",
2915
- participantId: input.existingParticipantIdByEmail,
2916
- email: input.email
2917
- };
2918
- }
2919
- return {
2920
- kind: "create_contactable_person",
2921
- seed: {
2922
- displayName: input.displayName,
2923
- email: input.email,
2924
- phone: input.phone
2925
- }
2926
- };
2927
- case "without_email": return {
2928
- kind: "create_provisional_person",
2929
- seed: {
2930
- displayName: input.displayName,
2931
- phone: input.phone
2932
- }
2933
- };
2934
- default: return assertNever(input);
2935
- }
2936
- }
2937
- function planCustomerPersonResolution(input) {
2938
- const resolution = planPersonResolution(toCustomerPersonResolutionInput(input));
2939
- switch (resolution.kind) {
2940
- case "reuse_existing_person_by_email": return {
2941
- kind: "link_existing_person_by_email",
2942
- customerId: input.customerId,
2943
- participantId: resolution.participantId,
2944
- email: resolution.email
2945
- };
2946
- case "create_contactable_person": return {
2947
- kind: "create_contactable_person_for_customer",
2948
- customerId: input.customerId,
2949
- seed: resolution.seed
2950
- };
2951
- case "create_provisional_person": return {
2952
- kind: "create_provisional_person_for_customer",
2953
- customerId: input.customerId,
2954
- seed: resolution.seed
2955
- };
2956
- default: return assertNever(resolution);
2957
- }
2958
- }
2959
- function toCustomerPersonResolutionInput(input) {
2960
- switch (input.kind) {
2961
- case "with_email": return {
2962
- kind: "with_email",
2963
- displayName: input.displayName,
2964
- email: input.email,
2965
- phone: input.phone,
2966
- existingParticipantIdByEmail: input.existingParticipantIdByEmail
2967
- };
2968
- case "without_email": return {
2969
- kind: "without_email",
2970
- displayName: input.displayName,
2971
- phone: input.phone
2972
- };
2973
- default: return assertNever(input);
2974
- }
2975
- }
2976
- function planParticipationPersonResolution(input) {
2977
- switch (input.resolution.kind) {
2978
- case "blocked_duplicate_participant_in_booking": return {
2979
- kind: "blocked_duplicate_participant_in_booking",
2980
- slotKey: input.resolution.slotKey,
2981
- email: input.resolution.email,
2982
- conflictingSlotKeys: input.resolution.conflictingSlotKeys
2983
- };
2984
- case "create_participant":
2985
- case "reuse_participant_by_site_email":
2986
- case "staff_managed_slot": return applyParticipationResolutionPlan({
2987
- slotKey: input.resolution.slotKey,
2988
- participation: input.slot.participation,
2989
- resolution: planPersonResolution(toSlotPersonResolutionInput({
2990
- slot: input.slot,
2991
- resolution: input.resolution
2992
- }))
2993
- });
2994
- default: return assertNever(input.resolution);
2995
- }
2996
- }
2997
- function toSlotPersonResolutionInput(input) {
2998
- switch (input.resolution.kind) {
2999
- case "create_participant": return {
3000
- kind: "with_email",
3001
- displayName: input.resolution.displayName,
3002
- email: input.resolution.email,
3003
- phone: input.resolution.phone,
3004
- existingParticipantIdByEmail: null
3005
- };
3006
- case "reuse_participant_by_site_email": return {
3007
- kind: "with_email",
3008
- displayName: input.slot.displayName,
3009
- email: input.resolution.email,
3010
- phone: input.slot.phone,
3011
- existingParticipantIdByEmail: input.resolution.participantId
3012
- };
3013
- case "staff_managed_slot": return {
3014
- kind: "without_email",
3015
- displayName: input.slot.displayName,
3016
- phone: input.slot.phone
3017
- };
3018
- default: return assertNever(input.resolution);
3019
- }
3020
- }
3021
- function applyParticipationResolutionPlan(input) {
3022
- switch (input.resolution.kind) {
3023
- case "reuse_existing_person_by_email": return {
3024
- kind: "reuse_existing_person_for_slot",
3025
- slotKey: input.slotKey,
3026
- participantId: input.resolution.participantId,
3027
- participation: input.participation
3028
- };
3029
- case "create_contactable_person": return {
3030
- kind: "create_contactable_person_for_slot",
3031
- slotKey: input.slotKey,
3032
- seed: input.resolution.seed,
3033
- participation: input.participation
3034
- };
3035
- case "create_provisional_person": return {
3036
- kind: "create_provisional_person_for_slot",
3037
- slotKey: input.slotKey,
3038
- seed: input.resolution.seed,
3039
- participation: input.participation
3040
- };
3041
- default: return assertNever(input.resolution);
3042
- }
3043
- }
3044
- function planSingleSubjectParticipantMaterialization(input) {
3045
- const resolution = planPersonResolution(toSingleSubjectPersonResolutionInput(input));
3046
- switch (resolution.kind) {
3047
- case "reuse_existing_person_by_email": return {
3048
- kind: "reuse_existing_person_and_participation",
3049
- customerId: input.customerId,
3050
- participantId: resolution.participantId,
3051
- participation: input.participation
3052
- };
3053
- case "create_contactable_person": return {
3054
- kind: "create_contactable_person_and_participation",
3055
- customerId: input.customerId,
3056
- seed: resolution.seed,
3057
- participation: input.participation
3058
- };
3059
- case "create_provisional_person": return {
3060
- kind: "create_provisional_person_and_participation",
3061
- customerId: input.customerId,
3062
- seed: resolution.seed,
3063
- participation: input.participation
3064
- };
3065
- default: return assertNever(resolution);
3066
- }
3067
- }
3068
- function toSingleSubjectPersonResolutionInput(input) {
3069
- switch (input.kind) {
3070
- case "participant_subject": return {
3071
- kind: "with_email",
3072
- displayName: input.displayName,
3073
- email: input.email,
3074
- phone: input.phone,
3075
- existingParticipantIdByEmail: input.existingParticipantIdByEmail
3076
- };
3077
- case "provisional_single_subject_participant": return {
3078
- kind: "without_email",
3079
- displayName: input.displayName,
3080
- phone: input.phone
3081
- };
3082
- default: return assertNever(input);
3083
- }
3084
- }
3085
- function planCleanupReadiness(evidence) {
3086
- const blockers = collectCleanupReadinessBlockers(evidence);
3087
- if (blockers.length === 0) {
3088
- return {
3089
- kind: "ready",
3090
- evidence,
3091
- blockers: []
3092
- };
3093
- }
3094
- return {
3095
- kind: "blocked",
3096
- evidence,
3097
- blockers
3098
- };
3099
- }
3100
- function collectCleanupReadinessBlockers(evidence) {
3101
- const blockers = [];
3102
- if (evidence.legacyCustomerEmailCallSiteCount > 0) {
3103
- blockers.push("legacy_customer_email_call_sites_remaining");
3104
- }
3105
- if (evidence.parityFailureSurfaces.length > 0) {
3106
- blockers.push("parity_failures_remaining");
3107
- }
3108
- if (evidence.unresolvedHealthRowCount > 0) {
3109
- blockers.push("unresolved_health_rows_remaining");
3110
- }
3111
- return blockers;
3112
- }
3113
-
3114
3084
  //#endregion
3115
3085
  //#region ../core/src/public-url-validation.ts
3116
3086
  function buildPublicUrlValidationError(message, errorFactory) {
@@ -4836,6 +4806,10 @@ function parseMailingListSiteSetup(value) {
4836
4806
  });
4837
4807
  }
4838
4808
  if (value.tag !== "configured") return setupError("tag");
4809
+ return parseConfiguredSiteSetup(value, siteId.value);
4810
+ }
4811
+ function parseConfiguredSiteSetup(value, siteId) {
4812
+ if (typeof value.bookingOptInEnabled !== "boolean") return setupError("shape");
4839
4813
  const revision = parseMailingListSetupRevision(value.revision);
4840
4814
  if (revision === null) return setupError("revision");
4841
4815
  const form = parseMailingListGlobalForm(value.form);
@@ -4844,8 +4818,9 @@ function parseMailingListSiteSetup(value) {
4844
4818
  if (!provider.ok) return provider;
4845
4819
  return ok({
4846
4820
  tag: "configured",
4847
- siteId: siteId.value,
4821
+ siteId,
4848
4822
  revision,
4823
+ bookingOptInEnabled: value.bookingOptInEnabled,
4849
4824
  form: form.value,
4850
4825
  provider: provider.value
4851
4826
  });
@@ -5164,19 +5139,61 @@ function parseMailingListRuntimeContext(value) {
5164
5139
  if (!isRecord$1(value)) return invalidRuntimeContext();
5165
5140
  if (value.tag === "not_configured") return ok({ tag: "not_configured" });
5166
5141
  if (value.tag !== "configured") return invalidRuntimeContext();
5142
+ return parseConfiguredRuntimeContext(value);
5143
+ }
5144
+ function parseConfiguredRuntimeContext(value) {
5167
5145
  const revision = parseMailingListSetupRevision(value.revision);
5168
5146
  const form = parseMailingListGlobalForm(value.form);
5169
5147
  const provider = parseRuntimeProvider(value.provider);
5170
- if (revision === null || !form.ok || provider === null) {
5148
+ const bookingSignup = parseBookingSignupOffer(value.bookingSignup);
5149
+ if (revision === null || !form.ok || provider === null || bookingSignup === null) {
5171
5150
  return invalidRuntimeContext();
5172
5151
  }
5173
5152
  return ok({
5174
5153
  tag: "configured",
5175
5154
  revision,
5155
+ bookingSignup,
5176
5156
  form: form.value,
5177
5157
  provider
5178
5158
  });
5179
5159
  }
5160
+ /** Destination facts are resolved once by the provider adapter, before projection. */
5161
+ function planBookingSignupOffer(setup, destination) {
5162
+ if (destination === "unavailable") return {
5163
+ tag: "unavailable",
5164
+ reason: "destination"
5165
+ };
5166
+ const required = resolveRequiredGlobalFields(setup.form.fields);
5167
+ if (setup.form.fields.preset !== "emailOnly" && setup.form.fields.preset !== "emailName" || required.some((field) => field !== "name")) {
5168
+ return {
5169
+ tag: "unavailable",
5170
+ reason: "fields"
5171
+ };
5172
+ }
5173
+ return setup.bookingOptInEnabled ? { tag: "offered" } : {
5174
+ tag: "unavailable",
5175
+ reason: "disabled"
5176
+ };
5177
+ }
5178
+ function resolveBookingSignupOffer(context, siteName) {
5179
+ const name = siteName?.trim() ?? "";
5180
+ return context?.tag === "configured" && context.bookingSignup.tag === "offered" ? { siteName: name.length > 0 ? name : "this site" } : null;
5181
+ }
5182
+ function parseBookingSignupOffer(value) {
5183
+ if (value === undefined) return {
5184
+ tag: "unavailable",
5185
+ reason: "disabled"
5186
+ };
5187
+ if (!isRecord$1(value)) return null;
5188
+ if (value.tag === "offered") return { tag: "offered" };
5189
+ if (value.tag === "unavailable" && (value.reason === "disabled" || value.reason === "fields" || value.reason === "destination")) {
5190
+ return {
5191
+ tag: "unavailable",
5192
+ reason: value.reason
5193
+ };
5194
+ }
5195
+ return null;
5196
+ }
5180
5197
  function parseRuntimeProvider(value) {
5181
5198
  if (!isRecord$1(value)) return null;
5182
5199
  switch (value.provider) {
@@ -6057,14 +6074,18 @@ function generateUniqueSlug(text, existingSlugs) {
6057
6074
 
6058
6075
  //#endregion
6059
6076
  //#region ../core/src/uuid.ts
6060
- /**
6061
- * Validates whether a string matches UUID v4 format
6062
- * Format: 8-4-4-4-12 hexadecimal digits
6063
- * Example: 550e8400-e29b-41d4-a716-446655440000
6064
- */
6077
+ /** Validate a UUID in the existing 8-4-4-4-12 hexadecimal format. */
6065
6078
  function isValidUuid(str) {
6066
6079
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
6067
6080
  }
6081
+ function generateUuidV4(cryptoSource = globalThis.crypto) {
6082
+ if (typeof cryptoSource.randomUUID === "function") return cryptoSource.randomUUID();
6083
+ const bytes = cryptoSource.getRandomValues(new Uint8Array(16));
6084
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
6085
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
6086
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
6087
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
6088
+ }
6068
6089
 
6069
6090
  //#endregion
6070
6091
  //#region ../core/src/stable-json.ts
@@ -7137,6 +7158,18 @@ function parseSiteNavigationLocations(rows) {
7137
7158
  return ok(locations);
7138
7159
  }
7139
7160
 
7161
+ //#endregion
7162
+ //#region ../core/src/navigation-portal-labels.ts
7163
+ /**
7164
+ * What a customer sign-in link in a site's menu says when the site hasn't set
7165
+ * its own text. The site renderer falls back to these, and the dashboard's
7166
+ * menu editor shows them, so what an editor sees is what visitors get.
7167
+ */
7168
+ const PORTAL_NAV_DEFAULT_LABELS = {
7169
+ loggedOut: "Login",
7170
+ loggedIn: "My Account"
7171
+ };
7172
+
7140
7173
  //#endregion
7141
7174
  //#region ../core/src/event-preset-vocabulary.ts
7142
7175
  const EVENT_PRESET_VOCABULARY = {