Safety-checking middleware for Express and Next.js apps that call an OpenAI-compatible API. In short: Verdict Core decides, verdict-node enforces at the HTTP edge — this package does not make policy decisions itself, it checks each outgoing request against a decision made elsewhere before letting it through.
This package includes canonical ExecutionEnvelope v1 verification following the verdict-core contract specification.
The verifier implements fail-closed validation:
- Schema Validation: Rejects unknown fields (strict v1 contract)
- Eligibility:
admittedmust betruewith no contradictory deny signals - Digest Check:
policy_digestmust match the expected value - Expiry Check:
execution_constraints.expires_atis REQUIRED- Missing expiry → EXPIRED (fail closed: envelopes MUST have bounded lifetime)
- Timezone-naive timestamps → EXPIRED
- Unparseable timestamps → EXPIRED
now >= expires_at→ EXPIRED
The verifier never throws on untrusted input. Malformed data returns REJECT_UNKNOWN.
import { verifyExecutionEnvelope, EnvelopeVerdict } from '@bodanglin/verdict-node';
const verdict = verifyExecutionEnvelope(envelope, {
now: '2024-01-15T12:30:00Z',
expectedPolicyDigest: 'a'.repeat(64),
});
if (verdict === EnvelopeVerdict.ACCEPT) {
// Envelope is valid
} else {
// Handle DENY, EXPIRED, DIGEST_MISMATCH, or REJECT_UNKNOWN
}The package vendors canonical test fixtures from verdict-core at SHA 15d1f8f9edcd37250655331a425a07d5767a98eb.
See contracts/fixtures/execution-envelope/v1/README.md for details.
@bodanglin/verdict-node is a TypeScript middleware library for Express and Next.js. In plain terms, it sits in front of your app's calls to an OpenAI-compatible API and checks each request before it goes out — it does not decide what is allowed; that is the job of Verdict Core (the Python control plane). This package's job is to enforce Core's decision at the HTTP edge: core decides, node enforces.
The mechanism it enforces against is called an ExecutionEnvelope — plain-language: a signed record of what Core has authorized for a given request. By default, the standalone Express forwarder rejects a request outright ("fail-closed") if it arrives without a valid envelope or fails a policy check. The canonical cross-language contract for that envelope between Core (Python) and Node (TypeScript) is still being reconciled, so this alpha must not be represented as complete end-to-end policy enforcement yet. Node also retains its own local classification, discovery, ranking, and fallback behavior for compatibility routing; those heuristics are separate from, and not a substitute for, Core's authorization.
Works with any OpenAI-compatible client: Claude Code, Codex, Cursor, Cline, Hermes, Agents SDK, raw HTTP.
Alpha — not production-ready. Current implementation provides:
- Fail-closed
ExecutionEnvelopevalidation in the standalone forwarder by default - A shared pre-forward envelope check for streaming and non-streaming requests when an envelope is supplied to the gateway
- Zod request/response schemas
- Heuristic criticality classification and model catalog discovery for compatibility routing
- Bounded fallback ladder for selected HTTP/network failures
- Explicit compatibility opt-outs for deployments that do not yet require Core decisions or envelopes
- Streaming SSE and non-streaming JSON forwarding
- In-memory score cache (process-local)
Missing (tracked on release board):
- Verified Ruflo/RuVector IntelligenceService integration
- Persistent learning / cross-process state
- Reliable live quota/headroom data
- Full OpenAI field preservation
- Complete adversarial streaming/fallback contract
This release supports TypeScript 5.9.x with ts-jest@29.4.x and Jest 30.
ts-jest@29.4.x declares typescript >=4.3 <7, so TypeScript 7 is not a
supported configuration for this package. Keep the compiler pinned to the
documented 5.9 line until a ts-jest release with an explicit TypeScript 7 peer
range is available and verified. Issue #14
tracks that upgrade; the supported ceiling is intentional rather than hidden
behind an install fallback.
Current published version: 0.2.0 (2026-09-26). Install from npm; build from source only if you need unreleased changes.
npm install @bodanglin/verdict-node
# or
pnpm add @bodanglin/verdict-node
# or
yarn add @bodanglin/verdict-nodePeer dependency: express@>=5.0.0 <6 only when using Express middleware. Next.js /api routes can use the generic handler without mounting Express.
import express from 'express';
import { createForwarder } from '@bodanglin/verdict-node/middleware';
const app = express();
app.use(express.json());
// Obtain these values from independent trusted Core outputs.
const coreEnvelope: unknown = await loadCoreEnvelope();
const trustedPolicyDigest = await loadTrustedPolicyDigest();
app.use(
'/v1',
createForwarder({
baseUrl: process.env.VERDICT_UPSTREAM ?? 'http://127.0.0.1:20132/v1',
apiKey: process.env.OMNIROUTE_API_KEY,
executionEnvelope: coreEnvelope,
expectedPolicyDigest: trustedPolicyDigest,
})
);
app.listen(3000, () => console.log('verdict-node listening on :3000'));executionEnvelope is currently configured on the middleware instance. Create or scope middleware instances so an envelope cannot be reused for unrelated requests, and derive trustedPolicyDigest from an independent trusted policy source rather than from the envelope itself. requireExecutionEnvelope defaults to true; setting it to false is an explicit compatibility opt-out, not Core-authorized execution.
// pages/api/chat/completions.ts
import { createNextApiHandler } from '@bodanglin/verdict-node';
export default createNextApiHandler({
baseUrl: process.env.OMNIROUTE_BASE_URL ?? 'http://127.0.0.1:20132/v1',
apiKey: process.env.OMNIROUTE_API_KEY,
decisionEndpoint: process.env.VERDICT_CORE_DECISION_ENDPOINT,
});Fail-closed handler: createNextApiHandler returns after middleware writes HTTP 503 or another refusal (headersSent or statusCode >= 400) and does not call proxy(). Envelope validation, ladder-model recheck, and policy-digest integrity remain fail-closed. Compatibility opt-out (requireCoreDecision: false) is explicit only.
import { createForwarder, type ForwarderConfig } from '@bodanglin/verdict-node/middleware';
const config: ForwarderConfig = {
baseUrl: process.env.VERDICT_UPSTREAM ?? 'http://127.0.0.1:20132/v1',
apiKey: process.env.OMNIROUTE_API_KEY,
executionEnvelope: coreEnvelope,
expectedPolicyDigest: trustedPolicyDigest,
timeoutMs: 30_000,
maxRetries: 3,
};
app.use('/v1', createForwarder(config));import { createNextApiHandler, type GatewayConfig } from '@bodanglin/verdict-node';
const config: GatewayConfig = {
baseUrl: process.env.OMNIROUTE_BASE_URL,
apiKey: process.env.OMNIROUTE_API_KEY,
decisionEndpoint: process.env.VERDICT_CORE_DECISION_ENDPOINT,
decisionTimeoutMs: 2_000,
};
export default createNextApiHandler(config);The standalone Express forwarder validates the configured envelope before its first upstream fetch. By default it rejects missing, invalid, expired, or out-of-bounds envelopes with machine-readable denial codes. It then forwards non-streaming JSON or streaming SSE responses without substituting the request model.
The higher-level gateway requests a Core routing decision by default (requireCoreDecision: true). If no decision is available, the decision is denied, times out, or is malformed, or no decision endpoint is configured, middleware writes HTTP 503 and the handler returns without calling proxy(), so nothing is forwarded upstream. The default path also refuses to forward without an envelope, rechecks locally substituted ladder models against the envelope, and refuses when the envelope policy digest does not match independent evidence. These cases are covered by regression tests in tests/router.test.ts. The compatibility opt-out (requireCoreDecision: false) is explicit only.
import type { GatewayConfig, OpenAIChatCompletionRequest } from '@bodanglin/verdict-node';
import type {
ForwarderConfig,
OpenAIChatCompletionResponse,
OpenAIChatCompletionChunk,
} from '@bodanglin/verdict-node/middleware';Verdict Core is the intended authority for policy-gated execution; Node is an edge and transport adapter. Core and Node do not yet share a fully reconciled, published ExecutionEnvelope contract or verified issuance-to-enforcement fixture. Until that work is complete, treat the envelope support here as partial enforcement rather than proof of end-to-end Core authorization.
For the higher-level gateway, point decisionEndpoint (or VERDICT_CORE_DECISION_ENDPOINT) at the Core routing-decision endpoint. The standalone forwarder instead accepts an envelope through ForwarderConfig.executionEnvelope and requires one by default. Both APIs expose explicit compatibility opt-outs; those modes are not policy-gated execution. createNextApiHandler is fail-closed after a refusal (see above). End-to-end parity still needs shared Core fixtures and a published, reconciled ExecutionEnvelope contract; see ADR-001.
# Install deps
npm install
# Type-check
npm run typecheck
# Lint
npm run lint
# Test
npm test
# Build
npm run build
# Verify package
npm run verify:packageverdict-node/
├── src/
│ ├── index.ts # Gateway and Next.js exports
│ ├── verifier.ts # ExecutionEnvelope v1 verifier
│ ├── adapters/
│ │ └── contract-to-middleware.ts # Canonical-decision adapter
│ └── middleware/
│ ├── index.ts # Middleware exports
│ ├── forwarder.ts # Express JSON/SSE forwarder
│ └── validator.ts # Validation helpers
├── tests/
├── scripts/ # Package verification
├── docs/adr/
└── package.json
| Package | Purpose |
|---|---|
verdict-core |
Python control plane |
@bodanglin/verdict-node |
Express/Next.js middleware (this repo) |
verdict-cockpit |
Next.js dashboard |
verdict-risk |
Risk engine |
verdict-strategy |
Signal feature evaluator |
verdict-backtest |
Monte Carlo harness |
| OmniRoute | Per OmniRoute's own description: 250+ providers, 90+ free tiers (third-party claim, not verified by this repository) |
- Verdict Core: https://github.com/mrnicholasbcarter-code/verdict-core
- Verdict Cockpit: https://github.com/mrnicholasbcarter-code/verdict-cockpit
- Issues: https://github.com/mrnicholasbcarter-code/verdict-node/issues
- Discord: https://discord.gg/verdict
MIT — see LICENSE
This package is ESM-only. CommonJS consumers can use require() on Node versions that support require(esm):
- Node >= 20.19.0
- Node >= 22.12.0
ESM (recommended):
import { verifyExecutionEnvelope } from '@bodanglin/verdict-node';CommonJS (requires Node >= 20.19 or >= 22.12):
const { verifyExecutionEnvelope } = require('@bodanglin/verdict-node');Older Node versions must use ESM imports or upgrade to a supported version.