Keyring

API keys

Mint a key for a tenant in a project and an environment, and read what the SDK gives your handler.

A key belongs to one project, one tenant and one environment, and carries optional scopes, an optional expiry and optional per-key rate limits.

API keys → Mint a key asks for exactly those. Over the API:

curl -X POST https://keyring-api.belghalem.fr/v1/keys \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"project_id":"'$PROJECT_ID'","tenant_id":"'$TENANT_ID'","env":"test","name":"Local dev key","scopes":["read:orders"]}'
{
  "object": "api_key",
  "id": "01a0...",
  "env": "test",
  "display_prefix": "kr_test_a1b2c3",
  "key": "kr_test_...",
  "key_shown_once": true,
  "scopes": ["read:orders"]
}

The key is shown once. display_prefix is what you will see everywhere afterwards: the prefix plus six characters of the body, enough to recognise a key in a log and useless as a credential.

The key format

kr_live_<32 base64url chars><8 hex chars>     48 characters
kr_test_<32 base64url chars><8 hex chars>

24 random bytes, 192 bits of entropy, and a CRC-32 over the prefix and body. The checksum is not a security control: it lets a typo be refused with no lookup, and it is one of GitHub's requirements for a scannable secret. Key format has the rest, including why the environment is inside the hashed material.

What your handler sees

After the middleware, req.keyring (or the KeyringContext your framework hands you) is:

FieldMeaning
keyId, workspaceId, projectId, tenantIdThe identity. null on a fail-open decision, where there is no policy record.
env'live' or 'test', read from the key itself.
scopesWhat the key was minted with. has(scope) matches them.
displayPrefixLoggable by construction.
expiresAtEpoch milliseconds, or null.
degradedTrue whenever the decision came from stale or incomplete policy.
verifiedFalse when the key was admitted with no policy record at all.
rateLimitThe counter states after this request, or null when the counters were unreachable.
your resourcesWhatever you passed as resources, resolved for this key's environment.

It is KeyringContext | null in every adapter: null on a route configured skip: true, so read it with ?..

Scopes

Scopes are free-form strings you define. A route can require them:

routes: { 'POST /v1/payments': { onUnavailable: 'closed', scopes: ['write:payments'] } }

A key without the scope gets 403 with missing_scopes listing what it lacks. A route that declares scopes is deliberately not scope-checked on a fail-open decision, because there is no scope set to check against; that is why a money route also sets onUnavailable: 'closed'.

Editing a key

The key's page has Edit (name, scopes, expiry, rate limits, a free-form meta), Rotate and Revoke. PATCH /v1/keys/:id is the same edit. Every change bumps the project's policy version and reaches every SDK node within a poll.

Expiry

expires_at is honoured by the SDK from its cached policy, so an expired key dies inside your process with nothing scheduled to run at the hour mark. A leaked live key reported by GitHub's secret scanning is quarantined the same way: its expiry is pulled in to one hour, never extended, and you get a mail.

On this page