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
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
| Option | Type | Required | Description |
|---|---|---|---|
issuer | string | Yes | Issuer URI for the iss claim in issued receipts |
signingKey | Ed25519 private key | Yes | Ed25519 private key (JWK or raw bytes) for receipt signing |
audience | string | No | Default audience for the aud claim; can be overridden per route |
routes | string | RegExp | Array | No | Pattern 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.
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.
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.
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.