Skip to content

Installation

1. Install

bash
npm install @nestarc/api-keys

The published peer ranges are @nestjs/common and @nestjs/core ^10.0.0, reflect-metadata ^0.2.0, and rxjs ^7.0.0. @prisma/client ^5.0.0 || ^6.0.0 is optional and only required when you use PrismaApiKeyStorage. Version 0.3.1 verifies matching Prisma CLI/client versions 5.22.0 and 6.19.3 against PostgreSQL and performs a strict tarball consumer install on Prisma 6 without peer-dependency bypass flags. Prisma 7 is not yet in the supported range.

2. Add the Prisma model

Add the current model to your Prisma schema:

v0.3 package contents

The v0.3 npm tarballs do not include prisma/schema.example.prisma, despite the upstream README mentioning that source path. Use the model below or the v0.3.1 versioned source schema.

prisma
model ApiKey {
  id              String    @id @default(cuid())
  tenantId        String
  name            String
  environment     String
  prefix          String    @unique
  hash            String
  pepperVersion   Int       @default(1)
  scopes          String[]
  allowedIpCidrs  String[]  @default([])
  lastUsedAt      DateTime?
  expiresAt       DateTime?
  revokedAt       DateTime?
  rotatedAt       DateTime?
  replacedByKeyId String?
  createdBy       String?
  createdAt       DateTime  @default(now())

  @@index([tenantId, environment])
  @@index([tenantId, revokedAt])
  @@index([replacedByKeyId])
}

Run a migration after merging the model:

bash
npx prisma migrate dev --name add_api_keys

The raw secret is never persisted. Storage contains the safe lookup prefix, a SHA-256 hash, the pepper version, tenant and policy fields, and lifecycle timestamps.

Upgrading an existing installation

If your application already uses 0.2, add allowedIpCidrs String[] @default([]) and migrate. Existing records become unrestricted because their arrays are empty.

If you are upgrading directly from 0.1, also add rotatedAt, replacedByKeyId, createdBy, and the replacedByKeyId index introduced in 0.2. Custom storage adapters upgrading from 0.1 must implement findById() and atomic rotate(); 0.3 adds no further storage methods.

3. Register the module

typescript
import { Module } from '@nestjs/common';
import { ApiKeysModule, PrismaApiKeyStorage } from '@nestarc/api-keys';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

@Module({
  imports: [
    ApiKeysModule.forRoot({
      namespace: 'acme',
      peppers: { 1: process.env.API_KEY_PEPPER! },
      currentPepperVersion: 1,
      storage: new PrismaApiKeyStorage(prisma),
    }),
  ],
})
export class AppModule {}

currentPepperVersion defaults to the highest configured version. The module fails at startup when there are no peppers or the selected version is missing, preventing a deployment from issuing keys it cannot verify.

4. Issue your first key

typescript
import { Injectable } from '@nestjs/common';
import { ApiKeysService } from '@nestarc/api-keys';

@Injectable()
export class OnboardingService {
  constructor(private readonly apiKeys: ApiKeysService) {}

  async issuePrimaryKey(tenantId: string) {
    const { id, key } = await this.apiKeys.create({
      tenantId,
      name: 'Primary',
      environment: 'live',
      scopes: [{ resource: 'reports', level: 'read' }],
    });

    // Show `key` once. Store and reference only `id` afterward.
    return { id, key };
  }
}

5. Protect a route

typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiKeysGuard, RequireScope } from '@nestarc/api-keys';

@Controller('reports')
@UseGuards(ApiKeysGuard)
export class ReportsController {
  @Get()
  @RequireScope('reports', 'read')
  list() {
    return [];
  }
}

The guard reads the Authorization: Bearer header, verifies the key, enforces environment, IP, and scope policy, and then attaches ApiKeyContext to the request.

Module options

OptionTypeDefaultDescription
namespacestringnkProduct prefix used in issued keys.
peppersRecord<number, string>requiredServer-side hash secrets keyed by version.
currentPepperVersionnumberhighest configuredPepper used for new and replacement keys.
storageApiKeyStoragerequiredPersistence adapter.
debounceMsnumber60000Minimum interval between best-effort lastUsedAt writes.
ttlPolicyApiKeyTtlPolicynoneDefault/max lifetime and non-expiring-key policy.
onEventApiKeyEventSinknoneAudit-safe lifecycle event sink.
onEventError(error, event) => voidnoneIsolated lifecycle sink failure reporter.
emitUsageEventsbooleanfalseEnables high-volume api_key.used events.
contextWriterApiKeyContextWriternoneCopies verified context into request-local infrastructure.
clientIpResolverApiKeyClientIpResolverreads request.ipResolves the client IP for restricted keys.
onMetricApiKeyMetricSinknoneReceives bounded verification outcome and latency metrics.
onMetricError(error, metric) => voidnoneIsolated metric sink failure reporter.
onAuthFailed(prefix, code) => voidno-opLegacy authentication-failure callback; prefer lifecycle events for structured payloads.

There is no defaultEnvironment module option. create() defaults each omitted environment to live; pass environment: 'test' when issuing sandbox credentials.

Production checklist

  • Generate peppers with high entropy and keep them outside source control.
  • Configure HTTP proxy trust before relying on request.ip, or supply a verified clientIpResolver.
  • Decide whether keys may be non-expiring with ttlPolicy; do not rely on application convention alone.
  • Redact raw keys from logs, traces, error reports, and request captures.
  • Monitor event and metric sink failures through their dedicated error callbacks.

Released under the MIT License.