Keyring

@keyring-dev/sdk types

The option, decision and event types of @keyring-dev/sdk, split from the main page for weight.

Generated from packages/sdk/src/index.ts and the declarations it exports, by packages/docs/tools/generate-reference.mjs. Every signature below is printed from the source; the prose is the source's own doc comment.
@keyring-dev/sdk is on npm under the MIT license: npm install @keyring-dev/sdk, on Node 22 or later. Questions: hello@keyringdev.com.

The functions and classes and constants are on the main page.

Interfaces

AllowDecision

export interface AllowDecision {
  readonly outcome: 'allow';
  readonly key: DecidedKey;
  /** True whenever the decision was made from stale or incomplete data. */
  readonly degraded: boolean;
  /** For the request log; never for the caller. */
  readonly reason: DenialReason | null;
  /**
   * The store held no record for a key it could not prove absent. The only
   * thing this is allowed to cause is an on-demand fill for the *next*
   * request (see `fill.ts`); a synchronous "miss -> single remote verify"
   * fallback was deliberately not built, because that branch is what puts a
   * round trip on a cold cache.
   */
  readonly fill: boolean;
}

DecidedKey

The identity an adapter puts on the request. verified: false is the fail-open case: the environment and the display prefix are read out of the key itself -- both are in the key material, parsed locally, with no cache and no network involved -- and everything the policy would have said is unknown.

export interface DecidedKey {
  readonly keyId: string | null;
  readonly workspaceId: string | null;
  readonly projectId: string | null;
  readonly tenantId: string | null;
  readonly env: Environment;
  readonly scopes: readonly string[];
  readonly displayPrefix: string;
  readonly expiresAt: number | null;
  /**
   * The limits the policy says apply, already resolved against the project
   * default by the control plane. Empty on the fail-open branch for the same
   * reason `scopes` is: there is no policy record, so there is nothing to
   * enforce, and inventing a limit would be as wrong as inventing a scope.
   */
  readonly rateLimits: readonly RateLimitRule[];
  /**
   * False when the key was admitted without a policy record -- the
   * `stale-then-open` branch. A handler that cares (and a scoped one should)
   * reads this rather than inferring it from empty scopes.
   */
  readonly verified: boolean;
}

DecideOptions

export interface DecideOptions {
  readonly store: PolicyStore;
  readonly mode: UnavailableMode;
  readonly maxStalenessMs?: number;
  readonly requiredScopes?: readonly string[];
  /** Set only when a node serves a single environment. Unset routes on the key. */
  readonly env?: Environment;
  readonly now?: number;
  /** Seconds advertised on a 503. Defaults to the staleness budget. */
  readonly retryAfterSeconds?: number;
  /**
   * True while the poller's own credential is failing to authenticate against
   * the control plane (a 401 on the policy poll -- see `PolicyAuthFailedError`
   * in `@keyring-dev/cache`) -- or, on a *cold* node that has never loaded a
   * snapshot, a non-transient 400/403/404 at boot (see
   * `PolicyBootRefusedError`). The fail-open defence is built for a control
   * plane that is slow or unreachable; both of these say the opposite, that
   * this credential or this
   * configuration does not authenticate, which is not evidence that any
   * *other* key is fine to admit. So while this holds, a miss is refused
   * regardless of `maxStalenessMs`: with 401 under `stale-then-open` and
   * `stale-then-closed`, and with `closed`'s own 503 and `Retry-After`,
   * as before -- and a hit still verifies from the store exactly as it
   * always does. This does not refuse all traffic: only what the store
   * cannot vouch for.
   */
  readonly authFailed?: boolean;
}

DenyDecision

export interface DenyDecision {
  readonly outcome: 'deny';
  readonly status: 401 | 403 | 503;
  readonly code: PublicErrorCode | 'unavailable';
  readonly message: string;
  readonly reason: DenialReason | 'missing_credential' | 'policy_unavailable';
  readonly degraded: boolean;
  readonly displayPrefix: string | null;
  /**
   * The environment out of the key's own hashed material, parsed locally, and
   * `null` only when there is no key to read it from -- no credential at all,
   * or one too malformed to parse. The request log routes a denial on this: a
   * `kr_test_` key refused is a test-mode event, and recording it as live is
   * both the wrong retention window and the wrong dashboard.
   */
  readonly env: Environment | null;
  readonly missingScopes?: readonly string[];
  /** Seconds, for the `retry-after` header a 503 carries. */
  readonly retryAfterSeconds?: number;
  /** As on `AllowDecision`. */
  readonly fill: boolean;
}

