Checkpoint Docs
Enforce

Middleware Enforcement

Code-based enforcement for Next.js, Express, and ASP.NET Core applications

What is Middleware Enforcement?

Checkpoint Middleware adds AI agent detection and enforcement directly in your application code. It runs before your route handlers, classifying every request and applying the verdict from the Rust kya-os-engine (plus any composed Cedar policy you deploy).

Middleware is available for Next.js, Express, and ASP.NET Core applications.

Next.js has two shapes. withCheckpoint runs the detection engine in-process (WASM, no per-request network hop). withCheckpointApi dispatches to the Checkpoint SaaS gateway over HTTPS (no local WASM). Express and .NET run the engine in-process. Pick the shape that fits your runtime — see Basic Setup below.

Installation

npm install @kya-os/checkpoint-nextjs
npm install @kya-os/checkpoint-express

The .NET SDK ships as four packages, all published at the same version. Install the metapackage to pull the whole surface, or reference the adapter you need directly.

# Metapackage — pulls Checkpoint.Core + the adapters transitively
dotnet add package KyaOs.Checkpoint

# — or reference the pieces directly —
dotnet add package Checkpoint.AspNetCore   # ASP.NET Core (net8) middleware
dotnet add package Checkpoint.Core         # engine, options, detection types
# dotnet add package Checkpoint.AspNet     # classic ASP.NET / IIS (net462) module

Checkpoint.AspNetCore is the modern ASP.NET Core adapter; Checkpoint.AspNet is the classic .NET Framework (net462 / IIS) module. Both depend on Checkpoint.Core. KyaOs.Checkpoint is a convenience metapackage whose transitive dependencies always match the standalone packages exactly.

Basic Setup

Create or update middleware.ts in your project root. Choose the deployment shape for your runtime:

Local engine (withCheckpoint) — runs the WASM engine in-process. Lowest latency, deterministic verdicts, no per-request network hop.

// middleware.ts
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';

export default withCheckpoint({
  tenantHost: 'your.tenant.example',
  apiKey: process.env.CHECKPOINT_API_KEY, // optional — enables dashboard reporting
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

SaaS gateway (withCheckpointApi) — dispatches detection + enforcement to the Checkpoint gateway over HTTPS. No local WASM; centralized dashboard policy applies.

// middleware.ts
import { withCheckpointApi } from '@kya-os/checkpoint-nextjs/api-middleware';

export default withCheckpointApi({
  apiKey: process.env.CHECKPOINT_API_KEY,
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Mount withCheckpoint with app.use(...). Every request flows through the in-process WASM engine.

import express from 'express';
import { withCheckpoint } from '@kya-os/checkpoint-express';

const app = express();
app.use(express.json()); // body-parser — required for MCP-I envelope parsing

app.use(
  withCheckpoint({
    tenantHost: 'your.tenant.example',
    apiKey: process.env.CHECKPOINT_API_KEY, // optional — enables dashboard reporting
  })
);

app.get('/', (_req, res) => {
  res.json({ message: 'Protected by Checkpoint' });
});

app.listen(3000);

That's the whole API. The engine handles UA pattern detection, KYA-OS signature verification, scope evaluation, delegation-chain verification, status-list lookups, and policy evaluation. The wrapper translates the Express request, calls the engine, and applies the verdict to the response.

// Program.cs
using Checkpoint.AspNetCore.Extensions;
using Checkpoint.Core.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCheckpoint(options =>
{
    options.ProjectId = builder.Configuration["Checkpoint:ProjectId"]!;
    options.ApiKey = builder.Configuration["Checkpoint:ApiKey"]!;
    options.OnAgentDetected = DetectedAction.Block;
});

var app = builder.Build();

app.UseCheckpoint(); // Add early — before UseRouting()

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

app.Run();

See the full .NET integration guide for configuration options, signature verification, and KYA-OS instruct mode.

Configuration Options

The Next.js local-engine (withCheckpoint) and Express middleware share the same CheckpointConfig shape (Next.js adds a couple of runtime-specific fields). The SaaS-gateway middleware (withCheckpointApi) uses a different config — see SaaS gateway config below.

OptionTypeDefaultDescription
tenantHoststringRequiredTenant identifier (typically your dashboard hostname). Drives the PolicyEvaluator lookup.
enforcementMode'enforce' | 'observe''enforce''enforce' blocks; 'observe' passes everything through with X-Checkpoint-Would-Have-Been headers.
apiKeystringProject API key. Required for detections to land in the dashboard. Resolve from process.env.CHECKPOINT_API_KEY.
projectIdstringEnables in-process enforcement of the project's composed (/policy-compose) Cedar policy. Omit for detection + structured policy only.
onResult(result, req) => void | PromisePost-verdict observability callback. Fires after every verification (permit or block). Thrown errors are swallowed.
baseUrlstringhttps://kya.vouched.idDashboard base URL. Override for staging or self-hosted dashboards.
dashboardUrlstringopen-by-defaultTenant-policy source for the PolicyEvaluator.
engineConfigEngineConfig{ tier3Action: 'monitor' }Engine-default behaviour knobs. Opt into { tier3Action: 'block' } for engine-default blocking of Tier-3 UA matches.
delegationChallengeMode'spec-401' | 'negotiated''spec-401'Challenge-envelope mode. negotiated serves a body-readable 200 step-up to cooperative agents.
legacyEnvelopeFallbackbooleanfalseAccept legacy KYA-Delegation-header envelopes alongside the canonical body form.
policyCacheTtlSecondsnumber300Composed-policy fetch cache TTL. 0 refetches every request.
reputationBaselinenumber1.0Reputation returned for anonymous requests (trust-by-default).
argusUrlstringtrust-by-defaultArgus reputation-oracle base URL.
adaptersPartial<{...}>factory defaultsOverride the built-in DID resolver / status-list cache / reputation oracle / policy evaluator (tests).
debugbooleanfalseSurface reporter + composed-policy telemetry via console.warn / console.error.

Next.js withCheckpoint additionally accepts drainJsonBody (default true) and, for the Edge runtime, cedarWasmModule.

There is no mode: 'strict' | 'balanced' | 'lenient' knob, no onAgentDetected / onBlock callback, and no top-level storage or allowList config on withCheckpoint. Detection sensitivity comes from the engine's calibrated per-pattern scoring; enforcement rules come from your policies / composed Cedar; agent allowances come from the deployed policy, not a config array.

Detect-only (observe) mode

To run detection without blocking, set enforcementMode: 'observe'. Every request passes through; the engine still classifies each one and stamps X-Checkpoint-Would-Have-Been headers so you can see what enforcement would have done, and detections still report to the dashboard (when apiKey is set).

withCheckpoint({
  tenantHost: 'your.tenant.example',
  enforcementMode: 'observe', // classify + report, never block
  apiKey: process.env.CHECKPOINT_API_KEY,
});

SaaS gateway config (Next.js)

withCheckpointApi is a distinct middleware with its own configuration — it POSTs to the gateway rather than running the engine locally, so its knobs differ from CheckpointConfig:

OptionTypeDefaultDescription
apiKeystringenv varAPI key (or CHECKPOINT_API_KEY).
onBlock'block' | 'redirect' | 'challenge'dashboardOverride the action taken when the gateway decides to block.
redirectUrlstringdashboardTarget when onBlock: 'redirect'.
redirectMode'instruct' | 'http''instruct'instruct returns a 401 + MCP-I Link header; http returns a 302.
onAgentDetected(request, decision) => void | PromiseObservability callback fired when the gateway detects an agent.
skipPathsstring[]static assetsExtra paths to skip (glob patterns supported).
includePathsstring[]If set, only these paths are enforced.
failOpenbooleantrueAllow requests through on API error.

onBlock and onAgentDetected belong to withCheckpointApi (the SaaS-gateway path) only. The in-process withCheckpoint engine does not have them — read its verdict via the onResult callback instead (see below).

Reading the Verdict

Plain withCheckpoint attaches nothing to req. Read the engine's verdict through the onResult(result, req) callback, which fires after every verification with the full VerifyResult:

withCheckpoint({
  tenantHost: 'your.tenant.example',
  onResult: (result, req) => {
    // result.decision.kind is the verdict: 'permit' | 'block' | 'redirect' | ...
    console.log(`[${req.method} ${req.url}] verdict=${result.decision.kind}`);
  },
});

Errors thrown inside onResult are swallowed so an observability failure can never break the verdict path.

Observability & Storage (optional)

The session-tracking and event-storage primitives from the retired "enhanced" middleware are now composable exports you wire into withCheckpoint yourself — there is no separate enhanced middleware tier. This section is Express-specific.

Using .NET? Session tracking and signature verification are built into the base ASP.NET Core package — set EnableSessionTracking = true (the default) on CheckpointOptions. No extra wiring is needed.

Storage adapters

@kya-os/checkpoint-express exports MemoryStorageAdapter, RedisStorageAdapter, and an async createStorageAdapter factory. Construct an adapter and record events from onResult:

import { withCheckpoint, createStorageAdapter } from '@kya-os/checkpoint-express';

// createStorageAdapter is async and returns a StorageAdapter.
const storage = await createStorageAdapter({
  type: 'redis',
  ttl: 86400,
  redis: {
    url: process.env.REDIS_URL!,
    token: process.env.REDIS_TOKEN!,
  },
});

app.use(
  withCheckpoint({
    tenantHost: 'your.tenant.example',
    onResult: async (result, req) => {
      await storage.storeEvent({
        eventId: crypto.randomUUID(),
        sessionId: req.headers['x-request-id']?.toString() ?? crypto.randomUUID(),
        timestamp: new Date().toISOString(),
        agentType: result.detectionDetail.detectionClass.type,
        agentName: result.detectionDetail.detectedAgent?.name ?? 'unknown',
        confidence: result.detectionDetail.confidence,
        path: req.path,
        method: req.method,
        userAgent: req.headers['user-agent'],
        detectionReasons: result.detectionDetail.reasons,
        verificationMethod: result.detectionDetail.verificationMethod,
      });
    },
  })
);

createStorageAdapter({ type: 'memory' }) (or no argument) returns an in-memory adapter — fine for development, but data is lost on restart and not shared across instances. Use Redis (Upstash) for production.

The StorageAdapter interface

Implement this interface to plug in your own backend ({ type: 'custom', custom: myAdapter }):

import type { StorageAdapter } from '@kya-os/checkpoint-express';

const myAdapter: StorageAdapter = {
  storeEvent: async (event) => {
    /* persist an AgentDetectionEvent */
  },
  storeSession: async (session) => {
    /* upsert an AgentSession */
  },
  getEvents: async (sessionId, limit) => {
    /* events for a session */ return [];
  },
  getSession: async (sessionId) => {
    /* one session or null */ return null;
  },
  getRecentEvents: async (limit) => {
    /* recent events */ return [];
  },
  getActiveSessions: async (limit) => {
    /* active sessions */ return [];
  },
  cleanup: async (before) => {
    /* optional — clear data older than `before` */
  },
};

Session tracking

The Express package also exports ExpressSessionTracker and withSessionTracking for cookie/header-based session continuity. withSessionTracking(middleware, { enabled: true }) wraps a middleware so a returning agent's session is attached to req.agentShield on subsequent requests:

import { withCheckpoint, withSessionTracking } from '@kya-os/checkpoint-express';

const checkpoint = withCheckpoint({ tenantHost: 'your.tenant.example' });

app.use(withSessionTracking(checkpoint, { enabled: true }));

req.agentShield is populated only when you wire session tracking. Plain withCheckpoint does not attach anything to req — use onResult to read the verdict.

Response Headers

The engine path (withCheckpoint, Express and Next.js) sets X-Checkpoint-* headers on the response and writes a __checkpoint_verdict cookie that is byte-identical across both runtimes (shared encodeVerdictCookie primitive). In observe mode it adds X-Checkpoint-Would-Have-Been so you can see the verdict enforcement would have applied.

The X-Checkpoint-Engine header carries the engine name. Exact header keys are produced by the engine's renderDecisionAsResponse; the SDK propagates them verbatim.

When you enable Express session tracking, ExpressSessionTracker additionally emits kya-session, kya-session-agent, and kya-session-id (and withSessionTracking sets kya-detected / kya-agent on a continued session).

Earlier docs listed KYA-Detected / KYA-Confidence / KYA-Agent / KYA-Verification / KYA-AI-Visitor headers for the engine path — those were stale. The in-process engine emits X-Checkpoint-*. The KYA-* detection headers belong to the SaaS-gateway path (withCheckpointApi), which sets KYA-Detected / KYA-Confidence / KYA-Agent on pass-through responses.

Response shape (Express)

The middleware adapts the engine's Decision to one of four response shapes:

VerdictAction
Permit / Observenext() — pass through, set verdict cookie + X-Checkpoint-* headers
Redirectres.redirect(302, target)
Block + HTMLres.redirect(302, '/blocked') — your /blocked route reads the verdict cookie
Block + non-HTMLres.status(<engine-status>).json(body) (4xx, for JSON-API clients)

.NET Configuration

Configure the ASP.NET Core adapter through CheckpointOptions in AddCheckpoint(options => …). Selected options (see the .NET integration guide for the full list):

OptionTypeDefaultDescription
ApiKeystring?Dashboard API key (sk_live_… / sk_test_…). Enables policy enforcement.
ProjectIdstring?Project ID in the dashboard.
BaseUrlstringhttps://kya.vouched.idCheckpoint API base URL.
OnAgentDetectedDetectedActionDetectedAction.LogAction when a detected agent exceeds the confidence threshold. Values: Log, Block, Allow, Redirect, Instruct, Challenge.
ConfidenceThresholddouble70Minimum confidence (0–100) to trigger OnAgentDetected.
EnableSessionTrackingbooltrueTrack agent sessions across requests using a canonical session ID.
EnableSignatureVerificationbooltrueVerify Ed25519 signatures (ChatGPT RFC 9421, KYA-OS agents).
EnableComposedPolicybooltrueWire in-process composed /policy-compose Cedar evaluation (shadow-first).
Tier3ActionTier3ActionMonitorEngine-default behaviour for Tier-3 UA matches (Monitor / Block / Challenge).
McpServerUrlstring?MCP server URL used as the redirect target when OnAgentDetected = Redirect.
FailOpenbooltrueAllow requests through on middleware error.

OnAgentDetected = DetectedAction.Block and EnableSessionTracking are real members of CheckpointOptions — verified against Checkpoint.Core. The .NET API deliberately keeps OnAgentDetected (an enum action) where the TS engine SDK uses composed policy + onResult; the two SDKs are configured differently by design.

Advanced Usage (Next.js)

API route protection (SaaS gateway)

Protect individual App Router routes with withCheckpointApi:

import { withCheckpointApi } from '@kya-os/checkpoint-nextjs';

export const GET = withCheckpointApi(async (req) => {
  return Response.json({ data: 'protected' });
});

Client-side hooks

For React applications, import hooks from a separate entry point:

import { useAgentDetection } from '@kya-os/checkpoint-nextjs/hooks';
Hooks are client-only. Use middleware for server-side detection.

Route Matching (Next.js)

Control which routes the middleware applies to with the Next.js matcher config:

export const config = {
  matcher: [
    // Match all paths except static assets
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

To protect only specific routes:

export const config = {
  matcher: ['/api/:path*', '/dashboard/:path*'],
};

Next Steps