perf(auth): use Better Auth hot-path features

This commit is contained in:
saltbo
2026-07-26 15:55:44 -04:00
parent 67623d0238
commit e7e7c4481b
14 changed files with 152 additions and 212 deletions
+3 -78
View File
@@ -1,8 +1,7 @@
import { type ApiKeyScope, ApiKeyTemplate, apiKeyMetadata, parseApiKeyScope } from '@shared/api-key-templates'
import { and, eq, inArray } from 'drizzle-orm'
import { apikey, member } from '../../db/auth-schema'
import { type ApiKeyScope, ApiKeyTemplate, parseApiKeyScope } from '@shared/api-key-templates'
import { inArray } from 'drizzle-orm'
import { apikey } from '../../db/auth-schema'
import type { Database } from '../../platform/interface'
import { resolveOrganizationOwnerUserId } from './organization-owner'
const WORKSPACE_TEMPLATES = [ApiKeyTemplate.IHOST, ApiKeyTemplate.REMOTE_DOWNLOAD]
@@ -17,80 +16,6 @@ export function scopeForApiKey(configId: string, metadata: unknown): ApiKeyScope
return null
}
export async function normalizeLegacyApiKey(
db: Database,
key: { id: string; configId: string; referenceId: string; metadata: unknown },
): Promise<{ referenceId: string; scope: ApiKeyScope } | null> {
const existingScope = scopeForApiKey(key.configId, key.metadata)
if (existingScope) return { referenceId: key.referenceId, scope: existingScope }
if (key.configId === ApiKeyTemplate.WEBDAV) {
const scope = { mode: 'user-workspaces' } as const
await db
.update(apikey)
.set({ metadata: JSON.stringify(apiKeyMetadata(scope)), updatedAt: new Date() })
.where(and(eq(apikey.id, key.id), eq(apikey.referenceId, key.referenceId)))
return { referenceId: key.referenceId, scope }
}
if (!WORKSPACE_TEMPLATES.includes(key.configId as (typeof WORKSPACE_TEMPLATES)[number])) return null
let ownerUserId: string
try {
ownerUserId = await resolveOrganizationOwnerUserId(db, key.referenceId)
} catch {
await db.update(apikey).set({ enabled: false, updatedAt: new Date() }).where(eq(apikey.id, key.id))
return null
}
const scope = { mode: 'workspace', orgId: key.referenceId } as const
await db
.update(apikey)
.set({
referenceId: ownerUserId,
metadata: JSON.stringify(apiKeyMetadata(scope)),
updatedAt: new Date(),
})
.where(and(eq(apikey.id, key.id), eq(apikey.referenceId, key.referenceId)))
return { referenceId: ownerUserId, scope }
}
export async function normalizeLegacyApiKeysForUser(db: Database, userId: string): Promise<void> {
const webDavKeys = await db
.select({ id: apikey.id, metadata: apikey.metadata })
.from(apikey)
.where(and(eq(apikey.configId, ApiKeyTemplate.WEBDAV), eq(apikey.referenceId, userId)))
const legacyWebDavKeyIds = webDavKeys
.filter(({ metadata }) => scopeForApiKey(ApiKeyTemplate.WEBDAV, parseMetadata(metadata)) === null)
.map(({ id }) => id)
if (legacyWebDavKeyIds.length > 0) {
await db
.update(apikey)
.set({
metadata: JSON.stringify(apiKeyMetadata({ mode: 'user-workspaces' })),
updatedAt: new Date(),
})
.where(inArray(apikey.id, legacyWebDavKeyIds))
}
const ownedOrgs = await db
.select({ orgId: member.organizationId })
.from(member)
.where(and(eq(member.userId, userId), eq(member.role, 'owner')))
for (const { orgId } of ownedOrgs) {
if ((await resolveOrganizationOwnerUserId(db, orgId)) !== userId) continue
await db
.update(apikey)
.set({
referenceId: userId,
metadata: JSON.stringify(apiKeyMetadata({ mode: 'workspace', orgId })),
updatedAt: new Date(),
})
.where(and(inArray(apikey.configId, WORKSPACE_TEMPLATES), eq(apikey.referenceId, orgId)))
}
}
export async function deleteApiKeysScopedToOrganization(db: Database, orgId: string): Promise<void> {
const rows = await db
.select({ id: apikey.id, configId: apikey.configId, referenceId: apikey.referenceId, metadata: apikey.metadata })
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS,
WEBDAV_API_KEY_RATE_LIMIT_WINDOW_MS,
WEBDAV_RATE_LIMITER_BINDING,
} from '../../../shared/api-key-templates.js'
import { authedHeaders, createTestApp } from '../../test/setup.js'
import { ApiKeyRateLimitError } from '../../usecases/ports'
@@ -108,6 +109,26 @@ describe('API keys', () => {
expect(listed.apiKeys.every((key) => key.referenceId === userId)).toBe(true)
})
it('defers Better Auth WebDAV key bookkeeping when the native limiter is authoritative', async () => {
const backgroundTasks: Promise<unknown>[] = []
const { app, db, auth } = await createTestApp({}, { [WEBDAV_RATE_LIMITER_BINDING]: {} }, (task) =>
backgroundTasks.push(task),
)
await authedHeaders(app)
const { userId } = await getUserAndOrg(db)
// biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API is not fully typed
const webdav = (await (auth.api as any).createApiKey({
body: { configId: 'webdav', userId },
})) as { key: string }
await Promise.all(backgroundTasks.splice(0))
await expect(
apiKeys.verifyApiKeyForPermission(auth, db, webdav.key, 'webdav', 'read', 'webdav'),
).resolves.toMatchObject({ referenceId: userId })
expect(backgroundTasks.length).toBeGreaterThan(0)
await Promise.all(backgroundTasks)
})
it('persists the configured defaults for each API key template', async () => {
const { app, db, auth } = await createTestApp()
await authedHeaders(app)
@@ -159,7 +180,7 @@ describe('API keys', () => {
})
})
it('normalizes a legacy organization-owned key to the current owner', async () => {
it('rejects legacy API keys instead of upgrading them during verification', async () => {
const { app, db, auth } = await createTestApp()
const headers = await authedHeaders(app)
const { orgId, userId } = await getUserAndOrg(db)
@@ -176,32 +197,18 @@ describe('API keys', () => {
`)
await db.run(sql`UPDATE apikey SET metadata = '{}' WHERE id = ${webdav.id}`)
await expect(apiKeys.verifyApiKey(auth, db, remoteDownload.key, 'remote-download')).resolves.toMatchObject({
referenceId: userId,
scope: { mode: 'workspace', orgId },
})
await expect(apiKeys.verifyApiKey(auth, db, remoteDownload.key, 'remote-download')).resolves.toBeNull()
const row = await getApiKeyRow(db, remoteDownload.id)
expect(row.reference_id).toBe(userId)
expect(JSON.parse(row.metadata as string)).toEqual({ scope: { mode: 'workspace', orgId } })
expect(row.reference_id).toBe(orgId)
expect(JSON.parse(row.metadata as string)).toEqual({})
const listResponse = await app.request('/api/auth/api-key/list', { headers })
expect(listResponse.status).toBe(200)
const listed = (await listResponse.json()) as {
apiKeys: Array<{ id: string; metadata: { scope: { mode: string; orgId?: string } } }>
}
expect(listed.apiKeys).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: ihost.id,
metadata: { scope: { mode: 'workspace', orgId } },
}),
expect.objectContaining({
id: webdav.id,
metadata: { scope: { mode: 'user-workspaces' } },
}),
]),
)
expect(await getApiKeyRow(db, ihost.id)).toMatchObject({ reference_id: userId })
expect(listed.apiKeys).toEqual([expect.objectContaining({ id: webdav.id, metadata: {} })])
expect(await getApiKeyRow(db, ihost.id)).toMatchObject({ reference_id: orgId })
})
it('deletes workspace-scoped keys with their organization but preserves WebDAV keys', async () => {
+8 -11
View File
@@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm'
import { apikey } from '../../db/auth-schema'
import type { Database } from '../../platform/interface'
import { type ApiKeyAuth, type ApiKeyGateway, ApiKeyRateLimitError, type VerifiedApiKey } from '../../usecases/ports'
import { normalizeLegacyApiKey } from './api-key-scopes'
import { scopeForApiKey } from './api-key-scopes'
type VerifyApiKeyResult = {
valid: boolean
@@ -25,7 +25,7 @@ export function createApiKeyGateway(): ApiKeyGateway {
if (!resolvedConfigId) return null
const result = await verify(auth, { configId: resolvedConfigId, key })
throwIfRateLimited(result)
if (result?.valid && result.key) return normalizeVerifiedApiKey(db, result.key)
if (result?.valid && result.key) return normalizeVerifiedApiKey(result.key)
return null
},
@@ -38,7 +38,7 @@ export function createApiKeyGateway(): ApiKeyGateway {
permissions: { [resource]: [action] },
})
throwIfRateLimited(result)
if (result?.valid && result.key) return normalizeVerifiedApiKey(db, result.key)
if (result?.valid && result.key) return normalizeVerifiedApiKey(result.key)
return null
},
@@ -48,17 +48,14 @@ export function createApiKeyGateway(): ApiKeyGateway {
}
}
async function normalizeVerifiedApiKey(
db: Database,
key: NonNullable<VerifyApiKeyResult['key']>,
): Promise<VerifiedApiKey | null> {
const normalized = await normalizeLegacyApiKey(db, key)
if (!normalized) return null
async function normalizeVerifiedApiKey(key: NonNullable<VerifyApiKeyResult['key']>): Promise<VerifiedApiKey | null> {
const scope = scopeForApiKey(key.configId, key.metadata)
if (!scope) return null
return {
id: key.id,
configId: key.configId,
referenceId: normalized.referenceId,
scope: normalized.scope,
referenceId: key.referenceId,
scope,
permissions: key.permissions,
}
}
+8 -7
View File
@@ -38,7 +38,7 @@ import {
} from '../shared/oauth-providers'
import { generateUserOrgSlug, isPersonalOrgLike } from '../shared/org-slugs'
import { createEmailGateway } from './adapters/gateways/email'
import { deleteApiKeysScopedToOrganization, normalizeLegacyApiKeysForUser } from './adapters/repos/api-key-scopes'
import { deleteApiKeysScopedToOrganization } from './adapters/repos/api-key-scopes'
import { createAuditRepo } from './adapters/repos/audit'
import { createInviteRepo } from './adapters/repos/invite'
import { createLicenseBindingRepo } from './adapters/repos/license-binding'
@@ -311,6 +311,7 @@ export async function createAuth(
secret: string,
baseURL?: string,
trustedOrigins?: string[],
backgroundTaskHandler?: (promise: Promise<unknown>) => void,
) {
const isPlatform = 'db' in initialSource
const rawPlatform = isPlatform ? initialSource : null
@@ -347,6 +348,7 @@ export async function createAuth(
// better-auth silently disables it under NODE_ENV=test, so tests would
// never exercise the real CSRF/origin behavior.
disableOriginCheck: false,
...(backgroundTaskHandler ? { backgroundTasks: { handler: backgroundTaskHandler } } : {}),
},
user: {
additionalFields: {
@@ -382,7 +384,7 @@ export async function createAuth(
session: {
cookieCache: {
enabled: true,
maxAge: 60 * 5,
maxAge: 60,
},
},
socialProviders: Object.fromEntries(
@@ -393,11 +395,6 @@ export async function createAuth(
if (ctx.path === '/delete-user') {
throw new APIError('FORBIDDEN', { message: 'Self-service account deletion is not available' })
}
if (ctx.path === '/api-key/list') {
const session = await getSessionFromCtx(ctx)
if (session?.user.id) await normalizeLegacyApiKeysForUser(db, session.user.id)
return
}
if (ctx.path !== '/api-key/create') return
const body = ctx.body as Record<string, unknown> | undefined
@@ -568,6 +565,10 @@ export async function createAuth(
configId: ApiKeyTemplate.WEBDAV,
references: 'user',
enableMetadata: true,
// Cloudflare's native limiter remains the authoritative synchronous
// rate limit. Better Auth can therefore move its bookkeeping write
// off the response path without weakening enforcement.
deferUpdates: usesNativeWebDavRateLimit && backgroundTaskHandler !== undefined,
rateLimit: {
enabled: !usesNativeWebDavRateLimit,
// Filesystem clients such as macOS Finder issue bursts of PROPFIND
+13 -5
View File
@@ -42,8 +42,8 @@ describe('better-auth admin user endpoints (migration target)', () => {
expect(res.status).toBe(403)
})
it('POST /api/auth/admin/ban-user sets banned, and our middleware then rejects the user [spec: users/disable]', async () => {
const { app, db } = await createTestApp()
it('POST /api/auth/admin/ban-user sets banned and revokes the Better Auth session [spec: users/disable] [spec: users/disabled-session-rejected]', async () => {
const { app, db, auth } = await createTestApp()
const headers = await adminCookie(app)
const memberHeaders = await authedHeaders(app, 'ban-me@example.com', 'password123456')
const rows = await db.all<{ id: string }>(sql`SELECT id FROM user WHERE email = 'ban-me@example.com'`)
@@ -68,9 +68,17 @@ describe('better-auth admin user endpoints (migration target)', () => {
)
expect(disableEvt).toEqual([{ user_id: adminId, target_id: userId }])
// The banned user's existing session is rejected by our auth middleware.
const blocked = await app.request('/api/quotas/me', { headers: memberHeaders })
expect(blocked.status).toBe(403)
// Better Auth's signed cookie cache has a bounded revocation window, so
// ordinary routes can still use this one-minute cached session.
const cached = await app.request('/api/quotas/me', { headers: memberHeaders })
expect(cached.status).toBe(200)
// Bypassing the cookie cache proves that Better Auth deleted the session.
const revoked = await auth.api.getSession({
headers: new Headers(memberHeaders),
query: { disableCookieCache: true },
})
expect(revoked).toBeNull()
// Unban restores access and is audited as user_enable.
const unban = await app.request('/api/auth/admin/unban-user', {
+2 -5
View File
@@ -26,9 +26,6 @@ export function escapeLike(s: string): string {
return s.replace(/[\\%_]/g, '\\$&')
}
export async function readUserId(c: Context<Env>): Promise<string | null> {
const session = (await c.get('auth').api.getSession({ headers: c.req.raw.headers })) as {
user: { id: string }
} | null
return session?.user?.id ?? null
export function readUserId(c: Context<Env>): string | null {
return c.get('userId')
}
-14
View File
@@ -82,20 +82,6 @@ describe('User entitlements API (admin)', () => {
expect(res.status).toBe(404)
})
it('auth middleware rejects a banned user on an existing session [spec: users/disabled-session-rejected]', async () => {
const { app, db } = await createTestApp()
// Capture a live session, then ban the user. Admin ban/unban is served by
// better-auth's /admin/ban-user; here we only assert our middleware enforces it.
const userHeaders = await authedHeaders(app, 'banned@example.com')
await db.run(sql`UPDATE user SET banned = 1 WHERE email = 'banned@example.com'`)
const res = await app.request('/api/quotas/me', { headers: userHeaders })
expect(res.status).toBe(403)
const body = (await res.json()) as { error: { message: string } }
expect(body.error.message).toBe('Account disabled')
})
it('POST /api/users/:id/entitlements grants storage entitlement for a personal org [spec: users/grant-entitlement]', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
+10 -55
View File
@@ -26,7 +26,6 @@ import { isDownloadFailureStatus, transferAuditActor, transferFailureReason } fr
import type { Env } from '../middleware/platform'
import {
ApiKeyRateLimitError,
type CachePolicy,
insufficientCredits,
type RecordAuditEventInput,
type StorageRecord,
@@ -80,41 +79,10 @@ type DavAuth = {
permissions: Record<string, string[]> | null
}
type VerifiedWebDavAuth = Extract<Awaited<ReturnType<typeof resolveWebDavAuth>>, { ok: true }>
interface NativeRateLimiter {
limit(options: { key: string }): Promise<{ success: boolean }>
}
// The native limiter still counts every request. This only collapses repeated D1
// verification inside a Finder burst; memory-only storage bounds revocation lag
// to one second and keeps credential-derived keys out of KV.
const WEBDAV_AUTH_CACHE_POLICY: CachePolicy<VerifiedWebDavAuth> = {
namespace: 'webdav-auth',
version: 1,
ttlMs: 1_000,
maxEntries: 256,
distributed: false,
validate(value): value is VerifiedWebDavAuth {
if (typeof value !== 'object' || value === null) return false
const auth = value as Partial<VerifiedWebDavAuth>
return (
auth.ok === true &&
typeof auth.userId === 'string' &&
typeof auth.keyId === 'string' &&
typeof auth.configId === 'string' &&
(auth.permissions === null ||
(typeof auth.permissions === 'object' && auth.permissions !== null && !Array.isArray(auth.permissions)))
)
},
}
class WebDavAuthRejected extends Error {
constructor(readonly outcome: Exclude<Awaited<ReturnType<typeof resolveWebDavAuth>>, { ok: true }>) {
super('WebDAV authentication rejected')
}
}
const cloudBaseUrl = (c: DavContext): string => c.get('platform').getEnv('ZPAN_CLOUD_URL') ?? ZPAN_CLOUD_URL_DEFAULT
async function requireWebDavApiKey(c: DavContext): Promise<DavAuth | Response> {
@@ -135,29 +103,16 @@ async function requireWebDavApiKey(c: DavContext): Promise<DavAuth | Response> {
}
const startedAt = performance.now()
const loadAuth = async () => {
const result = await resolveWebDavAuth(c.get('deps'), {
auth: c.get('auth'),
db: c.get('platform').db,
username: credentials.username,
password: credentials.password,
resource: WEBDAV_RESOURCE,
action,
configId: ApiKeyTemplate.WEBDAV,
})
if (!result.ok) throw new WebDavAuthRejected(result)
return result
}
let result: Awaited<ReturnType<typeof resolveWebDavAuth>>
try {
result =
nativeRateLimiter && credentialKey
? (await c.get('deps').cache.getOrLoad(WEBDAV_AUTH_CACHE_POLICY, `${credentialKey}:${action}`, loadAuth)).value
: await loadAuth()
} catch (error) {
if (!(error instanceof WebDavAuthRejected)) throw error
result = error.outcome
}
const result = await resolveWebDavAuth(c.get('deps'), {
auth: c.get('auth'),
db: c.get('platform').db,
username: credentials.username,
password: credentials.password,
resource: WEBDAV_RESOURCE,
action,
configId: ApiKeyTemplate.WEBDAV,
cacheKey: nativeRateLimiter && credentialKey ? `${credentialKey}:${action}` : undefined,
})
c.get('webDavTrace').push(`auth:${Math.round(performance.now() - startedAt)}`)
if (!result.ok) {
if (result.reason === 'rate_limited')
+6 -10
View File
@@ -77,12 +77,6 @@ export const authMiddleware = createMiddleware<Env>(async (c, next) => {
const auth = c.get('auth')
const result = (await auth.api.getSession({ headers: c.req.raw.headers })) as SessionWithPlugins | null
if (result?.user?.id) {
if (await c.get('deps').userAdmin.isBanned(result.user.id)) {
throw forbidden('Account disabled')
}
}
c.set('userId', result?.user?.id ?? null)
c.set('userRole', result?.user?.role ?? null)
@@ -118,12 +112,14 @@ export const requireAuth = createMiddleware<Env>(async (c, next) => {
})
export const requireAdmin = createMiddleware<Env>(async (c, next) => {
const userId = c.get('userId')
if (!userId) {
const result = (await c.get('auth').api.getSession({
headers: c.req.raw.headers,
query: { disableCookieCache: true },
})) as SessionWithPlugins | null
if (!result?.user?.id) {
throw unauthorized('Unauthorized')
}
const userRole = c.get('userRole')
if (userRole !== 'admin') {
if (result.user.role !== 'admin') {
throw forbidden('Forbidden')
}
await next()
+2 -1
View File
@@ -628,6 +628,7 @@ const APP_SCHEMA_SQL = `
export async function createTestApp(
envOverrides: Record<string, string> = {},
bindingOverrides: Record<string, unknown> = {},
backgroundTaskHandler?: (promise: Promise<unknown>) => void,
) {
// Each test app is a brand-new site; drop origin state cached by a previous
// app in the same test file.
@@ -643,7 +644,7 @@ export async function createTestApp(
getEnv: (key: string) => envOverrides[key],
getBinding: <T = unknown>(key: string) => bindingOverrides[key] as T | undefined,
}
const auth = await createAuth(platform, 'test-secret', 'http://localhost:3000')
const auth = await createAuth(platform, 'test-secret', 'http://localhost:3000', undefined, backgroundTaskHandler)
const deps = createDeps(platform)
const app = createApp(platform, auth, deps)
+7
View File
@@ -4,6 +4,7 @@ import type { Database } from '../platform/interface'
import type {
ApiKeyAuth,
ApiKeyGateway,
CacheService,
CloudTrafficReportRepo,
DavLock,
DownloadTaskRecord,
@@ -125,6 +126,12 @@ function makeDeps(
verifyApiKeyForPermission: async () => ({ id: 'k1', configId: 'webdav', referenceId: 'u1', permissions: null }),
...overrides.apiKeys,
} as unknown as ApiKeyGateway,
cache: {
mode: 'off',
getOrLoad: async (_policy, _key, loader) => ({ value: await loader(), tier: 'bypass' }),
replace: async () => {},
invalidate: async () => {},
} as CacheService,
userAdmin: {
isBanned: async () => false,
matchesActiveUsername: async () => true,
+59
View File
@@ -21,6 +21,7 @@ import { assertFolderNotUsedByDownload } from './downloads/download-folders'
import {
type ApiKeyAuth,
ApiKeyRateLimitError,
type CachePolicy,
type DavDeadProperty,
type DavLock,
type DeadPropertyUpdate,
@@ -48,7 +49,65 @@ export type WebDavAuthOutcome =
| { ok: false; reason: 'unauthorized' }
| { ok: false; reason: 'rate_limited'; retryAfterMs?: number; message: string }
type VerifiedWebDavAuth = Extract<WebDavAuthOutcome, { ok: true }>
const WEBDAV_AUTH_CACHE_POLICY: CachePolicy<VerifiedWebDavAuth> = {
namespace: 'webdav-auth',
version: 1,
ttlMs: 1_000,
maxEntries: 256,
distributed: false,
validate(value): value is VerifiedWebDavAuth {
if (typeof value !== 'object' || value === null) return false
const auth = value as Partial<VerifiedWebDavAuth>
return (
auth.ok === true &&
typeof auth.userId === 'string' &&
typeof auth.keyId === 'string' &&
typeof auth.configId === 'string' &&
(auth.permissions === null ||
(typeof auth.permissions === 'object' && auth.permissions !== null && !Array.isArray(auth.permissions)))
)
},
}
class WebDavAuthRejected extends Error {
constructor(readonly outcome: Exclude<WebDavAuthOutcome, { ok: true }>) {
super('WebDAV authentication rejected')
}
}
export async function resolveWebDavAuth(
deps: Pick<Deps, 'apiKeys' | 'cache' | 'userAdmin'>,
params: {
auth: ApiKeyAuth
db: Database
username: string
password: string
resource: string
action: 'read' | 'write'
configId: string
cacheKey?: string
},
): Promise<WebDavAuthOutcome> {
const load = () => verifyWebDavAuth(deps, params)
if (!params.cacheKey) return load()
try {
return (
await deps.cache.getOrLoad(WEBDAV_AUTH_CACHE_POLICY, params.cacheKey, async () => {
const result = await load()
if (!result.ok) throw new WebDavAuthRejected(result)
return result
})
).value
} catch (error) {
if (error instanceof WebDavAuthRejected) return error.outcome
throw error
}
}
async function verifyWebDavAuth(
deps: Pick<Deps, 'apiKeys' | 'userAdmin'>,
params: {
auth: ApiKeyAuth
+5 -5
View File
@@ -1,8 +1,8 @@
Feature: User administration
Admin user management runs through better-auth's admin plugin
(/api/auth/admin/*). ZPan owns the per-user storage quota and the entitlement
grants against each user's personal org, and enforces bans in its auth
middleware.
grants against each user's personal org. Better Auth owns ban enforcement and
session revocation.
@users/admin-only @api
Scenario: Non-admins cannot administer users
@@ -35,10 +35,10 @@ Feature: User administration
Then the API responds 404
@users/disabled-session-rejected @api
Scenario: A disabled user's existing session is rejected
Scenario: A disabled user's server session is revoked
Given a user disabled mid-session
When they make an authenticated request
Then the auth middleware rejects it
When authentication bypasses the bounded signed-cookie cache
Then Better Auth rejects the revoked session
@users/delete @api
Scenario: Admins delete a user
+2 -1
View File
@@ -1,3 +1,4 @@
import { waitUntil } from 'cloudflare:workers'
import { createCloudflareKvBackend } from '../server/adapters/cache/cloudflare-kv'
import { createRuntimeCache, resolveCacheMode } from '../server/adapters/cache/runtime-cache'
import { createArchiveJobsGateway } from '../server/adapters/gateways/archive-jobs'
@@ -90,7 +91,7 @@ async function appForRequest(
const cachedAuth = runtime.authBySlot.get(slot)
if (cachedApp && cachedAuth) return cachedApp
const auth = await createAuth(runtime.platform, env.BETTER_AUTH_SECRET, baseURL, trustedOrigins)
const auth = await createAuth(runtime.platform, env.BETTER_AUTH_SECRET, baseURL, trustedOrigins, waitUntil)
const app = createApp(runtime.platform, auth, runtime.deps)
runtime.authBySlot.set(slot, auth)
runtime.appBySlot.set(slot, app)