better-auth-studio 1.1.3-beta.51 → 1.1.3-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -423,6 +423,43 @@ app.listen(3000);
423
423
 
424
424
  Access at `http://localhost:3000/api/studio`
425
425
 
426
+ ### Cloudflare Workers
427
+
428
+ Use the Worker adapter when the Studio shell needs to run in a non-Node runtime. The adapter avoids Node-only imports, serves UI assets from Workers Assets or an asset map, and delegates Studio API routes to an edge-compatible handler.
429
+
430
+ ```typescript
431
+ import { betterAuthStudio } from "better-auth-studio/cloudflare-workers";
432
+ import { auth } from "./auth";
433
+
434
+ type Env = {
435
+ ASSETS: { fetch(request: Request): Promise<Response> | Response };
436
+ };
437
+
438
+ const studio = betterAuthStudio<Env>({
439
+ auth,
440
+ basePath: "/studio",
441
+ // Optional when your Worker has the default ASSETS binding.
442
+ assets: (env) => env.ASSETS,
443
+ apiHandler: async (request) => {
444
+ // Handle /api/* Studio routes with edge-compatible code here.
445
+ return new Response(JSON.stringify({ error: "Not implemented" }), {
446
+ status: 501,
447
+ headers: { "Content-Type": "application/json" },
448
+ });
449
+ },
450
+ });
451
+
452
+ export default {
453
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
454
+ return studio(request, env, ctx);
455
+ },
456
+ };
457
+ ```
458
+
459
+ Access at `https://your-worker.example.com/studio`
460
+
461
+ The built-in Studio API router still depends on Node-only modules, so Workers deployments should provide `apiHandler` for `/api/*` routes. Better Auth auth routes under `/auth/*` are delegated to `auth.handler` when available.
462
+
426
463
  ### Configuration Options
427
464
 
428
465
  | Option | Required | Description |