EnvScope

export interface EnvScope {
  readonly env: Environment;
  readonly tenantId: string | null;
  readonly projectId: string | null;
  readonly keyId: string | null;
  readonly degraded: boolean;
}

EventShipperOptions

export interface EventShipperOptions extends SamplerOptions {
  readonly baseUrl: string;
  readonly secretKey: string;
  readonly nodeId: string;
  readonly sdk: { readonly name: string; readonly version: string };
  readonly bufferSize?: number;
  readonly flushIntervalMs?: number;
  readonly flushEvents?: number;
  readonly requestTimeoutMs?: number;
  readonly shutdownTimeoutMs?: number;
  readonly fetch?: ShipperFetch;
  readonly onWarn?: (message: string, detail?: unknown) => void;
  /** Off in tests, and for a customer who owns their own signal handling. */
  readonly handleSignals?: boolean;
}

FillStats

export interface FillStats {
  readonly requested: number;
  readonly started: number;
  readonly coalesced: number;
  readonly throttled: number;
  readonly failed: number;
  readonly inFlight: boolean;
}

IdempotencyClaim

export interface IdempotencyClaim {
  readonly scopeHash: string;
  readonly env: Environment;
  readonly lockToken: string;
  readonly lockMs: number;
  readonly maxBodyBytes: number;
  /** True when a crashed holder's lock was taken over: at-least-once. */
  readonly stolen: boolean;
}

IdempotencyOptions

export interface IdempotencyOptions {
  readonly baseUrl: string;
  readonly secretKey: string;
  readonly mode?: IdempotencyUnavailableMode;
  readonly ttlMs?: number;
  readonly timeoutMs?: number;
  readonly methods?: readonly string[];
  readonly retryAfterSeconds?: number;
  /**
   * One deliberate deviation from Stripe: a 5xx that means "my
   * database was failing over" releases the record so the retry executes,
   * instead of being stored and replayed forever. A 5xx that means "this
   * operation was attempted and failed" is stored, like Stripe stores it.
   *
   * The default: `Retry-After` present, or status 503.
   *
   * `finish()` consults this only for `status >= 500`, whatever it returns: a
   * 2xx or a 4xx is an attempt that happened, and releasing the record for one
   * re-executes the operation on the retry.
   */
  readonly transient?: (
    status: number,
    headers: Readonly<Record<string, string>>,
  ) => boolean;
  readonly fetch?: ControlPlaneFetch;
  readonly onError?: (error: unknown) => void;
  readonly now?: () => number;
}

IdempotencyRequest

export interface IdempotencyRequest {
  readonly method: string;
  readonly path: string;
  readonly body: unknown;
  readonly idempotencyKey: string | null;
  readonly projectId: string;
  readonly env: Environment;
  readonly tenantId: string | null;
  /**
   * `KeyringContext.verified`. False on the fail-open branch, and load-bearing:
   * the scope is `(project, env, tenant)` and an unverified decision knows
   * none of the three -- `decide()` returns `projectId: null` and
   * `tenantId: null` for every key it admits without a policy record. Sharing
   * one namespace across them is a cross-tenant replay, so `begin()` refuses
   * instead. Required rather than optional: a port that forgets it should not
   * compile.
   */
  readonly verified: boolean;
}

IngestBatch

export interface IngestBatch {
  readonly object: 'ingest_batch';
  readonly protocol_version: number;
  readonly node_id: string;
  readonly sdk: { readonly name: string; readonly version: string };
  /**
   * The sampling rate in force for the events in this batch, so the dashboard
   * can extrapolate counts rather than under-reporting them.
   * `events_sampled_out` is the exact figure the rate approximates and is
   * carried too, because an extrapolation is a worse answer than a count when
   * the count is free.
   */
  readonly sample_rate: number;
  readonly events_sampled_out: number;
  /** Cumulative, so a gap in a dashboard is attributable rather than mysterious. */
  readonly events_dropped: number;
  readonly events: readonly UsageEvent[];
}

KeyringContextBase

Everything an adapter puts on req.keyring before the host's resources.

