Keyring

@keyring-dev/next

Next.js App Router route-handler wrapper, Node runtime.

Generated from packages/next/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/next is on npm under the MIT license: npm install @keyring-dev/next, on Node 22 or later. Questions: hello@keyringdev.com.

Functions

closeKeyring

Flushes the telemetry buffer and the disk snapshot, then forgets the instance, so the next getKeyring() builds a fresh one.

export async function closeKeyring(): Promise<void>;

currentKeyring

The instance this process is using, or null if nothing built one yet.

export function currentKeyring(): Keyring<ResourceMap> | null;

getKeyring

withKeyring calls this with no arguments, so a route file that configures nothing still gets a client: secretKey, projectId and baseUrl come from KEYRING_SECRET_KEY, KEYRING_PROJECT_ID and KEYRING_BASE_URL.

export function getKeyring<R extends ResourceMap = ResourceMap>(options?: KeyringOptions<R>): Keyring<R>;

withKeyring

export const GET = withKeyring(handler) in app/api/**\/route.ts, on the Node runtime.

Everything it does is translation: the decision, the three onUnavailable modes, the rate-limit modes and header spellings, the per-environment resources, the AsyncLocalStorage scope, the idempotency state machine and the telemetry ring buffer all live in @keyring-dev/sdk.

The order is the one the other two adapters use and it is not arbitrary: verification and the rate limit happen before the body is read, so an unauthenticated caller cannot get a 5 MB payload parsed on the way to a 401, and idempotency happens after, because its fingerprint covers the body.

The returned function's second parameter is required, and that is not a style choice. next build generates a type assertion per route file and checks the exported handler against RouteContext = { params: Promise<...> } via SecondArg<typeof GET>; an optional parameter makes that SegmentData<P> | undefined, undefined does not extend RouteContext, and the build fails for every TypeScript consumer with "Type ... is not a valid type for the function's second argument". There is no opt-out and next dev does not run the validator, so the route serves correctly right up to the deploy. Next.js always passes the argument; the ?? below is for a caller who is not Next.js.

export function withKeyring<P = Params>(handler: KeyringRouteHandler<P>, options: WithKeyringOptions = {}): (request: Request, context: SegmentData<P>) => Promise<Response>;

Interfaces

KeyringOptions

Re-exported from @keyring-dev/sdk.

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;
}

SegmentData

What Next.js 15 hands a route handler as its second argument. params is a promise from 15 onward; a static route gets one that resolves to {}.

export interface SegmentData<P> {
  readonly params: Promise<P>;
}

WithKeyringOptions

export interface WithKeyringOptions extends RouteRule {
  /**
   * An explicit client, for a process that runs several or for a test. Left
   * out, the wrapper uses the process-wide one from `getKeyring()`.
   */
  readonly instance?: Keyring<ResourceMap>;
  /**
   * The route *pattern* this file serves -- `'/api/orders/[id]'` -- for
   * matching `routes` rules and for the `route` column of the request log. The
   * concrete path is what a rule matches without it, which for a dynamic
   * segment is a different string on every request.
   */
  readonly route?: string;
}

Types

KeyringContext

Re-exported from @keyring-dev/sdk.

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

KeyringRouteContext

The second argument the wrapped handler receives: Next.js's own segment data with keyring on it.

KeyringContext | null and not KeyringContext -- the same property type @keyring-dev/express assigns and @keyring-dev/fastify decorates, because req.keyring's shape is one row of packages/sdk/README.md's cross-language table and an adapter that spelled it differently would make the Python and Go ports two contracts. It is null on a route configured skip: true, which is a route the customer asked us not to verify; a non-nullable field would lie about it. Unlike Express's, this one has no "from here onward" caveat: the wrapper builds the object it passes, so the property is always there.

export type KeyringRouteContext<P> = SegmentData<P> & {
  readonly keyring: KeyringContext | null;
};

KeyringRouteHandler

export type KeyringRouteHandler<P> = (
  request: Request,
  context: KeyringRouteContext<P>,
) => Response | Promise<Response>;

On this page