Lifecycle & Context
Version 0.2 added expiration policy, audit-safe lifecycle hooks, and stable helpers for reading or forwarding verified key context.
TTL policy
ApiKeysModule.forRoot({
namespace: 'acme',
peppers: { 1: process.env.API_KEY_PEPPER! },
storage,
ttlPolicy: {
defaultExpiresInMs: 90 * 24 * 60 * 60 * 1000,
maxExpiresInMs: 365 * 24 * 60 * 60 * 1000,
allowNeverExpires: false,
},
});| Option | Effect |
|---|---|
defaultExpiresInMs | Assigns an expiry when create() does not provide one. |
maxExpiresInMs | Rejects an expiry beyond the permitted lifetime. |
allowNeverExpires | When false, creation or rotation cannot resolve to expiresAt: null. |
An explicit expiresAt still passes through the policy. Replacement keys inherit the old expiry unless rotation overrides it.
Lifecycle events
ApiKeysModule.forRoot({
namespace: 'acme',
peppers: { 1: process.env.API_KEY_PEPPER! },
storage,
emitUsageEvents: false,
onEvent: async (event) => {
await auditLog.record(event);
},
onEventError: (error, event) => {
logger.warn({ error, eventType: event.type }, 'API key event sink failed');
},
});The sink can receive:
| Event | Emitted when |
|---|---|
api_key.created | A new key record is inserted. |
api_key.revoked | An existing key is revoked. |
api_key.rotated | A replacement and grace deadline are committed. |
api_key.auth_failed | Credential parsing or verification fails. |
api_key.used | Credential-only verification or full request authorization succeeds and emitUsageEvents is enabled. |
api_key.authorization_denied | A verified credential fails request environment, IP, or scope policy. |
Raw keys, hashes, and pepper values are never included. Event sink failures are isolated from key operations; report them through onEventError and monitor that path independently.
api_key.used is disabled by default because it can be high volume. Prefer verification metrics for aggregate success and latency signals.
Read verified context in controllers
import {
ApiKeyContext,
ApiKeysGuard,
CurrentApiKey,
} from '@nestarc/api-keys';
@UseGuards(ApiKeysGuard)
@Get()
list(@CurrentApiKey() apiKey: ApiKeyContext) {
return this.reports.listForTenant(apiKey.tenantId);
}ApiKeyContext contains:
keyIdand the safe lookupprefix;tenantIdandenvironment;- flattened scopes;
- the normalized
allowedIpCidrsarray.
For framework-level code, use getApiKeyContext(request). The stable request property name is exported as API_KEY_CONTEXT_PROPERTY and currently resolves to apiKey.
Bridge to tenancy or request-local storage
contextWriter runs only after environment, IP, and scope checks pass:
ApiKeysModule.forRoot({
namespace: 'acme',
peppers: { 1: process.env.API_KEY_PEPPER! },
storage,
contextWriter: (apiKey, request) => {
authenticatedMachineContext.write(request, {
tenantId: apiKey.tenantId,
apiKeyId: apiKey.keyId,
});
},
});This is the right boundary for tenancy, RLS, or correlation infrastructure that must only see an authorized machine identity.
RBAC composition
@nestarc/rbac can map this context to an api_key subject:
import { RbacModule } from '@nestarc/rbac';
import { createApiKeySubjectResolver } from '@nestarc/rbac/integrations/api-keys';
RbacModule.forRoot({
storage: rbacStorage,
subjectResolver: createApiKeySubjectResolver(),
tenant: { requiredByDefault: true },
});Run ApiKeysGuard before RbacGuard. API key scopes limit capabilities embedded in the credential; RBAC permissions evaluate role bindings for the key id. When both are required, both must pass.
Detached observer values
In 0.4, stored dates, operation results, lifecycle/metric payloads, and verified context are defensively copied at public boundaries. contextWriter receives an isolated copy and cannot replace the authenticated request.apiKey that reaches downstream guards. Denied request authorization produces no usage timestamp update or successful usage event. These observers remain best-effort hooks after operations, not a transactional audit log.