export interface KeyringContextBase {
  readonly keyId: string | null;
  readonly workspaceId: string | null;
  readonly projectId: string | null;
  readonly tenantId: string | null;
  readonly env: Environment;
  readonly scopes: readonly string[];
  readonly displayPrefix: string;
  readonly expiresAt: number | null;
  /**
   * True whenever the decision was made from stale or
   * incomplete policy -- not only when a never-seen key was let through. The
   * customer can log it, alert on it, or downgrade behaviour themselves.
   */
  readonly degraded: boolean;
  /** False when the key was admitted without a policy record (fail-open). */
  readonly verified: boolean;
  /**
   * What the limits said about this request, or `null` when no limit applied to
   * this key -- which is the common case -- or when the counter store could not
   * be reached and `onUnavailable` let the request through anyway. `degraded`
   * is set in that second case, so `rateLimit === null && degraded` is
   * distinguishable from "this key has no limits".
   *
   * Filled after `handle()`, because it is the one thing on this object that
   * costs a network call. Everything else here was decided synchronously.
   */
  readonly rateLimit: readonly RateLimitState[] | null;
  has(scope: string): boolean;
  /**
   * A defense-in-depth check at the call site. Compares
   * against *this request's* environment rather than the ambient one, so it
   * still means something in a handler that never entered `runInEnv`.
   */
  assertEnv(expected: Environment): void;
}

KeyringOptions

export interface KeyringOptions<R extends ResourceMap = ResourceMap> {
  /**
   * A `krsk_` vendor secret key. Never a tenant's `kr_` key.
   * Defaults to `KEYRING_SECRET_KEY`.
   */
  readonly secretKey?: string;
  /** Defaults to `KEYRING_PROJECT_ID`. */
  readonly projectId?: string;
  /**
   * The control plane's URL. No default: pass it, or set `KEYRING_BASE_URL`.
   */
  readonly baseUrl?: string;
  /**
   * Unset means one endpoint serves both environments and routes on the key,
   * which is the shape the docs lead with. Set it only for a deployment that
   * serves one environment and should refuse the other outright.
   */
  readonly env?: Environment;
  /** The tenants this node serves, so the cache is narrowed by tenant rather than by project. */
  readonly tenantIds?: readonly string[];
  readonly maxCachedKeys?: number;
  /**
   * How far the SDK may raise `maxCachedKeys` by itself before it refuses to
   * serve a project that does not fit. `DEFAULT_MAX_AUTO_CACHED_KEYS` (38,000,
   * ~32 MiB of heap on a three-scope, two-rule key; ~46 MiB on a heavier one)
   * unless set, and never below `maxCachedKeys`.
   */
  readonly maxAutoCachedKeys?: number;
  /**
   * What to do when a node that has **never served** finds the project has more
   * live keys than it can cache and no `tenantIds` narrows it.
   *
   * The default throws it, which crashes the process at boot with the actionable
   * message -- that is what "refuse to start" means, and it is deliberate. Such
   * a node can never hold a complete cache, and an incomplete cache under the
   * `stale-then-open` default admits *any* well-formed key, unverified and
   * unscoped, for the life of the process. A deploy that cannot verify keys
   * should not take traffic.
   *
   * It is **not** called on a node that is already serving when its project
   * outgrows the cache. That one keeps serving and keeps polling, reports the
   * same error through `onError` on every poll, and reports
   * `stats().policyCache.scopeTooLarge` -- the readiness predicate to drain it
   * on. Killing it instead is a fleet-wide outage of your own API one vendor
   * `PATCH` away, and stopping its poller is worse still: the node then never
   * learns another revocation.
   *
   * Set this to handle the boot case yourself -- to page, to fail a readiness
   * probe, to exit with your own code. A no-op leaves the node serving on the
   * store it has, which at boot is an empty one: every well-formed key is
   * admitted unverified until the first successful poll, and there will not be
   * one. The fail-open table in the README says what that admits.
   */
  readonly onScopeTooLarge?: (error: PolicyScopeTooLargeError) => void;
  readonly policyRefreshMs?: number;
  readonly maxStalenessMs?: number;
  readonly onUnavailable?: UnavailableMode;
  /**
   * What a request does when the *counter store* cannot be reached, which is
   * a different question from what it does when the *policy* cannot be.
   * `open` by default; see `limits/limiter.ts` for why, and why idempotency's
   * default is the opposite.
   */
  readonly rateLimitOnUnavailable?: RateLimitUnavailableMode;
  /** The legacy pre-standard header triple. On, because clients read it. */
  readonly legacyRateLimitHeaders?: boolean;
  readonly rateLimitTimeoutMs?: number;
  /**
   * `false` disables the feature; an object configures it. Enabled by
   * default -- a mutating request with no `Idempotency-Key` costs nothing,
   * and the header is how a caller opts in.
   */
  readonly idempotency?:
    | boolean
    | Omit<IdempotencyOptions, 'baseUrl' | 'secretKey' | 'fetch' | 'onError'>;
  readonly routes?: RouteRules;
  readonly scopes?: readonly string[];
  /** Resolved per request from the key's env. */
  readonly resources?: R;
  readonly cacheDir?: string;
  readonly persist?: boolean;
  readonly nodeId?: string;
  /** Set false to send nothing at all. */
  readonly telemetry?: boolean;
  readonly bufferSize?: number;
  readonly flushIntervalMs?: number;
  readonly flushEvents?: number;
  readonly maxEventsPerSecond?: number;
  readonly handleSignals?: boolean;
  readonly lazyFill?: boolean | OnDemandFillOptions;
  readonly sdkName?: string;
  readonly fetch?: FetchLike & ShipperFetch & ControlPlaneFetch;
  readonly onWarn?: (message: string, detail?: unknown) => void;
  /** Injectable so a test can exercise the environment defaults. */
  readonly processEnv?: Record<string, string | undefined>;
  readonly onError?: (error: unknown) => void;
  /** Escape hatch for tests: a store to use instead of the polled cache. */
  readonly store?: PolicyStore;
  readonly now?: () => number;
}

