npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agentmandateprotocol/middleware

v1.0.1

Published

A-MAP request verification middleware for Express, Hono, and fetch — drop-in agent authorization

Readme

@agentmandateprotocol/middleware

HTTP middleware for A-MAP (Agent Mandate Protocol) — cryptographic authorization for AI agents.

npm install @agentmandateprotocol/middleware

Two ways to use A-MAP with HTTP

Option A — Client-side fetch guard (works with any API today)

You are an agent owner. Your agent makes HTTP requests to external APIs. You want to enforce limits on what your agent is allowed to call — without any cooperation from those APIs.

Agent → AmapFetchGuard → fetch() → API server
              ↑
      enforcement here
      server never knows

AmapFetchGuard wraps any fetch-compatible function. If the mandate doesn't cover the request being made, the call is blocked locally before any network traffic.

Option B — Server-side middleware (for API and tool developers)

You are building an HTTP API or tool. You want agents to prove they have a human-signed mandate before your endpoint runs.

Agent → HTTP request → amapVerifier() → your handler
                             ↑
                     enforcement here

amapVerifier() (Express) and amapHonoVerifier() (Hono/Cloudflare Workers) sit in front of your routes. No mandate, no access.


⚠️ Distributed deployments — nonce store warning

The default InMemoryNonceStore is not safe behind a load balancer.

Each server instance has its own nonce memory. A replayed request routed to a different instance will pass the nonce check — defeating replay prevention entirely.

Production with multiple instances: you must provide a shared nonce store.

| Deployment | Recommended store | |---|---| | Single instance / development | InMemoryNonceStore (default) — fine | | Cloudflare Workers | CloudflareKVNonceStore (included) | | Node.js multi-process | Redis-backed store (implement NonceStore) |

The middleware logs a console warning when InMemoryNonceStore is used outside of test environments.


Option A: Client-side fetch guard

Quickstart

import { AmapFetchGuard } from '@agentmandateprotocol/middleware'
import { LocalKeyResolver } from '@agentmandateprotocol/core'

const keyResolver = new LocalKeyResolver(new Map([
  ['did:amap:human:alice:abc123', alicePublicKey],
  ['did:amap:agent:my-agent:1.0:def456', agentPublicKey],
]))

// Wrap the global fetch — agent uses this instead
const guarded = new AmapFetchGuard(fetch, {
  mandate: currentSessionMandate,   // DelegationToken[] from amap.issue()
  keyResolver,
  mode: 'enforce',
  rules: {
    'GET /api/emails':         { requires: ['email:read'] },
    'POST /api/emails/send':   { requires: ['email:send'] },
    'DELETE /api/emails/*':    { requires: ['email:delete'] },
    'POST *':                  { requires: ['api:write'] },
    '*':                       { requires: ['api:allow'] },
  },
})

// Agent uses guarded.fetch instead of fetch — same API
const res = await guarded.fetch('https://api.example.com/api/emails/delete/123', {
  method: 'DELETE',
})
// → throws PERMISSION_INFLATION if mandate lacks 'email:delete'
// → network call never made, API server never receives it

Why this matters

The API server's cooperation is irrelevant. Whether the server is a simple REST API, a complex third-party service, or anything in between — the guard intercepts before the network call.

Prompt injection fails at the guard. An injected instruction tells the agent to delete files it shouldn't touch. The agent calls guarded.fetch('/api/critical-data', { method: 'DELETE' }). The guard checks the mandate. Mandate says api:read only. Call rejected locally. No network traffic, no server log entry, no damage.

Audit mode for observation. Run in audit mode for a week. Every request is logged — allowed or blocked — and the agent runs uninterrupted. Write precise rules based on observed behavior, not guesswork.

new AmapFetchGuard(fetchFn, options)

class AmapFetchGuard {
  constructor(
    fetchFn: (url: string | URL, init?: RequestInit) => Promise<Response>,
    options: AmapFetchGuardOptions,
  )
  async fetch(url: string | URL, init?: RequestInit): Promise<Response>
}

Pass any fetch-compatible function — the global fetch, node-fetch, an Axios wrapper, etc.

