File size: 2,705 Bytes
46252cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * Per-request async context, propagated to every log line and audit record produced while handling a
 * request. Today it carries the X-Request-ID plus the resolved API key and client IP, so audit log
 * rows are stamped with who made the call and from where WITHOUT every controller having to thread
 * `@CurrentApiKey()` + `req.ip` into each `auditService.log*()` call (most call sites legitimately
 * don't know the key — they fire from deep inside services). Backed by a dedicated AsyncLocalStorage
 * instance — independent of the existing plugin/hook ALS instances (multiple ALS instances coexist fine).
 */
import { AsyncLocalStorage } from 'node:async_hooks';

export interface RequestContext {
  requestId: string;
  /** The API key that authenticated this request, if any (@Public routes have none). */
  apiKeyId?: string;
  apiKeyName?: string;
  /** The real client IP (ProxyAwareThrottlerGuard's notion, honoring TRUSTED_PROXIES). */
  ipAddress?: string;
}

const requestContextStorage = new AsyncLocalStorage<RequestContext>();

/** Run `fn` (and every async continuation it starts) with `requestId` as the active context. */
export function runWithRequestId<T>(requestId: string, fn: () => T): T {
  return requestContextStorage.run({ requestId }, fn);
}

/** The active request id, or `undefined` when not running inside a request scope. */
export function getRequestId(): string | undefined {
  return requestContextStorage.getStore()?.requestId;
}

/**
 * Stamp the resolved API key + client IP into the active request context so downstream audit log
 * writes (which typically happen inside services without DI access to the key) can attribute the
 * action. No-op outside a request scope (e.g. worker/cron), so callers don't need to guard. The
 * guard/middleware call this once they've resolved the key; services never need to.
 */
export function setRequestActor(actor: { apiKeyId?: string; apiKeyName?: string; ipAddress?: string }): void {
  const store = requestContextStorage.getStore();
  if (!store) return; // not in a request scope — nothing to stamp
  if (actor.apiKeyId !== undefined) store.apiKeyId = actor.apiKeyId;
  if (actor.apiKeyName !== undefined) store.apiKeyName = actor.apiKeyName;
  if (actor.ipAddress !== undefined) store.ipAddress = actor.ipAddress;
}

/** The active request's resolved actor (API key + IP), or `undefined` outside a request scope. */
export function getRequestActor(): { apiKeyId?: string; apiKeyName?: string; ipAddress?: string } | undefined {
  const store = requestContextStorage.getStore();
  if (!store) return undefined;
  return { apiKeyId: store.apiKeyId, apiKeyName: store.apiKeyName, ipAddress: store.ipAddress };
}