OnDemandFillOptions

export interface OnDemandFillOptions {
  /** Floor between two miss-triggered refreshes. A burst becomes one fetch. */
  readonly minIntervalMs?: number;
  /** Ceiling over a rolling minute, whatever the burst pattern. */
  readonly maxPerMinute?: number;
  readonly now?: () => number;
  readonly onError?: (error: unknown) => void;
}

RateLimitCheckRequest

export interface RateLimitCheckRequest {
  readonly projectId: string;
  readonly env: Environment;
  readonly keyId: string | null;
  readonly tenantId: string | null;
  readonly rules: readonly RateLimitRule[];
  readonly cost: number;
}

RateLimitDecision

export interface RateLimitDecision {
  /** False only when a limit refused. An unreachable store never lands here. */
  readonly allowed: boolean;
  /** Empty when no limits applied, or when the store could not be reached. */
  readonly states: readonly RateLimitState[];
  readonly limitedId: string | null;
  readonly retryAfterSeconds: number | null;
  /** True when the verdict is the `onUnavailable` policy's, not the store's. */
  readonly degraded: boolean;
  /** Headers to put on the response, both spellings. */
  readonly headers: Readonly<Record<string, string>>;
  /** 429 when a limit refused, 503 when the store was unreachable in `closed`. */
  readonly status: 429 | 503 | null;
  /** For the vendor's request log; never for the caller. */
  readonly reason: string | null;
}

RateLimitHeaderOptions

Re-exported from @keyring-dev/core.

Both spellings ship.

The current IETF draft (draft-ietf-httpapi-ratelimit-headers-11) is RFC 9651 structured fields: RateLimit-Policy is a dictionary of the configured limits and RateLimit a dictionary of what is left. That is the spelling a new client should read.

The legacy RateLimit-Limit / -Remaining / -Reset triple is the pre-08 draft, and it is what every client library actually in production parses. It ships by default for that reason, and it is a config flag rather than a permanent fixture because it will eventually be right to turn it off.

The triple can carry only one policy, so it carries the most constrained one -- the policy that refused, if one did, and otherwise the one with the least remaining. Reporting the first configured limit instead would tell a caller they have 9,000 requests left in the day while the per-second limit is the one refusing them.

export interface RateLimitHeaderOptions {
    /** On by default. */
    readonly legacy?: boolean;
    /** The policy that refused, when one did. */
    readonly limitedId?: string | null;
    readonly retryAfterSeconds?: number | null;
}

RateLimitTransport

export interface RateLimitTransport {
  check(request: RateLimitCheckRequest): Promise<RateLimitWire>;
}

RateLimitWire

export interface RateLimitWire {
  readonly allowed: boolean;
  readonly limited_policy: string | null;
  readonly retry_after_seconds: number | null;
  readonly policies: ReadonlyArray<{
    id: string;
    limit: number;
    window_ms: number;
    remaining: number;
    reset_ms: number;
  }>;
}

RedactOptions

Re-exported from @keyring-dev/core.

