Skip to content

Latest commit

 

History

134 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@bodanglin/verdict-node — TypeScript Gateway Adapter

npm TypeScript License CI

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.


ExecutionEnvelope Verification

This package includes canonical ExecutionEnvelope v1 verification following the verdict-core contract specification.

Verification Rules

The verifier implements fail-closed validation:

  1. Schema Validation: Rejects unknown fields (strict v1 contract)
  2. Eligibility: admitted must be true with no contradictory deny signals
  3. Digest Check: policy_digest must match the expected value
  4. Expiry Check: execution_constraints.expires_at is 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.

Usage

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
}

Canonical Fixtures

The package vendors canonical test fixtures from verdict-core at SHA 15d1f8f9edcd37250655331a425a07d5767a98eb.

See contracts/fixtures/execution-envelope/v1/README.md for details.

What is @bodanglin/verdict-node?

@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.


Status

Alpha — not production-ready. Current implementation provides:

  • Fail-closed ExecutionEnvelope validation 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

Supported TypeScript toolchain

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.


Install

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-node

Peer dependency: express@>=5.0.0 <6 only when using Express middleware. Next.js /api routes can use the generic handler without mounting Express.


Quick Start

Express standalone forwarder

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.

Next.js /api route (fail-closed)

// 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.


Configuration

ForwarderConfig

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));

GatewayConfig

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);

API

createForwarder(config: ForwarderConfig): express.RequestHandler

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.

createNextApiHandler(config: GatewayConfig): NextApiHandlerLike

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.

Types

import type { GatewayConfig, OpenAIChatCompletionRequest } from '@bodanglin/verdict-node';
import type {
  ForwarderConfig,
  OpenAIChatCompletionResponse,
  OpenAIChatCompletionChunk,
} from '@bodanglin/verdict-node/middleware';

Integration with Verdict Core

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.


Development

# 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:package

Project Structure

verdict-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

Ecosystem

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)

Links


License

MIT — see LICENSE

CJS Support

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.

About

TypeScript gateway adapter for Express and Next.js: enforces Verdict Core decisions and execution envelopes at the HTTP edge (fail-closed). OpenAI-compatible forwarding. Alpha.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages