Express Middleware

Sign it. Prove it. -- in your Express app.

JACS provides jacsMiddleware for Express v4/v5 that verifies incoming signed request bodies and optionally auto-signs JSON responses. No body-parser gymnastics, no monkey-patching.

5-Minute Quickstart

1. Install

npm install @hai.ai/jacs express

2. Create a JACS client

import { JacsClient } from '@hai.ai/jacs/client';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});

3. Add signing middleware

import express from 'express';
import { jacsMiddleware } from '@hai.ai/jacs/express';

const app = express();
app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client, verify: true }));

app.post('/api/data', (req, res) => {
  console.log(req.jacsPayload); // verified payload
  res.json({ status: 'ok' });
});

app.listen(3000);

Quick Start

import express from 'express';
import { JacsClient } from '@hai.ai/jacs/client';
import { jacsMiddleware } from '@hai.ai/jacs/express';

const client = await JacsClient.quickstart({
  name: 'my-agent',
  domain: 'my-agent.example.com',
});
const app = express();

app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client, verify: true }));

app.post('/api/data', (req, res) => {
  console.log(req.jacsPayload); // verified payload
  res.json({ status: 'ok' });
});

app.listen(3000);

Options

jacsMiddleware({
  client?: JacsClient;      // Pre-initialized client (preferred)
  configPath?: string;       // Path to jacs.config.json (if no client)
  sign?: boolean;            // Auto-sign res.json() responses (default: false)
  verify?: boolean;          // Verify incoming POST/PUT/PATCH bodies (default: true)
  optional?: boolean;        // Allow unsigned requests through (default: false)
  authReplay?: boolean | {   // Replay protection for auth-style endpoints (default: false)
    enabled?: boolean;
    maxAgeSeconds?: number;    // default: 30
    clockSkewSeconds?: number; // default: 5
    cacheTtlSeconds?: number;  // default: maxAge + skew
  };
})

If neither client nor configPath is provided, the middleware initializes a client with JacsClient.quickstart({ name: 'jacs-express', domain: 'localhost' }) on first request.

What the Middleware Does

Every request gets req.jacsClient -- a JacsClient instance you can use for manual signing/verification in route handlers.

POST/PUT/PATCH with verify: true (default): The string body is verified as a JACS document. On success, req.jacsPayload contains the extracted payload. On failure, a 401 is returned (unless optional: true).

With sign: true: res.json() is intercepted to auto-sign the response body before sending.

Verify Incoming Requests

app.use(express.text({ type: 'application/json' }));
app.use(jacsMiddleware({ client }));

app.post('/api/process', (req, res) => {
  if (!req.jacsPayload) {
    return res.status(400).json({ error: 'Missing payload' });
  }

  const { action, data } = req.jacsPayload;
  res.json({ processed: true, action });
});

With optional: true, unsigned requests pass through with req.jacsPayload unset:

app.use(jacsMiddleware({ client, optional: true }));

app.post('/api/mixed', (req, res) => {
  if (req.jacsPayload) {
    // Verified JACS request
    res.json({ verified: true, data: req.jacsPayload });
  } else {
    // Unsigned request -- handle accordingly
    res.json({ verified: false });
  }
});

Auth Replay Protection (Auth Endpoints)

Enable replay protection when signed JACS bodies are used as authentication artifacts:

app.use(
  jacsMiddleware({
    client,
    verify: true,
    authReplay: { enabled: true, maxAgeSeconds: 30, clockSkewSeconds: 5 },
  })
);

When enabled, middleware enforces:

  • signature timestamp freshness (maxAgeSeconds + clockSkewSeconds)
  • single-use (signerId, signature) dedupe inside a TTL cache

Notes:

  • Keep this mode scoped to auth-style endpoints.
  • Cache is in-memory per process; use a shared cache for multi-instance deployments.

Auto-Sign Responses

Enable sign: true to intercept res.json() calls:

app.use(jacsMiddleware({ client, sign: true }));

app.post('/api/data', (req, res) => {
  // This response will be JACS-signed automatically
  res.json({ result: 42, timestamp: new Date().toISOString() });
});

Manual Signing in Routes

Use req.jacsClient for fine-grained control:

app.use(jacsMiddleware({ client }));

app.post('/api/custom', async (req, res) => {
  const result = processData(req.jacsPayload);

  // Sign manually
  const signed = await req.jacsClient.signMessage(result);
  res.type('application/json').send(signed.raw);
});

Per-Route Middleware

Apply JACS to specific routes only:

const app = express();
const jacs = jacsMiddleware({ client });

// Public routes -- no JACS
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Protected routes
app.use('/api', express.text({ type: 'application/json' }), jacs);

app.post('/api/secure', (req, res) => {
  res.json({ data: req.jacsPayload });
});

Multiple Agents

Use different JacsClient instances per route group:

const adminClient = await JacsClient.quickstart({
  name: 'admin-agent',
  domain: 'admin.example.com',
  algorithm: 'pq2025',
});
const userClient = await JacsClient.quickstart({
  name: 'user-agent',
  domain: 'user.example.com',
  algorithm: 'ring-Ed25519',
});

app.use('/admin', express.text({ type: 'application/json' }));
app.use('/admin', jacsMiddleware({ client: adminClient }));

app.use('/user', express.text({ type: 'application/json' }));
app.use('/user', jacsMiddleware({ client: userClient }));

Migration from JACSExpressMiddleware

The legacy JACSExpressMiddleware from @hai.ai/jacs/http still works but is deprecated. To migrate:

OldNew
import { JACSExpressMiddleware } from '@hai.ai/jacs/http'import { jacsMiddleware } from '@hai.ai/jacs/express'
JACSExpressMiddleware({ configPath: '...' })jacsMiddleware({ configPath: '...' })
Per-request agent initShared client, lazy-loaded once
res.send() monkey-patchres.json() interception (opt-in)

The new middleware is simpler, faster (no per-request init), and gives you req.jacsClient for manual operations.

Next Steps