export interface RedactOptions {
    readonly replacement?: string;
    readonly maxDepth?: number;
}

RequestFacts

export interface RequestFacts {
  readonly method: string;
  readonly path: string;
  readonly token: string | null;
  /** The framework's route pattern, when it has one. Matched before `path`. */
  readonly route?: string | null;
  readonly requestId?: string | null;
}

RequestOutcome

export interface RequestOutcome<R extends ResourceMap = ResourceMap> {
  readonly decision: Decision;
  readonly rule: RouteRule | undefined;
  /** Undefined when the route is configured `skip: true`. */
  readonly context: KeyringContext<R> | undefined;
}

RingBufferStats

export interface RingBufferStats {
  readonly size: number;
  readonly capacity: number;
  readonly dropped: number;
}

RouteRule

export interface RouteRule {
  /** Per route, because a payment is not a health check. */
  readonly onUnavailable?: UnavailableMode;
  /** No verification, no `req.keyring`, no event. */
  readonly skip?: boolean;
  readonly scopes?: readonly string[];
  readonly maxStalenessMs?: number;
}

SamplerOptions

export interface SamplerOptions {
  readonly maxEventsPerSecond?: number;
  readonly now?: () => number;
  /** 0 to 1; above this the sampler tightens further. */
  readonly targetOccupancy?: number;
}

ShipperStats

export interface ShipperStats {
  readonly buffered: number;
  readonly dropped: number;
  readonly sampledOut: number;
  readonly sampleRate: number;
  readonly batchesSent: number;
  readonly batchesFailed: number;
  readonly eventsSent: number;
  /**
   * Events the control plane refused (a non-429 4xx) and the shipper therefore
   * never retried. Separate from `dropped`, which is buffer pressure: this one
   * is a protocol or payload disagreement, and it is the number that makes a
   * batch the server will never take visible in a scrape rather than only in an
   * `onWarn` callback most integrators never wire.
   */
  readonly eventsRejected: number;
  readonly rateLimitedUntil: number | null;
}

StoredResponse

export interface StoredResponse {
  readonly status: number;
  readonly headers: Readonly<Record<string, string>>;
  readonly body: string | null;
  readonly bodyStored: boolean;
}

UsageEvent

One request, as it reaches the vendor's request log, and the cross-language definition of what goes on the wire -- so the numbers here are the ones a Python or Go port will size against.

379-405 bytes per event on the wire, measured on this shape in both directions of id cardinality. The 246 B Postgres row is not the wire size: the difference is the JSON envelope, and quoting the storage figure for the wire understates a batch by about 40%.

The ring buffer is sized against neither -- it holds objects, not bytes, and 10,000 buffered events measure 2.68 MiB of V8 heap (281 B/event), which rounds to ~2.5 MB.

denial_reason is here and never in a response body: every 401 looks identical to the caller, and the reason travels separately so the vendor's own log can say which key was revoked and which never existed.

export interface UsageEvent {
  readonly ts: string;
  readonly project_id: string | null;
  readonly env: Environment;
  readonly tenant_id: string | null;
  readonly key_id: string | null;
  readonly display_prefix: string | null;
  readonly method: string;
  readonly path: string;
  readonly route: string | null;
  readonly status: number;
  readonly duration_ms: number;
  readonly degraded: boolean;
  readonly denial_reason: string | null;
  readonly request_id: string | null;
}

WinstonLogEntry

Re-exported from @keyring-dev/core.

export interface WinstonLogEntry {
    level: string;
    message: unknown;
    [key: string | symbol]: unknown;
}

Types

ControlPlaneFetch

export type ControlPlaneFetch = (
  input: string,
  init: {
    method: string;
    headers: Record<string, string>;
    body: string;
    signal: AbortSignal;
  },
) => Promise<{ status: number; json(): Promise<unknown> }>;

Decision

export type Decision = AllowDecision | DenyDecision;

IdempotencyOutcome

export type IdempotencyOutcome =
  /** Run the handler, then call `complete` or `release`. */
  | { readonly kind: 'execute'; readonly claim: IdempotencyClaim }
  /** Write this response verbatim, plus `Idempotent-Replayed: true`. */
  | { readonly kind: 'replay'; readonly response: StoredResponse }
  /** 422: same key, different request. */
  | { readonly kind: 'conflict' }
  /** 409 + `Retry-After`: a duplicate is executing right now. */
  | { readonly kind: 'in_progress'; readonly retryAfterSeconds: number }
  /** 409: the first response was over the cap and was not stored. */
  | { readonly kind: 'not_replayable' }
  /** No `Idempotency-Key`, or the route is not covered. Nothing to do. */
  | { readonly kind: 'skip' }
  /** The store could not be reached and the mode is `closed`. */
  | { readonly kind: 'unavailable'; readonly retryAfterSeconds: number }
  /** The store could not be reached and the mode is `open`. */
  | { readonly kind: 'degraded' };

