Skip to content
v0.16.1Package available

Express Middleware

Add signed proof 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 (interaction-record+jwt format).

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
audienceExtractor(req) => stringNoDerives the audience for each request
skip(req) => booleanNoReturn true to exclude a request from receipt issuance

Route-Level Overrides

Per-request behavior is controlled through predicate options on the middleware config. audienceExtractor derives the audience from each request, and skip excludes requests from receipt issuance.

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

app.use(peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
  // Derive the audience per request
  audienceExtractor: (req) =>
    req.path.startsWith('/partner') ? 'partner.example.com' : 'agent.consumer.com',
  // Skip receipt issuance for selected requests
  skip: (req) => req.path === '/health',
}));

Route Filtering

The skip predicate controls which paths receive receipts. Returning true passes a request through without receipt overhead.

route-filter.tsTypeScript
app.use(peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
  skip: (req) => !req.path.startsWith('/api/'),
}));

// /api/v1/users   -> receipt attached
// /health         -> no receipt (skipped)
// /docs           -> no receipt (skipped)

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 } from '@peac/middleware-express';

const middleware = peacMiddleware({
  issuer: 'https://api.example.com',
  signingKey: process.env.PEAC_SIGNING_KEY,
  onError: (error, req, res) => {
    console.error('Receipt issuance failed:', error.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.