@usebetterdev/audit-next 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +6 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -37,27 +37,11 @@ var import_headers = require("next/headers");
|
|
|
37
37
|
var import_server = require("next/server");
|
|
38
38
|
var import_audit_core2 = require("@usebetterdev/audit-core");
|
|
39
39
|
var AUDIT_ACTOR_HEADER = "x-better-audit-actor-id";
|
|
40
|
-
var defaultExtractor = {
|
|
41
|
-
actor: (0, import_audit_core.fromBearerToken)("sub")
|
|
42
|
-
};
|
|
43
|
-
async function tryExtract(extractor, request, onError) {
|
|
44
|
-
if (!extractor) {
|
|
45
|
-
return void 0;
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
return await extractor(request);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (onError) {
|
|
51
|
-
onError(error);
|
|
52
|
-
}
|
|
53
|
-
return void 0;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
40
|
function createAuditMiddleware(options = {}) {
|
|
57
|
-
const extractor = options.extractor ?? defaultExtractor;
|
|
41
|
+
const extractor = options.extractor ?? import_audit_core.defaultExtractor;
|
|
58
42
|
const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;
|
|
59
43
|
return async (request) => {
|
|
60
|
-
const actorId = await
|
|
44
|
+
const actorId = await (0, import_audit_core.safeExtract)(
|
|
61
45
|
extractor.actor,
|
|
62
46
|
request,
|
|
63
47
|
options.onError
|
|
@@ -73,9 +57,9 @@ function betterAuditNext(options = {}) {
|
|
|
73
57
|
return createAuditMiddleware(options);
|
|
74
58
|
}
|
|
75
59
|
function withAuditRoute(handler, options = {}) {
|
|
76
|
-
const extractor = options.extractor ?? defaultExtractor;
|
|
60
|
+
const extractor = options.extractor ?? import_audit_core.defaultExtractor;
|
|
77
61
|
return async (request, context) => {
|
|
78
|
-
const actorId = await
|
|
62
|
+
const actorId = await (0, import_audit_core.safeExtract)(
|
|
79
63
|
extractor.actor,
|
|
80
64
|
request,
|
|
81
65
|
options.onError
|
|
@@ -88,7 +72,7 @@ function withAuditRoute(handler, options = {}) {
|
|
|
88
72
|
};
|
|
89
73
|
}
|
|
90
74
|
function withAudit(action, options = {}) {
|
|
91
|
-
const extractor = options.extractor ?? defaultExtractor;
|
|
75
|
+
const extractor = options.extractor ?? import_audit_core.defaultExtractor;
|
|
92
76
|
return async (...args) => {
|
|
93
77
|
let actorId;
|
|
94
78
|
try {
|
|
@@ -96,7 +80,7 @@ function withAudit(action, options = {}) {
|
|
|
96
80
|
const syntheticRequest = new Request("http://localhost", {
|
|
97
81
|
headers: headersList
|
|
98
82
|
});
|
|
99
|
-
actorId = await
|
|
83
|
+
actorId = await (0, import_audit_core.safeExtract)(
|
|
100
84
|
extractor.actor,
|
|
101
85
|
syntheticRequest,
|
|
102
86
|
options.onError
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n fromBearerToken,\n getAuditContext,\n runWithAuditContext,\n} from \"@usebetterdev/audit-core\";\nimport type {\n AuditContext,\n ContextExtractor,\n ValueExtractor,\n} from \"@usebetterdev/audit-core\";\nimport { headers } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type RouteContext = Record<string, unknown> | undefined;\n\nexport type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;\n\n/**\n * Shared options across all audit wrappers.\n */\nexport interface BaseAuditOptions {\n /** Context extractor for pulling actor identity from the request. */\n extractor?: ContextExtractor;\n /** Called when an extractor throws. Defaults to silent fail-open. */\n onError?: (error: unknown) => void;\n}\n\nexport interface CreateAuditMiddlewareOptions extends BaseAuditOptions {\n /**\n * Request header name used to forward the extracted actor id to downstream\n * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.\n */\n actorHeader?: string;\n}\n\nexport type WithAuditRouteOptions = BaseAuditOptions;\n\nexport type WithAuditOptions = BaseAuditOptions;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default header name used to forward the actor id from middleware to route\n * handlers.\n *\n * Use this constant to keep the middleware and route handler in sync:\n * ```ts\n * // middleware.ts\n * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER\n *\n * // app/api/orders/route.ts\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport const AUDIT_ACTOR_HEADER = \"x-better-audit-actor-id\";\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/** Default extractor: reads `sub` from `Authorization: Bearer <jwt>` as actor. */\nconst defaultExtractor: ContextExtractor = {\n actor: fromBearerToken(\"sub\"),\n};\n\n/**\n * Runs a value extractor safely, returning undefined on error (fail open).\n */\nasync function tryExtract(\n extractor: ValueExtractor | undefined,\n request: Request,\n onError: ((error: unknown) => void) | undefined,\n): Promise<string | undefined> {\n if (!extractor) {\n return undefined;\n }\n try {\n return await extractor(request);\n } catch (error: unknown) {\n if (onError) {\n onError(error);\n }\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// createAuditMiddleware / betterAuditNext\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Next.js edge middleware function that extracts audit context from\n * the incoming request and forwards the actor id as a request header to\n * downstream route handlers.\n *\n * **Important:** Next.js middleware runs in a separate Edge Runtime execution\n * context from route handlers — AsyncLocalStorage set here does NOT propagate.\n * This middleware forwards the extracted actor id via a request header\n * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route\n * handlers to read that header and populate ALS.\n *\n * The middleware **always** overwrites the actor header on the forwarded\n * request — when extraction fails, it is set to an empty string. This prevents\n * clients from spoofing the actor id by sending the header directly.\n *\n * The middleware is non-blocking: if context extraction fails, the request\n * proceeds with an empty actor header (fail open).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAuditMiddleware } from \"@usebetterdev/audit-next\";\n * export default createAuditMiddleware();\n * export const config = { matcher: \"/api/:path*\" };\n *\n * // app/api/orders/route.ts\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function createAuditMiddleware(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n const extractor = options.extractor ?? defaultExtractor;\n const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;\n\n return async (request) => {\n const actorId = await tryExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n // Always forward with an explicit header value so clients cannot spoof\n // the actor id by sending the header directly. When extraction fails the\n // header is set to \"\" — fromHeader() treats empty values as undefined.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(actorHeader, actorId ?? \"\");\n\n return NextResponse.next({\n request: { headers: requestHeaders },\n });\n };\n}\n\n/**\n * Convenience alias for `createAuditMiddleware`. Use as the default export\n * in `middleware.ts`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { betterAuditNext } from \"@usebetterdev/audit-next\";\n * export default betterAuditNext();\n * ```\n */\nexport function betterAuditNext(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n return createAuditMiddleware(options);\n}\n\n// ---------------------------------------------------------------------------\n// withAuditRoute\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js App Router route handler with audit context propagation.\n *\n * Extracts the actor identity from the incoming request (by default reads\n * `sub` from `Authorization: Bearer <jwt>`, or reads the\n * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and\n * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`\n * returns the correct context within the handler.\n *\n * `NextRequest` is a Web `Request` subclass — extractors from audit-core\n * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.\n *\n * The wrapper is non-blocking: if extraction fails, the handler runs without\n * audit context (fail open).\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts — standalone (no middleware)\n * import { withAuditRoute } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler);\n *\n * // app/api/orders/route.ts — paired with createAuditMiddleware\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function withAuditRoute<C extends RouteContext = RouteContext>(\n handler: (request: NextRequest, context: C) => Promise<Response>,\n options: WithAuditRouteOptions = {},\n): (request: NextRequest, context: C) => Promise<Response> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (request, context) => {\n const actorId = await tryExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n if (actorId === undefined) {\n return handler(request, context);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => handler(request, context));\n };\n}\n\n// ---------------------------------------------------------------------------\n// withAudit (server actions)\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js server action with audit context propagation.\n *\n * Server actions do not receive a `Request` object. This wrapper reads all\n * request headers via `next/headers` and constructs a synthetic `Request` to\n * run the configured extractor against, then wraps the action in an\n * AsyncLocalStorage scope so that `getAuditContext()` returns the correct\n * context within the action.\n *\n * The wrapper is non-blocking: if extraction fails (including when `headers()`\n * is unavailable), the action runs without audit context (fail open).\n *\n * @example\n * ```ts\n * // app/actions.ts\n * \"use server\";\n * import { withAudit } from \"@usebetterdev/audit-next\";\n *\n * export const createOrder = withAudit(async (formData: FormData) => {\n * // getAuditContext() returns the actor here\n * });\n * ```\n */\nexport function withAudit<TArgs extends unknown[], TResult>(\n action: (...args: TArgs) => Promise<TResult>,\n options: WithAuditOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (...args) => {\n let actorId: string | undefined;\n\n try {\n const headersList = await headers();\n const syntheticRequest = new Request(\"http://localhost\", {\n headers: headersList,\n });\n actorId = await tryExtract(\n extractor.actor,\n syntheticRequest,\n options.onError,\n );\n } catch (error: unknown) {\n // headers() throws outside Next.js request context — fail open\n options.onError?.(error);\n }\n\n if (actorId === undefined) {\n return action(...args);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => action(...args));\n };\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { AuditContext, ContextExtractor, ValueExtractor };\nexport { fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from \"@usebetterdev/audit-core\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAIO;AAMP,qBAAwB;AACxB,oBAA6B;AA2R7B,IAAAA,qBAA8F;AAvOvF,IAAM,qBAAqB;AAOlC,IAAM,mBAAqC;AAAA,EACzC,WAAO,mCAAgB,KAAK;AAC9B;AAKA,eAAe,WACb,WACA,SACA,SAC6B;AAC7B,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AACA,MAAI;AACF,WAAO,MAAM,UAAU,OAAO;AAAA,EAChC,SAAS,OAAgB;AACvB,QAAI,SAAS;AACX,cAAQ,KAAK;AAAA,IACf;AACA,WAAO;AAAA,EACT;AACF;AAsCO,SAAS,sBACd,UAAwC,CAAC,GACzB;AAChB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,cAAc,QAAQ,eAAe;AAE3C,SAAO,OAAO,YAAY;AACxB,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAKA,UAAM,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;AAClD,mBAAe,IAAI,aAAa,WAAW,EAAE;AAE7C,WAAO,2BAAa,KAAK;AAAA,MACvB,SAAS,EAAE,SAAS,eAAe;AAAA,IACrC,CAAC;AAAA,EACH;AACF;AAaO,SAAS,gBACd,UAAwC,CAAC,GACzB;AAChB,SAAO,sBAAsB,OAAO;AACtC;AAkCO,SAAS,eACd,SACA,UAAiC,CAAC,GACuB;AACzD,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,OAAO,SAAS,YAAY;AACjC,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,QAAQ,SAAS,OAAO;AAAA,IACjC;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,eAAO,uCAAoB,cAAc,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC1E;AACF;AA6BO,SAAS,UACd,QACA,UAA4B,CAAC,GACS;AACtC,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,UAAU,SAAS;AACxB,QAAI;AAEJ,QAAI;AACF,YAAM,cAAc,UAAM,wBAAQ;AAClC,YAAM,mBAAmB,IAAI,QAAQ,oBAAoB;AAAA,QACvD,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAAA,QACd,UAAU;AAAA,QACV;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF,SAAS,OAAgB;AAEvB,cAAQ,UAAU,KAAK;AAAA,IACzB;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,GAAG,IAAI;AAAA,IACvB;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,eAAO,uCAAoB,cAAc,MAAM,OAAO,GAAG,IAAI,CAAC;AAAA,EAChE;AACF;","names":["import_audit_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n defaultExtractor,\n fromBearerToken,\n getAuditContext,\n runWithAuditContext,\n safeExtract,\n} from \"@usebetterdev/audit-core\";\nimport type {\n AuditContext,\n ContextExtractor,\n ValueExtractor,\n} from \"@usebetterdev/audit-core\";\nimport { headers } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type RouteContext = Record<string, unknown> | undefined;\n\nexport type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;\n\n/**\n * Shared options across all audit wrappers.\n */\nexport interface BaseAuditOptions {\n /** Context extractor for pulling actor identity from the request. */\n extractor?: ContextExtractor;\n /** Called when an extractor throws. Defaults to silent fail-open. */\n onError?: (error: unknown) => void;\n}\n\nexport interface CreateAuditMiddlewareOptions extends BaseAuditOptions {\n /**\n * Request header name used to forward the extracted actor id to downstream\n * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.\n */\n actorHeader?: string;\n}\n\nexport type WithAuditRouteOptions = BaseAuditOptions;\n\nexport type WithAuditOptions = BaseAuditOptions;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default header name used to forward the actor id from middleware to route\n * handlers.\n *\n * Use this constant to keep the middleware and route handler in sync:\n * ```ts\n * // middleware.ts\n * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER\n *\n * // app/api/orders/route.ts\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport const AUDIT_ACTOR_HEADER = \"x-better-audit-actor-id\";\n\n// ---------------------------------------------------------------------------\n// createAuditMiddleware / betterAuditNext\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Next.js edge middleware function that extracts audit context from\n * the incoming request and forwards the actor id as a request header to\n * downstream route handlers.\n *\n * **Important:** Next.js middleware runs in a separate Edge Runtime execution\n * context from route handlers — AsyncLocalStorage set here does NOT propagate.\n * This middleware forwards the extracted actor id via a request header\n * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route\n * handlers to read that header and populate ALS.\n *\n * The middleware **always** overwrites the actor header on the forwarded\n * request — when extraction fails, it is set to an empty string. This prevents\n * clients from spoofing the actor id by sending the header directly.\n *\n * The middleware is non-blocking: if context extraction fails, the request\n * proceeds with an empty actor header (fail open).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAuditMiddleware } from \"@usebetterdev/audit-next\";\n * export default createAuditMiddleware();\n * export const config = { matcher: \"/api/:path*\" };\n *\n * // app/api/orders/route.ts\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function createAuditMiddleware(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n const extractor = options.extractor ?? defaultExtractor;\n const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;\n\n return async (request) => {\n const actorId = await safeExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n // Always forward with an explicit header value so clients cannot spoof\n // the actor id by sending the header directly. When extraction fails the\n // header is set to \"\" — fromHeader() treats empty values as undefined.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(actorHeader, actorId ?? \"\");\n\n return NextResponse.next({\n request: { headers: requestHeaders },\n });\n };\n}\n\n/**\n * Convenience alias for `createAuditMiddleware`. Use as the default export\n * in `middleware.ts`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { betterAuditNext } from \"@usebetterdev/audit-next\";\n * export default betterAuditNext();\n * ```\n */\nexport function betterAuditNext(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n return createAuditMiddleware(options);\n}\n\n// ---------------------------------------------------------------------------\n// withAuditRoute\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js App Router route handler with audit context propagation.\n *\n * Extracts the actor identity from the incoming request (by default reads\n * `sub` from `Authorization: Bearer <jwt>`, or reads the\n * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and\n * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`\n * returns the correct context within the handler.\n *\n * `NextRequest` is a Web `Request` subclass — extractors from audit-core\n * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.\n *\n * The wrapper is non-blocking: if extraction fails, the handler runs without\n * audit context (fail open).\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts — standalone (no middleware)\n * import { withAuditRoute } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler);\n *\n * // app/api/orders/route.ts — paired with createAuditMiddleware\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function withAuditRoute<C extends RouteContext = RouteContext>(\n handler: (request: NextRequest, context: C) => Promise<Response>,\n options: WithAuditRouteOptions = {},\n): (request: NextRequest, context: C) => Promise<Response> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (request, context) => {\n const actorId = await safeExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n if (actorId === undefined) {\n return handler(request, context);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => handler(request, context));\n };\n}\n\n// ---------------------------------------------------------------------------\n// withAudit (server actions)\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js server action with audit context propagation.\n *\n * Server actions do not receive a `Request` object. This wrapper reads all\n * request headers via `next/headers` and constructs a synthetic `Request` to\n * run the configured extractor against, then wraps the action in an\n * AsyncLocalStorage scope so that `getAuditContext()` returns the correct\n * context within the action.\n *\n * The wrapper is non-blocking: if extraction fails (including when `headers()`\n * is unavailable), the action runs without audit context (fail open).\n *\n * @example\n * ```ts\n * // app/actions.ts\n * \"use server\";\n * import { withAudit } from \"@usebetterdev/audit-next\";\n *\n * export const createOrder = withAudit(async (formData: FormData) => {\n * // getAuditContext() returns the actor here\n * });\n * ```\n */\nexport function withAudit<TArgs extends unknown[], TResult>(\n action: (...args: TArgs) => Promise<TResult>,\n options: WithAuditOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (...args) => {\n let actorId: string | undefined;\n\n try {\n const headersList = await headers();\n const syntheticRequest = new Request(\"http://localhost\", {\n headers: headersList,\n });\n actorId = await safeExtract(\n extractor.actor,\n syntheticRequest,\n options.onError,\n );\n } catch (error: unknown) {\n // headers() throws outside Next.js request context — fail open\n options.onError?.(error);\n }\n\n if (actorId === undefined) {\n return action(...args);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => action(...args));\n };\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { AuditContext, ContextExtractor, ValueExtractor };\nexport { fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from \"@usebetterdev/audit-core\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAMO;AAMP,qBAAwB;AACxB,oBAA6B;AA6P7B,IAAAA,qBAA8F;AAzMvF,IAAM,qBAAqB;AAsC3B,SAAS,sBACd,UAAwC,CAAC,GACzB;AAChB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,cAAc,QAAQ,eAAe;AAE3C,SAAO,OAAO,YAAY;AACxB,UAAM,UAAU,UAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAKA,UAAM,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;AAClD,mBAAe,IAAI,aAAa,WAAW,EAAE;AAE7C,WAAO,2BAAa,KAAK;AAAA,MACvB,SAAS,EAAE,SAAS,eAAe;AAAA,IACrC,CAAC;AAAA,EACH;AACF;AAaO,SAAS,gBACd,UAAwC,CAAC,GACzB;AAChB,SAAO,sBAAsB,OAAO;AACtC;AAkCO,SAAS,eACd,SACA,UAAiC,CAAC,GACuB;AACzD,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,OAAO,SAAS,YAAY;AACjC,UAAM,UAAU,UAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,QAAQ,SAAS,OAAO;AAAA,IACjC;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,eAAO,uCAAoB,cAAc,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC1E;AACF;AA6BO,SAAS,UACd,QACA,UAA4B,CAAC,GACS;AACtC,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,UAAU,SAAS;AACxB,QAAI;AAEJ,QAAI;AACF,YAAM,cAAc,UAAM,wBAAQ;AAClC,YAAM,mBAAmB,IAAI,QAAQ,oBAAoB;AAAA,QACvD,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,UAAM;AAAA,QACd,UAAU;AAAA,QACV;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF,SAAS,OAAgB;AAEvB,cAAQ,UAAU,KAAK;AAAA,IACzB;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,GAAG,IAAI;AAAA,IACvB;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,eAAO,uCAAoB,cAAc,MAAM,OAAO,GAAG,IAAI,CAAC;AAAA,EAChE;AACF;","names":["import_audit_core"]}
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
runWithAuditContext
|
|
3
|
+
defaultExtractor,
|
|
4
|
+
runWithAuditContext,
|
|
5
|
+
safeExtract
|
|
5
6
|
} from "@usebetterdev/audit-core";
|
|
6
7
|
import { headers } from "next/headers";
|
|
7
8
|
import { NextResponse } from "next/server";
|
|
8
9
|
import { fromBearerToken as fromBearerToken2, fromCookie, fromHeader, getAuditContext as getAuditContext2, runWithAuditContext as runWithAuditContext2 } from "@usebetterdev/audit-core";
|
|
9
10
|
var AUDIT_ACTOR_HEADER = "x-better-audit-actor-id";
|
|
10
|
-
var defaultExtractor = {
|
|
11
|
-
actor: fromBearerToken("sub")
|
|
12
|
-
};
|
|
13
|
-
async function tryExtract(extractor, request, onError) {
|
|
14
|
-
if (!extractor) {
|
|
15
|
-
return void 0;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
return await extractor(request);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (onError) {
|
|
21
|
-
onError(error);
|
|
22
|
-
}
|
|
23
|
-
return void 0;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
11
|
function createAuditMiddleware(options = {}) {
|
|
27
12
|
const extractor = options.extractor ?? defaultExtractor;
|
|
28
13
|
const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;
|
|
29
14
|
return async (request) => {
|
|
30
|
-
const actorId = await
|
|
15
|
+
const actorId = await safeExtract(
|
|
31
16
|
extractor.actor,
|
|
32
17
|
request,
|
|
33
18
|
options.onError
|
|
@@ -45,7 +30,7 @@ function betterAuditNext(options = {}) {
|
|
|
45
30
|
function withAuditRoute(handler, options = {}) {
|
|
46
31
|
const extractor = options.extractor ?? defaultExtractor;
|
|
47
32
|
return async (request, context) => {
|
|
48
|
-
const actorId = await
|
|
33
|
+
const actorId = await safeExtract(
|
|
49
34
|
extractor.actor,
|
|
50
35
|
request,
|
|
51
36
|
options.onError
|
|
@@ -66,7 +51,7 @@ function withAudit(action, options = {}) {
|
|
|
66
51
|
const syntheticRequest = new Request("http://localhost", {
|
|
67
52
|
headers: headersList
|
|
68
53
|
});
|
|
69
|
-
actorId = await
|
|
54
|
+
actorId = await safeExtract(
|
|
70
55
|
extractor.actor,
|
|
71
56
|
syntheticRequest,
|
|
72
57
|
options.onError
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n fromBearerToken,\n getAuditContext,\n runWithAuditContext,\n} from \"@usebetterdev/audit-core\";\nimport type {\n AuditContext,\n ContextExtractor,\n ValueExtractor,\n} from \"@usebetterdev/audit-core\";\nimport { headers } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type RouteContext = Record<string, unknown> | undefined;\n\nexport type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;\n\n/**\n * Shared options across all audit wrappers.\n */\nexport interface BaseAuditOptions {\n /** Context extractor for pulling actor identity from the request. */\n extractor?: ContextExtractor;\n /** Called when an extractor throws. Defaults to silent fail-open. */\n onError?: (error: unknown) => void;\n}\n\nexport interface CreateAuditMiddlewareOptions extends BaseAuditOptions {\n /**\n * Request header name used to forward the extracted actor id to downstream\n * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.\n */\n actorHeader?: string;\n}\n\nexport type WithAuditRouteOptions = BaseAuditOptions;\n\nexport type WithAuditOptions = BaseAuditOptions;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default header name used to forward the actor id from middleware to route\n * handlers.\n *\n * Use this constant to keep the middleware and route handler in sync:\n * ```ts\n * // middleware.ts\n * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER\n *\n * // app/api/orders/route.ts\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport const AUDIT_ACTOR_HEADER = \"x-better-audit-actor-id\";\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/** Default extractor: reads `sub` from `Authorization: Bearer <jwt>` as actor. */\nconst defaultExtractor: ContextExtractor = {\n actor: fromBearerToken(\"sub\"),\n};\n\n/**\n * Runs a value extractor safely, returning undefined on error (fail open).\n */\nasync function tryExtract(\n extractor: ValueExtractor | undefined,\n request: Request,\n onError: ((error: unknown) => void) | undefined,\n): Promise<string | undefined> {\n if (!extractor) {\n return undefined;\n }\n try {\n return await extractor(request);\n } catch (error: unknown) {\n if (onError) {\n onError(error);\n }\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// createAuditMiddleware / betterAuditNext\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Next.js edge middleware function that extracts audit context from\n * the incoming request and forwards the actor id as a request header to\n * downstream route handlers.\n *\n * **Important:** Next.js middleware runs in a separate Edge Runtime execution\n * context from route handlers — AsyncLocalStorage set here does NOT propagate.\n * This middleware forwards the extracted actor id via a request header\n * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route\n * handlers to read that header and populate ALS.\n *\n * The middleware **always** overwrites the actor header on the forwarded\n * request — when extraction fails, it is set to an empty string. This prevents\n * clients from spoofing the actor id by sending the header directly.\n *\n * The middleware is non-blocking: if context extraction fails, the request\n * proceeds with an empty actor header (fail open).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAuditMiddleware } from \"@usebetterdev/audit-next\";\n * export default createAuditMiddleware();\n * export const config = { matcher: \"/api/:path*\" };\n *\n * // app/api/orders/route.ts\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function createAuditMiddleware(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n const extractor = options.extractor ?? defaultExtractor;\n const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;\n\n return async (request) => {\n const actorId = await tryExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n // Always forward with an explicit header value so clients cannot spoof\n // the actor id by sending the header directly. When extraction fails the\n // header is set to \"\" — fromHeader() treats empty values as undefined.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(actorHeader, actorId ?? \"\");\n\n return NextResponse.next({\n request: { headers: requestHeaders },\n });\n };\n}\n\n/**\n * Convenience alias for `createAuditMiddleware`. Use as the default export\n * in `middleware.ts`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { betterAuditNext } from \"@usebetterdev/audit-next\";\n * export default betterAuditNext();\n * ```\n */\nexport function betterAuditNext(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n return createAuditMiddleware(options);\n}\n\n// ---------------------------------------------------------------------------\n// withAuditRoute\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js App Router route handler with audit context propagation.\n *\n * Extracts the actor identity from the incoming request (by default reads\n * `sub` from `Authorization: Bearer <jwt>`, or reads the\n * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and\n * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`\n * returns the correct context within the handler.\n *\n * `NextRequest` is a Web `Request` subclass — extractors from audit-core\n * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.\n *\n * The wrapper is non-blocking: if extraction fails, the handler runs without\n * audit context (fail open).\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts — standalone (no middleware)\n * import { withAuditRoute } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler);\n *\n * // app/api/orders/route.ts — paired with createAuditMiddleware\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function withAuditRoute<C extends RouteContext = RouteContext>(\n handler: (request: NextRequest, context: C) => Promise<Response>,\n options: WithAuditRouteOptions = {},\n): (request: NextRequest, context: C) => Promise<Response> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (request, context) => {\n const actorId = await tryExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n if (actorId === undefined) {\n return handler(request, context);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => handler(request, context));\n };\n}\n\n// ---------------------------------------------------------------------------\n// withAudit (server actions)\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js server action with audit context propagation.\n *\n * Server actions do not receive a `Request` object. This wrapper reads all\n * request headers via `next/headers` and constructs a synthetic `Request` to\n * run the configured extractor against, then wraps the action in an\n * AsyncLocalStorage scope so that `getAuditContext()` returns the correct\n * context within the action.\n *\n * The wrapper is non-blocking: if extraction fails (including when `headers()`\n * is unavailable), the action runs without audit context (fail open).\n *\n * @example\n * ```ts\n * // app/actions.ts\n * \"use server\";\n * import { withAudit } from \"@usebetterdev/audit-next\";\n *\n * export const createOrder = withAudit(async (formData: FormData) => {\n * // getAuditContext() returns the actor here\n * });\n * ```\n */\nexport function withAudit<TArgs extends unknown[], TResult>(\n action: (...args: TArgs) => Promise<TResult>,\n options: WithAuditOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (...args) => {\n let actorId: string | undefined;\n\n try {\n const headersList = await headers();\n const syntheticRequest = new Request(\"http://localhost\", {\n headers: headersList,\n });\n actorId = await tryExtract(\n extractor.actor,\n syntheticRequest,\n options.onError,\n );\n } catch (error: unknown) {\n // headers() throws outside Next.js request context — fail open\n options.onError?.(error);\n }\n\n if (actorId === undefined) {\n return action(...args);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => action(...args));\n };\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { AuditContext, ContextExtractor, ValueExtractor };\nexport { fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from \"@usebetterdev/audit-core\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EAEA;AAAA,OACK;AAMP,SAAS,eAAe;AACxB,SAAS,oBAAoB;AA2R7B,SAAS,mBAAAA,kBAAiB,YAAY,YAAY,mBAAAC,kBAAiB,uBAAAC,4BAA2B;AAvOvF,IAAM,qBAAqB;AAOlC,IAAM,mBAAqC;AAAA,EACzC,OAAO,gBAAgB,KAAK;AAC9B;AAKA,eAAe,WACb,WACA,SACA,SAC6B;AAC7B,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AACA,MAAI;AACF,WAAO,MAAM,UAAU,OAAO;AAAA,EAChC,SAAS,OAAgB;AACvB,QAAI,SAAS;AACX,cAAQ,KAAK;AAAA,IACf;AACA,WAAO;AAAA,EACT;AACF;AAsCO,SAAS,sBACd,UAAwC,CAAC,GACzB;AAChB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,cAAc,QAAQ,eAAe;AAE3C,SAAO,OAAO,YAAY;AACxB,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAKA,UAAM,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;AAClD,mBAAe,IAAI,aAAa,WAAW,EAAE;AAE7C,WAAO,aAAa,KAAK;AAAA,MACvB,SAAS,EAAE,SAAS,eAAe;AAAA,IACrC,CAAC;AAAA,EACH;AACF;AAaO,SAAS,gBACd,UAAwC,CAAC,GACzB;AAChB,SAAO,sBAAsB,OAAO;AACtC;AAkCO,SAAS,eACd,SACA,UAAiC,CAAC,GACuB;AACzD,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,OAAO,SAAS,YAAY;AACjC,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,QAAQ,SAAS,OAAO;AAAA,IACjC;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,WAAO,oBAAoB,cAAc,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC1E;AACF;AA6BO,SAAS,UACd,QACA,UAA4B,CAAC,GACS;AACtC,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,UAAU,SAAS;AACxB,QAAI;AAEJ,QAAI;AACF,YAAM,cAAc,MAAM,QAAQ;AAClC,YAAM,mBAAmB,IAAI,QAAQ,oBAAoB;AAAA,QACvD,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAAA,QACd,UAAU;AAAA,QACV;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF,SAAS,OAAgB;AAEvB,cAAQ,UAAU,KAAK;AAAA,IACzB;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,GAAG,IAAI;AAAA,IACvB;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,WAAO,oBAAoB,cAAc,MAAM,OAAO,GAAG,IAAI,CAAC;AAAA,EAChE;AACF;","names":["fromBearerToken","getAuditContext","runWithAuditContext"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n defaultExtractor,\n fromBearerToken,\n getAuditContext,\n runWithAuditContext,\n safeExtract,\n} from \"@usebetterdev/audit-core\";\nimport type {\n AuditContext,\n ContextExtractor,\n ValueExtractor,\n} from \"@usebetterdev/audit-core\";\nimport { headers } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type RouteContext = Record<string, unknown> | undefined;\n\nexport type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;\n\n/**\n * Shared options across all audit wrappers.\n */\nexport interface BaseAuditOptions {\n /** Context extractor for pulling actor identity from the request. */\n extractor?: ContextExtractor;\n /** Called when an extractor throws. Defaults to silent fail-open. */\n onError?: (error: unknown) => void;\n}\n\nexport interface CreateAuditMiddlewareOptions extends BaseAuditOptions {\n /**\n * Request header name used to forward the extracted actor id to downstream\n * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.\n */\n actorHeader?: string;\n}\n\nexport type WithAuditRouteOptions = BaseAuditOptions;\n\nexport type WithAuditOptions = BaseAuditOptions;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Default header name used to forward the actor id from middleware to route\n * handlers.\n *\n * Use this constant to keep the middleware and route handler in sync:\n * ```ts\n * // middleware.ts\n * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER\n *\n * // app/api/orders/route.ts\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport const AUDIT_ACTOR_HEADER = \"x-better-audit-actor-id\";\n\n// ---------------------------------------------------------------------------\n// createAuditMiddleware / betterAuditNext\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a Next.js edge middleware function that extracts audit context from\n * the incoming request and forwards the actor id as a request header to\n * downstream route handlers.\n *\n * **Important:** Next.js middleware runs in a separate Edge Runtime execution\n * context from route handlers — AsyncLocalStorage set here does NOT propagate.\n * This middleware forwards the extracted actor id via a request header\n * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route\n * handlers to read that header and populate ALS.\n *\n * The middleware **always** overwrites the actor header on the forwarded\n * request — when extraction fails, it is set to an empty string. This prevents\n * clients from spoofing the actor id by sending the header directly.\n *\n * The middleware is non-blocking: if context extraction fails, the request\n * proceeds with an empty actor header (fail open).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAuditMiddleware } from \"@usebetterdev/audit-next\";\n * export default createAuditMiddleware();\n * export const config = { matcher: \"/api/:path*\" };\n *\n * // app/api/orders/route.ts\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function createAuditMiddleware(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n const extractor = options.extractor ?? defaultExtractor;\n const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;\n\n return async (request) => {\n const actorId = await safeExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n // Always forward with an explicit header value so clients cannot spoof\n // the actor id by sending the header directly. When extraction fails the\n // header is set to \"\" — fromHeader() treats empty values as undefined.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(actorHeader, actorId ?? \"\");\n\n return NextResponse.next({\n request: { headers: requestHeaders },\n });\n };\n}\n\n/**\n * Convenience alias for `createAuditMiddleware`. Use as the default export\n * in `middleware.ts`.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { betterAuditNext } from \"@usebetterdev/audit-next\";\n * export default betterAuditNext();\n * ```\n */\nexport function betterAuditNext(\n options: CreateAuditMiddlewareOptions = {},\n): NextMiddleware {\n return createAuditMiddleware(options);\n}\n\n// ---------------------------------------------------------------------------\n// withAuditRoute\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js App Router route handler with audit context propagation.\n *\n * Extracts the actor identity from the incoming request (by default reads\n * `sub` from `Authorization: Bearer <jwt>`, or reads the\n * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and\n * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`\n * returns the correct context within the handler.\n *\n * `NextRequest` is a Web `Request` subclass — extractors from audit-core\n * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.\n *\n * The wrapper is non-blocking: if extraction fails, the handler runs without\n * audit context (fail open).\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts — standalone (no middleware)\n * import { withAuditRoute } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler);\n *\n * // app/api/orders/route.ts — paired with createAuditMiddleware\n * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from \"@usebetterdev/audit-next\";\n * export const GET = withAuditRoute(handler, {\n * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },\n * });\n * ```\n */\nexport function withAuditRoute<C extends RouteContext = RouteContext>(\n handler: (request: NextRequest, context: C) => Promise<Response>,\n options: WithAuditRouteOptions = {},\n): (request: NextRequest, context: C) => Promise<Response> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (request, context) => {\n const actorId = await safeExtract(\n extractor.actor,\n request,\n options.onError,\n );\n\n if (actorId === undefined) {\n return handler(request, context);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => handler(request, context));\n };\n}\n\n// ---------------------------------------------------------------------------\n// withAudit (server actions)\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a Next.js server action with audit context propagation.\n *\n * Server actions do not receive a `Request` object. This wrapper reads all\n * request headers via `next/headers` and constructs a synthetic `Request` to\n * run the configured extractor against, then wraps the action in an\n * AsyncLocalStorage scope so that `getAuditContext()` returns the correct\n * context within the action.\n *\n * The wrapper is non-blocking: if extraction fails (including when `headers()`\n * is unavailable), the action runs without audit context (fail open).\n *\n * @example\n * ```ts\n * // app/actions.ts\n * \"use server\";\n * import { withAudit } from \"@usebetterdev/audit-next\";\n *\n * export const createOrder = withAudit(async (formData: FormData) => {\n * // getAuditContext() returns the actor here\n * });\n * ```\n */\nexport function withAudit<TArgs extends unknown[], TResult>(\n action: (...args: TArgs) => Promise<TResult>,\n options: WithAuditOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n const extractor = options.extractor ?? defaultExtractor;\n\n return async (...args) => {\n let actorId: string | undefined;\n\n try {\n const headersList = await headers();\n const syntheticRequest = new Request(\"http://localhost\", {\n headers: headersList,\n });\n actorId = await safeExtract(\n extractor.actor,\n syntheticRequest,\n options.onError,\n );\n } catch (error: unknown) {\n // headers() throws outside Next.js request context — fail open\n options.onError?.(error);\n }\n\n if (actorId === undefined) {\n return action(...args);\n }\n\n const auditContext: AuditContext = { actorId };\n\n return runWithAuditContext(auditContext, () => action(...args));\n };\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { AuditContext, ContextExtractor, ValueExtractor };\nexport { fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from \"@usebetterdev/audit-core\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EAGA;AAAA,EACA;AAAA,OACK;AAMP,SAAS,eAAe;AACxB,SAAS,oBAAoB;AA6P7B,SAAS,mBAAAA,kBAAiB,YAAY,YAAY,mBAAAC,kBAAiB,uBAAAC,4BAA2B;AAzMvF,IAAM,qBAAqB;AAsC3B,SAAS,sBACd,UAAwC,CAAC,GACzB;AAChB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,cAAc,QAAQ,eAAe;AAE3C,SAAO,OAAO,YAAY;AACxB,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAKA,UAAM,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;AAClD,mBAAe,IAAI,aAAa,WAAW,EAAE;AAE7C,WAAO,aAAa,KAAK;AAAA,MACvB,SAAS,EAAE,SAAS,eAAe;AAAA,IACrC,CAAC;AAAA,EACH;AACF;AAaO,SAAS,gBACd,UAAwC,CAAC,GACzB;AAChB,SAAO,sBAAsB,OAAO;AACtC;AAkCO,SAAS,eACd,SACA,UAAiC,CAAC,GACuB;AACzD,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,OAAO,SAAS,YAAY;AACjC,UAAM,UAAU,MAAM;AAAA,MACpB,UAAU;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,QAAQ,SAAS,OAAO;AAAA,IACjC;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,WAAO,oBAAoB,cAAc,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC1E;AACF;AA6BO,SAAS,UACd,QACA,UAA4B,CAAC,GACS;AACtC,QAAM,YAAY,QAAQ,aAAa;AAEvC,SAAO,UAAU,SAAS;AACxB,QAAI;AAEJ,QAAI;AACF,YAAM,cAAc,MAAM,QAAQ;AAClC,YAAM,mBAAmB,IAAI,QAAQ,oBAAoB;AAAA,QACvD,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,MAAM;AAAA,QACd,UAAU;AAAA,QACV;AAAA,QACA,QAAQ;AAAA,MACV;AAAA,IACF,SAAS,OAAgB;AAEvB,cAAQ,UAAU,KAAK;AAAA,IACzB;AAEA,QAAI,YAAY,QAAW;AACzB,aAAO,OAAO,GAAG,IAAI;AAAA,IACvB;AAEA,UAAM,eAA6B,EAAE,QAAQ;AAE7C,WAAO,oBAAoB,cAAc,MAAM,OAAO,GAAG,IAAI,CAAC;AAAA,EAChE;AACF;","names":["fromBearerToken","getAuditContext","runWithAuditContext"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/audit-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"repository": "github:usebetter-dev/usebetter",
|
|
5
5
|
"bugs": "https://github.com/usebetter-dev/usebetter/issues",
|
|
6
6
|
"homepage": "https://github.com/usebetter-dev/usebetter#readme",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@usebetterdev/audit-core": "0.
|
|
27
|
+
"@usebetterdev/audit-core": "0.7.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"next": ">=14.0.0"
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"next": "^15.5.2",
|
|
35
35
|
"tsup": "^8.3.5",
|
|
36
36
|
"typescript": "~5.7.2",
|
|
37
|
-
"vitest": "^2.1.6"
|
|
37
|
+
"vitest": "^2.1.6",
|
|
38
|
+
"@usebetterdev/test-utils": "^0.5.2"
|
|
38
39
|
},
|
|
39
40
|
"engines": {
|
|
40
41
|
"node": ">=22"
|