npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aranzatech/aranza-auth

v0.3.2

Published

Módulo de autenticación extensible para NestJS — JWT, refresh tokens, register/login y adapters MongoDB/Prisma/Cosmos/DynamoDB

Readme

@aranzatech/aranza-auth

Módulo de autenticación extensible para NestJS. JWT, refresh tokens, register/login y adapters MongoDB/Prisma/Cosmos/DynamoDB — sin acoplar tu dominio (org, roles, users, etc.).

Ideal para reutilizar auth en todos tus proyectos AranzaTech: instalas, configuras, extiendes el modelo y listo.


Tabla de contenidos


Instalación

npm install @aranzatech/aranza-auth

Peer dependencies (instalar en tu app Nest):

npm install @nestjs/common @nestjs/core @nestjs/jwt @nestjs/passport \
  passport passport-jwt bcryptjs \
  class-validator class-transformer reflect-metadata @nestjs/swagger

Para MongoDB agrega @nestjs/mongoose mongoose. Para Prisma agrega @prisma/client y tu PrismaService. Para Cosmos agrega @azure/cosmos. Para DynamoDB agrega @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb.

NestJS 11+ recomendado.


Inicio rápido (5 minutos)

1. Variables de entorno

MONGODB_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your-access-secret-min-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars

2. Conectar el módulo

// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { AuthModule } from "@aranzatech/aranza-auth";
import { MongoAuthModule } from "@aranzatech/aranza-auth/mongo";

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    MongooseModule.forRoot(process.env.MONGODB_URI!),
    AuthModule.forRootAsync({
      imports: [ConfigModule, MongoAuthModule.forFeature()],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.getOrThrow("JWT_SECRET"),
        refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
        expiresIn: "1h",
        refreshExpiresIn: "7d",
        identifierField: "email",
      }),
    }),
  ],
})
export class AppModule {}

3. Habilitar validación global (requerido)

Los DTOs usan class-validator. Activa el pipe global en main.ts:

import { ValidationPipe } from "@nestjs/common";

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);

4. Probar

# Register
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"SecurePass123!"}'

# Login
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"SecurePass123!"}'

# Me (usa el accessToken del login)
curl http://localhost:3000/auth/me \
  -H "Authorization: Bearer <accessToken>"

Con eso ya tienes auth funcional para un POC.


Variables de entorno

| Variable | Requerida | Descripción | |----------|-----------|-------------| | MONGODB_URI | Según adapter | Connection string de MongoDB | | DATABASE_URL | Según adapter | Connection string de Prisma/SQL | | JWT_SECRET | Sí | Secret para access tokens | | JWT_REFRESH_SECRET | Sí | Secret para refresh tokens (distinto al anterior) | | JWT_EXPIRES_IN | No | TTL access token (default 1h si lo mapeas en config) | | JWT_REFRESH_EXPIRES_IN | No | TTL refresh token (default 7d) | | JWT_ISSUER / JWT_AUDIENCE | No | Solo si configuras jwtIssuer / jwtAudience | | FRONTEND_URL | No | Base URL para links en emails (verify/reset) |

Ver .env.example en el repo como plantilla para apps consumidoras (la lib no lee .env directamente).


Configuración del módulo

AuthModule.forRootAsync() acepta un objeto AuthModuleOptions:

AuthModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    // ── JWT (requerido) ──────────────────────────────────
    secret: config.getOrThrow("JWT_SECRET"),
    refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
    expiresIn: "1h",           // access token  (default: "1h")
    refreshExpiresIn: "7d",    // refresh token (default: "7d")

    // ── Identificador de login ───────────────────────────
    identifierField: "email",  // "email" | "username" (default: "email")

    // ── Features opcionales ──────────────────────────────
    features: {
      emailVerification: false,   // default: false
      passwordReset: false,       // default: false
      refreshTokenRotation: true, // default: true
      accountLockout: false,      // default: false
    },

    // ── Lockout (si accountLockout: true) ────────────────
    lockout: {
      maxAttempts: 5,              // default: 5
      lockoutDurationMs: 15 * 60_000, // default: 15 min
    },

    // ── JWT issuer/audience (opcional) ───────────────────
    jwtIssuer: "my-api",
    jwtAudience: "my-app",

    // ── Performance (opcional) ───────────────────────────
    jwtValidationCacheTtlMs: 0,  // 0 = siempre validar en DB; max 300000 (5 min)

    // ── Ruta del controller ──────────────────────────────
    routePrefix: "auth",  // default: "auth" → /auth/login

    // ── Password policy ──────────────────────────────────
    bcryptRounds: 10,           // 10–14, default 10
    passwordComplexity: false,  // upper + lower + digit

    // ── TTL de tokens de email/reset ─────────────────────
    emailVerificationTokenTtlMs: 24 * 60 * 60 * 1000,  // 24h
    passwordResetTokenTtlMs: 15 * 60 * 1000,           // 15min

    // ── Hooks personalizados ─────────────────────────────
    hooks: AppAuthHooks,       // default: DefaultAuthHooks (Nest DI vía ModuleRef)
    // hooksProvider: { ... }, // alternativa: provider Nest completo
  }),
  // routePrefix y hooks también pueden ir aquí (nivel forRootAsync):
  // routePrefix: "auth",
  // hooks: AppAuthHooks,
}),

AuthModule.forRoot(options) acepta las mismas opciones de forma síncrona (sin imports/inject).

Orden de imports

El adapter de persistencia debe importarse dentro de AuthModule.forRootAsync:

AuthModule.forRootAsync({
  imports: [ConfigModule, MongoAuthModule.forFeature()],
  // ...
}),

