@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.
- 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 -444
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +540 -442
- package/dist/client/types.cjs +0 -4
- package/dist/client/types.js +1 -3
- package/dist/index.cjs +2700 -2996
- package/dist/index.js +2698 -2993
- package/dist/lib/middleware/turnstile-challenge.cjs +145 -29
- package/dist/lib/middleware/turnstile-challenge.js +140 -4
- package/dist/routes/internal/gforms-webhook.d.ts.map +1 -1
- package/dist/services/slots.d.ts +6 -22
- package/dist/services/slots.d.ts.map +1 -1
- package/dist/worker.js +2758 -3073
- package/package.json +157 -157
- 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/chunk-WEW5LGZC.js
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { createMiddleware } from 'hono/factory';
|
|
2
|
-
|
|
3
|
-
// src/lib/middleware/turnstile-challenge.ts
|
|
4
|
-
var TURNSTILE_PATH = "/.well-known/leapify/turnstile";
|
|
5
|
-
var TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`;
|
|
6
|
-
var TURNSTILE_COOKIE_NAME = "leapify-turnstile";
|
|
7
|
-
var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
8
|
-
var COOKIE_MAX_AGE_SEC = 86400;
|
|
9
|
-
var EXEMPT_PATHS = [
|
|
10
|
-
"/health",
|
|
11
|
-
"/internal",
|
|
12
|
-
"/api/auth",
|
|
13
|
-
"/api/uploads/images",
|
|
14
|
-
"/api/classes",
|
|
15
|
-
"/api/faqs",
|
|
16
|
-
"/api/config",
|
|
17
|
-
"/api/themes",
|
|
18
|
-
"/api/organizations",
|
|
19
|
-
"/api/docs",
|
|
20
|
-
"/api/openapi.json",
|
|
21
|
-
TURNSTILE_VERIFY_PATH
|
|
22
|
-
];
|
|
23
|
-
function base64urlEncode(bytes) {
|
|
24
|
-
let binary = "";
|
|
25
|
-
for (const byte of bytes) {
|
|
26
|
-
binary += String.fromCharCode(byte);
|
|
27
|
-
}
|
|
28
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
29
|
-
}
|
|
30
|
-
function base64urlDecode(str) {
|
|
31
|
-
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
32
|
-
const binary = atob(padded);
|
|
33
|
-
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
34
|
-
for (let i = 0; i < binary.length; i++) {
|
|
35
|
-
bytes[i] = binary.charCodeAt(i);
|
|
36
|
-
}
|
|
37
|
-
return bytes;
|
|
38
|
-
}
|
|
39
|
-
async function importHmacKey(secret) {
|
|
40
|
-
return crypto.subtle.importKey(
|
|
41
|
-
"raw",
|
|
42
|
-
new TextEncoder().encode(secret),
|
|
43
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
44
|
-
false,
|
|
45
|
-
["sign", "verify"]
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
async function signCookie(secret, ip) {
|
|
49
|
-
const ts = Date.now();
|
|
50
|
-
const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)));
|
|
51
|
-
const payload = `${ip}:${ts}:${nonce}`;
|
|
52
|
-
const key = await importHmacKey(secret);
|
|
53
|
-
const sig = await crypto.subtle.sign(
|
|
54
|
-
"HMAC",
|
|
55
|
-
key,
|
|
56
|
-
new TextEncoder().encode(payload)
|
|
57
|
-
);
|
|
58
|
-
const sigB64 = base64urlEncode(new Uint8Array(sig));
|
|
59
|
-
return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`;
|
|
60
|
-
}
|
|
61
|
-
async function validateCookie(secret, cookie, ip) {
|
|
62
|
-
try {
|
|
63
|
-
const [payloadB64, sigB64] = cookie.split(".");
|
|
64
|
-
if (!payloadB64 || !sigB64) return false;
|
|
65
|
-
const payloadBytes = base64urlDecode(payloadB64);
|
|
66
|
-
const sigBytes = base64urlDecode(sigB64);
|
|
67
|
-
const key = await importHmacKey(secret);
|
|
68
|
-
const valid = await crypto.subtle.verify(
|
|
69
|
-
"HMAC",
|
|
70
|
-
key,
|
|
71
|
-
sigBytes,
|
|
72
|
-
payloadBytes
|
|
73
|
-
);
|
|
74
|
-
if (!valid) return false;
|
|
75
|
-
const payload = new TextDecoder().decode(payloadBytes);
|
|
76
|
-
const [cookieIp, tsStr] = payload.split(":");
|
|
77
|
-
if (cookieIp !== ip) return false;
|
|
78
|
-
const ts = parseInt(tsStr, 10);
|
|
79
|
-
if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1e3) return false;
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function getClientIp(c) {
|
|
86
|
-
return c.req.header("CF-Connecting-IP") ?? c.req.header("X-Real-IP") ?? c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ?? "unknown";
|
|
87
|
-
}
|
|
88
|
-
function isExempt(path) {
|
|
89
|
-
const normalized = path.toLowerCase().replace(/\/$/, "");
|
|
90
|
-
return EXEMPT_PATHS.some((p) => {
|
|
91
|
-
const ep = p.toLowerCase().replace(/\/$/, "");
|
|
92
|
-
return normalized === ep || normalized.startsWith(ep + "/");
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function setCookieHeader(c, token) {
|
|
96
|
-
const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
|
|
97
|
-
c.header(
|
|
98
|
-
"Set-Cookie",
|
|
99
|
-
`${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
async function handleTurnstileVerify(c) {
|
|
103
|
-
const body = await c.req.json();
|
|
104
|
-
const { token } = body;
|
|
105
|
-
if (!token) {
|
|
106
|
-
return c.json(
|
|
107
|
-
{ error: { code: "VALIDATION_ERROR", message: "Missing Turnstile token" } },
|
|
108
|
-
422
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
const secret = c.env.TURNSTILE_SECRET_KEY;
|
|
112
|
-
if (!secret) {
|
|
113
|
-
return c.json(
|
|
114
|
-
{ error: { code: "CONFIG_ERROR", message: "Turnstile not configured" } },
|
|
115
|
-
500
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const ip = getClientIp(c);
|
|
119
|
-
const formData = new URLSearchParams();
|
|
120
|
-
formData.append("secret", secret);
|
|
121
|
-
formData.append("response", token);
|
|
122
|
-
if (ip !== "unknown") {
|
|
123
|
-
formData.append("remoteip", ip);
|
|
124
|
-
}
|
|
125
|
-
const res = await fetch(VERIFY_URL, {
|
|
126
|
-
method: "POST",
|
|
127
|
-
body: formData
|
|
128
|
-
});
|
|
129
|
-
const outcome = await res.json();
|
|
130
|
-
if (!outcome.success) {
|
|
131
|
-
return c.json(
|
|
132
|
-
{ error: { code: "TURNSTILE_FAILED", message: "Turnstile verification failed", details: outcome["error-codes"] } },
|
|
133
|
-
403
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
const cookieToken = await signCookie(secret, ip);
|
|
137
|
-
setCookieHeader(c, cookieToken);
|
|
138
|
-
return c.json({ success: true });
|
|
139
|
-
}
|
|
140
|
-
function createTurnstileMiddleware() {
|
|
141
|
-
return createMiddleware(async (c, next) => {
|
|
142
|
-
if (isExempt(c.req.path)) return next();
|
|
143
|
-
if (c.req.method === "OPTIONS") return next();
|
|
144
|
-
if (c.req.header("Authorization")) return next();
|
|
145
|
-
const secret = c.env.TURNSTILE_SECRET_KEY;
|
|
146
|
-
if (!secret) return next();
|
|
147
|
-
const cookieHeader = c.req.header("Cookie") ?? "";
|
|
148
|
-
const cookieMatch = cookieHeader.match(
|
|
149
|
-
new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`)
|
|
150
|
-
);
|
|
151
|
-
if (cookieMatch) {
|
|
152
|
-
const ip = getClientIp(c);
|
|
153
|
-
const valid = await validateCookie(secret, cookieMatch[1], ip);
|
|
154
|
-
if (valid) return next();
|
|
155
|
-
}
|
|
156
|
-
return c.json(
|
|
157
|
-
{ error: { code: "TURNSTILE_REQUIRED", message: "Turnstile verification required" } },
|
|
158
|
-
401
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export { TURNSTILE_COOKIE_NAME, TURNSTILE_PATH, TURNSTILE_VERIFY_PATH, createTurnstileMiddleware, handleTurnstileVerify };
|
|
164
|
-
//# sourceMappingURL=chunk-WEW5LGZC.js.map
|
|
165
|
-
//# sourceMappingURL=chunk-WEW5LGZC.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/middleware/turnstile-challenge.ts"],"names":[],"mappings":"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","file":"chunk-WEW5LGZC.js","sourcesContent":["import { createMiddleware } from 'hono/factory'\r\nimport type { Context } from 'hono'\r\nimport type { LeapifyBindings } from '../../types'\r\n\r\nexport const TURNSTILE_PATH = '/.well-known/leapify/turnstile'\r\n\r\nexport const TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`\r\n\r\nexport const TURNSTILE_COOKIE_NAME = 'leapify-turnstile'\r\n\r\nconst VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'\r\n\r\nconst COOKIE_MAX_AGE_SEC = 86400\r\n\r\nconst EXEMPT_PATHS = [\r\n \"/health\",\r\n \"/internal\",\r\n \"/api/auth\",\r\n \"/api/uploads/images\",\r\n \"/api/classes\",\r\n \"/api/faqs\",\r\n \"/api/config\",\r\n \"/api/themes\",\r\n \"/api/organizations\",\r\n \"/api/docs\",\r\n \"/api/openapi.json\",\r\n TURNSTILE_VERIFY_PATH,\r\n];\r\n\r\nfunction base64urlEncode(bytes: Uint8Array): string {\r\n let binary = ''\r\n for (const byte of bytes) {\r\n binary += String.fromCharCode(byte)\r\n }\r\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\r\n}\r\n\r\nfunction base64urlDecode(str: string): Uint8Array<ArrayBuffer> {\r\n const padded = str.replace(/-/g, '+').replace(/_/g, '/')\r\n const binary = atob(padded)\r\n const bytes = new Uint8Array(new ArrayBuffer(binary.length))\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i)\r\n }\r\n return bytes\r\n}\r\n\r\nasync function importHmacKey(secret: string): Promise<CryptoKey> {\r\n return crypto.subtle.importKey(\r\n 'raw',\r\n new TextEncoder().encode(secret),\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n false,\r\n ['sign', 'verify']\r\n )\r\n}\r\n\r\nasync function signCookie(secret: string, ip: string): Promise<string> {\r\n const ts = Date.now()\r\n const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)))\r\n const payload = `${ip}:${ts}:${nonce}`\r\n const key = await importHmacKey(secret)\r\n const sig = await crypto.subtle.sign(\r\n 'HMAC',\r\n key,\r\n new TextEncoder().encode(payload)\r\n )\r\n const sigB64 = base64urlEncode(new Uint8Array(sig))\r\n return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`\r\n}\r\n\r\nasync function validateCookie(\r\n secret: string,\r\n cookie: string,\r\n ip: string\r\n): Promise<boolean> {\r\n try {\r\n const [payloadB64, sigB64] = cookie.split('.')\r\n if (!payloadB64 || !sigB64) return false\r\n\r\n const payloadBytes = base64urlDecode(payloadB64)\r\n const sigBytes = base64urlDecode(sigB64)\r\n\r\n const key = await importHmacKey(secret)\r\n const valid = await crypto.subtle.verify(\r\n 'HMAC',\r\n key,\r\n sigBytes,\r\n payloadBytes\r\n )\r\n if (!valid) return false\r\n\r\n const payload = new TextDecoder().decode(payloadBytes)\r\n const [cookieIp, tsStr] = payload.split(':')\r\n\r\n if (cookieIp !== ip) return false\r\n\r\n const ts = parseInt(tsStr, 10)\r\n if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1000) return false\r\n\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\nfunction getClientIp(c: Context<{ Bindings: LeapifyBindings }>): string {\r\n return (\r\n c.req.header('CF-Connecting-IP') ??\r\n c.req.header('X-Real-IP') ??\r\n c.req.header('X-Forwarded-For')?.split(',')[0]?.trim() ??\r\n 'unknown'\r\n )\r\n}\r\n\r\nfunction isExempt(path: string): boolean {\r\n const normalized = path.toLowerCase().replace(/\\/$/, '')\r\n return EXEMPT_PATHS.some((p) => {\r\n const ep = p.toLowerCase().replace(/\\/$/, '')\r\n return normalized === ep || normalized.startsWith(ep + '/')\r\n })\r\n}\r\n\r\nfunction setCookieHeader(c: Context<{ Bindings: LeapifyBindings }>, token: string): void {\r\n const isSecure = c.req.raw.url.startsWith(\"https\") || c.req.header(\"x-forwarded-proto\") === \"https\";\r\n c.header(\r\n \"Set-Cookie\",\r\n `${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${\r\n isSecure ? \"Secure; \" : \"\"\r\n }HttpOnly; SameSite=Lax`,\r\n );\r\n}\r\n\r\n/**\r\n * POST /.well-known/leapify/turnstile/verify\r\n *\r\n * Validates a Turnstile token and issues a signed cookie on success.\r\n */\r\nexport async function handleTurnstileVerify(\r\n c: Context<{ Bindings: LeapifyBindings }>\r\n) {\r\n const body = await c.req.json<{ token?: string }>()\r\n const { token } = body\r\n\r\n if (!token) {\r\n return c.json(\r\n { error: { code: 'VALIDATION_ERROR', message: 'Missing Turnstile token' } },\r\n 422\r\n )\r\n }\r\n\r\n const secret = c.env.TURNSTILE_SECRET_KEY\r\n if (!secret) {\r\n return c.json(\r\n { error: { code: 'CONFIG_ERROR', message: 'Turnstile not configured' } },\r\n 500\r\n )\r\n }\r\n\r\n const ip = getClientIp(c)\r\n const formData = new URLSearchParams()\r\n formData.append('secret', secret)\r\n formData.append('response', token)\r\n if (ip !== 'unknown') {\r\n formData.append('remoteip', ip)\r\n }\r\n\r\n const res = await fetch(VERIFY_URL, {\r\n method: 'POST',\r\n body: formData,\r\n })\r\n const outcome = await res.json() as { success: boolean; 'error-codes'?: string[] }\r\n\r\n if (!outcome.success) {\r\n return c.json(\r\n { error: { code: 'TURNSTILE_FAILED', message: 'Turnstile verification failed', details: outcome['error-codes'] } },\r\n 403\r\n )\r\n }\r\n\r\n const cookieToken = await signCookie(secret, ip)\r\n setCookieHeader(c, cookieToken)\r\n\r\n return c.json({ success: true })\r\n}\r\n\r\n/**\r\n * Turnstile challenge middleware.\r\n *\r\n * Requires a valid Turnstile-signed cookie on all non-exempt requests.\r\n * The client must first solve a Turnstile challenge and POST the token\r\n * to the verify endpoint to obtain the cookie.\r\n *\r\n * Exempt paths: /health, /internal, /api/auth, /api/uploads/images,\r\n * and the verify endpoint itself.\r\n */\r\nexport function createTurnstileMiddleware() {\r\n return createMiddleware<{ Bindings: LeapifyBindings }>(async (c, next) => {\r\n if (isExempt(c.req.path)) return next()\r\n\r\n if (c.req.method === 'OPTIONS') return next()\r\n\r\n // Skip challenge for authenticated requests (Bearer token present)\r\n // The auth middleware will handle session validation instead.\r\n if (c.req.header('Authorization')) return next()\r\n\r\n const secret = c.env.TURNSTILE_SECRET_KEY\r\n if (!secret) return next()\r\n\r\n const cookieHeader = c.req.header('Cookie') ?? ''\r\n const cookieMatch = cookieHeader.match(\r\n new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`)\r\n )\r\n if (cookieMatch) {\r\n const ip = getClientIp(c)\r\n const valid = await validateCookie(secret, cookieMatch[1], ip)\r\n if (valid) return next()\r\n }\r\n\r\n return c.json(\r\n { error: { code: 'TURNSTILE_REQUIRED', message: 'Turnstile verification required' } },\r\n 401\r\n )\r\n })\r\n}\r\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/auth.ts","../../src/client/turnstile.ts","../../src/client/session.ts","../../src/client/index.ts"],"names":["createAuthClient"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * Better Auth client helper for Leapify API consumers.\r\n *\r\n * This module is **browser-safe** — no Cloudflare, Drizzle, or Hono deps.\r\n * It wraps Better Auth's client SDK with the bearer plugin so that tokens\r\n * can be stored and retrieved as plain strings (no cookie dependency on\r\n * the consumer's frontend).\r\n *\r\n * @example\r\n * // lib/auth.ts (frontend)\r\n * import { createLeapifyAuthClient, signInWithGoogleRedirect } from 'leapify/client'\r\n *\r\n * export const authClient = createLeapifyAuthClient(process.env.NEXT_PUBLIC_API_URL!)\r\n *\r\n * // Redirect-based Google sign-in:\r\n * await signInWithGoogleRedirect(authClient, '/dashboard')\r\n */\r\n\r\nimport { createAuthClient } from 'better-auth/client'\r\n\r\nconst AUTH_TOKEN_KEY = 'better-auth.session_token'\r\n\r\n/**\r\n * Create a Better Auth client bound to the Leapify Worker URL.\r\n *\r\n * It uses the 'Bearer' auth type to send the stored session token\r\n * in the Authorization header.\r\n */\r\nexport function createLeapifyAuthClient(baseUrl: string) {\r\n return createAuthClient({\r\n baseURL: baseUrl,\r\n fetchOptions: {\r\n auth: {\r\n type: 'Bearer',\r\n token: () => {\r\n if (typeof window !== 'undefined') {\r\n return localStorage.getItem(AUTH_TOKEN_KEY) || ''\r\n }\r\n return ''\r\n }\r\n }\r\n }\r\n })\r\n}\r\n\r\nexport type LeapifyAuthClient = ReturnType<typeof createLeapifyAuthClient>\r\n\r\n/**\r\n * Sign in with Google via OAuth redirect flow.\r\n *\r\n * Redirects the browser to Google's OAuth page. After authentication,\r\n * Google redirects back to the Better Auth callback endpoint, which\r\n * creates a session and redirects to `callbackURL`.\r\n *\r\n * Call `syncCookieSessionToStorage()` on app init to restore the\r\n * session from the cookie after a redirect-based sign-in.\r\n *\r\n * @param authClient - Client created by createLeapifyAuthClient\r\n * @param callbackURL - Path or URL to redirect to after successful auth (e.g. '/dashboard')\r\n *\r\n * @example\r\n * import { signInWithGoogleRedirect } from 'leapify/client'\r\n *\r\n * document.getElementById('google-btn').onclick = () => {\r\n * signInWithGoogleRedirect(authClient, '/dashboard')\r\n * }\r\n */\r\nexport async function signInWithGoogleRedirect(\r\n authClient: LeapifyAuthClient,\r\n callbackURL: string,\r\n): Promise<void> {\r\n await authClient.signIn.social({\r\n provider: 'google',\r\n callbackURL,\r\n })\r\n}\r\n\r\n/**\r\n * Sync a cookie-based Better Auth session into localStorage.\r\n *\r\n * After an OAuth redirect flow, Better Auth stores the session in an\r\n * HTTP-only cookie. This function reads that session via `getSession()`\r\n * and stores the token in localStorage so that subsequent API calls\r\n * using the Bearer token work correctly.\r\n *\r\n * Call this once on app initialization, before `initializeSession()`.\r\n *\r\n * @param authClient - Client created by createLeapifyAuthClient\r\n *\r\n * @example\r\n * import { syncCookieSessionToStorage, initializeSession } from 'leapify/client'\r\n *\r\n * // On app mount:\r\n * await syncCookieSessionToStorage(authClient)\r\n * const user = await initializeSession(API_URL, getToken)\r\n */\r\nexport async function syncCookieSessionToStorage(\r\n authClient: LeapifyAuthClient,\r\n): Promise<void> {\r\n try {\r\n const result = await authClient.getSession()\r\n const data = result?.data as { session?: { token?: string } } | undefined\r\n const token = data?.session?.token\r\n if (token) {\r\n localStorage.setItem(AUTH_TOKEN_KEY, token)\r\n }\r\n } catch {\r\n // No cookie session — user is a guest.\r\n }\r\n}\r\n\r\n/**\r\n * Get the current bearer token from storage, or null for guests.\r\n * Pass this to `createLeapifyClient` as the `getToken` option.\r\n *\r\n * @example\r\n * import { createLeapifyClient } from 'leapify/client'\r\n * import { createLeapifyAuthClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * const authClient = createLeapifyAuthClient(API_URL)\r\n * const api = createLeapifyClient(API_URL, () => getLeapifyToken(authClient))\r\n */\r\nexport async function getLeapifyToken(\r\n // @ts-ignore - Kept for backwards compatibility with previous signature\r\n authClient?: LeapifyAuthClient,\r\n): Promise<string | null> {\r\n if (typeof window !== 'undefined') {\r\n return localStorage.getItem(AUTH_TOKEN_KEY)\r\n }\r\n return null\r\n}\r\n\r\n/**\r\n * Sign out the current user.\r\n */\r\nexport async function signOut(authClient: LeapifyAuthClient) {\r\n const result = await authClient.signOut()\r\n if (typeof window !== 'undefined') {\r\n localStorage.removeItem(AUTH_TOKEN_KEY)\r\n }\r\n return result\r\n}\r\n","declare global {\r\n interface Window {\r\n turnstile: {\r\n render: (\r\n container: string | HTMLElement,\r\n opts: { sitekey: string; callback: (token: string) => void },\r\n ) => string;\r\n remove: (widgetId: string) => void;\r\n };\r\n }\r\n}\r\n\r\nconst TURNSTILE_VERIFY_PATH = \"/.well-known/leapify/turnstile/verify\";\r\n\r\nfunction getTurnstileSiteKey(): string | undefined {\r\n const config = (window as unknown as Record<string, unknown>).__CONFIG__ as\r\n | { turnstileSiteKey?: string }\r\n | undefined;\r\n return config?.turnstileSiteKey;\r\n}\r\n\r\nfunction loadTurnstileScript(): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (typeof window.turnstile !== \"undefined\") {\r\n resolve();\r\n return;\r\n }\r\n const script = document.createElement(\"script\");\r\n script.src =\r\n \"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\";\r\n script.async = true;\r\n script.defer = true;\r\n script.onload = () => resolve();\r\n script.onerror = () => reject(new Error(\"Failed to load Turnstile script\"));\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\nfunction executeTurnstile(siteKey: string): Promise<string> {\r\n let widgetId: string | undefined;\r\n\r\n const cleanup = () => {\r\n if (widgetId && typeof window.turnstile?.remove === \"function\") {\r\n window.turnstile.remove(widgetId);\r\n }\r\n const el = document.getElementById(\"leapify-turnstile-container\");\r\n el?.remove();\r\n };\r\n\r\n return new Promise((resolve) => {\r\n const container = document.createElement(\"div\");\r\n container.id = \"leapify-turnstile-container\";\r\n container.style.display = \"none\";\r\n document.body.appendChild(container);\r\n\r\n // Timeout guard — Turnstile iframe can hang if postMessage origin mismatch\r\n // or other widget issues prevent the callback from firing.\r\n // After 3s, continue without the cookie; the server-side auth middleware\r\n // will handle verified sessions via the Authorization header instead.\r\n const timer = setTimeout(() => {\r\n cleanup();\r\n resolve(\"\");\r\n }, 3_000);\r\n\r\n widgetId = window.turnstile.render(`#${container.id}`, {\r\n sitekey: siteKey,\r\n callback: (token: string) => {\r\n clearTimeout(timer);\r\n cleanup();\r\n resolve(token);\r\n },\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Solve a Turnstile challenge and obtain a signed cookie from the backend.\r\n *\r\n * Loads the Turnstile script (if not already loaded), executes an invisible\r\n * challenge, and posts the token to the backend verify endpoint. The server\r\n * sets a signed cookie that bypasses Turnstile for subsequent requests.\r\n *\r\n * Call once on app initialization before any API requests.\r\n *\r\n * @param baseUrl - The Leapify Worker URL. If omitted, uses the current origin.\r\n * @param siteKey - Turnstile site key. If omitted, reads from window.__CONFIG__.\r\n * @returns `true` if the challenge was solved and cookie was set.\r\n */\r\nexport async function solveTurnstileChallenge(\r\n baseUrl?: string,\r\n siteKey?: string,\r\n): Promise<boolean> {\r\n siteKey = siteKey ?? getTurnstileSiteKey();\r\n if (!siteKey) return false;\r\n\r\n const base = baseUrl?.replace(/\\/$/, \"\") ?? \"\";\r\n\r\n try {\r\n await loadTurnstileScript();\r\n const token = await executeTurnstile(siteKey);\r\n\r\n if (!token) return false;\r\n\r\n const res = await fetch(`${base}${TURNSTILE_VERIFY_PATH}`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({ token }),\r\n credentials: \"include\",\r\n });\r\n\r\n return res.ok;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n","/**\r\n * Browser-safe session initialization helper.\r\n *\r\n * Checks for an existing session token and fetches the user profile.\r\n * Callers should run solveTurnstileChallenge() separately if the server\r\n * enforces Turnstile for unauthenticated requests.\r\n *\r\n * Import from 'leapify/client' — no Cloudflare, Drizzle, or Hono deps.\r\n *\r\n * @example\r\n * import { initializeSession, createLeapifyClient } from 'leapify/client'\r\n *\r\n * const user = await initializeSession(\r\n * 'https://api.leap.yourdomain.com',\r\n * () => getLeapifyToken(),\r\n * )\r\n * if (user) {\r\n * console.log(`Welcome ${user.name} (${user.role})`)\r\n * }\r\n */\r\n\r\nimport type { UserProfile } from \"./types\";\r\n\r\n/**\r\n * Initialize a browser session: restore existing token and fetch profile.\r\n *\r\n * @param baseUrl - The Leapify Worker URL.\r\n * @param getToken - Async function returning the current session token, or null.\r\n * @returns The authenticated user profile, or null if not signed in.\r\n */\r\nexport async function initializeSession(\r\n baseUrl: string,\r\n getToken: () => Promise<string | null>,\r\n): Promise<UserProfile | null> {\r\n const token = await getToken();\r\n if (!token) return null;\r\n\r\n const base = baseUrl.replace(/\\/$/, \"\");\r\n const res = await fetch(`${base}/api/users/me`, {\r\n headers: { Authorization: `Bearer ${token}` },\r\n });\r\n\r\n if (!res.ok) return null;\r\n\r\n const body = await res.json().catch(() => ({}));\r\n return (body as { data: UserProfile | null }).data ?? null;\r\n}\r\n","/**\r\n * Leapify browser-safe API client.\r\n *\r\n * Import from 'leapify/client' — no Cloudflare, Drizzle, or Hono dependencies.\r\n *\r\n * @example\r\n * import { createLeapifyClient, createLeapifyAuthClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * const authClient = createLeapifyAuthClient(process.env.NEXT_PUBLIC_API_URL!)\r\n * const api = createLeapifyClient(\r\n * process.env.NEXT_PUBLIC_API_URL!,\r\n * () => getLeapifyToken(authClient),\r\n * )\r\n *\r\n * const events = await api.getEvents()\r\n */\r\n\r\nexport type {\r\n LeapEvent,\r\n SlotInfo,\r\n UserProfile,\r\n BookmarkEntry,\r\n Faq,\r\n Theme,\r\n Organization,\r\n SiteConfig,\r\n ToggleBookmarkResult,\r\n LeapifyErrorBody,\r\n UserRole,\r\n EventStatus,\r\n CreateEventBody,\r\n CreateFaqBody,\r\n HealthResponse,\r\n RuntimeConfig,\r\n} from \"./types\";\r\n\r\nexport {\r\n createLeapifyAuthClient,\r\n signInWithGoogleRedirect,\r\n syncCookieSessionToStorage,\r\n getLeapifyToken,\r\n signOut,\r\n} from \"./auth\";\r\nexport type { LeapifyAuthClient } from \"./auth\";\r\n\r\nexport { solveTurnstileChallenge } from \"./turnstile\";\r\nexport { initializeSession } from \"./session\";\r\n\r\n/**\r\n * Read the runtime config injected by the worker into HTML pages.\r\n * Returns null if not running in a browser or config not injected.\r\n */\r\nexport function getClientConfig(): RuntimeConfig | null {\r\n if (typeof window === \"undefined\") return null;\r\n const config = (window as unknown as Record<string, unknown>).__CONFIG__;\r\n if (!config || typeof config !== \"object\") return null;\r\n return config as RuntimeConfig;\r\n}\r\n\r\nimport type { RuntimeConfig } from \"./types\";\r\n\r\n/**\r\n * Structured error thrown by all client methods on non-2xx responses.\r\n *\r\n * @example\r\n * import { LeapifyApiError } from 'leapify/client'\r\n *\r\n * try {\r\n * await api.toggleBookmark(eventId)\r\n * } catch (err) {\r\n * if (err instanceof LeapifyApiError && err.code === 'UNAUTHORIZED') {\r\n * // redirect to sign-in\r\n * }\r\n * }\r\n */\r\nexport class LeapifyApiError extends Error {\r\n constructor(\r\n public readonly status: number,\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = \"LeapifyApiError\";\r\n }\r\n}\r\n\r\n// ─── Error code constants ───────────────────────────────────────────────────\r\n\r\nexport const LEAPIFY_ERROR_CODES = {\r\n UNAUTHORIZED: \"UNAUTHORIZED\",\r\n DOMAIN_RESTRICTED: \"DOMAIN_RESTRICTED\",\r\n FORBIDDEN: \"FORBIDDEN\",\r\n NOT_FOUND: \"NOT_FOUND\",\r\n CONFLICT: \"CONFLICT\",\r\n TOO_MANY_REQUESTS: \"TOO_MANY_REQUESTS\",\r\n SERVICE_UNAVAILABLE: \"SERVICE_UNAVAILABLE\",\r\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\r\n} as const;\r\n\r\nexport type LeapifyErrorCode = keyof typeof LEAPIFY_ERROR_CODES;\r\n\r\n// ─── Client factory ─────────────────────────────────────────────────────────\r\n\r\nimport type {\r\n LeapEvent,\r\n SlotInfo,\r\n UserProfile,\r\n BookmarkEntry,\r\n Faq,\r\n Theme,\r\n Organization,\r\n SiteConfig,\r\n ToggleBookmarkResult,\r\n LeapifyErrorBody,\r\n CreateEventBody,\r\n CreateFaqBody,\r\n HealthResponse,\r\n} from \"./types\";\r\n\r\ntype GetTokenFn = () => Promise<string | null>;\r\n\r\nasync function buildHeaders(\r\n getToken: GetTokenFn | undefined,\r\n extra: Record<string, string> = {},\r\n): Promise<Record<string, string>> {\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n ...extra,\r\n };\r\n if (getToken) {\r\n const token = await getToken();\r\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n return headers;\r\n}\r\n\r\nasync function parseResponse<T>(res: Response): Promise<T> {\r\n if (res.status === 204) return undefined as T;\r\n\r\n const body = await res.json().catch(() => ({}));\r\n\r\n if (!res.ok) {\r\n const err = (body as LeapifyErrorBody)?.error;\r\n throw new LeapifyApiError(\r\n res.status,\r\n err?.code ?? \"UNKNOWN\",\r\n err?.message ?? res.statusText,\r\n );\r\n }\r\n\r\n return (body as { data: T }).data;\r\n}\r\n\r\n/**\r\n * Creates a typed Leapify API client bound to a base URL.\r\n *\r\n * @param baseUrl - The deployed Leapify Worker URL (e.g. `https://api.leap.yourdomain.com`).\r\n * @param getToken - Optional async function that returns a session token string,\r\n * or null for guest requests. Use `getLeapifyToken()` from this module.\r\n *\r\n * @example\r\n * // lib/api.ts\r\n * import { createLeapifyClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * export const api = createLeapifyClient(\r\n * process.env.NEXT_PUBLIC_API_URL!,\r\n * () => getLeapifyToken(),\r\n * )\r\n */\r\nexport function createLeapifyClient(baseUrl: string, getToken?: GetTokenFn) {\r\n const base = baseUrl.replace(/\\/$/, \"\");\r\n\r\n async function get<T>(path: string, init?: RequestInit): Promise<T> {\r\n const headers = await buildHeaders(getToken, init?.headers as Record<string, string>);\r\n const res = await fetch(`${base}${path}`, { ...init, method: \"GET\", headers });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function post<T>(path: string, body?: unknown): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"POST\",\r\n headers,\r\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function postFormData<T>(path: string, formData: FormData): Promise<T> {\r\n const headers: Record<string, string> = {};\r\n if (getToken) {\r\n const token = await getToken();\r\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"POST\",\r\n headers,\r\n body: formData,\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function patch<T>(path: string, body: unknown): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"PATCH\",\r\n headers,\r\n body: JSON.stringify(body),\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function del<T>(path: string): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, { method: \"DELETE\", headers });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n return {\r\n // ── Site Config ────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /config\r\n * Returns site-wide configuration. Check `maintenanceMode` and\r\n * `comingSoonUntil` on app load to gate the UI appropriately.\r\n * Use `now` (server unix epoch) for timestamp comparisons.\r\n */\r\n getConfig(): Promise<SiteConfig> {\r\n return get<SiteConfig>(\"/api/config\");\r\n },\r\n\r\n /**\r\n * PATCH /api/config/:key — admin only.\r\n * Upserts a site config value. Requires admin or super_admin role.\r\n */\r\n updateConfig<K extends string>(key: K, value: unknown): Promise<{ key: K; value: unknown }> {\r\n return patch(`/api/config/${encodeURIComponent(key)}`, { value });\r\n },\r\n\r\n // ── Events ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/classes\r\n * Returns all published classes. Response is ETag-cached for 7 days.\r\n */\r\n getEvents(): Promise<LeapEvent[]> {\r\n return get<LeapEvent[]>(\"/api/classes\");\r\n },\r\n\r\n /**\r\n * GET /api/classes/admin — admin only.\r\n * Returns all classes regardless of status.\r\n */\r\n getAdminEvents(): Promise<LeapEvent[]> {\r\n return get<LeapEvent[]>(\"/api/classes/admin\");\r\n },\r\n\r\n /**\r\n * POST /api/classes/admin/publish — admin only.\r\n * Batch publish queued classes immediately or schedule them for later.\r\n */\r\n batchPublish(ids: string[], releaseAt?: number): Promise<{ updated: number }> {\r\n return post(\"/api/classes/admin/publish\", { ids, releaseAt });\r\n },\r\n\r\n /**\r\n * GET /api/classes/:slug\r\n * Returns a single published class by slug.\r\n */\r\n getEvent(slug: string): Promise<LeapEvent> {\r\n return get<LeapEvent>(`/api/classes/${encodeURIComponent(slug)}`);\r\n },\r\n\r\n /**\r\n * GET /api/classes/:slug/slots\r\n * Returns real-time slot availability. CF edge caches this for 5 seconds.\r\n * Poll every 8–10 seconds on class detail pages.\r\n */\r\n getSlots(slug: string): Promise<SlotInfo> {\r\n return get<SlotInfo>(`/api/classes/${encodeURIComponent(slug)}/slots`);\r\n },\r\n\r\n /**\r\n * POST /api/classes/:slug/reconcile — admin only.\r\n * Corrects slot count for a single event by fetching the real Google Forms response count.\r\n */\r\n reconcileEvent(slug: string): Promise<{ registeredSlots: number }> {\r\n return post<{ registeredSlots: number }>(`/api/classes/${encodeURIComponent(slug)}/reconcile`);\r\n },\r\n\r\n /**\r\n * POST /api/classes — admin only.\r\n * Creates a new class. Auto-generates slug from title.\r\n */\r\n createEvent(data: CreateEventBody): Promise<LeapEvent> {\r\n return post<LeapEvent>(\"/api/classes\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/classes/:slug — admin only.\r\n * Updates an existing class by slug.\r\n */\r\n updateEvent(slug: string, data: Partial<CreateEventBody>): Promise<LeapEvent> {\r\n return patch<LeapEvent>(`/api/classes/${encodeURIComponent(slug)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/classes/:slug — admin only.\r\n * Deletes a class.\r\n */\r\n deleteEvent(slug: string): Promise<void> {\r\n return del<void>(`/api/classes/${encodeURIComponent(slug)}`);\r\n },\r\n\r\n // ── Themes ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/themes\r\n * Returns all themes.\r\n */\r\n getThemes(): Promise<Theme[]> {\r\n return get<Theme[]>(\"/api/themes\");\r\n },\r\n\r\n /**\r\n * POST /api/themes — admin only.\r\n */\r\n createTheme(data: Omit<Theme, \"id\" | \"createdAt\" | \"path\"> & { path?: string }): Promise<Theme> {\r\n return post<Theme>(\"/api/themes\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/themes/:id — admin only.\r\n */\r\n updateTheme(id: string, data: Partial<Omit<Theme, \"id\" | \"createdAt\">>): Promise<Theme> {\r\n return patch<Theme>(`/api/themes/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/themes/:id — admin only.\r\n */\r\n deleteTheme(id: string): Promise<void> {\r\n return del<void>(`/api/themes/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Organizations ──────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/organizations\r\n * Returns all organizations.\r\n */\r\n getOrganizations(): Promise<Organization[]> {\r\n return get<Organization[]>(\"/api/organizations\");\r\n },\r\n\r\n /**\r\n * POST /api/organizations — admin only.\r\n */\r\n createOrganization(data: Omit<Organization, \"id\" | \"createdAt\">): Promise<Organization> {\r\n return post<Organization>(\"/api/organizations\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/organizations/:id — admin only.\r\n */\r\n updateOrganization(id: string, data: Partial<Omit<Organization, \"id\" | \"createdAt\">>): Promise<Organization> {\r\n return patch<Organization>(`/api/organizations/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/organizations/:id — admin only.\r\n */\r\n deleteOrganization(id: string): Promise<void> {\r\n return del<void>(`/api/organizations/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Users ──────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users/me\r\n * Returns the authenticated user's profile, or null for guests.\r\n * Use `profile.role` to gate admin UI.\r\n */\r\n getMe(): Promise<UserProfile | null> {\r\n return get<UserProfile | null>(\"/api/users/me\");\r\n },\r\n\r\n // ── Admin: User Management ────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users — admin only.\r\n * Returns all registered users.\r\n */\r\n getUsers(): Promise<UserProfile[]> {\r\n return get<UserProfile[]>(\"/api/users\");\r\n },\r\n\r\n /**\r\n * PATCH /api/users/:id/role — admin only.\r\n * Changes a user's role.\r\n */\r\n updateUserRole(id: string, role: string): Promise<UserProfile> {\r\n return patch<UserProfile>(`/api/users/${encodeURIComponent(id)}/role`, { role });\r\n },\r\n\r\n /**\r\n * POST /api/users/by-email — admin only.\r\n * Finds or creates a user by email and sets their role.\r\n */\r\n upsertUserByEmail(email: string, role: string): Promise<UserProfile> {\r\n return post<UserProfile>(\"/api/users/by-email\", { email, role });\r\n },\r\n\r\n // ── Bookmarks ──────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users/me/bookmarks\r\n * Returns the authenticated user's bookmarked events.\r\n * Returns an empty array for unauthenticated users.\r\n */\r\n getBookmarks(): Promise<BookmarkEntry[]> {\r\n return get<BookmarkEntry[]>(\"/api/users/me/bookmarks\");\r\n },\r\n\r\n /**\r\n * POST /api/users/me/bookmarks/:eventId\r\n * Toggles a bookmark on/off. Requires authentication.\r\n * Returns `{ bookmarked: true }` (201) on add, `{ bookmarked: false }` (200) on remove.\r\n */\r\n toggleBookmark(eventId: string): Promise<ToggleBookmarkResult> {\r\n return post<ToggleBookmarkResult>(\r\n `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`,\r\n );\r\n },\r\n\r\n /**\r\n * DELETE /api/users/me/bookmarks/:eventId\r\n * Removes a bookmark. Requires authentication.\r\n */\r\n deleteBookmark(eventId: string): Promise<ToggleBookmarkResult> {\r\n return del<ToggleBookmarkResult>(\r\n `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`,\r\n );\r\n },\r\n\r\n // ── FAQs ───────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/faqs\r\n * Returns all active FAQs. Cached in KV for 10 minutes.\r\n * The `answer` field is markdown — render with a markdown library.\r\n */\r\n getFaqs(): Promise<Faq[]> {\r\n return get<Faq[]>(\"/api/faqs\");\r\n },\r\n\r\n /**\r\n * POST /api/faqs — admin only.\r\n * Creates a new FAQ item.\r\n */\r\n createFaq(data: CreateFaqBody): Promise<Faq> {\r\n return post<Faq>(\"/api/faqs\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/faqs/:id — admin only.\r\n * Updates an existing FAQ item.\r\n */\r\n updateFaq(id: string, data: Partial<CreateFaqBody>): Promise<Faq> {\r\n return patch<Faq>(`/api/faqs/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/faqs/:id — admin only.\r\n * Soft-deletes a FAQ (sets isActive: false).\r\n */\r\n deleteFaq(id: string): Promise<{ deleted: boolean }> {\r\n return del<{ deleted: boolean }>(`/api/faqs/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Uploads ────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * POST /api/uploads/images — admin only.\r\n * Uploads an image file to R2. Accepts multipart/form-data.\r\n * Returns the public URL, storage key, size, and content type.\r\n */\r\n uploadImage(file: File | Blob): Promise<{\r\n url: string;\r\n key: string;\r\n size: number;\r\n contentType: string;\r\n }> {\r\n const formData = new FormData();\r\n formData.append(\"file\", file);\r\n return postFormData(\"/api/uploads/images\", formData);\r\n },\r\n\r\n // ── Health ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /health\r\n * Public health check. Returns provider availability status.\r\n */\r\n healthCheck(): Promise<HealthResponse> {\r\n return get<HealthResponse>(\"/health\");\r\n },\r\n };\r\n}\r\n\r\nexport type LeapifyClient = ReturnType<typeof createLeapifyClient>;\r\n"]}
|
package/dist/client/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/auth.ts","../../src/client/turnstile.ts","../../src/client/session.ts","../../src/client/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * Better Auth client helper for Leapify API consumers.\r\n *\r\n * This module is **browser-safe** — no Cloudflare, Drizzle, or Hono deps.\r\n * It wraps Better Auth's client SDK with the bearer plugin so that tokens\r\n * can be stored and retrieved as plain strings (no cookie dependency on\r\n * the consumer's frontend).\r\n *\r\n * @example\r\n * // lib/auth.ts (frontend)\r\n * import { createLeapifyAuthClient, signInWithGoogleRedirect } from 'leapify/client'\r\n *\r\n * export const authClient = createLeapifyAuthClient(process.env.NEXT_PUBLIC_API_URL!)\r\n *\r\n * // Redirect-based Google sign-in:\r\n * await signInWithGoogleRedirect(authClient, '/dashboard')\r\n */\r\n\r\nimport { createAuthClient } from 'better-auth/client'\r\n\r\nconst AUTH_TOKEN_KEY = 'better-auth.session_token'\r\n\r\n/**\r\n * Create a Better Auth client bound to the Leapify Worker URL.\r\n *\r\n * It uses the 'Bearer' auth type to send the stored session token\r\n * in the Authorization header.\r\n */\r\nexport function createLeapifyAuthClient(baseUrl: string) {\r\n return createAuthClient({\r\n baseURL: baseUrl,\r\n fetchOptions: {\r\n auth: {\r\n type: 'Bearer',\r\n token: () => {\r\n if (typeof window !== 'undefined') {\r\n return localStorage.getItem(AUTH_TOKEN_KEY) || ''\r\n }\r\n return ''\r\n }\r\n }\r\n }\r\n })\r\n}\r\n\r\nexport type LeapifyAuthClient = ReturnType<typeof createLeapifyAuthClient>\r\n\r\n/**\r\n * Sign in with Google via OAuth redirect flow.\r\n *\r\n * Redirects the browser to Google's OAuth page. After authentication,\r\n * Google redirects back to the Better Auth callback endpoint, which\r\n * creates a session and redirects to `callbackURL`.\r\n *\r\n * Call `syncCookieSessionToStorage()` on app init to restore the\r\n * session from the cookie after a redirect-based sign-in.\r\n *\r\n * @param authClient - Client created by createLeapifyAuthClient\r\n * @param callbackURL - Path or URL to redirect to after successful auth (e.g. '/dashboard')\r\n *\r\n * @example\r\n * import { signInWithGoogleRedirect } from 'leapify/client'\r\n *\r\n * document.getElementById('google-btn').onclick = () => {\r\n * signInWithGoogleRedirect(authClient, '/dashboard')\r\n * }\r\n */\r\nexport async function signInWithGoogleRedirect(\r\n authClient: LeapifyAuthClient,\r\n callbackURL: string,\r\n): Promise<void> {\r\n await authClient.signIn.social({\r\n provider: 'google',\r\n callbackURL,\r\n })\r\n}\r\n\r\n/**\r\n * Sync a cookie-based Better Auth session into localStorage.\r\n *\r\n * After an OAuth redirect flow, Better Auth stores the session in an\r\n * HTTP-only cookie. This function reads that session via `getSession()`\r\n * and stores the token in localStorage so that subsequent API calls\r\n * using the Bearer token work correctly.\r\n *\r\n * Call this once on app initialization, before `initializeSession()`.\r\n *\r\n * @param authClient - Client created by createLeapifyAuthClient\r\n *\r\n * @example\r\n * import { syncCookieSessionToStorage, initializeSession } from 'leapify/client'\r\n *\r\n * // On app mount:\r\n * await syncCookieSessionToStorage(authClient)\r\n * const user = await initializeSession(API_URL, getToken)\r\n */\r\nexport async function syncCookieSessionToStorage(\r\n authClient: LeapifyAuthClient,\r\n): Promise<void> {\r\n try {\r\n const result = await authClient.getSession()\r\n const data = result?.data as { session?: { token?: string } } | undefined\r\n const token = data?.session?.token\r\n if (token) {\r\n localStorage.setItem(AUTH_TOKEN_KEY, token)\r\n }\r\n } catch {\r\n // No cookie session — user is a guest.\r\n }\r\n}\r\n\r\n/**\r\n * Get the current bearer token from storage, or null for guests.\r\n * Pass this to `createLeapifyClient` as the `getToken` option.\r\n *\r\n * @example\r\n * import { createLeapifyClient } from 'leapify/client'\r\n * import { createLeapifyAuthClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * const authClient = createLeapifyAuthClient(API_URL)\r\n * const api = createLeapifyClient(API_URL, () => getLeapifyToken(authClient))\r\n */\r\nexport async function getLeapifyToken(\r\n // @ts-ignore - Kept for backwards compatibility with previous signature\r\n authClient?: LeapifyAuthClient,\r\n): Promise<string | null> {\r\n if (typeof window !== 'undefined') {\r\n return localStorage.getItem(AUTH_TOKEN_KEY)\r\n }\r\n return null\r\n}\r\n\r\n/**\r\n * Sign out the current user.\r\n */\r\nexport async function signOut(authClient: LeapifyAuthClient) {\r\n const result = await authClient.signOut()\r\n if (typeof window !== 'undefined') {\r\n localStorage.removeItem(AUTH_TOKEN_KEY)\r\n }\r\n return result\r\n}\r\n","declare global {\r\n interface Window {\r\n turnstile: {\r\n render: (\r\n container: string | HTMLElement,\r\n opts: { sitekey: string; callback: (token: string) => void },\r\n ) => string;\r\n remove: (widgetId: string) => void;\r\n };\r\n }\r\n}\r\n\r\nconst TURNSTILE_VERIFY_PATH = \"/.well-known/leapify/turnstile/verify\";\r\n\r\nfunction getTurnstileSiteKey(): string | undefined {\r\n const config = (window as unknown as Record<string, unknown>).__CONFIG__ as\r\n | { turnstileSiteKey?: string }\r\n | undefined;\r\n return config?.turnstileSiteKey;\r\n}\r\n\r\nfunction loadTurnstileScript(): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (typeof window.turnstile !== \"undefined\") {\r\n resolve();\r\n return;\r\n }\r\n const script = document.createElement(\"script\");\r\n script.src =\r\n \"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit\";\r\n script.async = true;\r\n script.defer = true;\r\n script.onload = () => resolve();\r\n script.onerror = () => reject(new Error(\"Failed to load Turnstile script\"));\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\nfunction executeTurnstile(siteKey: string): Promise<string> {\r\n let widgetId: string | undefined;\r\n\r\n const cleanup = () => {\r\n if (widgetId && typeof window.turnstile?.remove === \"function\") {\r\n window.turnstile.remove(widgetId);\r\n }\r\n const el = document.getElementById(\"leapify-turnstile-container\");\r\n el?.remove();\r\n };\r\n\r\n return new Promise((resolve) => {\r\n const container = document.createElement(\"div\");\r\n container.id = \"leapify-turnstile-container\";\r\n container.style.display = \"none\";\r\n document.body.appendChild(container);\r\n\r\n // Timeout guard — Turnstile iframe can hang if postMessage origin mismatch\r\n // or other widget issues prevent the callback from firing.\r\n // After 3s, continue without the cookie; the server-side auth middleware\r\n // will handle verified sessions via the Authorization header instead.\r\n const timer = setTimeout(() => {\r\n cleanup();\r\n resolve(\"\");\r\n }, 3_000);\r\n\r\n widgetId = window.turnstile.render(`#${container.id}`, {\r\n sitekey: siteKey,\r\n callback: (token: string) => {\r\n clearTimeout(timer);\r\n cleanup();\r\n resolve(token);\r\n },\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Solve a Turnstile challenge and obtain a signed cookie from the backend.\r\n *\r\n * Loads the Turnstile script (if not already loaded), executes an invisible\r\n * challenge, and posts the token to the backend verify endpoint. The server\r\n * sets a signed cookie that bypasses Turnstile for subsequent requests.\r\n *\r\n * Call once on app initialization before any API requests.\r\n *\r\n * @param baseUrl - The Leapify Worker URL. If omitted, uses the current origin.\r\n * @param siteKey - Turnstile site key. If omitted, reads from window.__CONFIG__.\r\n * @returns `true` if the challenge was solved and cookie was set.\r\n */\r\nexport async function solveTurnstileChallenge(\r\n baseUrl?: string,\r\n siteKey?: string,\r\n): Promise<boolean> {\r\n siteKey = siteKey ?? getTurnstileSiteKey();\r\n if (!siteKey) return false;\r\n\r\n const base = baseUrl?.replace(/\\/$/, \"\") ?? \"\";\r\n\r\n try {\r\n await loadTurnstileScript();\r\n const token = await executeTurnstile(siteKey);\r\n\r\n if (!token) return false;\r\n\r\n const res = await fetch(`${base}${TURNSTILE_VERIFY_PATH}`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({ token }),\r\n credentials: \"include\",\r\n });\r\n\r\n return res.ok;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n","/**\r\n * Browser-safe session initialization helper.\r\n *\r\n * Checks for an existing session token and fetches the user profile.\r\n * Callers should run solveTurnstileChallenge() separately if the server\r\n * enforces Turnstile for unauthenticated requests.\r\n *\r\n * Import from 'leapify/client' — no Cloudflare, Drizzle, or Hono deps.\r\n *\r\n * @example\r\n * import { initializeSession, createLeapifyClient } from 'leapify/client'\r\n *\r\n * const user = await initializeSession(\r\n * 'https://api.leap.yourdomain.com',\r\n * () => getLeapifyToken(),\r\n * )\r\n * if (user) {\r\n * console.log(`Welcome ${user.name} (${user.role})`)\r\n * }\r\n */\r\n\r\nimport type { UserProfile } from \"./types\";\r\n\r\n/**\r\n * Initialize a browser session: restore existing token and fetch profile.\r\n *\r\n * @param baseUrl - The Leapify Worker URL.\r\n * @param getToken - Async function returning the current session token, or null.\r\n * @returns The authenticated user profile, or null if not signed in.\r\n */\r\nexport async function initializeSession(\r\n baseUrl: string,\r\n getToken: () => Promise<string | null>,\r\n): Promise<UserProfile | null> {\r\n const token = await getToken();\r\n if (!token) return null;\r\n\r\n const base = baseUrl.replace(/\\/$/, \"\");\r\n const res = await fetch(`${base}/api/users/me`, {\r\n headers: { Authorization: `Bearer ${token}` },\r\n });\r\n\r\n if (!res.ok) return null;\r\n\r\n const body = await res.json().catch(() => ({}));\r\n return (body as { data: UserProfile | null }).data ?? null;\r\n}\r\n","/**\r\n * Leapify browser-safe API client.\r\n *\r\n * Import from 'leapify/client' — no Cloudflare, Drizzle, or Hono dependencies.\r\n *\r\n * @example\r\n * import { createLeapifyClient, createLeapifyAuthClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * const authClient = createLeapifyAuthClient(process.env.NEXT_PUBLIC_API_URL!)\r\n * const api = createLeapifyClient(\r\n * process.env.NEXT_PUBLIC_API_URL!,\r\n * () => getLeapifyToken(authClient),\r\n * )\r\n *\r\n * const events = await api.getEvents()\r\n */\r\n\r\nexport type {\r\n LeapEvent,\r\n SlotInfo,\r\n UserProfile,\r\n BookmarkEntry,\r\n Faq,\r\n Theme,\r\n Organization,\r\n SiteConfig,\r\n ToggleBookmarkResult,\r\n LeapifyErrorBody,\r\n UserRole,\r\n EventStatus,\r\n CreateEventBody,\r\n CreateFaqBody,\r\n HealthResponse,\r\n RuntimeConfig,\r\n} from \"./types\";\r\n\r\nexport {\r\n createLeapifyAuthClient,\r\n signInWithGoogleRedirect,\r\n syncCookieSessionToStorage,\r\n getLeapifyToken,\r\n signOut,\r\n} from \"./auth\";\r\nexport type { LeapifyAuthClient } from \"./auth\";\r\n\r\nexport { solveTurnstileChallenge } from \"./turnstile\";\r\nexport { initializeSession } from \"./session\";\r\n\r\n/**\r\n * Read the runtime config injected by the worker into HTML pages.\r\n * Returns null if not running in a browser or config not injected.\r\n */\r\nexport function getClientConfig(): RuntimeConfig | null {\r\n if (typeof window === \"undefined\") return null;\r\n const config = (window as unknown as Record<string, unknown>).__CONFIG__;\r\n if (!config || typeof config !== \"object\") return null;\r\n return config as RuntimeConfig;\r\n}\r\n\r\nimport type { RuntimeConfig } from \"./types\";\r\n\r\n/**\r\n * Structured error thrown by all client methods on non-2xx responses.\r\n *\r\n * @example\r\n * import { LeapifyApiError } from 'leapify/client'\r\n *\r\n * try {\r\n * await api.toggleBookmark(eventId)\r\n * } catch (err) {\r\n * if (err instanceof LeapifyApiError && err.code === 'UNAUTHORIZED') {\r\n * // redirect to sign-in\r\n * }\r\n * }\r\n */\r\nexport class LeapifyApiError extends Error {\r\n constructor(\r\n public readonly status: number,\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = \"LeapifyApiError\";\r\n }\r\n}\r\n\r\n// ─── Error code constants ───────────────────────────────────────────────────\r\n\r\nexport const LEAPIFY_ERROR_CODES = {\r\n UNAUTHORIZED: \"UNAUTHORIZED\",\r\n DOMAIN_RESTRICTED: \"DOMAIN_RESTRICTED\",\r\n FORBIDDEN: \"FORBIDDEN\",\r\n NOT_FOUND: \"NOT_FOUND\",\r\n CONFLICT: \"CONFLICT\",\r\n TOO_MANY_REQUESTS: \"TOO_MANY_REQUESTS\",\r\n SERVICE_UNAVAILABLE: \"SERVICE_UNAVAILABLE\",\r\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\r\n} as const;\r\n\r\nexport type LeapifyErrorCode = keyof typeof LEAPIFY_ERROR_CODES;\r\n\r\n// ─── Client factory ─────────────────────────────────────────────────────────\r\n\r\nimport type {\r\n LeapEvent,\r\n SlotInfo,\r\n UserProfile,\r\n BookmarkEntry,\r\n Faq,\r\n Theme,\r\n Organization,\r\n SiteConfig,\r\n ToggleBookmarkResult,\r\n LeapifyErrorBody,\r\n CreateEventBody,\r\n CreateFaqBody,\r\n HealthResponse,\r\n} from \"./types\";\r\n\r\ntype GetTokenFn = () => Promise<string | null>;\r\n\r\nasync function buildHeaders(\r\n getToken: GetTokenFn | undefined,\r\n extra: Record<string, string> = {},\r\n): Promise<Record<string, string>> {\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n ...extra,\r\n };\r\n if (getToken) {\r\n const token = await getToken();\r\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n return headers;\r\n}\r\n\r\nasync function parseResponse<T>(res: Response): Promise<T> {\r\n if (res.status === 204) return undefined as T;\r\n\r\n const body = await res.json().catch(() => ({}));\r\n\r\n if (!res.ok) {\r\n const err = (body as LeapifyErrorBody)?.error;\r\n throw new LeapifyApiError(\r\n res.status,\r\n err?.code ?? \"UNKNOWN\",\r\n err?.message ?? res.statusText,\r\n );\r\n }\r\n\r\n return (body as { data: T }).data;\r\n}\r\n\r\n/**\r\n * Creates a typed Leapify API client bound to a base URL.\r\n *\r\n * @param baseUrl - The deployed Leapify Worker URL (e.g. `https://api.leap.yourdomain.com`).\r\n * @param getToken - Optional async function that returns a session token string,\r\n * or null for guest requests. Use `getLeapifyToken()` from this module.\r\n *\r\n * @example\r\n * // lib/api.ts\r\n * import { createLeapifyClient, getLeapifyToken } from 'leapify/client'\r\n *\r\n * export const api = createLeapifyClient(\r\n * process.env.NEXT_PUBLIC_API_URL!,\r\n * () => getLeapifyToken(),\r\n * )\r\n */\r\nexport function createLeapifyClient(baseUrl: string, getToken?: GetTokenFn) {\r\n const base = baseUrl.replace(/\\/$/, \"\");\r\n\r\n async function get<T>(path: string, init?: RequestInit): Promise<T> {\r\n const headers = await buildHeaders(getToken, init?.headers as Record<string, string>);\r\n const res = await fetch(`${base}${path}`, { ...init, method: \"GET\", headers });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function post<T>(path: string, body?: unknown): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"POST\",\r\n headers,\r\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function postFormData<T>(path: string, formData: FormData): Promise<T> {\r\n const headers: Record<string, string> = {};\r\n if (getToken) {\r\n const token = await getToken();\r\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"POST\",\r\n headers,\r\n body: formData,\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function patch<T>(path: string, body: unknown): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, {\r\n method: \"PATCH\",\r\n headers,\r\n body: JSON.stringify(body),\r\n });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n async function del<T>(path: string): Promise<T> {\r\n const headers = await buildHeaders(getToken);\r\n const res = await fetch(`${base}${path}`, { method: \"DELETE\", headers });\r\n return parseResponse<T>(res);\r\n }\r\n\r\n return {\r\n // ── Site Config ────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /config\r\n * Returns site-wide configuration. Check `maintenanceMode` and\r\n * `comingSoonUntil` on app load to gate the UI appropriately.\r\n * Use `now` (server unix epoch) for timestamp comparisons.\r\n */\r\n getConfig(): Promise<SiteConfig> {\r\n return get<SiteConfig>(\"/api/config\");\r\n },\r\n\r\n /**\r\n * PATCH /api/config/:key — admin only.\r\n * Upserts a site config value. Requires admin or super_admin role.\r\n */\r\n updateConfig<K extends string>(key: K, value: unknown): Promise<{ key: K; value: unknown }> {\r\n return patch(`/api/config/${encodeURIComponent(key)}`, { value });\r\n },\r\n\r\n // ── Events ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/classes\r\n * Returns all published classes. Response is ETag-cached for 7 days.\r\n */\r\n getEvents(): Promise<LeapEvent[]> {\r\n return get<LeapEvent[]>(\"/api/classes\");\r\n },\r\n\r\n /**\r\n * GET /api/classes/admin — admin only.\r\n * Returns all classes regardless of status.\r\n */\r\n getAdminEvents(): Promise<LeapEvent[]> {\r\n return get<LeapEvent[]>(\"/api/classes/admin\");\r\n },\r\n\r\n /**\r\n * POST /api/classes/admin/publish — admin only.\r\n * Batch publish queued classes immediately or schedule them for later.\r\n */\r\n batchPublish(ids: string[], releaseAt?: number): Promise<{ updated: number }> {\r\n return post(\"/api/classes/admin/publish\", { ids, releaseAt });\r\n },\r\n\r\n /**\r\n * GET /api/classes/:slug\r\n * Returns a single published class by slug.\r\n */\r\n getEvent(slug: string): Promise<LeapEvent> {\r\n return get<LeapEvent>(`/api/classes/${encodeURIComponent(slug)}`);\r\n },\r\n\r\n /**\r\n * GET /api/classes/:slug/slots\r\n * Returns real-time slot availability. CF edge caches this for 5 seconds.\r\n * Poll every 8–10 seconds on class detail pages.\r\n */\r\n getSlots(slug: string): Promise<SlotInfo> {\r\n return get<SlotInfo>(`/api/classes/${encodeURIComponent(slug)}/slots`);\r\n },\r\n\r\n /**\r\n * POST /api/classes/:slug/reconcile — admin only.\r\n * Corrects slot count for a single event by fetching the real Google Forms response count.\r\n */\r\n reconcileEvent(slug: string): Promise<{ registeredSlots: number }> {\r\n return post<{ registeredSlots: number }>(`/api/classes/${encodeURIComponent(slug)}/reconcile`);\r\n },\r\n\r\n /**\r\n * POST /api/classes — admin only.\r\n * Creates a new class. Auto-generates slug from title.\r\n */\r\n createEvent(data: CreateEventBody): Promise<LeapEvent> {\r\n return post<LeapEvent>(\"/api/classes\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/classes/:slug — admin only.\r\n * Updates an existing class by slug.\r\n */\r\n updateEvent(slug: string, data: Partial<CreateEventBody>): Promise<LeapEvent> {\r\n return patch<LeapEvent>(`/api/classes/${encodeURIComponent(slug)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/classes/:slug — admin only.\r\n * Deletes a class.\r\n */\r\n deleteEvent(slug: string): Promise<void> {\r\n return del<void>(`/api/classes/${encodeURIComponent(slug)}`);\r\n },\r\n\r\n // ── Themes ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/themes\r\n * Returns all themes.\r\n */\r\n getThemes(): Promise<Theme[]> {\r\n return get<Theme[]>(\"/api/themes\");\r\n },\r\n\r\n /**\r\n * POST /api/themes — admin only.\r\n */\r\n createTheme(data: Omit<Theme, \"id\" | \"createdAt\" | \"path\"> & { path?: string }): Promise<Theme> {\r\n return post<Theme>(\"/api/themes\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/themes/:id — admin only.\r\n */\r\n updateTheme(id: string, data: Partial<Omit<Theme, \"id\" | \"createdAt\">>): Promise<Theme> {\r\n return patch<Theme>(`/api/themes/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/themes/:id — admin only.\r\n */\r\n deleteTheme(id: string): Promise<void> {\r\n return del<void>(`/api/themes/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Organizations ──────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/organizations\r\n * Returns all organizations.\r\n */\r\n getOrganizations(): Promise<Organization[]> {\r\n return get<Organization[]>(\"/api/organizations\");\r\n },\r\n\r\n /**\r\n * POST /api/organizations — admin only.\r\n */\r\n createOrganization(data: Omit<Organization, \"id\" | \"createdAt\">): Promise<Organization> {\r\n return post<Organization>(\"/api/organizations\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/organizations/:id — admin only.\r\n */\r\n updateOrganization(id: string, data: Partial<Omit<Organization, \"id\" | \"createdAt\">>): Promise<Organization> {\r\n return patch<Organization>(`/api/organizations/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/organizations/:id — admin only.\r\n */\r\n deleteOrganization(id: string): Promise<void> {\r\n return del<void>(`/api/organizations/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Users ──────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users/me\r\n * Returns the authenticated user's profile, or null for guests.\r\n * Use `profile.role` to gate admin UI.\r\n */\r\n getMe(): Promise<UserProfile | null> {\r\n return get<UserProfile | null>(\"/api/users/me\");\r\n },\r\n\r\n // ── Admin: User Management ────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users — admin only.\r\n * Returns all registered users.\r\n */\r\n getUsers(): Promise<UserProfile[]> {\r\n return get<UserProfile[]>(\"/api/users\");\r\n },\r\n\r\n /**\r\n * PATCH /api/users/:id/role — admin only.\r\n * Changes a user's role.\r\n */\r\n updateUserRole(id: string, role: string): Promise<UserProfile> {\r\n return patch<UserProfile>(`/api/users/${encodeURIComponent(id)}/role`, { role });\r\n },\r\n\r\n /**\r\n * POST /api/users/by-email — admin only.\r\n * Finds or creates a user by email and sets their role.\r\n */\r\n upsertUserByEmail(email: string, role: string): Promise<UserProfile> {\r\n return post<UserProfile>(\"/api/users/by-email\", { email, role });\r\n },\r\n\r\n // ── Bookmarks ──────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/users/me/bookmarks\r\n * Returns the authenticated user's bookmarked events.\r\n * Returns an empty array for unauthenticated users.\r\n */\r\n getBookmarks(): Promise<BookmarkEntry[]> {\r\n return get<BookmarkEntry[]>(\"/api/users/me/bookmarks\");\r\n },\r\n\r\n /**\r\n * POST /api/users/me/bookmarks/:eventId\r\n * Toggles a bookmark on/off. Requires authentication.\r\n * Returns `{ bookmarked: true }` (201) on add, `{ bookmarked: false }` (200) on remove.\r\n */\r\n toggleBookmark(eventId: string): Promise<ToggleBookmarkResult> {\r\n return post<ToggleBookmarkResult>(\r\n `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`,\r\n );\r\n },\r\n\r\n /**\r\n * DELETE /api/users/me/bookmarks/:eventId\r\n * Removes a bookmark. Requires authentication.\r\n */\r\n deleteBookmark(eventId: string): Promise<ToggleBookmarkResult> {\r\n return del<ToggleBookmarkResult>(\r\n `/api/users/me/bookmarks/${encodeURIComponent(eventId)}`,\r\n );\r\n },\r\n\r\n // ── FAQs ───────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /api/faqs\r\n * Returns all active FAQs. Cached in KV for 10 minutes.\r\n * The `answer` field is markdown — render with a markdown library.\r\n */\r\n getFaqs(): Promise<Faq[]> {\r\n return get<Faq[]>(\"/api/faqs\");\r\n },\r\n\r\n /**\r\n * POST /api/faqs — admin only.\r\n * Creates a new FAQ item.\r\n */\r\n createFaq(data: CreateFaqBody): Promise<Faq> {\r\n return post<Faq>(\"/api/faqs\", data);\r\n },\r\n\r\n /**\r\n * PATCH /api/faqs/:id — admin only.\r\n * Updates an existing FAQ item.\r\n */\r\n updateFaq(id: string, data: Partial<CreateFaqBody>): Promise<Faq> {\r\n return patch<Faq>(`/api/faqs/${encodeURIComponent(id)}`, data);\r\n },\r\n\r\n /**\r\n * DELETE /api/faqs/:id — admin only.\r\n * Soft-deletes a FAQ (sets isActive: false).\r\n */\r\n deleteFaq(id: string): Promise<{ deleted: boolean }> {\r\n return del<{ deleted: boolean }>(`/api/faqs/${encodeURIComponent(id)}`);\r\n },\r\n\r\n // ── Uploads ────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * POST /api/uploads/images — admin only.\r\n * Uploads an image file to R2. Accepts multipart/form-data.\r\n * Returns the public URL, storage key, size, and content type.\r\n */\r\n uploadImage(file: File | Blob): Promise<{\r\n url: string;\r\n key: string;\r\n size: number;\r\n contentType: string;\r\n }> {\r\n const formData = new FormData();\r\n formData.append(\"file\", file);\r\n return postFormData(\"/api/uploads/images\", formData);\r\n },\r\n\r\n // ── Health ─────────────────────────────────────────────────────────────\r\n\r\n /**\r\n * GET /health\r\n * Public health check. Returns provider availability status.\r\n */\r\n healthCheck(): Promise<HealthResponse> {\r\n return get<HealthResponse>(\"/health\");\r\n },\r\n };\r\n}\r\n\r\nexport type LeapifyClient = ReturnType<typeof createLeapifyClient>;\r\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.cjs"}
|
package/dist/client/types.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
|