npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

express-audit-context

v0.1.4

Published

Request-scoped audit context and middleware for Express 4 and 5

Readme

express-audit-context

Request-scoped, JSON-safe audit events for Express 4 and 5. One middleware creates a context for each request, makes it available throughout asynchronous application work, and writes one event when the response completes.

It also includes optional, framework-agnostic browser helpers for fetch and Axios. They assign a fresh request ID to every browser request and let an explicitly scoped group of requests share a trace ID.

Install

This package supports Node.js 18+, ESM and CommonJS, Express 4.22.0 and later 4.x releases, and Express 5.2.1 and later 5.x releases.

npm install express express-audit-context
# or
pnpm add express express-audit-context

express is a required peer dependency. The Axios helper accepts an Axios-compatible instance but Axios is not bundled or required by this package.

Express quick start

Install the middleware before your routes and provide a sink. A sink chooses where completed audit events go: a log, queue, database, or external audit service.

import express from "express";
import {
  auditMiddleware,
  getAuditContext,
  type AuditEvent,
} from "express-audit-context";

const app = express();

app.use(auditMiddleware({
  sink: {
    write(event: AuditEvent): void {
      console.log(JSON.stringify(event));
    },
  },
}));

app.get("/orders/:id", async (request, response) => {
  const context = getAuditContext();
  context?.add({
    orderId: request.params.id,
    source: "orders-api",
  });

  await context?.withSpan("load order", async () => {
    await loadOrder(request.params.id);
  });

  response.json({ id: request.params.id });
});

app.listen(3000);

getAuditContext() returns the active context in work initiated by that request, including asynchronous work. It returns undefined outside an audit scope, so it is safe to use in shared services with optional chaining.

CommonJS applications use the same named API with require:

const express = require("express");
const { auditMiddleware, getAuditContext } = require("express-audit-context");

const app = express();
app.use(auditMiddleware({ sink: { write: (event) => console.log(event) } }));
app.get("/health", (_request, response) => {
  getAuditContext()?.set("route", "health");
  response.sendStatus(204);
});

The browser subpaths also accept require, for example require("express-audit-context/browser/fetch"). Within one process, import the package consistently through ESM or CommonJS when sharing an audit context or package-wide privacy settings; each format has its own module instance.

The middleware accepts incoming x-request-id and x-trace-id headers, validates them, and returns the resolved values in the response headers. Missing, invalid, or repeated values are replaced with UUIDs. They are correlation tokens only; they do not authenticate a caller.

Events and sinks

Each completed response produces one JSON-safe AuditEvent:

{
  "version": 1,
  "traceId": "checkout-flow-7",
  "requestId": "7d4fd446-ec88-43a1-a1d4-12f73d74be50",
  "startedAt": "2026-09-25T10:00:00.000Z",
  "endedAt": "2026-09-25T10:00:00.012Z",
  "durationMs": 12,
  "http": {
    "method": "GET",
    "path": "/orders/order-123",
    "statusCode": 200,
    "outcome": "completed"
  },
  "meta": { "orderId": "order-123" },
  "spans": [
    {
      "name": "load order",
      "startedAt": "2026-09-25T10:00:00.002Z",
      "atMs": 2,
      "endedAt": "2026-09-25T10:00:00.009Z",
      "durationMs": 7,
      "attributes": {}
    }
  ]
}

finish yields an outcome of completed; a connection that closes before completion yields aborted. Sink calls are deliberately not awaited, so a slow or failed sink cannot delay the response. Use onSinkError to observe synchronous throws or rejected sink promises.

Middleware configuration

