riverbankcms 0.1.0 → 0.2.2

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.
@@ -0,0 +1,4697 @@
1
+ #!/usr/bin/env node
2
+ //#region ../core/src/result.ts
3
+ function ok(value) {
4
+ return {
5
+ ok: true,
6
+ value
7
+ };
8
+ }
9
+ function err(error) {
10
+ return {
11
+ ok: false,
12
+ error
13
+ };
14
+ }
15
+
16
+ //#endregion
17
+ //#region ../core/src/branded.ts
18
+ const UUID_PATTERN$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ function parseUuidBrand(value, unsafeConstructor) {
20
+ if (typeof value !== "string") {
21
+ return err({ kind: "not_string" });
22
+ }
23
+ const normalized = value.trim().toLowerCase();
24
+ if (normalized.length === 0) {
25
+ return err({ kind: "empty_string" });
26
+ }
27
+ if (!UUID_PATTERN$1.test(normalized)) {
28
+ return err({ kind: "invalid_uuid" });
29
+ }
30
+ return ok(unsafeConstructor(normalized));
31
+ }
32
+ function expectParsedBrandedId(result) {
33
+ if (!result.ok) {
34
+ throw new Error(`Expected a validated branded UUID (${result.error.kind}).`);
35
+ }
36
+ return result.value;
37
+ }
38
+ const unsafeAsEntryId = (id) => id;
39
+ /**
40
+ * Mint a preview-fixture entry id. Unsafe at the type level because the
41
+ * function can't verify the input string is actually shaped as a preview
42
+ * id — the predicate `isPreviewEntryId` is the recommended narrowing path.
43
+ */
44
+ const unsafeAsPreviewEntryId = (id) => id;
45
+ const unsafeAsEventInstanceId = (id) => id;
46
+ const unsafeAsSiteId = (id) => id;
47
+ const unsafeAsApiKeyId = (id) => id;
48
+ const unsafeAsSiteRouteId = (id) => id;
49
+ const unsafeAsPageId = (id) => id;
50
+ const unsafeAsPageSurfaceId = (id) => id;
51
+ const unsafeAsOccurrenceId = (id) => id;
52
+ const unsafeAsEventOfferingId = (id) => id;
53
+ const unsafeAsEventSeriesId = (id) => id;
54
+ const unsafeAsEventPageRouteSlug = (value) => value;
55
+ const unsafeAsTicketTypeId = (id) => id;
56
+ const unsafeAsEventCategoryId = (id) => id;
57
+ const unsafeAsVenueId = (id) => id;
58
+ const unsafeAsPricingOptionId = (id) => id;
59
+ const unsafeAsMoneyDateString = (value) => value;
60
+ const unsafeAsStripeCheckoutSessionId = (value) => value;
61
+ const unsafeAsProductId = (id) => id;
62
+ const unsafeAsProductVariantId = (id) => id;
63
+ const unsafeAsProductCategoryId = (id) => id;
64
+ const unsafeAsMediaAssetId = (id) => id;
65
+ const unsafeAsUploadSessionId = (id) => id;
66
+ const unsafeAsStorageBucketName = (value) => value;
67
+ const unsafeAsStoragePath = (value) => value;
68
+ const unsafeAsRelativeStoragePath = (value) => value;
69
+ const unsafeAsSiteScopedStoragePath = (value) => value;
70
+ const unsafeAsFileChecksumSha256 = (value) => value;
71
+ const unsafeAsDetectedMimeType = (value) => value;
72
+ const unsafeAsDeclaredMimeType = (value) => value;
73
+ const unsafeAsOriginalFilename = (value) => value;
74
+ const unsafeAsSafeDownloadFilename = (value) => value;
75
+ const unsafeAsPositiveByteCount = (value) => value;
76
+ const unsafeAsPositiveSeconds = (value) => value;
77
+ const unsafeAsBookingSessionId = (id) => id;
78
+ const unsafeAsDatedOfferingCapacityHoldId = (value) => value;
79
+ const unsafeAsDatedOfferingCapacityHoldAuditRef = (value) => value;
80
+ const unsafeAsDatedOfferingRevocationAuditEventId = (value) => value;
81
+ const unsafeAsDatedOfferingNonCashProjectionClaimToken = (value) => value;
82
+ const unsafeAsDatedOfferingWaitlistCapacityEffectId = (value) => value;
83
+ const unsafeAsDatedOfferingWaitlistCapacityEffectClaimToken = (value) => value;
84
+ const unsafeAsDatedOfferingRunCancellationOperationId = (value) => value;
85
+ const unsafeAsEntitlementId = (id) => id;
86
+ const unsafeAsSiteFontId = (id) => id;
87
+ const unsafeAsUserId = (id) => id;
88
+ const unsafeAsFormId = (id) => id;
89
+ const unsafeAsFormSubmissionId = (id) => id;
90
+ const unsafeAsPassId = (id) => id;
91
+ const unsafeAsMembershipId = (id) => id;
92
+ const unsafeAsCourseId = (id) => id;
93
+ const unsafeAsCourseProgramId = (id) => id;
94
+ const unsafeAsCustomerId = (id) => id;
95
+ const unsafeAsOrderId = (id) => id;
96
+ const unsafeAsOrderItemId = (id) => id;
97
+ const unsafeAsOrderRefundId = (id) => id;
98
+ const unsafeAsOrderPaymentReceiptId = (id) => id;
99
+ const unsafeAsOrderFlexibleBalanceContractId = (id) => id;
100
+ const unsafeAsOrderPaymentPlanId = (id) => id;
101
+ const unsafeAsOrderPaymentPlanInstalmentId = (id) => id;
102
+ const unsafeAsManualPaymentOperationId = (id) => id;
103
+ const unsafeAsCheckoutLineId = (id) => id;
104
+ const unsafeAsAttendeeId = (id) => id;
105
+ const unsafeAsCustomerPassId = (id) => id;
106
+ const unsafeAsCustomerMembershipId = (id) => id;
107
+ const unsafeAsEnrollmentId = (id) => id;
108
+ const unsafeAsAppointmentId = (id) => id;
109
+ const unsafeAsAppointmentServiceId = (id) => id;
110
+ const unsafeAsAppointmentResourceId = (id) => id;
111
+ const unsafeAsAppointmentReservationId = (id) => id;
112
+ const unsafeAsAppointmentReservationRunId = (id) => id;
113
+ const unsafeAsAppointmentPaymentRequestId = (id) => id;
114
+ const unsafeAsStaffMemberId = (id) => id;
115
+ const unsafeAsBookingCalendarFeedTokenId = (id) => id;
116
+ const unsafeAsBookingCalendarFeedTokenPublicId = (id) => id;
117
+ const unsafeAsBookingCalendarFeedTokenSecret = (value) => value;
118
+ const unsafeAsBookingCalendarFeedTokenSecretHash = (value) => value;
119
+ const unsafeAsContentEntryId = (id) => id;
120
+ const unsafeAsContentBlockId = (id) => id;
121
+ const unsafeAsContentVersionId = (id) => id;
122
+ const unsafeAsEntryContentHash = (value) => value;
123
+ const unsafeAsStripeInvoiceId = (id) => id;
124
+ const unsafeAsNewsletterSubscriberId = (id) => id;
125
+ const unsafeAsNavigationMenuId = (id) => id;
126
+ const unsafeAsNavigationItemId = (id) => id;
127
+ const unsafeAsEmailAuditRowId = (id) => id;
128
+ const unsafeAsWaitlistEntryId = (id) => id;
129
+ const unsafeAsWaitlistHoldId = (id) => id;
130
+ const unsafeAsShipmentId = (id) => id;
131
+ const unsafeAsActivityLogEntryId = (id) => id;
132
+ const unsafeAsDemoBlueprintId = (id) => id;
133
+ const unsafeAsDemoBlueprintVersionId = (id) => id;
134
+ const unsafeAsDemoInstanceId = (id) => id;
135
+ const unsafeAsDemoClaimId = (id) => id;
136
+ const unsafeAsDemoLifecycleAuditEventId = (id) => id;
137
+ const unsafeAsDemoRuntimeId = (id) => id;
138
+ const unsafeAsDemoCapability = (value) => value;
139
+ const unsafeAsDemoPaymentFlowId = (id) => id;
140
+ const unsafeAsDemoClaimTtlHours = (value) => value;
141
+ const unsafeAsGiftingOfferId = (id) => id;
142
+ const unsafeAsGiftingInstrumentId = (id) => id;
143
+ const unsafeAsBookingRequirementId = (id) => id;
144
+ const unsafeAsBookingRequirementEvidenceId = (id) => id;
145
+ const unsafeAsBookingRequirementDocumentUploadId = (id) => id;
146
+ const unsafeAsBookingRequirementEvidenceAttachmentId = (id) => id;
147
+ const unsafeAsParticipantId = (id) => id;
148
+ const unsafeAsParticipantParticipationId = (id) => id;
149
+ const unsafeAsParticipantRosterAssertionJobId = (id) => id;
150
+ const unsafeAsParticipantBookingQuestionsTaskId = (id) => id;
151
+ const unsafeAsEventAttendeeGuestId = (id) => id;
152
+ const unsafeAsPersonRecordId = (id) => id;
153
+ const unsafeAsEncounterRecordId = (id) => id;
154
+ const unsafeAsRecordEntryId = (id) => id;
155
+ const unsafeAsProtectedPayloadKeyVersion = (id) => id;
156
+ const parseSiteId = (value) => parseUuidBrand(value, unsafeAsSiteId);
157
+ const parseApiKeyId = (value) => parseUuidBrand(value, unsafeAsApiKeyId);
158
+ const parseSiteRouteId = (value) => parseUuidBrand(value, unsafeAsSiteRouteId);
159
+ const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
160
+ const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
161
+ const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
162
+ const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
163
+ const parseCourseProgramId = (value) => parseUuidBrand(value, unsafeAsCourseProgramId);
164
+ const parseTicketTypeId = (value) => parseUuidBrand(value, unsafeAsTicketTypeId);
165
+ const parsePricingOptionId = (value) => parseUuidBrand(value, unsafeAsPricingOptionId);
166
+ const parseUserId = (value) => parseUuidBrand(value, unsafeAsUserId);
167
+ const parseProductId = (value) => parseUuidBrand(value, unsafeAsProductId);
168
+ const parseProductVariantId = (value) => parseUuidBrand(value, unsafeAsProductVariantId);
169
+ const parseProductCategoryId = (value) => parseUuidBrand(value, unsafeAsProductCategoryId);
170
+ const parseMediaAssetId = (value) => parseUuidBrand(value, unsafeAsMediaAssetId);
171
+ const parseContentEntryId = (value) => parseUuidBrand(value, unsafeAsContentEntryId);
172
+ const parseContentBlockId = (value) => parseUuidBrand(value, unsafeAsContentBlockId);
173
+ const parseContentVersionId = (value) => parseUuidBrand(value, unsafeAsContentVersionId);
174
+ const parseNavigationMenuId = (value) => parseUuidBrand(value, unsafeAsNavigationMenuId);
175
+ const parseNavigationItemId = (value) => parseUuidBrand(value, unsafeAsNavigationItemId);
176
+ const parsePassId = (value) => parseUuidBrand(value, unsafeAsPassId);
177
+ const parseMembershipId = (value) => parseUuidBrand(value, unsafeAsMembershipId);
178
+ const parseCourseId = (value) => parseUuidBrand(value, unsafeAsCourseId);
179
+ const parseFormId = (value) => parseUuidBrand(value, unsafeAsFormId);
180
+ const parseAppointmentId = (value) => parseUuidBrand(value, unsafeAsAppointmentId);
181
+ const parseAppointmentServiceId = (value) => parseUuidBrand(value, unsafeAsAppointmentServiceId);
182
+ const parseAppointmentResourceId = (value) => parseUuidBrand(value, unsafeAsAppointmentResourceId);
183
+ const parseAppointmentReservationId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationId);
184
+ const parseAppointmentReservationRunId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationRunId);
185
+ const parseAttendeeId = (value) => parseUuidBrand(value, unsafeAsAttendeeId);
186
+ const parseDatedOfferingCapacityHoldId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldId);
187
+ const parseDatedOfferingCapacityHoldAuditRef = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldAuditRef);
188
+ const parseDatedOfferingRevocationAuditEventId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRevocationAuditEventId);
189
+ const parseDatedOfferingNonCashProjectionClaimToken = (value) => parseUuidBrand(value, unsafeAsDatedOfferingNonCashProjectionClaimToken);
190
+ const parseDatedOfferingWaitlistCapacityEffectId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingWaitlistCapacityEffectId);
191
+ const parseDatedOfferingWaitlistCapacityEffectClaimToken = (value) => parseUuidBrand(value, unsafeAsDatedOfferingWaitlistCapacityEffectClaimToken);
192
+ const parseDatedOfferingRunCancellationOperationId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRunCancellationOperationId);
193
+ const parseCustomerId = (value) => parseUuidBrand(value, unsafeAsCustomerId);
194
+ const parseCustomerPassId = (value) => parseUuidBrand(value, unsafeAsCustomerPassId);
195
+ const parseCustomerMembershipId = (value) => parseUuidBrand(value, unsafeAsCustomerMembershipId);
196
+ const parseEventSeriesId = (value) => parseUuidBrand(value, unsafeAsEventSeriesId);
197
+ const parseEnrollmentId = (value) => parseUuidBrand(value, unsafeAsEnrollmentId);
198
+ const parseParticipantId = (value) => parseUuidBrand(value, unsafeAsParticipantId);
199
+ const parseParticipantParticipationId = (value) => parseUuidBrand(value, unsafeAsParticipantParticipationId);
200
+ const parseOrderItemId = (value) => parseUuidBrand(value, unsafeAsOrderItemId);
201
+ const parseOrderId = (value) => parseUuidBrand(value, unsafeAsOrderId);
202
+ const parseOrderRefundId = (value) => parseUuidBrand(value, unsafeAsOrderRefundId);
203
+ const parseOrderPaymentReceiptId = (value) => parseUuidBrand(value, unsafeAsOrderPaymentReceiptId);
204
+ const parseOrderFlexibleBalanceContractId = (value) => parseUuidBrand(value, unsafeAsOrderFlexibleBalanceContractId);
205
+ const parseOrderPaymentPlanId = (value) => parseUuidBrand(value, unsafeAsOrderPaymentPlanId);
206
+ const parseOrderPaymentPlanInstalmentId = (value) => parseUuidBrand(value, unsafeAsOrderPaymentPlanInstalmentId);
207
+ const parseManualPaymentOperationId = (value) => parseUuidBrand(value, unsafeAsManualPaymentOperationId);
208
+
209
+ //#endregion
210
+ //#region ../core/src/date-math.ts
211
+ function addDaysToDate(date, dayCount) {
212
+ const next = new Date(date);
213
+ next.setUTCDate(next.getUTCDate() + dayCount);
214
+ return next;
215
+ }
216
+ function addDaysToIso(isoString, dayCount) {
217
+ const date = new Date(isoString);
218
+ if (!Number.isFinite(date.getTime())) {
219
+ return isoString;
220
+ }
221
+ return addDaysToDate(date, dayCount).toISOString();
222
+ }
223
+ function subtractDaysFromIso(isoString, dayCount) {
224
+ return addDaysToIso(isoString, -dayCount);
225
+ }
226
+
227
+ //#endregion
228
+ //#region ../core/src/duration.ts
229
+ const DURATION_UNIT_MINUTES = {
230
+ minutes: 1,
231
+ hours: 60,
232
+ days: 60 * 24,
233
+ weeks: 60 * 24 * 7
234
+ };
235
+ const UNIT_LABELS_SINGULAR = {
236
+ minutes: "minute",
237
+ hours: "hour",
238
+ days: "day",
239
+ weeks: "week"
240
+ };
241
+ const UNIT_LABELS_PLURAL = {
242
+ minutes: "minutes",
243
+ hours: "hours",
244
+ days: "days",
245
+ weeks: "weeks"
246
+ };
247
+ function formatDurationUnitLabel(unit, count) {
248
+ return count === 1 ? UNIT_LABELS_SINGULAR[unit] : UNIT_LABELS_PLURAL[unit];
249
+ }
250
+ /**
251
+ * Picks the coarsest allowed unit that cleanly divides `minutes`. Falls back
252
+ * to the smallest allowed unit when no clean divisor exists.
253
+ */
254
+ function pickDurationDisplayUnit(minutes, allowed) {
255
+ if (allowed.length === 0) return "minutes";
256
+ if (minutes === 0) return allowed[0] ?? "minutes";
257
+ const sortedDesc = [...allowed].sort((a, b) => DURATION_UNIT_MINUTES[b] - DURATION_UNIT_MINUTES[a]);
258
+ for (const unit of sortedDesc) {
259
+ const unitMinutes = DURATION_UNIT_MINUTES[unit];
260
+ if (minutes >= unitMinutes && minutes % unitMinutes === 0) return unit;
261
+ }
262
+ return allowed[0] ?? "minutes";
263
+ }
264
+ /**
265
+ * Guarantees the widget invariant `displayValue x unit === storedMinutes` is
266
+ * representable by prepending minutes when no caller-provided unit divides.
267
+ */
268
+ function expandAllowedDurationUnits(value, base) {
269
+ if (base.includes("minutes")) return base;
270
+ const dividesAny = base.some((unit) => DURATION_UNIT_MINUTES[unit] > 0 && value % DURATION_UNIT_MINUTES[unit] === 0);
271
+ return dividesAny ? base : ["minutes", ...base];
272
+ }
273
+ function clampDurationMinutes(value, min, max) {
274
+ let result = value;
275
+ if (typeof min === "number" && result < min) result = min;
276
+ if (typeof max === "number" && result > max) result = max;
277
+ return result;
278
+ }
279
+
280
+ //#endregion
281
+ //#region ../core/src/demo-capability-policy.ts
282
+ const demoSurfaceClassificationValues = [
283
+ "allowed",
284
+ "blocked-hidden",
285
+ "blocked-disabled",
286
+ "internal-only",
287
+ "needs-classification"
288
+ ];
289
+ /** Evaluates a classified demo mutation surface and fails closed for inactive claims or unknown surfaces. */
290
+ function evaluateDemoCapability(input) {
291
+ const classification = input.classification;
292
+ if (input.claim.kind !== "active") {
293
+ return {
294
+ allowed: false,
295
+ classification,
296
+ reason: "claim_not_active",
297
+ surface: input.surface
298
+ };
299
+ }
300
+ switch (classification) {
301
+ case "allowed": return {
302
+ allowed: true,
303
+ classification: "allowed"
304
+ };
305
+ case "blocked-hidden":
306
+ case "blocked-disabled": return {
307
+ allowed: false,
308
+ classification,
309
+ reason: "dangerous_surface",
310
+ surface: input.surface
311
+ };
312
+ case "internal-only": return {
313
+ allowed: false,
314
+ classification: "internal-only",
315
+ reason: "internal_only",
316
+ surface: input.surface
317
+ };
318
+ case "needs-classification": return {
319
+ allowed: false,
320
+ classification: "needs-classification",
321
+ reason: "surface_not_classified",
322
+ surface: input.surface
323
+ };
324
+ default: return assertNever(classification);
325
+ }
326
+ }
327
+
328
+ //#endregion
329
+ //#region ../core/src/demo-claim-access.ts
330
+ function deriveDemoClaimAccessState(input) {
331
+ if (input.instance.status !== "claimed") {
332
+ return { kind: input.instance.status === "expired" ? "expired" : "missing" };
333
+ }
334
+ if (input.claim === null) {
335
+ return { kind: "missing" };
336
+ }
337
+ if (input.claim.status === "released") {
338
+ return { kind: "released" };
339
+ }
340
+ if (input.claim.status === "expired") {
341
+ return { kind: "expired" };
342
+ }
343
+ if (new Date(input.claim.expiresAt).getTime() <= input.now.getTime()) {
344
+ return { kind: "expired" };
345
+ }
346
+ return { kind: "active" };
347
+ }
348
+
349
+ //#endregion
350
+ //#region ../core/src/demo-lifecycle-status.ts
351
+ const demoBlueprintStatusValues = [
352
+ "draft",
353
+ "published",
354
+ "archived"
355
+ ];
356
+ const demoInstanceStatusValues = [
357
+ "warming",
358
+ "available",
359
+ "claimed",
360
+ "expired",
361
+ "recycling",
362
+ "failed"
363
+ ];
364
+ const demoClaimStatusValues = [
365
+ "active",
366
+ "expired",
367
+ "released"
368
+ ];
369
+ const demoBlueprintArtifactKindValues = ["site_backup", "manual_snapshot"];
370
+ function narrowDemoInstanceStatus(value) {
371
+ if (demoInstanceStatusValues.includes(value)) {
372
+ return value;
373
+ }
374
+ throw new Error(`Unexpected demo instance status: ${value}`);
375
+ }
376
+ function narrowDemoClaimStatus(value) {
377
+ if (demoClaimStatusValues.includes(value)) {
378
+ return value;
379
+ }
380
+ throw new Error(`Unexpected demo claim status: ${value}`);
381
+ }
382
+ function narrowDemoBlueprintArtifactKind(value) {
383
+ if (demoBlueprintArtifactKindValues.includes(value)) {
384
+ return value;
385
+ }
386
+ throw new Error(`Unexpected demo blueprint artifact kind: ${value}`);
387
+ }
388
+
389
+ //#endregion
390
+ //#region ../core/src/demo-runtime.ts
391
+ const RIVERBANK_RUNTIME_ENV_VAR = "RIVERBANK_RUNTIME_ENV";
392
+ const riverbankRuntimeEnvValues = [
393
+ "production",
394
+ "staging",
395
+ "demo"
396
+ ];
397
+ const runtimeByEnv = {
398
+ production: {
399
+ kind: "production",
400
+ demoAllowed: false
401
+ },
402
+ staging: {
403
+ kind: "staging",
404
+ demoAllowed: false
405
+ },
406
+ demo: {
407
+ kind: "demo",
408
+ demoAllowed: true
409
+ }
410
+ };
411
+ function parseRiverbankRuntimeEnv(rawValue) {
412
+ if (rawValue == null || rawValue.trim() === "") {
413
+ return {
414
+ kind: "invalid",
415
+ error: {
416
+ kind: "missing_runtime_env",
417
+ envVar: RIVERBANK_RUNTIME_ENV_VAR
418
+ }
419
+ };
420
+ }
421
+ const normalizedValue = rawValue.trim().toLowerCase();
422
+ if (isRiverbankRuntimeEnv(normalizedValue)) {
423
+ return {
424
+ kind: "resolved",
425
+ runtime: runtimeByEnv[normalizedValue]
426
+ };
427
+ }
428
+ return {
429
+ kind: "invalid",
430
+ error: {
431
+ kind: "unsupported_runtime_env",
432
+ envVar: RIVERBANK_RUNTIME_ENV_VAR,
433
+ rawValue,
434
+ normalizedValue,
435
+ supportedValues: riverbankRuntimeEnvValues
436
+ }
437
+ };
438
+ }
439
+ function resolveRiverbankRuntimeEnv(env) {
440
+ return parseRiverbankRuntimeEnv(env[RIVERBANK_RUNTIME_ENV_VAR]);
441
+ }
442
+ function requireDemoRuntime(resolution) {
443
+ if (resolution.kind === "invalid") {
444
+ return {
445
+ allowed: false,
446
+ reason: resolution.error.kind,
447
+ error: resolution.error
448
+ };
449
+ }
450
+ if (resolution.runtime.kind === "demo") {
451
+ return {
452
+ allowed: true,
453
+ runtime: resolution.runtime
454
+ };
455
+ }
456
+ return {
457
+ allowed: false,
458
+ reason: "not_demo_runtime",
459
+ runtime: resolution.runtime
460
+ };
461
+ }
462
+ function isRiverbankRuntimeEnv(value) {
463
+ return riverbankRuntimeEnvValues.includes(value);
464
+ }
465
+
466
+ //#endregion
467
+ //#region ../core/src/collections.ts
468
+ function dedupeStrings(values) {
469
+ return [...new Set(values)];
470
+ }
471
+
472
+ //#endregion
473
+ //#region ../core/src/assert-never.ts
474
+ function assertNever(value, message = "Unexpected variant") {
475
+ throw new Error(`${message}: ${String(value)}`);
476
+ }
477
+
478
+ //#endregion
479
+ //#region ../core/src/currency-format.ts
480
+ const CURRENCY_CODE_INPUT_PATTERN = /^[A-Za-z]{3}$/;
481
+ const CANONICAL_CURRENCY_CODE_PATTERN = /^[A-Z]{3}$/;
482
+ function isCurrencyCode(value) {
483
+ return typeof value === "string" && CANONICAL_CURRENCY_CODE_PATTERN.test(value);
484
+ }
485
+ /** Parse + normalize a currency code, or return null if it isn't a 3-letter code. */
486
+ function parseCurrencyCode(value) {
487
+ return CURRENCY_CODE_INPUT_PATTERN.test(value) ? value.toUpperCase() : null;
488
+ }
489
+ /**
490
+ * Brand an already-trusted currency code (DB column, generated constant, fixture).
491
+ * Use `parseCurrencyCode` for untrusted boundary values.
492
+ */
493
+ function unsafeCurrencyCode(value) {
494
+ return value.toUpperCase();
495
+ }
496
+ const CURRENCY_LOCALE_BY_CODE = {
497
+ USD: "en-US",
498
+ GBP: "en-GB",
499
+ EUR: "de-DE",
500
+ AUD: "en-AU",
501
+ CAD: "en-CA",
502
+ NZD: "en-NZ",
503
+ JPY: "ja-JP",
504
+ CNY: "zh-CN",
505
+ CHF: "de-CH"
506
+ };
507
+ function resolveCurrencyLocale(currencyCode, override) {
508
+ return override ?? CURRENCY_LOCALE_BY_CODE[currencyCode] ?? "en";
509
+ }
510
+ /**
511
+ * The single currency-amount formatter for the product: minor units in,
512
+ * localized string out.
513
+ *
514
+ * The locale is derived from the currency by default, so output is deterministic
515
+ * across server and client (no SSR/hydration mismatch). All other money
516
+ * formatters in the codebase should delegate here rather than constructing their
517
+ * own `Intl.NumberFormat`.
518
+ */
519
+ function formatCurrencyAmount(amountCents, currency, options) {
520
+ const currencyCode = currency.toUpperCase();
521
+ const locale = resolveCurrencyLocale(currencyCode, options?.locale);
522
+ try {
523
+ const exactFormatter = new Intl.NumberFormat(locale, {
524
+ style: "currency",
525
+ currency: currencyCode
526
+ });
527
+ const fractionDigits = options?.decimals === "auto" ? {
528
+ minimumFractionDigits: 0,
529
+ maximumFractionDigits: exactFormatter.resolvedOptions().maximumFractionDigits
530
+ } : {};
531
+ return new Intl.NumberFormat(locale, {
532
+ style: "currency",
533
+ currency: currencyCode,
534
+ ...fractionDigits
535
+ }).format(amountCents / 100);
536
+ } catch {
537
+ return `${(amountCents / 100).toFixed(2)} ${currencyCode}`;
538
+ }
539
+ }
540
+
541
+ //#endregion
542
+ //#region ../core/src/event-pricing.ts
543
+ const EVENT_PRICING_MODES = [
544
+ "single",
545
+ "ticket_types",
546
+ "sliding_scale"
547
+ ];
548
+ function isEventPricingMode(value) {
549
+ return typeof value === "string" && EVENT_PRICING_MODES.includes(value);
550
+ }
551
+ function publicEventPricingDisplay(pricing) {
552
+ return pricing.display;
553
+ }
554
+ function publicEventPricingType(pricing) {
555
+ switch (pricing.kind) {
556
+ case "drop_in":
557
+ case "free_registration": return "free";
558
+ case "single_paid":
559
+ case "ticket_types":
560
+ case "sliding_scale":
561
+ case "course": return isFreePublicEventPriceDisplay(pricing.display) ? "free" : "paid";
562
+ case "external": return "external";
563
+ }
564
+ }
565
+ function publicEventPricingCurrency(pricing) {
566
+ switch (pricing.kind) {
567
+ case "drop_in":
568
+ case "free_registration":
569
+ case "single_paid":
570
+ case "ticket_types":
571
+ case "sliding_scale": return pricing.currency;
572
+ case "course": return pricing.display.display.currency;
573
+ case "external": return null;
574
+ }
575
+ }
576
+ function publicEventScalarPrice(pricing) {
577
+ return pricing.kind === "single_paid" ? pricing.amount : null;
578
+ }
579
+ function publicEventPricingMode(pricing) {
580
+ switch (pricing.kind) {
581
+ case "single_paid":
582
+ case "free_registration": return "single";
583
+ case "ticket_types": return "ticket_types";
584
+ case "sliding_scale": return "sliding_scale";
585
+ case "drop_in":
586
+ case "course":
587
+ case "external": return undefined;
588
+ }
589
+ }
590
+ function publicEventTicketTypes(pricing) {
591
+ return pricing.kind === "ticket_types" ? pricing.ticketTypes : [];
592
+ }
593
+ function isFreePublicEventPriceDisplay(display) {
594
+ switch (display.kind) {
595
+ case "free": return true;
596
+ case "course": return display.display.kind === "free";
597
+ case "exact":
598
+ case "from":
599
+ case "range":
600
+ case "mixed":
601
+ case "unavailable":
602
+ case "external": return false;
603
+ }
604
+ }
605
+ function formatCoursePriceDisplayLabel(display, options) {
606
+ switch (display.kind) {
607
+ case "free":
608
+ case "unavailable": return "Course";
609
+ case "exact": return `Course (${formatCurrencyAmount(display.amount, display.currency, options)})`;
610
+ case "from": return `Course (From ${formatCurrencyAmount(display.amount, display.currency, options)})`;
611
+ case "range": return `Course (${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)})`;
612
+ default: return assertNever(display);
613
+ }
614
+ }
615
+ /**
616
+ * The single renderer for a precomputed booking price `display` ADT. This is
617
+ * the one place booking price variants map to user-facing strings — every call
618
+ * site (booking entry, events calendar, event/course details, listings, portal)
619
+ * should route through here so labels never drift. Adding a variant to the ADT
620
+ * breaks compilation here (via `assertNever`) until it is handled.
621
+ */
622
+ function formatBookingPriceDisplay(display, options = {}) {
623
+ switch (display.kind) {
624
+ case "free": return "Free";
625
+ case "exact": return formatCurrencyAmount(display.amount, display.currency, options);
626
+ case "from": return `From ${formatCurrencyAmount(display.amount, display.currency, options)}`;
627
+ case "range": return `${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)}`;
628
+ case "mixed": return display.paidToAmount > display.paidFromAmount ? `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)}` : `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)}`;
629
+ case "unavailable": return "Pricing unavailable";
630
+ case "external": return display.label;
631
+ case "course": return formatCoursePriceDisplayLabel(display.display, options);
632
+ default: return assertNever(display);
633
+ }
634
+ }
635
+ /** Render an event's price directly from its pricing value. */
636
+ function formatEventPricing(pricing, options = {}) {
637
+ return formatBookingPriceDisplay(publicEventPricingDisplay(pricing), options);
638
+ }
639
+
640
+ //#endregion
641
+ //#region ../core/src/event-page-surface-subject.ts
642
+ function createEventSeriesPageSurfaceSubject(eventSeriesId, bookingScope) {
643
+ return bookingScope === "whole_series" ? {
644
+ kind: "eventSeries",
645
+ eventSeriesId,
646
+ bookingScope,
647
+ isWholeSeriesBooking: true
648
+ } : {
649
+ kind: "eventSeries",
650
+ eventSeriesId,
651
+ bookingScope
652
+ };
653
+ }
654
+ function parseEventPageSurfaceSubject(value) {
655
+ if (!isRecord$6(value)) return err({ kind: "notObject" });
656
+ switch (value.kind) {
657
+ case "eventSeries": {
658
+ const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
659
+ if (!eventSeriesId.ok) {
660
+ return err({
661
+ kind: "invalidEventSeriesId",
662
+ error: eventSeriesId.error
663
+ });
664
+ }
665
+ if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
666
+ return err({
667
+ kind: "invalidEventSeriesBookingScope",
668
+ bookingScope: value.bookingScope
669
+ });
670
+ }
671
+ return ok(createEventSeriesPageSurfaceSubject(eventSeriesId.value, value.bookingScope));
672
+ }
673
+ case "eventOffering": {
674
+ const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
675
+ if (!eventOfferingId.ok) {
676
+ return err({
677
+ kind: "invalidEventOfferingId",
678
+ error: eventOfferingId.error
679
+ });
680
+ }
681
+ if (!Array.isArray(value.availableEventSeriesIds)) {
682
+ return err({
683
+ kind: "unsupportedSubject",
684
+ subjectKind: value.kind
685
+ });
686
+ }
687
+ const parsedSeriesIds = [];
688
+ for (const valueId of value.availableEventSeriesIds) {
689
+ const eventSeriesId = parseEventSeriesId(valueId);
690
+ if (!eventSeriesId.ok) {
691
+ return err({
692
+ kind: "invalidEventSeriesId",
693
+ error: eventSeriesId.error
694
+ });
695
+ }
696
+ parsedSeriesIds.push(eventSeriesId.value);
697
+ }
698
+ return ok({
699
+ kind: "eventOffering",
700
+ eventOfferingId: eventOfferingId.value,
701
+ availableEventSeriesIds: parsedSeriesIds
702
+ });
703
+ }
704
+ default: return err({
705
+ kind: "unsupportedSubject",
706
+ subjectKind: value.kind
707
+ });
708
+ }
709
+ }
710
+ function toEventPageSurfaceSubjectWire(subject) {
711
+ return subject;
712
+ }
713
+ function isRecord$6(value) {
714
+ return typeof value === "object" && value !== null && !Array.isArray(value);
715
+ }
716
+
717
+ //#endregion
718
+ //#region ../core/src/money-input.ts
719
+ function unsafeAsCents(value) {
720
+ return value;
721
+ }
722
+ function unsafeAsNonNegativeCents(value) {
723
+ return value;
724
+ }
725
+ function unsafeAsPositiveCents(value) {
726
+ return value;
727
+ }
728
+ function unwrapCents(value) {
729
+ return value;
730
+ }
731
+ function isWholeCents(value) {
732
+ return Number.isFinite(value) && Number.isInteger(value);
733
+ }
734
+ function isNonNegativeWholeCents(value) {
735
+ return isWholeCents(value) && value >= 0;
736
+ }
737
+ function isPositiveWholeCents(value) {
738
+ return isWholeCents(value) && value > 0;
739
+ }
740
+ /**
741
+ * Normalizes a derived amount to signed whole cents. Use parse*Cents for
742
+ * untrusted input that should fail instead of truncating or clamping.
743
+ */
744
+ function toCents(value) {
745
+ return unsafeAsCents(Math.trunc(value));
746
+ }
747
+ /**
748
+ * Normalizes a derived amount to non-negative whole cents. This intentionally
749
+ * clamps negatives to zero; use parseNonNegativeCents for validation.
750
+ */
751
+ function toNonNegativeCents(value) {
752
+ return unsafeAsNonNegativeCents(Math.max(0, Math.trunc(value)));
753
+ }
754
+ function parseCents(value) {
755
+ if (!Number.isFinite(value)) {
756
+ return err({ kind: "invalid-number" });
757
+ }
758
+ if (!Number.isInteger(value)) {
759
+ return err({ kind: "not-whole-cents" });
760
+ }
761
+ return ok(unsafeAsCents(value));
762
+ }
763
+ function parseNonNegativeCents(value) {
764
+ const parsed = parseCents(value);
765
+ if (!parsed.ok) {
766
+ return parsed;
767
+ }
768
+ if (parsed.value < 0) {
769
+ return err({ kind: "negative" });
770
+ }
771
+ return ok(unsafeAsNonNegativeCents(parsed.value));
772
+ }
773
+ function parsePositiveCents(value) {
774
+ const parsed = parseNonNegativeCents(value);
775
+ if (!parsed.ok) {
776
+ return parsed;
777
+ }
778
+ if (parsed.value === 0) {
779
+ return err({ kind: "zero-not-allowed" });
780
+ }
781
+ return ok(unsafeAsPositiveCents(parsed.value));
782
+ }
783
+ function normalizeMoneyInput(value, options) {
784
+ return (options.stripFormatting === true ? value.replace(/[^0-9.-]/g, "") : value).trim();
785
+ }
786
+ function parseFiniteMoneyAmount(value, options) {
787
+ const normalized = normalizeMoneyInput(value, options);
788
+ if (normalized.length === 0) {
789
+ return err({ kind: "empty" });
790
+ }
791
+ const amount = Number(normalized);
792
+ if (!Number.isFinite(amount)) {
793
+ return err({ kind: "invalid-number" });
794
+ }
795
+ return ok(amount);
796
+ }
797
+ function roundMoneyAmountToCents(amount) {
798
+ return Math.round(amount * 100);
799
+ }
800
+ function parsePositiveMoneyInputToCents(value, options = {}) {
801
+ const parsed = parseFiniteMoneyAmount(value, options);
802
+ if (!parsed.ok) {
803
+ return parsed;
804
+ }
805
+ if (parsed.value < 0) {
806
+ return err({ kind: "negative" });
807
+ }
808
+ const amountCents = roundMoneyAmountToCents(parsed.value);
809
+ if (amountCents <= 0) {
810
+ return err({ kind: "zero-not-allowed" });
811
+ }
812
+ return ok(unsafeAsPositiveCents(amountCents));
813
+ }
814
+ function parseNonNegativeMoneyInputToCents(value, options = {}) {
815
+ const parsed = parseFiniteMoneyAmount(value, options);
816
+ if (!parsed.ok) {
817
+ return parsed;
818
+ }
819
+ if (parsed.value < 0) {
820
+ return err({ kind: "negative" });
821
+ }
822
+ return ok(unsafeAsNonNegativeCents(roundMoneyAmountToCents(parsed.value)));
823
+ }
824
+ function parseMoneyInputToCents(value, options = {}) {
825
+ const parsed = parseFiniteMoneyAmount(value, options);
826
+ if (!parsed.ok) {
827
+ return null;
828
+ }
829
+ if (options.allowZero === true ? parsed.value < 0 : parsed.value <= 0) {
830
+ return null;
831
+ }
832
+ return roundMoneyAmountToCents(parsed.value);
833
+ }
834
+ function formatMoneyInputFromCents(amountCents) {
835
+ if (typeof amountCents !== "number" || !Number.isFinite(amountCents)) {
836
+ return "";
837
+ }
838
+ const sign = amountCents < 0 ? "-" : "";
839
+ const absoluteCents = Math.abs(Math.trunc(amountCents));
840
+ const units = Math.floor(absoluteCents / 100);
841
+ const fractional = String(absoluteCents % 100).padStart(2, "0");
842
+ return `${sign}${units}.${fractional}`;
843
+ }
844
+
845
+ //#endregion
846
+ //#region ../core/src/booking-flexible-balance.ts
847
+ const DEFAULT_FLEXIBLE_BALANCE_STRIPE_MINIMUM_CHARGE_CENTS = 50;
848
+ function clampFlexibleBalanceConfiguredAmount(amountCents, totalAmountCents) {
849
+ return Math.max(0, Math.min(amountCents, totalAmountCents));
850
+ }
851
+ function resolveFlexibleBalanceInitialPaymentCents(input) {
852
+ return clampFlexibleBalanceConfiguredAmount(input.config.upfrontAmountCents, input.totalAmountCents);
853
+ }
854
+ function calculateFlexibleBalanceRemainingBalanceCents(input) {
855
+ return Math.max(0, input.totalAmountCents - input.paidAmountCents);
856
+ }
857
+ function resolveFlexibleBalanceDueAt(input) {
858
+ switch (input.dueDateRule.kind) {
859
+ case "relative_to_start_date": return subtractDaysFromIso(input.bookingStartAt, input.dueDateRule.daysBeforeStart);
860
+ case "fixed_calendar_date": return input.dueDateRule.dueAt;
861
+ default: return assertNever$4(input.dueDateRule);
862
+ }
863
+ }
864
+ function calculateFlexibleBalanceMinimumPaymentCents(input) {
865
+ if (input.remainingBalanceCents <= 0) {
866
+ return 0;
867
+ }
868
+ const minimumPercentageBps = input.minimumPercentageBps ?? 1e3;
869
+ const percentageFloor = Math.ceil(input.remainingBalanceCents * minimumPercentageBps / 1e4);
870
+ return Math.min(input.remainingBalanceCents, Math.max(input.stripeMinimumChargeCents, percentageFloor));
871
+ }
872
+ function resolveFlexibleBalancePaymentEligibility(input) {
873
+ if (input.remainingBalanceCents <= 0 || input.status === "completed") {
874
+ return {
875
+ kind: "unavailable",
876
+ reason: "already_settled"
877
+ };
878
+ }
879
+ switch (input.status) {
880
+ case "payment_pending": return {
881
+ kind: "unavailable",
882
+ reason: "payment_pending"
883
+ };
884
+ case "cancelled_due_to_non_payment": return {
885
+ kind: "unavailable",
886
+ reason: "cancelled_due_to_non_payment"
887
+ };
888
+ case "refunded": return {
889
+ kind: "unavailable",
890
+ reason: "refunded"
891
+ };
892
+ case "waived": return {
893
+ kind: "unavailable",
894
+ reason: "waived"
895
+ };
896
+ case "overdue": return {
897
+ kind: "full_balance_only",
898
+ remainingBalanceCents: input.remainingBalanceCents,
899
+ reason: "overdue"
900
+ };
901
+ case "active": {
902
+ const minimumAmountCents = calculateFlexibleBalanceMinimumPaymentCents({
903
+ remainingBalanceCents: input.remainingBalanceCents,
904
+ stripeMinimumChargeCents: input.stripeMinimumChargeCents,
905
+ minimumPercentageBps: input.minimumPercentageBps
906
+ });
907
+ if (minimumAmountCents >= input.remainingBalanceCents) {
908
+ return {
909
+ kind: "full_balance_only",
910
+ remainingBalanceCents: input.remainingBalanceCents,
911
+ reason: "dust_balance"
912
+ };
913
+ }
914
+ return {
915
+ kind: "eligible",
916
+ minimumAmountCents,
917
+ maximumCustomAmountCents: input.remainingBalanceCents,
918
+ suggestedPayFullAmountCents: input.remainingBalanceCents
919
+ };
920
+ }
921
+ default: return assertNever$4(input.status);
922
+ }
923
+ }
924
+ function isFlexibleBalanceReminderSuppressed(input) {
925
+ const suppressionWindowHours = input.suppressionWindowHours ?? 24;
926
+ const bookedAtMs = Date.parse(input.bookedAt);
927
+ const reminderDueAtMs = Date.parse(input.reminderDueAt);
928
+ if (!Number.isFinite(bookedAtMs) || !Number.isFinite(reminderDueAtMs)) {
929
+ return false;
930
+ }
931
+ return reminderDueAtMs - bookedAtMs < suppressionWindowHours * 60 * 60 * 1e3;
932
+ }
933
+ function buildFlexibleBalanceReminderSchedule(input) {
934
+ const schedule = [
935
+ {
936
+ kind: "due_7_days",
937
+ scheduledFor: subtractDaysFromIso(input.dueAt, 7)
938
+ },
939
+ {
940
+ kind: "due_1_day",
941
+ scheduledFor: subtractDaysFromIso(input.dueAt, 1)
942
+ },
943
+ {
944
+ kind: "overdue_1_day",
945
+ scheduledFor: addDaysToIso(input.dueAt, 1)
946
+ }
947
+ ];
948
+ return schedule.map((entry) => ({
949
+ kind: entry.kind,
950
+ scheduledFor: entry.scheduledFor,
951
+ suppressed: isFlexibleBalanceReminderSuppressed({
952
+ bookedAt: input.bookedAt,
953
+ reminderDueAt: entry.scheduledFor
954
+ })
955
+ }));
956
+ }
957
+ function assertNever$4(value) {
958
+ throw new Error(`Unexpected value: ${String(value)}`);
959
+ }
960
+
961
+ //#endregion
962
+ //#region ../core/src/booking-refunds.ts
963
+ function normalizeBookingProtectedDepositPolicyState(value) {
964
+ return value === "not_applicable" || value === "refundable" || value === "non_refundable" ? value : "not_applicable";
965
+ }
966
+ function shouldAllowProtectedDepositOverride(command) {
967
+ return command.protectedDepositOverride === "allow_protected_deposit_override";
968
+ }
969
+ function createBookingRefundAmountRequest(amountCents) {
970
+ return amountCents == null ? { kind: "full_remaining" } : {
971
+ kind: "partial_amount",
972
+ amountCents
973
+ };
974
+ }
975
+ function createOriginalPaymentRefundCommand(options) {
976
+ const protectedDepositOverride = options.protectedDepositOverride ?? "respect_policy";
977
+ return options.kind === "refund_and_revoke" ? {
978
+ kind: "refund_and_revoke",
979
+ amount: { kind: "full_remaining" },
980
+ requestedRefundMethod: "original_payment",
981
+ protectedDepositOverride
982
+ } : {
983
+ kind: "refund_only",
984
+ amount: options.amount ?? { kind: "full_remaining" },
985
+ requestedRefundMethod: "original_payment",
986
+ protectedDepositOverride
987
+ };
988
+ }
989
+
990
+ //#endregion
991
+ //#region ../core/src/booking-instalments.ts
992
+ function isDepositBearingBookingInstalmentPlan(plan) {
993
+ switch (plan.kind) {
994
+ case "deposit_plus_one_remainder":
995
+ case "deposit_plus_scheduled_instalments": return true;
996
+ case "equal_scheduled_instalments": return false;
997
+ default: return assertNever$3(plan);
998
+ }
999
+ }
1000
+ function getBookingInstalmentDepositRefundability(plan) {
1001
+ return isDepositBearingBookingInstalmentPlan(plan) ? plan.depositRefundability : null;
1002
+ }
1003
+ function getBookingInstalmentProtectedDepositAmountCents(plan) {
1004
+ return isDepositBearingBookingInstalmentPlan(plan) ? plan.depositAmountCents : null;
1005
+ }
1006
+ function clampConfiguredAmount(amountCents, totalAmountCents) {
1007
+ return Math.max(0, Math.min(amountCents, totalAmountCents));
1008
+ }
1009
+ function splitAmountWithRemainderInFirst(totalAmountCents, partCount) {
1010
+ if (partCount <= 0) {
1011
+ return [];
1012
+ }
1013
+ const baseAmountCents = Math.floor(totalAmountCents / partCount);
1014
+ const remainderCents = totalAmountCents - baseAmountCents * partCount;
1015
+ return Array.from({ length: partCount }, (_, index) => index === 0 ? baseAmountCents + remainderCents : baseAmountCents);
1016
+ }
1017
+ function addIntervalPreservingMonthAnchor(isoString, intervalCount, intervalUnit) {
1018
+ const date = new Date(isoString);
1019
+ if (!Number.isFinite(date.getTime())) {
1020
+ return isoString;
1021
+ }
1022
+ switch (intervalUnit) {
1023
+ case "day":
1024
+ date.setUTCDate(date.getUTCDate() + intervalCount);
1025
+ return date.toISOString();
1026
+ case "week":
1027
+ date.setUTCDate(date.getUTCDate() + intervalCount * 7);
1028
+ return date.toISOString();
1029
+ case "month": return addMonthsPreservingAnchorDay(date, intervalCount).toISOString();
1030
+ default: return assertNever$3(intervalUnit);
1031
+ }
1032
+ }
1033
+ function getBookingInstalmentPlanIntervalCount(plan) {
1034
+ switch (plan.kind) {
1035
+ case "deposit_plus_one_remainder": return plan.remainderDueCount;
1036
+ case "deposit_plus_scheduled_instalments":
1037
+ case "equal_scheduled_instalments": return plan.intervalCount;
1038
+ default: return assertNever$3(plan);
1039
+ }
1040
+ }
1041
+ function omitZeroAmountFutureRows(rows) {
1042
+ if (rows.length <= 1) {
1043
+ return rows;
1044
+ }
1045
+ const firstRow = rows[0];
1046
+ if (firstRow === undefined) {
1047
+ return [];
1048
+ }
1049
+ const futureRows = rows.slice(1);
1050
+ return [firstRow, ...futureRows.filter((row) => row.amountCents > 0)];
1051
+ }
1052
+ function formatOrderInstalmentStatusLabel(input) {
1053
+ switch (input.status) {
1054
+ case "pending_initial_payment": return input.phaseKind === "initial" ? "Due today" : "Scheduled";
1055
+ case "scheduled": return "Scheduled";
1056
+ case "paid": return input.phaseKind === "initial" ? "Paid today" : "Paid";
1057
+ case "failed": return "Payment failed";
1058
+ case "waived": return "Waived";
1059
+ case "cancelled": return "Cancelled";
1060
+ case "cancelled_as_paid_early": return "Paid early";
1061
+ case "refunded": return "Refunded";
1062
+ default: return assertNever$3(input.status);
1063
+ }
1064
+ }
1065
+ const NEXT_DUE_STATUSES = new Set(["scheduled", "failed"]);
1066
+ function buildOrderInstalmentSummary(input) {
1067
+ const nextDue = input.instalments.find((instalment) => NEXT_DUE_STATUSES.has(instalment.status)) ?? null;
1068
+ return {
1069
+ status: input.paymentPlan.status,
1070
+ remainingBalanceCents: input.paymentPlan.remainingBalanceCents,
1071
+ nextDueAt: nextDue?.dueAt ?? null,
1072
+ nextDueAmountCents: nextDue?.amountCents ?? null,
1073
+ rows: [...input.instalments]
1074
+ };
1075
+ }
1076
+ function addMonthsPreservingAnchorDay(date, intervalCount) {
1077
+ const anchorDay = date.getUTCDate();
1078
+ const nextMonth = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + intervalCount, 1, date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds()));
1079
+ const lastDayOfTargetMonth = new Date(Date.UTC(nextMonth.getUTCFullYear(), nextMonth.getUTCMonth() + 1, 0)).getUTCDate();
1080
+ nextMonth.setUTCDate(Math.min(anchorDay, lastDayOfTargetMonth));
1081
+ return nextMonth;
1082
+ }
1083
+ function assertNever$3(value) {
1084
+ throw new Error(`Unexpected value: ${String(value)}`);
1085
+ }
1086
+
1087
+ //#endregion
1088
+ //#region ../core/src/booking-time-zone.ts
1089
+ const TIME_ZONE_VALIDITY_CACHE_LIMIT = 100;
1090
+ const timeZoneValidityCache = new Map();
1091
+ const NUMERIC_OFFSET_TIME_ZONE_PATTERN = /^(?:[+-]\d{2}(?::?\d{2})?|(?:UTC|GMT)[+-]\d{1,2}(?::?\d{2})?)$/iu;
1092
+ function isValidBookingTimeZone(timeZone) {
1093
+ const normalized = timeZone.trim();
1094
+ if (NUMERIC_OFFSET_TIME_ZONE_PATTERN.test(normalized)) return false;
1095
+ const cached = timeZoneValidityCache.get(normalized);
1096
+ if (cached !== undefined) return cached;
1097
+ let isValid;
1098
+ try {
1099
+ new Intl.DateTimeFormat("en-US", { timeZone: normalized }).format(new Date());
1100
+ isValid = true;
1101
+ } catch {
1102
+ isValid = false;
1103
+ }
1104
+ if (timeZoneValidityCache.size >= TIME_ZONE_VALIDITY_CACHE_LIMIT) {
1105
+ timeZoneValidityCache.clear();
1106
+ }
1107
+ timeZoneValidityCache.set(normalized, isValid);
1108
+ return isValid;
1109
+ }
1110
+ function parseBookingTimeZone(timeZone) {
1111
+ if (typeof timeZone !== "string") return err("invalid-time-zone");
1112
+ const normalized = timeZone.trim();
1113
+ if (normalized.length === 0 || NUMERIC_OFFSET_TIME_ZONE_PATTERN.test(normalized) || !isValidBookingTimeZone(normalized)) {
1114
+ return err("invalid-time-zone");
1115
+ }
1116
+ return ok(normalized);
1117
+ }
1118
+
1119
+ //#endregion
1120
+ //#region ../core/src/booking-date-time.ts
1121
+ const DEFAULT_LOCALE = "en-US";
1122
+ const FALLBACK_TIMEZONE = "UTC";
1123
+ const DEFAULT_BOOKING_DATE_TIME_FORMAT = {
1124
+ weekday: "short",
1125
+ month: "short",
1126
+ day: "numeric",
1127
+ hour: "numeric",
1128
+ minute: "2-digit"
1129
+ };
1130
+ const START_OF_DAY = {
1131
+ hour: 0,
1132
+ minute: 0,
1133
+ second: 0
1134
+ };
1135
+ const LOCAL_DATE_PATTERN = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/u;
1136
+ const DATE_TIME_FORMATTER_CACHE_LIMIT = 100;
1137
+ const dateTimeFormatterCache = new Map();
1138
+ function getSafeTimeZone(timeZone) {
1139
+ const normalized = typeof timeZone === "string" ? timeZone.trim() : "";
1140
+ return normalized.length > 0 && isValidBookingTimeZone(normalized) ? normalized : FALLBACK_TIMEZONE;
1141
+ }
1142
+ function parseStrictBookingTimeZone(timeZone) {
1143
+ const parsed = parseBookingTimeZone(timeZone);
1144
+ return parsed.ok ? parsed.value : null;
1145
+ }
1146
+ function formatParseError(error) {
1147
+ switch (error.reason) {
1148
+ case "invalid_starts_at": return `Expected a valid UTC/offset-aware ISO datetime in startsAt, received "${error.value}"`;
1149
+ case "invalid_ends_at": return `Expected a valid UTC/offset-aware ISO datetime in endsAt, received "${error.value ?? ""}"`;
1150
+ case "invalid_time_zone": return `Expected a valid IANA timezone in timeZone, received "${error.value}"`;
1151
+ }
1152
+ }
1153
+ function formatToPartsLookup(date, locale, timeZone, format) {
1154
+ return new Map(getCachedDateTimeFormatter(locale, timeZone, format).formatToParts(date).map((part) => [part.type, part.value]));
1155
+ }
1156
+ function getCachedDateTimeFormatter(locale, timeZone, format) {
1157
+ const formatKey = JSON.stringify(Object.entries(format).sort(([left], [right]) => left.localeCompare(right)));
1158
+ const key = `${locale}\u0000${timeZone}\u0000${formatKey}`;
1159
+ const cached = dateTimeFormatterCache.get(key);
1160
+ if (cached !== undefined) {
1161
+ return cached;
1162
+ }
1163
+ const formatter = new Intl.DateTimeFormat(locale, {
1164
+ ...format,
1165
+ timeZone
1166
+ });
1167
+ if (dateTimeFormatterCache.size >= DATE_TIME_FORMATTER_CACHE_LIMIT) {
1168
+ dateTimeFormatterCache.clear();
1169
+ }
1170
+ dateTimeFormatterCache.set(key, formatter);
1171
+ return formatter;
1172
+ }
1173
+ function getZonedDateTimeParts(date, timeZone) {
1174
+ const parts = formatToPartsLookup(date, "en-GB", timeZone, {
1175
+ year: "numeric",
1176
+ month: "2-digit",
1177
+ day: "2-digit",
1178
+ hour: "2-digit",
1179
+ minute: "2-digit",
1180
+ second: "2-digit",
1181
+ hour12: false
1182
+ });
1183
+ const read = (type) => Number(parts.get(type) ?? "");
1184
+ return {
1185
+ year: read("year"),
1186
+ month: read("month"),
1187
+ day: read("day"),
1188
+ hour: read("hour"),
1189
+ minute: read("minute"),
1190
+ second: read("second")
1191
+ };
1192
+ }
1193
+ function formatDateKey(date) {
1194
+ const year = String(date.year).padStart(4, "0");
1195
+ const month = String(date.month).padStart(2, "0");
1196
+ const day = String(date.day).padStart(2, "0");
1197
+ return `${year}-${month}-${day}`;
1198
+ }
1199
+ function toBookingTimeZone(timeZone) {
1200
+ return getSafeTimeZone(timeZone);
1201
+ }
1202
+ const DEFAULT_BOOKING_TIME_ZONE = FALLBACK_TIMEZONE;
1203
+ function resolveBookingTimezone(venueTimezone, siteDefaultTimezone) {
1204
+ return venueTimezone ?? siteDefaultTimezone;
1205
+ }
1206
+ function resolveBookingTimeZoneId(...candidates) {
1207
+ for (const candidate of candidates) {
1208
+ const normalized = typeof candidate === "string" ? candidate.trim() : "";
1209
+ if (normalized && isValidBookingTimeZone(normalized)) {
1210
+ return normalized;
1211
+ }
1212
+ }
1213
+ return FALLBACK_TIMEZONE;
1214
+ }
1215
+ function parseUtcIsoDateTimeString(value) {
1216
+ const normalized = value.trim();
1217
+ const hasUtcSuffix = normalized.endsWith("Z");
1218
+ const hasExplicitOffset = /[+-]\d{2}:\d{2}$/u.test(normalized);
1219
+ if (!hasUtcSuffix && !hasExplicitOffset) {
1220
+ return err("missing-timezone-offset");
1221
+ }
1222
+ const date = new Date(normalized);
1223
+ if (Number.isNaN(date.getTime())) {
1224
+ return err("invalid-date-time");
1225
+ }
1226
+ return ok(date.toISOString());
1227
+ }
1228
+ function toUtcIsoDateTimeString(value) {
1229
+ const parsed = parseUtcIsoDateTimeString(value);
1230
+ if (!parsed.ok) {
1231
+ const message = parsed.error === "missing-timezone-offset" ? `Expected an offset-aware ISO datetime, received "${value}"` : `Expected a valid ISO datetime, received "${value}"`;
1232
+ throw new Error(message);
1233
+ }
1234
+ return parsed.value;
1235
+ }
1236
+ function parseScheduledBookingDateTime(input) {
1237
+ let startsAt;
1238
+ try {
1239
+ startsAt = toUtcIsoDateTimeString(input.startsAt);
1240
+ } catch {
1241
+ return {
1242
+ ok: false,
1243
+ field: "startsAt",
1244
+ reason: "invalid_starts_at",
1245
+ value: input.startsAt
1246
+ };
1247
+ }
1248
+ const timeZone = parseStrictBookingTimeZone(input.timeZone);
1249
+ if (timeZone === null) {
1250
+ return {
1251
+ ok: false,
1252
+ field: "timeZone",
1253
+ reason: "invalid_time_zone",
1254
+ value: typeof input.timeZone === "string" ? input.timeZone : ""
1255
+ };
1256
+ }
1257
+ return {
1258
+ ok: true,
1259
+ value: {
1260
+ startsAt,
1261
+ timeZone,
1262
+ locale: input.locale
1263
+ }
1264
+ };
1265
+ }
1266
+ function parseScheduledBookingDateRange(input) {
1267
+ const start = parseScheduledBookingDateTime(input);
1268
+ if (!start.ok) {
1269
+ return start;
1270
+ }
1271
+ if (input.endsAt == null) {
1272
+ return {
1273
+ ok: true,
1274
+ value: start.value
1275
+ };
1276
+ }
1277
+ try {
1278
+ return {
1279
+ ok: true,
1280
+ value: {
1281
+ ...start.value,
1282
+ endsAt: toUtcIsoDateTimeString(input.endsAt)
1283
+ }
1284
+ };
1285
+ } catch {
1286
+ return {
1287
+ ok: false,
1288
+ field: "endsAt",
1289
+ reason: "invalid_ends_at",
1290
+ value: input.endsAt
1291
+ };
1292
+ }
1293
+ }
1294
+ function toScheduledBookingDateTime(input) {
1295
+ const result = parseScheduledBookingDateTime(input);
1296
+ if (result.ok) {
1297
+ return result.value;
1298
+ }
1299
+ throw new Error(formatParseError(result));
1300
+ }
1301
+ function toScheduledBookingDateRange(input) {
1302
+ const result = parseScheduledBookingDateRange(input);
1303
+ if (result.ok) {
1304
+ return result.value;
1305
+ }
1306
+ throw new Error(formatParseError(result));
1307
+ }
1308
+ function formatBookingInstant(isoString, options) {
1309
+ const date = new Date(isoString);
1310
+ if (Number.isNaN(date.getTime())) {
1311
+ return isoString;
1312
+ }
1313
+ return getCachedDateTimeFormatter(options.locale ?? DEFAULT_LOCALE, getSafeTimeZone(options.timeZone), options.format).format(date);
1314
+ }
1315
+ function formatBookingDate(input, format = {
1316
+ weekday: "long",
1317
+ year: "numeric",
1318
+ month: "long",
1319
+ day: "numeric"
1320
+ }) {
1321
+ return formatBookingInstant(input.startsAt, {
1322
+ locale: input.locale,
1323
+ timeZone: input.timeZone,
1324
+ format
1325
+ });
1326
+ }
1327
+ function formatBookingTime(input, format = {
1328
+ hour: "numeric",
1329
+ minute: "2-digit",
1330
+ hour12: true
1331
+ }) {
1332
+ return formatBookingInstant(input.startsAt, {
1333
+ locale: input.locale,
1334
+ timeZone: input.timeZone,
1335
+ format
1336
+ });
1337
+ }
1338
+ /**
1339
+ * Formats the compact English event-time presentation used by public surfaces.
1340
+ *
1341
+ * This intentionally owns the lowercase day-period token and zero-minute
1342
+ * omission instead of depending on locale-specific spacing or punctuation.
1343
+ * Callers must only use it for an English presentation contract.
1344
+ */
1345
+ function formatBookingCompactEnglishTime(input) {
1346
+ const date = new Date(input.startsAt);
1347
+ if (Number.isNaN(date.getTime())) {
1348
+ return input.startsAt;
1349
+ }
1350
+ const parts = formatToPartsLookup(date, input.locale ?? "en-GB", input.timeZone, {
1351
+ hour: "numeric",
1352
+ minute: "2-digit",
1353
+ hour12: true
1354
+ });
1355
+ const hour = parts.get("hour");
1356
+ const minute = parts.get("minute");
1357
+ const dayPeriod = parts.get("dayPeriod");
1358
+ if (hour === undefined || minute === undefined || dayPeriod === undefined) {
1359
+ return input.startsAt;
1360
+ }
1361
+ const clock = minute === "00" ? hour : `${hour}:${minute}`;
1362
+ return `${clock}${dayPeriod.toLowerCase()}`;
1363
+ }
1364
+ function formatBookingDateTime(input, format = DEFAULT_BOOKING_DATE_TIME_FORMAT) {
1365
+ if (format === DEFAULT_BOOKING_DATE_TIME_FORMAT) {
1366
+ return `${formatBookingDate(input, {
1367
+ weekday: "short",
1368
+ month: "short",
1369
+ day: "numeric"
1370
+ })}, ${formatBookingTime(input)}`;
1371
+ }
1372
+ return formatBookingInstant(input.startsAt, {
1373
+ locale: input.locale,
1374
+ timeZone: input.timeZone,
1375
+ format
1376
+ });
1377
+ }
1378
+ function formatBookingDateParts(input, options) {
1379
+ return {
1380
+ date: formatBookingDate(input, options?.dateFormat ?? {
1381
+ weekday: "short",
1382
+ month: "short",
1383
+ day: "numeric",
1384
+ year: "numeric"
1385
+ }),
1386
+ time: formatBookingTime(input, options?.timeFormat ?? {
1387
+ hour: "numeric",
1388
+ minute: "2-digit",
1389
+ hour12: true
1390
+ })
1391
+ };
1392
+ }
1393
+ function getBookingLocalDateKey(input) {
1394
+ return formatDateKey(getLocalScheduleDate(new Date(input.startsAt), input.timeZone));
1395
+ }
1396
+ function compareLocalScheduleDates(left, right) {
1397
+ return Date.UTC(left.year, left.month - 1, left.day) - Date.UTC(right.year, right.month - 1, right.day);
1398
+ }
1399
+ function parseBookingLocalDate(value) {
1400
+ const match = LOCAL_DATE_PATTERN.exec(value.trim());
1401
+ if (match === null || match.groups === undefined) {
1402
+ throw new Error(`Invalid booking date format: "${value}" (expected YYYY-MM-DD)`);
1403
+ }
1404
+ const year = Number(match.groups.year);
1405
+ const month = Number(match.groups.month);
1406
+ const day = Number(match.groups.day);
1407
+ const candidate = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
1408
+ if (Number.isNaN(candidate.getTime()) || candidate.getUTCFullYear() !== year || candidate.getUTCMonth() + 1 !== month || candidate.getUTCDate() !== day) {
1409
+ throw new Error(`Invalid booking date format: "${value}" (expected YYYY-MM-DD)`);
1410
+ }
1411
+ return {
1412
+ year,
1413
+ month,
1414
+ day
1415
+ };
1416
+ }
1417
+ function parseBookingUtcDateWindow(input) {
1418
+ const timeZone = resolveBookingTimeZoneId(input.timeZone);
1419
+ let startLocalDate;
1420
+ try {
1421
+ startLocalDate = parseBookingLocalDate(input.startDate);
1422
+ } catch {
1423
+ return {
1424
+ ok: false,
1425
+ field: "startDate",
1426
+ reason: "invalid_date_format",
1427
+ value: input.startDate
1428
+ };
1429
+ }
1430
+ let endLocalDate;
1431
+ try {
1432
+ endLocalDate = parseBookingLocalDate(input.endDate);
1433
+ } catch {
1434
+ return {
1435
+ ok: false,
1436
+ field: "endDate",
1437
+ reason: "invalid_date_format",
1438
+ value: input.endDate
1439
+ };
1440
+ }
1441
+ if (compareLocalScheduleDates(startLocalDate, endLocalDate) > 0) {
1442
+ return {
1443
+ ok: false,
1444
+ field: "endDate",
1445
+ reason: "invalid_date_range",
1446
+ value: input.endDate
1447
+ };
1448
+ }
1449
+ return {
1450
+ ok: true,
1451
+ value: {
1452
+ timeZone,
1453
+ startLocalDate,
1454
+ endLocalDate,
1455
+ startAt: localScheduleDateTimeToUtcDate(startLocalDate, START_OF_DAY, timeZone),
1456
+ endExclusive: localScheduleDateTimeToUtcDate(addLocalScheduleDays(endLocalDate, 1), START_OF_DAY, timeZone)
1457
+ }
1458
+ };
1459
+ }
1460
+ function buildBookingUtcDateWindow(input) {
1461
+ const result = parseBookingUtcDateWindow(input);
1462
+ if (result.ok) {
1463
+ return result.value;
1464
+ }
1465
+ if (result.reason === "invalid_date_range") {
1466
+ throw new Error(`Invalid booking date range: startDate "${input.startDate}" must be on or before endDate "${input.endDate}"`);
1467
+ }
1468
+ throw new Error(`Invalid booking date format: "${result.value}" (expected YYYY-MM-DD)`);
1469
+ }
1470
+ function formatBookingRange(input, options) {
1471
+ if (input.endsAt == null) {
1472
+ const parts = formatBookingDateParts(input, options);
1473
+ return `${parts.date}, ${parts.time}`;
1474
+ }
1475
+ const startKey = getBookingLocalDateKey(input);
1476
+ const endDateTime = toScheduledBookingDateTime({
1477
+ startsAt: input.endsAt,
1478
+ timeZone: input.timeZone,
1479
+ locale: input.locale
1480
+ });
1481
+ const endKey = getBookingLocalDateKey(endDateTime);
1482
+ const startDate = formatBookingDate(input, options?.dateFormat ?? {
1483
+ month: "short",
1484
+ day: "numeric",
1485
+ year: "numeric"
1486
+ });
1487
+ const startTime = formatBookingTime(input, options?.timeFormat);
1488
+ const endTime = formatBookingTime(endDateTime, options?.timeFormat);
1489
+ if (startKey === endKey) {
1490
+ return `${startDate}, ${startTime} - ${endTime}`;
1491
+ }
1492
+ const endDate = formatBookingDate(endDateTime, options?.dateFormat ?? {
1493
+ month: "short",
1494
+ day: "numeric",
1495
+ year: "numeric"
1496
+ });
1497
+ return `${startDate}, ${startTime} - ${endDate}, ${endTime}`;
1498
+ }
1499
+ function parseLocalScheduleTime(value) {
1500
+ const match = /^(?<hour>\d{1,2}):(?<minute>\d{2})(?::(?<second>\d{2}))?$/u.exec(value.trim());
1501
+ if (match === null || match.groups === undefined) {
1502
+ throw new Error(`Invalid start time format: "${value}" (expected HH:MM or HH:MM:SS)`);
1503
+ }
1504
+ const hour = Number(match.groups.hour);
1505
+ const minute = Number(match.groups.minute);
1506
+ const second = match.groups.second !== undefined ? Number(match.groups.second) : 0;
1507
+ if (Number.isNaN(hour) || Number.isNaN(minute) || Number.isNaN(second) || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
1508
+ throw new Error(`Invalid start time format: "${value}" (expected HH:MM or HH:MM:SS)`);
1509
+ }
1510
+ return {
1511
+ hour,
1512
+ minute,
1513
+ second
1514
+ };
1515
+ }
1516
+ function getLocalScheduleDate(date, timeZone) {
1517
+ const parts = getZonedDateTimeParts(date, getSafeTimeZone(timeZone));
1518
+ return {
1519
+ year: parts.year,
1520
+ month: parts.month,
1521
+ day: parts.day
1522
+ };
1523
+ }
1524
+ function getLocalScheduleTime(date, timeZone) {
1525
+ const parts = getZonedDateTimeParts(date, getSafeTimeZone(timeZone));
1526
+ return {
1527
+ hour: parts.hour,
1528
+ minute: parts.minute,
1529
+ second: parts.second
1530
+ };
1531
+ }
1532
+ function formatLocalScheduleDate(date) {
1533
+ return formatDateKey(date);
1534
+ }
1535
+ function formatLocalScheduleTime(time) {
1536
+ return `${String(time.hour).padStart(2, "0")}:${String(time.minute).padStart(2, "0")}`;
1537
+ }
1538
+ function getDaysInLocalMonth(year, month) {
1539
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
1540
+ }
1541
+ function addLocalScheduleDays(date, days) {
1542
+ const utcDate = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12, 0, 0));
1543
+ return {
1544
+ year: utcDate.getUTCFullYear(),
1545
+ month: utcDate.getUTCMonth() + 1,
1546
+ day: utcDate.getUTCDate()
1547
+ };
1548
+ }
1549
+ function addLocalScheduleMonths(date, months) {
1550
+ const targetMonthIndex = date.month - 1 + months;
1551
+ const base = new Date(Date.UTC(date.year, targetMonthIndex, 1, 12, 0, 0));
1552
+ const year = base.getUTCFullYear();
1553
+ const month = base.getUTCMonth() + 1;
1554
+ const day = Math.min(date.day, getDaysInLocalMonth(year, month));
1555
+ return {
1556
+ year,
1557
+ month,
1558
+ day
1559
+ };
1560
+ }
1561
+ function getLocalScheduleDayOfWeek(date) {
1562
+ return new Date(Date.UTC(date.year, date.month - 1, date.day, 12, 0, 0)).getUTCDay();
1563
+ }
1564
+ function localScheduleDateTimeToUtcDate(localDate, localTime, timeZone) {
1565
+ const resolvedTimeZone = getSafeTimeZone(timeZone);
1566
+ const targetLocalMs = Date.UTC(localDate.year, localDate.month - 1, localDate.day, localTime.hour, localTime.minute, localTime.second, 0);
1567
+ let guess = targetLocalMs;
1568
+ const seenGuesses = new Set();
1569
+ for (let iteration = 0; iteration < 4; iteration += 1) {
1570
+ seenGuesses.add(guess);
1571
+ const actual = getZonedDateTimeParts(new Date(guess), resolvedTimeZone);
1572
+ const actualLocalMs = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second, 0);
1573
+ const diff = actualLocalMs - targetLocalMs;
1574
+ if (diff === 0) {
1575
+ return new Date(guess);
1576
+ }
1577
+ const nextGuess = guess - diff;
1578
+ if (seenGuesses.has(nextGuess)) {
1579
+ return new Date(Math.max(guess, nextGuess));
1580
+ }
1581
+ guess = nextGuess;
1582
+ }
1583
+ return new Date(guess);
1584
+ }
1585
+
1586
+ //#endregion
1587
+ //#region ../core/src/dated-offering-settlement.ts
1588
+ function parseStripeReference(value) {
1589
+ if (typeof value !== "string") return err({ kind: "not_string" });
1590
+ const normalized = value.trim();
1591
+ return normalized.length === 0 ? err({ kind: "empty_string" }) : ok(normalized);
1592
+ }
1593
+ const parseStripePaymentIntentRef = (value) => parseStripeReference(value);
1594
+ const parseStripeWebhookEventRef = (value) => parseStripeReference(value);
1595
+ const parseStripeBalanceTransactionRef = (value) => parseStripeReference(value);
1596
+ const parseStripeChargeRef = (value) => parseStripeReference(value);
1597
+ const parseStripeAccountRef = (value) => parseStripeReference(value);
1598
+ const parseStripeCheckoutSessionId = (value) => parseStripeReference(value);
1599
+
1600
+ //#endregion
1601
+ //#region ../core/src/person-records.ts
1602
+ const RECORD_ENTRY_KINDS = [
1603
+ "person_note",
1604
+ "prep_note",
1605
+ "session_note",
1606
+ "follow_up_note",
1607
+ "booking_questions_snapshot",
1608
+ "correction_note"
1609
+ ];
1610
+ const PERSON_RECORD_NOTE_KINDS = [
1611
+ "person_note",
1612
+ "prep_note",
1613
+ "session_note",
1614
+ "follow_up_note",
1615
+ "correction_note"
1616
+ ];
1617
+ function parseRecordEntryKind(value) {
1618
+ return RECORD_ENTRY_KINDS.includes(value) ? ok(value) : err({
1619
+ kind: "unsupported_record_entry_kind",
1620
+ value
1621
+ });
1622
+ }
1623
+
1624
+ //#endregion
1625
+ //#region ../core/src/rectification.ts
1626
+ const RECTIFICATION_SURFACES = [
1627
+ "customer_profile",
1628
+ "booking_identity",
1629
+ "event_registration_identity",
1630
+ "course_enrollment_identity",
1631
+ "person_record_entry",
1632
+ "booking_question_snapshot",
1633
+ "form_submission",
1634
+ "newsletter_subscriber",
1635
+ "order_financial_record",
1636
+ "audit_event",
1637
+ "derived_history_ledger"
1638
+ ];
1639
+ const RECTIFICATION_ISSUE_URL_BASE = "https://github.com/wubss/builder/issues";
1640
+ const RECTIFICATION_IDENTITY_BOUNDARIES = {
1641
+ customer_commercial_current_identity: { kind: "customer_commercial_current_identity" },
1642
+ customer_backed_participant_current_identity: { kind: "customer_backed_participant_current_identity" },
1643
+ person_record_current_note: { kind: "person_record_current_note" },
1644
+ immutable_subject_snapshot: { kind: "immutable_subject_snapshot" },
1645
+ newsletter_current_subscription_identity: { kind: "newsletter_current_subscription_identity" },
1646
+ finance_integrity_record: { kind: "finance_integrity_record" },
1647
+ immutable_audit_history: { kind: "immutable_audit_history" },
1648
+ derived_history_projection_or_ledger: { kind: "derived_history_projection_or_ledger" }
1649
+ };
1650
+ const RECTIFICATION_HISTORY_PROPAGATION_PLANS = {
1651
+ update_current_identity_only: { kind: "update_current_identity_only" },
1652
+ append_correction_note: { kind: "append_correction_note" },
1653
+ annotate_immutable_snapshot: { kind: "annotate_immutable_snapshot" },
1654
+ manual_review_required: { kind: "manual_review_required" },
1655
+ blocked_immutable_history: { kind: "blocked_immutable_history" }
1656
+ };
1657
+
1658
+ //#endregion
1659
+ //#region ../core/src/location.ts
1660
+ function normalizeLocationAddressText(value) {
1661
+ if (typeof value !== "string") {
1662
+ return null;
1663
+ }
1664
+ const trimmed = value.trim();
1665
+ return trimmed.length > 0 ? trimmed : null;
1666
+ }
1667
+ function hasLocationCoordinates(value) {
1668
+ return value != null && typeof value.lat === "number" && typeof value.lng === "number";
1669
+ }
1670
+ function toLocationCoordinates(value) {
1671
+ if (!hasLocationCoordinates(value)) {
1672
+ return null;
1673
+ }
1674
+ return {
1675
+ lat: value.lat,
1676
+ lng: value.lng
1677
+ };
1678
+ }
1679
+
1680
+ //#endregion
1681
+ //#region ../core/src/string.ts
1682
+ /**
1683
+ * String utilities
1684
+ */
1685
+ /**
1686
+ * Return the string value when the input is already a string, otherwise null.
1687
+ */
1688
+ function asNullableString(value) {
1689
+ return typeof value === "string" ? value : null;
1690
+ }
1691
+ /**
1692
+ * Return a non-empty string value, otherwise null.
1693
+ */
1694
+ function asNonEmptyStringOrNull(value) {
1695
+ return typeof value === "string" && value.length > 0 ? value : null;
1696
+ }
1697
+ /**
1698
+ * Narrow nullable values to the present value.
1699
+ */
1700
+ function isPresent(value) {
1701
+ return value !== null && value !== undefined;
1702
+ }
1703
+ /**
1704
+ * Return true for string values that contain at least one character.
1705
+ */
1706
+ function isNonEmptyString(value) {
1707
+ return typeof value === "string" && value.length > 0;
1708
+ }
1709
+ /**
1710
+ * Narrow optional string values to non-empty strings.
1711
+ */
1712
+ function hasText(value) {
1713
+ return isNonEmptyString(value);
1714
+ }
1715
+ /**
1716
+ * Trim a user-entered string and collapse blank values to null.
1717
+ */
1718
+ function trimmedStringOrNull(value) {
1719
+ const trimmed = typeof value === "string" ? value.trim() : "";
1720
+ return trimmed.length > 0 ? trimmed : null;
1721
+ }
1722
+ /**
1723
+ * Capitalize the first character of a string
1724
+ */
1725
+ function capitalize(str) {
1726
+ return str.charAt(0).toUpperCase() + str.slice(1);
1727
+ }
1728
+
1729
+ //#endregion
1730
+ //#region ../core/src/business-address.ts
1731
+ function normalizeCountryCode(value) {
1732
+ const trimmed = trimmedStringOrNull(value);
1733
+ return trimmed !== null ? trimmed.toUpperCase() : null;
1734
+ }
1735
+ function normalizeCoordinate(value) {
1736
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1737
+ }
1738
+ function isLatitude(value) {
1739
+ return value >= -90 && value <= 90;
1740
+ }
1741
+ function isLongitude(value) {
1742
+ return value >= -180 && value <= 180;
1743
+ }
1744
+ function buildBusinessAddressDisplayText(input) {
1745
+ const displayText = [
1746
+ trimmedStringOrNull(input.addressLine1),
1747
+ trimmedStringOrNull(input.addressLine2),
1748
+ trimmedStringOrNull(input.locality),
1749
+ trimmedStringOrNull(input.region),
1750
+ trimmedStringOrNull(input.postalCode),
1751
+ normalizeCountryCode(input.countryCode)
1752
+ ].filter((part) => part !== null).join(", ");
1753
+ return displayText.length > 0 ? displayText : null;
1754
+ }
1755
+ function validateBusinessAddressDraft(input) {
1756
+ const addressLine1 = trimmedStringOrNull(input.addressLine1);
1757
+ const addressLine2 = trimmedStringOrNull(input.addressLine2);
1758
+ const locality = trimmedStringOrNull(input.locality);
1759
+ const region = trimmedStringOrNull(input.region);
1760
+ const postalCode = trimmedStringOrNull(input.postalCode);
1761
+ const countryCode = normalizeCountryCode(input.countryCode);
1762
+ const displayAddress = trimmedStringOrNull(input.displayAddress) ?? buildBusinessAddressDisplayText(input);
1763
+ const lat = normalizeCoordinate(input.lat);
1764
+ const lng = normalizeCoordinate(input.lng);
1765
+ const errors = [];
1766
+ if (addressLine1 === null) errors.push("missing_address_line_1");
1767
+ if (locality === null) errors.push("missing_locality");
1768
+ if (countryCode === null) errors.push("missing_country_code");
1769
+ if (displayAddress === null) errors.push("missing_display_address");
1770
+ if (lat === null) errors.push("missing_latitude");
1771
+ else if (!isLatitude(lat)) errors.push("invalid_latitude");
1772
+ if (lng === null) errors.push("missing_longitude");
1773
+ else if (!isLongitude(lng)) errors.push("invalid_longitude");
1774
+ if (errors.length > 0 || addressLine1 === null || locality === null || countryCode === null || displayAddress === null || lat === null || lng === null) {
1775
+ return {
1776
+ success: false,
1777
+ errors
1778
+ };
1779
+ }
1780
+ return {
1781
+ success: true,
1782
+ address: {
1783
+ addressLine1,
1784
+ addressLine2,
1785
+ locality,
1786
+ region,
1787
+ postalCode,
1788
+ countryCode,
1789
+ displayAddress,
1790
+ lat,
1791
+ lng
1792
+ }
1793
+ };
1794
+ }
1795
+ function isCompleteBusinessAddress(value) {
1796
+ if (value == null) return false;
1797
+ return validateBusinessAddressDraft(value).success;
1798
+ }
1799
+ function businessAddressToAddressedLocation(address) {
1800
+ if (address == null) return null;
1801
+ return {
1802
+ addressText: address.displayAddress,
1803
+ lat: address.lat,
1804
+ lng: address.lng
1805
+ };
1806
+ }
1807
+
1808
+ //#endregion
1809
+ //#region ../core/src/try-async.ts
1810
+ async function tryAsync(run, toError) {
1811
+ try {
1812
+ return ok(await run());
1813
+ } catch (error) {
1814
+ return err(toError(error));
1815
+ }
1816
+ }
1817
+
1818
+ //#endregion
1819
+ //#region ../core/src/narrow-result.ts
1820
+ /** Unwrap a NarrowResult, throwing on failure. Use at persistence boundaries. */
1821
+ function unwrapNarrow(result) {
1822
+ if (!result.ok) throw new Error(result.error);
1823
+ return result.value;
1824
+ }
1825
+
1826
+ //#endregion
1827
+ //#region ../core/src/non-empty-array.ts
1828
+ function fromArray(items) {
1829
+ const [first, ...rest] = items;
1830
+ if (first === undefined) {
1831
+ return err("empty_array");
1832
+ }
1833
+ return ok([first, ...rest]);
1834
+ }
1835
+ function mapNonEmptyArray(items, map) {
1836
+ return items.map(map);
1837
+ }
1838
+
1839
+ //#endregion
1840
+ //#region ../core/src/editor-state.ts
1841
+ const unsafeAsEditorSiteId = (value) => value;
1842
+ const unsafeAsEditorPageId = (value) => value;
1843
+ const unsafeAsEditorEntryId = (value) => value;
1844
+ const unsafeAsEditorBlockId = (value) => value;
1845
+ const unsafeAsEditorMetadataValues = (value) => value;
1846
+ const unsafeAsEditorContentValues = (value) => value;
1847
+ const unsafeAsEditorBlockValues = (value) => value;
1848
+ function createEditorInsertBlockCommand(block, draft, index) {
1849
+ return {
1850
+ type: "insert-block",
1851
+ block,
1852
+ draft,
1853
+ index
1854
+ };
1855
+ }
1856
+ function createEditorRemoveBlockCommand(blockId) {
1857
+ return {
1858
+ type: "remove-block",
1859
+ blockId
1860
+ };
1861
+ }
1862
+ function createEditorReorderBlocksCommand(blockIds) {
1863
+ return {
1864
+ type: "reorder-blocks",
1865
+ blockIds
1866
+ };
1867
+ }
1868
+
1869
+ //#endregion
1870
+ //#region ../core/src/sensitive-collection.ts
1871
+ const SENSITIVE_COLLECTION_MODES = ["standard", "sensitive"];
1872
+ const SENSITIVE_COLLECTION_SURFACES = ["form", "booking_questions"];
1873
+ const SENSITIVE_COLLECTION_AUDIT_TARGET_TYPES = [
1874
+ "form",
1875
+ "appointment_service",
1876
+ "event_series",
1877
+ "course"
1878
+ ];
1879
+ const SENSITIVE_DOWNSTREAM_CHANNELS = [
1880
+ "owner_notification_email",
1881
+ "customer_auto_reply",
1882
+ "webhook_payload",
1883
+ "webhook_delivery_log",
1884
+ "csv_export",
1885
+ "dashboard_submission_view",
1886
+ "dsar_export",
1887
+ "protected_record_snapshot",
1888
+ "audit_metadata"
1889
+ ];
1890
+ const SENSITIVE_COLLECTION_CONFIRMATION_VERSION = 1;
1891
+ const SENSITIVE_COLLECTION_CONFIRMATION_COPY = [
1892
+ "I understand this form may collect sensitive client information.",
1893
+ "I understand Riverbank will restrict emails, webhooks, exports, and logs for these answers.",
1894
+ "I understand existing external copies made before enabling this setting cannot be automatically recalled."
1895
+ ];
1896
+ const SENSITIVE_SUBMISSION_DASHBOARD_REMINDER_COPY = "This submission is marked sensitive. Only use or share these answers where necessary, and avoid copying them into emails or external systems unless you have a clear reason.";
1897
+ const SENSITIVE_SUBMISSION_DETAIL_REMINDER_COPY = "This submission is marked sensitive. Keep answers inside Riverbank unless there is a clear reason to copy them elsewhere.";
1898
+ const SENSITIVE_METADATA_ONLY_PAYLOAD_KEYS = [
1899
+ "submissionId",
1900
+ "siteId",
1901
+ "surfaceId",
1902
+ "surfaceTitle",
1903
+ "submittedAt",
1904
+ "dashboardUrl",
1905
+ "sensitiveCollectionMode"
1906
+ ];
1907
+ const STANDARD_SENSITIVE_COLLECTION_CONFIG = {
1908
+ mode: "standard",
1909
+ enabledAt: null,
1910
+ enabledByUserId: null,
1911
+ confirmationVersion: null
1912
+ };
1913
+ function parseSensitiveCollectionMode(value) {
1914
+ return value === "standard" || value === "sensitive" ? value : null;
1915
+ }
1916
+ function parseSensitiveCollectionConfirmationVersion(value) {
1917
+ return value === SENSITIVE_COLLECTION_CONFIRMATION_VERSION ? SENSITIVE_COLLECTION_CONFIRMATION_VERSION : null;
1918
+ }
1919
+ function isSensitiveCollectionAdminRole(actorRole) {
1920
+ return actorRole === "site_owner" || actorRole === "site_admin";
1921
+ }
1922
+ function deriveEffectiveSensitiveCollectionMode(input) {
1923
+ return input.snapshotMode === "sensitive" || input.currentMode === "sensitive" ? "sensitive" : "standard";
1924
+ }
1925
+ function planSensitiveCollectionConfigChange(input) {
1926
+ const { current, requested, actorRole, siteProfile } = input;
1927
+ if (current.mode === "sensitive" && requested.mode === "sensitive") {
1928
+ return siteProfile === "sensitive_client_records" ? {
1929
+ kind: "noop_already_sensitive",
1930
+ current
1931
+ } : {
1932
+ kind: "noop_already_sensitive_grandfathered",
1933
+ current
1934
+ };
1935
+ }
1936
+ if (current.mode === "standard" && requested.mode === "standard") {
1937
+ return {
1938
+ kind: "noop_standard",
1939
+ current
1940
+ };
1941
+ }
1942
+ if (!isSensitiveCollectionAdminRole(actorRole)) {
1943
+ return { kind: "denied_actor_not_admin" };
1944
+ }
1945
+ if (requested.mode === "standard") {
1946
+ return {
1947
+ kind: "allowed_disable",
1948
+ next: STANDARD_SENSITIVE_COLLECTION_CONFIG
1949
+ };
1950
+ }
1951
+ if (siteProfile !== "sensitive_client_records") {
1952
+ return { kind: "denied_site_profile_not_enabled" };
1953
+ }
1954
+ if (requested.confirmationVersion !== SENSITIVE_COLLECTION_CONFIRMATION_VERSION) {
1955
+ return {
1956
+ kind: "invalid_confirmation_version",
1957
+ expectedVersion: SENSITIVE_COLLECTION_CONFIRMATION_VERSION
1958
+ };
1959
+ }
1960
+ return {
1961
+ kind: "allowed_enable",
1962
+ next: {
1963
+ mode: "sensitive",
1964
+ enabledAt: input.nowIso,
1965
+ enabledByUserId: input.actorUserId,
1966
+ confirmationVersion: requested.confirmationVersion
1967
+ }
1968
+ };
1969
+ }
1970
+ function planSensitiveSubmissionTreatment(input) {
1971
+ const { mode, channel } = input;
1972
+ if (mode === "standard") {
1973
+ return {
1974
+ kind: "standard_payload",
1975
+ channel
1976
+ };
1977
+ }
1978
+ switch (channel) {
1979
+ case "owner_notification_email":
1980
+ case "webhook_payload":
1981
+ case "webhook_delivery_log":
1982
+ case "audit_metadata": return {
1983
+ kind: "metadata_only",
1984
+ channel
1985
+ };
1986
+ case "customer_auto_reply": return {
1987
+ kind: "no_answer_echo",
1988
+ channel
1989
+ };
1990
+ case "csv_export": return {
1991
+ kind: "restricted_or_redacted",
1992
+ channel
1993
+ };
1994
+ case "dashboard_submission_view": return {
1995
+ kind: "authorized_with_reminder",
1996
+ channel
1997
+ };
1998
+ case "dsar_export": return {
1999
+ kind: "include_marked_sensitive",
2000
+ channel
2001
+ };
2002
+ case "protected_record_snapshot": return {
2003
+ kind: "protected_storage_required",
2004
+ channel
2005
+ };
2006
+ default: return assertNever$2(channel);
2007
+ }
2008
+ }
2009
+ function assertNever$2(value) {
2010
+ throw new Error(`Unhandled sensitive collection case: ${String(value)}`);
2011
+ }
2012
+
2013
+ //#endregion
2014
+ //#region ../core/src/participants.ts
2015
+ function unsafeAsParticipantEmail(value) {
2016
+ return value;
2017
+ }
2018
+ function unsafeAsParticipantSlotKey(value) {
2019
+ return value;
2020
+ }
2021
+ function unsafeAsParticipantCount(value) {
2022
+ return value;
2023
+ }
2024
+ function unsafeAsParticipantDisplayLabel(value) {
2025
+ return value;
2026
+ }
2027
+ /**
2028
+ * Canonicalizes an optional participant email for equality checks.
2029
+ *
2030
+ * This intentionally does not validate email shape; call parseParticipantEmail at
2031
+ * API/DB boundaries that require a syntactically usable address.
2032
+ */
2033
+ function normalizeParticipantEmail(value) {
2034
+ const normalized = value?.trim().normalize("NFC").toLowerCase() ?? "";
2035
+ return normalized.length > 0 ? unsafeAsParticipantEmail(normalized) : null;
2036
+ }
2037
+ function parseParticipantEmail(value) {
2038
+ const normalized = normalizeParticipantEmail(value);
2039
+ if (normalized === null) {
2040
+ return err({ kind: "empty_email" });
2041
+ }
2042
+ if (!isEmailLike(normalized)) {
2043
+ return err({ kind: "invalid_email_shape" });
2044
+ }
2045
+ return ok(normalized);
2046
+ }
2047
+ function parseParticipantCount(value) {
2048
+ if (!Number.isInteger(value)) {
2049
+ return err({ kind: "non_integer_participant_count" });
2050
+ }
2051
+ if (value <= 0) {
2052
+ return err({ kind: "non_positive_participant_count" });
2053
+ }
2054
+ return ok(unsafeAsParticipantCount(value));
2055
+ }
2056
+ function provisionalEventGuestParticipantIdentity(input) {
2057
+ return {
2058
+ kind: "event_guest",
2059
+ eventAttendeeGuestId: input.eventAttendeeGuestId,
2060
+ slotKey: input.slotKey
2061
+ };
2062
+ }
2063
+ const provisionalParticipantIdentityEncoders = { event_guest: (identity) => ({
2064
+ kind: identity.kind,
2065
+ source: "event_guest_backfill",
2066
+ eventAttendeeGuestId: identity.eventAttendeeGuestId,
2067
+ slotKey: identity.slotKey
2068
+ }) };
2069
+ function encodeProvisionalParticipantIdentity(identity) {
2070
+ return provisionalParticipantIdentityEncoders[identity.kind](identity);
2071
+ }
2072
+ function parseProvisionalParticipantIdentity(value) {
2073
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2074
+ return err({ kind: "not_an_object" });
2075
+ }
2076
+ const metadata = value;
2077
+ const kind = metadata.kind ?? metadata.source;
2078
+ if (kind !== "event_guest" && kind !== "event_guest_backfill") {
2079
+ return err({ kind: "unsupported_identity_kind" });
2080
+ }
2081
+ if (typeof metadata.eventAttendeeGuestId !== "string") {
2082
+ return err({ kind: "missing_event_attendee_guest_id" });
2083
+ }
2084
+ if (typeof metadata.slotKey !== "string") {
2085
+ return err({ kind: "missing_slot_key" });
2086
+ }
2087
+ return ok(provisionalEventGuestParticipantIdentity({
2088
+ eventAttendeeGuestId: unsafeAsEventAttendeeGuestId(metadata.eventAttendeeGuestId),
2089
+ slotKey: unsafeAsParticipantSlotKey(metadata.slotKey)
2090
+ }));
2091
+ }
2092
+ function deriveParticipantDisplayLabel(input) {
2093
+ const displayName = trimmedStringOrNull(input.displayName);
2094
+ if (displayName !== null) {
2095
+ return unsafeAsParticipantDisplayLabel(displayName);
2096
+ }
2097
+ if (input.emailNormalized !== null) {
2098
+ return unsafeAsParticipantDisplayLabel(input.emailNormalized);
2099
+ }
2100
+ return unsafeAsParticipantDisplayLabel("Participant");
2101
+ }
2102
+ function participantIdentityStateToContactFields(identityState) {
2103
+ switch (identityState.kind) {
2104
+ case "booker_supplied": return {
2105
+ identityState: "booker_supplied",
2106
+ email: null
2107
+ };
2108
+ case "contactable":
2109
+ case "portal_claimed": return {
2110
+ identityState: identityState.kind,
2111
+ email: identityState.email
2112
+ };
2113
+ case "staff_verified": return {
2114
+ identityState: "staff_verified",
2115
+ email: identityState.email
2116
+ };
2117
+ }
2118
+ }
2119
+ function resolveParticipantSelfServiceEligibility(input) {
2120
+ if (input.identityState.kind === "booker_supplied") {
2121
+ return { kind: "blocked_staff_managed_only" };
2122
+ }
2123
+ const email = input.identityState.email;
2124
+ if (email === null || input.participantId === null) {
2125
+ return { kind: "blocked_missing_email" };
2126
+ }
2127
+ return {
2128
+ kind: "eligible",
2129
+ participantId: input.participantId,
2130
+ email
2131
+ };
2132
+ }
2133
+ function projectAppointmentParticipantSlot(input) {
2134
+ return buildSlot({
2135
+ slotKey: unsafeAsParticipantSlotKey(`appointment:${input.appointmentId}`),
2136
+ contact: input.customer,
2137
+ participation: {
2138
+ kind: "appointment",
2139
+ appointmentId: input.appointmentId
2140
+ }
2141
+ });
2142
+ }
2143
+ function projectCourseEnrollmentParticipantSlot(input) {
2144
+ return buildSlot({
2145
+ slotKey: unsafeAsParticipantSlotKey(`course_enrollment:${input.enrollmentId}`),
2146
+ contact: input.customer,
2147
+ participation: {
2148
+ kind: "course_enrollment",
2149
+ enrollmentId: input.enrollmentId
2150
+ }
2151
+ });
2152
+ }
2153
+ function projectSingleSubjectParticipantRoster(input) {
2154
+ const slot = projectSingleSubjectParticipantSlot(input);
2155
+ return {
2156
+ slots: [slot],
2157
+ issues: reconcileParticipantRoster({
2158
+ expectedCount: unsafeAsParticipantCount(1),
2159
+ slots: [slot]
2160
+ })
2161
+ };
2162
+ }
2163
+ function buildSingleSubjectParticipantPersistenceIntent(input) {
2164
+ const slot = projectSingleSubjectParticipantSlot(input);
2165
+ const base = {
2166
+ customerId: input.customer.customerId,
2167
+ displayName: slot.displayName,
2168
+ phone: slot.phone,
2169
+ participation: slot.participation
2170
+ };
2171
+ if (slot.email === null) {
2172
+ return {
2173
+ kind: "provisional_single_subject_participant",
2174
+ ...base
2175
+ };
2176
+ }
2177
+ return {
2178
+ kind: "participant_subject",
2179
+ ...base,
2180
+ email: slot.email
2181
+ };
2182
+ }
2183
+ function projectSingleSubjectParticipantSlot(input) {
2184
+ switch (input.target.kind) {
2185
+ case "appointment": return projectAppointmentParticipantSlot({
2186
+ appointmentId: input.target.appointmentId,
2187
+ customer: input.customer
2188
+ });
2189
+ case "course_enrollment": return projectCourseEnrollmentParticipantSlot({
2190
+ enrollmentId: input.target.enrollmentId,
2191
+ customer: input.customer
2192
+ });
2193
+ default: return assertNever$1(input.target);
2194
+ }
2195
+ }
2196
+ function projectCourseSessionAttendanceParticipantSlot(input) {
2197
+ return {
2198
+ slotKey: unsafeAsParticipantSlotKey(`course_session_attendance:${input.attendeeId}`),
2199
+ participation: {
2200
+ kind: "course_session_attendance",
2201
+ attendeeId: input.attendeeId,
2202
+ enrollmentId: input.enrollmentId
2203
+ }
2204
+ };
2205
+ }
2206
+ function projectEventRegistrationParticipantRoster(input) {
2207
+ const bookerSlot = buildSlot({
2208
+ slotKey: unsafeAsParticipantSlotKey(`event_registration_booker:${input.attendeeId}`),
2209
+ contact: input.booker,
2210
+ participation: {
2211
+ kind: "event_registration_booker",
2212
+ attendeeId: input.attendeeId
2213
+ }
2214
+ });
2215
+ const guestSlots = input.guests.map((guest) => buildSlot({
2216
+ slotKey: unsafeAsParticipantSlotKey(`event_guest:${guest.guestId}`),
2217
+ contact: guest,
2218
+ participation: {
2219
+ kind: "event_guest",
2220
+ attendeeId: input.attendeeId,
2221
+ guestId: guest.guestId
2222
+ }
2223
+ }));
2224
+ const slots = [bookerSlot, ...guestSlots];
2225
+ return {
2226
+ slots,
2227
+ issues: reconcileParticipantRoster({
2228
+ expectedCount: input.ticketsCount,
2229
+ slots
2230
+ })
2231
+ };
2232
+ }
2233
+ function reconcileParticipantRoster(input) {
2234
+ const issues = [];
2235
+ if (input.slots.length < input.expectedCount) {
2236
+ issues.push({
2237
+ kind: "participant_count_below_ticket_count",
2238
+ expectedCount: input.expectedCount,
2239
+ actualCount: input.slots.length
2240
+ });
2241
+ }
2242
+ if (input.slots.length > input.expectedCount) {
2243
+ issues.push({
2244
+ kind: "participant_count_above_ticket_count",
2245
+ expectedCount: input.expectedCount,
2246
+ actualCount: input.slots.length
2247
+ });
2248
+ }
2249
+ issues.push(...findDuplicateParticipantEmails(input.slots));
2250
+ return issues;
2251
+ }
2252
+ function planParticipantSubjectResolution(input) {
2253
+ const existingParticipantIdByEmail = new Map(input.existingParticipantsByEmail.map((participant) => [participant.email, participant.participantId]));
2254
+ const duplicateSlotKeysByEmail = new Map(input.projection.issues.filter((issue) => issue.kind === "duplicate_email_in_booking_context").map((issue) => [issue.email, issue.slotKeys]));
2255
+ return input.projection.slots.map((slot) => {
2256
+ if (slot.email === null) {
2257
+ return {
2258
+ kind: "staff_managed_slot",
2259
+ slotKey: slot.slotKey
2260
+ };
2261
+ }
2262
+ const duplicateSlotKeys = duplicateSlotKeysByEmail.get(slot.email);
2263
+ if (duplicateSlotKeys !== undefined) {
2264
+ return {
2265
+ kind: "blocked_duplicate_participant_in_booking",
2266
+ slotKey: slot.slotKey,
2267
+ email: slot.email,
2268
+ conflictingSlotKeys: duplicateSlotKeys
2269
+ };
2270
+ }
2271
+ const participantId = existingParticipantIdByEmail.get(slot.email);
2272
+ if (participantId !== undefined) {
2273
+ return {
2274
+ kind: "reuse_participant_by_site_email",
2275
+ slotKey: slot.slotKey,
2276
+ email: slot.email,
2277
+ participantId
2278
+ };
2279
+ }
2280
+ return {
2281
+ kind: "create_participant",
2282
+ slotKey: slot.slotKey,
2283
+ email: slot.email,
2284
+ displayName: slot.displayName,
2285
+ phone: slot.phone
2286
+ };
2287
+ });
2288
+ }
2289
+ function buildSlot(input) {
2290
+ const email = normalizeParticipantEmail(input.contact.email);
2291
+ return {
2292
+ slotKey: input.slotKey,
2293
+ displayName: trimmedStringOrNull(input.contact.name),
2294
+ email,
2295
+ phone: trimmedStringOrNull(input.contact.phone),
2296
+ identityState: email === null ? { kind: "booker_supplied" } : {
2297
+ kind: "contactable",
2298
+ email
2299
+ },
2300
+ participation: input.participation
2301
+ };
2302
+ }
2303
+ function findDuplicateParticipantEmails(slots) {
2304
+ const slotKeysByEmail = new Map();
2305
+ for (const slot of slots) {
2306
+ if (slot.email === null) {
2307
+ continue;
2308
+ }
2309
+ const existing = slotKeysByEmail.get(slot.email) ?? [];
2310
+ existing.push(slot.slotKey);
2311
+ slotKeysByEmail.set(slot.email, existing);
2312
+ }
2313
+ return [...slotKeysByEmail.entries()].filter(([, slotKeys]) => slotKeys.length > 1).map(([email, slotKeys]) => ({
2314
+ kind: "duplicate_email_in_booking_context",
2315
+ email,
2316
+ slotKeys
2317
+ }));
2318
+ }
2319
+ function isEmailLike(value) {
2320
+ const atIndex = value.indexOf("@");
2321
+ return atIndex > 0 && atIndex === value.lastIndexOf("@") && atIndex < value.length - 1;
2322
+ }
2323
+ function assertNever$1(value) {
2324
+ throw new Error(`Unexpected participant variant: ${JSON.stringify(value)}`);
2325
+ }
2326
+
2327
+ //#endregion
2328
+ //#region ../core/src/participant-identity.ts
2329
+ function planPersonResolution(input) {
2330
+ switch (input.kind) {
2331
+ case "with_email":
2332
+ if (input.existingParticipantIdByEmail !== null) {
2333
+ return {
2334
+ kind: "reuse_existing_person_by_email",
2335
+ participantId: input.existingParticipantIdByEmail,
2336
+ email: input.email
2337
+ };
2338
+ }
2339
+ return {
2340
+ kind: "create_contactable_person",
2341
+ seed: {
2342
+ displayName: input.displayName,
2343
+ email: input.email,
2344
+ phone: input.phone
2345
+ }
2346
+ };
2347
+ case "without_email": return {
2348
+ kind: "create_provisional_person",
2349
+ seed: {
2350
+ displayName: input.displayName,
2351
+ phone: input.phone
2352
+ }
2353
+ };
2354
+ default: return assertNever(input);
2355
+ }
2356
+ }
2357
+ function planCustomerPersonResolution(input) {
2358
+ const resolution = planPersonResolution(toCustomerPersonResolutionInput(input));
2359
+ switch (resolution.kind) {
2360
+ case "reuse_existing_person_by_email": return {
2361
+ kind: "link_existing_person_by_email",
2362
+ customerId: input.customerId,
2363
+ participantId: resolution.participantId,
2364
+ email: resolution.email
2365
+ };
2366
+ case "create_contactable_person": return {
2367
+ kind: "create_contactable_person_for_customer",
2368
+ customerId: input.customerId,
2369
+ seed: resolution.seed
2370
+ };
2371
+ case "create_provisional_person": return {
2372
+ kind: "create_provisional_person_for_customer",
2373
+ customerId: input.customerId,
2374
+ seed: resolution.seed
2375
+ };
2376
+ default: return assertNever(resolution);
2377
+ }
2378
+ }
2379
+ function toCustomerPersonResolutionInput(input) {
2380
+ switch (input.kind) {
2381
+ case "with_email": return {
2382
+ kind: "with_email",
2383
+ displayName: input.displayName,
2384
+ email: input.email,
2385
+ phone: input.phone,
2386
+ existingParticipantIdByEmail: input.existingParticipantIdByEmail
2387
+ };
2388
+ case "without_email": return {
2389
+ kind: "without_email",
2390
+ displayName: input.displayName,
2391
+ phone: input.phone
2392
+ };
2393
+ default: return assertNever(input);
2394
+ }
2395
+ }
2396
+ function planParticipationPersonResolution(input) {
2397
+ switch (input.resolution.kind) {
2398
+ case "blocked_duplicate_participant_in_booking": return {
2399
+ kind: "blocked_duplicate_participant_in_booking",
2400
+ slotKey: input.resolution.slotKey,
2401
+ email: input.resolution.email,
2402
+ conflictingSlotKeys: input.resolution.conflictingSlotKeys
2403
+ };
2404
+ case "create_participant":
2405
+ case "reuse_participant_by_site_email":
2406
+ case "staff_managed_slot": return applyParticipationResolutionPlan({
2407
+ slotKey: input.resolution.slotKey,
2408
+ participation: input.slot.participation,
2409
+ resolution: planPersonResolution(toSlotPersonResolutionInput({
2410
+ slot: input.slot,
2411
+ resolution: input.resolution
2412
+ }))
2413
+ });
2414
+ default: return assertNever(input.resolution);
2415
+ }
2416
+ }
2417
+ function toSlotPersonResolutionInput(input) {
2418
+ switch (input.resolution.kind) {
2419
+ case "create_participant": return {
2420
+ kind: "with_email",
2421
+ displayName: input.resolution.displayName,
2422
+ email: input.resolution.email,
2423
+ phone: input.resolution.phone,
2424
+ existingParticipantIdByEmail: null
2425
+ };
2426
+ case "reuse_participant_by_site_email": return {
2427
+ kind: "with_email",
2428
+ displayName: input.slot.displayName,
2429
+ email: input.resolution.email,
2430
+ phone: input.slot.phone,
2431
+ existingParticipantIdByEmail: input.resolution.participantId
2432
+ };
2433
+ case "staff_managed_slot": return {
2434
+ kind: "without_email",
2435
+ displayName: input.slot.displayName,
2436
+ phone: input.slot.phone
2437
+ };
2438
+ default: return assertNever(input.resolution);
2439
+ }
2440
+ }
2441
+ function applyParticipationResolutionPlan(input) {
2442
+ switch (input.resolution.kind) {
2443
+ case "reuse_existing_person_by_email": return {
2444
+ kind: "reuse_existing_person_for_slot",
2445
+ slotKey: input.slotKey,
2446
+ participantId: input.resolution.participantId,
2447
+ participation: input.participation
2448
+ };
2449
+ case "create_contactable_person": return {
2450
+ kind: "create_contactable_person_for_slot",
2451
+ slotKey: input.slotKey,
2452
+ seed: input.resolution.seed,
2453
+ participation: input.participation
2454
+ };
2455
+ case "create_provisional_person": return {
2456
+ kind: "create_provisional_person_for_slot",
2457
+ slotKey: input.slotKey,
2458
+ seed: input.resolution.seed,
2459
+ participation: input.participation
2460
+ };
2461
+ default: return assertNever(input.resolution);
2462
+ }
2463
+ }
2464
+ function planSingleSubjectParticipantMaterialization(input) {
2465
+ const resolution = planPersonResolution(toSingleSubjectPersonResolutionInput(input));
2466
+ switch (resolution.kind) {
2467
+ case "reuse_existing_person_by_email": return {
2468
+ kind: "reuse_existing_person_and_participation",
2469
+ customerId: input.customerId,
2470
+ participantId: resolution.participantId,
2471
+ participation: input.participation
2472
+ };
2473
+ case "create_contactable_person": return {
2474
+ kind: "create_contactable_person_and_participation",
2475
+ customerId: input.customerId,
2476
+ seed: resolution.seed,
2477
+ participation: input.participation
2478
+ };
2479
+ case "create_provisional_person": return {
2480
+ kind: "create_provisional_person_and_participation",
2481
+ customerId: input.customerId,
2482
+ seed: resolution.seed,
2483
+ participation: input.participation
2484
+ };
2485
+ default: return assertNever(resolution);
2486
+ }
2487
+ }
2488
+ function toSingleSubjectPersonResolutionInput(input) {
2489
+ switch (input.kind) {
2490
+ case "participant_subject": return {
2491
+ kind: "with_email",
2492
+ displayName: input.displayName,
2493
+ email: input.email,
2494
+ phone: input.phone,
2495
+ existingParticipantIdByEmail: input.existingParticipantIdByEmail
2496
+ };
2497
+ case "provisional_single_subject_participant": return {
2498
+ kind: "without_email",
2499
+ displayName: input.displayName,
2500
+ phone: input.phone
2501
+ };
2502
+ default: return assertNever(input);
2503
+ }
2504
+ }
2505
+ function planCleanupReadiness(evidence) {
2506
+ const blockers = collectCleanupReadinessBlockers(evidence);
2507
+ if (blockers.length === 0) {
2508
+ return {
2509
+ kind: "ready",
2510
+ evidence,
2511
+ blockers: []
2512
+ };
2513
+ }
2514
+ return {
2515
+ kind: "blocked",
2516
+ evidence,
2517
+ blockers
2518
+ };
2519
+ }
2520
+ function collectCleanupReadinessBlockers(evidence) {
2521
+ const blockers = [];
2522
+ if (evidence.legacyCustomerEmailCallSiteCount > 0) {
2523
+ blockers.push("legacy_customer_email_call_sites_remaining");
2524
+ }
2525
+ if (evidence.parityFailureSurfaces.length > 0) {
2526
+ blockers.push("parity_failures_remaining");
2527
+ }
2528
+ if (evidence.unresolvedHealthRowCount > 0) {
2529
+ blockers.push("unresolved_health_rows_remaining");
2530
+ }
2531
+ return blockers;
2532
+ }
2533
+
2534
+ //#endregion
2535
+ //#region ../core/src/public-url-validation.ts
2536
+ function buildPublicUrlValidationError(message, errorFactory) {
2537
+ return errorFactory !== undefined ? errorFactory(message) : new Error(message);
2538
+ }
2539
+ function parseIpv4(ip) {
2540
+ const parts = ip.split(".");
2541
+ if (parts.length !== 4) return null;
2542
+ const bytes = parts.map((p) => {
2543
+ if (!/^\d+$/.test(p)) return Number.NaN;
2544
+ const n = Number.parseInt(p, 10);
2545
+ return n >= 0 && n <= 255 ? n : Number.NaN;
2546
+ });
2547
+ if (bytes.some((b) => !Number.isFinite(b))) return null;
2548
+ return bytes;
2549
+ }
2550
+ function parseIpv6(ip) {
2551
+ const normalized = ip.toLowerCase();
2552
+ const zoneIndex = normalized.indexOf("%");
2553
+ const withoutZone = zoneIndex >= 0 ? normalized.slice(0, zoneIndex) : normalized;
2554
+ const expandSection = (section) => {
2555
+ if (!section) return [];
2556
+ const parts = section.split(":");
2557
+ const expanded$1 = [];
2558
+ for (const part of parts) {
2559
+ if (!part) return null;
2560
+ if (part.includes(".")) {
2561
+ const ipv4 = parseIpv4(part);
2562
+ if (ipv4 === null) return null;
2563
+ const [a, b, c, d] = ipv4;
2564
+ if (a === undefined || b === undefined || c === undefined || d === undefined) {
2565
+ return null;
2566
+ }
2567
+ expanded$1.push(a << 8 | b, c << 8 | d);
2568
+ continue;
2569
+ }
2570
+ if (!/^[0-9a-f]{1,4}$/i.test(part)) return null;
2571
+ expanded$1.push(Number.parseInt(part, 16));
2572
+ }
2573
+ return expanded$1;
2574
+ };
2575
+ const doubleColonIndex = withoutZone.indexOf("::");
2576
+ if (doubleColonIndex !== withoutZone.lastIndexOf("::")) return null;
2577
+ if (doubleColonIndex >= 0) {
2578
+ const head = withoutZone.slice(0, doubleColonIndex);
2579
+ const tail = withoutZone.slice(doubleColonIndex + 2);
2580
+ const headParts = expandSection(head);
2581
+ const tailParts = expandSection(tail);
2582
+ if (headParts === null || tailParts === null) return null;
2583
+ const zerosToInsert = 8 - (headParts.length + tailParts.length);
2584
+ if (zerosToInsert < 1) return null;
2585
+ return [
2586
+ ...headParts,
2587
+ ...Array.from({ length: zerosToInsert }, () => 0),
2588
+ ...tailParts
2589
+ ];
2590
+ }
2591
+ const expanded = expandSection(withoutZone);
2592
+ if (expanded === null || expanded.length !== 8) return null;
2593
+ return expanded;
2594
+ }
2595
+ function isPrivateIpv4(ip) {
2596
+ const bytes = parseIpv4(ip);
2597
+ if (bytes === null) return true;
2598
+ const a = bytes[0];
2599
+ const b = bytes[1];
2600
+ if (a === undefined || b === undefined) return true;
2601
+ if (a === 0) return true;
2602
+ if (a === 10) return true;
2603
+ if (a === 127) return true;
2604
+ if (a === 169 && b === 254) return true;
2605
+ if (a === 172) {
2606
+ if (b >= 16 && b <= 31) return true;
2607
+ }
2608
+ if (a === 192 && b === 0 && bytes[2] === 0) return true;
2609
+ if (a === 192 && b === 0 && bytes[2] === 2) return true;
2610
+ if (a === 192 && b === 168) return true;
2611
+ if (a === 100) {
2612
+ if (b >= 64 && b <= 127) return true;
2613
+ }
2614
+ if (a === 198 && (b === 18 || b === 19)) return true;
2615
+ if (a === 198 && b === 51 && bytes[2] === 100) return true;
2616
+ if (a === 203 && b === 0 && bytes[2] === 113) return true;
2617
+ if (a >= 224) return true;
2618
+ return false;
2619
+ }
2620
+ function isPrivateIpv6(ip) {
2621
+ const normalized = ip.toLowerCase();
2622
+ if (normalized === "::" || normalized === "::1") return true;
2623
+ if (normalized.startsWith("::ffff:")) {
2624
+ const maybeV4 = normalized.slice("::ffff:".length);
2625
+ if (parseIpv4(maybeV4) !== null) return isPrivateIpv4(maybeV4);
2626
+ return true;
2627
+ }
2628
+ const hextets = parseIpv6(normalized);
2629
+ if (hextets === null) return true;
2630
+ const first = hextets[0] ?? 0;
2631
+ const second = hextets[1] ?? 0;
2632
+ const third = hextets[2] ?? 0;
2633
+ const fourth = hextets[3] ?? 0;
2634
+ if ((first & 65024) === 64512) return true;
2635
+ if ((first & 65472) === 65152) return true;
2636
+ if (first === 256 && second === 0 && third === 0 && fourth === 0) return true;
2637
+ if (first === 8193 && second === 3512) return true;
2638
+ if (first === 8193 && (second & 65520) === 16) return true;
2639
+ if (first === 8194) return true;
2640
+ return false;
2641
+ }
2642
+ function isIpAddress(value) {
2643
+ return parseIpv4(value) !== null || parseIpv6(value) !== null;
2644
+ }
2645
+ function isPrivateIp(ip) {
2646
+ if (parseIpv4(ip) !== null) return isPrivateIpv4(ip);
2647
+ if (parseIpv6(ip) !== null) return isPrivateIpv6(ip);
2648
+ return true;
2649
+ }
2650
+ function assertAllowedPublicHostname(hostname, errorFactory) {
2651
+ const host = hostname.trim().toLowerCase().replace(/[。\uFF0E\uFF61]/gu, ".").replace(/\.+$/u, "");
2652
+ if (host.length === 0) throw buildPublicUrlValidationError("URL host is missing", errorFactory);
2653
+ if (host === "localhost" || host.endsWith(".localhost")) {
2654
+ throw buildPublicUrlValidationError("Localhost hosts are not allowed", errorFactory);
2655
+ }
2656
+ if (/^[0-9.]+$/.test(host) && !isIpAddress(host)) {
2657
+ throw buildPublicUrlValidationError("Numeric hosts are not allowed", errorFactory);
2658
+ }
2659
+ }
2660
+ function assertAllowedPort(url, options) {
2661
+ const { portPolicy, errorFactory } = options;
2662
+ const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : null;
2663
+ if (port !== null && (!Number.isFinite(port) || port <= 0 || port > 65535)) {
2664
+ throw buildPublicUrlValidationError("Invalid port", errorFactory);
2665
+ }
2666
+ if (portPolicy === "standard-only") {
2667
+ const allowedPort = url.protocol === "http:" ? 80 : 443;
2668
+ if (port !== null && port !== allowedPort) {
2669
+ throw buildPublicUrlValidationError("Non-standard ports are not allowed", errorFactory);
2670
+ }
2671
+ }
2672
+ }
2673
+ function assertAllowedPublicUrl(raw, options) {
2674
+ let url;
2675
+ try {
2676
+ url = new URL(raw);
2677
+ } catch {
2678
+ throw buildPublicUrlValidationError("Invalid URL", options.errorFactory);
2679
+ }
2680
+ if (!options.allowedProtocols.includes(url.protocol)) {
2681
+ throw buildPublicUrlValidationError(`Only ${options.allowedProtocols.join("/")} URLs are allowed`, options.errorFactory);
2682
+ }
2683
+ if (url.username || url.password) {
2684
+ throw buildPublicUrlValidationError("Credentialed URLs are not allowed", options.errorFactory);
2685
+ }
2686
+ const normalizedHostname = url.hostname.trim().toLowerCase().replace(/[。\uFF0E\uFF61]/gu, ".").replace(/\.+$/u, "");
2687
+ if (normalizedHostname.length === 0) {
2688
+ throw buildPublicUrlValidationError("URL host is missing", options.errorFactory);
2689
+ }
2690
+ url.hostname = normalizedHostname;
2691
+ assertAllowedPublicHostname(url.hostname, options.errorFactory);
2692
+ assertAllowedPort(url, options);
2693
+ const hostnameForIpCheck = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
2694
+ if (isIpAddress(hostnameForIpCheck) && isPrivateIp(hostnameForIpCheck)) {
2695
+ throw buildPublicUrlValidationError("Private IPs are not allowed", options.errorFactory);
2696
+ }
2697
+ url.hash = "";
2698
+ return url;
2699
+ }
2700
+
2701
+ //#endregion
2702
+ //#region ../core/src/public-profile.ts
2703
+ const publicProfileWeekdays = [
2704
+ "monday",
2705
+ "tuesday",
2706
+ "wednesday",
2707
+ "thursday",
2708
+ "friday",
2709
+ "saturday",
2710
+ "sunday"
2711
+ ];
2712
+ const knownSocialPlatforms = [
2713
+ "facebook",
2714
+ "instagram",
2715
+ "linkedin",
2716
+ "tiktok",
2717
+ "x",
2718
+ "youtube"
2719
+ ];
2720
+ function isRecord$5(value) {
2721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2722
+ }
2723
+ function trimmedOptionalString(value) {
2724
+ if (value === null || value === undefined) return null;
2725
+ if (typeof value !== "string") return undefined;
2726
+ const trimmed = value.trim();
2727
+ return trimmed.length > 0 ? trimmed : null;
2728
+ }
2729
+ function parsePublicContactDetails(value) {
2730
+ if (!isRecord$5(value)) {
2731
+ return err({
2732
+ code: "invalid_shape",
2733
+ path: "contact",
2734
+ message: "Contact details must be an object."
2735
+ });
2736
+ }
2737
+ const phone = trimmedOptionalString(value.phone);
2738
+ const email = trimmedOptionalString(value.email);
2739
+ if (phone === undefined || email === undefined) {
2740
+ return err({
2741
+ code: "invalid_shape",
2742
+ path: "contact",
2743
+ message: "Phone and email must be text values."
2744
+ });
2745
+ }
2746
+ if (phone === null && email === null) {
2747
+ return err({
2748
+ code: "contact_method_required",
2749
+ path: "contact",
2750
+ message: "Add a phone number or email address."
2751
+ });
2752
+ }
2753
+ if (email !== null && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email)) {
2754
+ return err({
2755
+ code: "invalid_email",
2756
+ path: "contact.email",
2757
+ message: "Enter a valid email address."
2758
+ });
2759
+ }
2760
+ if (phone !== null && !/^\+?[0-9][0-9 ().-]{5,30}$/u.test(phone)) {
2761
+ return err({
2762
+ code: "invalid_phone",
2763
+ path: "contact.phone",
2764
+ message: "Enter a valid phone number."
2765
+ });
2766
+ }
2767
+ if (phone !== null && email !== null) {
2768
+ return ok({
2769
+ kind: "phone-and-email",
2770
+ phone,
2771
+ email
2772
+ });
2773
+ }
2774
+ if (phone !== null) return ok({
2775
+ kind: "phone",
2776
+ phone,
2777
+ email: null
2778
+ });
2779
+ if (email !== null) return ok({
2780
+ kind: "email",
2781
+ phone: null,
2782
+ email
2783
+ });
2784
+ return err({
2785
+ code: "contact_method_required",
2786
+ path: "contact",
2787
+ message: "Add a phone number or email address."
2788
+ });
2789
+ }
2790
+ function parsePublicProfileTimeZone(value) {
2791
+ const timeZone = trimmedOptionalString(value);
2792
+ if (timeZone === null || timeZone === undefined) {
2793
+ return err({
2794
+ code: "invalid_timezone",
2795
+ path: "openingHours.timeZone",
2796
+ message: "Choose a timezone."
2797
+ });
2798
+ }
2799
+ try {
2800
+ new Intl.DateTimeFormat("en-GB", { timeZone }).format(new Date(0));
2801
+ return ok(timeZone);
2802
+ } catch {
2803
+ return err({
2804
+ code: "invalid_timezone",
2805
+ path: "openingHours.timeZone",
2806
+ message: "Choose a valid IANA timezone."
2807
+ });
2808
+ }
2809
+ }
2810
+ function parseOpeningHoursTime(value, path) {
2811
+ if (typeof value !== "string" || !/^(?:[01]\d|2[0-3]):[0-5]\d$/u.test(value)) {
2812
+ return err({
2813
+ code: "invalid_time",
2814
+ path,
2815
+ message: "Use a 24-hour time such as 09:00."
2816
+ });
2817
+ }
2818
+ return ok(value);
2819
+ }
2820
+ function minutesSinceMidnight(value) {
2821
+ const [hours = "0", minutes = "0"] = value.split(":");
2822
+ return Number(hours) * 60 + Number(minutes);
2823
+ }
2824
+ function parseOpeningHoursDay(value, expectedWeekday, index) {
2825
+ const path = `openingHours.days.${index}`;
2826
+ if (!isRecord$5(value) || value.weekday !== expectedWeekday) {
2827
+ return err({
2828
+ code: "invalid_weekdays",
2829
+ path,
2830
+ message: `Expected ${expectedWeekday} in calendar order.`
2831
+ });
2832
+ }
2833
+ if (value.kind === "closed") return ok({
2834
+ kind: "closed",
2835
+ weekday: expectedWeekday
2836
+ });
2837
+ if (value.kind !== "open" || !Array.isArray(value.intervals) || value.intervals.length === 0) {
2838
+ return err({
2839
+ code: "invalid_interval",
2840
+ path: `${path}.intervals`,
2841
+ message: "Open days need at least one opening interval."
2842
+ });
2843
+ }
2844
+ const intervals = [];
2845
+ for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
2846
+ const intervalPath = `${path}.intervals.${intervalIndex}`;
2847
+ if (!isRecord$5(rawInterval)) {
2848
+ return err({
2849
+ code: "invalid_interval",
2850
+ path: intervalPath,
2851
+ message: "Opening interval is invalid."
2852
+ });
2853
+ }
2854
+ const opensAt = parseOpeningHoursTime(rawInterval.opensAt, `${intervalPath}.opensAt`);
2855
+ if (!opensAt.ok) return opensAt;
2856
+ const closesAt = parseOpeningHoursTime(rawInterval.closesAt, `${intervalPath}.closesAt`);
2857
+ if (!closesAt.ok) return closesAt;
2858
+ if (minutesSinceMidnight(opensAt.value) >= minutesSinceMidnight(closesAt.value)) {
2859
+ return err({
2860
+ code: "invalid_interval",
2861
+ path: intervalPath,
2862
+ message: "Closing time must be later than opening time on the same day."
2863
+ });
2864
+ }
2865
+ intervals.push({
2866
+ opensAt: opensAt.value,
2867
+ closesAt: closesAt.value
2868
+ });
2869
+ }
2870
+ intervals.sort((left, right) => minutesSinceMidnight(left.opensAt) - minutesSinceMidnight(right.opensAt));
2871
+ for (let intervalIndex = 1; intervalIndex < intervals.length; intervalIndex += 1) {
2872
+ const previous = intervals[intervalIndex - 1];
2873
+ const current = intervals[intervalIndex];
2874
+ if (previous !== undefined && current !== undefined && minutesSinceMidnight(current.opensAt) < minutesSinceMidnight(previous.closesAt)) {
2875
+ return err({
2876
+ code: "overlapping_intervals",
2877
+ path: `${path}.intervals.${intervalIndex}`,
2878
+ message: "Opening intervals cannot overlap."
2879
+ });
2880
+ }
2881
+ }
2882
+ const [first, ...rest] = intervals;
2883
+ if (first === undefined) {
2884
+ return err({
2885
+ code: "invalid_interval",
2886
+ path,
2887
+ message: "Open days need an opening interval."
2888
+ });
2889
+ }
2890
+ return ok({
2891
+ kind: "open",
2892
+ weekday: expectedWeekday,
2893
+ intervals: [first, ...rest]
2894
+ });
2895
+ }
2896
+ function parseWeeklyOpeningHours(value) {
2897
+ if (!isRecord$5(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
2898
+ return err({
2899
+ code: "invalid_weekdays",
2900
+ path: "openingHours.days",
2901
+ message: "Opening hours must include all seven weekdays."
2902
+ });
2903
+ }
2904
+ const timeZone = parsePublicProfileTimeZone(value.timeZone);
2905
+ if (!timeZone.ok) return timeZone;
2906
+ const days = [];
2907
+ for (const [index, weekday] of publicProfileWeekdays.entries()) {
2908
+ const day = parseOpeningHoursDay(value.days[index], weekday, index);
2909
+ if (!day.ok) return day;
2910
+ days.push(day.value);
2911
+ }
2912
+ const [monday, tuesday, wednesday, thursday, friday, saturday, sunday] = days;
2913
+ if (monday === undefined || tuesday === undefined || wednesday === undefined || thursday === undefined || friday === undefined || saturday === undefined || sunday === undefined) {
2914
+ return err({
2915
+ code: "invalid_weekdays",
2916
+ path: "openingHours.days",
2917
+ message: "Opening hours are incomplete."
2918
+ });
2919
+ }
2920
+ return ok({
2921
+ timeZone: timeZone.value,
2922
+ days: [
2923
+ monday,
2924
+ tuesday,
2925
+ wednesday,
2926
+ thursday,
2927
+ friday,
2928
+ saturday,
2929
+ sunday
2930
+ ]
2931
+ });
2932
+ }
2933
+ function parseSocialLinkId(value, path) {
2934
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(value)) {
2935
+ return err({
2936
+ code: "invalid_social_link",
2937
+ path,
2938
+ message: "Social link ID is invalid."
2939
+ });
2940
+ }
2941
+ return ok(value);
2942
+ }
2943
+ function parseSocialUrl(value, path) {
2944
+ if (typeof value !== "string") {
2945
+ return err({
2946
+ code: "invalid_social_link",
2947
+ path,
2948
+ message: "Enter a valid public web address."
2949
+ });
2950
+ }
2951
+ try {
2952
+ const url = assertAllowedPublicUrl(value.trim(), {
2953
+ allowedProtocols: ["https:", "http:"],
2954
+ portPolicy: "standard-only"
2955
+ });
2956
+ return ok(url.toString());
2957
+ } catch {
2958
+ return err({
2959
+ code: "invalid_social_link",
2960
+ path,
2961
+ message: "Enter a valid public web address."
2962
+ });
2963
+ }
2964
+ }
2965
+ function parsePublicSocialLinks(value) {
2966
+ if (!Array.isArray(value)) {
2967
+ return err({
2968
+ code: "invalid_shape",
2969
+ path: "socialLinks",
2970
+ message: "Social links must be a list."
2971
+ });
2972
+ }
2973
+ const links = [];
2974
+ const ids = new Set();
2975
+ for (const [index, rawLink] of value.entries()) {
2976
+ const path = `socialLinks.${index}`;
2977
+ if (!isRecord$5(rawLink)) {
2978
+ return err({
2979
+ code: "invalid_social_link",
2980
+ path,
2981
+ message: "Social link is invalid."
2982
+ });
2983
+ }
2984
+ const id = parseSocialLinkId(rawLink.id, `${path}.id`);
2985
+ if (!id.ok) return id;
2986
+ if (ids.has(id.value)) {
2987
+ return err({
2988
+ code: "duplicate_social_link_id",
2989
+ path: `${path}.id`,
2990
+ message: "Social link IDs must be unique."
2991
+ });
2992
+ }
2993
+ ids.add(id.value);
2994
+ const url = parseSocialUrl(rawLink.url, `${path}.url`);
2995
+ if (!url.ok) return url;
2996
+ if (rawLink.kind === "known" && knownSocialPlatforms.includes(rawLink.platform)) {
2997
+ links.push({
2998
+ id: id.value,
2999
+ kind: "known",
3000
+ platform: rawLink.platform,
3001
+ url: url.value
3002
+ });
3003
+ continue;
3004
+ }
3005
+ const label = trimmedOptionalString(rawLink.label);
3006
+ if (rawLink.kind === "custom" && label !== null && label !== undefined) {
3007
+ links.push({
3008
+ id: id.value,
3009
+ kind: "custom",
3010
+ label,
3011
+ url: url.value
3012
+ });
3013
+ continue;
3014
+ }
3015
+ return err({
3016
+ code: "invalid_social_link",
3017
+ path,
3018
+ message: "Choose a known platform or add a custom label."
3019
+ });
3020
+ }
3021
+ return ok(links);
3022
+ }
3023
+ function parsePublicProfile(value) {
3024
+ if (!isRecord$5(value)) {
3025
+ return err({
3026
+ code: "invalid_shape",
3027
+ path: "",
3028
+ message: "Public profile must be an object."
3029
+ });
3030
+ }
3031
+ const contact = value.contact === null ? ok(null) : parsePublicContactDetails(value.contact);
3032
+ if (!contact.ok) return contact;
3033
+ const openingHours = value.openingHours === null ? ok(null) : parseWeeklyOpeningHours(value.openingHours);
3034
+ if (!openingHours.ok) return openingHours;
3035
+ const socialLinks = parsePublicSocialLinks(value.socialLinks);
3036
+ if (!socialLinks.ok) return socialLinks;
3037
+ return ok({
3038
+ contact: contact.value,
3039
+ openingHours: openingHours.value,
3040
+ socialLinks: socialLinks.value
3041
+ });
3042
+ }
3043
+ function emptyPublicProfile() {
3044
+ return {
3045
+ contact: null,
3046
+ openingHours: null,
3047
+ socialLinks: []
3048
+ };
3049
+ }
3050
+
3051
+ //#endregion
3052
+ //#region ../core/src/rich-text.ts
3053
+ function isRecord$4(value) {
3054
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3055
+ }
3056
+ function hasOnlyKeys(value, allowed) {
3057
+ return Object.keys(value).every((key) => allowed.has(key));
3058
+ }
3059
+ const rootKeys = new Set(["type", "content"]);
3060
+ const nodeKeys = new Set([
3061
+ "type",
3062
+ "content",
3063
+ "text",
3064
+ "attrs",
3065
+ "marks"
3066
+ ]);
3067
+ const markKeys = new Set(["type", "attrs"]);
3068
+ function isDenseArrayOf(value, predicate) {
3069
+ for (let index = 0; index < value.length; index += 1) {
3070
+ if (!(index in value) || !predicate(value[index])) return false;
3071
+ }
3072
+ return true;
3073
+ }
3074
+ function isTipTapMark(value) {
3075
+ return isRecord$4(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$4(value.attrs));
3076
+ }
3077
+ function isTipTapNode(value) {
3078
+ 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))) {
3079
+ return false;
3080
+ }
3081
+ if (value.type === "text") {
3082
+ return typeof value.text === "string" && value.text.length > 0 && value.content === undefined;
3083
+ }
3084
+ return value.text === undefined;
3085
+ }
3086
+ function isCanonicalRichTextDoc(value) {
3087
+ return isRecord$4(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3088
+ }
3089
+
3090
+ //#endregion
3091
+ //#region ../core/src/newsletter-signup-destination.ts
3092
+ 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;
3093
+ const MAILCHIMP_AUDIENCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3094
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
3095
+ function isRecord$3(value) {
3096
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3097
+ }
3098
+ function parseTrimmedString(value) {
3099
+ if (typeof value !== "string") return null;
3100
+ const trimmed = value.trim();
3101
+ return trimmed.length > 0 ? trimmed : null;
3102
+ }
3103
+ function parseNewsletterListId(value) {
3104
+ const trimmed = parseTrimmedString(value);
3105
+ return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3106
+ }
3107
+ function parseMailchimpConnectionId(value) {
3108
+ const trimmed = parseTrimmedString(value);
3109
+ return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
3110
+ }
3111
+ function parseMailchimpAudienceId(value) {
3112
+ const trimmed = parseTrimmedString(value);
3113
+ return trimmed !== null && MAILCHIMP_AUDIENCE_ID_PATTERN.test(trimmed) ? trimmed : null;
3114
+ }
3115
+ function parseMailchimpTagName(value) {
3116
+ const trimmed = parseTrimmedString(value);
3117
+ return trimmed !== null && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
3118
+ }
3119
+ function parseNewsletterSignupDestinationProvider(value) {
3120
+ return value === "riverbank" || value === "mailchimp" ? value : null;
3121
+ }
3122
+ function parseNewsletterSignupAuthoringDestinationValue(value) {
3123
+ if (!isRecord$3(value) || typeof value.tag !== "string") {
3124
+ return err({ kind: "invalid-destination-shape" });
3125
+ }
3126
+ switch (value.tag) {
3127
+ case "riverbank_list": {
3128
+ const listId = parseNewsletterListId(value.listId);
3129
+ return listId === null ? err({ kind: "invalid-newsletter-list-id" }) : ok({
3130
+ tag: "riverbank_list",
3131
+ listId
3132
+ });
3133
+ }
3134
+ case "mailchimp_audience": {
3135
+ const connectionId = parseMailchimpConnectionId(value.connectionId);
3136
+ if (connectionId === null) {
3137
+ return err({ kind: "invalid-mailchimp-connection-id" });
3138
+ }
3139
+ const audienceId = parseMailchimpAudienceId(value.audienceId);
3140
+ if (audienceId === null) {
3141
+ return err({ kind: "invalid-mailchimp-audience-id" });
3142
+ }
3143
+ const rawTags = value.tags ?? [];
3144
+ if (!Array.isArray(rawTags)) {
3145
+ return err({ kind: "invalid-mailchimp-tags" });
3146
+ }
3147
+ const tags = [];
3148
+ for (const valueTag of rawTags) {
3149
+ const tag = parseMailchimpTagName(valueTag);
3150
+ if (tag === null) return err({ kind: "invalid-mailchimp-tags" });
3151
+ tags.push(tag);
3152
+ }
3153
+ return ok({
3154
+ tag: "mailchimp_audience",
3155
+ connectionId,
3156
+ audienceId,
3157
+ tags
3158
+ });
3159
+ }
3160
+ default: return err({
3161
+ kind: "invalid-destination-tag",
3162
+ value: value.tag
3163
+ });
3164
+ }
3165
+ }
3166
+
3167
+ //#endregion
3168
+ //#region ../core/src/footer-engagement.ts
3169
+ function isRecord$2(value) {
3170
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3171
+ }
3172
+ function invalid(path, message) {
3173
+ return {
3174
+ code: "invalid_section",
3175
+ path,
3176
+ message
3177
+ };
3178
+ }
3179
+ function parseBoundedText(value, path, options) {
3180
+ if (options.optional === true && (value === null || value === undefined)) {
3181
+ return ok(null);
3182
+ }
3183
+ if (typeof value !== "string") {
3184
+ return err(invalid(path, "Footer engagement copy must be text."));
3185
+ }
3186
+ const text = value.trim();
3187
+ if (text.length === 0) {
3188
+ return options.optional === true ? ok(null) : err(invalid(path, "Footer engagement copy cannot be empty."));
3189
+ }
3190
+ return text.length <= options.maxLength ? ok(text) : err(invalid(path, `Footer engagement copy must be ${options.maxLength} characters or fewer.`));
3191
+ }
3192
+ function parseTreatment(value, path) {
3193
+ return value === "integrated" || value === "separate-row" ? ok(value) : err(invalid(path, "Footer engagement treatment is invalid."));
3194
+ }
3195
+ function parseEngagementBase(value, base, path) {
3196
+ const heading = parseBoundedText(value.heading, `${path}.heading`, { maxLength: 80 });
3197
+ if (!heading.ok) return heading;
3198
+ if (heading.value === null) {
3199
+ return err(invalid(`${path}.heading`, "Footer engagement heading cannot be empty."));
3200
+ }
3201
+ const treatment = parseTreatment(value.treatment, `${path}.treatment`);
3202
+ if (!treatment.ok) return treatment;
3203
+ if (!isRecord$2(value.source)) {
3204
+ return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3205
+ }
3206
+ const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
3207
+ maxLength: 240,
3208
+ optional: true
3209
+ });
3210
+ return intro.ok ? ok({
3211
+ ...base,
3212
+ heading: heading.value,
3213
+ treatment: treatment.value,
3214
+ intro: intro.value
3215
+ }) : intro;
3216
+ }
3217
+ function parseStandardContact(source, base, path) {
3218
+ const formId = parseFormId(source.formId);
3219
+ const { intro, ...section } = base;
3220
+ return formId.ok ? ok({
3221
+ ...section,
3222
+ kind: "engagement",
3223
+ source: {
3224
+ kind: "standard-contact",
3225
+ formId: formId.value,
3226
+ intro
3227
+ }
3228
+ }) : err(invalid(`${path}.formId`, "Footer engagement form ID is invalid."));
3229
+ }
3230
+ function parseMailingList(source, base, path) {
3231
+ const { intro, ...section } = base;
3232
+ const destination = parseNewsletterSignupAuthoringDestinationValue(source.destination);
3233
+ if (!destination.ok) {
3234
+ return err(invalid(`${path}.destination`, "Footer mailing-list destination is invalid."));
3235
+ }
3236
+ const buttonLabel = parseBoundedText(source.buttonLabel, `${path}.buttonLabel`, { maxLength: 40 });
3237
+ if (!buttonLabel.ok) return buttonLabel;
3238
+ const successMessage = parseBoundedText(source.successMessage, `${path}.successMessage`, { maxLength: 160 });
3239
+ if (!successMessage.ok) return successMessage;
3240
+ const privacyNote = parseBoundedText(source.privacyNote, `${path}.privacyNote`, {
3241
+ maxLength: 240,
3242
+ optional: true
3243
+ });
3244
+ if (!privacyNote.ok) return privacyNote;
3245
+ if (buttonLabel.value === null || successMessage.value === null) {
3246
+ return err(invalid(path, "Footer mailing-list copy cannot be empty."));
3247
+ }
3248
+ if (source.fieldPreset !== "emailOnly") {
3249
+ return err(invalid(`${path}.fieldPreset`, "Footer mailing-list signup must be email-only."));
3250
+ }
3251
+ return ok({
3252
+ ...section,
3253
+ kind: "engagement",
3254
+ source: {
3255
+ kind: "mailing-list",
3256
+ destination: destination.value,
3257
+ intro,
3258
+ buttonLabel: buttonLabel.value,
3259
+ successMessage: successMessage.value,
3260
+ privacyNote: privacyNote.value,
3261
+ fieldPreset: "emailOnly"
3262
+ }
3263
+ });
3264
+ }
3265
+ function parseFooterEngagementSectionValue(value, sectionBase, path) {
3266
+ const base = parseEngagementBase(value, sectionBase, path);
3267
+ if (!base.ok) return base;
3268
+ const source = value.source;
3269
+ if (!isRecord$2(source)) {
3270
+ return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3271
+ }
3272
+ switch (source.kind) {
3273
+ case "standard-contact": return parseStandardContact(source, base.value, `${path}.source`);
3274
+ case "mailing-list": return parseMailingList(source, base.value, `${path}.source`);
3275
+ default: return err(invalid(`${path}.source.kind`, "Footer engagement source kind is invalid."));
3276
+ }
3277
+ }
3278
+
3279
+ //#endregion
3280
+ //#region ../core/src/footer-arrangement.ts
3281
+ const sectionKinds = [
3282
+ "brand",
3283
+ "supporting-text",
3284
+ "social-links",
3285
+ "navigation",
3286
+ "business-address",
3287
+ "contact",
3288
+ "opening-hours",
3289
+ "engagement",
3290
+ "legal-navigation",
3291
+ "legal"
3292
+ ];
3293
+ const sectionKindOrder = new Map(sectionKinds.map((kind, index) => [kind, index]));
3294
+ const visualGroupOrder = [
3295
+ "identity",
3296
+ "main-navigation",
3297
+ "visit",
3298
+ "engagement",
3299
+ "legal-ending"
3300
+ ];
3301
+ function groupFor(kind) {
3302
+ switch (kind) {
3303
+ case "brand":
3304
+ case "supporting-text":
3305
+ case "social-links": return "identity";
3306
+ case "navigation": return "main-navigation";
3307
+ case "business-address":
3308
+ case "contact":
3309
+ case "opening-hours": return "visit";
3310
+ case "engagement": return "engagement";
3311
+ case "legal-navigation":
3312
+ case "legal": return "legal-ending";
3313
+ }
3314
+ }
3315
+ function buildGroups(sections, navigationLayout) {
3316
+ const groups = new Map();
3317
+ for (const section of sections) {
3318
+ const kind = groupFor(section.kind);
3319
+ groups.set(kind, [...groups.get(kind) ?? [], section]);
3320
+ }
3321
+ return visualGroupOrder.flatMap((kind) => {
3322
+ const groupSections = groups.get(kind);
3323
+ return groupSections === undefined || groupSections.length === 0 ? [] : [{
3324
+ kind,
3325
+ width: kind === "main-navigation" && navigationLayout === "inline-wrap" ? "full-row" : "column",
3326
+ sections: [...groupSections].sort((left, right) => (sectionKindOrder.get(left.kind) ?? Number.MAX_SAFE_INTEGER) - (sectionKindOrder.get(right.kind) ?? Number.MAX_SAFE_INTEGER))
3327
+ }];
3328
+ });
3329
+ }
3330
+ function membershipKey(sections) {
3331
+ const tokens = sections.map((section) => ({
3332
+ rank: sectionKindOrder.get(section.kind) ?? Number.MAX_SAFE_INTEGER,
3333
+ value: section.kind === "engagement" ? `engagement:${section.treatment}` : section.kind
3334
+ }));
3335
+ tokens.sort((left, right) => left.rank - right.rank);
3336
+ return tokens.map(({ value }) => value).join("+") || "empty";
3337
+ }
3338
+ function planFooterArrangement(renderableSections, policy) {
3339
+ const selectedSections = renderableSections.map(({ section }) => section);
3340
+ const groups = buildGroups(selectedSections, policy.navigationLayout);
3341
+ const engagement = selectedSections.find((section) => section.kind === "engagement");
3342
+ const separateEngagement = engagement?.treatment === "separate-row" ? groups.filter((group) => group.kind === "engagement") : [];
3343
+ const layoutGroups = separateEngagement.length === 0 ? groups : groups.filter((group) => group.kind !== "engagement");
3344
+ const legalGroups = layoutGroups.filter((group) => group.kind === "legal-ending");
3345
+ const unsortedMainGroups = policy.kind === "separate-legal-region" ? layoutGroups.filter((group) => group.kind !== "legal-ending") : layoutGroups;
3346
+ const mainGroups = [...unsortedMainGroups.filter((group) => group.width === "column"), ...unsortedMainGroups.filter((group) => group.width === "full-row")];
3347
+ const regions = [];
3348
+ if (separateEngagement.length > 0) {
3349
+ regions.push({
3350
+ kind: "engagement-row",
3351
+ columns: 1,
3352
+ groups: separateEngagement
3353
+ });
3354
+ }
3355
+ if (mainGroups.length > 0) {
3356
+ regions.push({
3357
+ kind: "main",
3358
+ columns: Math.max(1, Math.min(mainGroups.filter((group) => group.width === "column").length, 5)),
3359
+ groups: mainGroups
3360
+ });
3361
+ }
3362
+ if (policy.kind === "separate-legal-region" && legalGroups.length > 0) {
3363
+ regions.push({
3364
+ kind: "bottom",
3365
+ columns: 1,
3366
+ groups: legalGroups
3367
+ });
3368
+ }
3369
+ return {
3370
+ membershipKey: membershipKey(selectedSections),
3371
+ regions,
3372
+ mobileGroups: separateEngagement.length === 0 ? groups : [...separateEngagement, ...layoutGroups]
3373
+ };
3374
+ }
3375
+
3376
+ //#endregion
3377
+ //#region ../core/src/footer-composition.ts
3378
+ function unsafeAsFooterModuleId(value) {
3379
+ return value;
3380
+ }
3381
+ function parseFooterModuleId(value) {
3382
+ return parseSectionId(value, "sectionId");
3383
+ }
3384
+ function isRecord$1(value) {
3385
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3386
+ }
3387
+ function isRichText(value) {
3388
+ return isCanonicalRichTextDoc(value);
3389
+ }
3390
+ function parseSectionId(value, path) {
3391
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(value)) {
3392
+ return err({
3393
+ code: "invalid_section",
3394
+ path,
3395
+ message: "Footer section ID is invalid."
3396
+ });
3397
+ }
3398
+ return ok(unsafeAsFooterModuleId(value));
3399
+ }
3400
+ function parseHeading(value, path) {
3401
+ if (value === null || value === undefined) return ok(null);
3402
+ if (typeof value !== "string") {
3403
+ return err({
3404
+ code: "invalid_section",
3405
+ path,
3406
+ message: "Footer heading must be text."
3407
+ });
3408
+ }
3409
+ const heading = value.trim();
3410
+ return ok(heading.length > 0 ? heading : null);
3411
+ }
3412
+ function parseFooterSection(value, index) {
3413
+ const path = `sections.${index}`;
3414
+ if (!isRecord$1(value)) {
3415
+ return err({
3416
+ code: "invalid_section",
3417
+ path,
3418
+ message: "Footer section must be an object."
3419
+ });
3420
+ }
3421
+ const id = parseSectionId(value.id, `${path}.id`);
3422
+ if (!id.ok) return id;
3423
+ const heading = parseHeading(value.heading, `${path}.heading`);
3424
+ if (!heading.ok) return heading;
3425
+ if (typeof value.selected !== "boolean") {
3426
+ return err({
3427
+ code: "invalid_section",
3428
+ path: `${path}.selected`,
3429
+ message: "Footer selection intent is missing."
3430
+ });
3431
+ }
3432
+ const base = {
3433
+ id: id.value,
3434
+ selected: value.selected
3435
+ };
3436
+ switch (value.kind) {
3437
+ case "brand":
3438
+ if (typeof value.showLogo === "boolean" && typeof value.showTitle === "boolean") {
3439
+ return ok({
3440
+ ...base,
3441
+ kind: "brand",
3442
+ heading: heading.value,
3443
+ showLogo: value.showLogo,
3444
+ showTitle: value.showTitle
3445
+ });
3446
+ }
3447
+ break;
3448
+ case "navigation":
3449
+ if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
3450
+ return ok({
3451
+ ...base,
3452
+ kind: "navigation",
3453
+ heading: heading.value,
3454
+ source: {
3455
+ kind: "menu-role",
3456
+ role: "main"
3457
+ }
3458
+ });
3459
+ }
3460
+ break;
3461
+ case "legal-navigation":
3462
+ if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
3463
+ return ok({
3464
+ ...base,
3465
+ kind: "legal-navigation",
3466
+ heading: heading.value,
3467
+ source: {
3468
+ kind: "menu-role",
3469
+ role: "legal-policy"
3470
+ }
3471
+ });
3472
+ }
3473
+ break;
3474
+ case "business-address":
3475
+ if (isRecord$1(value.source) && value.source.kind === "business-address") {
3476
+ return ok({
3477
+ ...base,
3478
+ kind: "business-address",
3479
+ heading: heading.value,
3480
+ source: { kind: "business-address" }
3481
+ });
3482
+ }
3483
+ break;
3484
+ case "contact":
3485
+ case "opening-hours":
3486
+ case "social-links":
3487
+ if (isRecord$1(value.source) && value.source.kind === "public-profile") {
3488
+ return ok({
3489
+ ...base,
3490
+ kind: value.kind,
3491
+ heading: heading.value,
3492
+ source: { kind: "public-profile" }
3493
+ });
3494
+ }
3495
+ break;
3496
+ case "supporting-text":
3497
+ case "legal":
3498
+ if (value.content === null || isRichText(value.content)) {
3499
+ return ok({
3500
+ ...base,
3501
+ kind: value.kind,
3502
+ heading: heading.value,
3503
+ content: value.content
3504
+ });
3505
+ }
3506
+ break;
3507
+ case "engagement": return parseFooterEngagementSectionValue(value, base, path);
3508
+ }
3509
+ return err({
3510
+ code: "invalid_section",
3511
+ path,
3512
+ message: "Footer section does not match its declared kind."
3513
+ });
3514
+ }
3515
+ function validateFooterComposition(composition) {
3516
+ const sectionIds = new Set();
3517
+ const kinds = new Set();
3518
+ for (const [index, section] of composition.sections.entries()) {
3519
+ if (sectionIds.has(section.id)) {
3520
+ return err({
3521
+ code: "duplicate_section_id",
3522
+ path: `sections.${index}.id`,
3523
+ message: "Footer section IDs must be unique."
3524
+ });
3525
+ }
3526
+ sectionIds.add(section.id);
3527
+ if (kinds.has(section.kind)) {
3528
+ return err({
3529
+ code: "duplicate_section_kind",
3530
+ path: `sections.${index}.kind`,
3531
+ message: "Each footer section kind can appear only once."
3532
+ });
3533
+ }
3534
+ kinds.add(section.kind);
3535
+ }
3536
+ return ok(composition);
3537
+ }
3538
+ function parseFooterCompositionV1(value) {
3539
+ if (!isRecord$1(value)) {
3540
+ return err({
3541
+ code: "invalid_shape",
3542
+ path: "",
3543
+ message: "Footer composition must be an object."
3544
+ });
3545
+ }
3546
+ if (value.schemaVersion !== 1) {
3547
+ return err({
3548
+ code: "unsupported_schema_version",
3549
+ path: "schemaVersion",
3550
+ message: "Footer composition version is not supported."
3551
+ });
3552
+ }
3553
+ if (!Array.isArray(value.sections)) {
3554
+ return err({
3555
+ code: "invalid_shape",
3556
+ path: "sections",
3557
+ message: "Footer sections are missing."
3558
+ });
3559
+ }
3560
+ const sections = [];
3561
+ for (const [index, rawSection] of value.sections.entries()) {
3562
+ const section = parseFooterSection(rawSection, index);
3563
+ if (!section.ok) return section;
3564
+ sections.push(section.value);
3565
+ }
3566
+ return validateFooterComposition({
3567
+ schemaVersion: 1,
3568
+ sections
3569
+ });
3570
+ }
3571
+ function parseLegacyPlacementSection(value, index, selectedIds) {
3572
+ if (!isRecord$1(value)) {
3573
+ return err({
3574
+ code: "invalid_section",
3575
+ path: `modules.${index}`,
3576
+ message: "Footer module must be an object."
3577
+ });
3578
+ }
3579
+ const selected = typeof value.id === "string" && selectedIds.has(value.id);
3580
+ const source = isRecord$1(value.source) ? value.source : null;
3581
+ const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
3582
+ ...value,
3583
+ source: {
3584
+ kind: "menu-role",
3585
+ role: "main"
3586
+ },
3587
+ selected
3588
+ } : {
3589
+ ...value,
3590
+ selected
3591
+ };
3592
+ return parseFooterSection(mapped, index);
3593
+ }
3594
+ /**
3595
+ * Bounded pre-production upgrader for the placement-shaped V1 that never reached production.
3596
+ * It is intentionally not called by the runtime parser.
3597
+ */
3598
+ function upgradePlacementFooterCompositionV1(value) {
3599
+ if (!isRecord$1(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
3600
+ return err({
3601
+ code: "invalid_shape",
3602
+ path: "",
3603
+ message: "Placement-based footer composition is invalid."
3604
+ });
3605
+ }
3606
+ const selectedIds = new Set();
3607
+ for (const [index, placement] of value.placements.entries()) {
3608
+ if (!isRecord$1(placement) || typeof placement.moduleId !== "string") {
3609
+ return err({
3610
+ code: "invalid_shape",
3611
+ path: `placements.${index}`,
3612
+ message: "Footer placement is invalid."
3613
+ });
3614
+ }
3615
+ if (selectedIds.has(placement.moduleId)) {
3616
+ return err({
3617
+ code: "invalid_shape",
3618
+ path: `placements.${index}.moduleId`,
3619
+ message: "A legacy footer module can only be placed once."
3620
+ });
3621
+ }
3622
+ selectedIds.add(placement.moduleId);
3623
+ }
3624
+ const sections = [];
3625
+ for (const [index, rawModule] of value.modules.entries()) {
3626
+ const section = parseLegacyPlacementSection(rawModule, index, selectedIds);
3627
+ if (!section.ok) return section;
3628
+ sections.push(section.value);
3629
+ }
3630
+ const sectionIds = new Set(sections.map((section) => section.id));
3631
+ const unknownPlacedId = [...selectedIds].find((sectionId) => !sectionIds.has(sectionId));
3632
+ if (unknownPlacedId !== undefined) {
3633
+ return err({
3634
+ code: "unknown_section",
3635
+ path: "placements",
3636
+ message: `Legacy placement references unknown section ${unknownPlacedId}.`
3637
+ });
3638
+ }
3639
+ return validateFooterComposition({
3640
+ schemaVersion: 1,
3641
+ sections
3642
+ });
3643
+ }
3644
+ function setFooterSectionSelected(composition, sectionId, selected) {
3645
+ if (!composition.sections.some((section) => section.id === sectionId)) {
3646
+ return err({
3647
+ code: "unknown_section",
3648
+ path: "sectionId",
3649
+ message: "Footer section was not found."
3650
+ });
3651
+ }
3652
+ return ok({
3653
+ ...composition,
3654
+ sections: composition.sections.map((section) => section.id === sectionId ? {
3655
+ ...section,
3656
+ selected
3657
+ } : section)
3658
+ });
3659
+ }
3660
+ function parseFooterEngagementSection(value) {
3661
+ const parsed = parseFooterSection(value, 0);
3662
+ if (!parsed.ok) return parsed;
3663
+ return parsed.value.kind === "engagement" ? ok(parsed.value) : err({
3664
+ code: "invalid_section",
3665
+ path: "sections.0.kind",
3666
+ message: "Footer section must be an engagement section."
3667
+ });
3668
+ }
3669
+ function upsertFooterEngagementSection(composition, section) {
3670
+ const hasEngagement = composition.sections.some((candidate) => candidate.kind === "engagement");
3671
+ return validateFooterComposition({
3672
+ ...composition,
3673
+ sections: hasEngagement ? composition.sections.map((candidate) => candidate.kind === "engagement" ? section : candidate) : [...composition.sections, section]
3674
+ });
3675
+ }
3676
+ function removeFooterEngagementSection(composition) {
3677
+ return {
3678
+ ...composition,
3679
+ sections: composition.sections.filter((section) => section.kind !== "engagement")
3680
+ };
3681
+ }
3682
+ function repairTargetFor(section) {
3683
+ switch (section.kind) {
3684
+ case "brand": return { kind: "site-identity" };
3685
+ case "navigation": return {
3686
+ kind: "navigation",
3687
+ role: "main"
3688
+ };
3689
+ case "legal-navigation": return {
3690
+ kind: "navigation",
3691
+ role: "legal-policy"
3692
+ };
3693
+ case "business-address": return { kind: "business-address" };
3694
+ case "engagement": return {
3695
+ kind: "engagement-source",
3696
+ source: section.source.kind
3697
+ };
3698
+ case "contact":
3699
+ case "opening-hours":
3700
+ case "social-links": return {
3701
+ kind: "public-profile",
3702
+ section: section.kind
3703
+ };
3704
+ case "supporting-text":
3705
+ case "legal": return {
3706
+ kind: "footer-section",
3707
+ sectionId: section.id
3708
+ };
3709
+ }
3710
+ }
3711
+ function resolveFooterSections(composition, availability) {
3712
+ return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability) ? {
3713
+ kind: "renderable",
3714
+ section
3715
+ } : {
3716
+ kind: "missing-source",
3717
+ section,
3718
+ repairTarget: repairTargetFor(section)
3719
+ });
3720
+ }
3721
+ function isSectionRenderable(section, availability) {
3722
+ switch (section.kind) {
3723
+ case "brand": return section.showLogo && availability.hasLogo || section.showTitle;
3724
+ case "navigation": return availability.hasMainNavigation;
3725
+ case "legal-navigation": return availability.hasLegalPolicyNavigation;
3726
+ case "business-address": return availability.hasBusinessAddress;
3727
+ case "contact": return availability.hasContact;
3728
+ case "opening-hours": return availability.hasOpeningHours;
3729
+ case "social-links": return availability.hasSocialLinks;
3730
+ case "engagement": return availability.hasEngagementSource;
3731
+ case "supporting-text":
3732
+ case "legal": return section.content !== null;
3733
+ }
3734
+ }
3735
+ function deriveFooterEditorOverview(composition) {
3736
+ return {
3737
+ selected: composition.sections.filter((section) => section.selected).map((section) => ({
3738
+ section,
3739
+ status: "selected"
3740
+ })),
3741
+ available: composition.sections.filter((section) => !section.selected).map((section) => ({
3742
+ section,
3743
+ status: "available"
3744
+ }))
3745
+ };
3746
+ }
3747
+ const defaultSections = (legacy, options) => {
3748
+ const footerVisible = options.footerVisible ?? true;
3749
+ return [
3750
+ {
3751
+ id: unsafeAsFooterModuleId("brand"),
3752
+ kind: "brand",
3753
+ heading: null,
3754
+ selected: footerVisible,
3755
+ showLogo: options.brand?.showLogo ?? true,
3756
+ showTitle: options.brand?.showTitle ?? true
3757
+ },
3758
+ {
3759
+ id: unsafeAsFooterModuleId("navigation"),
3760
+ kind: "navigation",
3761
+ heading: null,
3762
+ selected: footerVisible,
3763
+ source: {
3764
+ kind: "menu-role",
3765
+ role: "main"
3766
+ }
3767
+ },
3768
+ {
3769
+ id: unsafeAsFooterModuleId("legal-navigation"),
3770
+ kind: "legal-navigation",
3771
+ heading: null,
3772
+ selected: false,
3773
+ source: {
3774
+ kind: "menu-role",
3775
+ role: "legal-policy"
3776
+ }
3777
+ },
3778
+ {
3779
+ id: unsafeAsFooterModuleId("business-address"),
3780
+ kind: "business-address",
3781
+ heading: null,
3782
+ selected: footerVisible && legacy.showBusinessAddress === true,
3783
+ source: { kind: "business-address" }
3784
+ },
3785
+ {
3786
+ id: unsafeAsFooterModuleId("contact"),
3787
+ kind: "contact",
3788
+ heading: null,
3789
+ selected: false,
3790
+ source: { kind: "public-profile" }
3791
+ },
3792
+ {
3793
+ id: unsafeAsFooterModuleId("opening-hours"),
3794
+ kind: "opening-hours",
3795
+ heading: null,
3796
+ selected: false,
3797
+ source: { kind: "public-profile" }
3798
+ },
3799
+ {
3800
+ id: unsafeAsFooterModuleId("social-links"),
3801
+ kind: "social-links",
3802
+ heading: null,
3803
+ selected: false,
3804
+ source: { kind: "public-profile" }
3805
+ },
3806
+ {
3807
+ id: unsafeAsFooterModuleId("supporting-text"),
3808
+ kind: "supporting-text",
3809
+ heading: null,
3810
+ selected: footerVisible && isRichText(legacy.bodyText),
3811
+ content: isRichText(legacy.bodyText) ? legacy.bodyText : null
3812
+ },
3813
+ {
3814
+ id: unsafeAsFooterModuleId("legal"),
3815
+ kind: "legal",
3816
+ heading: null,
3817
+ selected: footerVisible && isRichText(legacy.bottomText),
3818
+ content: isRichText(legacy.bottomText) ? legacy.bottomText : null
3819
+ }
3820
+ ];
3821
+ };
3822
+ function normalizeLegacyFooterContentToV1(value, options = {}) {
3823
+ if (!isRecord$1(value)) {
3824
+ return err({
3825
+ code: "invalid_shape",
3826
+ path: "",
3827
+ message: "Legacy footer content must be an object."
3828
+ });
3829
+ }
3830
+ return validateFooterComposition({
3831
+ schemaVersion: 1,
3832
+ sections: defaultSections(value, options)
3833
+ });
3834
+ }
3835
+
3836
+ //#endregion
3837
+ //#region ../core/src/footer-navigation-layout.ts
3838
+ const footerNavigationLayouts = ["stack", "inline-wrap"];
3839
+ const defaultFooterNavigationLayout = "inline-wrap";
3840
+ function normalizeFooterNavigationLayout(value) {
3841
+ return value === "stack" ? "stack" : defaultFooterNavigationLayout;
3842
+ }
3843
+
3844
+ //#endregion
3845
+ //#region ../core/src/number-input.ts
3846
+ /**
3847
+ * Numeric form-input parsing helpers with preserve-prev semantics on
3848
+ * NaN/empty/negative. Use these in `NumberInput.onChange` so empty input
3849
+ * doesn't silently rewrite the user's value to 0.
3850
+ *
3851
+ * The caller skips the dispatch when `null` is returned; the input box
3852
+ * keeps showing whatever the user typed (it's an uncontrolled native
3853
+ * input from React's perspective until the next state change), and the
3854
+ * dirty/saved state stays consistent.
3855
+ */
3856
+ /**
3857
+ * Parse a `valueAsNumber` (from `<input type="number">`) into a finite
3858
+ * non-negative integer, or `null` if the value is empty / NaN / negative.
3859
+ * Fractional input is `Math.trunc`'d to an integer.
3860
+ */
3861
+ function parseFiniteNonNegativeInt(value) {
3862
+ if (!Number.isFinite(value)) return null;
3863
+ if (value < 0) return null;
3864
+ return Math.trunc(value);
3865
+ }
3866
+ /**
3867
+ * Parse a `valueAsNumber` into an integer bounded inclusive by `[min, max]`.
3868
+ * Returns `null` for out-of-range or non-finite input — same preserve-prev
3869
+ * pattern as `parseFiniteNonNegativeInt`.
3870
+ */
3871
+ function parseBoundedInt(value, min, max) {
3872
+ if (!Number.isFinite(value)) return null;
3873
+ const truncated = Math.trunc(value);
3874
+ if (truncated < min) return null;
3875
+ if (truncated > max) return null;
3876
+ return truncated;
3877
+ }
3878
+
3879
+ //#endregion
3880
+ //#region ../core/src/field-errors.ts
3881
+ function clearFieldError(errors, field) {
3882
+ const error = errors[field];
3883
+ if (error === undefined || error.length === 0) {
3884
+ return errors;
3885
+ }
3886
+ const nextErrors = { ...errors };
3887
+ delete nextErrors[field];
3888
+ return nextErrors;
3889
+ }
3890
+
3891
+ //#endregion
3892
+ //#region ../core/src/form-submission-preview.ts
3893
+ const FORM_SUBMISSION_PSEUDO_FIELD_ID = "submission";
3894
+ const HIDDEN_FIELD_IDS = new Set(["form_id", "formid"]);
3895
+ const PRIORITY_IDS = new Set([
3896
+ "name",
3897
+ "full_name",
3898
+ "fullname",
3899
+ "first_name",
3900
+ "firstname",
3901
+ "last_name",
3902
+ "lastname",
3903
+ "email",
3904
+ "email_address",
3905
+ "emailaddress"
3906
+ ]);
3907
+ const MAX_COLUMNS = 5;
3908
+ function isRecord(value) {
3909
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3910
+ }
3911
+ function isSchemaField(value) {
3912
+ return isRecord(value) && typeof value.id === "string";
3913
+ }
3914
+ function selectSubmissionPreviewFields(schemaFields, submissions) {
3915
+ const fields = Array.isArray(schemaFields) ? schemaFields.filter(isSchemaField) : [];
3916
+ if (!fields.length) {
3917
+ const fallbackKeys = collectFallbackKeys(submissions);
3918
+ return fallbackKeys.slice(0, MAX_COLUMNS).map((key) => ({
3919
+ id: key,
3920
+ label: humanizeSubmissionFieldKey(key),
3921
+ type: "text"
3922
+ }));
3923
+ }
3924
+ const scored = fields.map((field, index) => {
3925
+ const id = trimmedStringOrNull(field.id);
3926
+ if (id === null) return null;
3927
+ const type = (field.type ?? "text").toString().toLowerCase();
3928
+ if (shouldHideInPreview(type)) return null;
3929
+ const label = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
3930
+ const normalizedId = id.toLowerCase();
3931
+ const normalizedLabel = label.toLowerCase();
3932
+ let score = 0;
3933
+ if (PRIORITY_IDS.has(normalizedId)) score += 500;
3934
+ if (normalizedId.includes("name") || normalizedLabel.includes("name")) score += 550;
3935
+ if (normalizedId.includes("email") || normalizedLabel.includes("email") || type === "email") score += 450;
3936
+ if (normalizedId.includes("phone") || normalizedLabel.includes("phone")) score += 250;
3937
+ if (normalizedId.includes("company") || normalizedLabel.includes("company")) score += 150;
3938
+ if (type === "select" || type === "checkbox" || type === "radio") score += 120;
3939
+ if (field.required === true) score += 60;
3940
+ if (hasValueInSubmissions(submissions, id)) {
3941
+ score += 80;
3942
+ } else {
3943
+ score -= 40;
3944
+ }
3945
+ if ([
3946
+ "richtext",
3947
+ "textarea",
3948
+ "longtext"
3949
+ ].includes(type)) {
3950
+ score -= 100;
3951
+ }
3952
+ return {
3953
+ id,
3954
+ label,
3955
+ type,
3956
+ score,
3957
+ index
3958
+ };
3959
+ }).filter((value) => value !== null);
3960
+ scored.sort((a, b) => {
3961
+ if (b.score !== a.score) return b.score - a.score;
3962
+ return a.index - b.index;
3963
+ });
3964
+ return scored.slice(0, MAX_COLUMNS).map((item) => ({
3965
+ id: item.id,
3966
+ label: item.label,
3967
+ type: item.type
3968
+ }));
3969
+ }
3970
+ function shouldHideInPreview(type) {
3971
+ return [
3972
+ "textarea",
3973
+ "richtext",
3974
+ "markdown",
3975
+ "longtext",
3976
+ "html"
3977
+ ].includes(type.toLowerCase());
3978
+ }
3979
+ function hasValueInSubmissions(submissions, key) {
3980
+ return submissions.some((row) => valueIsMeaningful(row.dataJson?.[key]));
3981
+ }
3982
+ function collectFallbackKeys(submissions) {
3983
+ const counts = new Map();
3984
+ submissions.forEach((row) => {
3985
+ const data = row.dataJson;
3986
+ if (data === null) return;
3987
+ Object.keys(data).forEach((key) => {
3988
+ if (HIDDEN_FIELD_IDS.has(key.toLowerCase())) return;
3989
+ if (!counts.has(key)) counts.set(key, 0);
3990
+ counts.set(key, (counts.get(key) ?? 0) + (valueIsMeaningful(data[key]) ? 1 : 0));
3991
+ });
3992
+ });
3993
+ return Array.from(counts.entries()).sort((a, b) => {
3994
+ if (b[1] !== a[1]) return b[1] - a[1];
3995
+ return a[0].localeCompare(b[0]);
3996
+ }).map(([key]) => key);
3997
+ }
3998
+ function humanizeSubmissionFieldKey(input) {
3999
+ return input.replace(/[_\-]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/^\w/, (char) => char.toUpperCase());
4000
+ }
4001
+ function valueIsMeaningful(value) {
4002
+ if (value === null || value === undefined) return false;
4003
+ if (typeof value === "string") return value.trim().length > 0;
4004
+ if (Array.isArray(value)) return value.length > 0;
4005
+ if (isRecord(value)) return Object.keys(value).length > 0;
4006
+ return true;
4007
+ }
4008
+ function orderSubmissionFields(schemaFields, data) {
4009
+ const result = [];
4010
+ const seen = new Set();
4011
+ if (schemaFields !== undefined) {
4012
+ schemaFields.forEach((field) => {
4013
+ const id = trimmedStringOrNull(field.id);
4014
+ if (id === null) return;
4015
+ if (HIDDEN_FIELD_IDS.has(id.toLowerCase())) return;
4016
+ seen.add(id);
4017
+ result.push({
4018
+ id,
4019
+ label: trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id),
4020
+ value: data?.[id]
4021
+ });
4022
+ });
4023
+ }
4024
+ if (data !== null) {
4025
+ Object.entries(data).forEach(([key, value]) => {
4026
+ if (seen.has(key)) return;
4027
+ if (HIDDEN_FIELD_IDS.has(key.toLowerCase())) return;
4028
+ result.push({
4029
+ id: key,
4030
+ label: humanizeSubmissionFieldKey(key),
4031
+ value
4032
+ });
4033
+ });
4034
+ }
4035
+ if (result.length === 0) {
4036
+ result.push({
4037
+ id: FORM_SUBMISSION_PSEUDO_FIELD_ID,
4038
+ label: "Submission",
4039
+ value: data
4040
+ });
4041
+ }
4042
+ return result;
4043
+ }
4044
+
4045
+ //#endregion
4046
+ //#region ../core/src/csv-cells.ts
4047
+ const csvEmptyCell = () => ({ kind: "empty" });
4048
+ const csvTextCell = (value) => value == null || value === "" ? csvEmptyCell() : {
4049
+ kind: "text",
4050
+ value
4051
+ };
4052
+ const csvIntegerCell = (value) => value == null || !Number.isFinite(value) ? csvEmptyCell() : {
4053
+ kind: "integer",
4054
+ value
4055
+ };
4056
+ const csvDecimalCell = (value, decimals) => value == null || !Number.isFinite(value) ? csvEmptyCell() : {
4057
+ kind: "decimal",
4058
+ value,
4059
+ decimals
4060
+ };
4061
+ const csvMoneyCentsCell = (cents, currency) => {
4062
+ const nonEmptyCurrency = asNonEmptyStringOrNull(currency);
4063
+ return cents == null || !Number.isFinite(cents) || nonEmptyCurrency === null ? csvEmptyCell() : {
4064
+ kind: "moneyCents",
4065
+ cents,
4066
+ currency: nonEmptyCurrency
4067
+ };
4068
+ };
4069
+ const csvIsoDateCell = (value) => {
4070
+ const nonEmptyValue = asNonEmptyStringOrNull(value);
4071
+ return nonEmptyValue !== null ? {
4072
+ kind: "isoDate",
4073
+ value: nonEmptyValue
4074
+ } : csvEmptyCell();
4075
+ };
4076
+ const csvIsoDateTimeCell = (value) => {
4077
+ const nonEmptyValue = asNonEmptyStringOrNull(value);
4078
+ return nonEmptyValue !== null ? {
4079
+ kind: "isoDateTime",
4080
+ value: nonEmptyValue
4081
+ } : csvEmptyCell();
4082
+ };
4083
+ const csvBooleanCell = (value) => value == null ? csvEmptyCell() : {
4084
+ kind: "boolean",
4085
+ value
4086
+ };
4087
+ const DEFAULT_CURRENCY_MINOR_UNITS = 2;
4088
+ const CURRENCY_MINOR_UNITS = {
4089
+ BHD: 3,
4090
+ CLP: 0,
4091
+ JPY: 0,
4092
+ KWD: 3,
4093
+ OMR: 3,
4094
+ TND: 3
4095
+ };
4096
+ function currencyMinorUnits(currency) {
4097
+ return CURRENCY_MINOR_UNITS[currency.trim().toUpperCase()] ?? DEFAULT_CURRENCY_MINOR_UNITS;
4098
+ }
4099
+ function formatCsvCell(cell) {
4100
+ switch (cell.kind) {
4101
+ case "empty": return "";
4102
+ case "text": return cell.value;
4103
+ case "integer": return String(cell.value);
4104
+ case "decimal": return cell.value.toFixed(cell.decimals);
4105
+ case "moneyCents": {
4106
+ const minorUnits = currencyMinorUnits(cell.currency);
4107
+ return (cell.cents / 10 ** minorUnits).toFixed(minorUnits);
4108
+ }
4109
+ case "isoDate":
4110
+ case "isoDateTime": return cell.value;
4111
+ case "boolean": return cell.value ? "true" : "false";
4112
+ }
4113
+ }
4114
+
4115
+ //#endregion
4116
+ //#region ../core/src/csv-date-filter.ts
4117
+ function resolveExportDateWindow(input) {
4118
+ const basis = input.requestedBasis ?? input.policy.defaultDateBasis;
4119
+ const supportedDateBases = input.policy.supportedDateBases;
4120
+ if (!supportedDateBases.includes(basis)) {
4121
+ return {
4122
+ ok: false,
4123
+ error: {
4124
+ tag: "unsupported_date_basis",
4125
+ basis,
4126
+ exportId: input.exportId
4127
+ }
4128
+ };
4129
+ }
4130
+ const range = resolveExportDateRange(input.range, input.today ?? new Date());
4131
+ if (!range.ok) return range;
4132
+ if (range.window.from > range.window.to) {
4133
+ return {
4134
+ ok: false,
4135
+ error: {
4136
+ tag: "invalid_date_range",
4137
+ from: range.window.from,
4138
+ to: range.window.to
4139
+ }
4140
+ };
4141
+ }
4142
+ return {
4143
+ ok: true,
4144
+ window: {
4145
+ basis,
4146
+ from: range.window.from,
4147
+ to: range.window.to,
4148
+ timeZone: input.timeZone
4149
+ }
4150
+ };
4151
+ }
4152
+ function resolveExportDateRange(range, today) {
4153
+ if (range.kind === "custom") {
4154
+ if (typeof range.from !== "string" || range.from.length === 0 || typeof range.to !== "string" || range.to.length === 0) {
4155
+ return {
4156
+ ok: false,
4157
+ error: { tag: "partial_custom_range" }
4158
+ };
4159
+ }
4160
+ const from$1 = parseExportDate(range.from, "from");
4161
+ if (!from$1.ok) return from$1;
4162
+ const to$1 = parseExportDate(range.to, "to");
4163
+ if (!to$1.ok) return to$1;
4164
+ return {
4165
+ ok: true,
4166
+ window: {
4167
+ from: from$1.value,
4168
+ to: to$1.value
4169
+ }
4170
+ };
4171
+ }
4172
+ if (range.preset === "all") {
4173
+ return {
4174
+ ok: true,
4175
+ window: {
4176
+ from: unsafeAsMoneyDateString("1970-01-01"),
4177
+ to: unsafeAsMoneyDateString(formatDate(today))
4178
+ }
4179
+ };
4180
+ }
4181
+ const days = presetDays(range.preset);
4182
+ const to = floorUtcDate(today);
4183
+ const from = new Date(to);
4184
+ from.setUTCDate(from.getUTCDate() - (days - 1));
4185
+ return {
4186
+ ok: true,
4187
+ window: {
4188
+ from: unsafeAsMoneyDateString(formatDate(from)),
4189
+ to: unsafeAsMoneyDateString(formatDate(to))
4190
+ }
4191
+ };
4192
+ }
4193
+ function parseExportDate(value, field) {
4194
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
4195
+ return {
4196
+ ok: false,
4197
+ error: {
4198
+ tag: "invalid_date",
4199
+ field,
4200
+ value
4201
+ }
4202
+ };
4203
+ }
4204
+ const date = new Date(`${value}T00:00:00.000Z`);
4205
+ if (Number.isNaN(date.getTime()) || formatDate(date) !== value) {
4206
+ return {
4207
+ ok: false,
4208
+ error: {
4209
+ tag: "invalid_date",
4210
+ field,
4211
+ value
4212
+ }
4213
+ };
4214
+ }
4215
+ return {
4216
+ ok: true,
4217
+ value: unsafeAsMoneyDateString(value)
4218
+ };
4219
+ }
4220
+ function presetDays(preset) {
4221
+ switch (preset) {
4222
+ case "7d": return 7;
4223
+ case "30d": return 30;
4224
+ case "90d": return 90;
4225
+ case "365d": return 365;
4226
+ }
4227
+ }
4228
+ function floorUtcDate(date) {
4229
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
4230
+ }
4231
+ function formatDate(date) {
4232
+ return date.toISOString().slice(0, 10);
4233
+ }
4234
+ function describeExportDateBasis(basis) {
4235
+ switch (basis) {
4236
+ case "created_at": return "Created date";
4237
+ case "scheduled_at": return "Scheduled date";
4238
+ case "cancelled_at": return "Cancellation date";
4239
+ case "paid_at": return "Payment date";
4240
+ case "processed_at": return "Processed date";
4241
+ }
4242
+ }
4243
+
4244
+ //#endregion
4245
+ //#region ../core/src/csv-escape.ts
4246
+ function csvEscape(value) {
4247
+ let s = String(value);
4248
+ if (/^\s*[=+\-@]/.test(s)) {
4249
+ s = `'${s}`;
4250
+ }
4251
+ if (s.includes(",") || s.includes("\n") || s.includes("\"")) {
4252
+ return `"${s.replace(/"/g, "\"\"")}"`;
4253
+ }
4254
+ return s;
4255
+ }
4256
+
4257
+ //#endregion
4258
+ //#region ../core/src/csv-filename.ts
4259
+ const MAX_CSV_FILENAME_LENGTH = 180;
4260
+ const CSV_EXTENSION = ".csv";
4261
+ function toSafeCsvFilename(value) {
4262
+ const normalized = value.trim();
4263
+ if (!normalized || /[\0\r\n/\\]/.test(normalized)) {
4264
+ return {
4265
+ ok: false,
4266
+ filename: value
4267
+ };
4268
+ }
4269
+ const withExtension = normalized.toLowerCase().endsWith(CSV_EXTENSION) ? normalized : `${normalized}${CSV_EXTENSION}`;
4270
+ const filename = withExtension.length <= MAX_CSV_FILENAME_LENGTH ? withExtension : `${withExtension.slice(0, MAX_CSV_FILENAME_LENGTH - CSV_EXTENSION.length)}${CSV_EXTENSION}`;
4271
+ return {
4272
+ ok: true,
4273
+ filename
4274
+ };
4275
+ }
4276
+ function assertSafeCsvFilename(value) {
4277
+ const result = toSafeCsvFilename(value);
4278
+ if (!result.ok) {
4279
+ throw new Error(`Invalid CSV filename: ${value}`);
4280
+ }
4281
+ return result.filename;
4282
+ }
4283
+ const unsafeCsvFilename = assertSafeCsvFilename;
4284
+ function csvFilenameDateRange(input) {
4285
+ const from = asNonEmptyStringOrNull(input.from);
4286
+ const to = asNonEmptyStringOrNull(input.to);
4287
+ const range = (() => {
4288
+ if (from !== null && to !== null) {
4289
+ return `${from.slice(0, 10)}-to-${to.slice(0, 10)}`;
4290
+ }
4291
+ if (from !== null) {
4292
+ return `from-${from.slice(0, 10)}`;
4293
+ }
4294
+ if (to !== null) {
4295
+ return `to-${to.slice(0, 10)}`;
4296
+ }
4297
+ return input.fallback ?? "all";
4298
+ })();
4299
+ const prefix = asNonEmptyStringOrNull(input.prefix);
4300
+ if (prefix !== null) {
4301
+ return `${prefix}-${range}`;
4302
+ }
4303
+ return range;
4304
+ }
4305
+ function contentDispositionForCsv(filename) {
4306
+ const asciiFallback = String(filename).replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_");
4307
+ return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(String(filename))}`;
4308
+ }
4309
+
4310
+ //#endregion
4311
+ //#region ../core/src/csv-render.ts
4312
+ function renderCsvExport(input) {
4313
+ const maxRows = Number(input.definition.maxRows);
4314
+ if (input.rows.length > maxRows) {
4315
+ return {
4316
+ ok: false,
4317
+ error: {
4318
+ tag: "too_many_rows",
4319
+ maxRows,
4320
+ actualRows: input.rows.length
4321
+ }
4322
+ };
4323
+ }
4324
+ const filename = input.definition.filename(input.options);
4325
+ const safeFilename = toSafeCsvFilename(String(filename));
4326
+ if (!safeFilename.ok) {
4327
+ return {
4328
+ ok: false,
4329
+ error: {
4330
+ tag: "invalid_filename",
4331
+ filename: safeFilename.filename
4332
+ }
4333
+ };
4334
+ }
4335
+ const columns = input.definition.columns(input.options);
4336
+ const duplicate = findDuplicateColumnKey(columns);
4337
+ if (duplicate !== null) {
4338
+ return {
4339
+ ok: false,
4340
+ error: {
4341
+ tag: "duplicate_column_key",
4342
+ key: duplicate
4343
+ }
4344
+ };
4345
+ }
4346
+ const header = columns.map((column) => csvEscape(column.header)).join(",");
4347
+ const rows = input.rows.map((row) => columns.map((column) => csvEscape(formatCsvCell(column.cell(row)))).join(","));
4348
+ return {
4349
+ ok: true,
4350
+ csv: [header, ...rows].join("\n"),
4351
+ filename: safeFilename.filename,
4352
+ rowCount: input.rows.length,
4353
+ sensitivities: collectCsvColumnSensitivities(columns)
4354
+ };
4355
+ }
4356
+ function findDuplicateColumnKey(columns) {
4357
+ const seen = new Set();
4358
+ for (const column of columns) {
4359
+ const key = String(column.key);
4360
+ if (seen.has(key)) return key;
4361
+ seen.add(key);
4362
+ }
4363
+ return null;
4364
+ }
4365
+ function collectCsvColumnSensitivities(columns) {
4366
+ return new Set(columns.map((column) => column.sensitivity));
4367
+ }
4368
+ function describeRenderCsvError(error) {
4369
+ switch (error.tag) {
4370
+ case "too_many_rows": return `Export has ${error.actualRows} rows; narrow the filters to ${error.maxRows} rows or fewer.`;
4371
+ case "duplicate_column_key": return `Duplicate CSV column key: ${error.key}`;
4372
+ case "invalid_filename": return `Invalid CSV filename: ${error.filename}`;
4373
+ }
4374
+ }
4375
+
4376
+ //#endregion
4377
+ //#region ../core/src/csv-types.ts
4378
+ function unsafeAsCsvExportId(value) {
4379
+ return value;
4380
+ }
4381
+ function unsafeAsCsvColumnKey(value) {
4382
+ return value;
4383
+ }
4384
+ function asExportRowLimit(value) {
4385
+ if (!Number.isInteger(value) || value < 1) {
4386
+ throw new Error(`Invalid export row limit: ${value}`);
4387
+ }
4388
+ return value;
4389
+ }
4390
+
4391
+ //#endregion
4392
+ //#region ../core/src/email-format.ts
4393
+ function maskEmail(email) {
4394
+ if (email === null || email.length === 0) return null;
4395
+ const [localPart, domain] = email.split("@");
4396
+ if (domain === undefined || domain.length === 0 || localPart === undefined || localPart.length === 0) return email;
4397
+ const visibleLocal = localPart.length <= 2 ? `${localPart[0] ?? ""}***` : `${localPart.slice(0, 1)}${"*".repeat(Math.max(localPart.length - 2, 1))}${localPart.slice(-1)}`;
4398
+ return `${visibleLocal}@${domain}`;
4399
+ }
4400
+
4401
+ //#endregion
4402
+ //#region ../core/src/slug.ts
4403
+ /**
4404
+ * Convert a string to a URL-safe slug
4405
+ *
4406
+ * @param text - The text to slugify
4407
+ * @returns URL-safe slug (lowercase, alphanumeric + hyphens)
4408
+ *
4409
+ * @example
4410
+ * slugify("Initial Consultation") // "initial-consultation"
4411
+ * slugify("90-Minute Deep Tissue") // "90-minute-deep-tissue"
4412
+ * slugify("Reiki & Energy Healing") // "reiki-energy-healing"
4413
+ */
4414
+ function slugify(text) {
4415
+ return text.toString().toLowerCase().trim().replace(/\s+/g, "-").replace(/[^\w\-]+/g, "").replace(/\-\-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
4416
+ }
4417
+ /**
4418
+ * Generate a unique slug by appending a number if needed
4419
+ *
4420
+ * @param baseSlug - The base slug to make unique
4421
+ * @param existingSlugs - Array of slugs already in use
4422
+ * @returns Unique slug (adds -2, -3, etc. if needed)
4423
+ *
4424
+ * @example
4425
+ * makeUniqueSlug("consultation", ["consultation"])
4426
+ * // Returns: "consultation-2"
4427
+ *
4428
+ * makeUniqueSlug("consultation", ["consultation", "consultation-2"])
4429
+ * // Returns: "consultation-3"
4430
+ */
4431
+ function makeUniqueSlug(baseSlug, existingSlugs) {
4432
+ let slug = baseSlug;
4433
+ let counter = 2;
4434
+ while (existingSlugs.includes(slug)) {
4435
+ slug = `${baseSlug}-${counter}`;
4436
+ counter++;
4437
+ }
4438
+ return slug;
4439
+ }
4440
+ /**
4441
+ * Generate a slug from text and ensure it's unique
4442
+ *
4443
+ * @param text - The text to slugify
4444
+ * @param existingSlugs - Array of slugs already in use
4445
+ * @returns Unique URL-safe slug
4446
+ *
4447
+ * @example
4448
+ * generateUniqueSlug("Initial Consultation", ["initial-consultation"])
4449
+ * // Returns: "initial-consultation-2"
4450
+ */
4451
+ function generateUniqueSlug(text, existingSlugs) {
4452
+ const baseSlug = slugify(text);
4453
+ return makeUniqueSlug(baseSlug, existingSlugs);
4454
+ }
4455
+
4456
+ //#endregion
4457
+ //#region ../core/src/uuid.ts
4458
+ /**
4459
+ * Validates whether a string matches UUID v4 format
4460
+ * Format: 8-4-4-4-12 hexadecimal digits
4461
+ * Example: 550e8400-e29b-41d4-a716-446655440000
4462
+ */
4463
+ function isValidUuid(str) {
4464
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
4465
+ }
4466
+
4467
+ //#endregion
4468
+ //#region ../core/src/stable-json.ts
4469
+ function stableJsonStringify(value) {
4470
+ return JSON.stringify(sortStableJsonValue(value));
4471
+ }
4472
+ function sortStableJsonValue(value) {
4473
+ if (value === null) return null;
4474
+ if (typeof value === "string" || typeof value === "boolean") return value;
4475
+ if (typeof value === "number") {
4476
+ if (!Number.isFinite(value)) {
4477
+ throw new TypeError("Stable JSON only supports finite numbers");
4478
+ }
4479
+ return value;
4480
+ }
4481
+ if (Array.isArray(value)) {
4482
+ return value.map(sortStableJsonValue);
4483
+ }
4484
+ if (typeof value === "object") {
4485
+ const prototype = Object.getPrototypeOf(value);
4486
+ if (prototype !== Object.prototype && prototype !== null) {
4487
+ throw new TypeError("Stable JSON only supports plain objects");
4488
+ }
4489
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortStableJsonValue(child)]));
4490
+ }
4491
+ throw new TypeError(`Stable JSON does not support ${typeof value}`);
4492
+ }
4493
+
4494
+ //#endregion
4495
+ //#region ../core/src/secret.ts
4496
+ /**
4497
+ * Credential values that must remain inert when they cross diagnostics,
4498
+ * logging, or serialization boundaries.
4499
+ *
4500
+ * The value is deliberately held behind a non-enumerable capability rather
4501
+ * than exposed as a property. The capability uses a global symbol so a Secret
4502
+ * created by another bundled copy of core remains consumable at an approved
4503
+ * `exposeSecret` boundary.
4504
+ */
4505
+ const SECRET_KINDS = [
4506
+ "management-key",
4507
+ "content-key",
4508
+ "preview-key",
4509
+ "webhook-secret",
4510
+ "oauth-access-token",
4511
+ "oauth-refresh-token",
4512
+ "preview-session-refresh-token",
4513
+ "oauth-authorization-code",
4514
+ "oauth-pkce-verifier",
4515
+ "registrar-epp-code"
4516
+ ];
4517
+ const secretValueSymbol = Symbol.for("riverbank.secret.value");
4518
+ const SECRET_INSPECT_SYMBOL = Symbol.for("nodejs.util.inspect.custom");
4519
+ const MANAGEMENT_KEY_PREFIX = "bld_mgmt_sk_";
4520
+ function placeholderFor(kind) {
4521
+ return `[riverbank:redacted:${kind}]`;
4522
+ }
4523
+ /** Create an opaque secret after a caller has parsed/validated its boundary. */
4524
+ function createSecret(kind, value) {
4525
+ return createSecretInternal(kind, value);
4526
+ }
4527
+ /** Construct a Secret for an already validated kind/value pair. */
4528
+ function createSecretInternal(kind, value) {
4529
+ const placeholder = placeholderFor(kind);
4530
+ const secret = {
4531
+ kind: "secret",
4532
+ secretType: kind,
4533
+ toString: () => placeholder,
4534
+ toJSON: () => placeholder,
4535
+ [SECRET_INSPECT_SYMBOL]: () => placeholder
4536
+ };
4537
+ Object.defineProperty(secret, secretValueSymbol, {
4538
+ configurable: false,
4539
+ enumerable: false,
4540
+ value: () => value,
4541
+ writable: false
4542
+ });
4543
+ return Object.freeze(secret);
4544
+ }
4545
+ /**
4546
+ * Parse an untrusted management API key before constructing its Secret value.
4547
+ *
4548
+ * The Secret type protects an already validated value from accidental
4549
+ * disclosure; this boundary is what validates the value's wire format.
4550
+ */
4551
+ function parseManagementKey(raw) {
4552
+ if (typeof raw !== "string" || !raw.startsWith(MANAGEMENT_KEY_PREFIX) || raw.length === MANAGEMENT_KEY_PREFIX.length) {
4553
+ return err({ kind: "invalid-management-key" });
4554
+ }
4555
+ return ok(createSecretInternal("management-key", raw));
4556
+ }
4557
+ /**
4558
+ * The only generic plaintext exposure operation. Keep calls at named I/O
4559
+ * boundaries; do not pass exposed strings through domain/application layers.
4560
+ */
4561
+ function exposeSecret(secret) {
4562
+ const getValue = secret[secretValueSymbol];
4563
+ if (typeof getValue !== "function") return failInvalidSecret();
4564
+ return getValue();
4565
+ }
4566
+ function failInvalidSecret() {
4567
+ throw new TypeError("Invalid Riverbank secret");
4568
+ }
4569
+
4570
+ //#endregion
4571
+ //#region ../core/src/site-route-target.ts
4572
+ const SITE_ROUTE_NODE_TYPES = [
4573
+ "page",
4574
+ "content_entry",
4575
+ "page_surface"
4576
+ ];
4577
+ function parseSiteRouteTarget(input) {
4578
+ if (typeof input.nodeType !== "string") {
4579
+ return err({
4580
+ kind: "invalid-node-type",
4581
+ nodeType: input.nodeType
4582
+ });
4583
+ }
4584
+ switch (input.nodeType) {
4585
+ case "page": {
4586
+ const pageId = parsePageId(input.nodeId);
4587
+ return pageId.ok ? ok({
4588
+ kind: "page",
4589
+ pageId: pageId.value
4590
+ }) : err({
4591
+ kind: "invalid-page-id",
4592
+ error: pageId.error
4593
+ });
4594
+ }
4595
+ case "content_entry": {
4596
+ const entryId = parseContentEntryId(input.nodeId);
4597
+ return entryId.ok ? ok({
4598
+ kind: "contentEntry",
4599
+ entryId: entryId.value
4600
+ }) : err({
4601
+ kind: "invalid-content-entry-id",
4602
+ error: entryId.error
4603
+ });
4604
+ }
4605
+ case "page_surface": {
4606
+ const surfaceId = parsePageSurfaceId(input.nodeId);
4607
+ return surfaceId.ok ? ok({
4608
+ kind: "pageSurface",
4609
+ surfaceId: surfaceId.value
4610
+ }) : err({
4611
+ kind: "invalid-page-surface-id",
4612
+ error: surfaceId.error
4613
+ });
4614
+ }
4615
+ default: return err({
4616
+ kind: "unsupported-node-type",
4617
+ nodeType: input.nodeType
4618
+ });
4619
+ }
4620
+ }
4621
+ function siteRouteTargetNodeType(target) {
4622
+ switch (target.kind) {
4623
+ case "page": return "page";
4624
+ case "contentEntry": return "content_entry";
4625
+ case "pageSurface": return "page_surface";
4626
+ default: return assertNever(target);
4627
+ }
4628
+ }
4629
+ function foldSiteRouteTarget(target, cases) {
4630
+ switch (target.kind) {
4631
+ case "page": return cases.page(target.pageId);
4632
+ case "contentEntry": return cases.contentEntry(target.entryId);
4633
+ case "pageSurface": return cases.pageSurface(target.surfaceId);
4634
+ default: return assertNever(target);
4635
+ }
4636
+ }
4637
+
4638
+ //#endregion
4639
+ //#region ../core/src/planner.ts
4640
+ function plannerNoChange(details = {}) {
4641
+ return {
4642
+ ...details,
4643
+ tag: "no_change"
4644
+ };
4645
+ }
4646
+ function plannerUpdate(details = {}) {
4647
+ return {
4648
+ ...details,
4649
+ tag: "update"
4650
+ };
4651
+ }
4652
+ /** Identity at runtime; marks a planner-derived patch as shallow readonly. */
4653
+ function plannerPatch(patch) {
4654
+ return patch;
4655
+ }
4656
+ /** Default clock for production callers that do not inject time explicitly. */
4657
+ const systemPlanningClock = { now: () => Date.now() };
4658
+ /** Resolve current time from an explicit timestamp, injected clock, or system clock. */
4659
+ function resolvePlanningNow(input = {}) {
4660
+ if (typeof input.now === "number") {
4661
+ return input.now;
4662
+ }
4663
+ return (input.clock ?? systemPlanningClock).now();
4664
+ }
4665
+ /** Resolve current time as a Date from an explicit Date, injected clock, or system clock. */
4666
+ function resolvePlanningNowDate(input = {}) {
4667
+ if (input.now !== undefined) {
4668
+ return input.now;
4669
+ }
4670
+ return new Date((input.clock ?? systemPlanningClock).now());
4671
+ }
4672
+ /**
4673
+ * Resolve a Date-compatible planner input.
4674
+ *
4675
+ * Precedence: bare Date, `{ now }`, `{ clock }`, then `systemPlanningClock`.
4676
+ * Explicit Date inputs are returned by reference, not cloned.
4677
+ */
4678
+ function resolvePlanningDateInput(input = {}) {
4679
+ if (input instanceof Date) {
4680
+ return input;
4681
+ }
4682
+ return resolvePlanningNowDate(input);
4683
+ }
4684
+ /** Build an enabled selection while preserving literal value types. */
4685
+ function enabledSelection(value) {
4686
+ return {
4687
+ enabled: true,
4688
+ value
4689
+ };
4690
+ }
4691
+ /** Build a disabled selection. */
4692
+ function disabledSelection() {
4693
+ return { enabled: false };
4694
+ }
4695
+
4696
+ //#endregion
4697
+ export { unsafeAsDetectedMimeType as $, parseUtcIsoDateTimeString as A, formatCurrencyAmount as B, formatBookingDateParts as C, getBookingLocalDateKey as D, formatLocalScheduleTime as E, parseEventPageSurfaceSubject as F, parsePageId as G, parseApiKeyId as H, formatBookingPriceDisplay as I, unsafeAsAppointmentResourceId as J, parseSiteId as K, formatEventPricing as L, toScheduledBookingDateRange as M, toScheduledBookingDateTime as N, getLocalScheduleDate as O, parseBookingTimeZone as P, unsafeAsDemoCapability as Q, publicEventPricingCurrency as R, formatBookingDate as S, formatLocalScheduleDate as T, parseEventSeriesId as U, assertNever as V, parseMediaAssetId as W, unsafeAsBookingSessionId as X, unsafeAsBookingRequirementId as Y, unsafeAsDeclaredMimeType as Z, mapNonEmptyArray as _, unsafeAsUploadSessionId as _t, stableJsonStringify as a, unsafeAsOccurrenceId as at, normalizeLocationAddressText as b, ok as bt, parseFooterModuleId as c, unsafeAsPositiveSeconds as ct, parseNewsletterListId as d, unsafeAsSiteFontId as dt, unsafeAsEntitlementId as et, parseNewsletterSignupAuthoringDestinationValue as f, unsafeAsSiteId as ft, fromArray as g, unsafeAsStoragePath as gt, assertAllowedPublicUrl as h, unsafeAsStorageBucketName as ht, parseManagementKey as i, unsafeAsMediaAssetId as it, toBookingTimeZone as j, getLocalScheduleTime as k, parseMailchimpAudienceId as l, unsafeAsProductId as lt, isCanonicalRichTextDoc as m, unsafeAsStaffMemberId as mt, createSecret as n, unsafeAsEventSeriesId as nt, footerNavigationLayouts as o, unsafeAsOriginalFilename as ot, parseNewsletterSignupDestinationProvider as p, unsafeAsSiteScopedStoragePath as pt, parseUserId as q, exposeSecret as r, unsafeAsFileChecksumSha256 as rt, parseFooterCompositionV1 as s, unsafeAsPositiveByteCount as st, resolvePlanningDateInput as t, unsafeAsEventOfferingId as tt, parseMailchimpTagName as u, unsafeAsSafeDownloadFilename as ut, hasText as v, unsafeAsUserId as vt, formatBookingTime as w, toLocationCoordinates as x, isPresent as y, err as yt, publicEventPricingType as z };