Skip to content
v0.11.2Since v0.10.8

Express Middleware

Add verifiable interaction records to Express.js APIs. The middleware intercepts responses, signs a PEAC receipt, and attaches it via the PEAC-Receipt HTTP header. Automatic issuance on every governed response.

Packages: @peac/middleware-express, @peac/middleware-core

Setup

app.tsTypeScript
import { peacMiddleware } from '@peac/middleware-express';
import express from 'express';

const app = express();

app.use(peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
}));

app.get('/data', (req, res) => {
  res.json({ value: 42 });
  // PEAC-Receipt header is attached automatically
});

Install

pnpm add @peac/middleware-express @peac/middleware-core

How It Works

1. Intercept

The middleware hooks into the Express response lifecycle. When a response is about to be sent, it captures the route, method, status code, and content metadata.

2. Sign

A PEAC receipt is issued using the configured issuer identity and Ed25519 signing key. The receipt contains the interaction evidence as a compact JWS (peac-receipt/0.1 type).

3. Attach

The compact JWS is set as the PEAC-Receipt HTTP response header. The original response body and status code are unchanged.

Configuration Options

OptionTypeRequiredDescription
issuerstringYesIssuer URI for the iss claim in issued receipts
signingKeyEd25519 private keyYesEd25519 private key (JWK or raw bytes) for receipt signing
audiencestringNoDefault audience for the aud claim; can be overridden per route
routesstring | RegExp | ArrayNoPattern filter; only matching routes receive receipts. Defaults to all routes.

Route-Level Overrides

Individual routes can override the global middleware configuration by attaching options to the request object. This allows different audiences, purposes, or receipt behavior per endpoint.

route-override.tsTypeScript
import { peacMiddleware, setPeacOptions } from '@peac/middleware-express';

app.use(peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
}));

// Override audience for a specific route
app.get('/partner/data', (req, res) => {
  setPeacOptions(req, {
    audience: 'partner.example.com',
  });
  res.json({ value: 42 });
});

// Disable receipt issuance for a specific route
app.get('/health', (req, res) => {
  setPeacOptions(req, { skip: true });
  res.json({ status: 'ok' });
});

Route Filtering

The routes option controls which paths receive receipts. Unmatched routes pass through without receipt overhead.

route-filter.tsTypeScript
app.use(peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
  routes: ['/api/v1/*', '/data/*'],
}));

// /api/v1/users   -> receipt attached
// /data/export    -> receipt attached
// /health         -> no receipt (not matched)
// /docs           -> no receipt (not matched)

Error Isolation

Middleware failures do not block API responses. If receipt signing fails (key unavailable, malformed input, internal error), the response is sent without a PEAC-Receipt header. The error is emitted via the standard Express error event for logging and monitoring, but the API consumer receives their response uninterrupted.

error-handling.tsTypeScript
import { peacMiddleware, PeacMiddlewareError } from '@peac/middleware-express';

const middleware = peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
  onError: (err: PeacMiddlewareError) => {
    console.error('Receipt issuance failed:', err.code, err.message);
    // Response is still sent; receipt is omitted
  },
});

app.use(middleware);

Packages

The Express middleware is split into two packages with distinct responsibilities:

@peac/middleware-express

Layer 3.5: Express Binding

Express-specific middleware function. Hooks into the response lifecycle, applies route filtering, and attaches the PEAC-Receipt header.

@peac/middleware-core

Layer 3.5: Framework-Agnostic Core

Receipt issuance logic, configuration validation, and error handling. Framework-agnostic; can be used as a foundation for other HTTP framework adapters.

Links

Verifiable APIs in Three Lines

Every governed response carries a signed receipt. Consumers can verify the interaction independently using the issuer's public key, without trusting the API server. The middleware handles signing, header attachment, and error isolation; your route handlers remain unchanged.