Con Prisma:

import { PrismaService } from "./prisma.service";
import { PrismaAuthModule } from "@aranzatech/aranza-auth/prisma";

AuthModule.forRootAsync({
  imports: [
    ConfigModule,
    PrismaAuthModule.forFeature({
      prismaServiceToken: PrismaService,
      modelName: "authAccount",
    }),
  ],
  // ...
}),
imports: [
  MongooseModule.forRoot(...),
  AuthModule.forRootAsync({
    imports: [MongoAuthModule.forFeature(), ConfigModule],
    // ...
  }),
]

Feature flags

Todo está desactivado por defecto. Sin declarar features, obtienes el mínimo para POCs.

| Flag | Default | Qué hace | |------|---------|----------| | emailVerification | false | Envía email al register, bloquea login hasta verificar | | passwordReset | false | Habilita forgot-password y reset-password | | refreshTokenRotation | true | Guarda hash del refresh token en DB y lo rota | | accountLockout | false | Bloquea cuenta tras N intentos fallidos de login |

Modo POC (solo login/register)

// No declares features — o déjalo vacío:
AuthModule.forRootAsync({
  useFactory: () => ({
    secret: "...",
    refreshSecret: "...",
  }),
}),
  • Register crea cuenta con emailVerified: true → login inmediato.
  • Endpoints verify-email, resend-verification, forgot-password, reset-password responden 404.

Modo producción

features: {
  emailVerification: true,
  passwordReset: true,
  refreshTokenRotation: true,
},
hooks: AppAuthHooks,  // debe implementar sendEmail

Si activas emailVerification o passwordReset, debes implementar AuthHooks.sendEmail. Si no, register/forgot fallará con un error claro.


Endpoints

| Método | Ruta | Auth | Feature | Body | Respuesta | |--------|------|------|---------|------|-----------| | POST | /auth/register | — | — | { email?, username?, password } | { registered: true } | | POST | /auth/login | — | — | { email?, username?, password } | { accessToken, refreshToken } (HTTP 200) | | POST | /auth/refresh | — | — | { refreshToken } | { accessToken, refreshToken } | | POST | /auth/logout | Bearer | — | — | { loggedOut: true } | | GET | /auth/me | Bearer | — | — | objeto enriquecido via hooks | | POST | /auth/verify-email | — | emailVerification | { token } | { verified: true } | | POST | /auth/resend-verification | — | emailVerification | { email } | { sent: true } | | POST | /auth/forgot-password | — | passwordReset | { email } | { sent: true } | | POST | /auth/reset-password | — | passwordReset | { token, newPassword } | { reset: true } | | POST | /auth/change-password | Bearer | — | { currentPassword, newPassword } | { changed: true } |

Rutas usan routePrefix (default auth). Ej: routePrefix: "v1/auth"/v1/auth/login.

Errores comunes

| Código | Mensaje | Causa | |--------|---------|-------| | 401 | INVALID_CREDENTIALS | Email/username o password incorrectos | | 401 | EMAIL_NOT_VERIFIED | Feature emailVerification activa y email sin verificar | | 401 | ACCOUNT_DISABLED | Cuenta desactivada | | 401 | ACCOUNT_LOCKED | Demasiados intentos fallidos (features.accountLockout) | | 401 | PASSWORD_CHANGED | Access token emitido antes del último cambio de contraseña | | 401 | REFRESH_TOKEN_REUSE | Refresh reutilizado; sesiones revocadas | | 401 | INVALID_CURRENT_PASSWORD | Contraseña actual incorrecta en change-password | | 401 | INVALID_REFRESH_TOKEN | Refresh expirado, revocado o inválido | | 401 | UNAUTHORIZED | Sin Bearer token o token inválido en ruta protegida | | 404 | — | Feature desactivada (endpoint no disponible) | | 400 | TOKEN_INVALID_OR_EXPIRED | Token de verify/reset inválido o expirado | | 400 | PASSWORD_UNCHANGED | Nueva contraseña igual a la actual |


Proteger rutas propias

Usa el guard y el decorador exportados por la lib:

import { Controller, Get, UseGuards } from "@nestjs/common";
import {
  JwtAuthGuard,
  CurrentUser,
  type AuthJwtPayload,
} from "@aranzatech/aranza-auth";

@Controller("projects")
export class ProjectsController {
  @Get()
  @UseGuards(JwtAuthGuard)
  list(@CurrentUser() user: AuthJwtPayload) {
    // user.sub  → ID de la cuenta auth
    // user.email, user.orgId, etc. → lo que agregues en buildJwtPayload
    return { ownerId: user.sub };
  }
}

También puedes registrar JwtAuthGuard como guard global:

{ provide: APP_GUARD, useClass: JwtAuthGuard }

Rate limiting (producción)

La librería no incluye throttling interno — debes aplicarlo en tu app con @nestjs/throttler:

npm install @nestjs/throttler
import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
import { APP_GUARD } from "@nestjs/core";
import { AUTH_RATE_LIMIT_PRESETS } from "@aranzatech/aranza-auth";

@Module({
  imports: [
    ThrottlerModule.forRoot([
      AUTH_RATE_LIMIT_PRESETS.default,
      AUTH_RATE_LIMIT_PRESETS.credentials,
      AUTH_RATE_LIMIT_PRESETS.passwordReset,
    ]),
    // ...
  ],
  providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
})
export class AppModule {}

Presets exportados:

| Preset | Uso recomendado | Límite | |--------|-----------------|--------| | default | Rutas auth generales | 10 req/min | | credentials | /auth/login, /auth/register, /auth/refresh | 5 req/min | | passwordReset | /auth/forgot-password, /auth/reset-password, /auth/resend-verification | 3 req/min |

