@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.
package/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # @mrcharles/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put Mr Charles booking on any website: studio rooms (guests, no account) and
4
+ teacher lessons (hand-off to the app). Zero dependencies, ~6 kB min+gz, works
5
+ in the browser, in Astro/Next/SvelteKit server code, and as a plain `<script>`.
6
+
7
+ Docs: https://developers.mrcharles.app/integrate/sdk/
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @mrcharles/sdk
13
+ ```
14
+
15
+ or without a bundler:
16
+
17
+ ```html
18
+ <script src="https://developers.mrcharles.app/sdk/v0/mrcharles.min.js"></script>
19
+ <!-- exposes window.MrCharles -->
20
+ ```
21
+
22
+ ## 30-second version: embed our booking screen
23
+
24
+ ```js
25
+ import { createClient } from '@mrcharles/sdk';
26
+
27
+ const mc = createClient();
28
+ mc.mountRoomBooking('#book', {
29
+ slug: 'studio-uno-madrid',
30
+ roomId: 'd9a36c09-d151-5d4a-a077-296293b07be4',
31
+ lang: 'es',
32
+ onBooked: ({ reference, manageUrl }) => console.log('booked', reference, manageUrl),
33
+ });
34
+ ```
35
+
36
+ The iframe resizes to its content, takes card deposits, sends the SMS code,
37
+ and shows the manage link. You write nothing else.
38
+
39
+ ## Your own UI: the headless flow
40
+
41
+ ```js
42
+ const flow = mc.roomBooking({ slug, roomId, onChange: render });
43
+
44
+ await flow.loadSite(); // rooms, prices, timezone, captcha/stripe keys
45
+ await flow.loadAvailability({ days: 14 }); // free spans, studio's zone
46
+ flow.startTimesFor('2026-10-20', 120); // → ['09:00', '10:00', …]
47
+ flow.durationsFor('2026-10-20', '10:00'); // → [60, 120, 180]
48
+
49
+ await flow.hold({ date, startTime, durationMinutes, guestName, channel: 'phone', contact: '+34600111222' });
50
+ // state.step === 'verify' · flow.remainingMs() counts down from 12 min
51
+ await flow.verify(code); // 6 digits; flow.resend() after 60 s
52
+ await flow.confirm({ paymentMode: 'on_site', notes }); // state.step === 'done'
53
+ flow.state.confirmation.manageUrl; // hand this to the guest
54
+ ```
55
+
56
+ Every error is a `MrCharlesError` with a stable `code` (`SLOT_TAKEN`,
57
+ `HOLD_EXPIRED`, `CODE_INVALID`, …) and three helpers: `slotIsGone`,
58
+ `holdIsDead`, `isTransient`. The flow rotates the idempotency key when a hold
59
+ dies and keeps it across network retries, so you can simply call `hold()` again.
60
+
61
+ ## Lessons with a teacher
62
+
63
+ ```js
64
+ const t = await mc.teachers.profile(teacherUserId);
65
+ const offers = await mc.teachers.offers(teacherUserId);
66
+ const slots = await mc.teachers.slots({ instructorUserId: t.user.id, offerId: offers[0].id, date: '2026-10-13', days: 7 });
67
+ // <a href={mc.links.bookLesson({ offerId: offers[0].id, instructorId: t.user.id })}>Book</a>
68
+ ```
69
+
70
+ Students book inside the Mr Charles app (payment, reminders, cancellations),
71
+ so the SDK gives you the reads and the link, not a guest checkout.
72
+
73
+ ## Astro
74
+
75
+ ```astro
76
+ ---
77
+ import { createClient } from '@mrcharles/sdk';
78
+ const mc = createClient();
79
+ const site = await mc.sites.get('studio-uno-madrid'); // at build / on request
80
+ ---
81
+ <h1>{site.studio.name}</h1>
82
+ {site.rooms.filter((r) => r.bookable).map((r) => <a href={mc.links.bookRoom({ slug: site.studio.slug, roomId: r.id })}>{r.name}</a>)}
83
+ <div id="book"></div>
84
+ <script>
85
+ import { createClient } from '@mrcharles/sdk';
86
+ createClient().mountRoomBooking('#book', { slug: 'studio-uno-madrid', roomId: '…' });
87
+ </script>
88
+ ```
89
+
90
+ Availability and holds must run in the browser (or forward the guest's IP in
91
+ `headers: { 'X-Forwarded-For': ip }` when you proxy them).
92
+
93
+ ## API surface
94
+
95
+ | | |
96
+ |---|---|
97
+ | `createClient({ fetch, locale, headers })` | one client |
98
+ | `sites.get / resolve / availability` | studio payload, custom-domain lookup, free spans |
99
+ | `holds.create / verify / resend / confirm` | the raw calls, if you manage state yourself |
100
+ | `bookings.get / cancel` | a guest's booking by manage token |
101
+ | `teachers.profile / offers / offer / windows / policy / slots` | lesson reads |
102
+ | `links.site / bookRoom / manage / bookLesson / teacherProfile` | URLs for every hand-off |
103
+ | `mountRoomBooking(target, opts)` | iframe embed with resize + booked events |
104
+ | `roomBooking(opts)` | the stateful flow above |
105
+ | `startTimes / durationOptions / formatPrice / dateKey / addDays` | helpers |
106
+
107
+ ## Testing without bothering anyone
108
+
109
+ There is one API. To develop, point the SDK at a studio site in **sandbox
110
+ mode** — the platform's `demo-studio`, or your own site with *Modo pruebas*
111
+ switched on in the dashboard. A sandbox site returns the verification code
112
+ as `hold.devCode`, sends no SMS or email, offers `on_site` only, and purges
113
+ its bookings after seven days. `site.site.sandbox` tells you which mode you
114
+ are in; switch the slug to the real one at launch and nothing else changes.
@@ -0,0 +1,509 @@
1
+ // src/env.ts
2
+ var ENVIRONMENTS = {
3
+ production: {
4
+ apiBase: "https://api.mrcharles.app/api/v1",
5
+ appBase: "https://app.mrcharles.app",
6
+ sitesBase: "sites.mrcharles.app"
7
+ }
8
+ };
9
+ function resolveEndpoints(config = {}) {
10
+ const base = ENVIRONMENTS[config.env ?? "production"];
11
+ return {
12
+ apiBase: trimSlash(config.apiBase ?? base.apiBase),
13
+ appBase: trimSlash(config.appBase ?? base.appBase),
14
+ sitesBase: (config.sitesBase ?? base.sitesBase).replace(/^https?:\/\//, "").replace(/\/+$/, "")
15
+ };
16
+ }
17
+ function trimSlash(s) {
18
+ return s.replace(/\/+$/, "");
19
+ }
20
+
21
+ // src/errors.ts
22
+ class MrCharlesError extends Error {
23
+ code;
24
+ status;
25
+ requestId;
26
+ constructor(code, message, status = 0, requestId) {
27
+ super(message);
28
+ this.name = "MrCharlesError";
29
+ this.code = code;
30
+ this.status = status;
31
+ this.requestId = requestId;
32
+ }
33
+ get holdIsDead() {
34
+ return this.code === "HOLD_EXPIRED" || this.code === "TOO_MANY_ATTEMPTS";
35
+ }
36
+ get slotIsGone() {
37
+ return this.code === "SLOT_TAKEN" || this.code === "OUTSIDE_HOURS" || this.code === "OUTSIDE_BOOKING_WINDOW";
38
+ }
39
+ get isTransient() {
40
+ return this.code === "NETWORK_ERROR" || this.status >= 500;
41
+ }
42
+ }
43
+ function isMrCharlesError(e) {
44
+ return e instanceof MrCharlesError || typeof e === "object" && e !== null && e.name === "MrCharlesError";
45
+ }
46
+
47
+ // src/http.ts
48
+ function createHttp(apiBase, config) {
49
+ const resolved = config.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : undefined);
50
+ if (!resolved)
51
+ throw new Error("@mrcharles/sdk: no fetch available — pass { fetch } in the client config");
52
+ const doFetch = resolved;
53
+ async function request(method, path, body, params) {
54
+ let url = apiBase + path;
55
+ if (params) {
56
+ const q = new URLSearchParams;
57
+ for (const [k, v] of Object.entries(params))
58
+ if (v !== undefined && v !== "")
59
+ q.set(k, String(v));
60
+ const s = q.toString();
61
+ if (s)
62
+ url += (url.includes("?") ? "&" : "?") + s;
63
+ }
64
+ const headers = { Accept: "application/json", ...config.headers ?? {} };
65
+ if (config.locale)
66
+ headers["Accept-Language"] = config.locale;
67
+ if (body !== undefined)
68
+ headers["Content-Type"] = "application/json";
69
+ let res;
70
+ try {
71
+ res = await doFetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, cache: "no-store" });
72
+ } catch (e) {
73
+ throw new MrCharlesError("NETWORK_ERROR", e?.message || "Network error", 0);
74
+ }
75
+ const requestId = res.headers?.get?.("x-request-id") ?? undefined;
76
+ const text = await res.text();
77
+ let json = null;
78
+ try {
79
+ json = text ? JSON.parse(text) : null;
80
+ } catch {
81
+ json = null;
82
+ }
83
+ const envelope = json && typeof json === "object" ? json : {};
84
+ if (!res.ok || envelope.success === false) {
85
+ const code = envelope.error?.code ?? (res.status === 429 ? "RATE_LIMIT_EXCEEDED" : res.status === 404 ? "RESOURCE_NOT_FOUND" : "INTERNAL_ERROR");
86
+ const message = envelope.error?.message ?? res.statusText ?? "Request failed";
87
+ throw new MrCharlesError(code, message, res.status, requestId);
88
+ }
89
+ return "data" in envelope ? envelope.data : json;
90
+ }
91
+ return {
92
+ get: (path, params) => request("GET", path, undefined, params),
93
+ post: (path, body) => request("POST", path, body ?? {})
94
+ };
95
+ }
96
+
97
+ // src/links.ts
98
+ function createLinks(endpoints) {
99
+ const siteOrigin = (slug) => `https://${slug}.${endpoints.sitesBase}`;
100
+ return {
101
+ site(slug) {
102
+ return `${siteOrigin(slug)}/`;
103
+ },
104
+ bookRoom(o) {
105
+ const q = new URLSearchParams;
106
+ if (o.date)
107
+ q.set("date", o.date);
108
+ if (o.start)
109
+ q.set("start", o.start);
110
+ if (o.durationMinutes)
111
+ q.set("duration", String(o.durationMinutes));
112
+ if (o.lang)
113
+ q.set("lang", o.lang);
114
+ if (o.embed)
115
+ q.set("embed", "1");
116
+ const s = q.toString();
117
+ return `${siteOrigin(o.slug)}/reservar/${o.roomId}${s ? `?${s}` : ""}`;
118
+ },
119
+ manage(slug, manageToken) {
120
+ return `${siteOrigin(slug)}/r/${manageToken}`;
121
+ },
122
+ bookLesson(o) {
123
+ const q = new URLSearchParams({ instructorId: o.instructorId });
124
+ if (o.offerId)
125
+ q.set("offerId", o.offerId);
126
+ return `${endpoints.appBase}/bookings/book?${q.toString()}`;
127
+ },
128
+ teacherProfile(instructorUserId) {
129
+ return `${endpoints.appBase}/instructors/${instructorUserId}`;
130
+ }
131
+ };
132
+ }
133
+
134
+ // src/embed.ts
135
+ function isTrustedOrigin(origin, sitesBase) {
136
+ try {
137
+ const host = new URL(origin).host;
138
+ return host === sitesBase || host.endsWith(`.${sitesBase}`);
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function parseEmbedEvent(data) {
144
+ if (!data || typeof data !== "object")
145
+ return null;
146
+ const d = data;
147
+ if (d.type === "mrcharles:resize" && typeof d.height === "number")
148
+ return { type: "mrcharles:resize", height: d.height };
149
+ if (d.type === "mrcharles:booked" && typeof d.reference === "string") {
150
+ return {
151
+ type: "mrcharles:booked",
152
+ reference: d.reference,
153
+ bookingId: String(d.bookingId ?? ""),
154
+ manageUrl: String(d.manageUrl ?? ""),
155
+ status: String(d.status ?? "")
156
+ };
157
+ }
158
+ return null;
159
+ }
160
+ function createEmbed(endpoints) {
161
+ const links = createLinks(endpoints);
162
+ function mountRoomBooking(target, opts) {
163
+ const el = typeof target === "string" ? document.querySelector(target) : target;
164
+ if (!el)
165
+ throw new Error(`@mrcharles/sdk: embed target not found: ${String(target)}`);
166
+ const iframe = document.createElement("iframe");
167
+ iframe.src = links.bookRoom({ ...opts, embed: true });
168
+ iframe.title = opts.title ?? "Mr Charles booking";
169
+ iframe.loading = "lazy";
170
+ iframe.referrerPolicy = "strict-origin-when-cross-origin";
171
+ iframe.setAttribute("allow", "payment");
172
+ iframe.style.width = "100%";
173
+ iframe.style.border = "0";
174
+ iframe.style.display = "block";
175
+ iframe.style.height = `${opts.height ?? 760}px`;
176
+ if (opts.className)
177
+ iframe.className = opts.className;
178
+ const expectedOrigin = new URL(iframe.src).origin;
179
+ const onMessage = (e) => {
180
+ if (e.source !== iframe.contentWindow)
181
+ return;
182
+ if (e.origin !== expectedOrigin && !isTrustedOrigin(e.origin, endpoints.sitesBase))
183
+ return;
184
+ const ev = parseEmbedEvent(e.data);
185
+ if (!ev)
186
+ return;
187
+ if (ev.type === "mrcharles:resize") {
188
+ if (opts.autoResize !== false)
189
+ iframe.style.height = `${ev.height}px`;
190
+ opts.onResize?.(ev.height);
191
+ } else {
192
+ opts.onBooked?.(ev);
193
+ }
194
+ };
195
+ window.addEventListener("message", onMessage);
196
+ el.appendChild(iframe);
197
+ return {
198
+ iframe,
199
+ destroy() {
200
+ window.removeEventListener("message", onMessage);
201
+ iframe.remove();
202
+ }
203
+ };
204
+ }
205
+ return { mountRoomBooking };
206
+ }
207
+
208
+ // src/uuid.ts
209
+ function uuid() {
210
+ const c = globalThis.crypto;
211
+ if (c?.randomUUID)
212
+ return c.randomUUID();
213
+ const b = new Uint8Array(16);
214
+ if (c?.getRandomValues)
215
+ c.getRandomValues(b);
216
+ else
217
+ for (let i = 0;i < 16; i++)
218
+ b[i] = Math.floor(Math.random() * 256);
219
+ b[6] = b[6] & 15 | 64;
220
+ b[8] = b[8] & 63 | 128;
221
+ const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
222
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
223
+ }
224
+
225
+ // src/slots.ts
226
+ function toMinutes(hhmm) {
227
+ const [h, m] = hhmm.split(":").map(Number);
228
+ return (h || 0) * 60 + (m || 0);
229
+ }
230
+ function toHHMM(minutes) {
231
+ const h = Math.floor(minutes / 60);
232
+ const m = minutes % 60;
233
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
234
+ }
235
+ function addMinutes(hhmm, minutes) {
236
+ return toHHMM(toMinutes(hhmm) + minutes);
237
+ }
238
+ function startTimes(free, durationMinutes, stepMinutes) {
239
+ const step = Math.max(1, stepMinutes);
240
+ const out = new Set;
241
+ for (const span of free) {
242
+ const s = toMinutes(span.start);
243
+ const e = toMinutes(span.end);
244
+ for (let t = Math.ceil(s / step) * step;t + durationMinutes <= e; t += step)
245
+ out.add(toHHMM(t));
246
+ }
247
+ return [...out].sort((a, b) => toMinutes(a) - toMinutes(b));
248
+ }
249
+ function durationOptions(free, start, minMinutes, maxMinutes, stepMinutes) {
250
+ const s = toMinutes(start);
251
+ const span = free.find((f) => toMinutes(f.start) <= s && s < toMinutes(f.end));
252
+ if (!span)
253
+ return [];
254
+ const room = toMinutes(span.end) - s;
255
+ const out = [];
256
+ for (let d = minMinutes;d <= maxMinutes && d <= room; d += Math.max(1, stepMinutes))
257
+ out.push(d);
258
+ return out;
259
+ }
260
+ function formatPrice(cents, currency, locale = "es-ES") {
261
+ try {
262
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
263
+ } catch {
264
+ return `${(cents / 100).toFixed(2)} ${currency}`;
265
+ }
266
+ }
267
+ function dateKey(date, timeZone) {
268
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date);
269
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
270
+ return `${get("year")}-${get("month")}-${get("day")}`;
271
+ }
272
+ function addDays(key, days) {
273
+ const [y, m, d] = key.split("-").map(Number);
274
+ const t = Date.UTC(y, m - 1, d + days);
275
+ return new Date(t).toISOString().slice(0, 10);
276
+ }
277
+
278
+ // src/rooms.ts
279
+ function createRoomBooking(http, links, opts) {
280
+ let state = { step: "slot", site: null, room: null, availability: null, hold: null, confirmation: null, error: null, busy: false };
281
+ const listeners = new Set;
282
+ if (opts.onChange)
283
+ listeners.add(opts.onChange);
284
+ let idempotencyKey = uuid();
285
+ let expiryTimer;
286
+ const set = (patch) => {
287
+ state = { ...state, ...patch };
288
+ for (const l of listeners)
289
+ l(state);
290
+ };
291
+ const clearTimer = () => {
292
+ if (expiryTimer)
293
+ clearTimeout(expiryTimer);
294
+ expiryTimer = undefined;
295
+ };
296
+ const armTimer = (hold) => {
297
+ clearTimer();
298
+ const ms = new Date(hold.expiresAt).getTime() - Date.now();
299
+ if (ms <= 0)
300
+ return;
301
+ expiryTimer = setTimeout(() => {
302
+ if (state.step === "verify" || state.step === "pay") {
303
+ idempotencyKey = uuid();
304
+ set({ step: "expired", error: new MrCharlesError("HOLD_EXPIRED", "The hold expired. Pick a time again.", 410) });
305
+ }
306
+ }, ms);
307
+ };
308
+ async function run(fn) {
309
+ set({ busy: true });
310
+ try {
311
+ const out = await fn();
312
+ set({ busy: false, error: null });
313
+ return out;
314
+ } catch (e) {
315
+ const err = isMrCharlesError(e) ? e : new MrCharlesError("INTERNAL_ERROR", e?.message ?? "Unexpected error");
316
+ if (err.holdIsDead) {
317
+ clearTimer();
318
+ idempotencyKey = uuid();
319
+ set({ busy: false, error: err, step: "expired", hold: null });
320
+ } else {
321
+ set({ busy: false, error: err });
322
+ }
323
+ throw err;
324
+ }
325
+ }
326
+ const api = {
327
+ get state() {
328
+ return state;
329
+ },
330
+ subscribe(fn) {
331
+ listeners.add(fn);
332
+ fn(state);
333
+ return () => {
334
+ listeners.delete(fn);
335
+ };
336
+ },
337
+ loadSite: () => run(async () => {
338
+ const site = await http.get(`/public/sites/${encodeURIComponent(opts.slug)}`);
339
+ const room = site.rooms.find((r) => r.id === opts.roomId) ?? null;
340
+ if (!room)
341
+ throw new MrCharlesError("RESOURCE_NOT_FOUND", `Room ${opts.roomId} is not on site ${opts.slug}`, 404);
342
+ if (!room.bookable)
343
+ throw new MrCharlesError("ROOM_NOT_BOOKABLE", `${room.name} does not take online bookings right now`, 409);
344
+ set({ site, room });
345
+ return site;
346
+ }),
347
+ loadAvailability: (window2 = {}) => run(async () => {
348
+ const tz = state.site?.studio.timezone ?? "UTC";
349
+ const from = window2.from ?? dateKey(new Date, tz);
350
+ const to = window2.to ?? addDays(from, Math.min(31, Math.max(1, window2.days ?? 14)) - 1);
351
+ const availability = await http.get(`/public/sites/${encodeURIComponent(opts.slug)}/rooms/${encodeURIComponent(opts.roomId)}/availability`, { from, to });
352
+ set({ availability });
353
+ return availability;
354
+ }),
355
+ startTimesFor(date, durationMinutes) {
356
+ const a = state.availability;
357
+ if (!a)
358
+ return [];
359
+ const day = a.days.find((d) => d.date === date);
360
+ return day ? startTimes(day.free, durationMinutes, a.stepMinutes) : [];
361
+ },
362
+ durationsFor(date, startTime) {
363
+ const a = state.availability;
364
+ if (!a)
365
+ return [];
366
+ const day = a.days.find((d) => d.date === date);
367
+ return day ? durationOptions(day.free, startTime, a.minMinutes, a.maxMinutes, a.stepMinutes) : [];
368
+ },
369
+ hold: (details) => run(async () => {
370
+ if (state.step === "expired")
371
+ set({ step: "slot", error: null });
372
+ const body = {
373
+ roomId: opts.roomId,
374
+ date: details.date,
375
+ startTime: details.startTime,
376
+ durationMinutes: details.durationMinutes,
377
+ guestName: details.guestName,
378
+ channel: details.channel,
379
+ contact: details.contact,
380
+ locale: details.locale ?? state.site?.site.defaultLocale ?? "es",
381
+ idempotencyKey,
382
+ ...details.captchaToken ? { captchaToken: details.captchaToken } : {}
383
+ };
384
+ const hold = await http.post(`/public/sites/${encodeURIComponent(opts.slug)}/holds`, body);
385
+ armTimer(hold);
386
+ set({ hold, step: "verify", confirmation: null });
387
+ return hold;
388
+ }),
389
+ verify: (code) => run(async () => {
390
+ const id = requireHold(state).holdId;
391
+ const hold = await http.post(`/public/holds/${id}/verify`, { code: code.replace(/\D/g, "") });
392
+ set({ hold, step: "pay" });
393
+ return hold;
394
+ }),
395
+ resend: () => run(async () => {
396
+ const id = requireHold(state).holdId;
397
+ const hold = await http.post(`/public/holds/${id}/resend`);
398
+ set({ hold });
399
+ return hold;
400
+ }),
401
+ confirm: (req) => run(async () => {
402
+ const h = requireHold(state);
403
+ if (!h.paymentModes.includes(req.paymentMode)) {
404
+ throw new MrCharlesError("PAYMENT_MODE_DENIED", `This room does not accept ${req.paymentMode}`, 409);
405
+ }
406
+ const confirmation = await http.post(`/public/holds/${h.holdId}/confirm`, {
407
+ paymentMode: req.paymentMode,
408
+ notes: req.notes ?? "",
409
+ email: req.email ?? ""
410
+ });
411
+ clearTimer();
412
+ if (!confirmation.manageUrl && confirmation.manageToken)
413
+ confirmation.manageUrl = links.manage(opts.slug, confirmation.manageToken);
414
+ set({ confirmation, step: "done" });
415
+ return confirmation;
416
+ }),
417
+ remainingMs() {
418
+ const h = state.hold;
419
+ if (!h || state.step === "done" || state.step === "expired")
420
+ return 0;
421
+ return Math.max(0, new Date(h.expiresAt).getTime() - Date.now());
422
+ },
423
+ reset() {
424
+ clearTimer();
425
+ idempotencyKey = uuid();
426
+ set({ step: "slot", hold: null, confirmation: null, error: null, busy: false });
427
+ },
428
+ destroy() {
429
+ clearTimer();
430
+ listeners.clear();
431
+ }
432
+ };
433
+ return api;
434
+ }
435
+ function requireHold(s) {
436
+ if (!s.hold)
437
+ throw new MrCharlesError("INVALID_STATE", "No hold yet — call hold() first", 409);
438
+ return s.hold;
439
+ }
440
+
441
+ // src/client.ts
442
+ function createClient(config = {}) {
443
+ const endpoints = resolveEndpoints(config);
444
+ const http = createHttp(endpoints.apiBase, config);
445
+ const links = createLinks(endpoints);
446
+ const embed = createEmbed(endpoints);
447
+ const enc = encodeURIComponent;
448
+ const sites = {
449
+ get: (slug) => http.get(`/public/sites/${enc(slug)}`),
450
+ resolve: (host) => http.get("/public/sites/resolve", { host }),
451
+ availability: (slug, roomId, range) => http.get(`/public/sites/${enc(slug)}/rooms/${enc(roomId)}/availability`, range)
452
+ };
453
+ const holds = {
454
+ create: (slug, req) => http.post(`/public/sites/${enc(slug)}/holds`, req),
455
+ verify: (holdId, code) => http.post(`/public/holds/${enc(holdId)}/verify`, { code }),
456
+ resend: (holdId) => http.post(`/public/holds/${enc(holdId)}/resend`),
457
+ confirm: (holdId, req) => http.post(`/public/holds/${enc(holdId)}/confirm`, { paymentMode: req.paymentMode, notes: req.notes ?? "", email: req.email ?? "" })
458
+ };
459
+ const bookings = {
460
+ get: (manageToken) => http.get(`/public/bookings/manage/${enc(manageToken)}`),
461
+ cancel: (manageToken, reason = "") => http.post(`/public/bookings/manage/${enc(manageToken)}/cancel`, { reason })
462
+ };
463
+ const teachers = {
464
+ profile: (instructorUserId) => http.get(`/instructors/${enc(instructorUserId)}/teacher-profile`),
465
+ offers: (instructorUserId) => http.get(`/instructors/${enc(instructorUserId)}/offers`),
466
+ offer: (offerId) => http.get(`/offers/${enc(offerId)}`),
467
+ windows: (instructorUserId, range) => http.get(`/availability/${enc(instructorUserId)}/windows`, range),
468
+ policy: (instructorUserId) => http.get(`/availability/${enc(instructorUserId)}/policy`),
469
+ slots: (q) => http.get("/bookings/slots", { instructorId: q.instructorUserId, offerId: q.offerId, date: q.date, days: q.days ?? 7 })
470
+ };
471
+ return {
472
+ endpoints,
473
+ sites,
474
+ holds,
475
+ bookings,
476
+ teachers,
477
+ links,
478
+ mountRoomBooking: embed.mountRoomBooking,
479
+ roomBooking: (opts) => createRoomBooking(http, links, opts)
480
+ };
481
+ }
482
+
483
+ // src/index.ts
484
+ var VERSION = "0.1.0";
485
+ export {
486
+ ENVIRONMENTS,
487
+ MrCharlesError,
488
+ VERSION,
489
+ addDays,
490
+ addMinutes,
491
+ createClient,
492
+ createEmbed,
493
+ createLinks,
494
+ createRoomBooking,
495
+ dateKey,
496
+ durationOptions,
497
+ formatPrice,
498
+ isMrCharlesError,
499
+ isTrustedOrigin,
500
+ parseEmbedEvent,
501
+ resolveEndpoints,
502
+ startTimes,
503
+ toHHMM,
504
+ toMinutes,
505
+ uuid
506
+ };
507
+
508
+ //# debugId=147361D96F83431064756E2164756E21
509
+ //# sourceMappingURL=mrcharles.esm.js.map