@keyring/nest
NestJS module, guard and decorators, on the Express and Fastify platform adapters.
packages/nest/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/nestClasses
KeyringGuard
The authorization half of the adapter, and the reason it is a guard rather
than middleware: @SkipKeyring() is route metadata, and middleware runs
before Nest has resolved a route to read it from.
It does what @keyring/express's middleware and @keyring/fastify's
onRequest hook do and in the same order -- the decision, then the rate
limit, then request.keyring -- because the decision, the three
onUnavailable modes, the header spellings and the resources all live in
@keyring/sdk. Report section 6.5's rule is that an adapter is translation;
this file is the translation for both platforms at once.
Body parsing has already happened by the time a guard runs, which is
unavoidable on this framework: Nest's platform adapter parses before its
router. That is a difference from the other two adapters, where verification
deliberately precedes the parser, and it is the one place a Nest application
cannot be given that property from here. bodyLimit on the platform adapter
is where that bound belongs instead.
export class KeyringGuard implements CanActivate {
constructor(
@Inject(KEYRING_INSTANCE)
private readonly keyring: Keyring<ResourceMap>, private readonly reflector: Reflector);
async canActivate(context: ExecutionContext): Promise<boolean>;
}KeyringInterceptor
The two things a guard cannot do: the environment scope, because a guard has returned before the handler runs, and idempotency, because its fingerprint covers a body.
KeyringModule.forRoot() registers it globally and it authorizes nothing --
a request with no request.keyring (no guard on the route, or a route the
guard skipped) passes straight through and costs nothing. The guard is the
authorization decision and stays the customer's to wire.
export class KeyringInterceptor implements NestInterceptor {
constructor(
@Inject(KEYRING_INSTANCE)
private readonly keyring: Keyring<ResourceMap>);
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>>;
}KeyringModule
KeyringModule.forRoot({ ... }).
It provides the client, exports KeyringGuard and KEYRING_INSTANCE, and
registers one global interceptor: the environment scope, the idempotency
state machine and the usage event. That interceptor authorizes nothing and
does nothing at all on a request no guard verified, which is why it is safe
to register for you.
The guard is not registered for you, because the guard is the
authorization decision and hiding that inside an import is the wrong
default. Wire it where a Nest application wires every other one -- globally
with { provide: APP_GUARD, useClass: KeyringGuard }, or per controller with
@UseGuards(KeyringGuard).
export class KeyringModule {
static forRoot(options: KeyringModuleOptions = {}): DynamicModule;
static forRootAsync(options: KeyringModuleAsyncOptions): DynamicModule;
}Interfaces
KeyringModuleAsyncOptions
export interface KeyringModuleAsyncOptions extends Pick<
ModuleMetadata,
'imports'
> {
readonly useFactory: (
...args: never[]
) => KeyringModuleOptions | Promise<KeyringModuleOptions>;
readonly inject?: readonly unknown[];
readonly global?: boolean;
}KeyringModuleOptions
export interface KeyringModuleOptions extends KeyringOptions<ResourceMap> {
/**
* An already-constructed client, when one process runs several or when the
* application owns its lifecycle. The module never closes one it was given.
*/
readonly instance?: Keyring<ResourceMap>;
/** Visible to every module without importing it. Default `true`. */
readonly global?: boolean;
}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;
}KeyringRequest
What KeyringGuard puts on the request, for a handler that reads the raw
request rather than taking @KeyringContext():
handler(@Req() request: Request & KeyringRequest).
KeyringContext | null, the same property @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 @SkipKeyring() route -- a route the customer asked us not to
verify -- so a non-nullable field would lie about it.
The guarantee starts at the guard, the way Express's starts at the
middleware: nothing decorates the request on either platform from here, so
the property is an assignment. Nest middleware, and anything mounted on the
platform instance directly, runs above the guard and sees no property at
all. The type cannot say "defined from here onward", which is why
@KeyringContext() -- which answers null rather than undefined wherever
it is read -- is the access this package leads with.
export interface KeyringRequest {
keyring: KeyringContext | null;
}Types
KeyringContext
@KeyringContext() keyring: KeyringContext | null -- the verified context
for this request, or null.
KeyringContext | null and not KeyringContext: it is null on a route
carrying @SkipKeyring(), and on a request that never went through
KeyringGuard at all. That is the same property type @keyring/express
assigns and @keyring/fastify decorates, which is a cross-language contract
rather than a preference -- packages/sdk/README.md's table is the one the
Python and Go ports inherit.
The value and the type share a name on purpose, the way @Req() req: Request
reads in every Nest codebase.
export type KeyringContext = Context;Constants
KEYRING_INSTANCE
Injection token for the process's Keyring; KeyringModule exports it.
const KEYRING_INSTANCE: typeof KEYRING_INSTANCE;SkipKeyring
@SkipKeyring() on a handler or a controller: no verification, no context,
no usage event -- the same thing routes: { '...': { skip: true } } does in
the other two adapters, and it goes through the same Keyring.handle branch
rather than round the outside of it.
A health check is the case it exists for. It is a route the customer has declared unauthenticated; it does not make the guard optional anywhere else.
const SkipKeyring: () => MethodDecorator & ClassDecorator;