diff --git a/README.md b/README.md index f2a3827..148ca46 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,6 @@ skyscheduler/ #### Server - `BetterAuth` - site login/authentication -- `BetterAuthCloudflare` - helper for BetterAuth on CF - `Hono` - request routing/processing - `uuid` - id generation - `zod` - data validation diff --git a/package-lock.json b/package-lock.json index 6318904..d635ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@atproto/xrpc": "^0.8.10", "@hono/standard-validator": "^0.3.0", "better-auth": "^1.6.26", - "better-auth-cloudflare": "0.3.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", "hono": "^4.13.3", @@ -432,13 +431,6 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workers-types": { - "version": "5.20260819.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260819.1.tgz", - "integrity": "sha512-nUoWrl+16WfocHgXAARAvpQ7dLMF4tSmTvvgLLg5clYhP2j8CYerZ2KC8agnlWHqOAKp45B3F+LcsjNZrZyiRA==", - "license": "MIT OR Apache-2.0", - "peer": true - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3117,22 +3109,6 @@ } } }, - "node_modules/better-auth-cloudflare": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/better-auth-cloudflare/-/better-auth-cloudflare-0.3.1.tgz", - "integrity": "sha512-kvGYe1++fH4ODkb2o/WNkGWQtPYuWXjNPaKDAvPz8ChbaROW9TmZY0AIan9K52p+p+Pe53ZnQ7kKOWWZ2x0KrQ==", - "license": "MIT", - "dependencies": { - "drizzle-orm": "^0.45.0", - "mime": "^4.1.0", - "zod": "^4.3.0" - }, - "peerDependencies": { - "@better-auth/drizzle-adapter": "^1.5.0", - "@cloudflare/workers-types": ">=4", - "better-auth": "^1.5.0" - } - }, "node_modules/better-call": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz", @@ -4645,21 +4621,6 @@ "tslib": "^2.0.3" } }, - "node_modules/mime": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", - "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/miniflare": { "version": "5.20260815.0-alpha", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260815.0-alpha.tgz", diff --git a/package.json b/package.json index 1dcd2b4..a518c47 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@atproto/xrpc": "^0.8.10", "@hono/standard-validator": "^0.3.0", "better-auth": "^1.6.26", - "better-auth-cloudflare": "0.3.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", "hono": "^4.13.3", diff --git a/src/auth/index.ts b/src/auth/index.ts index be514d2..8ae779f 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,10 +1,13 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { betterAuth } from "better-auth/minimal"; import { username } from "better-auth/plugins"; -import { withCloudflare } from "better-auth-cloudflare"; import { APP_NAME } from "../appInfo"; import { DEFAULT_PDS } from "../config"; -import { BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits"; +import { + BSKY_MAX_USERNAME_LENGTH, + BSKY_MIN_USERNAME_LENGTH, + CF_KV_MIN_TTL, +} from "../limits"; import type { AllContext, BaseContext, Bindings, DBProcessor } from "../types"; import { createDMWithUsername } from "../utils/bsky/bskyMessage"; import { isInDev } from "../utils/helpers"; @@ -22,11 +25,19 @@ if (process.env["IS_SSG"] === "true") { waitUntil = (await import("cloudflare:workers")).waitUntil; } +function adjustTTL(inTTL?: number) { + let useTTL: number | undefined = undefined; + if (inTTL !== undefined) { + useTTL = (inTTL < CF_KV_MIN_TTL) ? CF_KV_MIN_TTL : inTTL; + } + return useTTL; +} + // Single auth configuration that handles both CLI and runtime scenarios function createAuth(c?: AllContext) { - const env: Bindings|undefined = c?.env; + const env: Bindings | undefined = c?.env; // Use actual DB for runtime, empty object for CLI - const db = c ? c.get("db") : ({} as DBProcessor); + const db: DBProcessor = c ? c.get("db") : ({} as DBProcessor); return betterAuth({ // pretty much disable everything but the /reset-password // this is just overkill tbh, we hardcode the routes we support in index @@ -49,7 +60,6 @@ function createAuth(c?: AllContext) { "/link-social", "/list-accounts", "/list-sessions", - "/cloudflare/geolocation", "/is-username-available", "/delete-user", "/delete-user/callback", @@ -66,119 +76,123 @@ function createAuth(c?: AllContext) { "/request-password-reset", "/revoke-sessions", ], - ...withCloudflare( - { - autoDetectIpAddress: false, - geolocationTracking: false, - d1: env - ? { - db, - options: { - usePlural: true, - debugLogs: false, - }, - } - : undefined, + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + sendResetPassword: async ({user, url, token}, _request) => { // @ts-ignore - kv: env?.KV, + const userName: string = user.username; + await createDMWithUsername(env!, userName, createPasswordResetMessage(url, token)) + .then((resp) => { + if (!resp) + throw new Error("FAILED_MESSAGE"); + }); }, - { - emailAndPassword: { - enabled: true, - requireEmailVerification: false, - sendResetPassword: async ({user, url, token}, _request) => { - // @ts-ignore - const userName: string = user.username; - await createDMWithUsername(env!, userName, createPasswordResetMessage(url, token)) - .then((resp) => { - if (!resp) - throw new Error("FAILED_MESSAGE"); - }); - }, + }, + plugins: [ + username({ + // We validate all of our usernames ahead of time + // do not use the validator in betterauth but instead our own ZOD system + usernameValidator: (_username) => { + return true; }, - plugins: [ - username({ - // We validate all of our usernames ahead of time - // do not use the validator in betterauth but instead our own ZOD system - usernameValidator: (_username) => { - return true; - }, - displayUsernameValidator: (_displayUsername) => { - return true; - }, - /* we do our own normalization in the zod schemas */ - usernameNormalization: false, - displayUsernameNormalization: false, - minUsernameLength: BSKY_MIN_USERNAME_LENGTH, - maxUsernameLength: BSKY_MAX_USERNAME_LENGTH - }) - ], - rateLimit: { - enabled: true, - window: 60, - max: 100, - "*": { - window: 60, - max: 100, - }, + displayUsernameValidator: (_displayUsername) => { + return true; }, - } - ), + /* we do our own normalization in the zod schemas */ + usernameNormalization: false, + displayUsernameNormalization: false, + minUsernameLength: BSKY_MIN_USERNAME_LENGTH, + maxUsernameLength: BSKY_MAX_USERNAME_LENGTH, + }), + ], + rateLimit: { + enabled: true, + window: 60, + max: 100, + "*": { + window: 60, + max: 100, + }, + }, appName: APP_NAME, secret: env?.BETTER_AUTH_SECRET, - baseURL: (isInDev(env)) ? undefined : env?.BETTER_AUTH_URL, + baseURL: isInDev(env) ? undefined : env?.BETTER_AUTH_URL, user: { additionalFields: { bskyAppPass: { type: "string", - required: true + required: true, }, pds: { type: "string", defaultValue: DEFAULT_PDS, - required: true - } + required: true, + }, }, changeEmail: { - enabled: false + enabled: false, }, deleteUser: { enabled: false, - } + }, + }, + secondaryStorage: { + get: async (key: string) => { + return await env!.KV.get(key); + }, + getAndDelete: async (key: string) => { + const origValue = await env?.KV.get(key); + await env?.KV.delete(key); + return origValue; + }, + set: async (key: string, value: string, ttl?: number) => { + await env!.KV.put(key, value, { expirationTtl: adjustTTL(ttl) }); + }, + increment: async (key: string, ttl?: number) => { + const origValue: string|null = await env!.KV.get(key); + if (origValue === null) { + await env?.KV.put(key, "1", { expirationTtl: adjustTTL(ttl) }); + return 1; + } + // This should probably check NaN + const newNumber = Number(origValue) + 1; + await env?.KV.put(key, newNumber.toString()); + return newNumber; + }, + delete: async (key: string) => { + await env?.KV.delete(key); + }, }, + database: drizzleAdapter(db ?? {}, { + provider: "sqlite", + usePlural: true, + debugLogs: false, + }), account: { accountLinking: { - enabled: false + enabled: false, }, }, telemetry: { - enabled: false + enabled: false, }, logger: { - disabled: true + disabled: true, }, advanced: { ipAddress: { - ipAddressHeaders: ['cf-connecting-ip'] + ipAddressHeaders: ["cf-connecting-ip"], }, backgroundTasks: { handler: waitUntil, }, }, - // Only add database adapter for CLI schema generation - // though better-auth-cloudflare just injects this anyways - ...(env ? {} : { - database: drizzleAdapter({}, { - provider: "sqlite", - usePlural: true, - debugLogs: false, - }), - }), }); -}; +} -const processAuthRoute = (ctx: BaseContext) => ctx.get("auth").handler(ctx.req.raw); +const processAuthRoute = (ctx: BaseContext) => + ctx.get("auth").handler(ctx.req.raw); // Export for runtime usage export { createAuth, processAuthRoute }; - diff --git a/src/limits.ts b/src/limits.ts index d8763aa..ed5e03b 100644 --- a/src/limits.ts +++ b/src/limits.ts @@ -152,3 +152,5 @@ export const MAX_GIF_LENGTH_LIMIT: number = MAX_GIF_LENGTH * TO_SEC; export const CF_IMAGES_FILE_SIZE_LIMIT_IN_MB: number = 70; export const CF_IMAGES_FILE_SIZE_LIMIT: number = CF_IMAGES_FILE_SIZE_LIMIT_IN_MB * MB_TO_BYTES; export const CF_IMAGES_MAX_DIMENSION: number = 10000; +// Limits for Cloudflare KV +export const CF_KV_MIN_TTL: number = 60; \ No newline at end of file