IdempotencyUnavailableMode

What happens to a request carrying an Idempotency-Key when the record store cannot be reached.

closed is the default here, and open is the default for rate limiting. That asymmetry is the whole decision, and it is deliberate.

A rate limit not enforced during an outage admits excess traffic. An idempotency record not written during an outage executes a payment twice -- and the request in front of us has explicitly asked for exactly-once by carrying the header. Refusing it with 503 is not us imposing our outage on a customer who did not ask; it is us declining to promise something we cannot deliver, to a caller who is holding a retry loop and will ask again.

The blast radius is bounded by construction: a request with no Idempotency-Key never reaches this code, so a Redis outage refuses only the mutating requests whose clients were built to retry them.

open exists for a customer whose handlers are already idempotent on their own side and who would rather serve.

export type IdempotencyUnavailableMode = 'closed' | 'open';

KeyringContext

export type KeyringContext<R extends ResourceMap = ResourceMap> =
  KeyringContextBase & ResolvedResources<R>;

RateLimitUnavailableMode

What happens to a request when the counters cannot be reached.

Deliberately the same two words the policy path already uses, and deliberately not the same default.

  • open (the default). The request is served, degraded is set, and no RateLimit header is emitted -- because we do not know the numbers, and inventing them is worse than omitting them. This matches the policy path's stale-then-open in spirit and in consequence: our outage is not the customer's outage. The cost is that a limit is not enforced while we are down, which is the same cost the customer already accepts on the policy path, and much smaller: a rate limit protects against excess, not against an intruder.
  • closed. The request is refused with 503 and Retry-After. For a route where admitting unmetered traffic is the expensive failure -- an LLM call, an outbound SMS, anything the customer pays per unit for.

There is no stale-then-open here, because there is no stale state to serve from: a counter is a shared number, not a cached record, and a local one is a different limiter (static-split, measured at +12 % overshoot and -6 % under-delivery, which this version does not ship).

export type RateLimitUnavailableMode = 'open' | 'closed';

ResolvedResources

export type ResolvedResources<R extends ResourceMap> = {
  readonly [K in keyof R]: R[K]['live'] | R[K]['test'];
};

ResourceMap

The pattern the docs lead with: declare the live and test halves of a dependency once, and the middleware resolves the right one per request. It turns "remember to check env" from a discipline into a wiring decision made in one place, which is the difference between a test-mode boundary that holds and one that holds until someone is in a hurry.

export type ResourceMap = Readonly<
  Record<string, { readonly live: unknown; readonly test: unknown }>
>;

RouteRules

export type RouteRules = Readonly<Record<string, RouteRule>>;

ShipperFetch

export type ShipperFetch = (
  input: string,
  init: {
    method: string;
    headers: Record<string, string>;
    body: Uint8Array;
    signal: AbortSignal;
  },
) => Promise<{ status: number; headers: { get(name: string): string | null } }>;

UnavailableMode

The three modes for what a request does when the store cannot prove an answer.

stale-then-open is the default. The cache is complete for the project, not a partial memo, so a key absent from a fresh complete snapshot is a key that does not exist -- and that miss is a 401 in every mode, here included.

What the fail-open branch admits is therefore a function of what the store can prove, which is why complete is honoured rather than assumed: a store that has evicted under maxCachedKeys, or that was filled from a tenant-narrowed snapshot, says complete: false, and every one of its misses lands in the degraded branch instead of the 401 one. Only for a store that is complete and merely stale is the bound "keys minted inside the staleness window"; for one that is incomplete or was never loaded it is every well-formed key, for as long as that lasts. README.md states it per store state, and that table is the one an integrator decides on.

export type UnavailableMode =
  'stale-then-open' | 'stale-then-closed' | 'closed';

WinstonTransformFunction

Re-exported from @keyring-dev/core.

export type WinstonTransformFunction = (info: WinstonLogEntry, opts?: unknown) => WinstonLogEntry | boolean;

On this page