@access-dlsu/leapify 0.260608.1 → 0.260608.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,476 +1,572 @@
1
- import '../chunk-PZ5AY32C.js';
2
- import { createAuthClient } from 'better-auth/client';
3
-
4
- var AUTH_TOKEN_KEY = "better-auth.session_token";
1
+ import { createAuthClient } from "better-auth/client";
2
+ //#region src/client/auth.ts
3
+ /**
4
+ * Better Auth client helper for Leapify API consumers.
5
+ *
6
+ * This module is **browser-safe** — no Cloudflare, Drizzle, or Hono deps.
7
+ * It wraps Better Auth's client SDK with the bearer plugin so that tokens
8
+ * can be stored and retrieved as plain strings (no cookie dependency on
9
+ * the consumer's frontend).
10
+ *
11
+ * @example
12
+ * // lib/auth.ts (frontend)
13
+ * import { createLeapifyAuthClient, signInWithGoogleRedirect } from 'leapify/client'
14
+ *
15
+ * export const authClient = createLeapifyAuthClient(process.env.NEXT_PUBLIC_API_URL!)
16
+ *
17
+ * // Redirect-based Google sign-in:
18
+ * await signInWithGoogleRedirect(authClient, '/dashboard')
19
+ */
20
+ const AUTH_TOKEN_KEY = "better-auth.session_token";
21
+ /**
22
+ * Create a Better Auth client bound to the Leapify Worker URL.
23
+ *
24
+ * It uses the 'Bearer' auth type to send the stored session token
25
+ * in the Authorization header.
26
+ */
5
27
  function createLeapifyAuthClient(baseUrl) {
6
- return createAuthClient({
7
- baseURL: baseUrl,
8
- fetchOptions: {
9
- auth: {
10
- type: "Bearer",
11
- token: () => {
12
- if (typeof window !== "undefined") {
13
- return localStorage.getItem(AUTH_TOKEN_KEY) || "";
14
- }
15
- return "";
16
- }
17
- }
18
- }
19
- });
28
+ return createAuthClient({
29
+ baseURL: baseUrl,
30
+ fetchOptions: { auth: {
31
+ type: "Bearer",
32
+ token: () => {
33
+ if (typeof window !== "undefined") return localStorage.getItem(AUTH_TOKEN_KEY) || "";
34
+ return "";
35
+ }
36
+ } }
37
+ });
20
38
  }
39
+ /**
40
+ * Sign in with Google via OAuth redirect flow.
41
+ *
42
+ * Redirects the browser to Google's OAuth page. After authentication,
43
+ * Google redirects back to the Better Auth callback endpoint, which
44
+ * creates a session and redirects to `callbackURL`.
45
+ *
46
+ * Call `syncCookieSessionToStorage()` on app init to restore the
47
+ * session from the cookie after a redirect-based sign-in.
48
+ *
49
+ * @param authClient - Client created by createLeapifyAuthClient
50
+ * @param callbackURL - Path or URL to redirect to after successful auth (e.g. '/dashboard')
51
+ *
52
+ * @example
53
+ * import { signInWithGoogleRedirect } from 'leapify/client'
54
+ *
55
+ * document.getElementById('google-btn').onclick = () => {
56
+ * signInWithGoogleRedirect(authClient, '/dashboard')
57
+ * }
58
+ */
21
59
  async function signInWithGoogleRedirect(authClient, callbackURL) {
22
- await authClient.signIn.social({
23
- provider: "google",
24
- callbackURL
25
- });
60
+ await authClient.signIn.social({
61
+ provider: "google",
62
+ callbackURL
63
+ });
26
64
  }
65
+ /**
66
+ * Sync a cookie-based Better Auth session into localStorage.
67
+ *
68
+ * After an OAuth redirect flow, Better Auth stores the session in an
69
+ * HTTP-only cookie. This function reads that session via `getSession()`
70
+ * and stores the token in localStorage so that subsequent API calls
71
+ * using the Bearer token work correctly.
72
+ *
73
+ * Call this once on app initialization, before `initializeSession()`.
74
+ *
75
+ * @param authClient - Client created by createLeapifyAuthClient
76
+ *
77
+ * @example
78
+ * import { syncCookieSessionToStorage, initializeSession } from 'leapify/client'
79
+ *
80
+ * // On app mount:
81
+ * await syncCookieSessionToStorage(authClient)
82
+ * const user = await initializeSession(API_URL, getToken)
83
+ */
27
84
  async function syncCookieSessionToStorage(authClient) {
28
- try {
29
- const result = await authClient.getSession();
30
- const data = result?.data;
31
- const token = data?.session?.token;
32
- if (token) {
33
- localStorage.setItem(AUTH_TOKEN_KEY, token);
34
- }
35
- } catch {
36
- }
85
+ try {
86
+ const token = ((await authClient.getSession())?.data)?.session?.token;
87
+ if (token) localStorage.setItem(AUTH_TOKEN_KEY, token);
88
+ } catch {}
37
89
  }
