@playstack/api-keys
Opaque customer API-key issuance, hashing, verification, scopes, usage, and revocation.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/api-keys authenticates customer and integration callers with opaque credentials that are shown once and stored only as indexed SHA-256 digests.
Install
npm install @playstack/api-keys @playstack/core @playstack/eventsCompose and issue
import { createApiKeys } from '@playstack/api-keys'
export const apiKeys = createApiKeys({
persistence,
events,
crypto: globalThis.crypto,
clock,
ids,
product: 'bm',
scopes: ['spans:read', 'spans:write'],
maxKeyRateLimitCapacity: 1_000,
})
const { secret, key } = await apiKeys.issue({
owner: { type: 'account', id: accountId },
name: 'Production ingest',
environment: 'live',
scopes: ['spans:write'],
createdBy: userId,
rateLimitOverride: { capacity: 500 },
})Return secret once. The stored record contains only its product/environment prefix, hash, and last four hexadecimal characters.
Service configuration
ApiKeysOptions property | Required | Purpose |
|---|---|---|
persistence | Yes | Transactional lookup, locking, listing, and retained revocation state. |
events | Yes | Emits typed issue, use, and revocation events. |
crypto | Yes | Web Crypto subtle and getRandomValues. |
clock, ids | Yes | Application-owned time and identifiers. |
product | Yes | Lowercase 2–16 character key prefix. |
scopes | Yes | Complete allowlist of canonical product scopes. |
maxKeyRateLimitCapacity | Yes | Ceiling for any per-key capacity override. |
lastUsedWriteIntervalMs | No | Usage-write throttle; defaults to one hour. |
format | No | Secret byte length and complete owner/environment prefix table. |
By default, secrets have the form <product>_<live|test>_<32 hex characters>
and contain 128 bits of CSPRNG entropy. For a product requiring 32-byte secrets,
set format.secretBytes: 32; supported integer lengths are 16–64 bytes.
// Supply this as ApiKeysOptions.format alongside the required ports.
const format = {
secretBytes: 32,
prefixes: {
live: { user: 'pmk_u', account: 'pmk_a', accountMember: 'pmk_m' },
test: { user: 'pmk_test_u', account: 'pmk_test_a', accountMember: 'pmk_test_m' },
},
}Prefixes omit the separator before the random suffix and use 1–64 lowercase
alphanumeric characters with single internal underscores, beginning with a letter.
product remains the canonical namespace, independent of the displayed prefix.
Issue and rotate share the snapshotted format; verification checks exact length,
prefix and stored owner/environment metadata. A prefix never grants authority.
Changing formats does not automatically accept older credentials: use explicit
legacyVerifiers only for application-owned imports, retaining live policy checks.
Issue and verification properties
| Property | Meaning |
|---|---|
owner | { type: account | accountMember | user, id }. |
name | Human-readable key label. |
environment | live or test, also encoded in the prefix. |
scopes | Non-empty subset of the configured scope allowlist. |
createdBy | Opaque actor ID. |
expiresAt | Optional absolute millisecond timestamp. |
rateLimitOverride.capacity | Optional per-key ceiling that cannot exceed the service maximum. |
requiredScopes | Optional verification requirement; every requested scope must be present. |
Persistence bridge
ApiKeysPersistence exposes a transaction boundary, locked lookup by hash, lookup by ID, owner-scoped listing, and save. Verification and revocation must lock the same record so a last-used update cannot race past revocation.
import { prismaApiKeysPersistence } from '@playstack/api-keys/prisma'
const persistence = prismaApiKeysPersistence(prisma)The managed Prisma fragment is security-critical and non-ejectable. The application owns migrations.
Rate-limit and Nest composition
The key’s optional override is only an inner ceiling. Compose @playstack/rate-limit so an account-plan bucket remains authoritative, then add @playstack/nest-api-keys at the HTTP boundary to verify scopes and consume both buckets atomically.
Revocation retains the record and emits a transactional audit event. Manual rotation can still issue a second key, deploy it, verify traffic, and revoke the first.
Rotate with a bounded grace period
const rotation = await apiKeys.rotate({
id: currentKeyId,
rotatedBy: userId,
overlapEndsAt: Date.now() + 60 * 60_000,
})Atomic rotation links both generations and keeps the previous key valid only through the configured overlap. maxRotationOverlapMs defaults to seven days. Retrying a completed rotation returns already_rotated rather than creating another key, but cannot recover the copy-once replacement secret.
revokeFamily() traverses both sides of the lineage and revokes every generation in one transaction. Missing links and cycles fail closed. Named legacy verifiers can validate an application-owned key format and optionally rehash the managed record during a gradual migration.
Boundary
API keys are not JWTs, OAuth tokens, third-party credentials, offline licences, or a complete authorization policy. Application code still authorizes the verified owner and scopes against the requested resource.
Live authorization policy
Optional authorization.assertAllowed(input, transaction) runs for issue, verify and rotate, including already-rotated retries. Verification checks policy on every use, not only last-used writes. Use the transaction to recheck membership, issuer ceilings and resource ownership; requested resource context is not trusted tenancy.
List/revoke management routes need separate authorization. Rotation preserves the original creator while recording the rotator; map replacement credentials to an application-owned stable grant actor. No generic machine-principal or vault-policy package is implied.
API entry points and requirements
Reference snapshot: @playstack/api-keys@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.
| Public entry point | Declaration file |
|---|---|
@playstack/api-keys | ./dist/index.d.ts |
@playstack/api-keys/errors | ./dist/errors.d.ts |
@playstack/api-keys/prisma | ./dist/prisma.d.ts |
@playstack/api-keys/testing | ./dist/testing.d.ts |
@playstack/api-keys/playstack.artifacts.json | No TypeScript declaration (asset or metadata export). |
@playstack/api-keys/package.json | No TypeScript declaration (asset or metadata export). |
Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.
Peer dependencies
This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.
For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.