Keyring
ReferencePackages

@keyring/nest

NestJS module, guard and decorators, on the Express and Fastify platform adapters.

Generated from 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/nest

Classes

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;

On this page