app.use(auditMiddleware({
  sink,
  traceIdHeader: "x-trace-id",
  requestIdHeader: "x-request-id",
  sensitiveFields: ["paymentToken"],
  sensitiveValueMask: "[REDACTED]",
  detectSensitiveValues: true,
  excludePaths: ["/health", "/metrics", /^\/internal\//],
  requestMetadata: (request) => ({
    caller: request.user ? { id: request.user.id, roles: request.user.roles } : undefined,
  }),
  requestPayload: (request) => request.body,
  onSinkError(error, event) {
    reportAuditSinkFailure(error, event);
  },
  cors: {
    origins: ["https://app.example.com"],
    credentials: true,
    allowedMethods: ["GET", "POST", "OPTIONS"],
    allowedHeaders: ["x-tenant-id"],
    exposedHeaders: ["x-rate-limit-remaining"],
  },
}));

| Option | Default | Purpose | | --- | --- | --- | | sink | required | Receives each finalized AuditEvent. | | traceIdHeader | "x-trace-id" | Request/response header for optional workflow correlation. | | requestIdHeader | "x-request-id" | Request/response header for per-request correlation. | | sensitiveFields | [] | Additional metadata or span-attribute field names to mask. | | sensitiveValueMask | "[REDACTED]" | Replacement text for masked values. | | detectSensitiveValues | true | Enables built-in sensitive-name and credential-value recognition. Set false to mask only sensitiveFields. | | excludePaths | [] | Exact paths or regular expressions that never create an audit event. | | requestMetadata | unset | Synchronously adds a sanitized projection supplied by the application's own authentication/context middleware. | | requestPayload | unset | Synchronously stores an opt-in projection from data that earlier application middleware made available, usually request.body. | | onSinkError | unset | Observes an isolated sink error after response completion. | | cors | unset | Explicit browser CORS policy and correlation-header support. |

CORS

Set cors only when this middleware should own the route's CORS policy. It responds to permitted preflight requests and automatically includes the configured request-ID and trace-ID headers in allowed/exposed headers.

origins is required when cors is enabled. Use exact origins for authenticated applications. origins: ["*"] is supported for public, non-credentialed APIs only; it throws when combined with credentials: true.

allowedMethods, allowedHeaders, and exposedHeaders append to the middleware's necessary defaults. credentials defaults to false.

Metadata and spans

Metadata captures final request facts. Spans capture execution points and durations relative to the beginning of the request. Both are emitted with the final event.

const context = getAuditContext();

context?.set("tenantId", tenantId);
context?.add({ operation: "checkout", itemCount: 3 });

// Immutable point in the request timeline; it needs no manual close.
context?.addSpan("validated input", { schema: "checkout" });

// Preferred timed span: it ends on success, throw, or promise rejection.
await context?.withSpan("charge card", async (span) => {
  span.set("provider", "stripe");
  await chargeCard();
});

// Manual timing is available when its lifecycle genuinely cannot be wrapped.
const publish = context?.startSpan("publish receipt");
try {
  await publishReceipt();
} finally {
  publish?.end();
}

withSpan is the usual choice because it is fail-safe. Any unfinished manual span is ended when the request context ends.

Sensitive-data masking

Masking occurs at event serialization, immediately before sink delivery. It covers metadata and span attributes without changing the raw in-process context values.

Built-in recognition covers common names such as authorization, cookie, password, token, accessToken, refreshToken, apiKey, secret, and clientSecret. When enabled, it also recognizes conservative whole-value formats including Bearer tokens, JWTs, PEM private keys, AWS access keys, and GitHub tokens.

Configure package-wide defaults once, then optionally add or override settings per middleware or standalone context:

import { configureAuditPrivacy } from "express-audit-context";

configureAuditPrivacy({
  sensitiveFields: ["paymentToken", "nationalId"],
  sensitiveValueMask: "[MASKED]",
});

app.use(auditMiddleware({
  sink,
  sensitiveFields: ["internalSecret"],
}));

This is an audit-output safeguard, not a substitute for avoiding sensitive collection, securing sinks, or access controls. Set detectSensitiveValues: false when your policy is to mask exactly the sensitiveFields names and nothing else; it disables both built-in sensitive-name matching and credential-value recognition.

Request metadata, callers, and payloads

requestMetadata and requestPayload are integration hooks, not built-in authentication or body-capture. This package does not decode JWTs, validate sessions, call Keycloak or another identity provider, load users from a database, parse request bodies, or add user/auth properties to Express requests. What is available in either resolver depends entirely on the middleware your application registered before auditMiddleware.

Put body parsing and authentication before auditMiddleware, then return a sanitized, JSON-safe projection from requestMetadata. The same API works across authentication strategies, but each application chooses where the trusted principal comes from:

| Application authentication | Typical earlier middleware responsibility | What requestMetadata reads | | --- | --- | --- | | Stateless JWT / API key | Verify the credential and attach selected claims. | request.auth, request.user, or another app-defined property. | | Server-side session | Restore the session and load/deserialize the user. | request.session, request.user, or an app-defined property. | | External provider (Keycloak, Auth0, gateway) | Verify or trust its token/header, then attach a minimal principal. | The application's normalized principal, not a raw provider token. | | In-app authentication | Validate credentials and resolve the local account. | A selected account ID, roles, tenant, or similar safe fields. |

There is intentionally no preferred property name or provider adapter. This keeps the package independent of both stateful and stateless authentication libraries.

requestPayload does not read or parse the body itself. It is deliberately opt-in and typically returns a selected subset of request.body after an earlier body parser has run. Its result is added as event.meta.payload and passes through the same recursive masking as other metadata. Do not return raw authorization headers, tokens, passwords, or unrestricted bodies; select the debugging fields you need and add their sensitive names to sensitiveFields.

app.use(express.json());
app.use(verifyJwtAndAttachPrincipal); // for example, sets request.auth
app.use(auditMiddleware({
  sink,
  sensitiveFields: ["paymentToken", "nationalId"],
  requestMetadata(request) {
    const auth = request.auth as { sub: string; realm_access?: { roles?: string[] } } | undefined;
    return auth ? { caller: { subject: auth.sub, roles: auth.realm_access?.roles ?? [] } } : undefined;
  },
  requestPayload(request) {
    const body = request.body as { orderId?: string; paymentToken?: string } | undefined;
    return body && { orderId: body.orderId, paymentToken: body.paymentToken };
  },
}));

Both resolvers are synchronous and run once at the beginning of the audit scope. If session restoration, token verification, or an identity lookup is asynchronous, complete it in earlier authentication middleware, attach the minimal resolved principal to the request, and then use requestMetadata. A sink's write(event) receives only the finalized event, not the Express request, so resolving authentication there is too late and couples delivery to request-specific dependencies.

Browser request correlation

Browser helpers add headers only. They do not collect browser telemetry, send events, or maintain a global trace state. This keeps the browser entry points small and makes an accidental leaked trace impossible.

Every request gets a new request ID. A trace ID is sent only inside an explicit traceRequests callback. Pass a second argument when you already have a trace ID; otherwise one is generated for that one callback.

Both browser helpers accept these options; fetch additionally accepts fetch when an application needs to provide its own implementation.

| Option | Default | Purpose | | --- | --- | --- | | traceIdHeader | "x-trace-id" | Header name used only for scoped trace correlation. | | requestIdHeader | "x-request-id" | Header name sent on every request. | | generateId | crypto.randomUUID | Function that produces request and generated trace IDs. | | fetch (fetch helper only) | globalThis.fetch | Fetch implementation to wrap. |

Fetch

import { createAuditFetch } from "express-audit-context/browser/fetch";

const auditFetch = createAuditFetch({
  // Optional: traceIdHeader, requestIdHeader, generateId, fetch
});

await auditFetch.fetch("/api/health"); // request ID only

await auditFetch.traceRequests(async (fetch) => {
  await fetch("/api/checkout/initial");
  await fetch("/api/checkout/poll");
  await fetch("/api/checkout/result");
}, "checkout-flow-7"); // Optional caller-provided trace ID

Use the scoped fetch argument for every request that belongs to the workflow. The exported auditFetch.fetch stays untraced.

Axios, including generated TypeScript-Axios clients

Axios support is independent of the fetch helper. Install it once on the same Axios instance your application already uses—often global Axios configured in main.ts—then export the returned helper:

import axios from "axios";
import { installAuditAxios } from "express-audit-context/browser/axios";

axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL;

export const auditAxios = installAuditAxios(axios, {
  // Optional: traceIdHeader, requestIdHeader, generateId
});

The installation adds a request interceptor that gives every Axios request a fresh request ID. It works unchanged with generated TypeScript-Axios clients that use that global instance:

// Existing generated client and service functions need no changes.
const api = new BillableApi();

await auditAxios.traceRequests(async (request) => {
  const billables = await request(() => api.listBillablesId({ id }));
  await request(() => api.getBillableById({
    billableId: billables.data.items[0].id,
  }));
});

Wrap each generated-client or manual Axios call with request(() => ...). The trace header is temporarily applied only while Axios starts that operation, then restored immediately. Independent or overlapping workflows therefore cannot inherit the trace ID.

Angular-generated clients

Generated TypeScript-Angular clients use Angular HttpClient, not fetch or Axios. Initialize a small application HttpContextToken plus functional interceptor. The interceptor assigns x-request-id to every request and reads an optional trace ID from that call's HttpContext; no Angular dependency is added to this package.

// audit-http.ts
import { HttpContextToken, HttpInterceptorFn } from "@angular/common/http";

export const AUDIT_TRACE_ID = new HttpContextToken<string | undefined>(() => undefined);

export const auditHttpInterceptor: HttpInterceptorFn = (request, next) => {
  const traceId = request.context.get(AUDIT_TRACE_ID);
  return next(request.clone({ setHeaders: {
    "x-request-id": crypto.randomUUID(),
    ...(traceId ? { "x-trace-id": traceId } : {}),
  } }));
};
// app.config.ts
import { provideHttpClient, withInterceptors } from "@angular/common/http";
import { auditHttpInterceptor } from "./audit-http";

export const appConfig = {
  providers: [provideHttpClient(withInterceptors([auditHttpInterceptor]))],
};
// Only the calls belonging to this workflow receive the trace ID.
api.createOrder(body, { context: new HttpContext().set(AUDIT_TRACE_ID, crypto.randomUUID()) });

This package intentionally does not export an Angular adapter, keeping Angular optional and the package framework-agnostic.

NestJS

Yes—when NestJS uses its default Express adapter, this is ordinary Express middleware and can be registered at application bootstrap:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
import { auditMiddleware } from "express-audit-context";

async function bootstrap() {
  const app = await NestFactory.create(AppModule); // default Express adapter

  app.use(auditMiddleware({
    sink: { write: (event) => console.log(JSON.stringify(event)) },
  }));

  await app.listen(3000);
}

void bootstrap();

Nest applications using FastifyAdapter are not supported by this Express middleware. A Fastify-specific adapter would be needed for that runtime.

Standalone contexts and custom adapters

The framework-independent core is also exported for jobs, workers, tests, or custom HTTP adapters:

import {
  AuditContext,
  createAuditEvent,
  getAuditContext,
  runWithAuditContext,
} from "express-audit-context";

const context = new AuditContext({ traceId: "batch-1", requestId: "job-42" });

await runWithAuditContext(context, async () => {
  getAuditContext()?.add({ job: "nightly-import" });
});

context.end();
const event = createAuditEvent(context, {
  method: "JOB",
  path: "/nightly-import",
  statusCode: 200,
  outcome: "completed",
});

Package boundaries

  • The root export contains the framework-independent core and Express middleware.
  • express-audit-context/browser/fetch contains only fetch correlation support.
  • express-audit-context/browser/axios contains only Axios correlation support.
  • express-audit-context/browser is an alias for the fetch entry point.
  • Audit events intentionally omit request and response bodies. Add only the metadata you need, and rely on masking for any values that may be sensitive.

License

ISC