Additional info
Error handling
Unified error shape, error codes, and HTTP mapping guidelines.
Error shape
Public APIs return a discriminated union-like Result<T>
:
type Result<T> = { result?: T; error?: UsefulKeyError };
type UsefulKeyError = {
code: string;
message: string;
retryable?: boolean;
cause?: unknown;
meta?: Record<string, unknown>;
};
Prefer handling error
explicitly rather than relying on exceptions.
Normalization and metadata
Errors are normalized via a utility so callers always receive a stable shape:
toError(err: unknown, fallbackCode: string, meta?: Record<string, unknown>): UsefulKeyError
- Preserves any thrown object that already has string
code
andmessage
. - Otherwise uses
fallbackCode
and derivesmessage
from the thrown value. - Always attaches the original value on
cause
and merges providedmeta
. - Core paths include
meta.op
to indicate the operation (e.g."getKey"
,"verifyKey"
,"createKey"
,"revokeKey"
,"extendKeyExpiry"
,"hardRemoveKey"
,"sweepExpired"
).
Core error codes
These codes originate from core and adapters:
UNKNOWN
KEYSTORE_READ_FAILED
KEYSTORE_WRITE_FAILED
KEYSTORE_REVOKE_FAILED
KEYSTORE_SWEEP_UNSUPPORTED
ANALYTICS_TRACK_FAILED
KEY_GENERATION_FAILED
PLUGIN_BLOCKED
PLUGIN_SETUP_FAILED
Additionally, some operations may emit validation-style codes when inputs are invalid (for example, INVALID_INPUT
from extendKeyExpiry
). Plugins may emit their own codes as well (for example, KEY_NOT_FOUND
, INVALID_ARGUMENT
).
Verification reasons
When verifyKey
rejects, the result
is present with valid: false
and a reason
:
not_found
revoked
expired
usage_exceeded
blocked_by_plugin
(default when a plugin rejects without a custom reason)insufficient_scope
rate_limited
(configurable in the plugin)namespace_required
(when rate limit plugin is enabled andnamespace
is missing)
HTTP mapping guidelines
valid: true
→ 200 OKvalid: false
reasons:not_found
→ 401/404 (pick one; 401 prevents leaking existence)revoked
,expired
→ 401 Unauthorizedusage_exceeded
→ 402/403/429 depending on product semantics; commonly 429insufficient_scope
→ 403 Forbiddenrate_limited
→ 429 Too Many Requests (include reset info if available)namespace_required
→ 400 Bad Request
Errors from core/adapters (error.code
):
KEYSTORE_*
→ 500 Internal Server Error (with retryable depending on store)KEY_GENERATION_FAILED
→ 500PLUGIN_BLOCKED
→ translate to a 4xx aligned with the plugin’sreason
Example translation (Express)
const res = await uk.verifyKey({ key, identifier: req.ip, namespace: "api" });
if (res.error) return reply.status(500).json(res.error);
if (!res.result!.valid) {
const r = res.result!;
const status = r.reason === "rate_limited" ? 429
: r.reason === "insufficient_scope" ? 403
: 401;
return reply.status(status).json(r);
}
return reply.json(res.result);