90
+ /**
91
+ * Get the current bearer token from storage, or null for guests.
92
+ * Pass this to `createLeapifyClient` as the `getToken` option.
93
+ *
94
+ * @example
95
+ * import { createLeapifyClient } from 'leapify/client'
96
+ * import { createLeapifyAuthClient, getLeapifyToken } from 'leapify/client'
97
+ *
98
+ * const authClient = createLeapifyAuthClient(API_URL)
99
+ * const api = createLeapifyClient(API_URL, () => getLeapifyToken(authClient))
100
+ */
38
101
  async function getLeapifyToken(authClient) {
39
- if (typeof window !== "undefined") {
40
- return localStorage.getItem(AUTH_TOKEN_KEY);
41
- }
42
- return null;
102
+ if (typeof window !== "undefined") return localStorage.getItem(AUTH_TOKEN_KEY);
103
+ return null;
43
104
  }
105
+ /**
106
+ * Sign out the current user.
107
+ */
44
108
  async function signOut(authClient) {
45
- const result = await authClient.signOut();
46
- if (typeof window !== "undefined") {
47
- localStorage.removeItem(AUTH_TOKEN_KEY);
48
- }
49
- return result;
109
+ const result = await authClient.signOut();
110
+ if (typeof window !== "undefined") localStorage.removeItem(AUTH_TOKEN_KEY);
111
+ return result;
50
112
  }
51
-
52
- // src/client/turnstile.ts
53
- var TURNSTILE_VERIFY_PATH = "/.well-known/leapify/turnstile/verify";
113
+ //#endregion
114
+ //#region src/client/turnstile.ts
115
+ const TURNSTILE_VERIFY_PATH = "/.well-known/leapify/turnstile/verify";
54
116
  function getTurnstileSiteKey() {
55
- const config = window.__CONFIG__;
56
- return config?.turnstileSiteKey;
117
+ return window.__CONFIG__?.turnstileSiteKey;
57
118
  }
58
119
  function loadTurnstileScript() {
59
- return new Promise((resolve, reject) => {
60
- if (typeof window.turnstile !== "undefined") {
61
- resolve();
62
- return;
63
- }
64
- const script = document.createElement("script");
65
- script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
66
- script.async = true;
67
- script.defer = true;
68
- script.onload = () => resolve();
69
- script.onerror = () => reject(new Error("Failed to load Turnstile script"));
70
- document.head.appendChild(script);
71
- });
120
+ return new Promise((resolve, reject) => {
121
+ if (typeof window.turnstile !== "undefined") {
122
+ resolve();
123
+ return;
124
+ }
125
+ const script = document.createElement("script");
126
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
127
+ script.async = true;
128
+ script.defer = true;
129
+ script.onload = () => resolve();
130
+ script.onerror = () => reject(/* @__PURE__ */ new Error("Failed to load Turnstile script"));
131
+ document.head.appendChild(script);
132
+ });
72
133
  }
73
134
  function executeTurnstile(siteKey) {
74
- let widgetId;
75
- const cleanup = () => {
76
- if (widgetId && typeof window.turnstile?.remove === "function") {
77
- window.turnstile.remove(widgetId);
78
- }
79
- const el = document.getElementById("leapify-turnstile-container");
80
- el?.remove();
81
- };
82
- return new Promise((resolve) => {
83
- const container = document.createElement("div");
84
- container.id = "leapify-turnstile-container";
85
- container.style.display = "none";
86
- document.body.appendChild(container);
87
- const timer = setTimeout(() => {
88
- cleanup();
89
- resolve("");
90
- }, 3e3);
91
- widgetId = window.turnstile.render(`#${container.id}`, {
92
- sitekey: siteKey,
93
- callback: (token) => {
94
- clearTimeout(timer);
95
- cleanup();
96
- resolve(token);
97
- }
98
- });
99
- });
135
+ let widgetId;
136
+ const cleanup = () => {
137
+ if (widgetId && typeof window.turnstile?.remove === "function") window.turnstile.remove(widgetId);
138
+ document.getElementById("leapify-turnstile-container")?.remove();
139
+ };
140
+ return new Promise((resolve) => {
141
+ const container = document.createElement("div");
142
+ container.id = "leapify-turnstile-container";
143
+ container.style.display = "none";
144
+ document.body.appendChild(container);
145
+ const timer = setTimeout(() => {
146
+ cleanup();
147
+ resolve("");
148
+ }, 3e3);
149
+ widgetId = window.turnstile.render(`#${container.id}`, {
150
+ sitekey: siteKey,
151
+ callback: (token) => {
152
+ clearTimeout(timer);
153
+ cleanup();
154
+ resolve(token);
155
+ }
156
+ });
157
+ });
100
158
  }
159
+ /**
160
+ * Solve a Turnstile challenge and obtain a signed cookie from the backend.
161
+ *
162
+ * Loads the Turnstile script (if not already loaded), executes an invisible
163
+ * challenge, and posts the token to the backend verify endpoint. The server
164
+ * sets a signed cookie that bypasses Turnstile for subsequent requests.
165
+ *
166
+ * Call once on app initialization before any API requests.
167
+ *
168
+ * @param baseUrl - The Leapify Worker URL. If omitted, uses the current origin.
169
+ * @param siteKey - Turnstile site key. If omitted, reads from window.__CONFIG__.
170
+ * @returns `true` if the challenge was solved and cookie was set.
171
+ */
101
172
  async function solveTurnstileChallenge(baseUrl, siteKey) {
102
- siteKey = siteKey ?? getTurnstileSiteKey();
103
- if (!siteKey) return false;
104
- const base = baseUrl?.replace(/\/$/, "") ?? "";
105
- try {
106
- await loadTurnstileScript();
107
- const token = await executeTurnstile(siteKey);
108
- if (!token) return false;
109
- const res = await fetch(`${base}${TURNSTILE_VERIFY_PATH}`, {
110
- method: "POST",
111
- headers: { "Content-Type": "application/json" },
112
- body: JSON.stringify({ token }),
113
- credentials: "include"
114
- });
115
- return res.ok;
116
- } catch {
117
- return false;
118
- }
173
+ siteKey = siteKey ?? getTurnstileSiteKey();
174
+ if (!siteKey) return false;
175
+ const base = baseUrl?.replace(/\/$/, "") ?? "";
176
+ try {
177
+ await loadTurnstileScript();
178
+ const token = await executeTurnstile(siteKey);
179
+ if (!token) return false;
180
+ return (await fetch(`${base}${TURNSTILE_VERIFY_PATH}`, {
181
+ method: "POST",
182
+ headers: { "Content-Type": "application/json" },
183
+ body: JSON.stringify({ token }),
184
+ credentials: "include"
185
+ })).ok;
186
+ } catch {
187
+ return false;
188
+ }
119
189
  }
120
-
121
- // src/client/session.ts
190
+ //#endregion
191
+ //#region src/client/session.ts
192
+ /**
193
+ * Initialize a browser session: restore existing token and fetch profile.
194
+ *
195
+ * @param baseUrl - The Leapify Worker URL.
196
+ * @param getToken - Async function returning the current session token, or null.
197
+ * @returns The authenticated user profile, or null if not signed in.
198
+ */
122
199
  async function initializeSession(baseUrl, getToken) {
123
- const token = await getToken();
124
- if (!token) return null;
125
- const base = baseUrl.replace(/\/$/, "");
126
- const res = await fetch(`${base}/api/users/me`, {
127
- headers: { Authorization: `Bearer ${token}` }
128
- });
129
- if (!res.ok) return null;
130
- const body = await res.json().catch(() => ({}));
131
- return body.data ?? null;
200
+ const token = await getToken();
201
+ if (!token) return null;
202
+ const base = baseUrl.replace(/\/$/, "");
203
+ const res = await fetch(`${base}/api/users/me`, { headers: { Authorization: `Bearer ${token}` } });
204
+ if (!res.ok) return null;
205
+ return (await res.json().catch(() => ({}))).data ?? null;
132
206
  }
133
-
134
- // src/client/index.ts
207
+ //#endregion
208
+ //#region src/client/index.ts
209
+ /**
210
+ * Read the runtime config injected by the worker into HTML pages.
211
+ * Returns null if not running in a browser or config not injected.
212
+ */
135
213
  function getClientConfig() {
136
- if (typeof window === "undefined") return null;
137
- const config = window.__CONFIG__;
138
- if (!config || typeof config !== "object") return null;
139
- return config;
214
+ if (typeof window === "undefined") return null;
215
+ const config = window.__CONFIG__;
216
+ if (!config || typeof config !== "object") return null;
217
+ return config;
140
218
  }
219
+ /**
220
+ * Structured error thrown by all client methods on non-2xx responses.
221
+ *
222
+ * @example
223
+ * import { LeapifyApiError } from 'leapify/client'
224
+ *
225
+ * try {
226
+ * await api.toggleBookmark(eventId)
227
+ * } catch (err) {
228
+ * if (err instanceof LeapifyApiError && err.code === 'UNAUTHORIZED') {
229
+ * // redirect to sign-in
230
+ * }
231
+ * }
232
+ */
141
233
  var LeapifyApiError = class extends Error {
142
- constructor(status, code, message) {
143
- super(message);
144
- this.status = status;
145
- this.code = code;
146
- this.name = "LeapifyApiError";
147
- }
148
- status;
149
- code;
234
+ status;
235
+ code;
236
+ constructor(status, code, message) {
237
+ super(message);
238
+ this.status = status;
239
+ this.code = code;
240
+ this.name = "LeapifyApiError";
241
+ }
150
242
  };
