@zamatica/auth-nestjs
v0.1.1
Published
NestJS Guard, decorators, and helpers for the zamatica fleet-env cert auth scheme. Designed DI-trap-free for safe consumption across bun-link boundaries — see CLAUDE.md / README for the discipline rules.
Readme
@zamatica/auth-nestjs
NestJS-specific bits of the zamatica fleet-env cert auth scheme: the CertAuthGuard, @RequiresPermission and @Public decorators, the per-deploy instance-id helper, the audit log shape, and the startup permission-registry scan.
Pure cryptographic and registry primitives live in @zamatica/auth-core (framework-agnostic). This lib is the NestJS adapter layer.
Install
bun add @zamatica/auth-nestjs @zamatica/auth-corePeer deps (consumers bring their own): @nestjs/common, @nestjs/core, reflect-metadata.
The bun-link DI discipline (read before consuming!)
This lib is intentionally designed to be safe to consume across the cross-repo bun link boundary, which is the dev-time pattern for @zamatica/* consumers. Bun-link creates two copies of @nestjs/core (one in the lib's tree, one in the consumer's). NestJS DI does class-identity checks (instanceof X), and those checks fail between copies. The trap is documented in the workspace CLAUDE.md (see feedback_bun_link_nestjs_di).
Two rules:
Always register the guard via
app.useGlobalGuards(new CertAuthGuard(...)), never via theAPP_GUARDtoken.APP_GUARDgoes through NestJS's DI tree and would re-trigger the class-identity trap.useGlobalGuardstakes a pre-instantiated instance and just callscanActivate(ctx)— no DI involved at registration time.Never
@Inject()a NestJS framework class (Reflector, ApplicationConfig, HttpAdapterHost, …). This lib reads route metadata viaReflect.getMetadatadirectly —reflect-metadatais a process-global polyfill, immune to the cross-copy problem. If a future feature genuinely needs framework-internal DI, that's the signal the feature belongs in the consumer repo, not in this lib.
The CRL poller is a plain class with start()/stop() (not @Injectable()) for the same reason.
Quick wiring
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { CertAuthGuard, scanRoutesForUndeclaredPermissions } from '@zamatica/auth-nestjs';
import { parseTrustBundle } from '@zamatica/auth-core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 1. Validate every @RequiresPermission target is in @zamatica/auth-core's registry.
// Hard-fail at startup, not at request time.
const scan = scanRoutesForUndeclaredPermissions(app);
if (scan.undeclared.length > 0) {
throw new Error(`Undeclared permissions in @RequiresPermission: ${scan.undeclared.join(', ')}`);
}
// 2. Construct and register the guard.
const trustBundle = parseTrustBundle(/* …read from disk… */).value;
const guard = new CertAuthGuard({
trustBundle,
mode: process.env['MTZ_AUTH_MODE'] === 'open' ? 'open' : 'cert',
expectedFleetEnv: process.env['MTZ_FLEET_ENV'] ?? 'prod',
signatureWindowSeconds: 300,
instanceId: 'auto', // auto-generates a per-process UUID
audit: { /* sink */ },
});
app.useGlobalGuards(guard);
await app.listen(3000);
}Decorator inventory
@Public()— opt a route out of auth entirely. Use sparingly:/health,/version, anything truly anonymous.@RequiresPermission('domain.action')— declares the permission the route requires. The string must be in@zamatica/auth-core's registry (registered via@PermissionGroupsomewhere). Startup scan enforces this.
/api/* routes without @RequiresPermission(): requires any valid cert (authentication only).
/admin/* routes without @RequiresPermission(): default-DENY (defense in depth). Adding an /admin/* route without thinking about permissions → 403, not a silent privilege escalation.
Development
bunx nx build auth-nestjs
bunx nx test auth-nestjs