Mapa por ruta (AUTH_RATE_LIMIT_ROUTES):

import { AUTH_RATE_LIMIT_ROUTES } from "@aranzatech/aranza-auth";
// AUTH_RATE_LIMIT_ROUTES.login → preset credentials
// AUTH_RATE_LIMIT_ROUTES["resend-verification"] → preset passwordReset

Aplica @Throttle() por controlador o ruta según tu política de seguridad.

Refresh token en cookie (opcional)

import { buildRefreshTokenCookie } from "@aranzatech/aranza-auth";

@Post("login")
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const tokens = await this.authService.login(dto);
  res.setHeader("Set-Cookie", buildRefreshTokenCookie(tokens.refreshToken));
  return { accessToken: tokens.accessToken };
}

Swagger / OpenAPI

Instala @nestjs/swagger en tu app (peer dependency):

npm install @nestjs/swagger

En main.ts:

import { setupAuthSwagger } from "@aranzatech/aranza-auth";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  setupAuthSwagger(app, {
    title: "Mi API",
    description: "Documentación OpenAPI",
    path: "api", // → http://localhost:3000/api
    version: "1.0",
    features: {
      emailVerification: true,
      passwordReset: true,
      accountLockout: true,
    },
    exportPath: "./openapi.json", // opcional: escribe el spec al arrancar
  });

  await app.listen(3000);
}

Los endpoints /auth/* aparecen bajo el tag auth. Rutas protegidas usan Bearer JWT (access-token).

Para probar en Swagger UI:

  1. POST /auth/login → copia accessToken
  2. Click Authorize → pega el token
  3. Llama GET /auth/me o tus rutas con @ApiBearerAuth('access-token')

Seguridad en producción

Desde v0.2.0 la lib valida configuración al arrancar (secrets ≥32 chars, access ≠ refresh).

Opciones recomendadas

AuthModule.forRootAsync({
  useFactory: (config: ConfigService) => ({
    secret: config.getOrThrow("JWT_SECRET"),
    refreshSecret: config.getOrThrow("JWT_REFRESH_SECRET"),
    expiresIn: "30m",           // access corto en prod
    refreshExpiresIn: "7d",
    bcryptRounds: 12,           // opcional, default 10
    passwordComplexity: true,   // upper + lower + digit
    features: {
      emailVerification: true,
      passwordReset: true,
      refreshTokenRotation: true,
      accountLockout: true,
    },
    lockout: {
      maxAttempts: 5,
      lockoutDurationMs: 15 * 60_000,
    },
  }),
});

Refresh tokens en cookies (recomendado)

Usa los helpers exportados (HttpOnly, Secure, SameSite=strict):

import { buildRefreshTokenCookie, buildClearRefreshTokenCookie } from "@aranzatech/aranza-auth";

res.setHeader("Set-Cookie", buildRefreshTokenCookie(tokens.refreshToken));
// logout:
res.setHeader("Set-Cookie", buildClearRefreshTokenCookie());

Alternativa manual:

res.cookie("refreshToken", tokens.refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 7 * 24 * 60 * 60 * 1000,
});

Códigos de error

import { AuthErrorCode } from "@aranzatech/aranza-auth";

// AuthErrorCode.REFRESH_TOKEN_REUSE → posible robo de token; sesiones revocadas

Ver SECURITY.md para tradeoffs (logout vs access JWT) y reporte de vulnerabilidades.


Extender con AuthHooks

La lib maneja auth genérico. Tu dominio (org, roles, users) va en hooks:

// app-auth.hooks.ts
import { Injectable } from "@nestjs/common";
import type { AuthHooks, BaseAuthAccount } from "@aranzatech/aranza-auth";

interface MyAuthAccount extends BaseAuthAccount {
  orgId: string;
  roleId: string;
}

@Injectable()
export class AppAuthHooks implements AuthHooks<MyAuthAccount> {
  constructor(private readonly orgService: OrgService) {}

  /** Campos extra en el JWT (no incluyas `sub` — lo añade la lib) */
  async buildJwtPayload(account: MyAuthAccount) {
    return {
      orgId: account.orgId,
      roleId: account.roleId,
    };
  }

  /** Respuesta de GET /auth/me */
  async enrichMe(account: MyAuthAccount) {
    const org = await this.orgService.findById(account.orgId);
    return {
      id: account.id,
      email: account.email,
      org: { id: org.id, name: org.name },
    };
  }

  /** Validaciones antes de crear cuenta */
  async onBeforeRegister(input) {
    // ej: verificar invitación, orgId, etc.
  }

  /** Envío de emails (requerido si activas emailVerification o passwordReset) */
  async sendEmail(type: "verify" | "reset", to: string, token: string) {
    const base = process.env.FRONTEND_URL ?? "http://localhost:3001";
    const path = type === "verify" ? "verify-email" : "reset-password";
    const url = `${base}/auth/${path}?token=${token}`;
    // await this.mailer.send({ to, subject: "...", html: url });
  }

  /** Eventos estables para auditoría, métricas o webhooks */
  async onAuthEvent(event) {
    // await this.audit.write(event);
  }
}

Registra los hooks en la config:

AuthModule.forRootAsync({
  useFactory: () => ({
    secret: "...",
    refreshSecret: "...",
    hooks: AppAuthHooks,
  }),
}),

Métodos disponibles en AuthHooks