Options:

| Option | Type | Default | Description | |---|---|---|---| | mandate | DelegationToken[] | required | The mandate chain the agent is operating under | | mode | 'enforce' \| 'audit' \| 'warn' | 'enforce' | Global enforcement mode | | rules | Record<string, FetchRule> | {} | Per-endpoint rules. '*' is the catch-all | | keyResolver | KeyResolver | (none) | For mandate chain verification | | onAudit | (entry: FetchAuditEntry) => void | (none) | Called for every request — allowed or blocked |

FetchRule:

interface FetchRule {
  requires: string[]                        // permissions that must be in the mandate
  policy?: 'enforce' | 'audit' | 'warn'    // per-rule override of global mode
}

FetchAuditEntry:

interface FetchAuditEntry {
  event: 'FETCH_ALLOWED' | 'FETCH_BLOCKED'
  method: string
  url: string
  path: string
  timestamp: string
  mandateId: string    // tokenId of the root token
  principal: string    // DID of the human who issued the mandate
  reason?: string      // present when FETCH_BLOCKED
}

Rule matching

Rules are matched against the string "METHOD /path". The '*' key is the catch-all.

| Rule key | Matches | |---|---| | 'GET /api/emails' | GET /api/emails (exact) | | 'DELETE /api/emails/*' | DELETE /api/emails/123, DELETE /api/emails/abc | | 'POST *' | Any POST request | | '*' | Any method, any path |

Rule resolution order:

  1. First matching pattern in the rules object (non-catch-all)
  2. '*' catch-all
  3. No match: the endpoint string itself ("METHOD /path") is required as a permission

Enforcement modes

enforce (default): Blocked requests throw AmapError with code PERMISSION_INFLATION. No network traffic.

audit: All requests go through. Blocked ones are logged via onAudit without interruption. Use to observe before locking down.

warn: Same as audit.

Per-rule policy overrides global mode:

new AmapFetchGuard(fetch, {
  mandate,
  mode: 'audit',          // globally: observe and pass through
  rules: {
    'DELETE *': {
      requires: ['data:delete'],
      policy: 'enforce',  // DELETE always blocks if unauthorized, even in audit mode
    },
    '*': { requires: ['api:allow'] },
  },
})

Audit logging

const guarded = new AmapFetchGuard(fetch, {
  mandate,
  mode: 'audit',
  onAudit: (entry) => {
    if (entry.event === 'FETCH_BLOCKED') {
      console.warn(`[AMAP] Blocked: ${entry.method} ${entry.path} — ${entry.reason}`)
    }
    myLogger.log(entry)
  },
})

Option B: Server-side middleware

Express

import express from 'express'
import { amapVerifier } from '@agentmandateprotocol/middleware'
import { LocalKeyResolver } from '@agentmandateprotocol/core'

const app = express()

const keyResolver = new LocalKeyResolver(new Map([
  ['did:amap:human:alice:abc123', alicePublicKey],
  ['did:amap:agent:my-agent:1.0:def456', agentPublicKey],
]))

// Protect a route
app.use('/api/email',
  amapVerifier({ expectedPermission: 'email:read', keyResolver })
)

app.get('/api/email', (req, res) => {
  const { principal, effectiveConstraints } = req.amapVerification!
  res.json({ authorizedBy: principal })
})

On success: req.amapVerification is set and next() is called. On failure: responds 401 with { error: AmapErrorCode, message: string }.

Options:

| Option | Type | Default | Description | |---|---|---|---| | expectedPermission | string | (none) | Permission the mandate must include | | keyResolver | KeyResolver | (none) | DID → public key resolution | | revocationChecker | RevocationChecker | (none) | Skip to omit revocation checks | | nonceStore | NonceStore | InMemoryNonceStore | See distributed warning above | | requestedAction | string | (none) | Evaluate allow/deny policy against this action | | getRequestParams | (req) => object | req.body (if object) | Extract params for parameterLocks checking |

