[AVAILABLE].
packages/api/src/middleware/caller-auth.ts, StaticKeyAuthenticator, verified against a live server in this session.Send a bearer key
Every route requiresAuthorization: Bearer <key> except GET /health, GET /ready,
GET /openapi.yaml, and GET /documentation (liveness/readiness probes and API
documentation, packages/api/src/app.ts).
Where keys come from
Keys are minted byscripts/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.
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).
Errors
See the Error catalog for every other error this API returns.