@keyring/express
Express and Connect middleware.
packages/express/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/expressFunctions
keyring
app.use(keyring()) -- the reference adapter of report section 6.5, and
everything in it is translation. The decision, the three onUnavailable
modes, the rate-limit modes and header spellings, the per-environment
resources, the AsyncLocalStorage scope and the telemetry ring buffer all
live in @keyring/sdk, which is what keeps this file the size the report
says an adapter should be and what makes the Python and Go ports a week each
rather than a month.
Where to mount it, and why it matters.
- Before your routes, and before any body parser. Verification needs the
Authorizationheader and nothing else, so making it wait for a body is paying parse cost on requests that are about to be rejected -- and it is a denial-of-service shape: an unauthenticated caller gets your JSON parser to run on a 5 MB body before anyone checks their key. Rate limiting needs no body either, so it is here too, which means a caller over their limit is refused before your parser runs. - After
express.static, or the middleware verifies your favicon. - Beside, not instead of, your own authentication. Keyring answers "which tenant of yours is calling, in which environment"; it does not answer "which of your staff is calling". Mount both, in whichever order matches which one owns the route.
- Your error handler stays last. This middleware only ever calls
next()or writes its own 401/403/429/503, and never throws into yours.
Idempotency is the one thing that cannot be here, because it has to hash a
body: keyringIdempotency() is a second mount, after your body parser.
export function keyring<R extends ResourceMap = ResourceMap>(source: Keyring<R> | KeyringOptions<R>): KeyringExpressMiddleware;keyringIdempotency
app.use(express.json(), keyringIdempotency(mw.keyring)) -- report section 4.
A second mount, and it has to be, because the fingerprint covers the body
and keyring() deliberately runs before any body parser. Mount this one
after the parser and before your routes. A request with no Idempotency-Key
passes straight through and costs nothing.
The response is buffered so it can be stored, which is the one real cost:
only for requests that carry the header, and capped by the control plane's
max_body_bytes (256 KB by default, report section 4.5).
export function keyringIdempotency(instance: Keyring<ResourceMap>): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: (error?: unknown) => void) => void;Interfaces
KeyringExpressMiddleware
export interface KeyringExpressMiddleware {
(
req: ExpressLikeRequest,
res: ExpressLikeResponse,
next: (error?: unknown) => void,
): void;
readonly keyring: Keyring<ResourceMap>;
}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;
}Types
KeyringContext
Re-exported from @keyring/sdk.
export type KeyringContext<R extends ResourceMap = ResourceMap> =
KeyringContextBase & ResolvedResources<R>;