Configuration Options
The Kerberos constructor accepts an optional third parameter with configuration options:
const kerberos = new Kerberos(policies, derivedRoles, {
logger: true, // Legacy console audit logging with summary + table + debug(json)
onError: 'deny', // 'throw' (default) or 'deny' — fail-closed evaluation errors
telemetry, // Optional: OpenTelemetry traces + metrics ({ api } or { tracer, meter })
cache, // Optional: any cache solution exposing get(key) (keyv, cacheable, ...)
cacheRetry: { attempts: 3 }, // Optional: retry policy for transient cache.get failures
codec, // Optional: (de)serialization codec for dynamic policies ({ jsep } or { deserialize })
relations, // Optional: ReBAC resolver for relation-backed derived roles
hooks: { beforeRequest, afterRequest }, // Optional: lifecycle hooks (awaited; throw to veto, return args to enrich)
hooksTimeoutMs: 500, // Optional: a hook that never settles fails as KerberosHookError instead of hanging
maxListeners: 10, // Optional: listener-leak warning threshold per event name (0 disables)
z, // Optional: validate with Zod
ajv, // Optional: validate with Ajv
typebox: Type, // Optional: switch Ajv validation to TypeBox builders
getCallId: () => `custom-${Date.now()}`, // Custom call ID generator (optional)
});Options
logger(boolean | KerberosLogger): Enable audit logging.truekeeps the legacy console behavior withgroup + summary + table + debug(json)falseor omitted disables logging- a custom
console-like logger keeps the legacy table/json flow - a structured logger such as
Pinoreceives one structured audit entry per evaluated action - Logging is pure observability: it never changes decisions or error behavior (that is
onError's job), and a throwing logger is swallowed — it can never affect authorization.
onError('throw' | 'deny', default'throw'): What happens when policy evaluation fails at runtime (a throwing condition function, a failing cache backend, a ReBAC resolver error).'throw'propagates the error to the caller;'deny'fails closed:isAllowedresolves tofalse,checkResourcesto one all-DENY result per requested resource (positional parity with the request, like the per-resource fail-closed path — entries that cannot be echoed back from malformed arguments are skipped),planResourcesto aKIND_ALWAYS_DENIEDfilter.- Malformed arguments are programming errors and always throw
KerberosValidationError, regardless of this option.
javascript// Fail-closed setup: evaluation errors deny instead of throwing. const kerberos = new Kerberos(policies, derivedRoles, { onError: 'deny' });telemetry(KerberosTelemetryOptions): Enable OpenTelemetry traces and metrics. Pass{ api }(the@opentelemetry/apimodule) or{ tracer, meter }instances — see OpenTelemetry.cache(CacheLike): An optional cache used as a fallback source for dynamic/stored policies. Any object exposing aget(key)method is accepted (keyv, cacheable, cache-manager, ...). See Caching / Storing policies.cacheRetry({ attempts?, delayMs?, jitter?, timeoutMs?, onExhausted? }, default{ attempts: 3, delayMs: 25, jitter: true }): Retry policy forcache.getfailures. Attempts are spaced by full-jitter exponential backoff (delayMsbase, doubling per attempt;delayMs: 0restores immediate retries); deterministic adapter errors (TypeError/SyntaxError) are never retried.timeoutMs(off by default) bounds each read attempt so a hung backend fails instead of hanging authorization. After the attempts are exhausted the failure surfaces asKerberosCacheError(and then followsonError) — unlessonExhausted: 'miss'opts into degraded mode: the read counts as a cache miss and evaluation falls through to the remaining static sources, so a cache outage no longer disables statically-resolvable decisions (the degradation stays visible via thekerberos.cache.requestserrormetric and a guarded error log entry).attempts: 1disables retrying.cacheKeyPrefix(string, default''): Prefix prepended to every cache key (policies and derived roles). Use it to namespace tenants or environments sharing one store — derived-roles documents are otherwise a single globalderivedRoles:<name>namespace, so two tenants publishing the same definition name on a shared store would silently overwrite each other.relationsTimeoutMs(number, off by default): Bounds eachrelations.check/relations.listcall; a resolver that neither resolves nor rejects fails asKerberosRelationsError(followingonError) instead of hanging the request.audit({ includeMeta?: boolean }): Engine-level audit enrichment. With{ includeMeta: true }and a logger attached, decision tracing runs for every request, so audit entries always carrymeta.resolutionand thepolicy-missreason — audit completeness stops depending on each call site remembering the per-requestincludeMetaflag. The response stays gated on the request flag.maxConcurrency(number, unbounded by default): Caps how many resources of acheckResourcesbatch evaluate at once. Without it a 10k-resource batch launches 10k concurrent evaluation chains (each issuing its own cache reads) — memory spikes, event-loop saturation and a thundering herd on the cache backend. The built-inRelationResolveraccepts the same option for itslookupResourcescandidate-verification fan-out.codec(PolicyCodec): How cached policy documents are transformed before construction:{ jsep }enables the built-in safe$exprevaluator,{ deserialize }plugs in your own logic, and when omitted cached values are passed to policy constructors as-is — seecodecoption — three modes.schemas({ enforcement?, definitions? }): Attribute schema enforcement — Cerbosschemasparity. Resource policies declareschemas.principalSchema/resourceSchemarefs (with optionalignoreWhen.actionsglobs); this option maps the refs to validators and picks the level:'reject'(default when set) denies requests whose attributes fail validation,'warn'reports without changing decisions,'none'disables (the Cerbos default when unconfigured). Failures are returned as Cerbos-shapedvalidationErrors({ path, message, source }) oncheckResourcesresults — regardless ofincludeMeta— and reach the audit log. A definition may be a JSON Schema object (compiled with theajvoption), a Zod schema, or a validator function. See Attribute schemas.relations(KerberosRelationsResolver): ReBAC resolver used by relation-backed derived roles — any object with acheck(args, opts)method (and an optional batchedlist). See ReBAC (Relations).hooks(KerberosHooks): Lifecycle hooks —beforeRequest,afterRequest,beforeResource,afterResource,onError— awaited inside the request flow. A throwing hook vetoes the request asKerberosHookError(followingonError);beforeRequestmay return replacement arguments to enrich the request (re-validated, markedenrichedon audit entries, spans and events);onError, a failed request'safterRequestand a failed resource'safterResourceare swallowed and never mask the original error. Unknown names / non-functions throw at construction. See Hooks & events.hooksTimeoutMs(number, off by default): Bounds every awaited hook invocation; a hook that neither resolves nor rejects fails asKerberosHookError(timedOut: true, then following that hook's throwing/swallowing rule) instead of hanging authorization — the hook counterpart ofrelationsTimeoutMs.maxListeners(number, default10): Listener-leak detection for the events façade — the first subscription past this count on one event name logs aconsole.warn(subscribe once at startup, not per request). Never a limit;0disables the warning. The built-inRelationResolveraccepts the same option.z: Enables validation using the built-in Zod schema builders.ajv: Enables validation using the built-in JSON Schema builders compiled with Ajv.typebox: When used together withajv, switches validation to the built-in TypeBox builders.getCallId(function): Custom function to generate call IDs for audit tracking.- Default behavior: Uses
crypto.randomUUID()in Node.js,window.crypto.randomUUID()in browsers, or falls back to a pseudo UUID generator - Custom example:
() => \req-${Date.now()}-${Math.random()}``
- Default behavior: Uses
Using Pino for Production Logging
If you want machine-readable audit logs in production, pass a Pino instance as the logger option:
import pino from 'pino';
import { Kerberos } from '@alexify/kerberos';
const logger = pino({ level: 'info' });
const kerberos = new Kerberos(policies, derivedRoles, {
logger,
});With Pino, Kerberos emits structured audit entries that include callId, reqId, reqKind, principalId, principalRoles (the role set the decision was based on — roles change over time, so past entries stay explainable), resourceId, action, effect, outputs, and meta. Fail-closed denials are part of the stream too: a resource whose evaluation failed inside a checkResources batch (and the onError: 'deny' fallback of isAllowed) logs its DENY decisions marked reason: 'evaluation-error', and planResources results (PlanResources.result, with the filter kind) go out at info level like other decision entries — only lifecycle *.start/*.finish events sit at debug. This mode is better suited for production ingestion than the default console table output.
It also emits lifecycle logs such as IsAllowed.start, IsAllowed.error, IsAllowed.finish, CheckResources.start, CheckResources.finish and PlanResources.*. Errors are always logged, but whether they are rethrown or converted into a fail-closed response is decided solely by the onError option — never by the logger.
Call ID Generation
Every request (isAllowed / checkResources / planResources) automatically generates a unique kerberosCallId for audit tracking:
- Node.js: Uses
crypto.randomUUID() - Browser: Uses
window.crypto.randomUUID() - Fallback: Pseudo UUID v4 generator if crypto APIs are unavailable
- Custom: Provide your own
getCallIdfunction for custom ID formats
This ID is included in both the response and audit logs for correlation.