better-auth-cloudflare
Version:
Seamlessly integrate better-auth with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services.
189 lines (188 loc) • 8.03 kB
JavaScript
import {} from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
import { schema } from "./schema";
import { createR2Storage, createR2Endpoints } from "./r2";
export * from "./client";
export * from "./schema";
export * from "./types";
export * from "./r2";
/**
* Cloudflare integration for Better Auth
*
* @param options - Plugin configuration options
* @returns Better Auth plugin for Cloudflare
*/
export const cloudflare = (options) => {
const opts = options ?? {};
// Default geolocationTracking to true if not specified
const geolocationTrackingEnabled = opts.geolocationTracking === undefined || opts.geolocationTracking;
let r2Storage = null;
return {
id: "cloudflare",
schema: schema(opts),
endpoints: {
getGeolocation: createAuthEndpoint("/cloudflare/geolocation", {
method: "GET",
}, async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (!session) {
return ctx.json({ error: "Unauthorized" }, { status: 401 });
}
const cf = await Promise.resolve(opts.cf);
if (!cf) {
return ctx.json({ error: "Cloudflare context is not available" }, { status: 404 });
}
// Extract and validate Cloudflare geolocation data
const context = extractGeolocationData(cf);
return ctx.json(context);
}),
...(opts.r2 ? createR2Endpoints(() => r2Storage, opts.r2) : {}),
},
init(init_ctx) {
if (opts.r2) {
r2Storage = createR2Storage(opts.r2, init_ctx.generateId);
}
return {
options: {
databaseHooks: {
session: {
create: {
before: async (s) => {
if (!geolocationTrackingEnabled) {
return s;
}
const cf = await Promise.resolve(opts.cf);
if (!cf) {
return s;
}
const geoData = extractGeolocationData(cf);
s.timezone = geoData.timezone;
s.city = geoData.city;
s.country = geoData.country;
s.region = geoData.region;
s.regionCode = geoData.regionCode;
s.colo = geoData.colo;
s.latitude = geoData.latitude;
s.longitude = geoData.longitude;
return s;
},
},
},
},
},
};
},
};
};
/**
* Safely extracts CloudflareGeolocation data, ignoring undefined values or other fields
*/
function extractGeolocationData(input) {
if (!input || typeof input !== "object") {
return {};
}
return {
timezone: input.timezone || undefined,
city: input.city || undefined,
country: input.country || undefined,
region: input.region || undefined,
regionCode: input.regionCode || undefined,
colo: input.colo || undefined,
latitude: input.latitude || undefined,
longitude: input.longitude || undefined,
};
}
/**
* Creates secondary storage using Cloudflare KV
*
* @param kv - Cloudflare KV namespace
* @returns SecondaryStorage implementation
*/
export const createKVStorage = (kv) => {
return {
get: async (key) => {
return kv.get(key);
},
set: async (key, value, ttl) => {
return kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
},
delete: async (key) => {
return kv.delete(key);
},
};
};
/**
* Enhances BetterAuthOptions with Cloudflare-specific configurations.
*
* This function integrates Cloudflare services like D1 for database and KV for secondary storage,
* and sets up IP address detection and geolocation tracking based on the provided Cloudflare options.
*
* @param cloudFlareOptions - Options for configuring Cloudflare integration.
* @param options - The base BetterAuthOptions to be enhanced.
* @returns BetterAuthOptions configured for use with Cloudflare.
*/
export const withCloudflare = (cloudFlareOptions, options) => {
const autoDetectIpEnabled = cloudFlareOptions.autoDetectIpAddress === undefined || cloudFlareOptions.autoDetectIpAddress === true;
const geolocationTrackingForSession = cloudFlareOptions.geolocationTracking === undefined || cloudFlareOptions.geolocationTracking === true;
if (autoDetectIpEnabled || geolocationTrackingForSession) {
if (!cloudFlareOptions.cf) {
throw new Error("Cloudflare context is required for geolocation or IP detection features. Be sure to pass the `cf` option to the withCloudflare function.");
}
}
let updatedAdvanced = { ...options.advanced };
if (autoDetectIpEnabled) {
updatedAdvanced.ipAddress = {
...(updatedAdvanced.ipAddress ?? {}),
ipAddressHeaders: ["cf-connecting-ip", "x-real-ip", ...(updatedAdvanced.ipAddress?.ipAddressHeaders ?? [])],
};
}
else if (updatedAdvanced.ipAddress?.ipAddressHeaders) {
// If autoDetectIp is disabled, ensure our headers are not in the list if they were added by default elsewhere
// This part is tricky as we don't know if they were from the user or our default.
// A safer approach might be to just rely on the user to not list them if they disable this flag.
// For now, let's assume if autoDetectIpEnabled is false, the user manages headers explicitly.
}
let updatedSession = { ...options.session };
if (geolocationTrackingForSession) {
updatedSession.storeSessionInDatabase = true;
}
else if (options.session?.storeSessionInDatabase === undefined) {
// If geolocationTracking is false, and the user hasn't set a preference for storeSessionInDatabase,
// it will remain undefined (i.e., Better Auth core default behavior).
// If user explicitly set it to true/false, that will be respected.
}
// Assert that only one database configuration is provided
const dbConfigs = [cloudFlareOptions.postgres, cloudFlareOptions.mysql, cloudFlareOptions.d1].filter(Boolean);
if (dbConfigs.length > 1) {
throw new Error("Only one database configuration can be provided. Please provide only one of postgres, mysql, or d1.");
}
// Determine which database configuration to use
let database;
if (cloudFlareOptions.postgres) {
database = drizzleAdapter(cloudFlareOptions.postgres.db, {
provider: "pg",
...cloudFlareOptions.postgres.options,
});
}
else if (cloudFlareOptions.mysql) {
database = drizzleAdapter(cloudFlareOptions.mysql.db, {
provider: "mysql",
...cloudFlareOptions.mysql.options,
});
}
else if (cloudFlareOptions.d1) {
database = drizzleAdapter(cloudFlareOptions.d1.db, {
provider: "sqlite",
...cloudFlareOptions.d1.options,
});
}
return {
...options,
database,
secondaryStorage: cloudFlareOptions.kv ? createKVStorage(cloudFlareOptions.kv) : undefined,
plugins: [cloudflare(cloudFlareOptions), ...(options.plugins ?? [])],
advanced: updatedAdvanced,
session: updatedSession,
};
};