request-context.ts36 lines · main
| 1 | import { 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 | */ |
| 17 | export 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 | |
| 26 | const storage = new AsyncLocalStorage<RequestContext>(); |
| 27 | |
| 28 | /** Run `fn` with `ctx` readable via getRequestContext() for its whole async tree. */ |
| 29 | export 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. */ |
| 34 | export function getRequestContext(): RequestContext | undefined { |
| 35 | return storage.getStore(); |
| 36 | } |