Idioma: English Español
Distribución: npm · Paquete: @ums/sdk-nestjs · Target: NestJS 10+
Esta es la distribución NestJS del UMS SDK. Es un adaptador delgado sobre @ums/sdk-authorization — no reimplementa el validador ni ninguna regla. Expone los mismos cuatro primitivos como Decorators idiomáticos de NestJS respaldados por un único UmsAuthGuard.
Para integración en 5 minutos, salta a quickstart.md. Ver el README TypeScript para la semántica subyacente del validador y accessor.
NestJS tiene conceptos de primera clase que mapean limpiamente a la autorización UMS:
| Concepto NestJS | Mapeo UMS SDK |
|---|---|
Decorator (@RequiresScope, etc.) |
Adjunta metadata vía Reflector |
Guard (CanActivate) |
Lee metadata, consulta AuthorizationValidator, retorna true/false |
Module (UmsSdkModule) |
Configura accessor, validator, mode, y los exporta |
| Exception filter | Mapea AuthorizationDeniedError a ForbiddenException (HTTP 403) |
| Request scope | AsyncLocalAuthGraphAccessor se integra naturalmente con el ciclo de vida de request de Nest |
Un consumidor NestJS escribe @RequiresScope en un route handler y el resto se conecta automáticamente.
@ums/sdk-nestjs
├── UmsSdkModule ← entrada de configuración .forRoot() / .forRootAsync()
├── UmsAuthGuard ← CanActivate — guard único para los cuatro primitivos
├── decorators/
│ ├── @RequiresScope
│ ├── @RequiresMenuOption
│ ├── @RequiresDomainAccess
│ └── @RequiresFeatureFlag
├── filters/
│ └── AuthorizationDeniedFilter ← mapea a ForbiddenException (HTTP 403)
└── middleware/
└── AuthGraphMiddleware ← parsea body del JWT, pobla accessor
Dependencias declaradas: @ums/sdk-authorization, @ums/sdk-contracts, @nestjs/common, @nestjs/core.
HTTP request
│
▼
AuthGraphMiddleware ← parsea JWT, pobla AsyncLocalAuthGraphAccessor
│
▼
Ruta del controller NestJS
│
▼
@UseGuards(UmsAuthGuard)
@RequiresScope("X.Y")
│
▼
UmsAuthGuard.canActivate() ← lee metadata Reflector, llama a validator.requireScope()
│
├── granted → corre handler del controller
└── denied → lanza ForbiddenException (HTTP 403)
o retorna false (si el guard está configurado así)
o solo loguea (modo audit-only)
El guard es request-scoped — lee el accessor poblado por el middleware para el request actual. Múltiples decorators en el mismo handler se evalúan en orden; la primera denegación short-circuit.
import { Module } from "@nestjs/common";
import { UmsSdkModule } from "@ums/sdk-nestjs";
@Module({
imports: [
UmsSdkModule.forRoot({
umsBaseUrl: "https://ums.example.com",
mode: "enforce", // o "audit-only"
schemaCompatibility: ">=1.0.0 <2.0.0",
}),
],
})
export class AppModule {}
UmsSdkModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
umsBaseUrl: config.get("UMS_BASE_URL")!,
mode: config.get("UMS_AUTH_MODE", "enforce"),
schemaCompatibility: ">=1.0.0 <2.0.0",
}),
});
import { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { AuthGraphMiddleware } from "@ums/sdk-nestjs";
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthGraphMiddleware).forRoutes("*");
}
}
Global:
{
provide: APP_GUARD,
useClass: UmsAuthGuard,
}
Por controller:
@Controller("orders")
@UseGuards(UmsAuthGuard)
export class OrdersController { ... }
Cuando se aplica globalmente, handlers sin decorator pasan sin modificación — el guard solo enforce cuando un decorator @Requires* está presente.
Los cuatro decorators tienen la misma forma: un identificador de target + objeto opcional de options.
@RequiresScope@Post(":id/approve")
@RequiresScope("PURCHASE_ORDER.APPROVE")
async approveOrder(@Param("id") id: string): Promise<void> { ... }
@RequiresMenuOption@Patch("stock/:id")
@RequiresMenuOption("STOCK_ADJUST")
async adjustStock(@Param("id") id: string, @Body() body: StockAdjustment): Promise<void> { ... }
@RequiresDomainAccess@Get(":id")
@RequiresDomainAccess("PURCHASE_ORDER", "VIEW")
async getOrder(@Param("id") id: string): Promise<OrderDto> { ... }
@RequiresFeatureFlag@Get(":id/pick-list")
@RequiresFeatureFlag("WMS_NEW_PICKING_UI")
async getPickList(@Param("id") id: string): Promise<PickListDto> { ... }
@Post(":id/approve-and-pick")
@RequiresScope("PURCHASE_ORDER.APPROVE")
@RequiresFeatureFlag("WMS_NEW_PICKING_UI")
async approveAndPick(@Param("id") id: string): Promise<void> { ... }
Ambos deben pasar. La primera denegación short-circuit.
@RequiresScope("X.Y", { auditOnly: true }) // sobreescribe modo global para este handler
@RequiresScope("X.Y", { onDenied: "ignore" }) // logea pero nunca bloquea, incluso en modo enforce
Comportamiento default: el guard lanza AuthorizationDeniedError, que el AuthorizationDeniedFilter (auto-registrado por UmsSdkModule.forRoot) mapea a una ForbiddenException de Nest (HTTP 403) con un body de error estructurado:
{
"statusCode": 403,
"error": "Forbidden",
"code": "AUTH_101",
"message": "Scope 'PURCHASE_ORDER.APPROVE' no concedido",
"primitive": "RequiresScope",
"target": "PURCHASE_ORDER.APPROVE",
"graphRequestId": "uuid"
}
Para customizar, provee tu propio filter:
@Catch(AuthorizationDeniedError)
export class MyDeniedFilter implements ExceptionFilter {
catch(err: AuthorizationDeniedError, host: ArgumentsHost) {
// mapeo custom — ej. redirect a /unauthorized para requests HTML
}
}
@ums/sdk-testing se reutiliza — no se necesitan utilidades de test específicas para NestJS.
import { Test } from "@nestjs/testing";
import { UmsSdkModule, UmsAuthGuard, RequiresScope } from "@ums/sdk-nestjs";
import { AuthGraphBuilder } from "@ums/sdk-testing";
@Controller("orders")
class OrdersController {
@Post(":id/approve")
@RequiresScope("PURCHASE_ORDER.APPROVE")
approve() { return { ok: true }; }
}
describe("OrdersController", () => {
it("deniega approve sin scope", async () => {
const module = await Test.createTestingModule({
imports: [UmsSdkModule.forRoot({ mode: "enforce" })],
controllers: [OrdersController],
}).compile();
const app = module.createNestApplication();
await app.init();
// Inyectar un grafo para el request de test vía un middleware solo-test
const graph = AuthGraphBuilder
.forTenant("LOGISTICS_CORE")
.withUser("ana.flores@example.com")
.build(); // sin scopes
// Usando supertest:
return request(app.getHttpServer())
.post("/orders/abc/approve")
.set("Authorization", `Bearer ${encodeFakeJwt(graph)}`)
.expect(403)
.expect((res) => {
expect(res.body.code).toBe("AUTH_101");
});
});
});
@ums/sdk-authorizationTodo en @ums/sdk-nestjs delega a @ums/sdk-authorization:
UmsAuthGuard llama a validator.requireScope(...) (o el método primitivo equivalente) desde @ums/sdk-authorization.@RequiresScope y similares usan Reflector de NestJS para adjuntar metadata; el guard la lee e invoca la misma regla equivalente a HOF.AuthGraphMiddleware usa el mismo AsyncLocalAuthGraphAccessor desde @ums/sdk-authorization.Esto es intencional: un consumidor NestJS y un consumidor TS plano de Express toman decisiones de autorización idénticas porque comparten el validador literalmente.