{"_id":"@bodanglin/verdict-node","_rev":"2-82d212bfe0df5339897f1ba063f924e7","name":"@bodanglin/verdict-node","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@bodanglin/verdict-node","version":"0.1.0","keywords":["llm","routing","ai","control-plane","safety","middleware","express","openai-compatible","zod"],"license":"MIT","_id":"@bodanglin/verdict-node@0.1.0","maintainers":[{"name":"beendanglin","email":"mr.nicholas.b.carter@gmail.com"}],"homepage":"https://github.com/mrnicholasbcarter-code/verdict-node#readme","bugs":{"url":"https://github.com/mrnicholasbcarter-code/verdict-node/issues"},"dist":{"shasum":"1df7c193ad8e17002265074760b04620a4a1ba65","tarball":"https://registry.npmjs.org/@bodanglin/verdict-node/-/verdict-node-0.1.0.tgz","fileCount":20,"integrity":"sha512-HEKq31wrKf+qSN03NSdb5Rh/wVHM2u/tYNt8SK01L65zvGlPMz5wpBA+R0nI6+UjmhnCc2cI4rAbzsuWZHgcBA==","signatures":[{"sig":"MEYCIQCA2V2D9Dm4boEmAfYBbXd77WeH8l/oEUAiKaJhCB0UiQIhAIMWUdMQ3TcrWA3ljFyE3zdiyR1+haaKUwL08eIT9mc1","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4386286},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./middleware":{"types":"./dist/middleware/index.d.ts","default":"./dist/middleware/index.js"}},"gitHead":"19f36c9533ffc2b53cd7e1871b90ef901009b0bc","scripts":{"lint":"tsc --noEmit","test":"jest","build":"tsc","typecheck":"tsc --noEmit","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\" \"*.md\" \"*.json\" \".github/workflows/*.yml\"","pack:dry-run":"npm pack --dry-run","verify:package":"npm run build && node scripts/verify-package.mjs"},"_npmUser":{"name":"beendanglin","email":"mr.nicholas.b.carter@gmail.com"},"repository":{"url":"git+https://github.com/mrnicholasbcarter-code/verdict-node.git","type":"git"},"_npmVersion":"11.17.0","description":"Enterprise LLM Criticality Router middleware for Express, Next.js — policy-gated, availability-aware routing control plane","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","dependencies":{"zod":"^3.25.76","@types/express":"^5.0.6","@bodanglin/verdict-contracts":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.4.2","config":"^4.4.2","express":"^5.2.1","ts-jest":"^29.4.11","prettier":"^3.9.5","supertest":"^7.2.2","typescript":"~5.9.3","@types/jest":"^30.0.0","@types/node":"^26.1.1","@types/supertest":"^7.2.1"},"peerDependencies":{"express":">=5.0.0 <6"},"_npmOperationalInternal":{"tmp":"tmp/verdict-node_0.1.0_1785043929432_0.4691255337768039","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@bodanglin/verdict-node@0.2.0","bugs":{"url":"https://github.com/mrnicholasbcarter-code/verdict-node/issues"},"dist":{"shasum":"40766830c514e124feae492ce845b7924dfe2d4f","tarball":"https://registry.npmjs.org/@bodanglin/verdict-node/-/verdict-node-0.2.0.tgz","fileCount":28,"integrity":"sha512-Zh7eNj/ZvjuLlHgliOd5Ux8r6NWdAgkeSDiSVLJ8N1uKuqroRVxFhSDXn1NgvVTG/JVLU9HpkvfVY2sfso/4hQ==","signatures":[{"sig":"MEQCIEytwz4359V01Qf1EKjHSu2FmpiFDh12Wowc+njVlO64AiBkTTeOvbQhQcwf7qyFp9wS5LUT0BY1sMnIFUx0XdhNlQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGJE0sf3762DBx2pZXr+jtJm2Ose80bocoTG5rfyece/AiEAouJppAfBm+8tROcYGoPsvikxrPatTe72fQ/bBdql4dk="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bodanglin%2fverdict-node@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":4455345},"main":"dist/index.js","name":"@bodanglin/verdict-node","type":"module","types":"dist/index.d.ts","engines":{"node":"^20.19.0 || >=22.12.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./middleware":{"types":"./dist/middleware/index.d.ts","default":"./dist/middleware/index.js"}},"gitHead":"65feea5f62f9d219ddfaffeb0e6b03e31b987e70","license":"MIT","scripts":{"lint":"tsc --noEmit","test":"node --experimental-vm-modules ./node_modules/jest/bin/jest.js","build":"tsc","typecheck":"tsc --noEmit","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\" \"*.md\" \"*.json\" \".github/workflows/*.yml\"","pack:dry-run":"npm pack --dry-run","consumer:smoke":"npm run build && node scripts/consumer-smoke.mjs","verify:package":"npm run build && node scripts/verify-package.mjs","test:forwarder:junit":"JEST_JUNIT_OUTPUT_DIR=\"${JUNIT_OUTPUT_DIR:-.}\" JEST_JUNIT_OUTPUT_NAME=\"${JUNIT_OUTPUT_NAME:-forwarder_test_results.xml}\" node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/middleware/forwarder.test.ts --reporters=default --reporters=jest-junit"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:15341736-3a02-4ac2-b2ab-a3292b6ddfea"}},"homepage":"https://github.com/mrnicholasbcarter-code/verdict-node#readme","keywords":["llm","routing","ai","gateway-adapter","safety","middleware","express","openai-compatible","zod"],"repository":{"url":"git+https://github.com/mrnicholasbcarter-code/verdict-node.git","type":"git"},"_npmVersion":"11.5.1","description":"OpenAI-compatible gateway adapter with execution-envelope validation for Express and Next.js","directories":{},"maintainers":[{"name":"beendanglin","email":"mr.nicholas.b.carter@gmail.com"}],"sideEffects":false,"_nodeVersion":"22.23.2","dependencies":{"zod":"^3.25.76","@types/express":"^5.0.6","@bodanglin/verdict-contracts":"^0.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.5.1","config":"^5.0.1","express":"^5.2.1","ts-jest":"^29.4.11","prettier":"^3.9.8","supertest":"^7.2.2","jest-junit":"^17.0.0","typescript":"~5.9.3","@types/jest":"^30.0.0","@types/node":"^26.6.1","@types/supertest":"^7.2.1"},"peerDependencies":{"express":">=5.0.0 <6"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/verdict-node_0.2.0_1790382077430_0.9611335107157657"}}},"time":{"created":"2026-07-26T05:32:09.271Z","modified":"2026-09-26T00:21:17.903Z","0.1.0":"2026-07-26T05:32:09.583Z","0.2.0":"2026-09-26T00:21:17.512Z"},"bugs":{"url":"https://github.com/mrnicholasbcarter-code/verdict-node/issues"},"license":"MIT","homepage":"https://github.com/mrnicholasbcarter-code/verdict-node#readme","keywords":["llm","routing","ai","gateway-adapter","safety","middleware","express","openai-compatible","zod"],"repository":{"url":"git+https://github.com/mrnicholasbcarter-code/verdict-node.git","type":"git"},"description":"OpenAI-compatible gateway adapter with execution-envelope validation for Express and Next.js","maintainers":[{"name":"beendanglin","email":"mr.nicholas.b.carter@gmail.com"}],"readme":"# @bodanglin/verdict-node — TypeScript Gateway Adapter\n\n[![npm](https://img.shields.io/npm/v/@bodanglin/verdict-node.svg)](https://www.npmjs.com/package/@bodanglin/verdict-node)\n[![TypeScript](https://img.shields.io/badge/typescript-strict-blue.svg)](https://www.typescriptlang.org/)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![CI](https://github.com/mrnicholasbcarter-code/verdict-node/actions/workflows/ci.yml/badge.svg)](https://github.com/mrnicholasbcarter-code/verdict-node/actions/workflows/ci.yml)\n\n> **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.\n\n---\n\n## ExecutionEnvelope Verification\n\nThis package includes canonical ExecutionEnvelope v1 verification following the [verdict-core contract specification](https://github.com/mrnicholasbcarter-code/verdict-core/blob/80ebaf23278473bb48bde807c1c3867e980a6e14/docs/contracts/EXECUTION_ENVELOPE_V1.md).\n\n### Verification Rules\n\nThe verifier implements fail-closed validation:\n\n1. **Schema Validation**: Rejects unknown fields (strict v1 contract)\n2. **Eligibility**: `admitted` must be `true` with no contradictory deny signals\n3. **Digest Check**: `policy_digest` must match the expected value\n4. **Expiry Check**: `execution_constraints.expires_at` is REQUIRED\n   - Missing expiry → EXPIRED (fail closed: envelopes MUST have bounded lifetime)\n   - Timezone-naive timestamps → EXPIRED\n   - Unparseable timestamps → EXPIRED\n   - `now >= expires_at` → EXPIRED\n\n**The verifier never throws on untrusted input.** Malformed data returns `REJECT_UNKNOWN`.\n\n### Usage\n\n```typescript\nimport { verifyExecutionEnvelope, EnvelopeVerdict } from '@bodanglin/verdict-node';\n\nconst verdict = verifyExecutionEnvelope(envelope, {\n  now: '2024-01-15T12:30:00Z',\n  expectedPolicyDigest: 'a'.repeat(64),\n});\n\nif (verdict === EnvelopeVerdict.ACCEPT) {\n  // Envelope is valid\n} else {\n  // Handle DENY, EXPIRED, DIGEST_MISMATCH, or REJECT_UNKNOWN\n}\n```\n\n### Canonical Fixtures\n\nThe package vendors canonical test fixtures from verdict-core at SHA `80ebaf23278473bb48bde807c1c3867e980a6e14`.\n\nSee `contracts/fixtures/execution-envelope/v1/README.md` for details.\n\n## What is @bodanglin/verdict-node?\n\n`@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**.\n\nThe 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.\n\n**Works with any OpenAI-compatible client**: Claude Code, Codex, Cursor, Cline, Hermes, Agents SDK, raw HTTP.\n\n---\n\n## Status\n\n**Alpha** — not production-ready. Current implementation provides:\n\n- Fail-closed `ExecutionEnvelope` validation in the standalone forwarder by default\n- A shared pre-forward envelope check for streaming and non-streaming requests when an envelope is supplied to the gateway\n- Zod request/response schemas\n- Heuristic criticality classification and model catalog discovery for compatibility routing\n- Bounded fallback ladder for selected HTTP/network failures\n- Explicit compatibility opt-outs for deployments that do not yet require Core decisions or envelopes\n- Streaming SSE and non-streaming JSON forwarding\n- In-memory score cache (process-local)\n\n**Missing** (tracked on release board):\n\n- Verified Ruflo/RuVector IntelligenceService integration\n- Persistent learning / cross-process state\n- Reliable live quota/headroom data\n- Full OpenAI field preservation\n- Complete adversarial streaming/fallback contract\n\n### Supported TypeScript toolchain\n\nThis release supports TypeScript `5.9.x` with `ts-jest@29.4.x` and Jest 30.\n`ts-jest@29.4.x` declares `typescript >=4.3 <7`, so TypeScript 7 is not a\nsupported configuration for this package. Keep the compiler pinned to the\ndocumented 5.9 line until a ts-jest release with an explicit TypeScript 7 peer\nrange is available and verified. Issue [#14](https://github.com/mrnicholasbcarter-code/verdict-node/issues/14)\ntracks that upgrade; the supported ceiling is intentional rather than hidden\nbehind an install fallback.\n\n---\n\n## Install\n\n> **Registry version lags this branch.** The only published release, `0.1.0`\n> (npm, 2026-07-26), predates execution-envelope enforcement, the\n> `createNextApiHandler` export, and the fail-closed handler fix. The behavior\n> in this README requires a build from source until a new version is\n> released:\n>\n> ```bash\n> git clone https://github.com/mrnicholasbcarter-code/verdict-node.git\n> cd verdict-node && npm ci && npm run build\n> npm install /path/to/verdict-node   # from your application\n> ```\n\n```bash\nnpm install @bodanglin/verdict-node\n# or\npnpm add @bodanglin/verdict-node\n# or\nyarn add @bodanglin/verdict-node\n```\n\n**Peer dependency**: `express@>=5.0.0 <6` only when using Express middleware. Next.js `/api` routes can use the generic handler without mounting Express.\n\n---\n\n## Quick Start\n\n### Express standalone forwarder\n\n```typescript\nimport express from 'express';\nimport { createForwarder } from '@bodanglin/verdict-node/middleware';\n\nconst app = express();\napp.use(express.json());\n\n// Obtain these values from independent trusted Core outputs.\nconst coreEnvelope: unknown = await loadCoreEnvelope();\nconst trustedPolicyDigest = await loadTrustedPolicyDigest();\n\napp.use(\n  '/v1',\n  createForwarder({\n    baseUrl: process.env.VERDICT_UPSTREAM ?? 'http://127.0.0.1:20132/v1',\n    apiKey: process.env.OMNIROUTE_API_KEY,\n    executionEnvelope: coreEnvelope,\n    expectedPolicyDigest: trustedPolicyDigest,\n  })\n);\n\napp.listen(3000, () => console.log('verdict-node listening on :3000'));\n```\n\n`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.\n\n### Next.js `/api` route (fail-closed)\n\n```typescript\n// pages/api/chat/completions.ts\nimport { createNextApiHandler } from '@bodanglin/verdict-node';\n\nexport default createNextApiHandler({\n  baseUrl: process.env.OMNIROUTE_BASE_URL ?? 'http://127.0.0.1:20132/v1',\n  apiKey: process.env.OMNIROUTE_API_KEY,\n  decisionEndpoint: process.env.VERDICT_CORE_DECISION_ENDPOINT,\n});\n```\n\n**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.\n\n---\n\n## Configuration\n\n### `ForwarderConfig`\n\n```typescript\nimport { createForwarder, type ForwarderConfig } from '@bodanglin/verdict-node/middleware';\n\nconst config: ForwarderConfig = {\n  baseUrl: process.env.VERDICT_UPSTREAM ?? 'http://127.0.0.1:20132/v1',\n  apiKey: process.env.OMNIROUTE_API_KEY,\n  executionEnvelope: coreEnvelope,\n  expectedPolicyDigest: trustedPolicyDigest,\n  timeoutMs: 30_000,\n  maxRetries: 3,\n};\n\napp.use('/v1', createForwarder(config));\n```\n\n### `GatewayConfig`\n\n```typescript\nimport { createNextApiHandler, type GatewayConfig } from '@bodanglin/verdict-node';\n\nconst config: GatewayConfig = {\n  baseUrl: process.env.OMNIROUTE_BASE_URL,\n  apiKey: process.env.OMNIROUTE_API_KEY,\n  decisionEndpoint: process.env.VERDICT_CORE_DECISION_ENDPOINT,\n  decisionTimeoutMs: 2_000,\n};\n\nexport default createNextApiHandler(config);\n```\n\n---\n\n## API\n\n### `createForwarder(config: ForwarderConfig): express.RequestHandler`\n\nThe 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.\n\n### `createNextApiHandler(config: GatewayConfig): NextApiHandlerLike`\n\nThe 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.\n\n### Types\n\n```typescript\nimport type { GatewayConfig, OpenAIChatCompletionRequest } from '@bodanglin/verdict-node';\nimport type {\n  ForwarderConfig,\n  OpenAIChatCompletionResponse,\n  OpenAIChatCompletionChunk,\n} from '@bodanglin/verdict-node/middleware';\n```\n\n---\n\n## Integration with Verdict Core\n\nVerdict 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.\n\nFor 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](docs/adr/ADR-001-execution-envelope-enforcement.md).\n\n---\n\n## Development\n\n```bash\n# Install deps\nnpm install\n\n# Type-check\nnpm run typecheck\n\n# Lint\nnpm run lint\n\n# Test\nnpm test\n\n# Build\nnpm run build\n\n# Verify package\nnpm run verify:package\n```\n\n---\n\n## Project Structure\n\n```\nverdict-node/\n├── src/\n│   ├── index.ts                         # Gateway and Next.js exports\n│   ├── adapters/\n│   │   └── contract-to-middleware.ts    # Canonical-decision adapter\n│   └── middleware/\n│       ├── index.ts                     # Middleware exports\n│       ├── forwarder.ts                 # Express JSON/SSE forwarder\n│       └── validator.ts                 # Validation helpers\n├── tests/\n├── scripts/                             # Package verification\n├── docs/adr/\n└── package.json\n```\n\n---\n\n## Ecosystem\n\n| Package                                                                          | Purpose                                                                                                              |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| [`verdict-core`](https://github.com/mrnicholasbcarter-code/verdict-core)         | Python control plane                                                                                                 |\n| `@bodanglin/verdict-node`                                                        | Express/Next.js middleware (this repo)                                                                               |\n| [`verdict-cockpit`](https://github.com/mrnicholasbcarter-code/verdict-cockpit)   | Next.js dashboard                                                                                                    |\n| [`verdict-risk`](https://github.com/mrnicholasbcarter-code/verdict-risk)         | Risk engine                                                                                                          |\n| [`verdict-edge`](https://github.com/mrnicholasbcarter-code/verdict-strategy)     | Edge mining framework                                                                                                |\n| [`verdict-backtest`](https://github.com/mrnicholasbcarter-code/verdict-backtest) | Monte Carlo harness                                                                                                  |\n| OmniRoute                                                                        | Per OmniRoute's own description: 250+ providers, 90+ free tiers (third-party claim, not verified by this repository) |\n\n---\n\n## Links\n\n- **Verdict Core**: https://github.com/mrnicholasbcarter-code/verdict-core\n- **Verdict Cockpit**: https://github.com/mrnicholasbcarter-code/verdict-cockpit\n- **Issues**: https://github.com/mrnicholasbcarter-code/verdict-node/issues\n- **Discord**: https://discord.gg/verdict\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE)\n\n## CJS Support\n\nThis package is **ESM-only**. CommonJS consumers can use `require()` on Node versions that support `require(esm)`:\n\n- **Node >= 20.19.0**\n- **Node >= 22.12.0**\n\n**ESM (recommended)**:\n\n```javascript\nimport { verifyExecutionEnvelope } from '@bodanglin/verdict-node';\n```\n\n**CommonJS** (requires Node >= 20.19 or >= 22.12):\n\n```javascript\nconst { verifyExecutionEnvelope } = require('@bodanglin/verdict-node');\n```\n\nOlder Node versions must use ESM imports or upgrade to a supported version.\n","readmeFilename":"README.md"}