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