Body verification: The middleware passes req.body to verifyRequest only when it is a string or Buffer (e.g., using express.text() or express.raw()). If using express.json(), the parsed body is used for parameterLocks checking but not for body hash verification. To verify the body hash, use express.text() and parse JSON yourself.

TypeScript — extend the Request type:

// The middleware augments the Express namespace automatically:
// req.amapVerification: VerificationResult | undefined

Hono (Cloudflare Workers)

import { Hono } from 'hono'
import { amapHonoVerifier, CloudflareKVNonceStore } from '@agentmandateprotocol/middleware'
import type { AmapHonoVariables } from '@agentmandateprotocol/middleware'

const app = new Hono<{ Variables: AmapHonoVariables }>()

app.use('/api/*', amapHonoVerifier({
  expectedPermission: 'email:read',
  keyResolver,
  nonceStore: new CloudflareKVNonceStore(env.AMAP_NONCES),
}))

app.get('/api/email', (c) => {
  const { principal } = c.get('amapVerification')
  return c.json({ authorizedBy: principal })
})

On success: c.get('amapVerification') is set and the next handler runs. On failure: returns c.json({ error, message }, 401).

Options: same as Express (no getRequestParams).

VerificationResult shape

Both middlewares attach this result:

{
  valid: true,
  principal: string,             // DID of the human who issued the root mandate
  effectiveConstraints: { ... }, // most restrictive constraints across the full chain
  chain: VerifiedLink[],         // each hop: { hop, token, issuer, delegate }
  auditId: string,               // UUID for audit logging
  appliedPolicy?: { ... }        // present when requestedAction was passed
}

CloudflareKVNonceStore

Production nonce store for Cloudflare Workers deployments.

import { CloudflareKVNonceStore } from '@agentmandateprotocol/middleware'

// In your Worker handler:
export default {
  async fetch(request: Request, env: Env) {
    // env.AMAP_NONCES is a KV binding declared in wrangler.toml
    const nonceStore = new CloudflareKVNonceStore(env.AMAP_NONCES)
    // pass to amapHonoVerifier or amapFetchGuard
  }
}
# wrangler.toml
[[kv_namespaces]]
binding = "AMAP_NONCES"
id = "your-kv-namespace-id"

Atomicity note: Cloudflare KV does not support atomic set-if-not-exists. Two simultaneous requests with the same nonce arriving at different Worker instances could theoretically both pass. This window is milliseconds in practice. For strict atomicity, use a Durable Object with a single writer.

Implementing your own NonceStore (Redis example)

import type { NonceStore } from '@agentmandateprotocol/core'
import { Redis } from 'ioredis'

class RedisNonceStore implements NonceStore {
  constructor(private redis: Redis) {}

  async checkAndStore(nonce: string, ttlMs: number): Promise<boolean> {
    // SET NX with TTL — atomic in Redis
    const result = await this.redis.set(`amap:nonce:${nonce}`, '1', 'PX', ttlMs, 'NX')
    return result === 'OK'
  }
}

checkAndStore must be atomic. Redis SET NX satisfies this. The method returns true if the nonce was fresh (first seen), false if it was already used.


Error codes

All errors from amapVerifier and amapHonoVerifier produce a { error: code, message } JSON response with HTTP 401. AmapFetchGuard throws AmapError with a .code property.

| Code | Meaning | |---|---| | BROKEN_CHAIN | Missing X-AMAP headers, or parentTokenHash mismatch | | TOKEN_EXPIRED | A token in the chain has expired | | INVALID_SIGNATURE | Signature verification failed at a hop | | INVALID_REQUEST_SIGNATURE | The request-level signature is invalid | | PERMISSION_INFLATION | Agent claims an action beyond what the mandate grants | | NONCE_REPLAYED | This nonce has already been used | | STALE_REQUEST | Request timestamp is outside the ±5 minute window | | PARAMETER_LOCK_VIOLATION | A locked parameter value doesn't match | | EXPLICIT_DENY | Action denied by deniedActions, or not in allowedActions | | AGENT_REVOKED | An agent in the chain has been revoked | | AGENT_UNKNOWN | A DID cannot be resolved to a public key |


Related packages