@medalsocial/booking 0.0.0-placeholder.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +220 -1
- package/dist/core/age.d.mts +34 -0
- package/dist/core/age.mjs +95 -0
- package/dist/core/age.mjs.map +1 -0
- package/dist/core/attempt-store.d.mts +111 -0
- package/dist/core/attempt-store.mjs +159 -0
- package/dist/core/attempt-store.mjs.map +1 -0
- package/dist/core/categories.d.mts +27 -0
- package/dist/core/categories.mjs +42 -0
- package/dist/core/categories.mjs.map +1 -0
- package/dist/core/clock.d.mts +33 -0
- package/dist/core/clock.mjs +251 -0
- package/dist/core/clock.mjs.map +1 -0
- package/dist/core/config.d.mts +171 -0
- package/dist/core/config.mjs +392 -0
- package/dist/core/config.mjs.map +1 -0
- package/dist/core/consent.d.mts +14 -0
- package/dist/core/consent.mjs +17 -0
- package/dist/core/consent.mjs.map +1 -0
- package/dist/core/deep-link.d.mts +40 -0
- package/dist/core/deep-link.mjs +160 -0
- package/dist/core/deep-link.mjs.map +1 -0
- package/dist/core/display-name.d.mts +18 -0
- package/dist/core/display-name.mjs +30 -0
- package/dist/core/display-name.mjs.map +1 -0
- package/dist/core/draft-store.d.mts +83 -0
- package/dist/core/draft-store.mjs +115 -0
- package/dist/core/draft-store.mjs.map +1 -0
- package/dist/core/dto.d.mts +46 -0
- package/dist/core/dto.mjs +157 -0
- package/dist/core/dto.mjs.map +1 -0
- package/dist/core/ics.d.mts +63 -0
- package/dist/core/ics.mjs +130 -0
- package/dist/core/ics.mjs.map +1 -0
- package/dist/core/index.d.mts +34 -0
- package/dist/core/index.mjs +31 -0
- package/dist/core/labels.d.mts +88 -0
- package/dist/core/labels.mjs +93 -0
- package/dist/core/labels.mjs.map +1 -0
- package/dist/core/machine.d.mts +598 -0
- package/dist/core/machine.mjs +878 -0
- package/dist/core/machine.mjs.map +1 -0
- package/dist/core/money.d.mts +12 -0
- package/dist/core/money.mjs +51 -0
- package/dist/core/money.mjs.map +1 -0
- package/dist/core/next-available.d.mts +20 -0
- package/dist/core/next-available.mjs +36 -0
- package/dist/core/next-available.mjs.map +1 -0
- package/dist/core/next-free.d.mts +25 -0
- package/dist/core/next-free.mjs +44 -0
- package/dist/core/next-free.mjs.map +1 -0
- package/dist/core/party-slots.d.mts +72 -0
- package/dist/core/party-slots.mjs +223 -0
- package/dist/core/party-slots.mjs.map +1 -0
- package/dist/core/paths.d.mts +11 -0
- package/dist/core/paths.mjs +56 -0
- package/dist/core/paths.mjs.map +1 -0
- package/dist/core/phone.d.mts +17 -0
- package/dist/core/phone.mjs +58 -0
- package/dist/core/phone.mjs.map +1 -0
- package/dist/core/portal/cookies.d.mts +29 -0
- package/dist/core/portal/dto.d.mts +140 -0
- package/dist/core/portal/dto.mjs +142 -0
- package/dist/core/portal/dto.mjs.map +1 -0
- package/dist/core/portal/family.d.mts +39 -0
- package/dist/core/portal/family.mjs +39 -0
- package/dist/core/portal/family.mjs.map +1 -0
- package/dist/core/portal/login-input.d.mts +29 -0
- package/dist/core/portal/login-input.mjs +29 -0
- package/dist/core/portal/login-input.mjs.map +1 -0
- package/dist/core/portal/next-path.d.mts +12 -0
- package/dist/core/portal/next-path.mjs +98 -0
- package/dist/core/portal/next-path.mjs.map +1 -0
- package/dist/core/portal/return-path.d.mts +7 -0
- package/dist/core/portal/return-path.mjs +49 -0
- package/dist/core/portal/return-path.mjs.map +1 -0
- package/dist/core/portal/session-cookie.d.mts +27 -0
- package/dist/core/portal/session-cookie.mjs +30 -0
- package/dist/core/portal/session-cookie.mjs.map +1 -0
- package/dist/core/portal/vipps-link.d.mts +56 -0
- package/dist/core/portal/vipps-link.mjs +136 -0
- package/dist/core/portal/vipps-link.mjs.map +1 -0
- package/dist/core/portal/vipps-return.d.mts +39 -0
- package/dist/core/portal/vipps-return.mjs +69 -0
- package/dist/core/portal/vipps-return.mjs.map +1 -0
- package/dist/core/rebook-store.d.mts +28 -0
- package/dist/core/rebook-store.mjs +60 -0
- package/dist/core/rebook-store.mjs.map +1 -0
- package/dist/core/restore-gate.d.mts +40 -0
- package/dist/core/restore-gate.mjs +96 -0
- package/dist/core/restore-gate.mjs.map +1 -0
- package/dist/core/stores.d.mts +8 -0
- package/dist/core/stores.mjs +23 -0
- package/dist/core/stores.mjs.map +1 -0
- package/dist/core/types.d.mts +241 -0
- package/dist/core/wire.d.mts +37 -0
- package/dist/next/cache/memory.d.mts +14 -0
- package/dist/next/cache/memory.mjs +51 -0
- package/dist/next/cache/memory.mjs.map +1 -0
- package/dist/next/cache/next-data.d.mts +6 -0
- package/dist/next/cache/next-data.mjs +35 -0
- package/dist/next/cache/next-data.mjs.map +1 -0
- package/dist/next/cache/noop.d.mts +6 -0
- package/dist/next/cache/noop.mjs +15 -0
- package/dist/next/cache/noop.mjs.map +1 -0
- package/dist/next/cache/workers.d.mts +29 -0
- package/dist/next/cache/workers.mjs +74 -0
- package/dist/next/cache/workers.mjs.map +1 -0
- package/dist/next/catalogue.d.mts +38 -0
- package/dist/next/catalogue.mjs +197 -0
- package/dist/next/catalogue.mjs.map +1 -0
- package/dist/next/handler.d.mts +11 -0
- package/dist/next/handler.mjs +150 -0
- package/dist/next/handler.mjs.map +1 -0
- package/dist/next/index.d.mts +17 -0
- package/dist/next/index.mjs +14 -0
- package/dist/next/loaders.d.mts +79 -0
- package/dist/next/loaders.mjs +195 -0
- package/dist/next/loaders.mjs.map +1 -0
- package/dist/next/manage-dto.d.mts +6 -0
- package/dist/next/manage-dto.mjs +28 -0
- package/dist/next/manage-dto.mjs.map +1 -0
- package/dist/next/medal.d.mts +98 -0
- package/dist/next/medal.mjs +154 -0
- package/dist/next/medal.mjs.map +1 -0
- package/dist/next/options.d.mts +158 -0
- package/dist/next/options.mjs +65 -0
- package/dist/next/options.mjs.map +1 -0
- package/dist/next/portal/guardian.d.mts +42 -0
- package/dist/next/portal/guardian.mjs +76 -0
- package/dist/next/portal/guardian.mjs.map +1 -0
- package/dist/next/portal/medal-portal.d.mts +161 -0
- package/dist/next/portal/medal-portal.mjs +825 -0
- package/dist/next/portal/medal-portal.mjs.map +1 -0
- package/dist/next/portal/session.d.mts +38 -0
- package/dist/next/portal/session.mjs +40 -0
- package/dist/next/portal/session.mjs.map +1 -0
- package/dist/next/portal/vipps-flash.d.mts +14 -0
- package/dist/next/portal/vipps-flash.mjs +41 -0
- package/dist/next/portal/vipps-flash.mjs.map +1 -0
- package/dist/next/portal.d.mts +205 -0
- package/dist/next/portal.mjs +469 -0
- package/dist/next/portal.mjs.map +1 -0
- package/dist/next/request.d.mts +51 -0
- package/dist/next/request.mjs +123 -0
- package/dist/next/request.mjs.map +1 -0
- package/dist/next/routes/catalogue-routes.mjs +330 -0
- package/dist/next/routes/catalogue-routes.mjs.map +1 -0
- package/dist/next/routes/create.mjs +422 -0
- package/dist/next/routes/create.mjs.map +1 -0
- package/dist/next/routes/manage.mjs +191 -0
- package/dist/next/routes/manage.mjs.map +1 -0
- package/dist/next/routes/portal-routes.mjs +312 -0
- package/dist/next/routes/portal-routes.mjs.map +1 -0
- package/dist/next/routes/shared.mjs +134 -0
- package/dist/next/routes/shared.mjs.map +1 -0
- package/dist/next/routes/vipps-return.mjs +166 -0
- package/dist/next/routes/vipps-return.mjs.map +1 -0
- package/dist/next/runtime.d.mts +49 -0
- package/dist/next/runtime.mjs +94 -0
- package/dist/next/runtime.mjs.map +1 -0
- package/dist/next/seed.d.mts +71 -0
- package/dist/next/seed.mjs +495 -0
- package/dist/next/seed.mjs.map +1 -0
- package/dist/next/server.d.mts +26 -0
- package/dist/next/server.mjs +63 -0
- package/dist/next/server.mjs.map +1 -0
- package/dist/react/BookingLink.d.mts +61 -0
- package/dist/react/BookingLink.mjs +102 -0
- package/dist/react/BookingLink.mjs.map +1 -0
- package/dist/react/BookingWizard.d.mts +49 -0
- package/dist/react/BookingWizard.mjs +577 -0
- package/dist/react/BookingWizard.mjs.map +1 -0
- package/dist/react/LoginPageQuery.d.mts +37 -0
- package/dist/react/LoginPageQuery.mjs +106 -0
- package/dist/react/LoginPageQuery.mjs.map +1 -0
- package/dist/react/LoginSheet.d.mts +49 -0
- package/dist/react/LoginSheet.mjs +98 -0
- package/dist/react/LoginSheet.mjs.map +1 -0
- package/dist/react/ManageBooking.d.mts +50 -0
- package/dist/react/ManageBooking.mjs +197 -0
- package/dist/react/ManageBooking.mjs.map +1 -0
- package/dist/react/PortalDashboard.d.mts +10 -0
- package/dist/react/Provider.d.mts +82 -0
- package/dist/react/Provider.mjs +132 -0
- package/dist/react/Provider.mjs.map +1 -0
- package/dist/react/actions.d.mts +133 -0
- package/dist/react/actions.mjs +90 -0
- package/dist/react/actions.mjs.map +1 -0
- package/dist/react/index.d.mts +28 -0
- package/dist/react/index.mjs +20 -0
- package/dist/react/kit.d.mts +54 -0
- package/dist/react/kit.mjs +127 -0
- package/dist/react/kit.mjs.map +1 -0
- package/dist/react/labels/index.d.mts +23 -0
- package/dist/react/labels/index.mjs +44 -0
- package/dist/react/labels/index.mjs.map +1 -0
- package/dist/react/labels/login.d.mts +10 -0
- package/dist/react/labels/login.mjs +13 -0
- package/dist/react/labels/login.mjs.map +1 -0
- package/dist/react/labels/manage.d.mts +15 -0
- package/dist/react/labels/manage.mjs +19 -0
- package/dist/react/labels/manage.mjs.map +1 -0
- package/dist/react/labels/portal.d.mts +25 -0
- package/dist/react/labels/portal.mjs +41 -0
- package/dist/react/labels/portal.mjs.map +1 -0
- package/dist/react/labels/screens.d.mts +11 -0
- package/dist/react/labels/screens.mjs +709 -0
- package/dist/react/labels/screens.mjs.map +1 -0
- package/dist/react/labels/wizard.d.mts +69 -0
- package/dist/react/labels/wizard.mjs +95 -0
- package/dist/react/labels/wizard.mjs.map +1 -0
- package/dist/react/link/index.d.mts +5 -0
- package/dist/react/link/index.mjs +5 -0
- package/dist/react/portal/SessionTouch.d.mts +19 -0
- package/dist/react/portal/SessionTouch.mjs +40 -0
- package/dist/react/portal/SessionTouch.mjs.map +1 -0
- package/dist/react/portal/age-prompt.d.mts +10 -0
- package/dist/react/portal/age-prompt.mjs +21 -0
- package/dist/react/portal/age-prompt.mjs.map +1 -0
- package/dist/react/portal/book-again.d.mts +18 -0
- package/dist/react/portal/book-again.mjs +21 -0
- package/dist/react/portal/book-again.mjs.map +1 -0
- package/dist/react/portal/child-summary.d.mts +17 -0
- package/dist/react/portal/child-summary.mjs +45 -0
- package/dist/react/portal/child-summary.mjs.map +1 -0
- package/dist/react/portal/dashboard.d.mts +74 -0
- package/dist/react/portal/dashboard.mjs +204 -0
- package/dist/react/portal/dashboard.mjs.map +1 -0
- package/dist/react/portal/leave.d.mts +17 -0
- package/dist/react/portal/leave.mjs +21 -0
- package/dist/react/portal/leave.mjs.map +1 -0
- package/dist/react/portal/sections.d.mts +12 -0
- package/dist/react/portal/sections.mjs +48 -0
- package/dist/react/portal/sections.mjs.map +1 -0
- package/dist/react/portal/tabs.d.mts +23 -0
- package/dist/react/portal/tabs.mjs +36 -0
- package/dist/react/portal/tabs.mjs.map +1 -0
- package/dist/react/portal/vipps-flash.d.mts +11 -0
- package/dist/react/portal/vipps-flash.mjs +18 -0
- package/dist/react/portal/vipps-flash.mjs.map +1 -0
- package/dist/react/portal/wired.mjs +202 -0
- package/dist/react/portal/wired.mjs.map +1 -0
- package/dist/react/screen-labels.mjs +14 -0
- package/dist/react/screen-labels.mjs.map +1 -0
- package/dist/react/shared.d.mts +13 -0
- package/dist/react/shared.mjs +7 -0
- package/dist/react/use-next-free.d.mts +19 -0
- package/dist/react/use-next-free.mjs +56 -0
- package/dist/react/use-next-free.mjs.map +1 -0
- package/dist/react/useBooking.d.mts +177 -0
- package/dist/react/useBooking.mjs +1205 -0
- package/dist/react/useBooking.mjs.map +1 -0
- package/dist/react/wizard/adapters.mjs +187 -0
- package/dist/react/wizard/adapters.mjs.map +1 -0
- package/dist/react/wizard/index.d.mts +11 -0
- package/dist/react/wizard/index.mjs +6 -0
- package/package.json +126 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.mjs","names":[],"sources":["../../src/core/clock.ts"],"sourcesContent":["/**\n * Every clock face the booking flow shows belongs to the business, never to\n * the viewer.\n *\n * A family booking from a holiday abroad has to read 15:00 on the chip they\n * will actually turn up for, and a slot at 00:30 on Saturday has to carry\n * Saturday's surcharge even though UTC still calls it Friday. `Date`'s\n * local-time getters answer for whoever is looking, so nothing in the wizard\n * uses them: hours, days and weekday names all come from here.\n *\n * Pure, and free of React and fetch for the same reason the wizard machine is\n * — the business's clock is a rule, and a rule that needs a DOM to test is a\n * rule nobody tests.\n *\n * `createClock(config)` returns the function set for one zone, one locale and\n * one set of day parts. The `Intl` formatters behind it are built once per\n * zone and locale and shared by every clock that asks for the same pair.\n */\n\nimport type { BookingConfig, BookingDaypart } from './config';\nimport { type BookingLabels, fill, labelText, resolveLabels } from './labels';\n\ninterface Formatters {\n /** `h23` rather than `hour12: false`, which renders midnight as `24:00` on some ICU builds. */\n time: Intl.DateTimeFormat;\n /**\n * `en-US` for the parts nobody reads: `weekday: 'short'` has to be matched\n * against a literal below, and `Sat`/`Sun` are stable where a localised\n * `lør.`/`søn.` would drift with the ICU data the runtime happens to ship.\n */\n parts: Intl.DateTimeFormat;\n weekdayLong: Intl.DateTimeFormat;\n weekdayShort: Intl.DateTimeFormat;\n dayMonth: Intl.DateTimeFormat;\n /**\n * `tor. 8. okt.` — a date that does not need to know what today is.\n *\n * The wizard can say «i morgen» because the visitor is standing in it. The\n * manage page cannot: it is opened out of an e-mail, possibly weeks later and\n * possibly by the other parent, and a relative day would be a claim about the\n * moment the page happened to render.\n */\n fullDate: Intl.DateTimeFormat;\n /** The wall clock down to the second — the one thing `parts` cannot give, and only `offsetMs` needs. */\n wallClock: Intl.DateTimeFormat;\n}\n\n/**\n * Built once each: constructing a formatter is the expensive half of `Intl`,\n * and the time step formats every slot in a week on each re-render.\n */\nconst FORMATTERS = new Map<string, Formatters>();\n\nfunction formattersFor(timeZone: string, locale: string): Formatters {\n const cacheKey = `${timeZone}\\u0000${locale}`;\n const cached = FORMATTERS.get(cacheKey);\n if (cached !== undefined) return cached;\n const built: Formatters = {\n time: new Intl.DateTimeFormat(locale, {\n timeZone,\n hour: '2-digit',\n minute: '2-digit',\n hourCycle: 'h23',\n }),\n parts: new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n hourCycle: 'h23',\n weekday: 'short',\n }),\n weekdayLong: new Intl.DateTimeFormat(locale, { timeZone, weekday: 'long' }),\n weekdayShort: new Intl.DateTimeFormat(locale, { timeZone, weekday: 'short', day: 'numeric' }),\n dayMonth: new Intl.DateTimeFormat(locale, { timeZone, day: 'numeric', month: 'short' }),\n fullDate: new Intl.DateTimeFormat(locale, {\n timeZone,\n weekday: 'short',\n day: 'numeric',\n month: 'short',\n }),\n wallClock: new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hourCycle: 'h23',\n }),\n };\n FORMATTERS.set(cacheKey, built);\n return built;\n}\n\nexport interface Clock {\n readonly timeZone: string;\n /** The day part keys, in order. */\n readonly dayparts: readonly string[];\n dayStart(ts: number, offsetDays?: number): number;\n window(fromTs: number, rangeDays: number): { fromTs: number; toTs: number; days: number[] };\n formatTime(ts: number): string;\n hour(ts: number): number;\n dayKey(ts: number): string;\n isWeekend(ts: number): boolean;\n daysBetween(from: number, to: number): number;\n daypartOf(ts: number): string;\n daypartLabel(key: string): string;\n dayLabel(ts: number, now?: number): string;\n dayChip(ts: number, now?: number): string;\n when(ts: number, now?: number): string;\n date(ts: number): string;\n dateTime(ts: number): string;\n weekday(ts: number): string;\n}\n\nexport type ClockConfig = Pick<BookingConfig, 'timeZone' | 'locale' | 'dayparts'> & {\n labels?: Partial<BookingLabels>;\n};\n\n/** One clock per config object: every factory that needs the clock asks for it. */\nconst CLOCKS = new WeakMap<ClockConfig, Clock>();\n\nexport function createClock(config: ClockConfig): Clock {\n const known = CLOCKS.get(config);\n if (known !== undefined) return known;\n const clock = buildClock(config);\n CLOCKS.set(config, clock);\n return clock;\n}\n\nfunction buildClock(config: ClockConfig): Clock {\n const { timeZone } = config;\n const f = formattersFor(timeZone, config.locale);\n const labels = resolveLabels(config.locale, config.labels);\n const dayparts: readonly BookingDaypart[] = config.dayparts;\n\n function partsOf(ts: number): Record<string, string> {\n const out: Record<string, string> = {};\n for (const part of f.parts.formatToParts(ts)) out[part.type] = part.value;\n return out;\n }\n\n /**\n * How far ahead of UTC the business is at a given instant — `+1 h` in winter,\n * `+2 h` in summer for Oslo.\n *\n * Read from the zone database rather than assumed, because the whole point of\n * the two functions below is the two days a year when it changes.\n */\n function offsetMs(ts: number): number {\n const out: Record<string, string> = {};\n for (const part of f.wallClock.formatToParts(ts)) out[part.type] = part.value;\n const asIfUtc = Date.UTC(\n Number(out.year),\n Number(out.month) - 1,\n Number(out.day),\n Number(out.hour),\n Number(out.minute),\n Number(out.second)\n );\n // The formatter has no milliseconds, so neither does the comparison.\n return asIfUtc - (ts - (((ts % 1000) + 1000) % 1000));\n }\n\n /**\n * Midnight on the business's clock, `offsetDays` calendar days after the day\n * `ts` falls in.\n *\n * The reason this is not `ts + n * 86_400_000`: adding a fixed 24 hours assumes\n * every day is 24 hours long, and two of them a year are not. Late on the\n * Saturday before the clocks go forward, +24 h lands on the Monday and Sunday\n * disappears; early on the Sunday they go back, +24 h lands on the same Sunday\n * again and the far end of the week is lost. Either way a day strip built that\n * way stops describing the week the availability query asked about.\n *\n * Two passes, because the offset has to be read at the answer rather than at the\n * guess: `wall` is midnight on the target date treated as if the zone were UTC,\n * and the offset in force at the true instant is the one that turns it back into\n * one. The second read settles the case where the guess falls on the other side\n * of a transition from the day it belongs to.\n */\n function dayStart(ts: number, offsetDays = 0): number {\n const parts = partsOf(ts);\n const wall = Date.UTC(\n Number(parts.year),\n Number(parts.month) - 1,\n Number(parts.day) + offsetDays\n );\n const guess = wall - offsetMs(wall);\n return wall - offsetMs(guess);\n }\n\n /**\n * The days the time step may offer and the instant the availability query\n * stops at — derived together, so they cannot disagree.\n *\n * `days` is one instant inside each business day, starting with the day\n * `fromTs` falls in; `toTs` is the midnight that ends the last of them. That is\n * the whole contract: every opening the engine can return has a chip to sit\n * on, and every chip covers a stretch the engine was actually asked about.\n *\n * `fromTs` is returned untouched and is NOT snapped to midnight: it is the\n * moment the visitor is standing in, and a query that started at this morning's\n * midnight would ask the engine about hours that are already gone.\n */\n function window(\n fromTs: number,\n rangeDays: number\n ): { fromTs: number; toTs: number; days: number[] } {\n return {\n fromTs,\n toTs: dayStart(fromTs, rangeDays),\n days: Array.from({ length: rangeDays }, (_, index) => dayStart(fromTs, index)),\n };\n }\n\n /** `15:00` — the string on the chip, the summary bar and the confirmation. */\n function formatTime(ts: number): string {\n return f.time.format(ts);\n }\n\n /** The hour 0–23 as the business reads it. */\n function hour(ts: number): number {\n return Number(partsOf(ts).hour);\n }\n\n /**\n * `2026-09-03` — the identity of a *business* day, which is what slots group\n * by. Slicing an ISO string instead would file 00:30 on Thursday under\n * Wednesday and split one opening day across two headings.\n */\n function dayKey(ts: number): string {\n const parts = partsOf(ts);\n return `${parts.year}-${parts.month}-${parts.day}`;\n }\n\n function isWeekend(ts: number): boolean {\n const weekday = partsOf(ts).weekday;\n return weekday === 'Sat' || weekday === 'Sun';\n }\n\n function utcMidnight(ts: number): number {\n const parts = partsOf(ts);\n return Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day));\n }\n\n /**\n * Whole days between two instants on the business calendar, so «tomorrow»\n * means the next opening day rather than \"in 24 hours\". Anchoring both to UTC\n * midnight after the parts have already been read in the zone keeps the\n * arithmetic clear of the hour the clocks change, where a naive subtraction\n * is off by one.\n */\n function daysBetween(from: number, to: number): number {\n return (utcMidnight(to) - utcMidnight(from)) / 86_400_000;\n }\n\n /** The configured part the hour falls in — on the business's clock. */\n function daypartOf(ts: number): string {\n const h = hour(ts);\n const part = dayparts.find((candidate) => h >= candidate.from && h < candidate.to);\n // A validated config covers 0–24, so this only answers for a hand-built one.\n return (part ?? dayparts[dayparts.length - 1]).key;\n }\n\n function daypartLabel(key: string): string {\n const label = labels[`daypart.${key}`];\n return label === undefined ? key : labelText(label);\n }\n\n /**\n * `i dag` / `i morgen` / `lørdag` / `10. sep.`, mid-sentence.\n *\n * The weekday name stops being useful after six days — «torsdag» could then be\n * either of two Thursdays — so the date takes over rather than being ambiguous\n * about which week the business expects you.\n */\n function dayLabel(ts: number, now: number = Date.now()): string {\n const days = daysBetween(now, ts);\n if (days === 0) return labelText(labels['clock.today']);\n if (days === 1) return labelText(labels['clock.tomorrow']);\n if (days > 1 && days < 7) return f.weekdayLong.format(ts);\n return f.dayMonth.format(ts);\n }\n\n /** The same day, sized for a chip in the date strip: `I dag`, `lør. 5.`. */\n function dayChip(ts: number, now: number = Date.now()): string {\n const days = daysBetween(now, ts);\n if (days === 0) return labelText(labels['clock.todayChip']);\n if (days === 1) return labelText(labels['clock.tomorrowChip']);\n return f.weekdayShort.format(ts);\n }\n\n /** `i dag 14:15` — a day and a time in one breath, for «Neste ledige». */\n function when(ts: number, now: number = Date.now()): string {\n return fill(labels['clock.when'], { day: dayLabel(ts, now), time: formatTime(ts) });\n }\n\n /**\n * `tor. 8. okt.` — the calendar date, with no reference to now.\n *\n * The month keeps ICU's trailing full stop (`okt.`, not `okt`), because that is\n * how Norwegian abbreviates a month and this file does not second-guess the\n * locale data.\n */\n function date(ts: number): string {\n return f.fullDate.format(ts);\n }\n\n /** `tor. 8. okt. kl. 15:00` — the whole appointment in one line, absolute in both halves. */\n function dateTime(ts: number): string {\n return fill(labels['clock.dateTime'], { date: date(ts), time: formatTime(ts) });\n }\n\n /** `lørdag` — the weekday's own name, for «lørdagspris». */\n function weekday(ts: number): string {\n return f.weekdayLong.format(ts);\n }\n\n return {\n timeZone,\n dayparts: dayparts.map((part) => part.key),\n dayStart,\n window,\n formatTime,\n hour,\n dayKey,\n isWeekend,\n daysBetween,\n daypartOf,\n daypartLabel,\n dayLabel,\n dayChip,\n when,\n date,\n dateTime,\n weekday,\n };\n}\n"],"mappings":";;;;;;AAmDA,MAAM,6BAAa,IAAI,IAAwB;AAE/C,SAAS,cAAc,UAAkB,QAA4B;CACnE,MAAM,WAAW,GAAG,SAAS,QAAQ;CACrC,MAAM,SAAS,WAAW,IAAI,QAAQ;CACtC,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,MAAM,QAAoB;EACxB,MAAM,IAAI,KAAK,eAAe,QAAQ;GACpC;GACA,MAAM;GACN,QAAQ;GACR,WAAW;EACb,CAAC;EACD,OAAO,IAAI,KAAK,eAAe,SAAS;GACtC;GACA,MAAM;GACN,OAAO;GACP,KAAK;GACL,MAAM;GACN,WAAW;GACX,SAAS;EACX,CAAC;EACD,aAAa,IAAI,KAAK,eAAe,QAAQ;GAAE;GAAU,SAAS;EAAO,CAAC;EAC1E,cAAc,IAAI,KAAK,eAAe,QAAQ;GAAE;GAAU,SAAS;GAAS,KAAK;EAAU,CAAC;EAC5F,UAAU,IAAI,KAAK,eAAe,QAAQ;GAAE;GAAU,KAAK;GAAW,OAAO;EAAQ,CAAC;EACtF,UAAU,IAAI,KAAK,eAAe,QAAQ;GACxC;GACA,SAAS;GACT,KAAK;GACL,OAAO;EACT,CAAC;EACD,WAAW,IAAI,KAAK,eAAe,SAAS;GAC1C;GACA,MAAM;GACN,OAAO;GACP,KAAK;GACL,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,WAAW;EACb,CAAC;CACH;CACA,WAAW,IAAI,UAAU,KAAK;CAC9B,OAAO;AACT;;AA4BA,MAAM,yBAAS,IAAI,QAA4B;AAE/C,SAAgB,YAAY,QAA4B;CACtD,MAAM,QAAQ,OAAO,IAAI,MAAM;CAC/B,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,QAAQ,WAAW,MAAM;CAC/B,OAAO,IAAI,QAAQ,KAAK;CACxB,OAAO;AACT;AAEA,SAAS,WAAW,QAA4B;CAC9C,MAAM,EAAE,aAAa;CACrB,MAAM,IAAI,cAAc,UAAU,OAAO,MAAM;CAC/C,MAAM,SAAS,cAAc,OAAO,QAAQ,OAAO,MAAM;CACzD,MAAM,WAAsC,OAAO;CAEnD,SAAS,QAAQ,IAAoC;EACnD,MAAM,MAA8B,CAAC;EACrC,KAAK,MAAM,QAAQ,EAAE,MAAM,cAAc,EAAE,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpE,OAAO;CACT;;;;;;;;CASA,SAAS,SAAS,IAAoB;EACpC,MAAM,MAA8B,CAAC;EACrC,KAAK,MAAM,QAAQ,EAAE,UAAU,cAAc,EAAE,GAAG,IAAI,KAAK,QAAQ,KAAK;EAUxE,OATgB,KAAK,IACnB,OAAO,IAAI,IAAI,GACf,OAAO,IAAI,KAAK,IAAI,GACpB,OAAO,IAAI,GAAG,GACd,OAAO,IAAI,IAAI,GACf,OAAO,IAAI,MAAM,GACjB,OAAO,IAAI,MAAM,CAGN,KAAK,MAAQ,KAAK,MAAQ,OAAQ;CACjD;;;;;;;;;;;;;;;;;;CAmBA,SAAS,SAAS,IAAY,aAAa,GAAW;EACpD,MAAM,QAAQ,QAAQ,EAAE;EACxB,MAAM,OAAO,KAAK,IAChB,OAAO,MAAM,IAAI,GACjB,OAAO,MAAM,KAAK,IAAI,GACtB,OAAO,MAAM,GAAG,IAAI,UACtB;EAEA,OAAO,OAAO,SADA,OAAO,SAAS,IAAI,CACN;CAC9B;;;;;;;;;;;;;;CAeA,SAAS,OACP,QACA,WACkD;EAClD,OAAO;GACL;GACA,MAAM,SAAS,QAAQ,SAAS;GAChC,MAAM,MAAM,KAAK,EAAE,QAAQ,UAAU,IAAI,GAAG,UAAU,SAAS,QAAQ,KAAK,CAAC;EAC/E;CACF;;CAGA,SAAS,WAAW,IAAoB;EACtC,OAAO,EAAE,KAAK,OAAO,EAAE;CACzB;;CAGA,SAAS,KAAK,IAAoB;EAChC,OAAO,OAAO,QAAQ,EAAE,CAAC,CAAC,IAAI;CAChC;;;;;;CAOA,SAAS,OAAO,IAAoB;EAClC,MAAM,QAAQ,QAAQ,EAAE;EACxB,OAAO,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM,GAAG,MAAM;CAC/C;CAEA,SAAS,UAAU,IAAqB;EACtC,MAAM,UAAU,QAAQ,EAAE,CAAC,CAAC;EAC5B,OAAO,YAAY,SAAS,YAAY;CAC1C;CAEA,SAAS,YAAY,IAAoB;EACvC,MAAM,QAAQ,QAAQ,EAAE;EACxB,OAAO,KAAK,IAAI,OAAO,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,IAAI,GAAG,OAAO,MAAM,GAAG,CAAC;CAChF;;;;;;;;CASA,SAAS,YAAY,MAAc,IAAoB;EACrD,QAAQ,YAAY,EAAE,IAAI,YAAY,IAAI,KAAK;CACjD;;CAGA,SAAS,UAAU,IAAoB;EACrC,MAAM,IAAI,KAAK,EAAE;EAGjB,QAFa,SAAS,MAAM,cAAc,KAAK,UAAU,QAAQ,IAAI,UAAU,EAEpE,KAAK,SAAS,SAAS,SAAS,GAAA,CAAI;CACjD;CAEA,SAAS,aAAa,KAAqB;EACzC,MAAM,QAAQ,OAAO,WAAW;EAChC,OAAO,UAAU,KAAA,IAAY,MAAM,UAAU,KAAK;CACpD;;;;;;;;CASA,SAAS,SAAS,IAAY,MAAc,KAAK,IAAI,GAAW;EAC9D,MAAM,OAAO,YAAY,KAAK,EAAE;EAChC,IAAI,SAAS,GAAG,OAAO,UAAU,OAAO,cAAc;EACtD,IAAI,SAAS,GAAG,OAAO,UAAU,OAAO,iBAAiB;EACzD,IAAI,OAAO,KAAK,OAAO,GAAG,OAAO,EAAE,YAAY,OAAO,EAAE;EACxD,OAAO,EAAE,SAAS,OAAO,EAAE;CAC7B;;CAGA,SAAS,QAAQ,IAAY,MAAc,KAAK,IAAI,GAAW;EAC7D,MAAM,OAAO,YAAY,KAAK,EAAE;EAChC,IAAI,SAAS,GAAG,OAAO,UAAU,OAAO,kBAAkB;EAC1D,IAAI,SAAS,GAAG,OAAO,UAAU,OAAO,qBAAqB;EAC7D,OAAO,EAAE,aAAa,OAAO,EAAE;CACjC;;CAGA,SAAS,KAAK,IAAY,MAAc,KAAK,IAAI,GAAW;EAC1D,OAAO,KAAK,OAAO,eAAe;GAAE,KAAK,SAAS,IAAI,GAAG;GAAG,MAAM,WAAW,EAAE;EAAE,CAAC;CACpF;;;;;;;;CASA,SAAS,KAAK,IAAoB;EAChC,OAAO,EAAE,SAAS,OAAO,EAAE;CAC7B;;CAGA,SAAS,SAAS,IAAoB;EACpC,OAAO,KAAK,OAAO,mBAAmB;GAAE,MAAM,KAAK,EAAE;GAAG,MAAM,WAAW,EAAE;EAAE,CAAC;CAChF;;CAGA,SAAS,QAAQ,IAAoB;EACnC,OAAO,EAAE,YAAY,OAAO,EAAE;CAChC;CAEA,OAAO;EACL;EACA,UAAU,SAAS,KAAK,SAAS,KAAK,GAAG;EACzC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { BookingLabels } from "./labels.mjs";
|
|
2
|
+
//#region src/core/config.d.ts
|
|
3
|
+
/** A service group, in display order. Medal's free-string `category` maps onto `key`. */
|
|
4
|
+
export interface BookingCategory {
|
|
5
|
+
key: string;
|
|
6
|
+
/** Who the group is for. `child` is the kids' path: guest children, `?antall=`, the held link service. */
|
|
7
|
+
audience: 'child' | 'adult' | 'any';
|
|
8
|
+
/**
|
|
9
|
+
* For a child's group: which adult group a service «grows up» into once the
|
|
10
|
+
* child has outgrown the kids' menu. A plain key maps every service in the
|
|
11
|
+
* group; a list maps by a word in the service name, first match wins
|
|
12
|
+
* (`{ nameIncludes: 'gutt', category: 'herre' }`). Case-insensitive.
|
|
13
|
+
*/
|
|
14
|
+
adultEquivalent?: string | ReadonlyArray<{
|
|
15
|
+
nameIncludes: string;
|
|
16
|
+
category: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/** A named part of the day on the business's clock, in local hours `[from, to)`. */
|
|
20
|
+
export interface BookingDaypart {
|
|
21
|
+
key: string;
|
|
22
|
+
from: number;
|
|
23
|
+
to: number;
|
|
24
|
+
}
|
|
25
|
+
export interface BookingConfig {
|
|
26
|
+
/** IANA zone the business keeps its hours in. Required. */
|
|
27
|
+
timeZone: string;
|
|
28
|
+
/** BCP-47, for every `Intl` formatter and the label pack. */
|
|
29
|
+
locale: string;
|
|
30
|
+
/** ISO 4217. Amounts stay in minor units (øre, pence) on the wire. */
|
|
31
|
+
currency: string;
|
|
32
|
+
/** `NO` is the only strict rule set in 0.x; every other country is `loose`. */
|
|
33
|
+
phone: {
|
|
34
|
+
country: string;
|
|
35
|
+
validate: 'strict' | 'loose';
|
|
36
|
+
};
|
|
37
|
+
/** URLs this site serves. */
|
|
38
|
+
paths: {
|
|
39
|
+
booking: string;
|
|
40
|
+
/** Prefix of the manage page; the token is the next segment. */
|
|
41
|
+
manage: string;
|
|
42
|
+
/** `null` = no customer portal. */
|
|
43
|
+
portal: string | null;
|
|
44
|
+
portalLogin: string;
|
|
45
|
+
api: string;
|
|
46
|
+
portalApi: string;
|
|
47
|
+
avatar: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The query keys a link may carry into the booking page.
|
|
51
|
+
*
|
|
52
|
+
* `category`, `service`, `stylist`, `party` and `who` are the marketing
|
|
53
|
+
* deep-link keys; `rebookService` / `rebookStylist` are the portal's «book
|
|
54
|
+
* again» link, read by the wizard's own prefill; `resume` marks a return
|
|
55
|
+
* from an external login; `time` is reserved for a pre-picked start.
|
|
56
|
+
*/
|
|
57
|
+
query: {
|
|
58
|
+
category: string;
|
|
59
|
+
service: string;
|
|
60
|
+
stylist: string;
|
|
61
|
+
party: string;
|
|
62
|
+
who: string;
|
|
63
|
+
time: string;
|
|
64
|
+
resume: string;
|
|
65
|
+
rebookService: string;
|
|
66
|
+
rebookStylist: string;
|
|
67
|
+
};
|
|
68
|
+
/** The values `query.who` accepts. */
|
|
69
|
+
whoValues: {
|
|
70
|
+
child: string;
|
|
71
|
+
adult: string;
|
|
72
|
+
};
|
|
73
|
+
/** Service groups, in display order. */
|
|
74
|
+
categories: BookingCategory[];
|
|
75
|
+
/** Where an unknown Medal category lands. Must be one of `categories`. */
|
|
76
|
+
fallbackCategory: string;
|
|
77
|
+
/** Step 1's rules. */
|
|
78
|
+
party: {
|
|
79
|
+
maxPeople: number;
|
|
80
|
+
allowParallel: boolean;
|
|
81
|
+
askWhoFirst: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** The bookable window and the page's prefetch. */
|
|
84
|
+
window: {
|
|
85
|
+
rangeDays: number;
|
|
86
|
+
prefetchLimit: number;
|
|
87
|
+
prefetchCategory: string | null;
|
|
88
|
+
};
|
|
89
|
+
/** Named day parts for the time step, contiguous from 0 to 24. */
|
|
90
|
+
dayparts: BookingDaypart[];
|
|
91
|
+
/** Set = the booking path hands off (307) to this URL instead of rendering. */
|
|
92
|
+
handoffUrl: string | null;
|
|
93
|
+
/** The business's own contact, for «call us» and the unavailable screen. */
|
|
94
|
+
contact: {
|
|
95
|
+
phone: string | null;
|
|
96
|
+
address: string | null;
|
|
97
|
+
name: string;
|
|
98
|
+
};
|
|
99
|
+
portal: {
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
methods: Array<'email_code' | 'vipps'>;
|
|
102
|
+
/** The session cookie. */
|
|
103
|
+
cookieName: string;
|
|
104
|
+
/** Where a login that left the site (Vipps) returns to; read once. */
|
|
105
|
+
nextCookieName: string;
|
|
106
|
+
/** Must be `__Host-`-prefixed: Secure, `Path=/`, no `Domain`. */
|
|
107
|
+
vippsBindingCookieName: string;
|
|
108
|
+
vippsLinkCookieName: string;
|
|
109
|
+
/**
|
|
110
|
+
* Where a finished login may send the visitor. `exact` matches the
|
|
111
|
+
* pathname only (the booking page, so a deeper manage URL never
|
|
112
|
+
* qualifies); `prefixes` matches anything below.
|
|
113
|
+
*/
|
|
114
|
+
returnPaths: {
|
|
115
|
+
exact: string[];
|
|
116
|
+
prefixes: string[];
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
consent: {
|
|
120
|
+
termsUrl: string | null;
|
|
121
|
+
/** `null` = no marketing box. */
|
|
122
|
+
marketing: {
|
|
123
|
+
text: string;
|
|
124
|
+
version: string;
|
|
125
|
+
} | null;
|
|
126
|
+
};
|
|
127
|
+
/** Namespaces browser storage keys: `<ns>:booking:draft`, … */
|
|
128
|
+
storageNamespace: string;
|
|
129
|
+
/**
|
|
130
|
+
* `prodId` stamps every calendar file. `uidDomain` is for UIDs the package
|
|
131
|
+
* mints itself (`/react`, 0.1.0); a booking id passed in as `uid` is used as
|
|
132
|
+
* is, so a calendar entry made before a site moved onto the package is still
|
|
133
|
+
* the same entry after it.
|
|
134
|
+
*/
|
|
135
|
+
ics: {
|
|
136
|
+
prodId: string;
|
|
137
|
+
uidDomain: string;
|
|
138
|
+
};
|
|
139
|
+
monitoring: {
|
|
140
|
+
enabled: boolean;
|
|
141
|
+
sampleRate: number;
|
|
142
|
+
};
|
|
143
|
+
/** Copy, merged over the built-in pack for `locale`. */
|
|
144
|
+
labels?: Partial<BookingLabels>;
|
|
145
|
+
}
|
|
146
|
+
type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]>; } : T;
|
|
147
|
+
/** What a site passes: `timeZone`, and whatever it wants to change. */
|
|
148
|
+
export type BookingConfigInput = {
|
|
149
|
+
timeZone: string;
|
|
150
|
+
} & DeepPartial<Omit<BookingConfig, 'timeZone'>>;
|
|
151
|
+
/** Thrown with zod's issue list when a config does not validate. */
|
|
152
|
+
export declare class BookingConfigError extends Error {
|
|
153
|
+
readonly issues: ReadonlyArray<{
|
|
154
|
+
path: string;
|
|
155
|
+
message: string;
|
|
156
|
+
}>;
|
|
157
|
+
constructor(issues: ReadonlyArray<{
|
|
158
|
+
path: string;
|
|
159
|
+
message: string;
|
|
160
|
+
}>);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The site's partial config with every default filled, validated and frozen.
|
|
164
|
+
*
|
|
165
|
+
* The portal's three secondary cookie names derive from `cookieName` unless
|
|
166
|
+
* set, and a login may return to the booking page unless `returnPaths` says
|
|
167
|
+
* otherwise. Idempotent: a resolved config resolves to an equal one.
|
|
168
|
+
*/
|
|
169
|
+
export declare function resolveBookingConfig(input: BookingConfigInput): Readonly<BookingConfig>;
|
|
170
|
+
//#endregion
|
|
171
|
+
//# sourceMappingURL=config.d.mts.map
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/core/config.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a site tells the booking package about itself.
|
|
5
|
+
*
|
|
6
|
+
* `BookingConfig` is CLIENT-SAFE by construction: it is serialised into the
|
|
7
|
+
* page as a prop, so it never holds a key, a session secret or a cache handle.
|
|
8
|
+
* Those live in `BookingServerOptions`, which only `/next` knows (P4).
|
|
9
|
+
*
|
|
10
|
+
* The package reads no environment variable and no CMS. A site resolves its
|
|
11
|
+
* own switches (a handoff URL, whether accounts are on, the contact block) and
|
|
12
|
+
* passes the answers in.
|
|
13
|
+
*
|
|
14
|
+
* Every field but `timeZone` has a default. `resolveBookingConfig` fills them,
|
|
15
|
+
* validates the whole object with zod and freezes it, so a config that reached
|
|
16
|
+
* a factory is one that passed.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_PATHS = {
|
|
19
|
+
booking: "/bestill",
|
|
20
|
+
manage: "/bestill/administrer",
|
|
21
|
+
portal: "/min-side",
|
|
22
|
+
portalLogin: "/min-side/logg-inn",
|
|
23
|
+
api: "/api/booking",
|
|
24
|
+
portalApi: "/api/portal",
|
|
25
|
+
avatar: "/api/booking/avatar"
|
|
26
|
+
};
|
|
27
|
+
/** Everything but `timeZone` (required) and the portal cookie names (derived). */
|
|
28
|
+
function defaults() {
|
|
29
|
+
return {
|
|
30
|
+
locale: "nb-NO",
|
|
31
|
+
currency: "NOK",
|
|
32
|
+
phone: {
|
|
33
|
+
country: "NO",
|
|
34
|
+
validate: "strict"
|
|
35
|
+
},
|
|
36
|
+
paths: { ...DEFAULT_PATHS },
|
|
37
|
+
query: {
|
|
38
|
+
category: "kategori",
|
|
39
|
+
service: "tjeneste",
|
|
40
|
+
stylist: "frisor",
|
|
41
|
+
party: "antall",
|
|
42
|
+
who: "hvem",
|
|
43
|
+
time: "tid",
|
|
44
|
+
resume: "resume",
|
|
45
|
+
rebookService: "service",
|
|
46
|
+
rebookStylist: "stylist"
|
|
47
|
+
},
|
|
48
|
+
whoValues: {
|
|
49
|
+
child: "barn",
|
|
50
|
+
adult: "voksen"
|
|
51
|
+
},
|
|
52
|
+
categories: [{
|
|
53
|
+
key: "barn",
|
|
54
|
+
audience: "child"
|
|
55
|
+
}, {
|
|
56
|
+
key: "annet",
|
|
57
|
+
audience: "any"
|
|
58
|
+
}],
|
|
59
|
+
fallbackCategory: "annet",
|
|
60
|
+
party: {
|
|
61
|
+
maxPeople: 3,
|
|
62
|
+
allowParallel: true,
|
|
63
|
+
askWhoFirst: true
|
|
64
|
+
},
|
|
65
|
+
window: {
|
|
66
|
+
rangeDays: 7,
|
|
67
|
+
prefetchLimit: 4,
|
|
68
|
+
prefetchCategory: null
|
|
69
|
+
},
|
|
70
|
+
dayparts: [
|
|
71
|
+
{
|
|
72
|
+
key: "formiddag",
|
|
73
|
+
from: 0,
|
|
74
|
+
to: 12
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "ettermiddag",
|
|
78
|
+
from: 12,
|
|
79
|
+
to: 17
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "kveld",
|
|
83
|
+
from: 17,
|
|
84
|
+
to: 24
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
handoffUrl: null,
|
|
88
|
+
contact: {
|
|
89
|
+
phone: null,
|
|
90
|
+
address: null,
|
|
91
|
+
name: ""
|
|
92
|
+
},
|
|
93
|
+
portal: {
|
|
94
|
+
enabled: false,
|
|
95
|
+
methods: ["email_code"],
|
|
96
|
+
cookieName: "booking_portal"
|
|
97
|
+
},
|
|
98
|
+
consent: {
|
|
99
|
+
termsUrl: null,
|
|
100
|
+
marketing: null
|
|
101
|
+
},
|
|
102
|
+
storageNamespace: "medal",
|
|
103
|
+
ics: {
|
|
104
|
+
prodId: "-//Medal Social//Booking//EN",
|
|
105
|
+
uidDomain: "booking.invalid"
|
|
106
|
+
},
|
|
107
|
+
monitoring: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
sampleRate: 1
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Objects merge key by key; arrays, scalars and `null` replace. Only
|
|
118
|
+
* `undefined` keeps the default — `paths.portal: null` means «no portal».
|
|
119
|
+
*/
|
|
120
|
+
function merge(base, patch) {
|
|
121
|
+
if (patch === void 0) return base;
|
|
122
|
+
if (!isRecord(base) || !isRecord(patch)) return patch;
|
|
123
|
+
const out = { ...base };
|
|
124
|
+
for (const [key, value] of Object.entries(patch)) out[key] = merge(base[key], value);
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function validTimeZone(value) {
|
|
128
|
+
try {
|
|
129
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function validLocale(value) {
|
|
136
|
+
try {
|
|
137
|
+
return Intl.getCanonicalLocales(value).length === 1;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const path = z.string().regex(/^\/[^\s?#]*$/, "must be a root-relative path");
|
|
143
|
+
const key = z.string().regex(/^[\w-]{1,64}$/, "must be 1–64 word characters");
|
|
144
|
+
/** Group keys and `?hvem=` values are matched against a lower-cased query value. */
|
|
145
|
+
const lowerKey = z.string().regex(/^[a-z0-9_-]{1,64}$/, "must be 1–64 lower-case word characters");
|
|
146
|
+
/** A prefix must end at a segment boundary, or `/flow` would also admit `/flow-evil`. */
|
|
147
|
+
const prefix = path.refine((value) => value.endsWith("/"), "a prefix must end with /");
|
|
148
|
+
/** The longest cookie name the config takes. */
|
|
149
|
+
const MAX_COOKIE_NAME = 128;
|
|
150
|
+
/** RFC 6265 token characters, which is every name a browser keeps intact. */
|
|
151
|
+
const cookieName = z.string().regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,128}$/);
|
|
152
|
+
/**
|
|
153
|
+
* How each secondary portal cookie name derives from `portal.cookieName` when
|
|
154
|
+
* the site sets none — the prefix and suffix, so a too-long base can be
|
|
155
|
+
* reported against the name the site actually wrote.
|
|
156
|
+
*/
|
|
157
|
+
const DERIVED_COOKIE_NAMES = {
|
|
158
|
+
nextCookieName: {
|
|
159
|
+
prefix: "",
|
|
160
|
+
suffix: "_next"
|
|
161
|
+
},
|
|
162
|
+
vippsBindingCookieName: {
|
|
163
|
+
prefix: "__Host-",
|
|
164
|
+
suffix: "_vipps_bind"
|
|
165
|
+
},
|
|
166
|
+
vippsLinkCookieName: {
|
|
167
|
+
prefix: "",
|
|
168
|
+
suffix: "_vipps_link"
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* A derived name that would be too long, said about `portal.cookieName`
|
|
173
|
+
* (which the site wrote) rather than a name it never set. `null` when every
|
|
174
|
+
* derived name fits — the schema then judges the characters as usual — and
|
|
175
|
+
* when the base is not a string at all (a runtime input the types did not
|
|
176
|
+
* catch): the schema's own issue on `portal.cookieName` says so.
|
|
177
|
+
*/
|
|
178
|
+
function derivedCookieNameIssue(session, derived) {
|
|
179
|
+
if (typeof session !== "string") return null;
|
|
180
|
+
const tooLong = derived.filter((name) => {
|
|
181
|
+
const { prefix, suffix } = DERIVED_COOKIE_NAMES[name];
|
|
182
|
+
return prefix.length + session.length + suffix.length > MAX_COOKIE_NAME;
|
|
183
|
+
});
|
|
184
|
+
if (tooLong.length === 0) return null;
|
|
185
|
+
return {
|
|
186
|
+
path: "portal.cookieName",
|
|
187
|
+
message: `must be at most ${Math.min(...tooLong.map((name) => {
|
|
188
|
+
const { prefix, suffix } = DERIVED_COOKIE_NAMES[name];
|
|
189
|
+
return MAX_COOKIE_NAME - prefix.length - suffix.length;
|
|
190
|
+
}))} characters, or the derived ${tooLong.map((name) => `portal.${name}`).join(", ")} would exceed ${MAX_COOKIE_NAME}; shorten it or set ${tooLong.length === 1 ? "that name" : "those names"} explicitly`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const schema = z.object({
|
|
194
|
+
timeZone: z.string().refine(validTimeZone, "must be an IANA time zone"),
|
|
195
|
+
locale: z.string().refine(validLocale, "must be a BCP-47 locale"),
|
|
196
|
+
currency: z.string().regex(/^[A-Z]{3}$/, "must be an ISO 4217 code"),
|
|
197
|
+
phone: z.object({
|
|
198
|
+
country: z.string().regex(/^[A-Z]{2}$/),
|
|
199
|
+
validate: z.enum(["strict", "loose"])
|
|
200
|
+
}),
|
|
201
|
+
paths: z.object({
|
|
202
|
+
booking: path,
|
|
203
|
+
manage: path,
|
|
204
|
+
portal: path.nullable(),
|
|
205
|
+
portalLogin: path,
|
|
206
|
+
api: path,
|
|
207
|
+
portalApi: path,
|
|
208
|
+
avatar: path
|
|
209
|
+
}),
|
|
210
|
+
query: z.object({
|
|
211
|
+
category: key,
|
|
212
|
+
service: key,
|
|
213
|
+
stylist: key,
|
|
214
|
+
party: key,
|
|
215
|
+
who: key,
|
|
216
|
+
time: key,
|
|
217
|
+
resume: key,
|
|
218
|
+
rebookService: key,
|
|
219
|
+
rebookStylist: key
|
|
220
|
+
}),
|
|
221
|
+
whoValues: z.object({
|
|
222
|
+
child: lowerKey,
|
|
223
|
+
adult: lowerKey
|
|
224
|
+
}),
|
|
225
|
+
categories: z.array(z.object({
|
|
226
|
+
key: lowerKey,
|
|
227
|
+
audience: z.enum([
|
|
228
|
+
"child",
|
|
229
|
+
"adult",
|
|
230
|
+
"any"
|
|
231
|
+
]),
|
|
232
|
+
adultEquivalent: z.union([lowerKey, z.array(z.object({
|
|
233
|
+
nameIncludes: z.string().min(1),
|
|
234
|
+
category: lowerKey
|
|
235
|
+
}))]).optional()
|
|
236
|
+
})).min(1),
|
|
237
|
+
fallbackCategory: lowerKey,
|
|
238
|
+
party: z.object({
|
|
239
|
+
maxPeople: z.number().int().min(1).max(20),
|
|
240
|
+
allowParallel: z.boolean(),
|
|
241
|
+
askWhoFirst: z.boolean()
|
|
242
|
+
}),
|
|
243
|
+
window: z.object({
|
|
244
|
+
rangeDays: z.number().int().min(1).max(62),
|
|
245
|
+
prefetchLimit: z.number().int().min(0).max(20),
|
|
246
|
+
prefetchCategory: lowerKey.nullable()
|
|
247
|
+
}),
|
|
248
|
+
dayparts: z.array(z.object({
|
|
249
|
+
key,
|
|
250
|
+
from: z.number().int().min(0).max(23),
|
|
251
|
+
to: z.number().int().min(1).max(24)
|
|
252
|
+
})).min(1),
|
|
253
|
+
handoffUrl: z.url({ protocol: /^https?$/ }).nullable(),
|
|
254
|
+
contact: z.object({
|
|
255
|
+
phone: z.string().nullable(),
|
|
256
|
+
address: z.string().nullable(),
|
|
257
|
+
name: z.string()
|
|
258
|
+
}),
|
|
259
|
+
portal: z.object({
|
|
260
|
+
enabled: z.boolean(),
|
|
261
|
+
methods: z.array(z.enum(["email_code", "vipps"])),
|
|
262
|
+
cookieName,
|
|
263
|
+
nextCookieName: cookieName,
|
|
264
|
+
vippsBindingCookieName: cookieName.refine((name) => name.startsWith("__Host-"), "must carry the __Host- prefix"),
|
|
265
|
+
vippsLinkCookieName: cookieName,
|
|
266
|
+
returnPaths: z.object({
|
|
267
|
+
exact: z.array(path),
|
|
268
|
+
prefixes: z.array(prefix)
|
|
269
|
+
})
|
|
270
|
+
}),
|
|
271
|
+
consent: z.object({
|
|
272
|
+
termsUrl: z.string().min(1).nullable(),
|
|
273
|
+
marketing: z.object({
|
|
274
|
+
text: z.string().min(1),
|
|
275
|
+
version: z.string().min(1)
|
|
276
|
+
}).nullable()
|
|
277
|
+
}),
|
|
278
|
+
storageNamespace: key,
|
|
279
|
+
ics: z.object({
|
|
280
|
+
prodId: z.string().regex(/^-\/\/.+\/\/.+$/),
|
|
281
|
+
uidDomain: z.string().min(1)
|
|
282
|
+
}),
|
|
283
|
+
monitoring: z.object({
|
|
284
|
+
enabled: z.boolean(),
|
|
285
|
+
sampleRate: z.number().min(0).max(1)
|
|
286
|
+
}),
|
|
287
|
+
labels: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional()
|
|
288
|
+
}).superRefine((config, ctx) => {
|
|
289
|
+
const keys = config.categories.map((category) => category.key);
|
|
290
|
+
const known = new Set(keys);
|
|
291
|
+
if (known.size !== keys.length) ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
path: ["categories"],
|
|
294
|
+
message: "category keys must be unique"
|
|
295
|
+
});
|
|
296
|
+
if (!known.has(config.fallbackCategory)) ctx.addIssue({
|
|
297
|
+
code: "custom",
|
|
298
|
+
path: ["fallbackCategory"],
|
|
299
|
+
message: "must be one of the categories"
|
|
300
|
+
});
|
|
301
|
+
if (config.window.prefetchCategory !== null && !known.has(config.window.prefetchCategory)) ctx.addIssue({
|
|
302
|
+
code: "custom",
|
|
303
|
+
path: ["window", "prefetchCategory"],
|
|
304
|
+
message: "must be one of the categories"
|
|
305
|
+
});
|
|
306
|
+
config.categories.forEach((category, index) => {
|
|
307
|
+
const targets = category.adultEquivalent === void 0 ? [] : typeof category.adultEquivalent === "string" ? [category.adultEquivalent] : category.adultEquivalent.map((rule) => rule.category);
|
|
308
|
+
for (const target of targets) if (!known.has(target)) ctx.addIssue({
|
|
309
|
+
code: "custom",
|
|
310
|
+
path: [
|
|
311
|
+
"categories",
|
|
312
|
+
index,
|
|
313
|
+
"adultEquivalent"
|
|
314
|
+
],
|
|
315
|
+
message: `«${target}» is not one of the categories`
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
let hour = 0;
|
|
319
|
+
config.dayparts.forEach((part, index) => {
|
|
320
|
+
if (part.from !== hour || part.to <= part.from) ctx.addIssue({
|
|
321
|
+
code: "custom",
|
|
322
|
+
path: ["dayparts", index],
|
|
323
|
+
message: "dayparts must run contiguously from 0 to 24"
|
|
324
|
+
});
|
|
325
|
+
hour = part.to;
|
|
326
|
+
});
|
|
327
|
+
if (hour !== 24) ctx.addIssue({
|
|
328
|
+
code: "custom",
|
|
329
|
+
path: ["dayparts"],
|
|
330
|
+
message: "dayparts must end at 24"
|
|
331
|
+
});
|
|
332
|
+
if (config.portal.methods.length === 0 && config.portal.enabled) ctx.addIssue({
|
|
333
|
+
code: "custom",
|
|
334
|
+
path: ["portal", "methods"],
|
|
335
|
+
message: "an enabled portal needs a login method"
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
function deepFreeze(value) {
|
|
339
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
340
|
+
Object.freeze(value);
|
|
341
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
/** Thrown with zod's issue list when a config does not validate. */
|
|
346
|
+
var BookingConfigError = class extends Error {
|
|
347
|
+
issues;
|
|
348
|
+
constructor(issues) {
|
|
349
|
+
super(`Invalid booking config:\n${issues.map((issue) => ` ${issue.path}: ${issue.message}`).join("\n")}`);
|
|
350
|
+
this.name = "BookingConfigError";
|
|
351
|
+
this.issues = issues;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* The site's partial config with every default filled, validated and frozen.
|
|
356
|
+
*
|
|
357
|
+
* The portal's three secondary cookie names derive from `cookieName` unless
|
|
358
|
+
* set, and a login may return to the booking page unless `returnPaths` says
|
|
359
|
+
* otherwise. Idempotent: a resolved config resolves to an equal one.
|
|
360
|
+
*/
|
|
361
|
+
function resolveBookingConfig(input) {
|
|
362
|
+
const merged = merge(defaults(), input);
|
|
363
|
+
const session = merged.portal.cookieName;
|
|
364
|
+
const candidate = {
|
|
365
|
+
...merged,
|
|
366
|
+
portal: {
|
|
367
|
+
...merged.portal,
|
|
368
|
+
nextCookieName: merged.portal.nextCookieName ?? `${session}_next`,
|
|
369
|
+
vippsBindingCookieName: merged.portal.vippsBindingCookieName ?? `__Host-${session}_vipps_bind`,
|
|
370
|
+
vippsLinkCookieName: merged.portal.vippsLinkCookieName ?? `${session}_vipps_link`,
|
|
371
|
+
returnPaths: {
|
|
372
|
+
exact: merged.portal.returnPaths?.exact ?? [merged.paths.booking],
|
|
373
|
+
prefixes: merged.portal.returnPaths?.prefixes ?? []
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const derived = Object.keys(DERIVED_COOKIE_NAMES).filter((name) => merged.portal[name] === void 0);
|
|
378
|
+
const result = schema.safeParse(candidate);
|
|
379
|
+
if (!result.success) {
|
|
380
|
+
const lengthIssue = derivedCookieNameIssue(session, derived);
|
|
381
|
+
const issues = result.error.issues.map((issue) => ({
|
|
382
|
+
path: issue.path.join("."),
|
|
383
|
+
message: issue.message
|
|
384
|
+
})).filter((issue) => lengthIssue === null || !derived.some((name) => issue.path === `portal.${name}`));
|
|
385
|
+
throw new BookingConfigError(lengthIssue === null ? issues : [lengthIssue, ...issues]);
|
|
386
|
+
}
|
|
387
|
+
return deepFreeze(result.data);
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
export { BookingConfigError, resolveBookingConfig };
|
|
391
|
+
|
|
392
|
+
//# sourceMappingURL=config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/core/config.ts"],"sourcesContent":["/**\n * What a site tells the booking package about itself.\n *\n * `BookingConfig` is CLIENT-SAFE by construction: it is serialised into the\n * page as a prop, so it never holds a key, a session secret or a cache handle.\n * Those live in `BookingServerOptions`, which only `/next` knows (P4).\n *\n * The package reads no environment variable and no CMS. A site resolves its\n * own switches (a handoff URL, whether accounts are on, the contact block) and\n * passes the answers in.\n *\n * Every field but `timeZone` has a default. `resolveBookingConfig` fills them,\n * validates the whole object with zod and freezes it, so a config that reached\n * a factory is one that passed.\n */\n\nimport { z } from 'zod';\nimport type { BookingLabels } from './labels';\n\n/** A service group, in display order. Medal's free-string `category` maps onto `key`. */\nexport interface BookingCategory {\n key: string;\n /** Who the group is for. `child` is the kids' path: guest children, `?antall=`, the held link service. */\n audience: 'child' | 'adult' | 'any';\n /**\n * For a child's group: which adult group a service «grows up» into once the\n * child has outgrown the kids' menu. A plain key maps every service in the\n * group; a list maps by a word in the service name, first match wins\n * (`{ nameIncludes: 'gutt', category: 'herre' }`). Case-insensitive.\n */\n adultEquivalent?: string | ReadonlyArray<{ nameIncludes: string; category: string }>;\n}\n\n/** A named part of the day on the business's clock, in local hours `[from, to)`. */\nexport interface BookingDaypart {\n key: string;\n from: number;\n to: number;\n}\n\nexport interface BookingConfig {\n /** IANA zone the business keeps its hours in. Required. */\n timeZone: string;\n /** BCP-47, for every `Intl` formatter and the label pack. */\n locale: string;\n /** ISO 4217. Amounts stay in minor units (øre, pence) on the wire. */\n currency: string;\n /** `NO` is the only strict rule set in 0.x; every other country is `loose`. */\n phone: { country: string; validate: 'strict' | 'loose' };\n /** URLs this site serves. */\n paths: {\n booking: string;\n /** Prefix of the manage page; the token is the next segment. */\n manage: string;\n /** `null` = no customer portal. */\n portal: string | null;\n portalLogin: string;\n api: string;\n portalApi: string;\n avatar: string;\n };\n /**\n * The query keys a link may carry into the booking page.\n *\n * `category`, `service`, `stylist`, `party` and `who` are the marketing\n * deep-link keys; `rebookService` / `rebookStylist` are the portal's «book\n * again» link, read by the wizard's own prefill; `resume` marks a return\n * from an external login; `time` is reserved for a pre-picked start.\n */\n query: {\n category: string;\n service: string;\n stylist: string;\n party: string;\n who: string;\n time: string;\n resume: string;\n rebookService: string;\n rebookStylist: string;\n };\n /** The values `query.who` accepts. */\n whoValues: { child: string; adult: string };\n /** Service groups, in display order. */\n categories: BookingCategory[];\n /** Where an unknown Medal category lands. Must be one of `categories`. */\n fallbackCategory: string;\n /** Step 1's rules. */\n party: { maxPeople: number; allowParallel: boolean; askWhoFirst: boolean };\n /** The bookable window and the page's prefetch. */\n window: { rangeDays: number; prefetchLimit: number; prefetchCategory: string | null };\n /** Named day parts for the time step, contiguous from 0 to 24. */\n dayparts: BookingDaypart[];\n /** Set = the booking path hands off (307) to this URL instead of rendering. */\n handoffUrl: string | null;\n /** The business's own contact, for «call us» and the unavailable screen. */\n contact: { phone: string | null; address: string | null; name: string };\n portal: {\n enabled: boolean;\n methods: Array<'email_code' | 'vipps'>;\n /** The session cookie. */\n cookieName: string;\n /** Where a login that left the site (Vipps) returns to; read once. */\n nextCookieName: string;\n /** Must be `__Host-`-prefixed: Secure, `Path=/`, no `Domain`. */\n vippsBindingCookieName: string;\n vippsLinkCookieName: string;\n /**\n * Where a finished login may send the visitor. `exact` matches the\n * pathname only (the booking page, so a deeper manage URL never\n * qualifies); `prefixes` matches anything below.\n */\n returnPaths: { exact: string[]; prefixes: string[] };\n };\n consent: {\n termsUrl: string | null;\n /** `null` = no marketing box. */\n marketing: { text: string; version: string } | null;\n };\n /** Namespaces browser storage keys: `<ns>:booking:draft`, … */\n storageNamespace: string;\n /**\n * `prodId` stamps every calendar file. `uidDomain` is for UIDs the package\n * mints itself (`/react`, 0.1.0); a booking id passed in as `uid` is used as\n * is, so a calendar entry made before a site moved onto the package is still\n * the same entry after it.\n */\n ics: { prodId: string; uidDomain: string };\n monitoring: { enabled: boolean; sampleRate: number };\n /** Copy, merged over the built-in pack for `locale`. */\n labels?: Partial<BookingLabels>;\n}\n\ntype DeepPartial<T> = T extends readonly unknown[]\n ? T\n : T extends object\n ? { [K in keyof T]?: DeepPartial<T[K]> }\n : T;\n\n/** What a site passes: `timeZone`, and whatever it wants to change. */\nexport type BookingConfigInput = { timeZone: string } & DeepPartial<\n Omit<BookingConfig, 'timeZone'>\n>;\n\nconst DEFAULT_PATHS: BookingConfig['paths'] = {\n booking: '/bestill',\n manage: '/bestill/administrer',\n portal: '/min-side',\n portalLogin: '/min-side/logg-inn',\n api: '/api/booking',\n portalApi: '/api/portal',\n avatar: '/api/booking/avatar',\n};\n\n/** Everything but `timeZone` (required) and the portal cookie names (derived). */\nfunction defaults(): Omit<BookingConfig, 'timeZone' | 'portal'> & {\n portal: Omit<\n BookingConfig['portal'],\n 'nextCookieName' | 'vippsBindingCookieName' | 'vippsLinkCookieName' | 'returnPaths'\n >;\n} {\n return {\n locale: 'nb-NO',\n currency: 'NOK',\n phone: { country: 'NO', validate: 'strict' },\n paths: { ...DEFAULT_PATHS },\n query: {\n category: 'kategori',\n service: 'tjeneste',\n stylist: 'frisor',\n party: 'antall',\n who: 'hvem',\n time: 'tid',\n resume: 'resume',\n rebookService: 'service',\n rebookStylist: 'stylist',\n },\n whoValues: { child: 'barn', adult: 'voksen' },\n categories: [\n { key: 'barn', audience: 'child' },\n { key: 'annet', audience: 'any' },\n ],\n fallbackCategory: 'annet',\n party: { maxPeople: 3, allowParallel: true, askWhoFirst: true },\n window: { rangeDays: 7, prefetchLimit: 4, prefetchCategory: null },\n dayparts: [\n { key: 'formiddag', from: 0, to: 12 },\n { key: 'ettermiddag', from: 12, to: 17 },\n { key: 'kveld', from: 17, to: 24 },\n ],\n handoffUrl: null,\n contact: { phone: null, address: null, name: '' },\n portal: { enabled: false, methods: ['email_code'], cookieName: 'booking_portal' },\n consent: { termsUrl: null, marketing: null },\n storageNamespace: 'medal',\n ics: { prodId: '-//Medal Social//Booking//EN', uidDomain: 'booking.invalid' },\n monitoring: { enabled: true, sampleRate: 1 },\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Objects merge key by key; arrays, scalars and `null` replace. Only\n * `undefined` keeps the default — `paths.portal: null` means «no portal».\n */\nfunction merge(base: unknown, patch: unknown): unknown {\n if (patch === undefined) return base;\n if (!isRecord(base) || !isRecord(patch)) return patch;\n const out: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(patch)) out[key] = merge(base[key], value);\n return out;\n}\n\nfunction validTimeZone(value: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: value });\n return true;\n } catch {\n return false;\n }\n}\n\nfunction validLocale(value: string): boolean {\n try {\n return Intl.getCanonicalLocales(value).length === 1;\n } catch {\n return false;\n }\n}\n\nconst path = z.string().regex(/^\\/[^\\s?#]*$/, 'must be a root-relative path');\nconst key = z.string().regex(/^[\\w-]{1,64}$/, 'must be 1–64 word characters');\n/** Group keys and `?hvem=` values are matched against a lower-cased query value. */\nconst lowerKey = z.string().regex(/^[a-z0-9_-]{1,64}$/, 'must be 1–64 lower-case word characters');\n/** A prefix must end at a segment boundary, or `/flow` would also admit `/flow-evil`. */\nconst prefix = path.refine((value) => value.endsWith('/'), 'a prefix must end with /');\n/** The longest cookie name the config takes. */\nconst MAX_COOKIE_NAME = 128;\n/** RFC 6265 token characters, which is every name a browser keeps intact. */\nconst cookieName = z.string().regex(/^[!#$%&'*+\\-.^_`|~0-9A-Za-z]{1,128}$/);\n\n/**\n * How each secondary portal cookie name derives from `portal.cookieName` when\n * the site sets none — the prefix and suffix, so a too-long base can be\n * reported against the name the site actually wrote.\n */\nconst DERIVED_COOKIE_NAMES = {\n nextCookieName: { prefix: '', suffix: '_next' },\n vippsBindingCookieName: { prefix: '__Host-', suffix: '_vipps_bind' },\n vippsLinkCookieName: { prefix: '', suffix: '_vipps_link' },\n} as const;\n\ntype DerivedCookieName = keyof typeof DERIVED_COOKIE_NAMES;\n\n/**\n * A derived name that would be too long, said about `portal.cookieName`\n * (which the site wrote) rather than a name it never set. `null` when every\n * derived name fits — the schema then judges the characters as usual — and\n * when the base is not a string at all (a runtime input the types did not\n * catch): the schema's own issue on `portal.cookieName` says so.\n */\nfunction derivedCookieNameIssue(\n session: unknown,\n derived: readonly DerivedCookieName[]\n): { path: string; message: string } | null {\n if (typeof session !== 'string') return null;\n const tooLong = derived.filter((name) => {\n const { prefix, suffix } = DERIVED_COOKIE_NAMES[name];\n return prefix.length + session.length + suffix.length > MAX_COOKIE_NAME;\n });\n if (tooLong.length === 0) return null;\n const room = Math.min(\n ...tooLong.map((name) => {\n const { prefix, suffix } = DERIVED_COOKIE_NAMES[name];\n return MAX_COOKIE_NAME - prefix.length - suffix.length;\n })\n );\n return {\n path: 'portal.cookieName',\n message: `must be at most ${room} characters, or the derived ${tooLong\n .map((name) => `portal.${name}`)\n .join(', ')} would exceed ${MAX_COOKIE_NAME}; shorten it or set ${\n tooLong.length === 1 ? 'that name' : 'those names'\n } explicitly`,\n };\n}\n\nconst schema = z\n .object({\n timeZone: z.string().refine(validTimeZone, 'must be an IANA time zone'),\n locale: z.string().refine(validLocale, 'must be a BCP-47 locale'),\n currency: z.string().regex(/^[A-Z]{3}$/, 'must be an ISO 4217 code'),\n phone: z.object({\n country: z.string().regex(/^[A-Z]{2}$/),\n validate: z.enum(['strict', 'loose']),\n }),\n paths: z.object({\n booking: path,\n manage: path,\n portal: path.nullable(),\n portalLogin: path,\n api: path,\n portalApi: path,\n avatar: path,\n }),\n query: z.object({\n category: key,\n service: key,\n stylist: key,\n party: key,\n who: key,\n time: key,\n resume: key,\n rebookService: key,\n rebookStylist: key,\n }),\n whoValues: z.object({ child: lowerKey, adult: lowerKey }),\n categories: z\n .array(\n z.object({\n key: lowerKey,\n audience: z.enum(['child', 'adult', 'any']),\n adultEquivalent: z\n .union([\n lowerKey,\n z.array(z.object({ nameIncludes: z.string().min(1), category: lowerKey })),\n ])\n .optional(),\n })\n )\n .min(1),\n fallbackCategory: lowerKey,\n party: z.object({\n maxPeople: z.number().int().min(1).max(20),\n allowParallel: z.boolean(),\n askWhoFirst: z.boolean(),\n }),\n window: z.object({\n rangeDays: z.number().int().min(1).max(62),\n prefetchLimit: z.number().int().min(0).max(20),\n prefetchCategory: lowerKey.nullable(),\n }),\n dayparts: z\n .array(\n z.object({\n key,\n from: z.number().int().min(0).max(23),\n to: z.number().int().min(1).max(24),\n })\n )\n .min(1),\n handoffUrl: z.url({ protocol: /^https?$/ }).nullable(),\n contact: z.object({\n phone: z.string().nullable(),\n address: z.string().nullable(),\n name: z.string(),\n }),\n portal: z.object({\n enabled: z.boolean(),\n methods: z.array(z.enum(['email_code', 'vipps'])),\n cookieName,\n nextCookieName: cookieName,\n vippsBindingCookieName: cookieName.refine(\n (name) => name.startsWith('__Host-'),\n 'must carry the __Host- prefix'\n ),\n vippsLinkCookieName: cookieName,\n returnPaths: z.object({ exact: z.array(path), prefixes: z.array(prefix) }),\n }),\n consent: z.object({\n termsUrl: z.string().min(1).nullable(),\n marketing: z.object({ text: z.string().min(1), version: z.string().min(1) }).nullable(),\n }),\n storageNamespace: key,\n ics: z.object({ prodId: z.string().regex(/^-\\/\\/.+\\/\\/.+$/), uidDomain: z.string().min(1) }),\n monitoring: z.object({ enabled: z.boolean(), sampleRate: z.number().min(0).max(1) }),\n labels: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),\n })\n .superRefine((config, ctx) => {\n const keys = config.categories.map((category) => category.key);\n const known = new Set(keys);\n if (known.size !== keys.length) {\n ctx.addIssue({\n code: 'custom',\n path: ['categories'],\n message: 'category keys must be unique',\n });\n }\n if (!known.has(config.fallbackCategory)) {\n ctx.addIssue({\n code: 'custom',\n path: ['fallbackCategory'],\n message: 'must be one of the categories',\n });\n }\n if (config.window.prefetchCategory !== null && !known.has(config.window.prefetchCategory)) {\n ctx.addIssue({\n code: 'custom',\n path: ['window', 'prefetchCategory'],\n message: 'must be one of the categories',\n });\n }\n config.categories.forEach((category, index) => {\n const targets =\n category.adultEquivalent === undefined\n ? []\n : typeof category.adultEquivalent === 'string'\n ? [category.adultEquivalent]\n : category.adultEquivalent.map((rule) => rule.category);\n for (const target of targets) {\n if (!known.has(target)) {\n ctx.addIssue({\n code: 'custom',\n path: ['categories', index, 'adultEquivalent'],\n message: `«${target}» is not one of the categories`,\n });\n }\n }\n });\n // Contiguous and covering the whole day, so every hour has exactly one part.\n let hour = 0;\n config.dayparts.forEach((part, index) => {\n if (part.from !== hour || part.to <= part.from) {\n ctx.addIssue({\n code: 'custom',\n path: ['dayparts', index],\n message: 'dayparts must run contiguously from 0 to 24',\n });\n }\n hour = part.to;\n });\n if (hour !== 24) {\n ctx.addIssue({ code: 'custom', path: ['dayparts'], message: 'dayparts must end at 24' });\n }\n if (config.portal.methods.length === 0 && config.portal.enabled) {\n ctx.addIssue({\n code: 'custom',\n path: ['portal', 'methods'],\n message: 'an enabled portal needs a login method',\n });\n }\n });\n\nfunction deepFreeze<T>(value: T): T {\n if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n Object.freeze(value);\n for (const child of Object.values(value)) deepFreeze(child);\n }\n return value;\n}\n\n/** Thrown with zod's issue list when a config does not validate. */\nexport class BookingConfigError extends Error {\n readonly issues: ReadonlyArray<{ path: string; message: string }>;\n\n constructor(issues: ReadonlyArray<{ path: string; message: string }>) {\n super(\n `Invalid booking config:\\n${issues.map((issue) => ` ${issue.path}: ${issue.message}`).join('\\n')}`\n );\n this.name = 'BookingConfigError';\n this.issues = issues;\n }\n}\n\n/**\n * The site's partial config with every default filled, validated and frozen.\n *\n * The portal's three secondary cookie names derive from `cookieName` unless\n * set, and a login may return to the booking page unless `returnPaths` says\n * otherwise. Idempotent: a resolved config resolves to an equal one.\n */\nexport function resolveBookingConfig(input: BookingConfigInput): Readonly<BookingConfig> {\n const merged = merge(defaults(), input) as Omit<BookingConfig, 'portal'> & {\n portal: Partial<BookingConfig['portal']> & Pick<BookingConfig['portal'], 'cookieName'>;\n };\n const session = merged.portal.cookieName;\n const candidate = {\n ...merged,\n portal: {\n ...merged.portal,\n nextCookieName: merged.portal.nextCookieName ?? `${session}_next`,\n vippsBindingCookieName:\n merged.portal.vippsBindingCookieName ?? `__Host-${session}_vipps_bind`,\n vippsLinkCookieName: merged.portal.vippsLinkCookieName ?? `${session}_vipps_link`,\n returnPaths: {\n exact: merged.portal.returnPaths?.exact ?? [merged.paths.booking],\n prefixes: merged.portal.returnPaths?.prefixes ?? [],\n },\n },\n };\n const derived = (Object.keys(DERIVED_COOKIE_NAMES) as DerivedCookieName[]).filter(\n (name) => merged.portal[name] === undefined\n );\n const result = schema.safeParse(candidate);\n if (!result.success) {\n const lengthIssue = derivedCookieNameIssue(session, derived);\n const issues = result.error.issues\n .map((issue) => ({ path: issue.path.join('.'), message: issue.message }))\n // The derived names' own complaints are the base name's, said once.\n .filter(\n (issue) => lengthIssue === null || !derived.some((name) => issue.path === `portal.${name}`)\n );\n throw new BookingConfigError(lengthIssue === null ? issues : [lengthIssue, ...issues]);\n }\n return deepFreeze(result.data as BookingConfig);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BookingConfig } from "./config.mjs";
|
|
2
|
+
//#region src/core/consent.d.ts
|
|
3
|
+
/** How the record says where the consent came from — the same word the CRM
|
|
4
|
+
* files the contact itself under. */
|
|
5
|
+
export declare const CONSENT_SOURCE = "booking";
|
|
6
|
+
export interface MarketingConsent {
|
|
7
|
+
text: string;
|
|
8
|
+
version: string;
|
|
9
|
+
source: typeof CONSENT_SOURCE;
|
|
10
|
+
}
|
|
11
|
+
/** The site's marketing consent, or `null` when it offers no marketing box. */
|
|
12
|
+
export declare function marketingConsent(config: Pick<BookingConfig, 'consent'>): MarketingConsent | null;
|
|
13
|
+
//#endregion
|
|
14
|
+
//# sourceMappingURL=consent.d.mts.map
|