@usebetterdev/audit-next 0.6.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/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @usebetterdev/audit-next
2
+
3
+ Next.js App Router adapter for [`@usebetterdev/audit-core`](../core). Extracts actor identity from incoming requests and propagates it via `AsyncLocalStorage` so that every audit log captured during a request automatically includes the correct actor.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @usebetterdev/audit-next @usebetterdev/audit-core
9
+ ```
10
+
11
+ Requires `next >= 14`.
12
+
13
+ ## How context propagation works in Next.js
14
+
15
+ Next.js App Router has three distinct execution contexts, each requiring a different approach:
16
+
17
+ | Context | Has `Request`? | ALS propagates? | Use |
18
+ |---|---|---|---|
19
+ | Edge Middleware (`middleware.ts`) | Yes | No — separate isolate | `createAuditMiddleware` |
20
+ | Route Handler (`route.ts`) | Yes | Yes | `withAuditRoute` |
21
+ | Server Action | No | Yes | `withAudit` |
22
+
23
+ **Edge Middleware runs in a separate V8 isolate from route handlers.** AsyncLocalStorage set in middleware does not carry over. Instead, `createAuditMiddleware` extracts the actor from the request and forwards it as a request header (`x-better-audit-actor-id` by default). `withAuditRoute` then reads that header and sets up ALS for the handler's execution.
24
+
25
+ ## Quick start
26
+
27
+ ### Option A — Middleware + route handlers (recommended for APIs)
28
+
29
+ ```ts
30
+ // middleware.ts
31
+ import { createAuditMiddleware } from "@usebetterdev/audit-next";
32
+
33
+ export default createAuditMiddleware();
34
+ export const config = { matcher: "/api/:path*" };
35
+ ```
36
+
37
+ ```ts
38
+ // app/api/orders/route.ts
39
+ import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER, getAuditContext } from "@usebetterdev/audit-next";
40
+ import { audit } from "@/lib/audit";
41
+
42
+ async function handler(request: NextRequest) {
43
+ const ctx = getAuditContext(); // { actorId: "user-123" }
44
+ await audit.captureLog({ tableName: "orders", operation: "INSERT", recordId: "ord-1", after: {} });
45
+ return Response.json({ ok: true });
46
+ }
47
+
48
+ export const POST = withAuditRoute(handler, {
49
+ extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
50
+ });
51
+ ```
52
+
53
+ > Use `AUDIT_ACTOR_HEADER` to keep the header name in sync between middleware and route handlers without hardcoding strings.
54
+
55
+ ### Option B — Route handler only (no middleware)
56
+
57
+ If you don't use Next.js middleware, `withAuditRoute` can extract directly from the request:
58
+
59
+ ```ts
60
+ // app/api/orders/route.ts
61
+ import { withAuditRoute } from "@usebetterdev/audit-next";
62
+
63
+ export const POST = withAuditRoute(handler);
64
+ // Reads `sub` from Authorization: Bearer <jwt> by default
65
+ ```
66
+
67
+ ### Option C — Server actions
68
+
69
+ ```ts
70
+ // app/actions.ts
71
+ "use server";
72
+ import { withAudit, getAuditContext } from "@usebetterdev/audit-next";
73
+ import { audit } from "@/lib/audit";
74
+
75
+ export const createOrder = withAudit(async (formData: FormData) => {
76
+ const ctx = getAuditContext(); // { actorId: "user-123" }
77
+ await audit.captureLog({ tableName: "orders", operation: "INSERT", recordId: "ord-1", after: {} });
78
+ });
79
+ ```
80
+
81
+ Server actions don't receive a `Request` object. `withAudit` reads all request headers via `next/headers` and constructs a synthetic request to run the extractor against.
82
+
83
+ ## Actor extraction
84
+
85
+ All three wrappers use a `ContextExtractor` to resolve the actor. The default extracts the `sub` claim from `Authorization: Bearer <jwt>`. The token is decoded **without** signature verification — that is the auth layer's responsibility.
86
+
87
+ ### Custom JWT claim
88
+
89
+ ```ts
90
+ import { fromBearerToken } from "@usebetterdev/audit-next";
91
+
92
+ withAuditRoute(handler, {
93
+ extractor: { actor: fromBearerToken("user_id") },
94
+ });
95
+ ```
96
+
97
+ ### Header-based extraction
98
+
99
+ Common when running behind an API gateway that forwards identity as a plain header:
100
+
101
+ ```ts
102
+ import { fromHeader } from "@usebetterdev/audit-next";
103
+
104
+ withAuditRoute(handler, {
105
+ extractor: { actor: fromHeader("x-user-id") },
106
+ });
107
+ ```
108
+
109
+ ### Cookie-based extraction
110
+
111
+ ```ts
112
+ import { fromCookie } from "@usebetterdev/audit-next";
113
+
114
+ withAudit(action, {
115
+ extractor: { actor: fromCookie("session_id") },
116
+ });
117
+ ```
118
+
119
+ ### Custom extractor
120
+
121
+ Write any async function that receives a `Request` and returns a string or `undefined`:
122
+
123
+ ```ts
124
+ withAuditRoute(handler, {
125
+ extractor: {
126
+ actor: async (request) => {
127
+ const key = request.headers.get("x-api-key");
128
+ if (!key) return undefined;
129
+ const owner = await resolveApiKeyOwner(key);
130
+ return owner?.id;
131
+ },
132
+ },
133
+ });
134
+ ```
135
+
136
+ ## Error handling
137
+
138
+ All wrappers **fail open** — if extraction fails, the request or action proceeds without audit context. No request is ever blocked by the audit layer.
139
+
140
+ Use `onError` to observe failures:
141
+
142
+ ```ts
143
+ createAuditMiddleware({
144
+ onError: (error) => console.error("Audit extraction failed:", error),
145
+ });
146
+ ```
147
+
148
+ ## Security note
149
+
150
+ `createAuditMiddleware` always overwrites the actor header on the forwarded request — including when extraction fails (sets it to `""`). This prevents clients from spoofing the actor identity by sending the header directly. **Do not trust `x-better-audit-actor-id` on incoming requests unless it was set by your middleware.**
151
+
152
+ ## API
153
+
154
+ ### `createAuditMiddleware(options?)`
155
+
156
+ Creates a Next.js edge middleware function. Extracts actor from the request and forwards it as a request header to downstream route handlers.
157
+
158
+ ### `betterAuditNext(options?)`
159
+
160
+ Convenience alias for `createAuditMiddleware`. Use as the default export in `middleware.ts`.
161
+
162
+ ### `withAuditRoute(handler, options?)`
163
+
164
+ Wraps an App Router route handler. Extracts actor from the incoming `NextRequest` and runs the handler inside an ALS scope.
165
+
166
+ ### `withAudit(action, options?)`
167
+
168
+ Wraps a server action. Reads headers via `next/headers`, extracts actor, and runs the action inside an ALS scope.
169
+
170
+ **Options (all wrappers):**
171
+
172
+ | Option | Type | Description |
173
+ |---|---|---|
174
+ | `extractor` | `ContextExtractor` | Actor extractor config. Defaults to JWT `sub` claim. |
175
+ | `onError` | `(error: unknown) => void` | Called when extraction throws. Defaults to no-op. |
176
+
177
+ **Additional option for `createAuditMiddleware`:**
178
+
179
+ | Option | Type | Description |
180
+ |---|---|---|
181
+ | `actorHeader` | `string` | Header name for forwarding the actor id. Defaults to `AUDIT_ACTOR_HEADER`. |
182
+
183
+ ### `AUDIT_ACTOR_HEADER`
184
+
185
+ The default header name (`"x-better-audit-actor-id"`) used to forward the actor id from middleware to route handlers. Import this constant in both files to avoid hardcoding the string.
186
+
187
+ ### `getAuditContext()`
188
+
189
+ Returns the current `AuditContext` from ALS, or `undefined` when called outside a request scope. Re-exported from `@usebetterdev/audit-core` for convenience.
package/dist/index.cjs ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AUDIT_ACTOR_HEADER: () => AUDIT_ACTOR_HEADER,
24
+ betterAuditNext: () => betterAuditNext,
25
+ createAuditMiddleware: () => createAuditMiddleware,
26
+ fromBearerToken: () => import_audit_core2.fromBearerToken,
27
+ fromCookie: () => import_audit_core2.fromCookie,
28
+ fromHeader: () => import_audit_core2.fromHeader,
29
+ getAuditContext: () => import_audit_core2.getAuditContext,
30
+ runWithAuditContext: () => import_audit_core2.runWithAuditContext,
31
+ withAudit: () => withAudit,
32
+ withAuditRoute: () => withAuditRoute
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+ var import_audit_core = require("@usebetterdev/audit-core");
36
+ var import_headers = require("next/headers");
37
+ var import_server = require("next/server");
38
+ var import_audit_core2 = require("@usebetterdev/audit-core");
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
+ function createAuditMiddleware(options = {}) {
57
+ const extractor = options.extractor ?? defaultExtractor;
58
+ const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;
59
+ return async (request) => {
60
+ const actorId = await tryExtract(
61
+ extractor.actor,
62
+ request,
63
+ options.onError
64
+ );
65
+ const requestHeaders = new Headers(request.headers);
66
+ requestHeaders.set(actorHeader, actorId ?? "");
67
+ return import_server.NextResponse.next({
68
+ request: { headers: requestHeaders }
69
+ });
70
+ };
71
+ }
72
+ function betterAuditNext(options = {}) {
73
+ return createAuditMiddleware(options);
74
+ }
75
+ function withAuditRoute(handler, options = {}) {
76
+ const extractor = options.extractor ?? defaultExtractor;
77
+ return async (request, context) => {
78
+ const actorId = await tryExtract(
79
+ extractor.actor,
80
+ request,
81
+ options.onError
82
+ );
83
+ if (actorId === void 0) {
84
+ return handler(request, context);
85
+ }
86
+ const auditContext = { actorId };
87
+ return (0, import_audit_core.runWithAuditContext)(auditContext, () => handler(request, context));
88
+ };
89
+ }
90
+ function withAudit(action, options = {}) {
91
+ const extractor = options.extractor ?? defaultExtractor;
92
+ return async (...args) => {
93
+ let actorId;
94
+ try {
95
+ const headersList = await (0, import_headers.headers)();
96
+ const syntheticRequest = new Request("http://localhost", {
97
+ headers: headersList
98
+ });
99
+ actorId = await tryExtract(
100
+ extractor.actor,
101
+ syntheticRequest,
102
+ options.onError
103
+ );
104
+ } catch (error) {
105
+ options.onError?.(error);
106
+ }
107
+ if (actorId === void 0) {
108
+ return action(...args);
109
+ }
110
+ const auditContext = { actorId };
111
+ return (0, import_audit_core.runWithAuditContext)(auditContext, () => action(...args));
112
+ };
113
+ }
114
+ // Annotate the CommonJS export names for ESM import in node:
115
+ 0 && (module.exports = {
116
+ AUDIT_ACTOR_HEADER,
117
+ betterAuditNext,
118
+ createAuditMiddleware,
119
+ fromBearerToken,
120
+ fromCookie,
121
+ fromHeader,
122
+ getAuditContext,
123
+ runWithAuditContext,
124
+ withAudit,
125
+ withAuditRoute
126
+ });
127
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,140 @@
1
+ import { ContextExtractor } from '@usebetterdev/audit-core';
2
+ export { AuditContext, ContextExtractor, ValueExtractor, fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from '@usebetterdev/audit-core';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+
5
+ type RouteContext = Record<string, unknown> | undefined;
6
+ type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;
7
+ /**
8
+ * Shared options across all audit wrappers.
9
+ */
10
+ interface BaseAuditOptions {
11
+ /** Context extractor for pulling actor identity from the request. */
12
+ extractor?: ContextExtractor;
13
+ /** Called when an extractor throws. Defaults to silent fail-open. */
14
+ onError?: (error: unknown) => void;
15
+ }
16
+ interface CreateAuditMiddlewareOptions extends BaseAuditOptions {
17
+ /**
18
+ * Request header name used to forward the extracted actor id to downstream
19
+ * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.
20
+ */
21
+ actorHeader?: string;
22
+ }
23
+ type WithAuditRouteOptions = BaseAuditOptions;
24
+ type WithAuditOptions = BaseAuditOptions;
25
+ /**
26
+ * Default header name used to forward the actor id from middleware to route
27
+ * handlers.
28
+ *
29
+ * Use this constant to keep the middleware and route handler in sync:
30
+ * ```ts
31
+ * // middleware.ts
32
+ * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER
33
+ *
34
+ * // app/api/orders/route.ts
35
+ * export const GET = withAuditRoute(handler, {
36
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
37
+ * });
38
+ * ```
39
+ */
40
+ declare const AUDIT_ACTOR_HEADER = "x-better-audit-actor-id";
41
+ /**
42
+ * Creates a Next.js edge middleware function that extracts audit context from
43
+ * the incoming request and forwards the actor id as a request header to
44
+ * downstream route handlers.
45
+ *
46
+ * **Important:** Next.js middleware runs in a separate Edge Runtime execution
47
+ * context from route handlers — AsyncLocalStorage set here does NOT propagate.
48
+ * This middleware forwards the extracted actor id via a request header
49
+ * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route
50
+ * handlers to read that header and populate ALS.
51
+ *
52
+ * The middleware **always** overwrites the actor header on the forwarded
53
+ * request — when extraction fails, it is set to an empty string. This prevents
54
+ * clients from spoofing the actor id by sending the header directly.
55
+ *
56
+ * The middleware is non-blocking: if context extraction fails, the request
57
+ * proceeds with an empty actor header (fail open).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // middleware.ts
62
+ * import { createAuditMiddleware } from "@usebetterdev/audit-next";
63
+ * export default createAuditMiddleware();
64
+ * export const config = { matcher: "/api/:path*" };
65
+ *
66
+ * // app/api/orders/route.ts
67
+ * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from "@usebetterdev/audit-next";
68
+ * export const GET = withAuditRoute(handler, {
69
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
70
+ * });
71
+ * ```
72
+ */
73
+ declare function createAuditMiddleware(options?: CreateAuditMiddlewareOptions): NextMiddleware;
74
+ /**
75
+ * Convenience alias for `createAuditMiddleware`. Use as the default export
76
+ * in `middleware.ts`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // middleware.ts
81
+ * import { betterAuditNext } from "@usebetterdev/audit-next";
82
+ * export default betterAuditNext();
83
+ * ```
84
+ */
85
+ declare function betterAuditNext(options?: CreateAuditMiddlewareOptions): NextMiddleware;
86
+ /**
87
+ * Wraps a Next.js App Router route handler with audit context propagation.
88
+ *
89
+ * Extracts the actor identity from the incoming request (by default reads
90
+ * `sub` from `Authorization: Bearer <jwt>`, or reads the
91
+ * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and
92
+ * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`
93
+ * returns the correct context within the handler.
94
+ *
95
+ * `NextRequest` is a Web `Request` subclass — extractors from audit-core
96
+ * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.
97
+ *
98
+ * The wrapper is non-blocking: if extraction fails, the handler runs without
99
+ * audit context (fail open).
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * // app/api/orders/route.ts — standalone (no middleware)
104
+ * import { withAuditRoute } from "@usebetterdev/audit-next";
105
+ * export const GET = withAuditRoute(handler);
106
+ *
107
+ * // app/api/orders/route.ts — paired with createAuditMiddleware
108
+ * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from "@usebetterdev/audit-next";
109
+ * export const GET = withAuditRoute(handler, {
110
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
111
+ * });
112
+ * ```
113
+ */
114
+ declare function withAuditRoute<C extends RouteContext = RouteContext>(handler: (request: NextRequest, context: C) => Promise<Response>, options?: WithAuditRouteOptions): (request: NextRequest, context: C) => Promise<Response>;
115
+ /**
116
+ * Wraps a Next.js server action with audit context propagation.
117
+ *
118
+ * Server actions do not receive a `Request` object. This wrapper reads all
119
+ * request headers via `next/headers` and constructs a synthetic `Request` to
120
+ * run the configured extractor against, then wraps the action in an
121
+ * AsyncLocalStorage scope so that `getAuditContext()` returns the correct
122
+ * context within the action.
123
+ *
124
+ * The wrapper is non-blocking: if extraction fails (including when `headers()`
125
+ * is unavailable), the action runs without audit context (fail open).
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // app/actions.ts
130
+ * "use server";
131
+ * import { withAudit } from "@usebetterdev/audit-next";
132
+ *
133
+ * export const createOrder = withAudit(async (formData: FormData) => {
134
+ * // getAuditContext() returns the actor here
135
+ * });
136
+ * ```
137
+ */
138
+ declare function withAudit<TArgs extends unknown[], TResult>(action: (...args: TArgs) => Promise<TResult>, options?: WithAuditOptions): (...args: TArgs) => Promise<TResult>;
139
+
140
+ export { AUDIT_ACTOR_HEADER, type BaseAuditOptions, type CreateAuditMiddlewareOptions, type NextMiddleware, type RouteContext, type WithAuditOptions, type WithAuditRouteOptions, betterAuditNext, createAuditMiddleware, withAudit, withAuditRoute };
@@ -0,0 +1,140 @@
1
+ import { ContextExtractor } from '@usebetterdev/audit-core';
2
+ export { AuditContext, ContextExtractor, ValueExtractor, fromBearerToken, fromCookie, fromHeader, getAuditContext, runWithAuditContext } from '@usebetterdev/audit-core';
3
+ import { NextRequest, NextResponse } from 'next/server';
4
+
5
+ type RouteContext = Record<string, unknown> | undefined;
6
+ type NextMiddleware = (request: NextRequest) => Promise<NextResponse>;
7
+ /**
8
+ * Shared options across all audit wrappers.
9
+ */
10
+ interface BaseAuditOptions {
11
+ /** Context extractor for pulling actor identity from the request. */
12
+ extractor?: ContextExtractor;
13
+ /** Called when an extractor throws. Defaults to silent fail-open. */
14
+ onError?: (error: unknown) => void;
15
+ }
16
+ interface CreateAuditMiddlewareOptions extends BaseAuditOptions {
17
+ /**
18
+ * Request header name used to forward the extracted actor id to downstream
19
+ * route handlers. Defaults to `AUDIT_ACTOR_HEADER`.
20
+ */
21
+ actorHeader?: string;
22
+ }
23
+ type WithAuditRouteOptions = BaseAuditOptions;
24
+ type WithAuditOptions = BaseAuditOptions;
25
+ /**
26
+ * Default header name used to forward the actor id from middleware to route
27
+ * handlers.
28
+ *
29
+ * Use this constant to keep the middleware and route handler in sync:
30
+ * ```ts
31
+ * // middleware.ts
32
+ * export default createAuditMiddleware(); // sets AUDIT_ACTOR_HEADER
33
+ *
34
+ * // app/api/orders/route.ts
35
+ * export const GET = withAuditRoute(handler, {
36
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
37
+ * });
38
+ * ```
39
+ */
40
+ declare const AUDIT_ACTOR_HEADER = "x-better-audit-actor-id";
41
+ /**
42
+ * Creates a Next.js edge middleware function that extracts audit context from
43
+ * the incoming request and forwards the actor id as a request header to
44
+ * downstream route handlers.
45
+ *
46
+ * **Important:** Next.js middleware runs in a separate Edge Runtime execution
47
+ * context from route handlers — AsyncLocalStorage set here does NOT propagate.
48
+ * This middleware forwards the extracted actor id via a request header
49
+ * (`x-better-audit-actor-id` by default). Use `withAuditRoute` in your route
50
+ * handlers to read that header and populate ALS.
51
+ *
52
+ * The middleware **always** overwrites the actor header on the forwarded
53
+ * request — when extraction fails, it is set to an empty string. This prevents
54
+ * clients from spoofing the actor id by sending the header directly.
55
+ *
56
+ * The middleware is non-blocking: if context extraction fails, the request
57
+ * proceeds with an empty actor header (fail open).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // middleware.ts
62
+ * import { createAuditMiddleware } from "@usebetterdev/audit-next";
63
+ * export default createAuditMiddleware();
64
+ * export const config = { matcher: "/api/:path*" };
65
+ *
66
+ * // app/api/orders/route.ts
67
+ * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from "@usebetterdev/audit-next";
68
+ * export const GET = withAuditRoute(handler, {
69
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
70
+ * });
71
+ * ```
72
+ */
73
+ declare function createAuditMiddleware(options?: CreateAuditMiddlewareOptions): NextMiddleware;
74
+ /**
75
+ * Convenience alias for `createAuditMiddleware`. Use as the default export
76
+ * in `middleware.ts`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // middleware.ts
81
+ * import { betterAuditNext } from "@usebetterdev/audit-next";
82
+ * export default betterAuditNext();
83
+ * ```
84
+ */
85
+ declare function betterAuditNext(options?: CreateAuditMiddlewareOptions): NextMiddleware;
86
+ /**
87
+ * Wraps a Next.js App Router route handler with audit context propagation.
88
+ *
89
+ * Extracts the actor identity from the incoming request (by default reads
90
+ * `sub` from `Authorization: Bearer <jwt>`, or reads the
91
+ * `x-better-audit-actor-id` header forwarded by `createAuditMiddleware`) and
92
+ * runs the handler inside an AsyncLocalStorage scope so that `getAuditContext()`
93
+ * returns the correct context within the handler.
94
+ *
95
+ * `NextRequest` is a Web `Request` subclass — extractors from audit-core
96
+ * (`fromBearerToken`, `fromHeader`, `fromCookie`) work directly.
97
+ *
98
+ * The wrapper is non-blocking: if extraction fails, the handler runs without
99
+ * audit context (fail open).
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * // app/api/orders/route.ts — standalone (no middleware)
104
+ * import { withAuditRoute } from "@usebetterdev/audit-next";
105
+ * export const GET = withAuditRoute(handler);
106
+ *
107
+ * // app/api/orders/route.ts — paired with createAuditMiddleware
108
+ * import { withAuditRoute, fromHeader, AUDIT_ACTOR_HEADER } from "@usebetterdev/audit-next";
109
+ * export const GET = withAuditRoute(handler, {
110
+ * extractor: { actor: fromHeader(AUDIT_ACTOR_HEADER) },
111
+ * });
112
+ * ```
113
+ */
114
+ declare function withAuditRoute<C extends RouteContext = RouteContext>(handler: (request: NextRequest, context: C) => Promise<Response>, options?: WithAuditRouteOptions): (request: NextRequest, context: C) => Promise<Response>;
115
+ /**
116
+ * Wraps a Next.js server action with audit context propagation.
117
+ *
118
+ * Server actions do not receive a `Request` object. This wrapper reads all
119
+ * request headers via `next/headers` and constructs a synthetic `Request` to
120
+ * run the configured extractor against, then wraps the action in an
121
+ * AsyncLocalStorage scope so that `getAuditContext()` returns the correct
122
+ * context within the action.
123
+ *
124
+ * The wrapper is non-blocking: if extraction fails (including when `headers()`
125
+ * is unavailable), the action runs without audit context (fail open).
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // app/actions.ts
130
+ * "use server";
131
+ * import { withAudit } from "@usebetterdev/audit-next";
132
+ *
133
+ * export const createOrder = withAudit(async (formData: FormData) => {
134
+ * // getAuditContext() returns the actor here
135
+ * });
136
+ * ```
137
+ */
138
+ declare function withAudit<TArgs extends unknown[], TResult>(action: (...args: TArgs) => Promise<TResult>, options?: WithAuditOptions): (...args: TArgs) => Promise<TResult>;
139
+
140
+ export { AUDIT_ACTOR_HEADER, type BaseAuditOptions, type CreateAuditMiddlewareOptions, type NextMiddleware, type RouteContext, type WithAuditOptions, type WithAuditRouteOptions, betterAuditNext, createAuditMiddleware, withAudit, withAuditRoute };
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ // src/index.ts
2
+ import {
3
+ fromBearerToken,
4
+ runWithAuditContext
5
+ } from "@usebetterdev/audit-core";
6
+ import { headers } from "next/headers";
7
+ import { NextResponse } from "next/server";
8
+ import { fromBearerToken as fromBearerToken2, fromCookie, fromHeader, getAuditContext as getAuditContext2, runWithAuditContext as runWithAuditContext2 } from "@usebetterdev/audit-core";
9
+ 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
+ function createAuditMiddleware(options = {}) {
27
+ const extractor = options.extractor ?? defaultExtractor;
28
+ const actorHeader = options.actorHeader ?? AUDIT_ACTOR_HEADER;
29
+ return async (request) => {
30
+ const actorId = await tryExtract(
31
+ extractor.actor,
32
+ request,
33
+ options.onError
34
+ );
35
+ const requestHeaders = new Headers(request.headers);
36
+ requestHeaders.set(actorHeader, actorId ?? "");
37
+ return NextResponse.next({
38
+ request: { headers: requestHeaders }
39
+ });
40
+ };
41
+ }
42
+ function betterAuditNext(options = {}) {
43
+ return createAuditMiddleware(options);
44
+ }
45
+ function withAuditRoute(handler, options = {}) {
46
+ const extractor = options.extractor ?? defaultExtractor;
47
+ return async (request, context) => {
48
+ const actorId = await tryExtract(
49
+ extractor.actor,
50
+ request,
51
+ options.onError
52
+ );
53
+ if (actorId === void 0) {
54
+ return handler(request, context);
55
+ }
56
+ const auditContext = { actorId };
57
+ return runWithAuditContext(auditContext, () => handler(request, context));
58
+ };
59
+ }
60
+ function withAudit(action, options = {}) {
61
+ const extractor = options.extractor ?? defaultExtractor;
62
+ return async (...args) => {
63
+ let actorId;
64
+ try {
65
+ const headersList = await headers();
66
+ const syntheticRequest = new Request("http://localhost", {
67
+ headers: headersList
68
+ });
69
+ actorId = await tryExtract(
70
+ extractor.actor,
71
+ syntheticRequest,
72
+ options.onError
73
+ );
74
+ } catch (error) {
75
+ options.onError?.(error);
76
+ }
77
+ if (actorId === void 0) {
78
+ return action(...args);
79
+ }
80
+ const auditContext = { actorId };
81
+ return runWithAuditContext(auditContext, () => action(...args));
82
+ };
83
+ }
84
+ export {
85
+ AUDIT_ACTOR_HEADER,
86
+ betterAuditNext,
87
+ createAuditMiddleware,
88
+ fromBearerToken2 as fromBearerToken,
89
+ fromCookie,
90
+ fromHeader,
91
+ getAuditContext2 as getAuditContext,
92
+ runWithAuditContext2 as runWithAuditContext,
93
+ withAudit,
94
+ withAuditRoute
95
+ };
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@usebetterdev/audit-next",
3
+ "version": "0.6.0",
4
+ "repository": "github:usebetter-dev/usebetter",
5
+ "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
+ "homepage": "https://github.com/usebetter-dev/usebetter#readme",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
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
+ "dependencies": {
34
+ "@usebetterdev/audit-core": "workspace:*"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=14.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.0",
41
+ "next": "^15.5.2",
42
+ "tsup": "^8.3.5",
43
+ "typescript": "~5.7.2",
44
+ "vitest": "^2.1.6"
45
+ },
46
+ "engines": {
47
+ "node": ">=22"
48
+ }
49
+ }