@mrcharles/sdk 0.0.0-stage → 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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/env.ts", "../src/errors.ts", "../src/http.ts", "../src/links.ts", "../src/embed.ts", "../src/uuid.ts", "../src/slots.ts", "../src/rooms.ts", "../src/client.ts", "../src/index.ts"],
4
+ "sourcesContent": [
5
+ "export type Environment = 'production';\n\nexport interface Endpoints {\n /** e.g. https://api.mrcharles.app/api/v1 */\n apiBase: string;\n /** e.g. https://app.mrcharles.app — where lessons are booked */\n appBase: string;\n /** e.g. sites.mrcharles.app — hosted studio sites live at <slug>.<sitesBase> */\n sitesBase: string;\n}\n\n/**\n * There is one public environment. Testing happens against a studio site in\n * sandbox mode on that same API — see developers.mrcharles.app/getting-started/sandbox —\n * so an integration never has to change hosts between development and launch.\n */\nexport const ENVIRONMENTS: Record<Environment, Endpoints> = {\n production: {\n apiBase: 'https://api.mrcharles.app/api/v1',\n appBase: 'https://app.mrcharles.app',\n sitesBase: 'sites.mrcharles.app',\n },\n};\n\nexport interface ClientConfig {\n /** Only 'production' exists; kept so a future environment is a non-breaking addition. */\n env?: Environment;\n /** Endpoint overrides — for Mr Charles' own deployments and tests, not for integrators. */\n apiBase?: string;\n appBase?: string;\n sitesBase?: string;\n /** Custom fetch (tests, server runtimes, proxies). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n /** Sent on every request as Accept-Language; the hold's own `locale` wins for messages. */\n locale?: string;\n /** Extra headers, e.g. X-Forwarded-For when calling from your server on a guest's behalf. */\n headers?: Record<string, string>;\n}\n\nexport function resolveEndpoints(config: ClientConfig = {}): Endpoints {\n const base = ENVIRONMENTS[config.env ?? 'production'];\n return {\n apiBase: trimSlash(config.apiBase ?? base.apiBase),\n appBase: trimSlash(config.appBase ?? base.appBase),\n sitesBase: (config.sitesBase ?? base.sitesBase).replace(/^https?:\\/\\//, '').replace(/\\/+$/, ''),\n };\n}\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '');\n}\n",
6
+ "/** Stable error codes of the public API. Branch on these; show `message` as-is. */\nexport type ErrorCode =\n | 'VALIDATION_ERROR'\n | 'RESOURCE_NOT_FOUND'\n | 'SITE_NOT_LIVE'\n | 'ROOM_NOT_BOOKABLE'\n | 'STUDIO_NOT_VERIFIED'\n | 'OUTSIDE_HOURS'\n | 'OUTSIDE_BOOKING_WINDOW'\n | 'SLOT_TAKEN'\n | 'HOLD_EXPIRED'\n | 'HOLD_NOT_VERIFIED'\n | 'HOLD_ALREADY_CONFIRMED'\n | 'CODE_INVALID'\n | 'TOO_MANY_ATTEMPTS'\n | 'RATE_LIMIT_EXCEEDED'\n | 'CAPTCHA_FAILED'\n | 'SMS_UNAVAILABLE'\n | 'PAYMENTS_UNAVAILABLE'\n | 'PAYMENT_MODE_DENIED'\n | 'CANCEL_TOO_LATE'\n | 'INVALID_STATE'\n | 'INTERNAL_ERROR'\n | 'NETWORK_ERROR'\n | (string & {});\n\nexport class MrCharlesError extends Error {\n readonly code: ErrorCode;\n readonly status: number;\n readonly requestId?: string;\n\n constructor(code: ErrorCode, message: string, status = 0, requestId?: string) {\n super(message);\n this.name = 'MrCharlesError';\n this.code = code;\n this.status = status;\n this.requestId = requestId;\n }\n\n /** The hold is gone — start the flow again with a fresh idempotency key. */\n get holdIsDead(): boolean {\n return this.code === 'HOLD_EXPIRED' || this.code === 'TOO_MANY_ATTEMPTS';\n }\n\n /** The slot is no longer available — refresh availability, keep the guest's details. */\n get slotIsGone(): boolean {\n return this.code === 'SLOT_TAKEN' || this.code === 'OUTSIDE_HOURS' || this.code === 'OUTSIDE_BOOKING_WINDOW';\n }\n\n /** Worth retrying the same request, with the same idempotency key. */\n get isTransient(): boolean {\n return this.code === 'NETWORK_ERROR' || this.status >= 500;\n }\n}\n\nexport function isMrCharlesError(e: unknown): e is MrCharlesError {\n return e instanceof MrCharlesError || (typeof e === 'object' && e !== null && (e as { name?: string }).name === 'MrCharlesError');\n}\n",
7
+ "import { MrCharlesError } from './errors';\nimport type { ClientConfig } from './env';\n\nexport interface Http {\n get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;\n post<T>(path: string, body?: unknown): Promise<T>;\n}\n\n/** Envelope-aware fetch against the API: unwraps `data`, throws MrCharlesError. */\nexport function createHttp(apiBase: string, config: ClientConfig): Http {\n const resolved = config.fetch ?? (typeof fetch === 'function' ? fetch.bind(globalThis) : undefined);\n if (!resolved) throw new Error('@mrcharles/sdk: no fetch available — pass { fetch } in the client config');\n const doFetch: typeof fetch = resolved;\n\n async function request<T>(method: 'GET' | 'POST', path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T> {\n let url = apiBase + path;\n if (params) {\n const q = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== '') q.set(k, String(v));\n const s = q.toString();\n if (s) url += (url.includes('?') ? '&' : '?') + s;\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...(config.headers ?? {}) };\n if (config.locale) headers['Accept-Language'] = config.locale;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await doFetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, cache: 'no-store' });\n } catch (e) {\n throw new MrCharlesError('NETWORK_ERROR', (e as Error)?.message || 'Network error', 0);\n }\n\n const requestId = res.headers?.get?.('x-request-id') ?? undefined;\n const text = await res.text();\n let json: unknown = null;\n try {\n json = text ? JSON.parse(text) : null;\n } catch {\n json = null;\n }\n const envelope = (json && typeof json === 'object' ? json : {}) as { success?: boolean; data?: unknown; error?: { code?: string; message?: string } };\n\n if (!res.ok || envelope.success === false) {\n const code = envelope.error?.code ?? (res.status === 429 ? 'RATE_LIMIT_EXCEEDED' : res.status === 404 ? 'RESOURCE_NOT_FOUND' : 'INTERNAL_ERROR');\n const message = envelope.error?.message ?? res.statusText ?? 'Request failed';\n throw new MrCharlesError(code, message, res.status, requestId);\n }\n return ('data' in envelope ? envelope.data : json) as T;\n }\n\n return {\n get: (path, params) => request('GET', path, undefined, params),\n post: (path, body) => request('POST', path, body ?? {}),\n };\n}\n",
8
+ "import type { Endpoints } from './env';\nimport type { Locale } from './types';\n\nexport interface RoomLinkOptions {\n slug: string;\n roomId: string;\n /** YYYY-MM-DD in the studio's zone */\n date?: string;\n /** HH:MM */\n start?: string;\n durationMinutes?: number;\n lang?: Locale;\n /** Hide the hosted site's header and footer (for iframes) */\n embed?: boolean;\n}\n\nexport function createLinks(endpoints: Endpoints) {\n const siteOrigin = (slug: string) => `https://${slug}.${endpoints.sitesBase}`;\n\n return {\n /** The studio's hosted site */\n site(slug: string): string {\n return `${siteOrigin(slug)}/`;\n },\n\n /** Hosted booking screen for a room, optionally pre-filled */\n bookRoom(o: RoomLinkOptions): string {\n const q = new URLSearchParams();\n if (o.date) q.set('date', o.date);\n if (o.start) q.set('start', o.start);\n if (o.durationMinutes) q.set('duration', String(o.durationMinutes));\n if (o.lang) q.set('lang', o.lang);\n if (o.embed) q.set('embed', '1');\n const s = q.toString();\n return `${siteOrigin(o.slug)}/reservar/${o.roomId}${s ? `?${s}` : ''}`;\n },\n\n /** A guest's booking page (cancel, details) from the manage token */\n manage(slug: string, manageToken: string): string {\n return `${siteOrigin(slug)}/r/${manageToken}`;\n },\n\n /**\n * Book a lesson in the Mr Charles app. Students need an account there;\n * this is the hand-off. `instructorId` is the teacher's **user** id.\n */\n bookLesson(o: { offerId?: string; instructorId: string }): string {\n const q = new URLSearchParams({ instructorId: o.instructorId });\n if (o.offerId) q.set('offerId', o.offerId);\n return `${endpoints.appBase}/bookings/book?${q.toString()}`;\n },\n\n /** The teacher's public profile in the app */\n teacherProfile(instructorUserId: string): string {\n return `${endpoints.appBase}/instructors/${instructorUserId}`;\n },\n };\n}\n\nexport type Links = ReturnType<typeof createLinks>;\n",
9
+ "import type { Endpoints } from './env';\nimport { createLinks, type RoomLinkOptions } from './links';\nimport type { EmbedBookedEvent, EmbedEvent } from './types';\n\nexport interface EmbedOptions extends Omit<RoomLinkOptions, 'embed'> {\n /** Called once when the guest completes a booking inside the frame */\n onBooked?: (e: EmbedBookedEvent) => void;\n /** Called on every height change; default behaviour sets the iframe height */\n onResize?: (height: number) => void;\n /** Initial height before the first resize message (px). Default 760. */\n height?: number;\n /** Set to false to keep a fixed height instead of following the content */\n autoResize?: boolean;\n title?: string;\n className?: string;\n}\n\nexport interface EmbedHandle {\n iframe: HTMLIFrameElement;\n /** Remove the iframe and the message listener */\n destroy(): void;\n}\n\n/** True when a postMessage came from the hosted sites domain of this environment. */\nexport function isTrustedOrigin(origin: string, sitesBase: string): boolean {\n try {\n const host = new URL(origin).host;\n return host === sitesBase || host.endsWith(`.${sitesBase}`);\n } catch {\n return false;\n }\n}\n\nexport function parseEmbedEvent(data: unknown): EmbedEvent | null {\n if (!data || typeof data !== 'object') return null;\n const d = data as Record<string, unknown>;\n if (d.type === 'mrcharles:resize' && typeof d.height === 'number') return { type: 'mrcharles:resize', height: d.height };\n if (d.type === 'mrcharles:booked' && typeof d.reference === 'string') {\n return {\n type: 'mrcharles:booked',\n reference: d.reference,\n bookingId: String(d.bookingId ?? ''),\n manageUrl: String(d.manageUrl ?? ''),\n status: String(d.status ?? ''),\n };\n }\n return null;\n}\n\nexport function createEmbed(endpoints: Endpoints) {\n const links = createLinks(endpoints);\n\n /**\n * Drop the hosted booking screen for a room into `target`. Resizes itself\n * to the content and reports the booking when it happens. Nothing else to\n * wire: codes, payments and the manage link are handled inside the frame.\n */\n function mountRoomBooking(target: HTMLElement | string, opts: EmbedOptions): EmbedHandle {\n const el = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (!el) throw new Error(`@mrcharles/sdk: embed target not found: ${String(target)}`);\n\n const iframe = document.createElement('iframe');\n iframe.src = links.bookRoom({ ...opts, embed: true });\n iframe.title = opts.title ?? 'Mr Charles booking';\n iframe.loading = 'lazy';\n iframe.referrerPolicy = 'strict-origin-when-cross-origin';\n iframe.setAttribute('allow', 'payment');\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = 'block';\n iframe.style.height = `${opts.height ?? 760}px`;\n if (opts.className) iframe.className = opts.className;\n\n const expectedOrigin = new URL(iframe.src).origin;\n const onMessage = (e: MessageEvent) => {\n if (e.source !== iframe.contentWindow) return;\n if (e.origin !== expectedOrigin && !isTrustedOrigin(e.origin, endpoints.sitesBase)) return;\n const ev = parseEmbedEvent(e.data);\n if (!ev) return;\n if (ev.type === 'mrcharles:resize') {\n if (opts.autoResize !== false) iframe.style.height = `${ev.height}px`;\n opts.onResize?.(ev.height);\n } else {\n opts.onBooked?.(ev);\n }\n };\n window.addEventListener('message', onMessage);\n el.appendChild(iframe);\n\n return {\n iframe,\n destroy() {\n window.removeEventListener('message', onMessage);\n iframe.remove();\n },\n };\n }\n\n return { mountRoomBooking };\n}\n",
10
+ "/** RFC 4122 v4, with a fallback for runtimes without crypto.randomUUID. */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n const b = new Uint8Array(16);\n if (c?.getRandomValues) c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6] & 0x0f) | 0x40;\n b[8] = (b[8] & 0x3f) | 0x80;\n const h = [...b].map((x) => x.toString(16).padStart(2, '0')).join('');\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n",
11
+ "import type { FreeSpan } from './types';\n\n/** \"17:30\" → 1050 */\nexport function toMinutes(hhmm: string): number {\n const [h, m] = hhmm.split(':').map(Number);\n return (h || 0) * 60 + (m || 0);\n}\n\n/** 1050 → \"17:30\" */\nexport function toHHMM(minutes: number): string {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\n}\n\nexport function addMinutes(hhmm: string, minutes: number): string {\n return toHHMM(toMinutes(hhmm) + minutes);\n}\n\n/**\n * Every start time at which a booking of `durationMinutes` fits inside one\n * of the free spans, on the `stepMinutes` grid. Sorted, de-duplicated.\n */\nexport function startTimes(free: FreeSpan[], durationMinutes: number, stepMinutes: number): string[] {\n const step = Math.max(1, stepMinutes);\n const out = new Set<string>();\n for (const span of free) {\n const s = toMinutes(span.start);\n const e = toMinutes(span.end);\n for (let t = Math.ceil(s / step) * step; t + durationMinutes <= e; t += step) out.add(toHHMM(t));\n }\n return [...out].sort((a, b) => toMinutes(a) - toMinutes(b));\n}\n\n/** Durations between min and max, on the step grid, that still fit after `start`. */\nexport function durationOptions(free: FreeSpan[], start: string, minMinutes: number, maxMinutes: number, stepMinutes: number): number[] {\n const s = toMinutes(start);\n const span = free.find((f) => toMinutes(f.start) <= s && s < toMinutes(f.end));\n if (!span) return [];\n const room = toMinutes(span.end) - s;\n const out: number[] = [];\n for (let d = minMinutes; d <= maxMinutes && d <= room; d += Math.max(1, stepMinutes)) out.push(d);\n return out;\n}\n\n/** 2500, \"EUR\" → \"25,00 €\" (locale-aware; minor units in, string out). */\nexport function formatPrice(cents: number, currency: string, locale = 'es-ES'): string {\n try {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(cents / 100);\n } catch {\n return `${(cents / 100).toFixed(2)} ${currency}`;\n }\n}\n\n/** \"2026-10-12\" for a Date, in the given IANA zone (today in the studio's zone). */\nexport function dateKey(date: Date, timeZone: string): string {\n const parts = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date);\n const get = (t: string) => parts.find((p) => p.type === t)?.value ?? '';\n return `${get('year')}-${get('month')}-${get('day')}`;\n}\n\n/** YYYY-MM-DD plus n days, calendar arithmetic only (no zone involved). */\nexport function addDays(key: string, days: number): string {\n const [y, m, d] = key.split('-').map(Number);\n const t = Date.UTC(y, m - 1, d + days);\n return new Date(t).toISOString().slice(0, 10);\n}\n",
12
+ "import type { Http } from './http';\nimport { MrCharlesError, isMrCharlesError } from './errors';\nimport { uuid } from './uuid';\nimport { addDays, dateKey, durationOptions, startTimes } from './slots';\nimport type { Links } from './links';\nimport type { Channel, ConfirmRequest, Confirmation, Hold, HoldRequest, Locale, PublicRoom, PublicSite, RoomAvailability } from './types';\n\nexport type RoomStep = 'slot' | 'details' | 'verify' | 'pay' | 'done' | 'expired';\n\nexport interface RoomBookingState {\n step: RoomStep;\n site: PublicSite | null;\n room: PublicRoom | null;\n availability: RoomAvailability | null;\n hold: Hold | null;\n confirmation: Confirmation | null;\n /** Last error from any action; cleared on the next successful one */\n error: MrCharlesError | null;\n busy: boolean;\n}\n\nexport interface GuestDetails {\n date: string;\n startTime: string;\n durationMinutes: number;\n guestName: string;\n channel: Channel;\n contact: string;\n locale?: Locale;\n captchaToken?: string;\n}\n\nexport interface RoomBookingOptions {\n slug: string;\n roomId: string;\n /** Called on every state change */\n onChange?: (state: RoomBookingState) => void;\n}\n\nexport interface RoomBooking {\n readonly state: RoomBookingState;\n subscribe(fn: (state: RoomBookingState) => void): () => void;\n\n /** Site payload (cached 60 s server-side). Resolves the room too. */\n loadSite(): Promise<PublicSite>;\n /** Free spans for a window; defaults to the next 14 days from today in the studio's zone. */\n loadAvailability(window?: { from?: string; to?: string; days?: number }): Promise<RoomAvailability>;\n /** Start times for a date and duration, from the loaded availability. */\n startTimesFor(date: string, durationMinutes: number): string[];\n /** Durations that fit after a start time, from the loaded availability. */\n durationsFor(date: string, startTime: string): number[];\n\n /** Reserve the slot and send the code. One idempotency key per attempt is handled for you. */\n hold(details: GuestDetails): Promise<Hold>;\n verify(code: string): Promise<Hold>;\n resend(): Promise<Hold>;\n confirm(req: ConfirmRequest): Promise<Confirmation>;\n\n /** Milliseconds until the hold expires; 0 when there is no live hold. */\n remainingMs(): number;\n /** Forget the hold and start over (new idempotency key). Keeps site and availability. */\n reset(): void;\n destroy(): void;\n}\n\n/**\n * The whole guest flow for one room as a small state machine: availability →\n * hold → code → confirm, with the expiry timer, idempotency key and error\n * classification handled. Render it with any UI.\n */\nexport function createRoomBooking(http: Http, links: Links, opts: RoomBookingOptions): RoomBooking {\n let state: RoomBookingState = { step: 'slot', site: null, room: null, availability: null, hold: null, confirmation: null, error: null, busy: false };\n const listeners = new Set<(s: RoomBookingState) => void>();\n if (opts.onChange) listeners.add(opts.onChange);\n let idempotencyKey = uuid();\n let expiryTimer: ReturnType<typeof setTimeout> | undefined;\n\n const set = (patch: Partial<RoomBookingState>) => {\n state = { ...state, ...patch };\n for (const l of listeners) l(state);\n };\n\n const clearTimer = () => {\n if (expiryTimer) clearTimeout(expiryTimer);\n expiryTimer = undefined;\n };\n\n const armTimer = (hold: Hold) => {\n clearTimer();\n const ms = new Date(hold.expiresAt).getTime() - Date.now();\n if (ms <= 0) return;\n expiryTimer = setTimeout(() => {\n if (state.step === 'verify' || state.step === 'pay') {\n idempotencyKey = uuid();\n set({ step: 'expired', error: new MrCharlesError('HOLD_EXPIRED', 'The hold expired. Pick a time again.', 410) });\n }\n }, ms);\n };\n\n async function run<T>(fn: () => Promise<T>): Promise<T> {\n set({ busy: true });\n try {\n const out = await fn();\n set({ busy: false, error: null });\n return out;\n } catch (e) {\n const err = isMrCharlesError(e) ? e : new MrCharlesError('INTERNAL_ERROR', (e as Error)?.message ?? 'Unexpected error');\n if (err.holdIsDead) {\n clearTimer();\n idempotencyKey = uuid();\n set({ busy: false, error: err, step: 'expired', hold: null });\n } else {\n set({ busy: false, error: err });\n }\n throw err;\n }\n }\n\n const api: RoomBooking = {\n get state() {\n return state;\n },\n subscribe(fn) {\n listeners.add(fn);\n fn(state);\n return () => {\n listeners.delete(fn);\n };\n },\n\n loadSite: () =>\n run(async () => {\n const site = await http.get<PublicSite>(`/public/sites/${encodeURIComponent(opts.slug)}`);\n const room = site.rooms.find((r) => r.id === opts.roomId) ?? null;\n if (!room) throw new MrCharlesError('RESOURCE_NOT_FOUND', `Room ${opts.roomId} is not on site ${opts.slug}`, 404);\n if (!room.bookable) throw new MrCharlesError('ROOM_NOT_BOOKABLE', `${room.name} does not take online bookings right now`, 409);\n set({ site, room });\n return site;\n }),\n\n loadAvailability: (window = {}) =>\n run(async () => {\n const tz = state.site?.studio.timezone ?? 'UTC';\n const from = window.from ?? dateKey(new Date(), tz);\n const to = window.to ?? addDays(from, Math.min(31, Math.max(1, window.days ?? 14)) - 1);\n const availability = await http.get<RoomAvailability>(\n `/public/sites/${encodeURIComponent(opts.slug)}/rooms/${encodeURIComponent(opts.roomId)}/availability`,\n { from, to },\n );\n set({ availability });\n return availability;\n }),\n\n startTimesFor(date, durationMinutes) {\n const a = state.availability;\n if (!a) return [];\n const day = a.days.find((d) => d.date === date);\n return day ? startTimes(day.free, durationMinutes, a.stepMinutes) : [];\n },\n\n durationsFor(date, startTime) {\n const a = state.availability;\n if (!a) return [];\n const day = a.days.find((d) => d.date === date);\n return day ? durationOptions(day.free, startTime, a.minMinutes, a.maxMinutes, a.stepMinutes) : [];\n },\n\n hold: (details) =>\n run(async () => {\n if (state.step === 'expired') set({ step: 'slot', error: null });\n const body: HoldRequest = {\n roomId: opts.roomId,\n date: details.date,\n startTime: details.startTime,\n durationMinutes: details.durationMinutes,\n guestName: details.guestName,\n channel: details.channel,\n contact: details.contact,\n locale: details.locale ?? state.site?.site.defaultLocale ?? 'es',\n idempotencyKey,\n ...(details.captchaToken ? { captchaToken: details.captchaToken } : {}),\n };\n const hold = await http.post<Hold>(`/public/sites/${encodeURIComponent(opts.slug)}/holds`, body);\n armTimer(hold);\n set({ hold, step: 'verify', confirmation: null });\n return hold;\n }),\n\n verify: (code) =>\n run(async () => {\n const id = requireHold(state).holdId;\n const hold = await http.post<Hold>(`/public/holds/${id}/verify`, { code: code.replace(/\\D/g, '') });\n set({ hold, step: 'pay' });\n return hold;\n }),\n\n resend: () =>\n run(async () => {\n const id = requireHold(state).holdId;\n const hold = await http.post<Hold>(`/public/holds/${id}/resend`);\n set({ hold });\n return hold;\n }),\n\n confirm: (req) =>\n run(async () => {\n const h = requireHold(state);\n if (!h.paymentModes.includes(req.paymentMode)) {\n throw new MrCharlesError('PAYMENT_MODE_DENIED', `This room does not accept ${req.paymentMode}`, 409);\n }\n const confirmation = await http.post<Confirmation>(`/public/holds/${h.holdId}/confirm`, {\n paymentMode: req.paymentMode,\n notes: req.notes ?? '',\n email: req.email ?? '',\n });\n clearTimer();\n if (!confirmation.manageUrl && confirmation.manageToken) confirmation.manageUrl = links.manage(opts.slug, confirmation.manageToken);\n set({ confirmation, step: 'done' });\n return confirmation;\n }),\n\n remainingMs() {\n const h = state.hold;\n if (!h || state.step === 'done' || state.step === 'expired') return 0;\n return Math.max(0, new Date(h.expiresAt).getTime() - Date.now());\n },\n\n reset() {\n clearTimer();\n idempotencyKey = uuid();\n set({ step: 'slot', hold: null, confirmation: null, error: null, busy: false });\n },\n\n destroy() {\n clearTimer();\n listeners.clear();\n },\n };\n\n return api;\n}\n\nfunction requireHold(s: RoomBookingState): Hold {\n if (!s.hold) throw new MrCharlesError('INVALID_STATE', 'No hold yet — call hold() first', 409);\n return s.hold;\n}\n",
13
+ "import { type ClientConfig, type Endpoints, resolveEndpoints } from './env';\nimport { createHttp } from './http';\nimport { createLinks } from './links';\nimport { createEmbed } from './embed';\nimport { createRoomBooking, type RoomBookingOptions } from './rooms';\nimport type {\n Booking, ConfirmRequest, Confirmation, Hold, HoldRequest, LessonOffer, LessonPolicy, LessonSlots,\n PublicSite, RoomAvailability, TeacherProfile, TeacherWindows,\n} from './types';\n\nexport function createClient(config: ClientConfig = {}) {\n const endpoints: Endpoints = resolveEndpoints(config);\n const http = createHttp(endpoints.apiBase, config);\n const links = createLinks(endpoints);\n const embed = createEmbed(endpoints);\n const enc = encodeURIComponent;\n\n const sites = {\n /** Everything needed to render a studio: rooms, prices, hours, payment and captcha keys. */\n get: (slug: string) => http.get<PublicSite>(`/public/sites/${enc(slug)}`),\n /** The site behind a custom domain. */\n resolve: (host: string) => http.get<PublicSite>('/public/sites/resolve', { host }),\n /** Free spans for a room between two dates (≤ 31 days), in the studio's zone. Never cache. */\n availability: (slug: string, roomId: string, range: { from: string; to: string }) =>\n http.get<RoomAvailability>(`/public/sites/${enc(slug)}/rooms/${enc(roomId)}/availability`, range),\n };\n\n const holds = {\n create: (slug: string, req: HoldRequest) => http.post<Hold>(`/public/sites/${enc(slug)}/holds`, req),\n verify: (holdId: string, code: string) => http.post<Hold>(`/public/holds/${enc(holdId)}/verify`, { code }),\n resend: (holdId: string) => http.post<Hold>(`/public/holds/${enc(holdId)}/resend`),\n confirm: (holdId: string, req: ConfirmRequest) =>\n http.post<Confirmation>(`/public/holds/${enc(holdId)}/confirm`, { paymentMode: req.paymentMode, notes: req.notes ?? '', email: req.email ?? '' }),\n };\n\n const bookings = {\n /** A guest's booking from its manage token. Poll this after a card payment. */\n get: (manageToken: string) => http.get<Booking>(`/public/bookings/manage/${enc(manageToken)}`),\n cancel: (manageToken: string, reason = '') => http.post<Booking>(`/public/bookings/manage/${enc(manageToken)}/cancel`, { reason }),\n };\n\n const teachers = {\n /** Public teacher profile. `instructorUserId` is the teacher's user id; the response's `user.id` is the one to keep. */\n profile: (instructorUserId: string) => http.get<TeacherProfile>(`/instructors/${enc(instructorUserId)}/teacher-profile`),\n /** Published lesson offers. */\n offers: (instructorUserId: string) => http.get<LessonOffer[]>(`/instructors/${enc(instructorUserId)}/offers`),\n offer: (offerId: string) => http.get<LessonOffer>(`/offers/${enc(offerId)}`),\n /** Free windows in the teacher's zone; use `slots` for something bookable. */\n windows: (instructorUserId: string, range: { from: string; to: string }) =>\n http.get<TeacherWindows>(`/availability/${enc(instructorUserId)}/windows`, range),\n policy: (instructorUserId: string) => http.get<LessonPolicy>(`/availability/${enc(instructorUserId)}/policy`),\n /** Concrete bookable start times for an offer — duration, policy and existing lessons applied. */\n slots: (q: { instructorUserId: string; offerId: string; date: string; days?: number }) =>\n http.get<LessonSlots>('/bookings/slots', { instructorId: q.instructorUserId, offerId: q.offerId, date: q.date, days: q.days ?? 7 }),\n };\n\n return {\n endpoints,\n sites,\n holds,\n bookings,\n teachers,\n links,\n /** Drop our hosted booking screen into your page. */\n mountRoomBooking: embed.mountRoomBooking,\n /** The headless flow for one room: availability → hold → code → confirm. */\n roomBooking: (opts: RoomBookingOptions) => createRoomBooking(http, links, opts),\n };\n}\n\nexport type MrCharlesClient = ReturnType<typeof createClient>;\n",
14
+ "export { createClient, type MrCharlesClient } from './client';\nexport { MrCharlesError, isMrCharlesError, type ErrorCode } from './errors';\nexport { ENVIRONMENTS, resolveEndpoints, type ClientConfig, type Endpoints, type Environment } from './env';\nexport { createRoomBooking, type RoomBooking, type RoomBookingOptions, type RoomBookingState, type RoomStep, type GuestDetails } from './rooms';\nexport { createLinks, type Links, type RoomLinkOptions } from './links';\nexport { createEmbed, isTrustedOrigin, parseEmbedEvent, type EmbedHandle, type EmbedOptions } from './embed';\nexport { startTimes, durationOptions, toMinutes, toHHMM, addMinutes, formatPrice, dateKey, addDays } from './slots';\nexport { uuid } from './uuid';\nexport type * from './types';\n\nexport const VERSION = '0.1.0';\n"
15
+ ],
16
+ "mappings": "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",
17
+ "debugId": "147361D96F83431064756E2164756E21",
18
+ "names": []
19
+ }
@@ -0,0 +1,4 @@
1
+ (()=>{var W=Object.defineProperty;var q=(t)=>t;function z(t,o){this[t]=q.bind(null,o)}var j=(t,o)=>{for(var e in o)W(t,e,{get:o[e],enumerable:!0,configurable:!0,set:z.bind(o,e)})};var A={};j(A,{ENVIRONMENTS:()=>B,MrCharlesError:()=>f,VERSION:()=>G,addDays:()=>v,addMinutes:()=>U,createClient:()=>V,createEmbed:()=>I,createLinks:()=>R,createRoomBooking:()=>w,dateKey:()=>M,durationOptions:()=>x,formatPrice:()=>F,isMrCharlesError:()=>k,isTrustedOrigin:()=>N,parseEmbedEvent:()=>D,resolveEndpoints:()=>O,startTimes:()=>C,toHHMM:()=>S,toMinutes:()=>b,uuid:()=>E});var B={production:{apiBase:"https://api.mrcharles.app/api/v1",appBase:"https://app.mrcharles.app",sitesBase:"sites.mrcharles.app"}};function O(t={}){let o=B[t.env??"production"];return{apiBase:P(t.apiBase??o.apiBase),appBase:P(t.appBase??o.appBase),sitesBase:(t.sitesBase??o.sitesBase).replace(/^https?:\/\//,"").replace(/\/+$/,"")}}function P(t){return t.replace(/\/+$/,"")}class f extends Error{code;status;requestId;constructor(t,o,e=0,n){super(o);this.name="MrCharlesError",this.code=t,this.status=e,this.requestId=n}get holdIsDead(){return this.code==="HOLD_EXPIRED"||this.code==="TOO_MANY_ATTEMPTS"}get slotIsGone(){return this.code==="SLOT_TAKEN"||this.code==="OUTSIDE_HOURS"||this.code==="OUTSIDE_BOOKING_WINDOW"}get isTransient(){return this.code==="NETWORK_ERROR"||this.status>=500}}function k(t){return t instanceof f||typeof t==="object"&&t!==null&&t.name==="MrCharlesError"}function H(t,o){let e=o.fetch??(typeof fetch==="function"?fetch.bind(globalThis):void 0);if(!e)throw Error("@mrcharles/sdk: no fetch available — pass { fetch } in the client config");let n=e;async function d(s,l,m,u){let g=t+l;if(u){let h=new URLSearchParams;for(let[K,L]of Object.entries(u))if(L!==void 0&&L!=="")h.set(K,String(L));let T=h.toString();if(T)g+=(g.includes("?")?"&":"?")+T}let r={Accept:"application/json",...o.headers??{}};if(o.locale)r["Accept-Language"]=o.locale;if(m!==void 0)r["Content-Type"]="application/json";let p;try{p=await n(g,{method:s,headers:r,body:m!==void 0?JSON.stringify(m):void 0,cache:"no-store"})}catch(h){throw new f("NETWORK_ERROR",h?.message||"Network error",0)}let i=p.headers?.get?.("x-request-id")??void 0,c=await p.text(),a=null;try{a=c?JSON.parse(c):null}catch{a=null}let y=a&&typeof a==="object"?a:{};if(!p.ok||y.success===!1){let h=y.error?.code??(p.status===429?"RATE_LIMIT_EXCEEDED":p.status===404?"RESOURCE_NOT_FOUND":"INTERNAL_ERROR"),T=y.error?.message??p.statusText??"Request failed";throw new f(h,T,p.status,i)}return"data"in y?y.data:a}return{get:(s,l)=>d("GET",s,void 0,l),post:(s,l)=>d("POST",s,l??{})}}function R(t){let o=(e)=>`https://${e}.${t.sitesBase}`;return{site(e){return`${o(e)}/`},bookRoom(e){let n=new URLSearchParams;if(e.date)n.set("date",e.date);if(e.start)n.set("start",e.start);if(e.durationMinutes)n.set("duration",String(e.durationMinutes));if(e.lang)n.set("lang",e.lang);if(e.embed)n.set("embed","1");let d=n.toString();return`${o(e.slug)}/reservar/${e.roomId}${d?`?${d}`:""}`},manage(e,n){return`${o(e)}/r/${n}`},bookLesson(e){let n=new URLSearchParams({instructorId:e.instructorId});if(e.offerId)n.set("offerId",e.offerId);return`${t.appBase}/bookings/book?${n.toString()}`},teacherProfile(e){return`${t.appBase}/instructors/${e}`}}}function N(t,o){try{let e=new URL(t).host;return e===o||e.endsWith(`.${o}`)}catch{return!1}}function D(t){if(!t||typeof t!=="object")return null;let o=t;if(o.type==="mrcharles:resize"&&typeof o.height==="number")return{type:"mrcharles:resize",height:o.height};if(o.type==="mrcharles:booked"&&typeof o.reference==="string")return{type:"mrcharles:booked",reference:o.reference,bookingId:String(o.bookingId??""),manageUrl:String(o.manageUrl??""),status:String(o.status??"")};return null}function I(t){let o=R(t);function e(n,d){let s=typeof n==="string"?document.querySelector(n):n;if(!s)throw Error(`@mrcharles/sdk: embed target not found: ${String(n)}`);let l=document.createElement("iframe");if(l.src=o.bookRoom({...d,embed:!0}),l.title=d.title??"Mr Charles booking",l.loading="lazy",l.referrerPolicy="strict-origin-when-cross-origin",l.setAttribute("allow","payment"),l.style.width="100%",l.style.border="0",l.style.display="block",l.style.height=`${d.height??760}px`,d.className)l.className=d.className;let m=new URL(l.src).origin,u=(g)=>{if(g.source!==l.contentWindow)return;if(g.origin!==m&&!N(g.origin,t.sitesBase))return;let r=D(g.data);if(!r)return;if(r.type==="mrcharles:resize"){if(d.autoResize!==!1)l.style.height=`${r.height}px`;d.onResize?.(r.height)}else d.onBooked?.(r)};return window.addEventListener("message",u),s.appendChild(l),{iframe:l,destroy(){window.removeEventListener("message",u),l.remove()}}}return{mountRoomBooking:e}}function E(){let t=globalThis.crypto;if(t?.randomUUID)return t.randomUUID();let o=new Uint8Array(16);if(t?.getRandomValues)t.getRandomValues(o);else for(let n=0;n<16;n++)o[n]=Math.floor(Math.random()*256);o[6]=o[6]&15|64,o[8]=o[8]&63|128;let e=[...o].map((n)=>n.toString(16).padStart(2,"0")).join("");return`${e.slice(0,8)}-${e.slice(8,12)}-${e.slice(12,16)}-${e.slice(16,20)}-${e.slice(20)}`}function b(t){let[o,e]=t.split(":").map(Number);return(o||0)*60+(e||0)}function S(t){let o=Math.floor(t/60),e=t%60;return`${String(o).padStart(2,"0")}:${String(e).padStart(2,"0")}`}function U(t,o){return S(b(t)+o)}function C(t,o,e){let n=Math.max(1,e),d=new Set;for(let s of t){let l=b(s.start),m=b(s.end);for(let u=Math.ceil(l/n)*n;u+o<=m;u+=n)d.add(S(u))}return[...d].sort((s,l)=>b(s)-b(l))}function x(t,o,e,n,d){let s=b(o),l=t.find((g)=>b(g.start)<=s&&s<b(g.end));if(!l)return[];let m=b(l.end)-s,u=[];for(let g=e;g<=n&&g<=m;g+=Math.max(1,d))u.push(g);return u}function F(t,o,e="es-ES"){try{return new Intl.NumberFormat(e,{style:"currency",currency:o}).format(t/100)}catch{return`${(t/100).toFixed(2)} ${o}`}}function M(t,o){let e=new Intl.DateTimeFormat("en-CA",{timeZone:o,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(t),n=(d)=>e.find((s)=>s.type===d)?.value??"";return`${n("year")}-${n("month")}-${n("day")}`}function v(t,o){let[e,n,d]=t.split("-").map(Number),s=Date.UTC(e,n-1,d+o);return new Date(s).toISOString().slice(0,10)}function w(t,o,e){let n={step:"slot",site:null,room:null,availability:null,hold:null,confirmation:null,error:null,busy:!1},d=new Set;if(e.onChange)d.add(e.onChange);let s=E(),l,m=(i)=>{n={...n,...i};for(let c of d)c(n)},u=()=>{if(l)clearTimeout(l);l=void 0},g=(i)=>{u();let c=new Date(i.expiresAt).getTime()-Date.now();if(c<=0)return;l=setTimeout(()=>{if(n.step==="verify"||n.step==="pay")s=E(),m({step:"expired",error:new f("HOLD_EXPIRED","The hold expired. Pick a time again.",410)})},c)};async function r(i){m({busy:!0});try{let c=await i();return m({busy:!1,error:null}),c}catch(c){let a=k(c)?c:new f("INTERNAL_ERROR",c?.message??"Unexpected error");if(a.holdIsDead)u(),s=E(),m({busy:!1,error:a,step:"expired",hold:null});else m({busy:!1,error:a});throw a}}return{get state(){return n},subscribe(i){return d.add(i),i(n),()=>{d.delete(i)}},loadSite:()=>r(async()=>{let i=await t.get(`/public/sites/${encodeURIComponent(e.slug)}`),c=i.rooms.find((a)=>a.id===e.roomId)??null;if(!c)throw new f("RESOURCE_NOT_FOUND",`Room ${e.roomId} is not on site ${e.slug}`,404);if(!c.bookable)throw new f("ROOM_NOT_BOOKABLE",`${c.name} does not take online bookings right now`,409);return m({site:i,room:c}),i}),loadAvailability:(i={})=>r(async()=>{let c=n.site?.studio.timezone??"UTC",a=i.from??M(new Date,c),y=i.to??v(a,Math.min(31,Math.max(1,i.days??14))-1),h=await t.get(`/public/sites/${encodeURIComponent(e.slug)}/rooms/${encodeURIComponent(e.roomId)}/availability`,{from:a,to:y});return m({availability:h}),h}),startTimesFor(i,c){let a=n.availability;if(!a)return[];let y=a.days.find((h)=>h.date===i);return y?C(y.free,c,a.stepMinutes):[]},durationsFor(i,c){let a=n.availability;if(!a)return[];let y=a.days.find((h)=>h.date===i);return y?x(y.free,c,a.minMinutes,a.maxMinutes,a.stepMinutes):[]},hold:(i)=>r(async()=>{if(n.step==="expired")m({step:"slot",error:null});let c={roomId:e.roomId,date:i.date,startTime:i.startTime,durationMinutes:i.durationMinutes,guestName:i.guestName,channel:i.channel,contact:i.contact,locale:i.locale??n.site?.site.defaultLocale??"es",idempotencyKey:s,...i.captchaToken?{captchaToken:i.captchaToken}:{}},a=await t.post(`/public/sites/${encodeURIComponent(e.slug)}/holds`,c);return g(a),m({hold:a,step:"verify",confirmation:null}),a}),verify:(i)=>r(async()=>{let c=_(n).holdId,a=await t.post(`/public/holds/${c}/verify`,{code:i.replace(/\D/g,"")});return m({hold:a,step:"pay"}),a}),resend:()=>r(async()=>{let i=_(n).holdId,c=await t.post(`/public/holds/${i}/resend`);return m({hold:c}),c}),confirm:(i)=>r(async()=>{let c=_(n);if(!c.paymentModes.includes(i.paymentMode))throw new f("PAYMENT_MODE_DENIED",`This room does not accept ${i.paymentMode}`,409);let a=await t.post(`/public/holds/${c.holdId}/confirm`,{paymentMode:i.paymentMode,notes:i.notes??"",email:i.email??""});if(u(),!a.manageUrl&&a.manageToken)a.manageUrl=o.manage(e.slug,a.manageToken);return m({confirmation:a,step:"done"}),a}),remainingMs(){let i=n.hold;if(!i||n.step==="done"||n.step==="expired")return 0;return Math.max(0,new Date(i.expiresAt).getTime()-Date.now())},reset(){u(),s=E(),m({step:"slot",hold:null,confirmation:null,error:null,busy:!1})},destroy(){u(),d.clear()}}}function _(t){if(!t.hold)throw new f("INVALID_STATE","No hold yet — call hold() first",409);return t.hold}function V(t={}){let o=O(t),e=H(o.apiBase,t),n=R(o),d=I(o),s=encodeURIComponent;return{endpoints:o,sites:{get:(r)=>e.get(`/public/sites/${s(r)}`),resolve:(r)=>e.get("/public/sites/resolve",{host:r}),availability:(r,p,i)=>e.get(`/public/sites/${s(r)}/rooms/${s(p)}/availability`,i)},holds:{create:(r,p)=>e.post(`/public/sites/${s(r)}/holds`,p),verify:(r,p)=>e.post(`/public/holds/${s(r)}/verify`,{code:p}),resend:(r)=>e.post(`/public/holds/${s(r)}/resend`),confirm:(r,p)=>e.post(`/public/holds/${s(r)}/confirm`,{paymentMode:p.paymentMode,notes:p.notes??"",email:p.email??""})},bookings:{get:(r)=>e.get(`/public/bookings/manage/${s(r)}`),cancel:(r,p="")=>e.post(`/public/bookings/manage/${s(r)}/cancel`,{reason:p})},teachers:{profile:(r)=>e.get(`/instructors/${s(r)}/teacher-profile`),offers:(r)=>e.get(`/instructors/${s(r)}/offers`),offer:(r)=>e.get(`/offers/${s(r)}`),windows:(r,p)=>e.get(`/availability/${s(r)}/windows`,p),policy:(r)=>e.get(`/availability/${s(r)}/policy`),slots:(r)=>e.get("/bookings/slots",{instructorId:r.instructorUserId,offerId:r.offerId,date:r.date,days:r.days??7})},links:n,mountRoomBooking:d.mountRoomBooking,roomBooking:(r)=>w(e,n,r)}}var G="0.1.0";window.MrCharles=A;})();
2
+
3
+ //# debugId=7ECBFC336B75DC2F64756E2164756E21
4
+ //# sourceMappingURL=mrcharles.min.js.map
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/env.ts", "../src/errors.ts", "../src/http.ts", "../src/links.ts", "../src/embed.ts", "../src/uuid.ts", "../src/slots.ts", "../src/rooms.ts", "../src/client.ts", "../src/index.ts", "../src/global.ts"],
4
+ "sourcesContent": [
5
+ "export type Environment = 'production';\n\nexport interface Endpoints {\n /** e.g. https://api.mrcharles.app/api/v1 */\n apiBase: string;\n /** e.g. https://app.mrcharles.app — where lessons are booked */\n appBase: string;\n /** e.g. sites.mrcharles.app — hosted studio sites live at <slug>.<sitesBase> */\n sitesBase: string;\n}\n\n/**\n * There is one public environment. Testing happens against a studio site in\n * sandbox mode on that same API — see developers.mrcharles.app/getting-started/sandbox —\n * so an integration never has to change hosts between development and launch.\n */\nexport const ENVIRONMENTS: Record<Environment, Endpoints> = {\n production: {\n apiBase: 'https://api.mrcharles.app/api/v1',\n appBase: 'https://app.mrcharles.app',\n sitesBase: 'sites.mrcharles.app',\n },\n};\n\nexport interface ClientConfig {\n /** Only 'production' exists; kept so a future environment is a non-breaking addition. */\n env?: Environment;\n /** Endpoint overrides — for Mr Charles' own deployments and tests, not for integrators. */\n apiBase?: string;\n appBase?: string;\n sitesBase?: string;\n /** Custom fetch (tests, server runtimes, proxies). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n /** Sent on every request as Accept-Language; the hold's own `locale` wins for messages. */\n locale?: string;\n /** Extra headers, e.g. X-Forwarded-For when calling from your server on a guest's behalf. */\n headers?: Record<string, string>;\n}\n\nexport function resolveEndpoints(config: ClientConfig = {}): Endpoints {\n const base = ENVIRONMENTS[config.env ?? 'production'];\n return {\n apiBase: trimSlash(config.apiBase ?? base.apiBase),\n appBase: trimSlash(config.appBase ?? base.appBase),\n sitesBase: (config.sitesBase ?? base.sitesBase).replace(/^https?:\\/\\//, '').replace(/\\/+$/, ''),\n };\n}\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '');\n}\n",
6
+ "/** Stable error codes of the public API. Branch on these; show `message` as-is. */\nexport type ErrorCode =\n | 'VALIDATION_ERROR'\n | 'RESOURCE_NOT_FOUND'\n | 'SITE_NOT_LIVE'\n | 'ROOM_NOT_BOOKABLE'\n | 'STUDIO_NOT_VERIFIED'\n | 'OUTSIDE_HOURS'\n | 'OUTSIDE_BOOKING_WINDOW'\n | 'SLOT_TAKEN'\n | 'HOLD_EXPIRED'\n | 'HOLD_NOT_VERIFIED'\n | 'HOLD_ALREADY_CONFIRMED'\n | 'CODE_INVALID'\n | 'TOO_MANY_ATTEMPTS'\n | 'RATE_LIMIT_EXCEEDED'\n | 'CAPTCHA_FAILED'\n | 'SMS_UNAVAILABLE'\n | 'PAYMENTS_UNAVAILABLE'\n | 'PAYMENT_MODE_DENIED'\n | 'CANCEL_TOO_LATE'\n | 'INVALID_STATE'\n | 'INTERNAL_ERROR'\n | 'NETWORK_ERROR'\n | (string & {});\n\nexport class MrCharlesError extends Error {\n readonly code: ErrorCode;\n readonly status: number;\n readonly requestId?: string;\n\n constructor(code: ErrorCode, message: string, status = 0, requestId?: string) {\n super(message);\n this.name = 'MrCharlesError';\n this.code = code;\n this.status = status;\n this.requestId = requestId;\n }\n\n /** The hold is gone — start the flow again with a fresh idempotency key. */\n get holdIsDead(): boolean {\n return this.code === 'HOLD_EXPIRED' || this.code === 'TOO_MANY_ATTEMPTS';\n }\n\n /** The slot is no longer available — refresh availability, keep the guest's details. */\n get slotIsGone(): boolean {\n return this.code === 'SLOT_TAKEN' || this.code === 'OUTSIDE_HOURS' || this.code === 'OUTSIDE_BOOKING_WINDOW';\n }\n\n /** Worth retrying the same request, with the same idempotency key. */\n get isTransient(): boolean {\n return this.code === 'NETWORK_ERROR' || this.status >= 500;\n }\n}\n\nexport function isMrCharlesError(e: unknown): e is MrCharlesError {\n return e instanceof MrCharlesError || (typeof e === 'object' && e !== null && (e as { name?: string }).name === 'MrCharlesError');\n}\n",
7
+ "import { MrCharlesError } from './errors';\nimport type { ClientConfig } from './env';\n\nexport interface Http {\n get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;\n post<T>(path: string, body?: unknown): Promise<T>;\n}\n\n/** Envelope-aware fetch against the API: unwraps `data`, throws MrCharlesError. */\nexport function createHttp(apiBase: string, config: ClientConfig): Http {\n const resolved = config.fetch ?? (typeof fetch === 'function' ? fetch.bind(globalThis) : undefined);\n if (!resolved) throw new Error('@mrcharles/sdk: no fetch available — pass { fetch } in the client config');\n const doFetch: typeof fetch = resolved;\n\n async function request<T>(method: 'GET' | 'POST', path: string, body?: unknown, params?: Record<string, string | number | undefined>): Promise<T> {\n let url = apiBase + path;\n if (params) {\n const q = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== '') q.set(k, String(v));\n const s = q.toString();\n if (s) url += (url.includes('?') ? '&' : '?') + s;\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...(config.headers ?? {}) };\n if (config.locale) headers['Accept-Language'] = config.locale;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await doFetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, cache: 'no-store' });\n } catch (e) {\n throw new MrCharlesError('NETWORK_ERROR', (e as Error)?.message || 'Network error', 0);\n }\n\n const requestId = res.headers?.get?.('x-request-id') ?? undefined;\n const text = await res.text();\n let json: unknown = null;\n try {\n json = text ? JSON.parse(text) : null;\n } catch {\n json = null;\n }\n const envelope = (json && typeof json === 'object' ? json : {}) as { success?: boolean; data?: unknown; error?: { code?: string; message?: string } };\n\n if (!res.ok || envelope.success === false) {\n const code = envelope.error?.code ?? (res.status === 429 ? 'RATE_LIMIT_EXCEEDED' : res.status === 404 ? 'RESOURCE_NOT_FOUND' : 'INTERNAL_ERROR');\n const message = envelope.error?.message ?? res.statusText ?? 'Request failed';\n throw new MrCharlesError(code, message, res.status, requestId);\n }\n return ('data' in envelope ? envelope.data : json) as T;\n }\n\n return {\n get: (path, params) => request('GET', path, undefined, params),\n post: (path, body) => request('POST', path, body ?? {}),\n };\n}\n",
8
+ "import type { Endpoints } from './env';\nimport type { Locale } from './types';\n\nexport interface RoomLinkOptions {\n slug: string;\n roomId: string;\n /** YYYY-MM-DD in the studio's zone */\n date?: string;\n /** HH:MM */\n start?: string;\n durationMinutes?: number;\n lang?: Locale;\n /** Hide the hosted site's header and footer (for iframes) */\n embed?: boolean;\n}\n\nexport function createLinks(endpoints: Endpoints) {\n const siteOrigin = (slug: string) => `https://${slug}.${endpoints.sitesBase}`;\n\n return {\n /** The studio's hosted site */\n site(slug: string): string {\n return `${siteOrigin(slug)}/`;\n },\n\n /** Hosted booking screen for a room, optionally pre-filled */\n bookRoom(o: RoomLinkOptions): string {\n const q = new URLSearchParams();\n if (o.date) q.set('date', o.date);\n if (o.start) q.set('start', o.start);\n if (o.durationMinutes) q.set('duration', String(o.durationMinutes));\n if (o.lang) q.set('lang', o.lang);\n if (o.embed) q.set('embed', '1');\n const s = q.toString();\n return `${siteOrigin(o.slug)}/reservar/${o.roomId}${s ? `?${s}` : ''}`;\n },\n\n /** A guest's booking page (cancel, details) from the manage token */\n manage(slug: string, manageToken: string): string {\n return `${siteOrigin(slug)}/r/${manageToken}`;\n },\n\n /**\n * Book a lesson in the Mr Charles app. Students need an account there;\n * this is the hand-off. `instructorId` is the teacher's **user** id.\n */\n bookLesson(o: { offerId?: string; instructorId: string }): string {\n const q = new URLSearchParams({ instructorId: o.instructorId });\n if (o.offerId) q.set('offerId', o.offerId);\n return `${endpoints.appBase}/bookings/book?${q.toString()}`;\n },\n\n /** The teacher's public profile in the app */\n teacherProfile(instructorUserId: string): string {\n return `${endpoints.appBase}/instructors/${instructorUserId}`;\n },\n };\n}\n\nexport type Links = ReturnType<typeof createLinks>;\n",
9
+ "import type { Endpoints } from './env';\nimport { createLinks, type RoomLinkOptions } from './links';\nimport type { EmbedBookedEvent, EmbedEvent } from './types';\n\nexport interface EmbedOptions extends Omit<RoomLinkOptions, 'embed'> {\n /** Called once when the guest completes a booking inside the frame */\n onBooked?: (e: EmbedBookedEvent) => void;\n /** Called on every height change; default behaviour sets the iframe height */\n onResize?: (height: number) => void;\n /** Initial height before the first resize message (px). Default 760. */\n height?: number;\n /** Set to false to keep a fixed height instead of following the content */\n autoResize?: boolean;\n title?: string;\n className?: string;\n}\n\nexport interface EmbedHandle {\n iframe: HTMLIFrameElement;\n /** Remove the iframe and the message listener */\n destroy(): void;\n}\n\n/** True when a postMessage came from the hosted sites domain of this environment. */\nexport function isTrustedOrigin(origin: string, sitesBase: string): boolean {\n try {\n const host = new URL(origin).host;\n return host === sitesBase || host.endsWith(`.${sitesBase}`);\n } catch {\n return false;\n }\n}\n\nexport function parseEmbedEvent(data: unknown): EmbedEvent | null {\n if (!data || typeof data !== 'object') return null;\n const d = data as Record<string, unknown>;\n if (d.type === 'mrcharles:resize' && typeof d.height === 'number') return { type: 'mrcharles:resize', height: d.height };\n if (d.type === 'mrcharles:booked' && typeof d.reference === 'string') {\n return {\n type: 'mrcharles:booked',\n reference: d.reference,\n bookingId: String(d.bookingId ?? ''),\n manageUrl: String(d.manageUrl ?? ''),\n status: String(d.status ?? ''),\n };\n }\n return null;\n}\n\nexport function createEmbed(endpoints: Endpoints) {\n const links = createLinks(endpoints);\n\n /**\n * Drop the hosted booking screen for a room into `target`. Resizes itself\n * to the content and reports the booking when it happens. Nothing else to\n * wire: codes, payments and the manage link are handled inside the frame.\n */\n function mountRoomBooking(target: HTMLElement | string, opts: EmbedOptions): EmbedHandle {\n const el = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (!el) throw new Error(`@mrcharles/sdk: embed target not found: ${String(target)}`);\n\n const iframe = document.createElement('iframe');\n iframe.src = links.bookRoom({ ...opts, embed: true });\n iframe.title = opts.title ?? 'Mr Charles booking';\n iframe.loading = 'lazy';\n iframe.referrerPolicy = 'strict-origin-when-cross-origin';\n iframe.setAttribute('allow', 'payment');\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = 'block';\n iframe.style.height = `${opts.height ?? 760}px`;\n if (opts.className) iframe.className = opts.className;\n\n const expectedOrigin = new URL(iframe.src).origin;\n const onMessage = (e: MessageEvent) => {\n if (e.source !== iframe.contentWindow) return;\n if (e.origin !== expectedOrigin && !isTrustedOrigin(e.origin, endpoints.sitesBase)) return;\n const ev = parseEmbedEvent(e.data);\n if (!ev) return;\n if (ev.type === 'mrcharles:resize') {\n if (opts.autoResize !== false) iframe.style.height = `${ev.height}px`;\n opts.onResize?.(ev.height);\n } else {\n opts.onBooked?.(ev);\n }\n };\n window.addEventListener('message', onMessage);\n el.appendChild(iframe);\n\n return {\n iframe,\n destroy() {\n window.removeEventListener('message', onMessage);\n iframe.remove();\n },\n };\n }\n\n return { mountRoomBooking };\n}\n",
10
+ "/** RFC 4122 v4, with a fallback for runtimes without crypto.randomUUID. */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n const b = new Uint8Array(16);\n if (c?.getRandomValues) c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6] & 0x0f) | 0x40;\n b[8] = (b[8] & 0x3f) | 0x80;\n const h = [...b].map((x) => x.toString(16).padStart(2, '0')).join('');\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n",
11
+ "import type { FreeSpan } from './types';\n\n/** \"17:30\" → 1050 */\nexport function toMinutes(hhmm: string): number {\n const [h, m] = hhmm.split(':').map(Number);\n return (h || 0) * 60 + (m || 0);\n}\n\n/** 1050 → \"17:30\" */\nexport function toHHMM(minutes: number): string {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;\n}\n\nexport function addMinutes(hhmm: string, minutes: number): string {\n return toHHMM(toMinutes(hhmm) + minutes);\n}\n\n/**\n * Every start time at which a booking of `durationMinutes` fits inside one\n * of the free spans, on the `stepMinutes` grid. Sorted, de-duplicated.\n */\nexport function startTimes(free: FreeSpan[], durationMinutes: number, stepMinutes: number): string[] {\n const step = Math.max(1, stepMinutes);\n const out = new Set<string>();\n for (const span of free) {\n const s = toMinutes(span.start);\n const e = toMinutes(span.end);\n for (let t = Math.ceil(s / step) * step; t + durationMinutes <= e; t += step) out.add(toHHMM(t));\n }\n return [...out].sort((a, b) => toMinutes(a) - toMinutes(b));\n}\n\n/** Durations between min and max, on the step grid, that still fit after `start`. */\nexport function durationOptions(free: FreeSpan[], start: string, minMinutes: number, maxMinutes: number, stepMinutes: number): number[] {\n const s = toMinutes(start);\n const span = free.find((f) => toMinutes(f.start) <= s && s < toMinutes(f.end));\n if (!span) return [];\n const room = toMinutes(span.end) - s;\n const out: number[] = [];\n for (let d = minMinutes; d <= maxMinutes && d <= room; d += Math.max(1, stepMinutes)) out.push(d);\n return out;\n}\n\n/** 2500, \"EUR\" → \"25,00 €\" (locale-aware; minor units in, string out). */\nexport function formatPrice(cents: number, currency: string, locale = 'es-ES'): string {\n try {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(cents / 100);\n } catch {\n return `${(cents / 100).toFixed(2)} ${currency}`;\n }\n}\n\n/** \"2026-10-12\" for a Date, in the given IANA zone (today in the studio's zone). */\nexport function dateKey(date: Date, timeZone: string): string {\n const parts = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date);\n const get = (t: string) => parts.find((p) => p.type === t)?.value ?? '';\n return `${get('year')}-${get('month')}-${get('day')}`;\n}\n\n/** YYYY-MM-DD plus n days, calendar arithmetic only (no zone involved). */\nexport function addDays(key: string, days: number): string {\n const [y, m, d] = key.split('-').map(Number);\n const t = Date.UTC(y, m - 1, d + days);\n return new Date(t).toISOString().slice(0, 10);\n}\n",
12
+ "import type { Http } from './http';\nimport { MrCharlesError, isMrCharlesError } from './errors';\nimport { uuid } from './uuid';\nimport { addDays, dateKey, durationOptions, startTimes } from './slots';\nimport type { Links } from './links';\nimport type { Channel, ConfirmRequest, Confirmation, Hold, HoldRequest, Locale, PublicRoom, PublicSite, RoomAvailability } from './types';\n\nexport type RoomStep = 'slot' | 'details' | 'verify' | 'pay' | 'done' | 'expired';\n\nexport interface RoomBookingState {\n step: RoomStep;\n site: PublicSite | null;\n room: PublicRoom | null;\n availability: RoomAvailability | null;\n hold: Hold | null;\n confirmation: Confirmation | null;\n /** Last error from any action; cleared on the next successful one */\n error: MrCharlesError | null;\n busy: boolean;\n}\n\nexport interface GuestDetails {\n date: string;\n startTime: string;\n durationMinutes: number;\n guestName: string;\n channel: Channel;\n contact: string;\n locale?: Locale;\n captchaToken?: string;\n}\n\nexport interface RoomBookingOptions {\n slug: string;\n roomId: string;\n /** Called on every state change */\n onChange?: (state: RoomBookingState) => void;\n}\n\nexport interface RoomBooking {\n readonly state: RoomBookingState;\n subscribe(fn: (state: RoomBookingState) => void): () => void;\n\n /** Site payload (cached 60 s server-side). Resolves the room too. */\n loadSite(): Promise<PublicSite>;\n /** Free spans for a window; defaults to the next 14 days from today in the studio's zone. */\n loadAvailability(window?: { from?: string; to?: string; days?: number }): Promise<RoomAvailability>;\n /** Start times for a date and duration, from the loaded availability. */\n startTimesFor(date: string, durationMinutes: number): string[];\n /** Durations that fit after a start time, from the loaded availability. */\n durationsFor(date: string, startTime: string): number[];\n\n /** Reserve the slot and send the code. One idempotency key per attempt is handled for you. */\n hold(details: GuestDetails): Promise<Hold>;\n verify(code: string): Promise<Hold>;\n resend(): Promise<Hold>;\n confirm(req: ConfirmRequest): Promise<Confirmation>;\n\n /** Milliseconds until the hold expires; 0 when there is no live hold. */\n remainingMs(): number;\n /** Forget the hold and start over (new idempotency key). Keeps site and availability. */\n reset(): void;\n destroy(): void;\n}\n\n/**\n * The whole guest flow for one room as a small state machine: availability →\n * hold → code → confirm, with the expiry timer, idempotency key and error\n * classification handled. Render it with any UI.\n */\nexport function createRoomBooking(http: Http, links: Links, opts: RoomBookingOptions): RoomBooking {\n let state: RoomBookingState = { step: 'slot', site: null, room: null, availability: null, hold: null, confirmation: null, error: null, busy: false };\n const listeners = new Set<(s: RoomBookingState) => void>();\n if (opts.onChange) listeners.add(opts.onChange);\n let idempotencyKey = uuid();\n let expiryTimer: ReturnType<typeof setTimeout> | undefined;\n\n const set = (patch: Partial<RoomBookingState>) => {\n state = { ...state, ...patch };\n for (const l of listeners) l(state);\n };\n\n const clearTimer = () => {\n if (expiryTimer) clearTimeout(expiryTimer);\n expiryTimer = undefined;\n };\n\n const armTimer = (hold: Hold) => {\n clearTimer();\n const ms = new Date(hold.expiresAt).getTime() - Date.now();\n if (ms <= 0) return;\n expiryTimer = setTimeout(() => {\n if (state.step === 'verify' || state.step === 'pay') {\n idempotencyKey = uuid();\n set({ step: 'expired', error: new MrCharlesError('HOLD_EXPIRED', 'The hold expired. Pick a time again.', 410) });\n }\n }, ms);\n };\n\n async function run<T>(fn: () => Promise<T>): Promise<T> {\n set({ busy: true });\n try {\n const out = await fn();\n set({ busy: false, error: null });\n return out;\n } catch (e) {\n const err = isMrCharlesError(e) ? e : new MrCharlesError('INTERNAL_ERROR', (e as Error)?.message ?? 'Unexpected error');\n if (err.holdIsDead) {\n clearTimer();\n idempotencyKey = uuid();\n set({ busy: false, error: err, step: 'expired', hold: null });\n } else {\n set({ busy: false, error: err });\n }\n throw err;\n }\n }\n\n const api: RoomBooking = {\n get state() {\n return state;\n },\n subscribe(fn) {\n listeners.add(fn);\n fn(state);\n return () => {\n listeners.delete(fn);\n };\n },\n\n loadSite: () =>\n run(async () => {\n const site = await http.get<PublicSite>(`/public/sites/${encodeURIComponent(opts.slug)}`);\n const room = site.rooms.find((r) => r.id === opts.roomId) ?? null;\n if (!room) throw new MrCharlesError('RESOURCE_NOT_FOUND', `Room ${opts.roomId} is not on site ${opts.slug}`, 404);\n if (!room.bookable) throw new MrCharlesError('ROOM_NOT_BOOKABLE', `${room.name} does not take online bookings right now`, 409);\n set({ site, room });\n return site;\n }),\n\n loadAvailability: (window = {}) =>\n run(async () => {\n const tz = state.site?.studio.timezone ?? 'UTC';\n const from = window.from ?? dateKey(new Date(), tz);\n const to = window.to ?? addDays(from, Math.min(31, Math.max(1, window.days ?? 14)) - 1);\n const availability = await http.get<RoomAvailability>(\n `/public/sites/${encodeURIComponent(opts.slug)}/rooms/${encodeURIComponent(opts.roomId)}/availability`,\n { from, to },\n );\n set({ availability });\n return availability;\n }),\n\n startTimesFor(date, durationMinutes) {\n const a = state.availability;\n if (!a) return [];\n const day = a.days.find((d) => d.date === date);\n return day ? startTimes(day.free, durationMinutes, a.stepMinutes) : [];\n },\n\n durationsFor(date, startTime) {\n const a = state.availability;\n if (!a) return [];\n const day = a.days.find((d) => d.date === date);\n return day ? durationOptions(day.free, startTime, a.minMinutes, a.maxMinutes, a.stepMinutes) : [];\n },\n\n hold: (details) =>\n run(async () => {\n if (state.step === 'expired') set({ step: 'slot', error: null });\n const body: HoldRequest = {\n roomId: opts.roomId,\n date: details.date,\n startTime: details.startTime,\n durationMinutes: details.durationMinutes,\n guestName: details.guestName,\n channel: details.channel,\n contact: details.contact,\n locale: details.locale ?? state.site?.site.defaultLocale ?? 'es',\n idempotencyKey,\n ...(details.captchaToken ? { captchaToken: details.captchaToken } : {}),\n };\n const hold = await http.post<Hold>(`/public/sites/${encodeURIComponent(opts.slug)}/holds`, body);\n armTimer(hold);\n set({ hold, step: 'verify', confirmation: null });\n return hold;\n }),\n\n verify: (code) =>\n run(async () => {\n const id = requireHold(state).holdId;\n const hold = await http.post<Hold>(`/public/holds/${id}/verify`, { code: code.replace(/\\D/g, '') });\n set({ hold, step: 'pay' });\n return hold;\n }),\n\n resend: () =>\n run(async () => {\n const id = requireHold(state).holdId;\n const hold = await http.post<Hold>(`/public/holds/${id}/resend`);\n set({ hold });\n return hold;\n }),\n\n confirm: (req) =>\n run(async () => {\n const h = requireHold(state);\n if (!h.paymentModes.includes(req.paymentMode)) {\n throw new MrCharlesError('PAYMENT_MODE_DENIED', `This room does not accept ${req.paymentMode}`, 409);\n }\n const confirmation = await http.post<Confirmation>(`/public/holds/${h.holdId}/confirm`, {\n paymentMode: req.paymentMode,\n notes: req.notes ?? '',\n email: req.email ?? '',\n });\n clearTimer();\n if (!confirmation.manageUrl && confirmation.manageToken) confirmation.manageUrl = links.manage(opts.slug, confirmation.manageToken);\n set({ confirmation, step: 'done' });\n return confirmation;\n }),\n\n remainingMs() {\n const h = state.hold;\n if (!h || state.step === 'done' || state.step === 'expired') return 0;\n return Math.max(0, new Date(h.expiresAt).getTime() - Date.now());\n },\n\n reset() {\n clearTimer();\n idempotencyKey = uuid();\n set({ step: 'slot', hold: null, confirmation: null, error: null, busy: false });\n },\n\n destroy() {\n clearTimer();\n listeners.clear();\n },\n };\n\n return api;\n}\n\nfunction requireHold(s: RoomBookingState): Hold {\n if (!s.hold) throw new MrCharlesError('INVALID_STATE', 'No hold yet — call hold() first', 409);\n return s.hold;\n}\n",
13
+ "import { type ClientConfig, type Endpoints, resolveEndpoints } from './env';\nimport { createHttp } from './http';\nimport { createLinks } from './links';\nimport { createEmbed } from './embed';\nimport { createRoomBooking, type RoomBookingOptions } from './rooms';\nimport type {\n Booking, ConfirmRequest, Confirmation, Hold, HoldRequest, LessonOffer, LessonPolicy, LessonSlots,\n PublicSite, RoomAvailability, TeacherProfile, TeacherWindows,\n} from './types';\n\nexport function createClient(config: ClientConfig = {}) {\n const endpoints: Endpoints = resolveEndpoints(config);\n const http = createHttp(endpoints.apiBase, config);\n const links = createLinks(endpoints);\n const embed = createEmbed(endpoints);\n const enc = encodeURIComponent;\n\n const sites = {\n /** Everything needed to render a studio: rooms, prices, hours, payment and captcha keys. */\n get: (slug: string) => http.get<PublicSite>(`/public/sites/${enc(slug)}`),\n /** The site behind a custom domain. */\n resolve: (host: string) => http.get<PublicSite>('/public/sites/resolve', { host }),\n /** Free spans for a room between two dates (≤ 31 days), in the studio's zone. Never cache. */\n availability: (slug: string, roomId: string, range: { from: string; to: string }) =>\n http.get<RoomAvailability>(`/public/sites/${enc(slug)}/rooms/${enc(roomId)}/availability`, range),\n };\n\n const holds = {\n create: (slug: string, req: HoldRequest) => http.post<Hold>(`/public/sites/${enc(slug)}/holds`, req),\n verify: (holdId: string, code: string) => http.post<Hold>(`/public/holds/${enc(holdId)}/verify`, { code }),\n resend: (holdId: string) => http.post<Hold>(`/public/holds/${enc(holdId)}/resend`),\n confirm: (holdId: string, req: ConfirmRequest) =>\n http.post<Confirmation>(`/public/holds/${enc(holdId)}/confirm`, { paymentMode: req.paymentMode, notes: req.notes ?? '', email: req.email ?? '' }),\n };\n\n const bookings = {\n /** A guest's booking from its manage token. Poll this after a card payment. */\n get: (manageToken: string) => http.get<Booking>(`/public/bookings/manage/${enc(manageToken)}`),\n cancel: (manageToken: string, reason = '') => http.post<Booking>(`/public/bookings/manage/${enc(manageToken)}/cancel`, { reason }),\n };\n\n const teachers = {\n /** Public teacher profile. `instructorUserId` is the teacher's user id; the response's `user.id` is the one to keep. */\n profile: (instructorUserId: string) => http.get<TeacherProfile>(`/instructors/${enc(instructorUserId)}/teacher-profile`),\n /** Published lesson offers. */\n offers: (instructorUserId: string) => http.get<LessonOffer[]>(`/instructors/${enc(instructorUserId)}/offers`),\n offer: (offerId: string) => http.get<LessonOffer>(`/offers/${enc(offerId)}`),\n /** Free windows in the teacher's zone; use `slots` for something bookable. */\n windows: (instructorUserId: string, range: { from: string; to: string }) =>\n http.get<TeacherWindows>(`/availability/${enc(instructorUserId)}/windows`, range),\n policy: (instructorUserId: string) => http.get<LessonPolicy>(`/availability/${enc(instructorUserId)}/policy`),\n /** Concrete bookable start times for an offer — duration, policy and existing lessons applied. */\n slots: (q: { instructorUserId: string; offerId: string; date: string; days?: number }) =>\n http.get<LessonSlots>('/bookings/slots', { instructorId: q.instructorUserId, offerId: q.offerId, date: q.date, days: q.days ?? 7 }),\n };\n\n return {\n endpoints,\n sites,\n holds,\n bookings,\n teachers,\n links,\n /** Drop our hosted booking screen into your page. */\n mountRoomBooking: embed.mountRoomBooking,\n /** The headless flow for one room: availability → hold → code → confirm. */\n roomBooking: (opts: RoomBookingOptions) => createRoomBooking(http, links, opts),\n };\n}\n\nexport type MrCharlesClient = ReturnType<typeof createClient>;\n",
14
+ "export { createClient, type MrCharlesClient } from './client';\nexport { MrCharlesError, isMrCharlesError, type ErrorCode } from './errors';\nexport { ENVIRONMENTS, resolveEndpoints, type ClientConfig, type Endpoints, type Environment } from './env';\nexport { createRoomBooking, type RoomBooking, type RoomBookingOptions, type RoomBookingState, type RoomStep, type GuestDetails } from './rooms';\nexport { createLinks, type Links, type RoomLinkOptions } from './links';\nexport { createEmbed, isTrustedOrigin, parseEmbedEvent, type EmbedHandle, type EmbedOptions } from './embed';\nexport { startTimes, durationOptions, toMinutes, toHHMM, addMinutes, formatPrice, dateKey, addDays } from './slots';\nexport { uuid } from './uuid';\nexport type * from './types';\n\nexport const VERSION = '0.1.0';\n",
15
+ "/**\n * Entry for the <script> build: exposes everything as `window.MrCharles`.\n *\n * <script src=\"https://developers.mrcharles.app/sdk/v0/mrcharles.min.js\"></script>\n * <script>\n * const mc = MrCharles.createClient();\n * mc.mountRoomBooking('#book', { slug: 'studio-uno-madrid', roomId: '…', lang: 'es' });\n * </script>\n */\nimport * as sdk from './index';\n\ndeclare global {\n interface Window {\n MrCharles: typeof sdk;\n }\n}\n\nwindow.MrCharles = sdk;\n"
16
+ ],
17
+ "mappings": "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",
18
+ "debugId": "7ECBFC336B75DC2F64756E2164756E21",
19
+ "names": []
20
+ }
@@ -0,0 +1,63 @@
1
+ import { type ClientConfig, type Endpoints } from './env';
2
+ import { type RoomBookingOptions } from './rooms';
3
+ import type { Booking, ConfirmRequest, Confirmation, Hold, HoldRequest, LessonOffer, LessonPolicy, LessonSlots, PublicSite, RoomAvailability, TeacherProfile, TeacherWindows } from './types';
4
+ export declare function createClient(config?: ClientConfig): {
5
+ endpoints: Endpoints;
6
+ sites: {
7
+ /** Everything needed to render a studio: rooms, prices, hours, payment and captcha keys. */
8
+ get: (slug: string) => Promise<PublicSite>;
9
+ /** The site behind a custom domain. */
10
+ resolve: (host: string) => Promise<PublicSite>;
11
+ /** Free spans for a room between two dates (≤ 31 days), in the studio's zone. Never cache. */
12
+ availability: (slug: string, roomId: string, range: {
13
+ from: string;
14
+ to: string;
15
+ }) => Promise<RoomAvailability>;
16
+ };
17
+ holds: {
18
+ create: (slug: string, req: HoldRequest) => Promise<Hold>;
19
+ verify: (holdId: string, code: string) => Promise<Hold>;
20
+ resend: (holdId: string) => Promise<Hold>;
21
+ confirm: (holdId: string, req: ConfirmRequest) => Promise<Confirmation>;
22
+ };
23
+ bookings: {
24
+ /** A guest's booking from its manage token. Poll this after a card payment. */
25
+ get: (manageToken: string) => Promise<Booking>;
26
+ cancel: (manageToken: string, reason?: string) => Promise<Booking>;
27
+ };
28
+ teachers: {
29
+ /** Public teacher profile. `instructorUserId` is the teacher's user id; the response's `user.id` is the one to keep. */
30
+ profile: (instructorUserId: string) => Promise<TeacherProfile>;
31
+ /** Published lesson offers. */
32
+ offers: (instructorUserId: string) => Promise<LessonOffer[]>;
33
+ offer: (offerId: string) => Promise<LessonOffer>;
34
+ /** Free windows in the teacher's zone; use `slots` for something bookable. */
35
+ windows: (instructorUserId: string, range: {
36
+ from: string;
37
+ to: string;
38
+ }) => Promise<TeacherWindows>;
39
+ policy: (instructorUserId: string) => Promise<LessonPolicy>;
40
+ /** Concrete bookable start times for an offer — duration, policy and existing lessons applied. */
41
+ slots: (q: {
42
+ instructorUserId: string;
43
+ offerId: string;
44
+ date: string;
45
+ days?: number;
46
+ }) => Promise<LessonSlots>;
47
+ };
48
+ links: {
49
+ site(slug: string): string;
50
+ bookRoom(o: import("./links").RoomLinkOptions): string;
51
+ manage(slug: string, manageToken: string): string;
52
+ bookLesson(o: {
53
+ offerId?: string;
54
+ instructorId: string;
55
+ }): string;
56
+ teacherProfile(instructorUserId: string): string;
57
+ };
58
+ /** Drop our hosted booking screen into your page. */
59
+ mountRoomBooking: (target: HTMLElement | string, opts: import("./embed").EmbedOptions) => import("./embed").EmbedHandle;
60
+ /** The headless flow for one room: availability → hold → code → confirm. */
61
+ roomBooking: (opts: RoomBookingOptions) => import("./rooms").RoomBooking;
62
+ };
63
+ export type MrCharlesClient = ReturnType<typeof createClient>;
@@ -0,0 +1,26 @@
1
+ import type { Endpoints } from './env';
2
+ import { type RoomLinkOptions } from './links';
3
+ import type { EmbedBookedEvent, EmbedEvent } from './types';
4
+ export interface EmbedOptions extends Omit<RoomLinkOptions, 'embed'> {
5
+ /** Called once when the guest completes a booking inside the frame */
6
+ onBooked?: (e: EmbedBookedEvent) => void;
7
+ /** Called on every height change; default behaviour sets the iframe height */
8
+ onResize?: (height: number) => void;
9
+ /** Initial height before the first resize message (px). Default 760. */
10
+ height?: number;
11
+ /** Set to false to keep a fixed height instead of following the content */
12
+ autoResize?: boolean;
13
+ title?: string;
14
+ className?: string;
15
+ }
16
+ export interface EmbedHandle {
17
+ iframe: HTMLIFrameElement;
18
+ /** Remove the iframe and the message listener */
19
+ destroy(): void;
20
+ }
21
+ /** True when a postMessage came from the hosted sites domain of this environment. */
22
+ export declare function isTrustedOrigin(origin: string, sitesBase: string): boolean;
23
+ export declare function parseEmbedEvent(data: unknown): EmbedEvent | null;
24
+ export declare function createEmbed(endpoints: Endpoints): {
25
+ mountRoomBooking: (target: HTMLElement | string, opts: EmbedOptions) => EmbedHandle;
26
+ };
@@ -0,0 +1,30 @@
1
+ export type Environment = 'production';
2
+ export interface Endpoints {
3
+ /** e.g. https://api.mrcharles.app/api/v1 */
4
+ apiBase: string;
5
+ /** e.g. https://app.mrcharles.app — where lessons are booked */
6
+ appBase: string;
7
+ /** e.g. sites.mrcharles.app — hosted studio sites live at <slug>.<sitesBase> */
8
+ sitesBase: string;
9
+ }
10
+ /**
11
+ * There is one public environment. Testing happens against a studio site in
12
+ * sandbox mode on that same API — see developers.mrcharles.app/getting-started/sandbox —
13
+ * so an integration never has to change hosts between development and launch.
14
+ */
15
+ export declare const ENVIRONMENTS: Record<Environment, Endpoints>;
16
+ export interface ClientConfig {
17
+ /** Only 'production' exists; kept so a future environment is a non-breaking addition. */
18
+ env?: Environment;
19
+ /** Endpoint overrides — for Mr Charles' own deployments and tests, not for integrators. */
20
+ apiBase?: string;
21
+ appBase?: string;
22
+ sitesBase?: string;
23
+ /** Custom fetch (tests, server runtimes, proxies). Defaults to globalThis.fetch. */
24
+ fetch?: typeof fetch;
25
+ /** Sent on every request as Accept-Language; the hold's own `locale` wins for messages. */
26
+ locale?: string;
27
+ /** Extra headers, e.g. X-Forwarded-For when calling from your server on a guest's behalf. */
28
+ headers?: Record<string, string>;
29
+ }
30
+ export declare function resolveEndpoints(config?: ClientConfig): Endpoints;
@@ -0,0 +1,15 @@
1
+ /** Stable error codes of the public API. Branch on these; show `message` as-is. */
2
+ export type ErrorCode = 'VALIDATION_ERROR' | 'RESOURCE_NOT_FOUND' | 'SITE_NOT_LIVE' | 'ROOM_NOT_BOOKABLE' | 'STUDIO_NOT_VERIFIED' | 'OUTSIDE_HOURS' | 'OUTSIDE_BOOKING_WINDOW' | 'SLOT_TAKEN' | 'HOLD_EXPIRED' | 'HOLD_NOT_VERIFIED' | 'HOLD_ALREADY_CONFIRMED' | 'CODE_INVALID' | 'TOO_MANY_ATTEMPTS' | 'RATE_LIMIT_EXCEEDED' | 'CAPTCHA_FAILED' | 'SMS_UNAVAILABLE' | 'PAYMENTS_UNAVAILABLE' | 'PAYMENT_MODE_DENIED' | 'CANCEL_TOO_LATE' | 'INVALID_STATE' | 'INTERNAL_ERROR' | 'NETWORK_ERROR' | (string & {});
3
+ export declare class MrCharlesError extends Error {
4
+ readonly code: ErrorCode;
5
+ readonly status: number;
6
+ readonly requestId?: string;
7
+ constructor(code: ErrorCode, message: string, status?: number, requestId?: string);
8
+ /** The hold is gone — start the flow again with a fresh idempotency key. */
9
+ get holdIsDead(): boolean;
10
+ /** The slot is no longer available — refresh availability, keep the guest's details. */
11
+ get slotIsGone(): boolean;
12
+ /** Worth retrying the same request, with the same idempotency key. */
13
+ get isTransient(): boolean;
14
+ }
15
+ export declare function isMrCharlesError(e: unknown): e is MrCharlesError;
@@ -0,0 +1,7 @@
1
+ import type { ClientConfig } from './env';
2
+ export interface Http {
3
+ get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
4
+ post<T>(path: string, body?: unknown): Promise<T>;
5
+ }
6
+ /** Envelope-aware fetch against the API: unwraps `data`, throws MrCharlesError. */
7
+ export declare function createHttp(apiBase: string, config: ClientConfig): Http;
@@ -0,0 +1,10 @@
1
+ export { createClient, type MrCharlesClient } from './client';
2
+ export { MrCharlesError, isMrCharlesError, type ErrorCode } from './errors';
3
+ export { ENVIRONMENTS, resolveEndpoints, type ClientConfig, type Endpoints, type Environment } from './env';
4
+ export { createRoomBooking, type RoomBooking, type RoomBookingOptions, type RoomBookingState, type RoomStep, type GuestDetails } from './rooms';
5
+ export { createLinks, type Links, type RoomLinkOptions } from './links';
6
+ export { createEmbed, isTrustedOrigin, parseEmbedEvent, type EmbedHandle, type EmbedOptions } from './embed';
7
+ export { startTimes, durationOptions, toMinutes, toHHMM, addMinutes, formatPrice, dateKey, addDays } from './slots';
8
+ export { uuid } from './uuid';
9
+ export type * from './types';
10
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,33 @@
1
+ import type { Endpoints } from './env';
2
+ import type { Locale } from './types';
3
+ export interface RoomLinkOptions {
4
+ slug: string;
5
+ roomId: string;
6
+ /** YYYY-MM-DD in the studio's zone */
7
+ date?: string;
8
+ /** HH:MM */
9
+ start?: string;
10
+ durationMinutes?: number;
11
+ lang?: Locale;
12
+ /** Hide the hosted site's header and footer (for iframes) */
13
+ embed?: boolean;
14
+ }
15
+ export declare function createLinks(endpoints: Endpoints): {
16
+ /** The studio's hosted site */
17
+ site(slug: string): string;
18
+ /** Hosted booking screen for a room, optionally pre-filled */
19
+ bookRoom(o: RoomLinkOptions): string;
20
+ /** A guest's booking page (cancel, details) from the manage token */
21
+ manage(slug: string, manageToken: string): string;
22
+ /**
23
+ * Book a lesson in the Mr Charles app. Students need an account there;
24
+ * this is the hand-off. `instructorId` is the teacher's **user** id.
25
+ */
26
+ bookLesson(o: {
27
+ offerId?: string;
28
+ instructorId: string;
29
+ }): string;
30
+ /** The teacher's public profile in the app */
31
+ teacherProfile(instructorUserId: string): string;
32
+ };
33
+ export type Links = ReturnType<typeof createLinks>;
@@ -0,0 +1,64 @@
1
+ import type { Http } from './http';
2
+ import { MrCharlesError } from './errors';
3
+ import type { Links } from './links';
4
+ import type { Channel, ConfirmRequest, Confirmation, Hold, Locale, PublicRoom, PublicSite, RoomAvailability } from './types';
5
+ export type RoomStep = 'slot' | 'details' | 'verify' | 'pay' | 'done' | 'expired';
6
+ export interface RoomBookingState {
7
+ step: RoomStep;
8
+ site: PublicSite | null;
9
+ room: PublicRoom | null;
10
+ availability: RoomAvailability | null;
11
+ hold: Hold | null;
12
+ confirmation: Confirmation | null;
13
+ /** Last error from any action; cleared on the next successful one */
14
+ error: MrCharlesError | null;
15
+ busy: boolean;
16
+ }
17
+ export interface GuestDetails {
18
+ date: string;
19
+ startTime: string;
20
+ durationMinutes: number;
21
+ guestName: string;
22
+ channel: Channel;
23
+ contact: string;
24
+ locale?: Locale;
25
+ captchaToken?: string;
26
+ }
27
+ export interface RoomBookingOptions {
28
+ slug: string;
29
+ roomId: string;
30
+ /** Called on every state change */
31
+ onChange?: (state: RoomBookingState) => void;
32
+ }
33
+ export interface RoomBooking {
34
+ readonly state: RoomBookingState;
35
+ subscribe(fn: (state: RoomBookingState) => void): () => void;
36
+ /** Site payload (cached 60 s server-side). Resolves the room too. */
37
+ loadSite(): Promise<PublicSite>;
38
+ /** Free spans for a window; defaults to the next 14 days from today in the studio's zone. */
39
+ loadAvailability(window?: {
40
+ from?: string;
41
+ to?: string;
42
+ days?: number;
43
+ }): Promise<RoomAvailability>;
44
+ /** Start times for a date and duration, from the loaded availability. */
45
+ startTimesFor(date: string, durationMinutes: number): string[];
46
+ /** Durations that fit after a start time, from the loaded availability. */
47
+ durationsFor(date: string, startTime: string): number[];
48
+ /** Reserve the slot and send the code. One idempotency key per attempt is handled for you. */
49
+ hold(details: GuestDetails): Promise<Hold>;
50
+ verify(code: string): Promise<Hold>;
51
+ resend(): Promise<Hold>;
52
+ confirm(req: ConfirmRequest): Promise<Confirmation>;
53
+ /** Milliseconds until the hold expires; 0 when there is no live hold. */
54
+ remainingMs(): number;
55
+ /** Forget the hold and start over (new idempotency key). Keeps site and availability. */
56
+ reset(): void;
57
+ destroy(): void;
58
+ }
59
+ /**
60
+ * The whole guest flow for one room as a small state machine: availability →
61
+ * hold → code → confirm, with the expiry timer, idempotency key and error
62
+ * classification handled. Render it with any UI.
63
+ */
64
+ export declare function createRoomBooking(http: Http, links: Links, opts: RoomBookingOptions): RoomBooking;
@@ -0,0 +1,19 @@
1
+ import type { FreeSpan } from './types';
2
+ /** "17:30" → 1050 */
3
+ export declare function toMinutes(hhmm: string): number;
4
+ /** 1050 → "17:30" */
5
+ export declare function toHHMM(minutes: number): string;
6
+ export declare function addMinutes(hhmm: string, minutes: number): string;
7
+ /**
8
+ * Every start time at which a booking of `durationMinutes` fits inside one
9
+ * of the free spans, on the `stepMinutes` grid. Sorted, de-duplicated.
10
+ */
11
+ export declare function startTimes(free: FreeSpan[], durationMinutes: number, stepMinutes: number): string[];
12
+ /** Durations between min and max, on the step grid, that still fit after `start`. */
13
+ export declare function durationOptions(free: FreeSpan[], start: string, minMinutes: number, maxMinutes: number, stepMinutes: number): number[];
14
+ /** 2500, "EUR" → "25,00 €" (locale-aware; minor units in, string out). */
15
+ export declare function formatPrice(cents: number, currency: string, locale?: string): string;
16
+ /** "2026-10-12" for a Date, in the given IANA zone (today in the studio's zone). */
17
+ export declare function dateKey(date: Date, timeZone: string): string;
18
+ /** YYYY-MM-DD plus n days, calendar arithmetic only (no zone involved). */
19
+ export declare function addDays(key: string, days: number): string;