| Método | Cuándo se ejecuta | Requerido | |--------|-------------------|-----------| | buildJwtPayload | Login, refresh | Sí | | enrichMe | GET /auth/me | No (usa default) | | onBeforeRegister | Antes de crear cuenta | No | | onAfterRegister | Después de crear cuenta | No | | onAfterLogin | Después de login exitoso | No | | onAuthEvent | Register/login/refresh/logout/email/password | No | | sendEmail | Register (verify) o forgot (reset) | Sí si features de email activas |

onAuthEvent emite eventos estables como login.succeeded, login.failed, refresh.reuse_detected, logout.succeeded, email_verification.requested, password_reset.succeeded y password_change.succeeded. La librería no los persiste: tu app decide si van a logs, métricas, webhooks o una futura API Identity.

Cuando usas el controller incluido, cada evento puede traer context.ipAddress, context.userAgent y context.requestId extraídos del request HTTP. Si usas AuthService directamente, puedes pasar ese contexto como parámetro opcional.


Ejemplos Nest reales

La fase de adopción vive en ejemplos pequeños y copiables:

| Ejemplo | Backend | Ruta | |---------|---------|------| | Nest + Mongo | Mongoose/MongoDB | examples/nest-mongo | | Nest + Prisma/Postgres | Prisma + PostgreSQL | examples/nest-prisma-postgres |

La guía completa para integrar la librería en apps Nest reales está en docs/NEST_APP_GUIDE.md.


Extender el schema MongoDB

El schema base incluye: email, username, passwordHash, refreshTokenHash, emailVerified, disabled y campos de tokens.

Agregar campos de dominio

import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { baseAuthAccountSchema } from "@aranzatech/aranza-auth/mongo";

@Schema()
export class AuthAccount {
  @Prop({ type: Types.ObjectId, ref: "Organization", required: true })
  orgId!: Types.ObjectId;

  @Prop({ type: Types.ObjectId, ref: "Role", required: true })
  roleId!: Types.ObjectId;
}

export const AuthAccountSchema = SchemaFactory.createForClass(AuthAccount);
AuthAccountSchema.add(baseAuthAccountSchema);

Registrar schema extendido

import { AuthAccount, AuthAccountSchema } from "./schemas/auth-account.schema";

MongoAuthModule.forFeature({
  name: AuthAccount.name,
  schema: AuthAccountSchema,
  identifierField: "username",  // si login es por username
}),

identifierField en MongoAuthModule.forFeature() debe coincidir con el de AuthModule.

Métodos extra en MongoAuthRepository

Útiles para admin o scripts (inyecta MongoAuthRepository o implementa en tu propio repo):

| Método | Uso | |--------|-----| | setAccountDisabled(id, disabled) | Deshabilitar/habilitar cuenta | | findUnverifiedByEmail(email) | Buscar cuenta pendiente de verificación |

Índice compuesto { email: 1, disabled: 1 } incluido para lookups frecuentes.


Matriz de adapters

Los cuatro adapters oficiales implementan IAuthRepository. La diferencia real está en cómo cada base garantiza unicidad, consultas secundarias y updates condicionales.

| Capacidad | Mongo | Prisma/SQL | Cosmos DB | DynamoDB | |-----------|-------|------------|-----------|----------| | Register/login por email | Sí | Sí | Sí | Sí | | Register/login por username | Sí | Sí | Sí | Sí | | Refresh token rotation atómica | Sí | Sí | Sí, con _etag | Sí, con condition expression | | Email verification | Sí | Sí | Sí | Sí | | Password reset | Sí | Sí | Sí | Sí | | Account lockout | Sí | Sí | Sí | Sí | | Disable/enable account | Sí | Sí | Sí | Sí | | Multi-session | Sí | Sí | Sí | Sí | | Unicidad de email/username | Índices unique | @unique | Unique key policy recomendada | Sentinels transaccionales | | Infra creada por la librería | Schema Mongoose | No | No | No |

Requisitos mínimos por adapter:

| Adapter | Infra requerida | |---------|-----------------| | Mongo | MongooseModule.forRoot(...) y MongoAuthModule.forFeature(...) | | Prisma | Modelos compatibles, PrismaService y delegates configurados | | Cosmos DB | Container existente con partición consistente; unique keys recomendadas | | DynamoDB | Tabla, GSIs de lookup, GSI de sesiones por cuenta y DynamoDBDocumentClient |


Adapter Prisma

El adapter Prisma implementa el mismo IAuthRepository que Mongo. Tu app aporta el PrismaService; la librería usa el delegate configurado, por defecto prisma.authAccount.

Modelo sugerido

model AuthAccount {
  id                             String    @id @default(cuid())
  email                          String?   @unique
  username                       String?   @unique
  passwordHash                   String
  refreshTokenHash               String?
  emailVerified                  Boolean   @default(false)
  disabled                       Boolean   @default(false)
  emailVerificationTokenHash     String?
  emailVerificationExpiresAt     DateTime?
  resetTokenHash                 String?
  resetTokenExpiresAt            DateTime?
  failedLoginAttempts            Int       @default(0)
  lockedUntil                    DateTime?
  lastLoginAt                    DateTime?
  passwordChangedAt              DateTime?
  createdAt                      DateTime  @default(now())
  updatedAt                      DateTime  @updatedAt

  @@index([email, disabled])
  @@index([emailVerificationTokenHash])
  @@index([resetTokenHash])
  @@index([emailVerificationExpiresAt])
  @@index([resetTokenExpiresAt])
}

model AuthSession {
  sessionId        String    @id
  accountId        String
  refreshTokenHash String
  expiresAt        DateTime
  revokedAt        DateTime?
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt

  @@index([accountId, revokedAt])
  @@index([expiresAt])
}

Registro en Nest

