@keyring/next
Next.js App Router route-handler wrapper, Node runtime.
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.npm install @keyring/nextFunctions
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, exactly as report section 6.5 asks of an
adapter: 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/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/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;
/** Defaults to `KEYRING_BASE_URL`, then to the EU control plane. */
readonly baseUrl?: string;
/**
* Report section 5.3: 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;
/** Report section 6.1, fix 1: the tenants this node serves. */
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,
* ~31 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;
/**
* Report section 3: 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;
/** Report section 3.6: the pre-08 header triple. On, because clients read it. */
readonly legacyRateLimitHeaders?: boolean;
readonly rateLimitTimeoutMs?: number;
/**
* Report section 4. `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[];
/** Report section 5.3, level 2. Resolved per request from the key's env. */
readonly resources?: R;
readonly cacheDir?: string;
readonly persist?: boolean;
readonly nodeId?: string;
/** Report section 6.3. 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/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/express assigns and @keyring/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>;