request-context.ts36 lines · main
1import { AsyncLocalStorage } from 'node:async_hooks';
2
3/**
4 * Per-request context carried through async boundaries.
5 *
6 * Better Auth's `databaseHooks` (see services/auth-tenant-pool.ts) run deep
7 * inside the auth library and never receive the originating HTTP request, so
8 * they can't read the visitor IP on their own. The auth-tenant bridge in
9 * routes/auth-service.ts sets this context (IP + projectId) around the call
10 * to Better Auth's handler; the user.create hook then reads it back to
11 * capture the sign-up's raw IP + geo (control-plane SEO analytics).
12 *
13 * AsyncLocalStorage propagates the value to every promise/callback spawned
14 * within `runWithRequestContext`, so no plumbing through function signatures
15 * is required. Outside a run scope `getRequestContext()` returns undefined.
16 */
17export interface RequestContext {
18 /** Raw visitor IP (first value of cf-connecting-ip / x-forwarded-for). */
19 ip: string | null;
20 /** The tenant/project the request resolved to, when known. */
21 projectId?: string | null;
22 /** User-Agent header for device fingerprinting. */
23 userAgent?: string | null;
24}
25
26const storage = new AsyncLocalStorage<RequestContext>();
27
28/** Run `fn` with `ctx` readable via getRequestContext() for its whole async tree. */
29export function runWithRequestContext<T>(ctx: RequestContext, fn: () => T): T {
30 return storage.run(ctx, fn);
31}
32
33/** The active request context, or undefined when called outside a run scope. */
34export function getRequestContext(): RequestContext | undefined {
35 return storage.getStore();
36}