import { Module } from "@nestjs/common";
import { AuthModule } from "@aranzatech/aranza-auth";
import { PrismaAuthModule } from "@aranzatech/aranza-auth/prisma";
import { PrismaService } from "./prisma.service";

@Module({
  providers: [PrismaService],
  imports: [
    AuthModule.forRootAsync({
      imports: [
        PrismaAuthModule.forFeature({
          prismaServiceToken: PrismaService,
          modelName: "authAccount",
        }),
      ],
      useFactory: () => ({
        secret: process.env.JWT_SECRET!,
        refreshSecret: process.env.JWT_REFRESH_SECRET!,
        identifierField: "email",
      }),
    }),
  ],
})
export class AppModule {}

Si tu modelo Prisma tiene otro nombre, cambia modelName para que coincida con el delegate generado (userAuthprisma.userAuth). Si usas features.multiSession, el adapter usa por defecto el delegate prisma.authSession; puedes cambiarlo con sessionModelName.


Adapter Cosmos DB

El adapter Cosmos usa un Container compatible con @azure/cosmos. La app consumidora crea el cliente, database y container; la librería solo recibe el provider del container.

Documento sugerido

{
  "kind": "auth_account",
  "id": "auth-id",
  "email": "[email protected]",
  "username": null,
  "passwordHash": "...",
  "refreshTokenHash": null,
  "emailVerified": false,
  "disabled": false,
  "emailVerificationTokenHash": null,
  "emailVerificationExpiresAt": null,
  "resetTokenHash": null,
  "resetTokenExpiresAt": null,
  "failedLoginAttempts": 0,
  "lockedUntil": null,
  "lastLoginAt": null,
  "passwordChangedAt": null,
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}

Para features.multiSession, configura un container de sesiones. Puede ser el mismo container si tu uniqueKeyPolicy no bloquea documentos sin email/username; en producción suele ser más limpio usar un container dedicado y pasarlo con sessionContainerToken.

{
  "kind": "auth_session",
  "id": "session-id",
  "sessionId": "session-id",
  "accountId": "auth-id",
  "refreshTokenHash": "...",
  "expiresAt": "2026-01-08T00:00:00.000Z",
  "revokedAt": null,
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}

Usa partition key /id para el modo simple. Si particionas por otro campo, pásalo como partitionKeyField.

Para producción, crea el container con unique keys para el identificador que uses:

{
  "uniqueKeyPolicy": {
    "uniqueKeys": [
      { "paths": ["/email"] },
      { "paths": ["/username"] }
    ]
  }
}

La librería hace un preflight anti-duplicado, pero la unique key policy es la barrera que cierra carreras concurrentes.

Registro en Nest

import { CosmosClient } from "@azure/cosmos";
import { CosmosAuthModule } from "@aranzatech/aranza-auth/cosmos";

const COSMOS_AUTH_CONTAINER = "CosmosAuthContainer";

@Module({
  providers: [
    {
      provide: COSMOS_AUTH_CONTAINER,
      useFactory: () => {
        const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);
        return client.database("app").container("auth_accounts");
      },
    },
  ],
  imports: [
    AuthModule.forRootAsync({
      imports: [
        CosmosAuthModule.forFeature({
          containerToken: COSMOS_AUTH_CONTAINER,
          sessionContainerToken: COSMOS_AUTH_CONTAINER,
          partitionKeyField: "id",
        }),
      ],
      useFactory: () => ({
        secret: process.env.JWT_SECRET!,
        refreshSecret: process.env.JWT_REFRESH_SECRET!,
      }),
    }),
  ],
})
export class AppModule {}

Adapter DynamoDB

El adapter DynamoDB usa DynamoDBDocumentClient y commands de @aws-sdk/lib-dynamodb, pasados por provider para mantener el SDK como dependencia opcional.

Tabla sugerida

  • Partition key: id (String)
  • GSI email-index: partition key email
  • GSI username-index: partition key username si usas identifierField: "username"
  • GSI email-verification-token-index: partition key emailVerificationTokenHash
  • GSI reset-token-index: partition key resetTokenHash
  • GSI session-account-index: partition key accountId para revocar todas las sesiones de una cuenta cuando features.multiSession está activo

Las fechas se guardan como strings ISO para que las comparaciones de expiración sean ordenables.

Registro en Nest

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  QueryCommand,
  TransactWriteCommand,
  UpdateCommand,
} from "@aws-sdk/lib-dynamodb";
import { DynamoAuthModule } from "@aranzatech/aranza-auth/dynamo";

const DYNAMO_AUTH_CLIENT = "DynamoAuthDocumentClient";
const DYNAMO_AUTH_COMMANDS = "DynamoAuthCommands";

@Module({
  providers: [
    {
      provide: DYNAMO_AUTH_CLIENT,
      useFactory: () => DynamoDBDocumentClient.from(new DynamoDBClient({})),
    },
    {
      provide: DYNAMO_AUTH_COMMANDS,
      useValue: {
        GetCommand,
        PutCommand,
        QueryCommand,
        TransactWriteCommand,
        UpdateCommand,
      },
    },
  ],
  imports: [
    AuthModule.forRootAsync({
      imports: [
        DynamoAuthModule.forFeature({
          clientToken: DYNAMO_AUTH_CLIENT,
          commandsToken: DYNAMO_AUTH_COMMANDS,
          tableName: "auth_accounts",
          emailIndexName: "email-index",
          usernameIndexName: "username-index",
          emailVerificationTokenHashIndexName:
            "email-verification-token-index",
          resetTokenHashIndexName: "reset-token-index",
          sessionAccountIdIndexName: "session-account-index",
        }),
      ],
      useFactory: () => ({
        secret: process.env.JWT_SECRET!,
        refreshSecret: process.env.JWT_REFRESH_SECRET!,
      }),
    }),
  ],
})
export class AppModule {}

