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' import { createShareRepo } from '../server/adapters/repos/share' import { createApp } from '../server/app' import type { Auth } from '../server/auth' import { createAuth } from '../server/auth' import { createDeps } from '../server/composition' import { isPotentialWebDavPublicRequest } from '../server/domain/webdav-public-url' import { isHandledError, standaloneJsonError } from '../server/middleware/error-handler' import { handleImageHostingDomainRequest } from '../server/middleware/image-hosting-domain' import { createCloudflarePlatform } from '../server/platform/cloudflare' import { platformContext } from '../server/platform/context' import type { Deps } from '../server/usecases/deps' import type { ArchiveJobMessage, CacheService } from '../server/usecases/ports' import { DirType } from '../shared/constants' import { handleScheduled } from './scheduled' interface Env { DB: D1Database BETTER_AUTH_SECRET: string BETTER_AUTH_URL?: string TRUSTED_ORIGINS?: string ASSETS: Fetcher CACHE_KV?: KVNamespace [key: string]: unknown } const SHARE_TOKEN_RE = /^\/s\/([^/?#]+)/ // Cache auth instances at isolate scope to avoid per-request DB queries and // better-auth init CPU. createAuth resolves $context before returning, so the // cache never carries a pending promise tied to its creating request (which // would hang every later auth call in the isolate). Fixed slots prevent a first // request on the WebDAV hostname from fixing the primary app base URL without // allowing arbitrary Host headers to grow the cache. Changes to OAuth provider // configs or env vars take effect on isolate recycle. type AuthSlot = 'configured' | 'primary' | 'webdav' interface WorkerRuntime { platform: ReturnType deps: Deps cache: CacheService authBySlot: Map appBySlot: Map> appInitBySlot: Map>> } let cachedRuntime: WorkerRuntime | undefined const responseCache = ( caches as unknown as { default: { match(request: Request): Promise put(request: Request, response: Response): Promise } } ).default function runtimeFor(env: Env): WorkerRuntime { if (cachedRuntime) return cachedRuntime const platform = createCloudflarePlatform(env) const distributed = env.CACHE_KV ? createCloudflareKvBackend(env.CACHE_KV) : undefined const cache = createRuntimeCache({ mode: resolveCacheMode(env.ZPAN_CACHE_MODE as string | undefined, !!distributed), distributed, }) cachedRuntime = { platform, cache, deps: createDeps(platform, { cache }), authBySlot: new Map(), appBySlot: new Map(), appInitBySlot: new Map(), } return cachedRuntime } async function appForRequest( runtime: WorkerRuntime, request: Request, env: Env, ): Promise> { const origin = new URL(request.url).origin const webDavRequest = isPotentialWebDavPublicRequest(request.url) const inferredOrigin = origin const baseURL = env.BETTER_AUTH_URL || inferredOrigin const trustedOrigins = env.TRUSTED_ORIGINS?.split(',') .map((value) => value.trim()) .filter(Boolean) || [inferredOrigin] const slot: AuthSlot = env.BETTER_AUTH_URL ? 'configured' : webDavRequest ? 'webdav' : 'primary' const cachedApp = runtime.appBySlot.get(slot) const cachedAuth = runtime.authBySlot.get(slot) if (cachedApp && cachedAuth) return cachedApp const pendingApp = runtime.appInitBySlot.get(slot) if (pendingApp) return pendingApp const appPromise = createAuth(runtime.platform, env.BETTER_AUTH_SECRET, baseURL, trustedOrigins, waitUntil).then( (auth) => { const app = createApp(runtime.platform, auth, runtime.deps) runtime.authBySlot.set(slot, auth) runtime.appBySlot.set(slot, app) return app }, ) runtime.appInitBySlot.set(slot, appPromise) try { return await appPromise } finally { runtime.appInitBySlot.delete(slot) } } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const { BETTER_AUTH_SECRET } = env if (!BETTER_AUTH_SECRET) { throw new Error('BETTER_AUTH_SECRET is not configured for this deployment.') } const runtime = runtimeFor(env) const imageDomainResponse = await handleImageDomainBeforeAuth(request, env, runtime) if (imageDomainResponse) return imageDomainResponse const edgeCached = await matchConfigzResponseCache(request, runtime.cache) if (edgeCached) return edgeCached const app = await appForRequest(runtime, request, env) return platformContext.run(runtime.platform, async () => { const url = new URL(request.url) const shareMatch = SHARE_TOKEN_RE.exec(url.pathname) if (shareMatch && request.method === 'GET') { return handleShareSsr(request, env, ctx, shareMatch[1], runtime.platform, app) } const response = await app.fetch(request, env, ctx) cacheConfigzResponse(request, response, runtime.cache, ctx) return response }) }, async scheduled(event: ScheduledEvent, env: Env): Promise { await handleScheduled(event, env) }, async queue(batch: MessageBatch, env: Env): Promise { const platform = createCloudflarePlatform(env) const archiveJobs = createArchiveJobsGateway(platform) for (const message of batch.messages) { await archiveJobs.runMessage(message.body) message.ack() } }, } async function handleImageDomainBeforeAuth( request: Request, env: Env, runtime: WorkerRuntime, ): Promise { if (!isImageDomainFastPathRequest(request, env)) return null try { return await platformContext.run(runtime.platform, () => handleImageHostingDomainRequest({ request, deps: runtime.deps, platform: runtime.platform, appHosts: [new URL(env.BETTER_AUTH_URL!).hostname.toLowerCase(), 'workers.dev'], webDavMountPath: '/dav', }), ) } catch (error) { if (!isHandledError(error)) console.error(`image_domain.unhandled_error code=${String(error)}`) return standaloneJsonError(error) } } export function isImageDomainFastPathRequest(request: Request, env: Pick): boolean { if (!env.BETTER_AUTH_URL) return false const url = new URL(request.url) if (url.pathname !== '/ih' && !url.pathname.startsWith('/ih/')) return false const host = (request.headers.get('host') ?? url.host).replace(/:\d+$/, '').toLowerCase() const appHost = new URL(env.BETTER_AUTH_URL).hostname.toLowerCase() return host !== appHost && !host.endsWith('.workers.dev') } function configzCacheKey(request: Request, includeValidators = false): Request | null { if (request.method !== 'GET') return null const url = new URL(request.url) if (url.pathname !== '/api/configz' && url.pathname !== '/api/configz/') return null url.search = '' const ifNoneMatch = request.headers.get('If-None-Match') return new Request(url.toString(), { method: 'GET', headers: includeValidators && ifNoneMatch ? { 'If-None-Match': ifNoneMatch } : undefined, }) } async function matchConfigzResponseCache(request: Request, cache: CacheService): Promise { if (cache.mode !== 'distributed') return null const key = configzCacheKey(request, true) if (!key) return null try { const matched = await responseCache.match(key) if (!matched) return null const response = new Response(matched.body, matched) response.headers.set('X-ZPan-Cache', 'edge') return response } catch (error) { console.error( JSON.stringify({ message: 'cache.response.get.error', error: error instanceof Error ? error.message : String(error), }), ) return null } } function cacheConfigzResponse(request: Request, response: Response, cache: CacheService, ctx: ExecutionContext): void { if (cache.mode !== 'distributed' || response.status !== 200) return const key = configzCacheKey(request) if (!key) return const task = responseCache.put(key, response.clone()).catch((error: unknown) => { console.error( JSON.stringify({ message: 'cache.response.put.error', error: error instanceof Error ? error.message : String(error), }), ) }) ctx.waitUntil(task) } interface ShareMeta { title: string description: string imageUrl: string } async function fetchShareMeta( platform: ReturnType, origin: string, token: string, ): Promise { const fallback: ShareMeta = { title: 'Share unavailable', description: 'Shared via ZPan', imageUrl: `${origin}/logo-512.png`, } try { const resolved = await createShareRepo(platform.db).resolveByToken(token) if (resolved.status !== 'ok') return fallback if (resolved.share.kind !== 'landing') return fallback const { share, matter } = resolved const expiry = share.expiresAt ? ` · Expires ${new Date(share.expiresAt).toLocaleDateString()}` : '' const description = `Shared via ZPan${expiry}` const isImage = matter.type.startsWith('image/') && matter.dirtype === DirType.FILE return { title: matter.name, description, imageUrl: isImage ? `${origin}/api/share/${token}/download` : `${origin}/logo-512.png`, } } catch { return fallback } } function escapeAttr(s: string): string { return s.replace(/&/g, '&').replace(/"/g, '"') } function buildOgTags(meta: ShareMeta, pageUrl: string): string { return [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ].join('\n ') } async function handleShareSsr( request: Request, env: Env, ctx: ExecutionContext, token: string, platform: ReturnType, app: ReturnType, ): Promise { const url = new URL(request.url) const origin = url.origin const [meta, spaRes] = await Promise.all([ fetchShareMeta(platform, origin, token), env.ASSETS.fetch(new Request(`${origin}/index.html`, { headers: request.headers })), ]) if (!spaRes.ok) { return app.fetch(request, env, ctx) } const html = await spaRes.text() const ogTags = buildOgTags(meta, url.href) const injected = html.replace( 'ZPan', `${meta.title.replace(/</g, '<')} — ZPan\n ${ogTags}`, ) return new Response(injected, { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8', 'Cache-Control': 'no-store', }, }) }