@@ -0,0 +1,101 @@
1
+ type MaybePromise<T> = T | Promise<T>;
2
+ export type CloudflareStudioAsset = string | Uint8Array | ArrayBuffer | Response;
3
+ export type CloudflareStudioAssetMap = Record<string, CloudflareStudioAsset>;
4
+ export type CloudflareStudioAssetBinding = {
5
+ fetch(request: Request): MaybePromise<Response>;
6
+ };
7
+ export type CloudflareStudioAssetSource<Env = unknown> = CloudflareStudioAssetBinding | CloudflareStudioAssetMap | ((env: Env) => MaybePromise<CloudflareStudioAssetBinding | CloudflareStudioAssetMap | null | undefined>);
8
+ export type CloudflareStudioMetadata = {
9
+ title?: string;
10
+ logo?: string;
11
+ favicon?: string;
12
+ company?: {
13
+ name?: string;
14
+ website?: string;
15
+ supportEmail?: string;
16
+ };
17
+ theme?: "dark" | "light" | "auto";
18
+ colors?: {
19
+ primary?: string;
20
+ secondary?: string;
21
+ accent?: string;
22
+ };
23
+ features?: {
24
+ users?: boolean;
25
+ sessions?: boolean;
26
+ organizations?: boolean;
27
+ analytics?: boolean;
28
+ tools?: boolean;
29
+ security?: boolean;
30
+ };
31
+ links?: Array<{
32
+ label: string;
33
+ url: string;
34
+ }>;
35
+ custom?: Record<string, unknown>;
36
+ };
37
+ export type CloudflareStudioAccessConfig = {
38
+ roles?: string[];
39
+ allowEmails?: string[];
40
+ allowIpAddresses?: string[];
41
+ blockIpAddresses?: string[];
42
+ sessionDuration?: number;
43
+ secret?: string;
44
+ };
45
+ export type CloudflareStudioApiContext<Env = unknown, Ctx = unknown> = {
46
+ env: Env;
47
+ ctx: Ctx;
48
+ path: string;
49
+ originalPath: string;
50
+ basePath: string;
51
+ config: CloudflareStudioConfig<Env, Ctx>;
52
+ };
53
+ export type CloudflareStudioApiHandler<Env = unknown, Ctx = unknown> = (request: Request, context: CloudflareStudioApiContext<Env, Ctx>) => MaybePromise<Response | null | undefined>;
54
+ export type CloudflareStudioIndexContext<Env = unknown, Ctx = unknown> = {
55
+ env: Env;
56
+ ctx: Ctx;
57
+ path: string;
58
+ config: CloudflareStudioConfig<Env, Ctx>;
59
+ };
60
+ export type CloudflareStudioConfig<Env = unknown, Ctx = unknown> = {
61
+ auth?: {
62
+ handler?: (request: Request) => MaybePromise<Response>;
63
+ [key: string]: unknown;
64
+ };
65
+ basePath?: string;
66
+ access?: CloudflareStudioAccessConfig;
67
+ metadata?: CloudflareStudioMetadata;
68
+ lastSeenAt?: {
69
+ enabled?: boolean;
70
+ columnName?: string;
71
+ };
72
+ tools?: {
73
+ exclude?: string[];
74
+ };
75
+ events?: {
76
+ enabled?: boolean;
77
+ liveMarquee?: {
78
+ enabled?: boolean;
79
+ pollInterval?: number;
80
+ speed?: number;
81
+ pauseOnHover?: boolean;
82
+ limit?: number;
83
+ sort?: "asc" | "desc";
84
+ colors?: Record<string, string>;
85
+ timeWindow?: Record<string, unknown>;
86
+ };
87
+ };
88
+ assets?: CloudflareStudioAssetSource<Env>;
89
+ indexHtml?: string | ((context: CloudflareStudioIndexContext<Env, Ctx>) => MaybePromise<string | Response | null | undefined>);
90
+ apiHandler?: CloudflareStudioApiHandler<Env, Ctx>;
91
+ };
92
+ export type CloudflareStudioFetchHandler<Env = unknown, Ctx = unknown> = (request: Request, env?: Env, ctx?: Ctx) => Promise<Response>;
93
+ /**
94
+ * Cloudflare Workers adapter for Better Auth Studio.
95
+ *
96
+ * This entrypoint avoids Node-only imports at module load time. It can serve Studio
97
+ * UI assets from a Workers Assets binding or an in-memory asset map and delegates
98
+ * API routes to an edge-compatible handler supplied by the host app.
99
+ */
100
+ export declare function betterAuthStudio<Env = unknown, Ctx = unknown>(config: CloudflareStudioConfig<Env, Ctx>): CloudflareStudioFetchHandler<Env, Ctx>;
101
+ export {};
@@ -0,0 +1,494 @@
1
+ const DEFAULT_METADATA = {
2
+ title: "Better Auth Studio",
3
+ logo: "",
4
+ favicon: "",
5
+ company: {
6
+ name: "",
7
+ website: "",
8
+ supportEmail: "",
9
+ },
10
+ theme: "dark",
11
+ };
12
+ const STATIC_FILE_NAMES = new Set(["/vite.svg", "/favicon.svg", "/logo.png", "/shaders.png"]);
13
+ const IP_HEADER_CANDIDATES = [
14
+ "x-forwarded-for",
15
+ "cf-connecting-ip",
16
+ "x-real-ip",
17
+ "x-client-ip",
18
+ "true-client-ip",
19
+ ];
20
+ /**
21
+ * Cloudflare Workers adapter for Better Auth Studio.
22
+ *
23
+ * This entrypoint avoids Node-only imports at module load time. It can serve Studio
24
+ * UI assets from a Workers Assets binding or an in-memory asset map and delegates
25
+ * API routes to an edge-compatible handler supplied by the host app.
26
+ */
27
+ export function betterAuthStudio(config) {
28
+ return async (request, env, ctx) => {
29
+ try {
30
+ const route = normalizeRoute(request, config.basePath);
31
+ const accessDecision = evaluateEdgeAccess(config.access, request);
32
+ if (!accessDecision.allowed) {
33
+ return jsonResponse({
34
+ success: false,
35
+ message: accessDecision.message,
36
+ reason: accessDecision.reason,
37
+ ...(accessDecision.ipAddress ? { ipAddress: accessDecision.ipAddress } : {}),
38
+ }, 403, request);
39
+ }
40
+ if (isStaticAssetPath(route.path)) {
41
+ return handleAssetRequest(request, route, config, env, ctx);
42
+ }
43
+ const apiPath = getApiPath(request, route, config);
44
+ if (apiPath) {
45
+ return handleApiRequest(request, route, apiPath, config, env, ctx);
46
+ }
47
+ return handleIndexRequest(request, route, config, env, ctx);
48
+ }
49
+ catch (error) {
50
+ console.error("Cloudflare Studio handler error:", error);
51
+ return jsonResponse({ error: "Internal server error" }, 500, request);
52
+ }
53
+ };
54
+ }
55
+ function normalizeBasePath(basePath) {
56
+ if (!basePath || basePath === "/")
57
+ return "";
58
+ const withLeadingSlash = basePath.startsWith("/") ? basePath : `/${basePath}`;
59
+ return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
60
+ }
61
+ function normalizeRoute(request, basePath) {
62
+ const url = new URL(request.url);
63
+ const normalizedBasePath = normalizeBasePath(basePath);
64
+ let path = url.pathname || "/";
65
+ if (normalizedBasePath) {
66
+ if (path === normalizedBasePath || path === `${normalizedBasePath}/`) {
67
+ path = "/";
68
+ }
69
+ else if (path.startsWith(`${normalizedBasePath}/`)) {
70
+ path = path.slice(normalizedBasePath.length) || "/";
71
+ }
72
+ }
73
+ return {
74
+ path: path || "/",
75
+ originalPath: url.pathname || "/",
76
+ basePath: normalizedBasePath,
77
+ search: url.search,
78
+ };
79
+ }
80
+ function isStaticAssetPath(path) {
81
+ return path.startsWith("/assets/") || STATIC_FILE_NAMES.has(path);
82
+ }
83
+ function wantsJsonResponse(request) {
84
+ const accept = request.headers.get("accept") || "";
85
+ return accept.includes("application/json") || accept === "*/*" || !accept.includes("text/html");
86
+ }
87
+ function getApiPath(request, route, config) {
88
+ if (route.path === "/api" || route.path.startsWith("/api/")) {
89
+ return route.path;
90
+ }
91
+ if (route.path === "/auth" || route.path.startsWith("/auth/")) {
92
+ return `/api${route.path}`;
93
+ }
94
+ if (config.basePath && route.path !== "/" && wantsJsonResponse(request)) {
95
+ return `/api${route.path}`;
96
+ }
97
+ return null;
98
+ }
99
+ async function handleApiRequest(request, route, apiPath, config, env, ctx) {
100
+ if (apiPath === "/api/health") {
101
+ return jsonResponse({
102
+ status: "ok",
103
+ environment: "cloudflare-workers",
104
+ timestamp: new Date().toISOString(),
105
+ }, 200, request);
106
+ }
107
+ const delegatedRequest = rewriteRequestPath(request, apiPath, route.search);
108
+ const context = {
109
+ env,
110
+ ctx,
111
+ path: apiPath,
112
+ originalPath: route.originalPath,
113
+ basePath: route.basePath,
114
+ config,
115
+ };
116
+ const apiResponse = await config.apiHandler?.(delegatedRequest, context);
117
+ if (apiResponse) {
118
+ return finalizeResponse(request, apiResponse);
119
+ }
120
+ if (apiPath.startsWith("/api/auth/") && typeof config.auth?.handler === "function") {
121
+ const authResponse = await config.auth.handler(delegatedRequest);
122
+ return finalizeResponse(request, authResponse);
123
+ }
124
+ return jsonResponse({
125
+ error: "Cloudflare Workers API handler not configured",
126
+ message: "The Cloudflare Workers adapter is edge-safe and can serve the Studio shell, but the built-in Studio API still depends on Node-only modules. Provide apiHandler to handle /api/* routes in your Worker.",
127
+ path: apiPath,
128
+ }, 501, request);
129
+ }
130
+ async function handleAssetRequest(request, route, config, env, _ctx) {
131
+ const source = await resolveAssetSource(config, env);
132
+ const response = source ? await readAsset(source, request, route.path, route.search) : null;
133
+ if (response && response.status !== 404) {
134
+ return finalizeResponse(request, response);
135
+ }
136
+ return new Response(null, {
137
+ status: 404,
138
+ headers: {
139
+ "Cache-Control": "no-cache",
140
+ },
141
+ });
142
+ }
143
+ async function handleIndexRequest(request, route, config, env, ctx) {
144
+ const configuredIndex = await readConfiguredIndex(config, env, ctx, route.path);
145
+ if (configuredIndex) {
146
+ return htmlResponse(request, await responseToHtml(configuredIndex), config);
147
+ }
148
+ const source = await resolveAssetSource(config, env);
149
+ const assetIndex = source ? await readAsset(source, request, "/index.html", route.search) : null;
150
+ if (assetIndex && assetIndex.status >= 200 && assetIndex.status < 300) {
151
+ return htmlResponse(request, await assetIndex.text(), config);
152
+ }
153
+ return htmlResponse(request, getMissingAssetsHtml(config), config, 503);
154
+ }
155
+ async function readConfiguredIndex(config, env, ctx, path) {
156
+ if (!config.indexHtml)
157
+ return null;
158
+ if (typeof config.indexHtml === "string") {
159
+ return config.indexHtml;
160
+ }
161
+ const value = await config.indexHtml({
162
+ env,
163
+ ctx,
164
+ path,
165
+ config,
166
+ });
167
+ return value || null;
168
+ }
169
+ async function responseToHtml(value) {
170
+ if (typeof value === "string")
171
+ return value;
172
+ return value.text();
173
+ }
174
+ async function resolveAssetSource(config, env) {
175
+ if (typeof config.assets === "function") {
176
+ return (await config.assets(env)) || null;
177
+ }
178
+ if (config.assets) {
179
+ return config.assets;
180
+ }
181
+ const defaultBinding = getDefaultAssetsBinding(env);
182
+ return defaultBinding || null;
183
+ }
184
+ function getDefaultAssetsBinding(env) {
185
+ const maybeAssets = env?.ASSETS;
186
+ if (isAssetBinding(maybeAssets)) {
187
+ return maybeAssets;
188
+ }
189
+ return null;
190
+ }
191
+ async function readAsset(source, request, path, search) {
192
+ if (isAssetBinding(source)) {
193
+ return source.fetch(rewriteRequestPath(request, path, search));
194
+ }
195
+ const asset = findAssetInMap(source, path);
196
+ if (!asset)
197
+ return null;
198
+ if (asset instanceof Response)
199
+ return asset.clone();
200
+ return new Response(asset, {
201
+ status: 200,
202
+ headers: {
203
+ "Content-Type": getContentType(path, typeof asset === "string"),
204
+ "Cache-Control": getCacheControl(path),
205
+ },
206
+ });
207
+ }
208
+ function isAssetBinding(value) {
209
+ return !!value && typeof value === "object" && typeof value.fetch === "function";
210
+ }
211
+ function findAssetInMap(source, path) {
212
+ const normalizedPath = path === "/" ? "/index.html" : path;
213
+ const candidates = [
214
+ normalizedPath,
215
+ normalizedPath.startsWith("/") ? normalizedPath.slice(1) : `/${normalizedPath}`,
216
+ ];
217
+ for (const candidate of candidates) {
218
+ const asset = source[candidate];
219
+ if (asset)
220
+ return asset;
221
+ }
222
+ return null;
223
+ }
224
+ function rewriteRequestPath(request, path, search) {
225
+ const url = new URL(request.url);
226
+ url.pathname = path;
227
+ url.search = search;
228
+ return new Request(url.toString(), request);
229
+ }
230
+ function prepareFrontendConfig(config) {
231
+ const metadata = {
232
+ ...DEFAULT_METADATA,
233
+ ...config.metadata,
234
+ company: {
235
+ ...DEFAULT_METADATA.company,
236
+ ...config.metadata?.company,
237
+ },
238
+ };
239
+ const liveMarqueeConfig = config.events?.liveMarquee;
240
+ const shouldIncludeLiveMarquee = !!liveMarqueeConfig || !!config.events?.enabled;
241
+ return {
242
+ basePath: normalizeBasePath(config.basePath),
243
+ metadata,
244
+ liveMarquee: shouldIncludeLiveMarquee
245
+ ? {
246
+ enabled: liveMarqueeConfig?.enabled !== false,
247
+ pollInterval: liveMarqueeConfig?.pollInterval || 2000,
248
+ speed: liveMarqueeConfig?.speed ?? 0.5,
249
+ pauseOnHover: liveMarqueeConfig?.pauseOnHover ?? true,
250
+ limit: liveMarqueeConfig?.limit ?? 50,
251
+ sort: liveMarqueeConfig?.sort ?? "desc",
252
+ colors: liveMarqueeConfig?.colors || undefined,
253
+ timeWindow: liveMarqueeConfig?.timeWindow || undefined,
254
+ }
255
+ : undefined,
256
+ lastSeenAt: config.lastSeenAt && typeof config.lastSeenAt === "object"
257
+ ? {
258
+ enabled: !!config.lastSeenAt.enabled,
259
+ columnName: config.lastSeenAt.columnName,
260
+ }
261
+ : undefined,
262
+ tools: config.tools && Array.isArray(config.tools.exclude) && config.tools.exclude.length > 0
263
+ ? { exclude: config.tools.exclude }
264
+ : undefined,
265
+ };
266
+ }
267
+ function injectConfig(html, config) {
268
+ const frontendConfig = prepareFrontendConfig(config);
269
+ const safeJson = JSON.stringify(frontendConfig)
270
+ .replace(/</g, "\\u003c")
271
+ .replace(/>/g, "\\u003e")
272
+ .replace(/&/g, "\\u0026");
273
+ const escapedTitle = escapeHtml(frontendConfig.metadata.title);
274
+ let modifiedHtml = html.replace(/<title>.*?<\/title>/i, `<title>${escapedTitle}</title>`);
275
+ if (frontendConfig.metadata.favicon) {
276
+ const favicon = escapeHtml(frontendConfig.metadata.favicon);
277
+ const faviconTag = `<link rel="icon" type="${getContentType(favicon)}" href="${favicon}" />`;
278
+ modifiedHtml = modifiedHtml.replace(/<link[^>]*rel=["'](icon|shortcut icon)["'][^>]*>/gi, faviconTag);
279
+ if (!modifiedHtml.includes('rel="icon"') && !modifiedHtml.includes("rel='icon'")) {
280
+ modifiedHtml = modifiedHtml.replace("</head>", ` ${faviconTag}\n</head>`);
281
+ }
282
+ }
283
+ if (frontendConfig.basePath) {
284
+ modifiedHtml = modifiedHtml
285
+ .replace(/href="\/assets\//g, `href="${frontendConfig.basePath}/assets/`)
286
+ .replace(/src="\/assets\//g, `src="${frontendConfig.basePath}/assets/`)
287
+ .replace(/href="\/vite\.svg"/g, `href="${frontendConfig.basePath}/vite.svg"`)
288
+ .replace(/href="\/favicon\.svg"/g, `href="${frontendConfig.basePath}/favicon.svg"`)
289
+ .replace(/href="\/logo\.png"/g, `href="${frontendConfig.basePath}/logo.png"`)
290
+ .replace(/src="\/logo\.png"/g, `src="${frontendConfig.basePath}/logo.png"`);
291
+ }
292
+ const script = `
293
+ <script>
294
+ const __BAS_THEME_KEY__ = "better-auth-studio-theme";
295
+ window.__STUDIO_CONFIG__ = ${safeJson};
296
+ Object.freeze(window.__STUDIO_CONFIG__);
297
+ try {
298
+ const configuredTheme = window.__STUDIO_CONFIG__?.metadata?.theme === "light" ? "light" : "dark";
299
+ const storedTheme = window.localStorage.getItem(__BAS_THEME_KEY__);
300
+ const activeTheme = storedTheme === "light" || storedTheme === "dark" ? storedTheme : configuredTheme;
301
+ document.documentElement.dataset.theme = activeTheme;
302
+ document.documentElement.style.colorScheme = activeTheme;
303
+ document.documentElement.classList.remove("light", "dark");
304
+ document.documentElement.classList.add(activeTheme);
305
+ } catch {
306
+ document.documentElement.dataset.theme = window.__STUDIO_CONFIG__?.metadata?.theme === "light" ? "light" : "dark";
307
+ document.documentElement.style.colorScheme = document.documentElement.dataset.theme;
308
+ document.documentElement.classList.remove("light", "dark");
309
+ document.documentElement.classList.add(document.documentElement.dataset.theme);
310
+ }
311
+ if (window.__STUDIO_CONFIG__?.metadata?.title) {
312
+ document.title = window.__STUDIO_CONFIG__.metadata.title;
313
+ }
314
+ </script>
315
+ `;
316
+ if (modifiedHtml.includes("</head>")) {
317
+ return modifiedHtml.replace("</head>", `${script}</head>`);
318
+ }
319
+ return `${script}${modifiedHtml}`;
320
+ }
321
+ function htmlResponse(request, html, config, status = 200) {
322
+ const body = request.method === "HEAD" ? null : injectConfig(html, config);
323
+ return new Response(body, {
324
+ status,
325
+ headers: {
326
+ "Content-Type": "text/html; charset=utf-8",
327
+ "Cache-Control": "no-cache",
328
+ },
329
+ });
330
+ }
331
+ function jsonResponse(data, status, request) {
332
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(data), {
333
+ status,
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ "Cache-Control": "no-cache",
337
+ },
338
+ });
339
+ }
340
+ function finalizeResponse(request, response) {
341
+ if (request.method !== "HEAD")
342
+ return response;
343
+ return new Response(null, {
344
+ status: response.status,
345
+ statusText: response.statusText,
346
+ headers: response.headers,
347
+ });
348
+ }
349
+ function getMissingAssetsHtml(config) {
350
+ const basePath = normalizeBasePath(config.basePath) || "/";
351
+ return `<!DOCTYPE html>
352
+ <html>
353
+ <head>
354
+ <title>Better Auth Studio</title>
355
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
356
+ <style>
357
+ body { font-family: system-ui, sans-serif; background: #050505; color: #f5f5f5; max-width: 720px; margin: 56px auto; padding: 0 24px; line-height: 1.6; }
358
+ code { background: #181818; border: 1px solid #2a2a2a; padding: 2px 6px; }
359
+ pre { background: #101010; border: 1px solid #2a2a2a; padding: 16px; overflow-x: auto; }
360
+ </style>
361
+ </head>
362
+ <body>
363
+ <h1>Studio assets are not configured</h1>
364
+ <p>The Cloudflare Workers adapter is running at <code>${escapeHtml(basePath)}</code>, but it could not find Studio UI assets.</p>
365
+ <p>Bind Workers Assets as <code>ASSETS</code>, pass an <code>assets</code> binding, or provide <code>indexHtml</code> when creating the handler.</p>
366
+ <pre>import { betterAuthStudio } from "better-auth-studio/cloudflare-workers";
367
+
368
+ const studio = betterAuthStudio({
369
+ basePath: "${escapeHtml(basePath === "/" ? "" : basePath)}",
370
+ });
371
+
372
+ export default {
373
+ fetch: (request, env, ctx) => studio(request, env, ctx),
374
+ };</pre>
375
+ </body>
376
+ </html>`;
377
+ }
378
+ function evaluateEdgeAccess(accessConfig, request) {
379
+ const ipAddress = extractClientIp(request.headers);
380
+ const allowIpAddresses = accessConfig?.allowIpAddresses?.filter((value) => value.trim().length);
381
+ if (allowIpAddresses?.length) {
382
+ if (!ipAddress || !allowIpAddresses.some((rule) => ipMatchesRule(ipAddress, rule))) {
383
+ return {
384
+ allowed: false,
385
+ ipAddress,
386
+ reason: "ip_not_allowed",
387
+ message: "Access denied. This IP address is not in the allowed list.",
388
+ };
389
+ }
390
+ }
391
+ const blockIpAddresses = accessConfig?.blockIpAddresses?.filter((value) => value.trim().length);
392
+ if (blockIpAddresses?.length && ipAddress) {
393
+ const blocked = blockIpAddresses.some((rule) => ipMatchesRule(ipAddress, rule));
394
+ if (blocked) {
395
+ return {
396
+ allowed: false,
397
+ ipAddress,
398
+ reason: "ip_blocked",
399
+ message: "Access denied. This IP address is blocked.",
400
+ };
401
+ }
402
+ }
403
+ return { allowed: true, ipAddress };
404
+ }
405
+ function extractClientIp(headers) {
406
+ for (const header of IP_HEADER_CANDIDATES) {
407
+ const value = headers.get(header);
408
+ if (!value)
409
+ continue;
410
+ const ip = normalizeIpToken(value.split(",")[0]);
411
+ if (ip)
412
+ return ip;
413
+ }
414
+ const forwarded = headers.get("forwarded");
415
+ if (forwarded) {
416
+ const entries = forwarded.split(",");
417
+ for (const entry of entries) {
418
+ const match = entry.match(/for=("?\[?[a-fA-F0-9:.]+\]?"?)/i);
419
+ if (!match)
420
+ continue;
421
+ const ip = normalizeIpToken(match[1]);
422
+ if (ip)
423
+ return ip;
424
+ }
425
+ }
426
+ return null;
427
+ }
428
+ function normalizeIpToken(raw) {
429
+ if (!raw)
430
+ return null;
431
+ let value = raw.trim();
432
+ if (!value || value.toLowerCase() === "unknown")
433
+ return null;
434
+ if (value.toLowerCase().startsWith("for="))
435
+ value = value.slice(4).trim();
436
+ value = value.replace(/^"+|"+$/g, "");
437
+ if (value.startsWith("[")) {
438
+ const end = value.indexOf("]");
439
+ if (end > 0)
440
+ value = value.slice(1, end);
441
+ }
442
+ if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(value))
443
+ value = value.split(":")[0] || value;
444
+ if (value.startsWith("::ffff:"))
445
+ value = value.slice(7);
446
+ return value || null;
447
+ }
448
+ function ipMatchesRule(ipAddress, rule) {
449
+ const trimmedRule = rule.trim();
450
+ if (!trimmedRule)
451
+ return false;
452
+ if (!trimmedRule.includes("*")) {
453
+ return ipAddress === normalizeIpToken(trimmedRule);
454
+ }
455
+ const pattern = `^${escapeRegExp(trimmedRule).replace(/\\\*/g, ".*")}$`;
456
+ return new RegExp(pattern).test(ipAddress);
457
+ }
458
+ function escapeRegExp(value) {
459
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
460
+ }
461
+ function escapeHtml(value) {
462
+ return value
463
+ .replace(/&/g, "&amp;")
464
+ .replace(/</g, "&lt;")
465
+ .replace(/>/g, "&gt;")
466
+ .replace(/"/g, "&quot;")
467
+ .replace(/'/g, "&#39;");
468
+ }
469
+ function getContentType(path, preferHtml = false) {
470
+ const cleanPath = path.split("?")[0] || "";
471
+ const ext = cleanPath.slice(cleanPath.lastIndexOf(".")).toLowerCase();
472
+ const types = {
473
+ ".html": "text/html; charset=utf-8",
474
+ ".js": "application/javascript",
475
+ ".css": "text/css",
476
+ ".json": "application/json",
477
+ ".png": "image/png",
478
+ ".jpg": "image/jpeg",
479
+ ".jpeg": "image/jpeg",
480
+ ".svg": "image/svg+xml",
481
+ ".ico": "image/x-icon",
482
+ ".webp": "image/webp",
483
+ ".woff": "font/woff",
484
+ ".woff2": "font/woff2",
485
+ ".ttf": "font/ttf",
486
+ };
487
+ return types[ext] || (preferHtml ? "text/html; charset=utf-8" : "application/octet-stream");
488
+ }
489
+ function getCacheControl(path) {
490
+ if (path.match(/\.(js|css|png|jpg|jpeg|svg|webp|woff|woff2|ttf)$/)) {
491
+ return "public, max-age=31536000, immutable";
492
+ }
493
+ return "no-cache";
494
+ }
@@ -9,7 +9,7 @@ export type UniversalResponse = {
9
9
  status: number;
10
10
  headers: Record<string, string>;
11
11
  setCookies?: string[];
12
- body: string | Buffer;
12
+ body: string | Uint8Array | ArrayBuffer;
13
13
  };
14
14
  export type TimeWindowPreset = "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "12h" | "1d" | "2d" | "3d" | "7d" | "14d" | "30d";
15
15
  export type TimeWindowConfig = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth-studio",
3
- "version": "1.1.3-beta.51",
3
+ "version": "1.1.3-beta.52",
4
4
  "description": "Studio for Better Auth",
5
5
  "keywords": [
6
6
  "admin",
@@ -28,6 +28,7 @@
28
28
  "./express": "./dist/adapters/express.js",
29
29
  "./nextjs": "./dist/adapters/nextjs.js",
30
30
  "./hono": "./dist/adapters/hono.js",
31
+ "./cloudflare-workers": "./dist/adapters/cloudflare-workers.js",
31
32
  "./elysia": "./dist/adapters/elysia.js",
32
33
  "./svelte-kit": "./dist/adapters/svelte-kit.js",
33
34
  "./solid-start": "./dist/adapters/solid-start.js",