Flujos opcionales (email y password)

Verificación de email

  1. Activa features.emailVerification: true
  2. Implementa sendEmail en hooks
  3. Register envía email con token → usuario visita link → POST /auth/verify-email con { token }
  4. Si el usuario no recibió el email: POST /auth/resend-verification con { email } → siempre { sent: true }
  5. Login bloqueado hasta emailVerified: true

Si usas identifierField: "username", el register debe incluir email además del username.

Forgot / Reset password

  1. Activa features.passwordReset: true
  2. Implementa sendEmail en hooks
  3. POST /auth/forgot-password con { email } → siempre responde { sent: true } (no revela si el email existe)
  4. Usuario recibe link → POST /auth/reset-password con { token, newPassword }
  5. Reset invalida refresh tokens activos

Multi-session / multi-device

Desde 0.4.0, la librería incluye el contrato IAuthSessionRepository y el feature flag features.multiSession.

Cuando multiSession está apagado, la librería conserva el comportamiento histórico: una sola sesión refresh por cuenta usando refreshTokenHash en el account.

Cuando multiSession está activo:

  • cada login crea una sesión independiente
  • el refresh JWT incluye sid
  • refresh rota el hash de esa sesión
  • logout revoca solo la sesión actual si el access token trae sid
  • reset/change password revocan todas las sesiones de la cuenta
  • refresh token reuse revoca todas las sesiones de la cuenta

Los adapters oficiales registran AUTH_SESSION_REPOSITORY desde su forFeature() cuando tienen la infraestructura necesaria:

| Adapter | Requisito multi-session | |---------|--------------------------| | Mongo | MongoAuthModule.forFeature() registra schema y repository automáticamente | | Prisma | Modelo/delegate AuthSession, por defecto prisma.authSession | | Cosmos DB | Container configurado; por defecto usa el mismo containerToken | | DynamoDB | sessionAccountIdIndexName para revocar todas las sesiones de una cuenta |

AuthModule.forRootAsync({
  imports: [MongoAuthModule.forFeature()],
  useFactory: () => ({
    secret: process.env.JWT_SECRET!,
    refreshSecret: process.env.JWT_REFRESH_SECRET!,
    features: {
      refreshTokenRotation: true,
      multiSession: true,
    },
  }),
});

Para adapters propios, registra un provider para AUTH_SESSION_REPOSITORY que implemente IAuthSessionRepository.


Exports públicos

| Import | Contenido | |--------|-----------| | @aranzatech/aranza-auth | AuthModule, AuthService, guards, DTOs, setupAuthSwagger, AuthErrorCode, cookie helpers, rate-limit presets | | @aranzatech/aranza-auth/cosmos | CosmosAuthModule, CosmosAuthRepository, CosmosAuthSessionRepository, tipos estructurales del adapter | | @aranzatech/aranza-auth/dynamo | DynamoAuthModule, DynamoAuthRepository, DynamoAuthSessionRepository, tipos estructurales del adapter | | @aranzatech/aranza-auth/mongo | MongoAuthModule, MongoAuthRepository, MongoAuthSessionRepository, baseAuthAccountSchema, authSessionSchema | | @aranzatech/aranza-auth/prisma | PrismaAuthModule, PrismaAuthRepository, PrismaAuthSessionRepository, tipos estructurales del adapter |

Principales exports:

| Símbolo | Uso | |---------|-----| | AuthErrorCode | Códigos de error (INVALID_CREDENTIALS, UNAUTHORIZED, …) | | AUTH_RATE_LIMIT_PRESETS / AUTH_RATE_LIMIT_ROUTES | Throttling con @nestjs/throttler | | buildRefreshTokenCookie / buildClearRefreshTokenCookie | Cookies HttpOnly para refresh | | IAuthRepository / CreateAccountData | Contrato estable para adapters propios | | IAuthSessionRepository / AuthSession | Contrato de sesiones multi-device | | AuthHooks / AuthEvent / BaseAuthAccount / AuthTokens | Contratos estables para extensión de dominio y auditoría | | MeResponseDto / ResendVerificationDto | Swagger + tipos | | AuthHooksConstructor | Tipado de hooks custom con Nest DI | | setupAuthSwagger | Swagger UI + Bearer JWT | | buildIdentityScopeClaims / readIdentityScopeFromClaims | Claims estándar para tenantId, clientId, organizationId | | createAuthEventForwarder | Forwarder HTTP opcional para enviar AuthEvent a un endpoint externo |

Tokens de inyección

import {
  AUTH_MODULE_OPTIONS,
  AUTH_HOOKS,
  AUTH_REPOSITORY,
  AUTH_SESSION_REPOSITORY,
} from "@aranzatech/aranza-auth";

Útiles si necesitas acceder a config o reemplazar el repository manualmente. AUTH_SESSION_REPOSITORY se usa cuando features.multiSession está activo.


Estabilidad de API pública

Desde 0.3.0, la librería separa explícitamente lo estable de lo interno para poder crecer hacia más adapters y, luego, hacia Identity-as-a-Service.

Estable para apps consumidoras

Estos exports forman parte del contrato público:

