@usebetterdev/audit-next 0.6.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 usebetter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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 tryExtract(
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 tryExtract(
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 tryExtract(
83
+ actorId = await (0, import_audit_core.safeExtract)(
100
84
  extractor.actor,
101
85
  syntheticRequest,
102
86
  options.onError
@@ -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
- fromBearerToken,
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 tryExtract(
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 tryExtract(
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 tryExtract(
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.6.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",
@@ -23,15 +23,8 @@
23
23
  "dist",
24
24
  "README.md"
25
25
  ],
26
- "scripts": {
27
- "build": "tsup",
28
- "build:types": "tsc --build tsconfig.build.json",
29
- "lint": "oxlint",
30
- "test": "vitest run",
31
- "typecheck": "tsc --noEmit"
32
- },
33
26
  "dependencies": {
34
- "@usebetterdev/audit-core": "workspace:*"
27
+ "@usebetterdev/audit-core": "0.7.0"
35
28
  },
36
29
  "peerDependencies": {
37
30
  "next": ">=14.0.0"
@@ -41,9 +34,17 @@
41
34
  "next": "^15.5.2",
42
35
  "tsup": "^8.3.5",
43
36
  "typescript": "~5.7.2",
44
- "vitest": "^2.1.6"
37
+ "vitest": "^2.1.6",
38
+ "@usebetterdev/test-utils": "^0.5.2"
45
39
  },
46
40
  "engines": {
47
41
  "node": ">=22"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "build:types": "tsc --build tsconfig.build.json",
46
+ "lint": "oxlint",
47
+ "test": "vitest run",
48
+ "typecheck": "tsc --noEmit"
48
49
  }
49
- }
50
+ }