
Company News
Free Business Plan Upgrades for Open Source Maintainers
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.
@bolyra/gateway
Advanced tools
Bolyra MCP Auth Gateway — standalone reverse proxy that verifies agent ZKP credentials before forwarding to upstream MCP servers.
Bolyra MCP Auth Gateway -- standalone reverse proxy that verifies agent ZKP credentials before forwarding requests to upstream MCP servers.
Put @bolyra/gateway in front of your MCP server; it verifies agent authority, prevents replay, enforces delegated scopes, and gives you signed audit logs for every tool call.
# Install
npm install @bolyra/gateway
# Create a config file (optional)
cat > gateway.yaml << 'EOF'
target: http://localhost:3000/mcp
port: 4100
devMode: true
EOF
# Run
npx @bolyra/gateway --config ./gateway.yaml
Or run directly with CLI flags:
npx @bolyra/gateway --target http://localhost:3000/mcp --dev --port 4100
+----------------------------------+
| @bolyra/gateway |
Agent -------->| |--------> Upstream MCP Server
(with proof | 1. Parse Authorization header | (unmodified)
bundle) | 2. verifyBundle() from @bolyra/mcp|
| 3. checkToolPolicy() |
| 4. Nonce replay check |
| 5. Emit signed receipt |
| 6. Proxy request + X-Bolyra-* hdrs|
| |
| Config: gateway.yaml |
| Receipts: ./receipts/ or stdout |
+----------------------------------+
Request routing:
GET /healthz -- returns gateway status (intercepted, not proxied)tools/call -- forwarded to upstream without authtools/call -- auth verified, then forwarded if authorizednpx @bolyra/gateway [options]
Options:
--target <url> Upstream MCP server URL (required unless in config)
--port <number> Gateway listen port (default: 4100)
--config <path> Path to gateway config file (default: ./gateway.yaml)
--dev Enable Bolyra Core mode (classical checks: tool policy,
nonce replay, signed receipts, credential binding
against registered credentials; ZK proof verification
off. Without registered credentials, permission claims
are self-asserted — see Credential Binding below)
--credentials <path> Credentials file (YAML/JSON map: commitment ->
{ permissionBitmask, expiryTimestamp? }); overrides
the config's credentials section
--receipt-dir <path> Directory for receipt JSON files (default: ./receipts/)
--receipt-stdout Write receipts to stdout (NDJSON)
--no-receipts Disable receipt generation
--network <name> Bolyra network (default: base-sepolia)
--help Show help
--version Show version
CLI flags override config file values.
# gateway.yaml
target: http://localhost:3000/mcp
port: 4100
network: base-sepolia
# Bolyra Core mode: classical checks only, ZK proof verification skipped
devMode: false
# Registered credentials (see "Credential Binding" below).
# Core mode (devMode: true): claims are checked against this registry —
# unknown commitments, forged masks, and expired credentials are denied.
# Production mode: this map becomes the resolveCredential source for
# verifyBundle's Poseidon3 scopeCommitment binding (expiryTimestamp
# required per entry).
# type: registry (on-chain resolution) is not supported by the packaged
# gateway yet and fails validation — use the library resolveCredential
# option for custom resolution.
credentials:
type: static
map:
"48201394857102938471029384": # credential commitment (decimal)
permissionBitmask: 3 # READ_DATA | WRITE_DATA
expiryTimestamp: "1893456000" # unix seconds; optional in Core mode
# Per-tool policies
tools:
write_file:
requireBitmask: 2 # WRITE_DATA (0b10)
delete_file:
requireBitmask: 6 # WRITE_DATA + FINANCIAL_SMALL (0b110)
maxChainDepth: 0 # Direct credentials only
transfer_funds:
requireBitmask: 28 # FINANCIAL_* (0b11100)
minScore: 90
# Nonce replay protection
nonce:
store: memory # "memory" (default) or "redis"
maxProofAge: 300 # seconds
# Redis config (required when store: redis)
# redis:
# url: ${REDIS_URL} # required
# keyPrefix: "bolyra:nonce:" # optional, default shown
# connectTimeout: 5000 # optional, ms
# Receipt signing — every allow/deny decision is ES256K-signed (see
# "Signed Receipts" below). Without privateKey an ephemeral key is
# generated at startup; set it for a stable, pinnable signer address.
receipts:
enabled: true
issuer: "my-gateway"
keyId: "k1"
privateKey: "${BOLYRA_RECEIPT_KEY}" # 32-byte hex secp256k1 key, env var substitution
output: file # "file", "stdout", or "webhook"
dir: ./receipts/
# Health check
health:
enabled: true
path: /healthz
# Optional HMAC signing for X-Bolyra-* headers
# hmac:
# secret: "hex-encoded-shared-secret"
Environment variables are substituted in string values using ${VAR_NAME} syntax.
For embedding the gateway middleware in your own server:
import {
createGatewayMiddleware,
createGatewayProxy,
loadConfig,
injectBolyraHeaders,
computeHmac,
verifyHmac,
createReceiptWriter,
createHealthHandler,
} from '@bolyra/gateway';
import type {
GatewayConfig,
GatewayMiddlewareOptions,
GatewayRequest,
} from '@bolyra/gateway';
// Option 1: Full proxy server
const config = loadConfig({ target: 'http://localhost:3000/mcp', dev: true });
const server = createGatewayProxy({ config });
server.listen(4100);
// Option 2: Just the middleware (for custom servers)
const middleware = createGatewayMiddleware({ config });
// Use in your request handler:
// const authorized = await middleware(req, res, toolName);
| Export | Description |
|---|---|
createGatewayProxy(options) | Create full reverse proxy HTTP server |
createGatewayMiddleware(options) | Auth verification middleware only |
loadConfig(flags?) | Load and validate gateway configuration |
injectBolyraHeaders(authCtx, receiptId?) | Build X-Bolyra-* headers |
computeHmac(headers, secret) | HMAC-SHA256 sign X-Bolyra-* headers |
verifyHmac(headers, secret, hmac) | Verify HMAC on X-Bolyra-* headers |
createReceiptWriter(config) | Create pluggable receipt output |
createGatewayReceiptSigner(config, override?) | Resolve the ES256K receipt signer (configured key or ephemeral) |
createHealthHandler(config) | Create /healthz endpoint handler |
extractToolName(body) | Extract tool name from JSON-RPC body |
loadCredentialsFile(path) | Load a --credentials file (bare map or full section) |
buildCredentialRegistry(credentials) | Compile a static credentials section into a lookup registry |
checkCredentialBinding(bundle, authCtx, registry) | Core-mode registered-credential check (see Credential Binding) |
createStaticCredentialResolver(credentials) | Static resolveCredential for production scopeCommitment binding |
hasStaticCredentials(credentials) | True when a usable static credential map is configured |
RedisNonceStore | Redis-backed NonceStore for multi-instance deployments |
For multi-instance deployments, use Redis for shared nonce replay protection:
# gateway.yaml
nonce:
store: redis
maxProofAge: 300
redis:
url: ${REDIS_URL}
keyPrefix: "bolyra:nonce:" # optional
Library usage:
import { createGatewayMiddleware, RedisNonceStore } from '@bolyra/gateway';
const store = new RedisNonceStore({ url: 'redis://localhost:6379' });
const middleware = createGatewayMiddleware({ config, nonceStore: store });
// Graceful shutdown
process.on('SIGTERM', () => store.close());
Fail-closed behavior: If Redis is unreachable, the gateway returns HTTP 500, never passes requests through. Monitor Redis availability as a critical dependency.
When a request passes verification, the gateway injects these headers into the proxied request:
| Header | Value |
|---|---|
X-Bolyra-Verified | true |
X-Bolyra-DID | did:bolyra:{network}:{commitment} |
X-Bolyra-Score | Verification score (0-100) |
X-Bolyra-Permissions | Effective permission bitmask (decimal) |
X-Bolyra-Chain-Depth | Delegation chain depth (0 = direct) |
X-Bolyra-Receipt-ID | Signed receipt id for this decision (when receipts enabled) |
X-Bolyra-HMAC | HMAC-SHA256 signature (when HMAC configured) |
The upstream server can use these headers for logging/auditing without importing Bolyra.
Every tools/call decision — allow and deny, dev mode and production —
produces an ES256K-signed receipt (SignedReceipt from
@bolyra/receipts: secp256k1 + keccak256,
Ethereum-compatible recovery). This includes rejections with no usable proof
bundle at all: a missing or malformed Authorization header still leaves a
signed anonymous deny receipt, so the audit trail has no unsigned gaps.
Each receipt carries the verdict (decision.allowed), a human-readable
reasonCode (which tool, which permissions were required vs. held), the agent
DID when known, score, permission bitmask, delegation chain depth, hashes of
the presented proof material, and the decision timestamp. Any edit to a
receipt breaks its signature.
Signing key resolution:
receiptSigner option (library embedders)receipts.privateKey from the config (recommended for production)file output mode, persists it to
{receipt-dir}/signer.json so auditors can pin the trust anchor.Independent verification needs only the receipt (plus, optionally, the pinned signer address) — no gateway, no database:
import { verifyReceipt } from '@bolyra/receipts';
const receipt = JSON.parse(fs.readFileSync('receipts/2026-07-10/....json', 'utf8'));
verifyReceipt(receipt); // true — signature + payload hash check
verifyReceipt(receipt, pinnedSigner); // true — also pins the signer address
Signatures make each receipt tamper-evident; hash-chaining makes the receipt
stream tamper-evident. Every receipt the gateway signs carries
chain: { seq, prevReceiptHash } inside the signed payload (seq is 0-based
per gateway process; the first receipt links to the 32-zero-byte genesis
sentinel) plus a top-level receiptHash that the next receipt links to.
Deleting, reordering, or inserting receipts in a collected log breaks chain
verification even though every remaining signature stays individually valid:
# stdout mode: collect NDJSON to a file, verify signatures + chain
bolyra receipt verify-chain receipts.ndjson --signer 0xPinnedSigner
Notes for auditors:
chain-restart.verify-chain prints and re-verify with
--expect-head / --expect-count. How and how often you anchor those
checkpoints (object-lock storage, transparency log, chain) is
enterprise-configurable deployment policy.--allow-unchained accepts
them as a leading prefix (signatures still verified) while chain-verifying
the rest. A chain-less receipt after a chained one always fails — it could
be an inserted line.| Mode | Description |
|---|---|
file | JSON files in day-rotated directories: receipts/2026-06-18/{timestamp}-{receipt-id}.json, plus signer.json with the pinned signer address |
stdout | NDJSON to stdout (one JSON line per receipt) |
webhook | HTTP POST to configured URL with optional auth headers |
All modes are non-blocking. Write and signing failures are logged but never
interrupt request processing. The signing key is probe-validated at startup;
in the exceptional case that signing still fails at runtime, the decision is
recorded as a raw JSON record explicitly tagged "unsigned": true (with the
signing error), so audit consumers can detect the gap — an unsigned record
can never masquerade as a signed receipt.
Receipts always record the gateway's final decision: a request that authenticates successfully but fails tool policy gets a deny receipt. A bundle that parses as JSON but is missing proof material entirely is denied fail-closed (HTTP 401) with a signed anonymous receipt.
In Core mode (--dev) proofs are mocked, so the permission mask inside a
bundle is asserted by the caller. Registering credentials turns that claim
into an enforced check — the same registered-credential pattern as
examples/verified-actions-demo,
now packaged:
credential_unknown)credential_mismatch)credential_mismatch
— scope narrowing is one-way)credential_expired)All denials are fail-closed and receipted like every other decision.
Registering a dev credential. A commitment in Core mode is an opaque identifier the platform assigns (production commitments are Poseidon hashes of the credential). Generate one and register it:
# 1. Generate a commitment
node -e "console.log(BigInt('0x' + require('crypto').randomBytes(16).toString('hex')).toString())"
# 2. Register it (credentials.yaml)
cat > credentials.yaml << 'EOF'
"48201394857102938471029384":
permissionBitmask: 3 # READ_DATA | WRITE_DATA
EOF
# 3. Run with binding enforced
npx @bolyra/gateway --target http://localhost:3000/mcp --dev --credentials ./credentials.yaml
Hand the commitment (and its granted mask) to the agent; its proof bundles
must carry exactly that identity and mask. The --credentials file can be a
bare map (above) or the full config-section shape (type: static, map:);
the credentials: section in gateway.yaml is equivalent.
Without registered credentials, Core mode stays frictionless for
tutorials: any claim passes (current behavior), but the gateway warns at
startup and every allow receipt is flagged
[credential-binding: none — permission claims self-asserted] so audit
consumers can see the tradeoff — the same visibility pattern as the
ephemeral receipt-signing key.
In production mode the same credentials section is compiled into a
resolveCredential implementation, engaging verifyBundle's cryptographic
binding: the verifier recomputes Poseidon3(permissionBitmask, commitment, expiryTimestamp) from the registered credential and requires it to equal
the proof's public scopeCommitment output — a forged mask cannot produce
a valid proof. expiryTimestamp is therefore required per entry in
production. Library embedders can still pass resolveCredential directly;
it takes precedence over the config section.
The gateway is the trust boundary. Agents present proof bundles; the gateway verifies them before forwarding.
Deployment guidance:
Threats addressed:
verifyReceipt)| Bit | Permission | Value |
|---|---|---|
| 0 | READ_DATA | 1 |
| 1 | WRITE_DATA | 2 |
| 2 | FINANCIAL_SMALL (<$100) | 4 |
| 3 | FINANCIAL_MEDIUM (<$10K) | 8 |
| 4 | FINANCIAL_UNLIMITED | 16 |
| 5 | SIGN_ON_BEHALF | 32 |
| 6 | SUB_DELEGATE | 64 |
| 7 | ACCESS_PII | 128 |
Higher tiers imply lower (e.g., FINANCIAL_MEDIUM implies FINANCIAL_SMALL).
Run the gateway as a container with zero Node.js setup required.
docker pull ghcr.io/bolyra/gateway:latest
# Bolyra Core mode -- classical checks, no circuits, no config file needed
docker run --rm -p 4100:4100 ghcr.io/bolyra/gateway \
--target http://host.docker.internal:3000/mcp --dev
# Mount config file + set env vars
docker run -d \
-v $(pwd)/gateway.yaml:/etc/bolyra/gateway.yaml:ro \
-e REDIS_URL=redis://redis:6379 \
-e BOLYRA_RECEIPT_KEY=hex-encoded-key \
-p 4100:4100 \
ghcr.io/bolyra/gateway
# From repo root
docker build -f Dockerfile.gateway -t bolyra-gateway:local .
# Run with local build
docker run --rm -p 4100:4100 bolyra-gateway:local \
--target http://host.docker.internal:3000/mcp --dev
# Verify health
curl http://localhost:4100/healthz
docker run --rm -p 8080:8080 ghcr.io/bolyra/gateway \
--target http://upstream:3000/mcp --port 8080 --dev
node:22-alpinebolyra (UID 1001, non-root)GET /healthz every 30s/etc/bolyra/gateway.yaml/app/receipts/ (writable)linux/amd64, linux/arm64CLI flags passed after the image name override config file values. See CLI Reference above.
Apache-2.0
FAQs
Bolyra MCP Auth Gateway — standalone reverse proxy that verifies agent ZKP credentials before forwarding to upstream MCP servers.
The npm package @bolyra/gateway receives a total of 36 weekly downloads. As such, @bolyra/gateway popularity was classified as not popular.
We found that @bolyra/gateway demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.