| Área | Contrato | |------|----------| | Módulo Nest | AuthModule, AuthModuleOptions, AuthModuleAsyncOptions | | Flujos auth | DTOs públicos, AuthService, TokenService, JwtAuthGuard, CurrentUser | | Extensión | AuthHooks, AuthHooksConstructor, AuthEvent, BaseAuthAccount, AuthTokens, RegisterInput | | Persistencia | IAuthRepository, CreateAccountData, AUTH_REPOSITORY | | Sesiones | IAuthSessionRepository, AuthSession, AUTH_SESSION_REPOSITORY | | Errores y seguridad | AuthErrorCode, cookie helpers, rate-limit presets/routes | | Adapters | MongoAuthModule, PrismaAuthModule, CosmosAuthModule, DynamoAuthModule |

Contrato de adapters

Todos los adapters deben implementar IAuthRepository. Eso significa que Mongo, Prisma, Cosmos, Dynamo y cualquier adapter futuro deben soportar las mismas capacidades base:

  • register/login por identificador
  • refresh token y rotación atómica
  • cambio y reset de password
  • verificación de email
  • lockout por intentos fallidos
  • enable/disable de cuentas
  • lecturas públicas sin secretos y lecturas internas con hashes

Los tipos estructurales como PrismaAuthDelegate, CosmosAuthContainer o DynamoAuthDocumentClient son públicos porque permiten integrar infraestructura real de la app consumidora, pero son contratos de bajo nivel del adapter. Si cambia el SDK externo de Prisma, Azure o AWS, podrían necesitar ajustes en una versión minor o major según el impacto.

Interno, no garantizado

No se considera API pública:

  • archivos dentro de __tests__/
  • fixtures, harnesses in-memory y smoke tests
  • estructura interna de servicios, estrategias o utilidades no exportadas
  • campos privados de documentos/tablas que no estén representados en BaseAuthAccount o AuthAccountWithSecrets

Política semver

| Versión | Qué puede cambiar | |---------|-------------------| | Patch | Fixes, documentación, tests, mejoras internas sin romper tipos públicos | | Minor | Nuevos adapters, nuevos exports, nuevos campos opcionales, nuevas features apagadas por defecto | | Major | Cambios incompatibles en DTOs, endpoints, IAuthRepository, AuthHooks, errores o comportamiento por defecto |

Mientras estemos en 0.x, vamos a tratar IAuthRepository, IAuthSessionRepository, AuthHooks, DTOs públicos y entrypoints de adapters como contratos estables. Si necesitamos romperlos para multi-tenant, OAuth/OIDC o Identity-as-a-Service, lo correcto será planearlo como minor o major según el impacto real.


Auditoría y eventos

La guía completa está en docs/AUDIT_EVENTS.md.

Reglas rápidas:

  • usa AuthHooks.onAuthEvent para logs, métricas, webhooks o forwarding hacia Identity
  • usa createAuthEventForwarder si quieres enviar eventos a un endpoint HTTP externo
  • no guardes passwords, refresh tokens, verification tokens ni reset tokens
  • trata identifier como dato personal porque puede ser email o username
  • usa context.requestId para correlación entre logs
  • monitorea refresh.reuse_detected como señal de riesgo alto

El forwarder es fail-open por defecto: si el endpoint externo cae, el login/refresh no se bloquea. Puedes usar failOpen: false cuando tu modelo de cumplimiento requiera bloquear el flujo si no se pudo auditar.


Identity scope

La guía completa está en docs/IDENTITY_SCOPE.md.

La librería define un vocabulario opcional para preparar multi-tenant / multi-app sin imponerlo:

| Campo | Uso | |-------|-----| | tenantId | Frontera de tenant/customer | | clientId | App/cliente que consume auth | | organizationId | Organización/workspace dentro de un tenant |

Puedes usar buildIdentityScopeClaims(account) dentro de AuthHooks.buildJwtPayload y readIdentityScopeFromClaims(user) dentro de guards propios.

Los headers x-tenant-id, x-client-id y x-organization-id se copian a AuthEvent.context.scope como metadata de auditoría. No deben usarse solos para autorizar acceso.


Identity service roadmap

La librería Nest sigue siendo integración local. El futuro Identity service debe vivir como producto HTTP separado y OpenAPI-first.

Documentos actuales:

| Documento | Uso | |-----------|-----| | docs/IDENTITY_SERVICE_OPENAPI.md | Mapa de recursos, endpoints, versionado y milestones | | docs/identity-service.openapi.yaml | Draft OpenAPI 3.1 para tenants, clients, users, auth, sessions, audit y discovery | | docs/SDK_GENERATION.md | Estrategia para SDKs Node/Python/etc. generados desde OpenAPI | | docs/IDENTITY_HTTP_CONTRACT.md | Reglas HTTP para errores, paginacion, scope headers y request IDs | | docs/IDENTITY_SERVICE_STARTER.md | Starter del futuro servicio separado AranzaIdentity | | docs/IDENTITY_SERVICE_HANDOFF.md | Handoff hacia el proyecto AranzaIdentity |

Este contrato reutiliza AuthEvent, AuthIdentityScope, sesiones y tokens como vocabulario común para futuros SDKs Node/Python/etc.


Identity Node client

La guía completa está en docs/IDENTITY_NODE_CLIENT.md.

La librería incluye un cliente mínimo para prototipos y primeros consumidores del futuro Identity service:

import { createIdentityClient } from "@aranzatech/aranza-auth";

const identity = createIdentityClient({
  baseUrl: "https://identity.example.com",
  serviceToken: process.env.IDENTITY_SERVICE_TOKEN,
});

await identity.ingestAuthEvent(event);

Este cliente es un puente de fase 5, no un SDK generado final. Está alineado con docs/identity-service.openapi.yaml.


