@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.
- package/dist/auth/auth.d.ts +5 -5
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/client/auth.d.ts +64 -60
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/index.cjs +541 -446
- package/dist/client/index.js +540 -444
- package/dist/client/types.cjs +0 -4
- package/dist/client/types.js +1 -3
- package/dist/index.cjs +2700 -2972
- package/dist/index.js +2698 -2969
- package/dist/lib/middleware/turnstile-challenge.cjs +145 -29
- package/dist/lib/middleware/turnstile-challenge.js +140 -4
- package/dist/worker.js +2758 -3049
- package/package.json +157 -156
- package/dist/chunk-NYEPGZMP.cjs +0 -171
- package/dist/chunk-NYEPGZMP.cjs.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -9
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-Q7SFCCGT.cjs +0 -11
- package/dist/chunk-Q7SFCCGT.cjs.map +0 -1
- package/dist/chunk-WEW5LGZC.js +0 -165
- package/dist/chunk-WEW5LGZC.js.map +0 -1
- package/dist/client/index.cjs.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/types.cjs.map +0 -1
- package/dist/client/types.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/middleware/turnstile-challenge.cjs.map +0 -1
- package/dist/lib/middleware/turnstile-challenge.js.map +0 -1
- package/dist/worker.js.map +0 -1
package/dist/client/index.js
CHANGED
|
@@ -1,476 +1,572 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/client/turnstile.ts
|
|
115
|
+
const TURNSTILE_VERIFY_PATH = "/.well-known/leapify/turnstile/verify";
|
|
54
116
|
function getTurnstileSiteKey() {
|
|
55
|
-
|
|
56
|
-
return config?.turnstileSiteKey;
|
|
117
|
+
return window.__CONFIG__?.turnstileSiteKey;
|
|
57
118
|
}
|
|
58
119
|
function loadTurnstileScript() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|