Skip to main content
[AVAILABLE]. packages/api/src/middleware/caller-auth.ts, StaticKeyAuthenticator, verified against a live server in this session.

Send a bearer key

Every route requires Authorization: Bearer <key> except GET /health, GET /ready, GET /openapi.yaml, and GET /documentation (liveness/readiness probes and API documentation, packages/api/src/app.ts).
A missing or invalid header returns 401 before a Business Transaction is even constructed:

Where keys come from

Keys are minted by scripts/generate-api-key.ts, one per calling system. The raw key is shown once and never written to disk; only a SHA-256 hash of it is configured server-side via PARMANA_API_KEYS, and comparisons run in constant time (packages/api/src/auth/StaticKeyAuthenticator.ts). There is no self-service key-management endpoint: issuing and rotating keys is an operator action, not an API call. See Deploy patterns for the full setup and rotation procedure.

What this layer does and does not prove

Caller authentication answers exactly one question: should this HTTP request be entertained at all. It is the first thing that runs, ahead of Policy evaluation and gateway attestation, and it is independent of both:
  • A well-authenticated caller submitting a Policy-rejected transaction is still rejected, see Policies and the decision.
  • A well-authenticated caller does not thereby prove anything about who authorized the underlying business action, that is what Execution Authorization and the gateway establish, on a completely separate signature.
Keeping these layers apart is deliberate: see How Parmana thinks.

Local development

PARMANA_AUTH_DISABLED=true skips this middleware entirely and logs a loud warning at startup every time it does. It exists for local development and running the tutorials. Never set it in a real deployment. It removes the only authentication this API has, and every route becomes reachable by anyone who can reach the port.

Rate limiting

[AVAILABLE], packages/api/src/middleware/rate-limit.ts.
POST /execute — the endpoint that signs and writes to the database on every request — is rate-limited per authenticated caller identity (the callerId this page establishes), not by IP: a design-partner integration commonly calls from a shared backend IP, where an IP-keyed limit would either starve every caller behind it or be loose enough to mean nothing. It is mounted only when caller authentication is enabled; there is no caller identity to key off when auth is disabled. GET /health and GET /ready get a separate, deliberately more permissive limit keyed by IP instead, since both are legitimately polled on a fixed interval by PaaS health-check infrastructure and must never be tight enough to throttle that. A rejected request gets 429, {"error":"Rate limit exceeded. Try again later.","code": "RATE_LIMITED"}, and a Retry-After header, and never reaches policy evaluation or signing — no nonce is consumed and nothing is signed for a request this middleware rejects. Both limits have working defaults sized for a design-partner evaluation deployment, not high-volume production traffic, and are independently configurable per deployment via RATE_LIMIT_EXECUTE_PER_MINUTE / RATE_LIMIT_HEALTH_PER_MINUTE (see .env.example).
Scope, precisely, the same caveat as the replay-nonce store’s (see Idempotency and nonces). The limiter’s store is express-rate-limit’s default, in-memory, single-process store. A multi-machine deployment counts independently per machine — the effective ceiling for a given caller is the configured per-minute limit times the number of machines serving traffic, not a fleet-wide limit enforced once across every machine. A shared store (Redis or equivalent) would close that gap; none is wired in today.

Errors

See the Error catalog for every other error this API returns.