151
- var LEAPIFY_ERROR_CODES = {
152
- UNAUTHORIZED: "UNAUTHORIZED",
153
- DOMAIN_RESTRICTED: "DOMAIN_RESTRICTED",
154
- FORBIDDEN: "FORBIDDEN",
155
- NOT_FOUND: "NOT_FOUND",
156
- CONFLICT: "CONFLICT",
157
- TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
158
- SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
159
- INTERNAL_ERROR: "INTERNAL_ERROR"
243
+ const LEAPIFY_ERROR_CODES = {
244
+ UNAUTHORIZED: "UNAUTHORIZED",
245
+ DOMAIN_RESTRICTED: "DOMAIN_RESTRICTED",
246
+ FORBIDDEN: "FORBIDDEN",
247
+ NOT_FOUND: "NOT_FOUND",
248
+ CONFLICT: "CONFLICT",
249
+ TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS",
250
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
251
+ INTERNAL_ERROR: "INTERNAL_ERROR"
160
252
  };
161
253
  async function buildHeaders(getToken, extra = {}) {
162
- const headers = {
163
- "Content-Type": "application/json",
164
- ...extra
165
- };
166
- if (getToken) {
167
- const token = await getToken();
168
- if (token) headers["Authorization"] = `Bearer ${token}`;
169
- }
170
- return headers;
254
+ const headers = {
255
+ "Content-Type": "application/json",
256
+ ...extra
257
+ };
258
+ if (getToken) {
259
+ const token = await getToken();
260
+ if (token) headers["Authorization"] = `Bearer ${token}`;
261
+ }
262
+ return headers;
171
263
  }
172
264
  async function parseResponse(res) {
173
- if (res.status === 204) return void 0;
174
- const body = await res.json().catch(() => ({}));
175
- if (!res.ok) {
176
- const err = body?.error;
177
- throw new LeapifyApiError(
178
- res.status,
179
- err?.code ?? "UNKNOWN",
180
- err?.message ?? res.statusText
181
- );
182
- }
183
- return body.data;
265
+ if (res.status === 204) return void 0;
266
+ const body = await res.json().catch(() => ({}));
267
+ if (!res.ok) {
268
+ const err = body?.error;
269
+ throw new LeapifyApiError(res.status, err?.code ?? "UNKNOWN", err?.message ?? res.statusText);
270
+ }
271
+ return body.data;
184
272
  }
273
+ /**
274
+ * Creates a typed Leapify API client bound to a base URL.
275
+ *
276
+ * @param baseUrl - The deployed Leapify Worker URL (e.g. `https://api.leap.yourdomain.com`).
277
+ * @param getToken - Optional async function that returns a session token string,
278
+ * or null for guest requests. Use `getLeapifyToken()` from this module.
279
+ *
280
+ * @example
281
+ * // lib/api.ts
282
+ * import { createLeapifyClient, getLeapifyToken } from 'leapify/client'
283
+ *
284
+ * export const api = createLeapifyClient(
285
+ * process.env.NEXT_PUBLIC_API_URL!,
286
+ * () => getLeapifyToken(),
287
+ * )
288
+ */
185
289
  function createLeapifyClient(baseUrl, getToken) {
186
- const base = baseUrl.replace(/\/$/, "");
187
- async function get(path, init) {
188
- const headers = await buildHeaders(getToken, init?.headers);
189
- const res = await fetch(`${base}${path}`, { ...init, method: "GET", headers });
190
- return parseResponse(res);
191
- }
192
- async function post(path, body) {
193
- const headers = await buildHeaders(getToken);
194
- const res = await fetch(`${base}${path}`, {
195
- method: "POST",
196
- headers,
197
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
198
- });
199
- return parseResponse(res);
200
- }
201
- async function postFormData(path, formData) {
202
- const headers = {};
203
- if (getToken) {
204
- const token = await getToken();
205
- if (token) headers["Authorization"] = `Bearer ${token}`;
206
- }
207
- const res = await fetch(`${base}${path}`, {
208
- method: "POST",
209
- headers,
210
- body: formData
211
- });
212
- return parseResponse(res);
213
- }
214
- async function patch(path, body) {
215
- const headers = await buildHeaders(getToken);
216
- const res = await fetch(`${base}${path}`, {
217
- method: "PATCH",
218
- headers,
219
- body: JSON.stringify(body)
220
- });
221
- return parseResponse(res);
222
- }
223
- async function del(path) {
224
- const headers = await buildHeaders(getToken);
225
- const res = await fetch(`${base}${path}`, { method: "DELETE", headers });
226
- return parseResponse(res);
227
- }
228
- return {
229
- // ── Site Config ────────────────────────────────────────────────────────
230
- /**
231
- * GET /config
232
- * Returns site-wide configuration. Check `maintenanceMode` and
233
- * `comingSoonUntil` on app load to gate the UI appropriately.
234
- * Use `now` (server unix epoch) for timestamp comparisons.
235
- */
236
- getConfig() {
237
- return get("/api/config");
238
- },
239
- /**
240
- * PATCH /api/config/:key — admin only.
241
- * Upserts a site config value. Requires admin or super_admin role.
242
- */
243
- updateConfig(key, value) {
244
- return patch(`/api/config/${encodeURIComponent(key)}`, { value });
245
- },
246
- // ── Events ─────────────────────────────────────────────────────────────
247
- /**
248
- * GET /api/classes
249
- * Returns all published classes. Response is ETag-cached for 7 days.
250
- */
251
- getEvents() {
252
- return get("/api/classes");
253
- },
254
- /**
255
- * GET /api/classes/admin — admin only.
256
- * Returns all classes regardless of status.
257
- */
258
- getAdminEvents() {
259
- return get("/api/classes/admin");
260
- },
261
- /**
262
- * POST /api/classes/admin/publish — admin only.
263
- * Batch publish queued classes immediately or schedule them for later.
264
- */
265
- batchPublish(ids, releaseAt) {
266
- return post("/api/classes/admin/publish", { ids, releaseAt });
267
- },
268
- /**
269
- * GET /api/classes/:slug
270
- * Returns a single published class by slug.
271
- */
272
- getEvent(slug) {
273
- return get(`/api/classes/${encodeURIComponent(slug)}`);
274
- },
275
- /**
276
- * GET /api/classes/:slug/slots
277
- * Returns real-time slot availability. CF edge caches this for 5 seconds.
278
- * Poll every 8–10 seconds on class detail pages.
279
- */
280
- getSlots(slug) {
281
- return get(`/api/classes/${encodeURIComponent(slug)}/slots`);
282
- },
283
- /**
284
- * POST /api/classes/:slug/reconcile — admin only.
285
- * Corrects slot count for a single event by fetching the real Google Forms response count.
286
- */
287
- reconcileEvent(slug) {
288
- return post(`/api/classes/${encodeURIComponent(slug)}/reconcile`);
289
- },
290
- /**
291
- * POST /api/classes — admin only.
292
- * Creates a new class. Auto-generates slug from title.
293
- */
294
- createEvent(data) {
295
- return post("/api/classes", data);
296
- },
297
- /**
298
- * PATCH /api/classes/:slug — admin only.
299
- * Updates an existing class by slug.
300
- */
301
- updateEvent(slug, data) {
302
- return patch(`/api/classes/${encodeURIComponent(slug)}`, data);
303
- },
304
- /**
305
- * DELETE /api/classes/:slug — admin only.
306
- * Deletes a class.
307
- */
308
- deleteEvent(slug) {
309
- return del(`/api/classes/${encodeURIComponent(slug)}`);
310
- },
311
- // ── Themes ─────────────────────────────────────────────────────────────
312
- /**
313
- * GET /api/themes
314
- * Returns all themes.
315
- */
316
- getThemes() {
317
- return get("/api/themes");
318
- },
319
- /**
320
- * POST /api/themes — admin only.
321
- */
322
- createTheme(data) {
323
- return post("/api/themes", data);
324
- },
325
- /**
326
- * PATCH /api/themes/:id — admin only.
327
- */
328
- updateTheme(id, data) {
329
- return patch(`/api/themes/${encodeURIComponent(id)}`, data);
330
- },
331
- /**
332
- * DELETE /api/themes/:id — admin only.
333
- */
334
- deleteTheme(id) {
335
- return del(`/api/themes/${encodeURIComponent(id)}`);
336
- },
337
- // ── Organizations ──────────────────────────────────────────────────────
338
- /**
339
- * GET /api/organizations
340
- * Returns all organizations.
341
- */
342
- getOrganizations() {
343
- return get("/api/organizations");
344
- },
345
- /**
346
- * POST /api/organizations — admin only.
347
- */
348
- createOrganization(data) {
349
- return post("/api/organizations", data);
350
- },
351
- /**
352
- * PATCH /api/organizations/:id — admin only.
353
- */
354
- updateOrganization(id, data) {
355
- return patch(`/api/organizations/${encodeURIComponent(id)}`, data);
356
- },
357
- /**
358
- * DELETE /api/organizations/:id — admin only.
359
- */
360
- deleteOrganization(id) {
361
- return del(`/api/organizations/${encodeURIComponent(id)}`);
362
- },
363
- // ── Users ──────────────────────────────────────────────────────────────
364
- /**
365
- * GET /api/users/me
366
- * Returns the authenticated user's profile, or null for guests.
367
- * Use `profile.role` to gate admin UI.
368
- */
369
- getMe() {
370
- return get("/api/users/me");
371
- },
372
- // ── Admin: User Management ────────────────────────────────────────────
373
- /**
374
- * GET /api/users admin only.
375
- * Returns all registered users.
376
- */
377
- getUsers() {
378
- return get("/api/users");
379
- },
380
- /**
381
- * PATCH /api/users/:id/role admin only.
382
- * Changes a user's role.
383
- */
384
- updateUserRole(id, role) {
385
- return patch(`/api/users/${encodeURIComponent(id)}/role`, { role });
386
- },
387
- /**
388
- * POST /api/users/by-email admin only.
389
- * Finds or creates a user by email and sets their role.
390
- */
391
- upsertUserByEmail(email, role) {
392
- return post("/api/users/by-email", { email, role });
393
- },
394
- // ── Bookmarks ──────────────────────────────────────────────────────────
395
- /**
396
- * GET /api/users/me/bookmarks
397
- * Returns the authenticated user's bookmarked events.
398
- * Returns an empty array for unauthenticated users.
399
- */
400
- getBookmarks() {
401
- return get("/api/users/me/bookmarks");
402
- },
403
- /**
404
- * POST /api/users/me/bookmarks/:eventId
405
- * Toggles a bookmark on/off. Requires authentication.
406
- * Returns `{ bookmarked: true }` (201) on add, `{ bookmarked: false }` (200) on remove.
407
- */
408
- toggleBookmark(eventId) {
409
- return post(
410
- `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`
411
- );
412
- },
413
- /**
414
- * DELETE /api/users/me/bookmarks/:eventId
415
- * Removes a bookmark. Requires authentication.
416
- */
417
- deleteBookmark(eventId) {
418
- return del(
419
- `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`
420
- );
421
- },
422
- // ── FAQs ───────────────────────────────────────────────────────────────
423
- /**
424
- * GET /api/faqs
425
- * Returns all active FAQs. Cached in KV for 10 minutes.
426
- * The `answer` field is markdown — render with a markdown library.
427
- */
428
- getFaqs() {
429
- return get("/api/faqs");
430
- },
431
- /**
432
- * POST /api/faqs — admin only.
433
- * Creates a new FAQ item.
434
- */
435
- createFaq(data) {
436
- return post("/api/faqs", data);
437
- },
438
- /**
439
- * PATCH /api/faqs/:id — admin only.
440
- * Updates an existing FAQ item.
441
- */
442
- updateFaq(id, data) {
443
- return patch(`/api/faqs/${encodeURIComponent(id)}`, data);
444
- },
445
- /**
446
- * DELETE /api/faqs/:id — admin only.
447
- * Soft-deletes a FAQ (sets isActive: false).
448
- */
449
- deleteFaq(id) {
450
- return del(`/api/faqs/${encodeURIComponent(id)}`);
451
- },
452
- // ── Uploads ────────────────────────────────────────────────────────────
453
- /**
454
- * POST /api/uploads admin only.
455
- * Uploads an image file to R2. Accepts multipart/form-data.
456
- * Returns the public URL, storage key, size, and content type.
457
- */
458
- uploadImage(file) {
459
- const formData = new FormData();
460
- formData.append("file", file);
461
- return postFormData("/api/uploads", formData);
462
- },
463
- // ── Health ─────────────────────────────────────────────────────────────
464
- /**
465
- * GET /health
466
- * Public health check. Returns provider availability status.
467
- */
468
- healthCheck() {
469
- return get("/health");
470
- }
471
- };
290
+ const base = baseUrl.replace(/\/$/, "");
291
+ async function get(path, init) {
292
+ const headers = await buildHeaders(getToken, init?.headers);
293
+ return parseResponse(await fetch(`${base}${path}`, {
294
+ ...init,
295
+ method: "GET",
296
+ headers
297
+ }));
298
+ }
299
+ async function post(path, body) {
300
+ const headers = await buildHeaders(getToken);
301
+ return parseResponse(await fetch(`${base}${path}`, {
302
+ method: "POST",
303
+ headers,
304
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
305
+ }));
306
+ }
307
+ async function postFormData(path, formData) {
308
+ const headers = {};
309
+ if (getToken) {
310
+ const token = await getToken();
311
+ if (token) headers["Authorization"] = `Bearer ${token}`;
312
+ }
313
+ return parseResponse(await fetch(`${base}${path}`, {
314
+ method: "POST",
315
+ headers,
316
+ body: formData
317
+ }));
318
+ }
319
+ async function patch(path, body) {
320
+ const headers = await buildHeaders(getToken);
321
+ return parseResponse(await fetch(`${base}${path}`, {
322
+ method: "PATCH",
323
+ headers,
324
+ body: JSON.stringify(body)
325
+ }));
326
+ }
327
+ async function del(path) {
328
+ const headers = await buildHeaders(getToken);
329
+ return parseResponse(await fetch(`${base}${path}`, {
330
+ method: "DELETE",
331
+ headers
332
+ }));
333
+ }
334
+ return {
335
+ /**
336
+ * GET /config
337
+ * Returns site-wide configuration. Check `maintenanceMode` and
338
+ * `comingSoonUntil` on app load to gate the UI appropriately.
339
+ * Use `now` (server unix epoch) for timestamp comparisons.
340
+ */
341
+ getConfig() {
342
+ return get("/api/config");
343
+ },
344
+ /**
345
+ * PATCH /api/config/:key admin only.
346
+ * Upserts a site config value. Requires admin or super_admin role.
347
+ */
348
+ updateConfig(key, value) {
349
+ return patch(`/api/config/${encodeURIComponent(key)}`, { value });
350
+ },
351
+ /**
352
+ * GET /api/classes
353
+ * Returns all published classes. Response is ETag-cached for 7 days.
354
+ */
355
+ getEvents() {
356
+ return get("/api/classes");
357
+ },
358
+ /**
359
+ * GET /api/classes/admin — admin only.
360
+ * Returns all classes regardless of status.
361
+ */
362
+ getAdminEvents() {
363
+ return get("/api/classes/admin");
364
+ },
365
+ /**
366
+ * POST /api/classes/admin/publish — admin only.
367
+ * Batch publish queued classes immediately or schedule them for later.
368
+ */
369
+ batchPublish(ids, releaseAt) {
370
+ return post("/api/classes/admin/publish", {
371
+ ids,
372
+ releaseAt
373
+ });
374
+ },
375
+ /**
376
+ * GET /api/classes/:slug
377
+ * Returns a single published class by slug.
378
+ */
379
+ getEvent(slug) {
380
+ return get(`/api/classes/${encodeURIComponent(slug)}`);
381
+ },
382
+ /**
383
+ * GET /api/classes/:slug/slots
384
+ * Returns real-time slot availability. CF edge caches this for 5 seconds.
385
+ * Poll every 8–10 seconds on class detail pages.
386
+ */
387
+ getSlots(slug) {
388
+ return get(`/api/classes/${encodeURIComponent(slug)}/slots`);
389
+ },
390
+ /**
391
+ * POST /api/classes/:slug/reconcile — admin only.
392
+ * Corrects slot count for a single event by fetching the real Google Forms response count.
393
+ */
394
+ reconcileEvent(slug) {
395
+ return post(`/api/classes/${encodeURIComponent(slug)}/reconcile`);
396
+ },
397
+ /**
398
+ * POST /api/classes — admin only.
399
+ * Creates a new class. Auto-generates slug from title.
400
+ */
401
+ createEvent(data) {
402
+ return post("/api/classes", data);
403
+ },
404
+ /**
405
+ * PATCH /api/classes/:slug admin only.
406
+ * Updates an existing class by slug.
407
+ */
408
+ updateEvent(slug, data) {
409
+ return patch(`/api/classes/${encodeURIComponent(slug)}`, data);
410
+ },
411
+ /**
412
+ * DELETE /api/classes/:slug — admin only.
413
+ * Deletes a class.
414
+ */
415
+ deleteEvent(slug) {
416
+ return del(`/api/classes/${encodeURIComponent(slug)}`);
417
+ },
418
+ /**
419
+ * GET /api/themes
420
+ * Returns all themes.
421
+ */
422
+ getThemes() {
423
+ return get("/api/themes");
424
+ },
425
+ /**
426
+ * POST /api/themes — admin only.
427
+ */
428
+ createTheme(data) {
429
+ return post("/api/themes", data);
430
+ },
431
+ /**
432
+ * PATCH /api/themes/:id admin only.
433
+ */
434
+ updateTheme(id, data) {
435
+ return patch(`/api/themes/${encodeURIComponent(id)}`, data);
436
+ },
437
+ /**
438
+ * DELETE /api/themes/:id — admin only.
439
+ */
440
+ deleteTheme(id) {
441
+ return del(`/api/themes/${encodeURIComponent(id)}`);
442
+ },
443
+ /**
444
+ * GET /api/organizations
445
+ * Returns all organizations.
446
+ */
447
+ getOrganizations() {
448
+ return get("/api/organizations");
449
+ },
450
+ /**
451
+ * POST /api/organizations — admin only.
452
+ */
453
+ createOrganization(data) {
454
+ return post("/api/organizations", data);
455
+ },
456
+ /**
457
+ * PATCH /api/organizations/:id — admin only.
458
+ */
459
+ updateOrganization(id, data) {
460
+ return patch(`/api/organizations/${encodeURIComponent(id)}`, data);
461
+ },
462
+ /**
463
+ * DELETE /api/organizations/:id — admin only.
464
+ */
465
+ deleteOrganization(id) {
466
+ return del(`/api/organizations/${encodeURIComponent(id)}`);
467
+ },
468
+ /**
469
+ * GET /api/users/me
470
+ * Returns the authenticated user's profile, or null for guests.
471
+ * Use `profile.role` to gate admin UI.
472
+ */
473
+ getMe() {
474
+ return get("/api/users/me");
475
+ },
476
+ /**
477
+ * GET /api/users — admin only.
478
+ * Returns all registered users.
479
+ */
480
+ getUsers() {
481
+ return get("/api/users");
482
+ },
483
+ /**
484
+ * PATCH /api/users/:id/role — admin only.
485
+ * Changes a user's role.
486
+ */
487
+ updateUserRole(id, role) {
488
+ return patch(`/api/users/${encodeURIComponent(id)}/role`, { role });
489
+ },
490
+ /**
491
+ * POST /api/users/by-email — admin only.
492
+ * Finds or creates a user by email and sets their role.
493
+ */
494
+ upsertUserByEmail(email, role) {
495
+ return post("/api/users/by-email", {
496
+ email,
497
+ role
498
+ });
499
+ },
500
+ /**
501
+ * GET /api/users/me/bookmarks
502
+ * Returns the authenticated user's bookmarked events.
503
+ * Returns an empty array for unauthenticated users.
504
+ */
505
+ getBookmarks() {
506
+ return get("/api/users/me/bookmarks");
507
+ },
508
+ /**
509
+ * POST /api/users/me/bookmarks/:eventId
510
+ * Toggles a bookmark on/off. Requires authentication.
511
+ * Returns `{ bookmarked: true }` (201) on add, `{ bookmarked: false }` (200) on remove.
512
+ */
513
+ toggleBookmark(eventId) {
514
+ return post(`/api/users/me/bookmarks/${encodeURIComponent(eventId)}`);
515
+ },
516
+ /**
517
+ * DELETE /api/users/me/bookmarks/:eventId
518
+ * Removes a bookmark. Requires authentication.
519
+ */
520
+ deleteBookmark(eventId) {
521
+ return del(`/api/users/me/bookmarks/${encodeURIComponent(eventId)}`);
522
+ },
523
+ /**
524
+ * GET /api/faqs
525
+ * Returns all active FAQs. Cached in KV for 10 minutes.
526
+ * The `answer` field is markdown — render with a markdown library.
527
+ */
528
+ getFaqs() {
529
+ return get("/api/faqs");
530
+ },
531
+ /**
532
+ * POST /api/faqs — admin only.
533
+ * Creates a new FAQ item.
534
+ */
535
+ createFaq(data) {
536
+ return post("/api/faqs", data);
537
+ },
538
+ /**
539
+ * PATCH /api/faqs/:id — admin only.
540
+ * Updates an existing FAQ item.
541
+ */
542
+ updateFaq(id, data) {
543
+ return patch(`/api/faqs/${encodeURIComponent(id)}`, data);
544
+ },
545
+ /**
546
+ * DELETE /api/faqs/:id admin only.
547
+ * Soft-deletes a FAQ (sets isActive: false).
548
+ */
549
+ deleteFaq(id) {
550
+ return del(`/api/faqs/${encodeURIComponent(id)}`);
551
+ },
552
+ /**
553
+ * POST /api/uploads — admin only.
554
+ * Uploads an image file to R2. Accepts multipart/form-data.
555
+ * Returns the public URL, storage key, size, and content type.
556
+ */
557
+ uploadImage(file) {
558
+ const formData = new FormData();
559
+ formData.append("file", file);
560
+ return postFormData("/api/uploads", formData);
561
+ },
562
+ /**
563
+ * GET /health
564
+ * Public health check. Returns provider availability status.
565
+ */
566
+ healthCheck() {
567
+ return get("/health");
568
+ }
569
+ };
472
570
  }
473
-
571
+ //#endregion
474
572
  export { LEAPIFY_ERROR_CODES, LeapifyApiError, createLeapifyAuthClient, createLeapifyClient, getClientConfig, getLeapifyToken, initializeSession, signInWithGoogleRedirect, signOut, solveTurnstileChallenge, syncCookieSessionToStorage };
475
- //# sourceMappingURL=index.js.map
476
- //# sourceMappingURL=index.js.map