Requisitos del proyecto consumidor

  • [ ] NestJS 11+
  • [ ] ValidationPipe global activo
  • [ ] reflect-metadata importado al inicio de main.ts
  • [ ] Un adapter configurado: Mongo, Prisma, Cosmos o Dynamo
  • [ ] La base correspondiente conectada en la app consumidora
  • [ ] Secrets JWT distintos para access y refresh
  • [ ] @nestjs/throttler en rutas /auth/* (ver Rate limiting)
  • [ ] Access token TTL corto (15–60 min) en producción
  • [ ] Si usas features de email: implementar AuthHooks.sendEmail

Limitaciones conocidas (v0.4.x)

| Limitación | Workaround / versión futura | |------------|----------------------------| | Adapters Prisma/Cosmos/Dynamo estructurales; schema/infra viven en la app consumidora | Usar los modelos sugeridos o implementar IAuthRepository propio | | Sin OAuth (Google, GitHub, etc.) | Roadmap futuro | | Multi-session es opt-in | Activar features.multiSession y configurar la infraestructura de sesiones del adapter | | jwtValidationCacheTtlMs > 0 retrasa revoke/disable en access tokens | Usar 0 si necesitas revocación inmediata |


Migración desde ≤0.2.1

  1. Actualiza a @aranzatech/[email protected].
  2. Re-login obligatorio: refresh JWT y hashes almacenados cambiaron (payload mínimo + HMAC-SHA256).
  3. Los clientes deben parsear AuthErrorCode en message (no strings legibles).
  4. Activa forbidNonWhitelisted: true en ValidationPipe.
  5. Opcional: jwtIssuer / jwtAudience, cookies con buildRefreshTokenCookie().

Desarrollo

npm install
npm run ci    # lint + tests (unit + e2e) + coverage gates + audit + build
npm run ci:local # alias local de ci
npm run release:check # ci local + npm pack dry-run
npm run test:identity:openapi # valida el draft OpenAPI de Identity
npm run test:identity:service-blueprint # valida el starter del servicio Identity separado
npm run test:identity:python # valida el cliente Python de referencia
npm test      # solo tests

El plan de estabilización vive en docs/STABILIZATION.md.

Adapter contract tests

Los adapters deben cumplir el mismo contrato IAuthRepository. La suite reusable vive en:

__tests__/helpers/auth-repository-contract.ts

Hoy corre contra Mongo, Prisma, Cosmos y Dynamo con harnesses in-memory:

__tests__/mongo-auth.repository.contract.test.ts
__tests__/prisma-auth.repository.contract.test.ts
__tests__/cosmos-auth.repository.contract.test.ts
__tests__/dynamo-auth.repository.contract.test.ts

Para nuevos adapters, crea un harness que devuelva un repositorio fresco y llama:

describeAuthRepositoryContract("MyAuthRepository", {
  createRepository: () => new MyAuthRepository(...),
});

Validaciones locales y emuladas

La suite rápida usa harnesses in-memory. Para validar adapters contra engines reales/emulados, corre estos comandos localmente antes de publicar o cuando cambies un adapter:

npm run test:integration:local      # MongoMemoryServer + Prisma SQLite
npm run test:integration:mongo      # Mongo real en memoria
npm run test:integration:prisma     # Prisma Client + SQLite
npm run test:integration:dynamo     # requiere DynamoDB Local en DYNAMODB_LOCAL_ENDPOINT
npm run test:integration:cosmos     # requiere COSMOS_ENDPOINT + COSMOS_KEY

El workflow de GitHub Actions queda intencionalmente liviano para no gastar minutos:

  • test: lint, coverage, threshold, audit y build.

Las integraciones Mongo/Prisma/Dynamo/Cosmos son locales/manuales. Nota: la imagen oficial de Cosmos DB emulator publica linux/amd64; en Macs Apple Silicon puede terminar con Exited (139) bajo carga del SDK. Para validación local estable en Mac usa un recurso Cosmos de prueba con COSMOS_ENDPOINT/COSMOS_KEY.

Smoke tests reales

Los smoke tests apuntan a infraestructura real y están apagados por defecto. Úsalos solo contra recursos temporales o dedicados de prueba:

npm run test:smoke:mongo      # requiere MONGO_SMOKE_URI
npm run test:smoke:dynamo     # requiere credenciales AWS o DYNAMODB_SMOKE_ENDPOINT
npm run test:smoke:cosmos     # requiere COSMOS_SMOKE_ENDPOINT + COSMOS_SMOKE_KEY
npm run test:smoke:postgres   # requiere PRISMA_SMOKE_DATABASE_URL + PRISMA_SMOKE_ALLOW_SCHEMA_PUSH=1

Variables útiles:

| Smoke | Variables | |-------|-----------| | Mongo | MONGO_SMOKE_URI, opcional MONGO_SMOKE_DATABASE | | DynamoDB | AWS_REGION/credenciales AWS, opcional DYNAMODB_SMOKE_ENDPOINT, DYNAMODB_SMOKE_TABLE | | Cosmos DB | COSMOS_SMOKE_ENDPOINT, COSMOS_SMOKE_KEY, opcional COSMOS_SMOKE_DATABASE_ID, COSMOS_SMOKE_CONTAINER_ID, COSMOS_SMOKE_KEEP_DATABASE=1 | | PostgreSQL/Prisma | PRISMA_SMOKE_DATABASE_URL, PRISMA_SMOKE_ALLOW_SCHEMA_PUSH=1 |

Los smoke tests crean recursos con prefijo aranza-auth-smoke-* cuando pueden y los eliminan al terminar. Para PostgreSQL usa una base dedicada: el script ejecuta prisma db push sobre PRISMA_SMOKE_DATABASE_URL.

Licencia

MIT © AranzaTech