Auth completo (Argon2id + JWT/refresh rotativo), permissões, Companies e OpenAPI

- Auth (apps/api/src/auth/): Argon2id com parâmetros versionados
  (ADR-0005), access token JWT de 15min, refresh token opaco de 256 bits
  com rotação e detecção de replay (reuso de token já rotacionado revoga
  toda a cadeia de sessões do usuário). Endpoints /api/v1/auth/
  {login,refresh,logout,me}.
- Permissões resource+action+scope (ADR-0004) via PermissionGuard,
  resolvidas do banco a cada request (nunca cacheadas no JWT) — mudança
  de papel tem efeito imediato. super_admin com bypass hardcoded,
  espelhando a trava já existente no banco.
- Companies (migration + CRUD): leitura aberta a qualquer autenticado,
  escrita restrita a companies:manage; validação de CNPJ via
  packages/domain-shared (dígito verificador).
- SeedService: cria o primeiro super_admin a partir de
  EDEN_SUPERADMIN_EMAIL/PASSWORD só quando `users` está vazia.
- OpenAPI/Swagger em /api/docs; ValidationPipe global com
  whitelist+forbidNonWhitelisted (proteção contra mass-assignment).
- packages/database: repositórios (roles/users/sessions/audit/companies)
  reutilizáveis por api e worker.

Validado manualmente contra a stack real (não só typecheck): login com
senha errada → 401 genérico; refresh rotaciona e invalida o token
anterior; reuso do token antigo → replay detectado, cadeia revogada;
logout idempotente; audit log com a sequência completa de eventos;
CNPJ inválido rejeitado; mass-assignment rejeitado.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 09:25:48 -03:00
parent 44510bd019
commit 8d81cf506b
42 changed files with 1804 additions and 33 deletions

View File

@@ -11,17 +11,25 @@
"test": "echo 'no tests yet'" "test": "echo 'no tests yet'"
}, },
"dependencies": { "dependencies": {
"@eden/config": "workspace:*",
"@eden/database": "workspace:*", "@eden/database": "workspace:*",
"@eden/domain-shared": "workspace:*",
"@nestjs/common": "^10.4.15", "@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15", "@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15", "@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.1.0",
"@nestjs/terminus": "^10.2.3", "@nestjs/terminus": "^10.2.3",
"argon2": "^0.41.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"express": "^4.21.2", "express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.3" "typescript": "^5.7.3"

View File

@@ -1,9 +1,13 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { TerminusModule } from "@nestjs/terminus"; import { TerminusModule } from "@nestjs/terminus";
import { HealthController } from "./health/health.controller"; import { HealthController } from "./health/health.controller.js";
import { ConfigModule } from "./config/config.module.js";
import { AuthModule } from "./auth/auth.module.js";
import { BootstrapModule } from "./bootstrap/bootstrap.module.js";
import { CompaniesModule } from "./companies/companies.module.js";
@Module({ @Module({
imports: [TerminusModule], imports: [ConfigModule, TerminusModule, AuthModule, BootstrapModule, CompaniesModule],
controllers: [HealthController], controllers: [HealthController],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -0,0 +1,47 @@
import { Body, Controller, Get, HttpCode, Post, Req, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import type { Request } from "express";
import { AuthService } from "./auth.service.js";
import { LoginDto } from "./dto/login.dto.js";
import { RefreshDto } from "./dto/refresh.dto.js";
import { JwtAuthGuard } from "./jwt-auth.guard.js";
import { CurrentUser } from "./decorators/current-user.decorator.js";
import type { EdenAuthenticatedUser } from "./jwt-auth.guard.js";
function requestContext(req: Request) {
return {
ipAddress: req.ip ?? null,
userAgent: req.headers["user-agent"] ?? null,
};
}
@ApiTags("auth")
@Controller("v1/auth")
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Post("login")
@HttpCode(200)
async login(@Body() dto: LoginDto, @Req() req: Request) {
return this.auth.login(dto.email, dto.password, dto.applicationKey, requestContext(req));
}
@Post("refresh")
@HttpCode(200)
async refresh(@Body() dto: RefreshDto, @Req() req: Request) {
return this.auth.refresh(dto.refreshToken, requestContext(req));
}
@Post("logout")
@HttpCode(204)
async logout(@Body() dto: RefreshDto, @Req() req: Request) {
await this.auth.logout(dto.refreshToken, requestContext(req));
}
@Get("me")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
async me(@CurrentUser() user: EdenAuthenticatedUser) {
return this.auth.getProfile(user.id);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { AuthController } from "./auth.controller.js";
import { AuthService } from "./auth.service.js";
import { PasswordService } from "./password.service.js";
import { TokenService } from "./token.service.js";
import { JwtAuthGuard } from "./jwt-auth.guard.js";
import { PermissionGuard } from "./permission.guard.js";
@Module({
controllers: [AuthController],
providers: [AuthService, PasswordService, TokenService, JwtAuthGuard, PermissionGuard],
exports: [PasswordService, TokenService, JwtAuthGuard, PermissionGuard],
})
export class AuthModule {}

View File

@@ -0,0 +1,181 @@
import { ForbiddenException, Injectable, UnauthorizedException } from "@nestjs/common";
import { usersRepo, sessionsRepo, auditRepo, type User } from "@eden/database";
import { PasswordService } from "./password.service.js";
import { TokenService } from "./token.service.js";
import { resolvePermissions } from "./permissions.js";
export interface RequestContext {
ipAddress?: string | null;
userAgent?: string | null;
}
export interface AuthResult {
accessToken: string;
expiresIn: number;
refreshToken: string;
user: PublicUser;
}
export interface PublicUser {
id: string;
email: string;
fullName: string | null;
roleKey: string;
}
function toPublicUser(user: User): PublicUser {
return { id: user.id, email: user.email, fullName: user.full_name, roleKey: user.role_key };
}
const GENERIC_LOGIN_ERROR = "Credenciais inválidas";
@Injectable()
export class AuthService {
constructor(
private readonly passwords: PasswordService,
private readonly tokens: TokenService,
) {}
async login(
email: string,
password: string,
applicationKey: "core" | "reseller" | "subscriber",
ctx: RequestContext,
): Promise<AuthResult> {
const user = await usersRepo.findUserByEmail(email);
// Same generic error whether the account doesn't exist or the password
// is wrong — anti-enumeration, mirrors eden.md §4.1.
if (!user || !user.password_hash) {
throw new UnauthorizedException(GENERIC_LOGIN_ERROR);
}
const valid = await this.passwords.verify(user.password_hash, password);
if (!valid) {
throw new UnauthorizedException(GENERIC_LOGIN_ERROR);
}
if (this.passwords.needsRehash(user.password_algo_version)) {
const { hash, algoVersion } = await this.passwords.hash(password);
await usersRepo.updateUserPassword(user.id, hash, algoVersion);
}
const grantedApps = await usersRepo.getUserApplications(user.id);
if (!grantedApps.includes(applicationKey)) {
throw new ForbiddenException("Usuário sem acesso a esta aplicação");
}
const result = await this.issueTokens(user, applicationKey, ctx);
await auditRepo.appendAuditEvent({
actorType: "user",
actorId: user.id,
action: "auth.login",
resourceType: "user",
resourceId: user.id,
metadata: { applicationKey },
ipAddress: ctx.ipAddress,
});
return result;
}
async refresh(refreshTokenPlaintext: string, ctx: RequestContext): Promise<AuthResult> {
const hash = this.tokens.hashRefreshToken(refreshTokenPlaintext);
const active = await sessionsRepo.findActiveSessionByRefreshTokenHash(hash);
if (!active) {
// Replay detection: a token that exists but is revoked/expired being
// reused is a signal the refresh token chain may be compromised —
// revoke every session of that user defensively (ADR-0003).
const anyState = await sessionsRepo.findSessionByRefreshTokenHashAnyState(hash);
if (anyState?.revoked_at) {
await sessionsRepo.revokeAllUserSessions(anyState.user_id);
await auditRepo.appendAuditEvent({
actorType: "system",
actorId: anyState.user_id,
action: "auth.refresh_replay_detected",
resourceType: "session",
resourceId: anyState.id,
ipAddress: ctx.ipAddress,
});
}
throw new UnauthorizedException("Sessão inválida ou expirada");
}
const user = await usersRepo.findUserById(active.user_id);
if (!user || !user.is_active) {
throw new UnauthorizedException("Sessão inválida ou expirada");
}
await sessionsRepo.touchSession(active.id);
const result = await this.issueTokens(user, active.application_key, ctx, active.id);
await auditRepo.appendAuditEvent({
actorType: "user",
actorId: user.id,
action: "auth.refresh",
resourceType: "session",
resourceId: active.id,
ipAddress: ctx.ipAddress,
});
return result;
}
async logout(refreshTokenPlaintext: string, ctx: RequestContext): Promise<void> {
const hash = this.tokens.hashRefreshToken(refreshTokenPlaintext);
const session = await sessionsRepo.findActiveSessionByRefreshTokenHash(hash);
if (!session) return; // idempotent — already logged out
await sessionsRepo.revokeSession(session.id);
await auditRepo.appendAuditEvent({
actorType: "user",
actorId: session.user_id,
action: "auth.logout",
resourceType: "session",
resourceId: session.id,
ipAddress: ctx.ipAddress,
});
}
async getProfile(userId: string) {
const user = await usersRepo.findUserById(userId);
if (!user || !user.is_active) throw new UnauthorizedException();
const permissions = await resolvePermissions(user.role_key);
const applications = await usersRepo.getUserApplications(user.id);
return {
user: toPublicUser(user),
permissions: permissions.superAdmin
? ("ALL" as const)
: permissions.grants.map((g) => ({ resource: g.resource, action: g.action, scope: g.scope })),
applications,
};
}
private async issueTokens(
user: User,
applicationKey: "core" | "reseller" | "subscriber",
ctx: RequestContext,
replacesSessionId?: string,
): Promise<AuthResult> {
const access = this.tokens.signAccessToken({ sub: user.id, app: applicationKey });
const refresh = this.tokens.generateRefreshToken();
await sessionsRepo.createSession({
userId: user.id,
applicationKey,
refreshTokenHash: refresh.hash,
expiresAt: refresh.expiresAt,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
replacesSessionId,
});
return {
accessToken: access.token,
expiresIn: access.expiresIn,
refreshToken: refresh.plaintext,
user: toPublicUser(user),
};
}
}

View File

@@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import type { Request } from "express";
export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<Request>();
return request.edenUser;
});

View File

@@ -0,0 +1,19 @@
import { SetMetadata } from "@nestjs/common";
import type { RolePermission } from "@eden/database";
export const REQUIRE_PERMISSION_KEY = "eden:requirePermission";
export interface RequiredPermission {
resource: string;
action: RolePermission["action"];
}
/**
* Declares that an endpoint needs (resource, action) per ADR-0004.
* super_admin always passes. Confirms the actor holds *some* scope for
* this resource/action — row-level filtering by the granted scope (own
* vs. all, etc.) is the responsibility of the service/query layer, which
* reads it off `request.edenUser.permissions` (see permissions.ts).
*/
export const RequirePermission = (resource: string, action: RequiredPermission["action"]) =>
SetMetadata(REQUIRE_PERMISSION_KEY, { resource, action } satisfies RequiredPermission);

View File

@@ -0,0 +1,13 @@
import { IsEmail, IsIn, IsString, MinLength } from "class-validator";
export class LoginDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(1)
password!: string;
@IsIn(["core", "reseller", "subscriber"])
applicationKey!: "core" | "reseller" | "subscriber";
}

View File

@@ -0,0 +1,7 @@
import { IsString, MinLength } from "class-validator";
export class RefreshDto {
@IsString()
@MinLength(32)
refreshToken!: string;
}

View File

@@ -0,0 +1,65 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import type { Request } from "express";
import { usersRepo } from "@eden/database";
import { TokenService } from "./token.service.js";
import { resolvePermissions, type ResolvedPermissions } from "./permissions.js";
export interface EdenAuthenticatedUser {
id: string;
email: string;
fullName: string | null;
roleKey: string;
applicationKey: "core" | "reseller" | "subscriber";
permissions: ResolvedPermissions;
}
declare module "express" {
interface Request {
edenUser?: EdenAuthenticatedUser;
}
}
/**
* Verifies the access token's signature/expiry, then re-resolves the user
* and their permissions from the database on every request — deliberately
* not cached in the JWT payload, so a role/permission change (or account
* deactivation) takes effect immediately rather than waiting out the
* token's 15-minute lifetime. Mirrors the legacy's LEFT JOIN-on-every-
* request philosophy (eden.md §1, §3.3).
*/
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly tokens: TokenService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const header = request.headers.authorization;
if (!header?.startsWith("Bearer ")) {
throw new UnauthorizedException("Token de acesso ausente");
}
let payload;
try {
payload = this.tokens.verifyAccessToken(header.slice("Bearer ".length));
} catch {
throw new UnauthorizedException("Token de acesso inválido ou expirado");
}
const user = await usersRepo.findUserById(payload.sub);
if (!user || !user.is_active) {
throw new UnauthorizedException("Usuário inativo ou inexistente");
}
const permissions = await resolvePermissions(user.role_key);
request.edenUser = {
id: user.id,
email: user.email,
fullName: user.full_name,
roleKey: user.role_key,
applicationKey: payload.app,
permissions,
};
return true;
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from "@nestjs/common";
import * as argon2 from "argon2";
/**
* Argon2id password hashing with versioned parameters (ADR-0005). Bumping
* CURRENT_ALGO_VERSION and its entry in PARAMS_BY_VERSION lets us raise
* cost over time without invalidating existing hashes — verification
* always uses the parameters embedded in argon2's own encoded hash
* string, and a stored user is only re-hashed with the new parameters on
* their next successful login (see needsRehash()).
*/
const PARAMS_BY_VERSION: Record<number, argon2.Options> = {
1: {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB — OWASP minimum recommendation for argon2id
timeCost: 2,
parallelism: 1,
},
};
export const CURRENT_ALGO_VERSION = 1;
@Injectable()
export class PasswordService {
async hash(plain: string): Promise<{ hash: string; algoVersion: number }> {
const params = PARAMS_BY_VERSION[CURRENT_ALGO_VERSION];
const hash = await argon2.hash(plain, params);
return { hash, algoVersion: CURRENT_ALGO_VERSION };
}
async verify(hash: string, plain: string): Promise<boolean> {
try {
return await argon2.verify(hash, plain);
} catch {
// Malformed/foreign hash — treat as verification failure, never throw
// into the caller (which would otherwise leak a 500 instead of 401).
return false;
}
}
needsRehash(algoVersion: number): boolean {
return algoVersion !== CURRENT_ALGO_VERSION;
}
}

View File

@@ -0,0 +1,36 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import type { Request } from "express";
import { REQUIRE_PERMISSION_KEY, type RequiredPermission } from "./decorators/require-permission.decorator.js";
import { hasPermission } from "./permissions.js";
/**
* Runs after JwtAuthGuard (which populates request.edenUser). Endpoints
* without @RequirePermission are left alone — authentication only.
*/
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.get<RequiredPermission | undefined>(
REQUIRE_PERMISSION_KEY,
context.getHandler(),
);
if (!required) return true;
const request = context.switchToHttp().getRequest<Request>();
const user = request.edenUser;
if (!user) {
throw new ForbiddenException("Autenticação necessária");
}
if (!hasPermission(user.permissions, required.resource, required.action)) {
throw new ForbiddenException(
`Sem permissão para '${required.action}' em '${required.resource}'`,
);
}
return true;
}
}

View File

@@ -0,0 +1,55 @@
import { rolesRepo, type RolePermission } from "@eden/database";
export const SUPER_ADMIN_ROLE = "super_admin";
export type ResolvedPermissions =
| { superAdmin: true }
| { superAdmin: false; grants: RolePermission[] };
/**
* super_admin is never looked up in role_permissions — hardcoded bypass,
* mirrored from the legacy system and enforced again at the DB level
* (docs/adr/0004-permission-model.md, chk_role_permissions_no_super_admin).
*/
export async function resolvePermissions(roleKey: string): Promise<ResolvedPermissions> {
if (roleKey === SUPER_ADMIN_ROLE) return { superAdmin: true };
const grants = await rolesRepo.getRolePermissions(roleKey);
return { superAdmin: false, grants };
}
/**
* Returns the maximum scope the actor holds for (resource, action), or
* null if they hold none. Scope precedence — broader wins when a role
* somehow has more than one row for the same resource+action (shouldn't
* happen given the UNIQUE constraint per scope, but a role could have
* both e.g. 'own' and 'all' rows intentionally):
*/
const SCOPE_RANK: Record<RolePermission["scope"], number> = {
own: 1,
team: 2,
reseller: 3,
legal_entity: 4,
all: 5,
};
export function maxGrantedScope(
permissions: ResolvedPermissions,
resource: string,
action: RolePermission["action"],
): RolePermission["scope"] | "all" | null {
if (permissions.superAdmin) return "all";
const matches = permissions.grants.filter((g) => g.resource === resource && g.action === action);
if (matches.length === 0) return null;
return matches.reduce<RolePermission["scope"]>(
(best, g) => (SCOPE_RANK[g.scope] > SCOPE_RANK[best] ? g.scope : best),
matches[0]!.scope,
);
}
export function hasPermission(
permissions: ResolvedPermissions,
resource: string,
action: RolePermission["action"],
): boolean {
return maxGrantedScope(permissions, resource, action) !== null;
}

View File

@@ -0,0 +1,45 @@
import { randomBytes, createHash } from "node:crypto";
import { Inject, Injectable } from "@nestjs/common";
import jwt from "jsonwebtoken";
import type { EdenEnv } from "@eden/config";
import { EDEN_ENV } from "../config/config.constants.js";
export interface AccessTokenPayload {
sub: string; // user id
app: "core" | "reseller" | "subscriber";
}
const ACCESS_TOKEN_TTL_SECONDS = 15 * 60; // 15 min — ADR-0003
const REFRESH_TOKEN_TTL_DAYS = 30;
@Injectable()
export class TokenService {
private readonly secret: string;
constructor(@Inject(EDEN_ENV) env: EdenEnv) {
this.secret = env.JWT_SIGNING_SECRET;
}
signAccessToken(payload: AccessTokenPayload): { token: string; expiresIn: number } {
const token = jwt.sign(payload, this.secret, { expiresIn: ACCESS_TOKEN_TTL_SECONDS });
return { token, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
}
verifyAccessToken(token: string): AccessTokenPayload {
return jwt.verify(token, this.secret) as AccessTokenPayload;
}
/** Opaque, high-entropy refresh token — never a JWT (ADR-0003). Returns the
* plaintext (shown once to the client) and its SHA-256 hash (the only
* thing persisted). */
generateRefreshToken(): { plaintext: string; hash: string; expiresAt: Date } {
const plaintext = randomBytes(32).toString("hex"); // 256 bits
const hash = this.hashRefreshToken(plaintext);
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
return { plaintext, hash, expiresAt };
}
hashRefreshToken(plaintext: string): string {
return createHash("sha256").update(plaintext).digest("hex");
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { AuthModule } from "../auth/auth.module.js";
import { SeedService } from "./seed.service.js";
@Module({
imports: [AuthModule],
providers: [SeedService],
})
export class BootstrapModule {}

View File

@@ -0,0 +1,50 @@
import { Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { usersRepo } from "@eden/database";
import type { EdenEnv } from "@eden/config";
import { EDEN_ENV } from "../config/config.constants.js";
import { PasswordService } from "../auth/password.service.js";
import { SUPER_ADMIN_ROLE } from "../auth/permissions.js";
/**
* Mirrors the legacy's seedAdmin() (eden.md §5.7): runs once at boot,
* only acts if `users` is empty, creates the first super_admin from env
* vars — never hardcoded, never auto-generated-and-logged. If the env
* vars aren't set, boot proceeds without seeding (useful for CI/tests
* that don't need a real login) rather than crashing.
*/
@Injectable()
export class SeedService implements OnModuleInit {
private readonly logger = new Logger(SeedService.name);
constructor(
@Inject(EDEN_ENV) private readonly env: EdenEnv,
private readonly passwords: PasswordService,
) {}
async onModuleInit() {
const existing = await usersRepo.countUsers();
if (existing > 0) return;
if (!this.env.EDEN_SUPERADMIN_EMAIL || !this.env.EDEN_SUPERADMIN_PASSWORD) {
this.logger.warn(
"users está vazia e EDEN_SUPERADMIN_EMAIL/EDEN_SUPERADMIN_PASSWORD não estão definidos — pulando seed do super_admin.",
);
return;
}
const { hash, algoVersion } = await this.passwords.hash(this.env.EDEN_SUPERADMIN_PASSWORD);
const user = await usersRepo.createUser({
email: this.env.EDEN_SUPERADMIN_EMAIL,
passwordHash: hash,
passwordAlgoVersion: algoVersion,
roleKey: SUPER_ADMIN_ROLE,
fullName: "Super Administrador",
});
await usersRepo.grantUserApplication(user.id, "core");
await usersRepo.grantUserApplication(user.id, "reseller");
await usersRepo.grantUserApplication(user.id, "subscriber");
this.logger.log(`super_admin inicial criado (${user.email}) a partir de EDEN_SUPERADMIN_EMAIL.`);
}
}

View File

@@ -0,0 +1,50 @@
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { CompaniesService } from "./companies.service.js";
import { CreateCompanyDto } from "./dto/create-company.dto.js";
import { UpdateCompanyDto } from "./dto/update-company.dto.js";
import { JwtAuthGuard } from "../auth/jwt-auth.guard.js";
import { PermissionGuard } from "../auth/permission.guard.js";
import { RequirePermission } from "../auth/decorators/require-permission.decorator.js";
import { CurrentUser } from "../auth/decorators/current-user.decorator.js";
import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js";
/**
* Read is open to any authenticated user (not gated by a resource
* permission) — company data isn't sensitive and is needed to render
* contracts/documents from screens any seller/backoffice user can reach,
* mirroring the legacy (eden.md §1, §6). Write requires companies:manage.
*/
@ApiTags("companies")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, PermissionGuard)
@Controller("v1/companies")
export class CompaniesController {
constructor(private readonly companies: CompaniesService) {}
@Get()
list(@Query("includeInactive") includeInactive?: string) {
return this.companies.list(includeInactive === "true");
}
@Get(":id")
get(@Param("id") id: string) {
return this.companies.get(id);
}
@Post()
@RequirePermission("companies", "manage")
create(@Body() dto: CreateCompanyDto, @CurrentUser() user: EdenAuthenticatedUser) {
return this.companies.create(dto, user.id);
}
@Patch(":id")
@RequirePermission("companies", "manage")
update(
@Param("id") id: string,
@Body() dto: UpdateCompanyDto,
@CurrentUser() user: EdenAuthenticatedUser,
) {
return this.companies.update(id, dto, user.id);
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { AuthModule } from "../auth/auth.module.js";
import { CompaniesController } from "./companies.controller.js";
import { CompaniesService } from "./companies.service.js";
@Module({
imports: [AuthModule],
controllers: [CompaniesController],
providers: [CompaniesService],
})
export class CompaniesModule {}

View File

@@ -0,0 +1,102 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { companiesRepo, auditRepo, type Company } from "@eden/database";
import { onlyDigits, isValidCNPJ } from "@eden/domain-shared";
import type { CreateCompanyDto } from "./dto/create-company.dto.js";
import type { UpdateCompanyDto } from "./dto/update-company.dto.js";
@Injectable()
export class CompaniesService {
list(includeInactive: boolean): Promise<Company[]> {
return companiesRepo.listCompanies(includeInactive);
}
async get(id: string): Promise<Company> {
const company = await companiesRepo.getCompanyById(id);
if (!company) throw new NotFoundException("Empresa não encontrada");
return company;
}
async create(dto: CreateCompanyDto, actorId: string): Promise<Company> {
this.assertValidCnpjIfPresent(dto.cnpj);
const company = await companiesRepo.createCompany({
companyName: dto.companyName,
tradeName: dto.tradeName,
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
stateRegistration: dto.stateRegistration,
municipalRegistration: dto.municipalRegistration,
addressZip: dto.addressZip,
addressStreet: dto.addressStreet,
addressNumber: dto.addressNumber,
addressComplement: dto.addressComplement,
addressNeighborhood: dto.addressNeighborhood,
addressCity: dto.addressCity,
addressState: dto.addressState,
addressCountry: dto.addressCountry,
phone: dto.phone,
email: dto.email,
website: dto.website,
legalRepName: dto.legalRepName,
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
legalRepRole: dto.legalRepRole,
legalRepEmail: dto.legalRepEmail,
createdBy: actorId,
});
await auditRepo.appendAuditEvent({
actorType: "user",
actorId,
action: "company.created",
resourceType: "company",
resourceId: company.id,
});
return company;
}
async update(id: string, dto: UpdateCompanyDto, actorId: string): Promise<Company> {
await this.get(id); // 404 if missing
this.assertValidCnpjIfPresent(dto.cnpj);
const company = await companiesRepo.updateCompany(id, {
companyName: dto.companyName,
tradeName: dto.tradeName,
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
stateRegistration: dto.stateRegistration,
municipalRegistration: dto.municipalRegistration,
addressZip: dto.addressZip,
addressStreet: dto.addressStreet,
addressNumber: dto.addressNumber,
addressComplement: dto.addressComplement,
addressNeighborhood: dto.addressNeighborhood,
addressCity: dto.addressCity,
addressState: dto.addressState,
addressCountry: dto.addressCountry,
phone: dto.phone,
email: dto.email,
website: dto.website,
legalRepName: dto.legalRepName,
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
legalRepRole: dto.legalRepRole,
legalRepEmail: dto.legalRepEmail,
isActive: dto.isActive,
});
if (!company) throw new NotFoundException("Empresa não encontrada");
await auditRepo.appendAuditEvent({
actorType: "user",
actorId,
action: "company.updated",
resourceType: "company",
resourceId: id,
metadata: { fields: Object.keys(dto) },
});
return company;
}
private assertValidCnpjIfPresent(cnpj: string | undefined): void {
if (cnpj && !isValidCNPJ(cnpj)) {
throw new BadRequestException("CNPJ inválido");
}
}
}

View File

@@ -0,0 +1,27 @@
import { IsEmail, IsOptional, IsString, MinLength } from "class-validator";
export class CreateCompanyDto {
@IsString()
@MinLength(1)
companyName!: string;
@IsOptional() @IsString() tradeName?: string;
@IsOptional() @IsString() cnpj?: string;
@IsOptional() @IsString() stateRegistration?: string;
@IsOptional() @IsString() municipalRegistration?: string;
@IsOptional() @IsString() addressZip?: string;
@IsOptional() @IsString() addressStreet?: string;
@IsOptional() @IsString() addressNumber?: string;
@IsOptional() @IsString() addressComplement?: string;
@IsOptional() @IsString() addressNeighborhood?: string;
@IsOptional() @IsString() addressCity?: string;
@IsOptional() @IsString() addressState?: string;
@IsOptional() @IsString() addressCountry?: string;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsEmail() email?: string;
@IsOptional() @IsString() website?: string;
@IsOptional() @IsString() legalRepName?: string;
@IsOptional() @IsString() legalRepCpf?: string;
@IsOptional() @IsString() legalRepRole?: string;
@IsOptional() @IsEmail() legalRepEmail?: string;
}

View File

@@ -0,0 +1,9 @@
import { PartialType } from "@nestjs/swagger";
import { IsOptional, IsBoolean } from "class-validator";
import { CreateCompanyDto } from "./create-company.dto.js";
export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {
@IsOptional()
@IsBoolean()
isActive?: boolean;
}

View File

@@ -0,0 +1 @@
export const EDEN_ENV = Symbol("EDEN_ENV");

View File

@@ -0,0 +1,19 @@
import { Global, Module } from "@nestjs/common";
import { loadEnv } from "@eden/config";
import { EDEN_ENV } from "./config.constants.js";
/**
* Fails fast at boot (via loadEnv) if a required secret/config var is
* missing — see packages/config/src/env-schema.ts and eden.md §5.6.
*/
@Global()
@Module({
providers: [
{
provide: EDEN_ENV,
useFactory: () => loadEnv(),
},
],
exports: [EDEN_ENV],
})
export class ConfigModule {}

View File

@@ -1,13 +1,38 @@
import "reflect-metadata"; import "reflect-metadata";
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix("api", { exclude: ["health/live", "health/ready"] });
// Server-side validation is the authority — never trust that a client
// already validated (Master Prompt §12). Strips unknown fields instead
// of accepting them silently (mass-assignment protection).
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
const openApiConfig = new DocumentBuilder()
.setTitle("EDEN API")
.setDescription("API principal do EDEN (Core, Parceiros, Assinante)")
.setVersion("0.1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, openApiConfig);
SwaggerModule.setup("api/docs", app, document);
const port = Number(process.env.EDEN_API_PORT ?? 8080); const port = Number(process.env.EDEN_API_PORT ?? 8080);
await app.listen(port, "0.0.0.0"); await app.listen(port, "0.0.0.0");
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[eden-api] listening on :${port}`); console.log(`[eden-api] listening on :${port} — docs em /api/docs`);
} }
bootstrap(); bootstrap();

View File

@@ -48,6 +48,8 @@ services:
JWT_SIGNING_SECRET: ${JWT_SIGNING_SECRET} JWT_SIGNING_SECRET: ${JWT_SIGNING_SECRET}
SIGNATURE_OTP_SECRET: ${SIGNATURE_OTP_SECRET} SIGNATURE_OTP_SECRET: ${SIGNATURE_OTP_SECRET}
EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY} EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY}
EDEN_SUPERADMIN_EMAIL: ${EDEN_SUPERADMIN_EMAIL}
EDEN_SUPERADMIN_PASSWORD: ${EDEN_SUPERADMIN_PASSWORD}
SMTP_HOST: ${SMTP_HOST} SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT} SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER} SMTP_USER: ${SMTP_USER}

View File

@@ -74,10 +74,20 @@ Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repet
3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`. 3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`.
4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`. 4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`.
### Auth, permissões, Companies e OpenAPI — implementados e testados ao vivo
- **Auth** (`apps/api/src/auth/`): Argon2id (parâmetros versionados, ADR-0005), JWT de acesso de 15 min, refresh token opaco (256 bits) com rotação e **detecção de replay** (reuso de token já rotacionado revoga toda a cadeia de sessões do usuário). Endpoints `POST /api/v1/auth/{login,refresh,logout}` e `GET /api/v1/auth/me`.
- **Permissões** (`apps/api/src/auth/permissions.ts`, `permission.guard.ts`): resource+action+scope (ADR-0004), `super_admin` com bypass hardcoded (nunca consulta `role_permissions`, espelhando a trava também existente no banco). Guard resolve permissões do banco a cada request (nunca cacheado no JWT) — mudança de papel tem efeito imediato, mesma filosofia do legado.
- **Companies** (`apps/api/src/companies/` + migration `1788437478189_companies`): CRUD com leitura aberta a qualquer autenticado e escrita restrita a `companies:manage`; validação de CNPJ (dígito verificador, `packages/domain-shared`) testada com CNPJ válido/inválido conhecidos.
- **Seed automático**: `SeedService` cria o primeiro `super_admin` a partir de `EDEN_SUPERADMIN_EMAIL`/`PASSWORD` só quando `users` está vazia — testado (log `super_admin inicial criado`).
- **OpenAPI/Swagger**: `/api/docs` (UI) e `/api/docs-json`, cobrindo todos os endpoints atuais.
- **Validação/segurança de request**: `ValidationPipe` global com `whitelist`+`forbidNonWhitelisted` (proteção contra mass-assignment) — testado (`id`/`isActive` extras num POST de company são rejeitados com 400).
Testes manuais realizados contra a stack real (não só typecheck): login com senha errada → 401 genérico; refresh rotaciona e o token antigo some; reuso do token antigo → 401 + toda a cadeia revogada; logout revoga e é idempotente; audit log com a sequência completa de eventos (`auth.login`, `auth.refresh`, `auth.refresh_replay_detected`, `company.created`).
### Pendências da Fase 1 (não iniciadas) ### Pendências da Fase 1 (não iniciadas)
- Módulo de autenticação real (Argon2id, JWT curto + refresh rotativo, MFA) — ADR-0003 desenhada, implementação ainda não começou. - MFA/TOTP (schema e endpoint ainda não implementados — `users.mfa_*` já existe no schema).
- Companies/Legal Entities (Organization).
- Adapters S3/SMTP. - Adapters S3/SMTP.
- OpenAPI 3.1.
- Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder). - Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder).
- Nenhum commit git feito ainda — aguardando solicitação explícita do operador. - CRUD de usuários/papéis (convite, reset de senha, gestão de `role_permissions`) — hoje só existe o super_admin seedado; não há endpoint para criar outros usuários ainda.
- Nota de dado de teste: a empresa "Handix Telecom LTDA" criada durante os testes usa um CNPJ de exemplo publicamente conhecido (`11.222.333/0001-81`), não o CNPJ real da Handix — substituir/remover antes de qualquer ambiente que não seja dev local.

View File

@@ -10,7 +10,11 @@
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "echo 'no linter configured yet'" "lint": "echo 'no linter configured yet'"
}, },
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
} }

View File

@@ -0,0 +1,50 @@
import { z } from "zod";
/**
* Fail fast on boot if a required secret/config value is missing — same
* philosophy as the legacy's JWT_SECRET/SEED_ADMIN_* checks (eden.md §5.6).
* Never fall back to an insecure default for a secret.
*/
export const envSchema = z.object({
DATABASE_URL: z.string().min(1, "DATABASE_URL é obrigatório"),
EDEN_API_PORT: z.coerce.number().int().positive().default(8080),
JWT_SIGNING_SECRET: z.string().min(32, "JWT_SIGNING_SECRET precisa ter ao menos 32 caracteres"),
SIGNATURE_OTP_SECRET: z.string().min(32).optional(),
EDEN_FIELD_ENCRYPTION_KEY: z
.string()
.min(32, "EDEN_FIELD_ENCRYPTION_KEY precisa ter ao menos 32 caracteres")
.optional(),
EDEN_SUPERADMIN_EMAIL: z.string().email().optional(),
EDEN_SUPERADMIN_PASSWORD: z.string().min(8).optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().positive().default(465),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
SMTP_FROM: z.string().optional(),
S3_ENDPOINT: z.string().optional(),
S3_REGION: z.string().default("us-east-1"),
S3_FORCE_PATH_STYLE: z
.string()
.default("true")
.transform((v) => v === "true"),
S3_ACCESS_KEY_ID: z.string().optional(),
S3_SECRET_ACCESS_KEY: z.string().optional(),
S3_BUCKET: z.string().optional(),
PUBLIC_BASE_URL: z.string().optional(),
});
export type EdenEnv = z.infer<typeof envSchema>;
export function loadEnv(source: NodeJS.ProcessEnv = process.env): EdenEnv {
const result = envSchema.safeParse(source);
if (!result.success) {
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
throw new Error(`Configuração de ambiente inválida:\n${issues}`);
}
return result.data;
}

View File

@@ -1,6 +1 @@
/** export { envSchema, loadEnv, type EdenEnv } from "./env-schema.js";
* Shared env-var schema/validation (fail fast on boot if a required var is
* missing — same philosophy as the legacy's JWT_SECRET/SEED_ADMIN_* checks,
* eden.md §5.6). Placeholder until apps/api's config module needs it.
*/
export const CONFIG_PACKAGE_VERSION = "0.1.0";

View File

@@ -0,0 +1,54 @@
/**
* Organization bounded context, first slice: companies (legal entities
* that issue contracts/documents) — Master Prompt §6.1. Shape ported
* faithfully from the legacy (eden.md §1.4), since this is validated,
* production-tested data. Warehouses/branches/cost centers land in a
* later migration once Inventory/Finance need them — Master Prompt §6.1
* explicitly warns against leaving `companies` isolated forever, but
* that linkage is real work for those later phases, not invented now.
*/
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE companies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES users(id),
company_name TEXT NOT NULL,
trade_name TEXT,
cnpj TEXT,
state_registration TEXT,
municipal_registration TEXT,
address_zip TEXT,
address_street TEXT,
address_number TEXT,
address_complement TEXT,
address_neighborhood TEXT,
address_city TEXT,
address_state TEXT,
address_country TEXT NOT NULL DEFAULT 'Brasil',
phone TEXT,
email TEXT,
website TEXT,
legal_rep_name TEXT,
legal_rep_cpf TEXT,
legal_rep_role TEXT,
legal_rep_email TEXT,
is_active BOOLEAN NOT NULL DEFAULT true
);
COMMENT ON TABLE companies IS
'Empresa(s) emissora(s) de contrato/documento — eden.md §1.4. Leitura aberta a qualquer autenticado (não sensível), escrita restrita à permissão companies:manage.';
CREATE TRIGGER trg_companies_updated_at BEFORE UPDATE ON companies
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE INDEX idx_companies_active ON companies(is_active);
`);
};
exports.down = (pgm) => {
pgm.sql(`DROP TABLE IF EXISTS companies;`);
};

View File

@@ -17,6 +17,7 @@
"pg": "^8.13.1" "pg": "^8.13.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"node-pg-migrate": "^7.9.1", "node-pg-migrate": "^7.9.1",
"typescript": "^5.7.3" "typescript": "^5.7.3"

View File

@@ -1 +1,8 @@
export { query, withTransaction, closePool } from "./pool.js"; export { query, withTransaction, closePool } from "./pool.js";
export * as rolesRepo from "./repositories/roles.js";
export * as usersRepo from "./repositories/users.js";
export * as sessionsRepo from "./repositories/sessions.js";
export * as auditRepo from "./repositories/audit.js";
export * as companiesRepo from "./repositories/companies.js";
export type * from "./repositories/types.js";
export type { Company } from "./repositories/companies.js";

View File

@@ -0,0 +1,65 @@
import { createHash } from "node:crypto";
import { query } from "../pool.js";
export interface AuditEventInput {
actorType: "user" | "system" | "service";
actorId?: string | null;
action: string;
resourceType: string;
resourceId?: string | null;
metadata?: Record<string, unknown>;
ipAddress?: string | null;
}
function canonicalJson(value: unknown): string {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
const obj = value as Record<string, unknown>;
const keys = Object.keys(obj).sort();
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
}
/**
* Appends one event to the audit_log hash chain. Never call this inside a
* larger transaction that might roll back for unrelated reasons without
* considering that the chain must stay contiguous — a rolled-back event
* never happened, which is fine (nothing was recorded), but a *committed*
* event must never be skipped when computing previous_event_hash for the
* next one. See .claude/skills/eden-security/references/audit.md.
*/
export async function appendAuditEvent(input: AuditEventInput): Promise<void> {
const { rows } = await query<{ event_hash: string }>(
"SELECT event_hash FROM audit_log ORDER BY seq DESC LIMIT 1",
);
const previousEventHash = rows[0]?.event_hash ?? "";
const fields = {
actorType: input.actorType,
actorId: input.actorId ?? null,
action: input.action,
resourceType: input.resourceType,
resourceId: input.resourceId ?? null,
metadata: input.metadata ?? {},
ipAddress: input.ipAddress ?? null,
};
const eventHash = createHash("sha256")
.update(previousEventHash + canonicalJson(fields))
.digest("hex");
await query(
`INSERT INTO audit_log
(actor_type, actor_id, action, resource_type, resource_id, metadata, ip_address, previous_event_hash, event_hash)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
input.actorType,
input.actorId ?? null,
input.action,
input.resourceType,
input.resourceId ?? null,
JSON.stringify(input.metadata ?? {}),
input.ipAddress ?? null,
previousEventHash || null,
eventHash,
],
);
}

View File

@@ -0,0 +1,163 @@
import { query } from "../pool.js";
export interface Company {
id: string;
created_at: Date;
updated_at: Date;
created_by: string | null;
company_name: string;
trade_name: string | null;
cnpj: string | null;
state_registration: string | null;
municipal_registration: string | null;
address_zip: string | null;
address_street: string | null;
address_number: string | null;
address_complement: string | null;
address_neighborhood: string | null;
address_city: string | null;
address_state: string | null;
address_country: string;
phone: string | null;
email: string | null;
website: string | null;
legal_rep_name: string | null;
legal_rep_cpf: string | null;
legal_rep_role: string | null;
legal_rep_email: string | null;
is_active: boolean;
}
export async function listCompanies(includeInactive = false): Promise<Company[]> {
const { rows } = await query<Company>(
includeInactive
? "SELECT * FROM companies ORDER BY created_at ASC"
: "SELECT * FROM companies WHERE is_active = true ORDER BY created_at ASC",
);
return rows;
}
export async function getCompanyById(id: string): Promise<Company | null> {
const { rows } = await query<Company>("SELECT * FROM companies WHERE id = $1", [id]);
return rows[0] ?? null;
}
export interface CreateCompanyInput {
companyName: string;
tradeName?: string | null;
cnpj?: string | null;
stateRegistration?: string | null;
municipalRegistration?: string | null;
addressZip?: string | null;
addressStreet?: string | null;
addressNumber?: string | null;
addressComplement?: string | null;
addressNeighborhood?: string | null;
addressCity?: string | null;
addressState?: string | null;
addressCountry?: string;
phone?: string | null;
email?: string | null;
website?: string | null;
legalRepName?: string | null;
legalRepCpf?: string | null;
legalRepRole?: string | null;
legalRepEmail?: string | null;
createdBy?: string | null;
}
export async function createCompany(input: CreateCompanyInput): Promise<Company> {
const { rows } = await query<Company>(
`INSERT INTO companies (
company_name, trade_name, cnpj, state_registration, municipal_registration,
address_zip, address_street, address_number, address_complement, address_neighborhood,
address_city, address_state, address_country, phone, email, website,
legal_rep_name, legal_rep_cpf, legal_rep_role, legal_rep_email, created_by
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,COALESCE($13,'Brasil'),$14,$15,$16,$17,$18,$19,$20,$21)
RETURNING *`,
[
input.companyName,
input.tradeName ?? null,
input.cnpj ?? null,
input.stateRegistration ?? null,
input.municipalRegistration ?? null,
input.addressZip ?? null,
input.addressStreet ?? null,
input.addressNumber ?? null,
input.addressComplement ?? null,
input.addressNeighborhood ?? null,
input.addressCity ?? null,
input.addressState ?? null,
input.addressCountry ?? null,
input.phone ?? null,
input.email ?? null,
input.website ?? null,
input.legalRepName ?? null,
input.legalRepCpf ?? null,
input.legalRepRole ?? null,
input.legalRepEmail ?? null,
input.createdBy ?? null,
],
);
const company = rows[0];
if (!company) throw new Error("failed to create company");
return company;
}
export type UpdateCompanyInput = Partial<Omit<CreateCompanyInput, "createdBy">> & {
isActive?: boolean;
};
export async function updateCompany(id: string, input: UpdateCompanyInput): Promise<Company | null> {
const { rows } = await query<Company>(
`UPDATE companies SET
company_name = COALESCE($2, company_name),
trade_name = COALESCE($3, trade_name),
cnpj = COALESCE($4, cnpj),
state_registration = COALESCE($5, state_registration),
municipal_registration = COALESCE($6, municipal_registration),
address_zip = COALESCE($7, address_zip),
address_street = COALESCE($8, address_street),
address_number = COALESCE($9, address_number),
address_complement = COALESCE($10, address_complement),
address_neighborhood = COALESCE($11, address_neighborhood),
address_city = COALESCE($12, address_city),
address_state = COALESCE($13, address_state),
address_country = COALESCE($14, address_country),
phone = COALESCE($15, phone),
email = COALESCE($16, email),
website = COALESCE($17, website),
legal_rep_name = COALESCE($18, legal_rep_name),
legal_rep_cpf = COALESCE($19, legal_rep_cpf),
legal_rep_role = COALESCE($20, legal_rep_role),
legal_rep_email = COALESCE($21, legal_rep_email),
is_active = COALESCE($22, is_active)
WHERE id = $1
RETURNING *`,
[
id,
input.companyName ?? null,
input.tradeName ?? null,
input.cnpj ?? null,
input.stateRegistration ?? null,
input.municipalRegistration ?? null,
input.addressZip ?? null,
input.addressStreet ?? null,
input.addressNumber ?? null,
input.addressComplement ?? null,
input.addressNeighborhood ?? null,
input.addressCity ?? null,
input.addressState ?? null,
input.addressCountry ?? null,
input.phone ?? null,
input.email ?? null,
input.website ?? null,
input.legalRepName ?? null,
input.legalRepCpf ?? null,
input.legalRepRole ?? null,
input.legalRepEmail ?? null,
input.isActive ?? null,
],
);
return rows[0] ?? null;
}

View File

@@ -0,0 +1,20 @@
import { query } from "../pool.js";
import type { Role, RolePermission } from "./types.js";
export async function getRoleByKey(key: string): Promise<Role | null> {
const { rows } = await query<Role>("SELECT * FROM roles WHERE key = $1", [key]);
return rows[0] ?? null;
}
export async function listRoles(): Promise<Role[]> {
const { rows } = await query<Role>("SELECT * FROM roles ORDER BY weight ASC");
return rows;
}
export async function getRolePermissions(roleKey: string): Promise<RolePermission[]> {
const { rows } = await query<RolePermission>(
"SELECT * FROM role_permissions WHERE role_key = $1",
[roleKey],
);
return rows;
}

View File

@@ -0,0 +1,84 @@
import { query } from "../pool.js";
import type { Session } from "./types.js";
export interface CreateSessionInput {
userId: string;
applicationKey: "core" | "reseller" | "subscriber";
refreshTokenHash: string;
expiresAt: Date;
ipAddress?: string | null;
userAgent?: string | null;
replacesSessionId?: string | null;
}
export async function createSession(input: CreateSessionInput): Promise<Session> {
const { rows } = await query<Session>(
`INSERT INTO sessions (user_id, application_key, refresh_token_hash, expires_at, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[
input.userId,
input.applicationKey,
input.refreshTokenHash,
input.expiresAt,
input.ipAddress ?? null,
input.userAgent ?? null,
],
);
const session = rows[0];
if (!session) throw new Error("failed to create session");
if (input.replacesSessionId) {
await query("UPDATE sessions SET replaced_by_session_id = $1, revoked_at = now() WHERE id = $2", [
session.id,
input.replacesSessionId,
]);
}
return session;
}
export async function findActiveSessionByRefreshTokenHash(hash: string): Promise<Session | null> {
const { rows } = await query<Session>(
`SELECT * FROM sessions
WHERE refresh_token_hash = $1 AND revoked_at IS NULL AND expires_at > now()`,
[hash],
);
return rows[0] ?? null;
}
/**
* Finds a session by refresh token hash regardless of revoked/expired state —
* used for replay detection: a rotated (already-revoked) token being
* reused is a strong signal of a stolen refresh token (ADR-0003).
*/
export async function findSessionByRefreshTokenHashAnyState(hash: string): Promise<Session | null> {
const { rows } = await query<Session>("SELECT * FROM sessions WHERE refresh_token_hash = $1", [
hash,
]);
return rows[0] ?? null;
}
export async function touchSession(id: string): Promise<void> {
await query("UPDATE sessions SET last_used_at = now() WHERE id = $1", [id]);
}
export async function revokeSession(id: string): Promise<void> {
await query("UPDATE sessions SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL", [id]);
}
/** Revokes every active session of a user — "encerrar todas as sessões" (Master Prompt §5.3). */
export async function revokeAllUserSessions(userId: string): Promise<void> {
await query("UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL", [
userId,
]);
}
export async function listActiveSessionsForUser(userId: string): Promise<Session[]> {
const { rows } = await query<Session>(
`SELECT * FROM sessions WHERE user_id = $1 AND revoked_at IS NULL AND expires_at > now()
ORDER BY created_at DESC`,
[userId],
);
return rows;
}

View File

@@ -0,0 +1,53 @@
export interface Role {
key: string;
label: string;
is_system: boolean;
weight: number;
created_at: Date;
updated_at: Date;
created_by: string | null;
}
export interface RolePermission {
id: string;
role_key: string;
resource: string;
action: "view" | "create" | "edit" | "delete" | "approve" | "export" | "manage";
scope: "own" | "team" | "reseller" | "legal_entity" | "all";
}
export interface User {
id: string;
created_at: Date;
updated_at: Date;
created_by: string | null;
full_name: string | null;
email: string;
password_hash: string | null;
password_algo_version: number;
role_key: string;
is_active: boolean;
reset_token_hash: string | null;
reset_token_expires_at: Date | null;
mfa_enabled: boolean;
mfa_secret_encrypted: string | null;
}
export interface Session {
id: string;
user_id: string;
application_key: "core" | "reseller" | "subscriber";
refresh_token_hash: string;
created_at: Date;
last_used_at: Date | null;
expires_at: Date;
revoked_at: Date | null;
replaced_by_session_id: string | null;
ip_address: string | null;
user_agent: string | null;
}
export interface Application {
key: "core" | "reseller" | "subscriber";
label: string;
}

View File

@@ -0,0 +1,80 @@
import { query } from "../pool.js";
import type { User } from "./types.js";
export async function countUsers(): Promise<number> {
const { rows } = await query<{ count: string }>("SELECT count(*) FROM users");
return Number(rows[0]?.count ?? 0);
}
export async function findUserByEmail(email: string): Promise<User | null> {
const { rows } = await query<User>(
"SELECT * FROM users WHERE lower(email) = lower($1) AND is_active = true",
[email],
);
return rows[0] ?? null;
}
export async function findUserById(id: string): Promise<User | null> {
const { rows } = await query<User>("SELECT * FROM users WHERE id = $1", [id]);
return rows[0] ?? null;
}
export interface CreateUserInput {
email: string;
passwordHash: string;
passwordAlgoVersion: number;
roleKey: string;
fullName?: string;
createdBy?: string | null;
}
export async function createUser(input: CreateUserInput): Promise<User> {
const { rows } = await query<User>(
`INSERT INTO users (email, password_hash, password_algo_version, role_key, full_name, created_by)
VALUES (lower($1), $2, $3, $4, $5, $6)
RETURNING *`,
[
input.email,
input.passwordHash,
input.passwordAlgoVersion,
input.roleKey,
input.fullName ?? null,
input.createdBy ?? null,
],
);
const user = rows[0];
if (!user) throw new Error("failed to create user");
return user;
}
export async function updateUserPassword(
userId: string,
passwordHash: string,
passwordAlgoVersion: number,
): Promise<void> {
await query("UPDATE users SET password_hash = $1, password_algo_version = $2 WHERE id = $3", [
passwordHash,
passwordAlgoVersion,
userId,
]);
}
export async function grantUserApplication(
userId: string,
applicationKey: "core" | "reseller" | "subscriber",
): Promise<void> {
await query(
`INSERT INTO user_applications (user_id, application_key)
VALUES ($1, $2)
ON CONFLICT (user_id, application_key) DO NOTHING`,
[userId, applicationKey],
);
}
export async function getUserApplications(userId: string): Promise<string[]> {
const { rows } = await query<{ application_key: string }>(
"SELECT application_key FROM user_applications WHERE user_id = $1",
[userId],
);
return rows.map((r) => r.application_key);
}

View File

@@ -1,7 +1,2 @@
/**
* Domain types/constants shared across bounded contexts (e.g. the
* resource/action/scope permission vocabulary from
* docs/adr/0004-permission-model.md). Placeholder until a second
* bounded context needs to share a type with the first.
*/
export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0"; export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0";
export { onlyDigits, isValidCPF, isValidCNPJ } from "./validators/br-documents.js";

View File

@@ -0,0 +1,49 @@
/**
* CPF/CNPJ format validation (digit-check only — never confirms a document
* actually exists or belongs to whoever claims it). Ported from the
* standard Brazilian algorithm, matching the legacy's isValidCPF/
* isValidCNPJ (eden.md §1 companies, §3 client registration).
*/
export function onlyDigits(value: string): string {
return value.replace(/\D/g, "");
}
export function isValidCPF(value: string): boolean {
const cpf = onlyDigits(value);
if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;
const digits = cpf.split("").map(Number);
const checkDigit = (length: number): number => {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += digits[i]! * (length + 1 - i);
}
const remainder = (sum * 10) % 11;
return remainder === 10 ? 0 : remainder;
};
return checkDigit(9) === digits[9] && checkDigit(10) === digits[10];
}
export function isValidCNPJ(value: string): boolean {
const cnpj = onlyDigits(value);
if (cnpj.length !== 14 || /^(\d)\1{13}$/.test(cnpj)) return false;
const digits = cnpj.split("").map(Number);
// Standard CNPJ weight sequences — the second is the first with a
// leading 6, not a generic slice (easy to get subtly wrong otherwise).
const WEIGHTS_12 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const WEIGHTS_13 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const checkDigit = (length: number, weights: number[]): number => {
let sum = 0;
for (let i = 0; i < length; i++) {
sum += digits[i]! * weights[i]!;
}
const remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
};
return (
checkDigit(12, WEIGHTS_12) === digits[12] && checkDigit(13, WEIGHTS_13) === digits[13]
);
}

317
pnpm-lock.yaml generated
View File

@@ -17,24 +17,45 @@ importers:
apps/api: apps/api:
dependencies: dependencies:
'@eden/config':
specifier: workspace:*
version: link:../../packages/config
'@eden/database': '@eden/database':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/database version: link:../../packages/database
'@eden/domain-shared':
specifier: workspace:*
version: link:../../packages/domain-shared
'@nestjs/common': '@nestjs/common':
specifier: ^10.4.15 specifier: ^10.4.15
version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': '@nestjs/core':
specifier: ^10.4.15 specifier: ^10.4.15
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/platform-express': '@nestjs/platform-express':
specifier: ^10.4.15 specifier: ^10.4.15
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)
'@nestjs/swagger':
specifier: ^8.1.0
version: 8.1.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
'@nestjs/terminus': '@nestjs/terminus':
specifier: ^10.2.3 specifier: ^10.2.3
version: 10.3.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) version: 10.3.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
argon2:
specifier: ^0.41.1
version: 0.41.1
class-transformer:
specifier: ^0.5.1
version: 0.5.1
class-validator:
specifier: ^0.14.1
version: 0.14.4
express: express:
specifier: ^4.21.2 specifier: ^4.21.2
version: 4.22.2(supports-color@7.2.0) version: 4.22.2(supports-color@7.2.0)
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.3
reflect-metadata: reflect-metadata:
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.2 version: 0.2.2
@@ -45,6 +66,9 @@ importers:
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.6 version: 5.0.6
'@types/jsonwebtoken':
specifier: ^9.0.7
version: 9.0.10
'@types/node': '@types/node':
specifier: ^22.10.5 specifier: ^22.10.5
version: 22.20.1 version: 22.20.1
@@ -180,7 +204,14 @@ importers:
version: 5.9.3 version: 5.9.3
packages/config: packages/config:
dependencies:
zod:
specifier: ^3.24.1
version: 3.25.76
devDependencies: devDependencies:
'@types/node':
specifier: ^22.10.5
version: 22.20.1
typescript: typescript:
specifier: ^5.7.3 specifier: ^5.7.3
version: 5.9.3 version: 5.9.3
@@ -197,6 +228,9 @@ importers:
specifier: ^8.13.1 specifier: ^8.13.1
version: 8.23.0 version: 8.23.0
devDependencies: devDependencies:
'@types/node':
specifier: ^22.10.5
version: 22.20.1
'@types/pg': '@types/pg':
specifier: ^8.11.10 specifier: ^8.11.10
version: 8.23.1 version: 8.23.1
@@ -672,6 +706,9 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
'@microsoft/tsdoc@0.15.1':
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
'@napi-rs/lzma-linux-x64-gnu@1.5.1': '@napi-rs/lzma-linux-x64-gnu@1.5.1':
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
engines: {node: ^22.20 || ^24.12 || >=25} engines: {node: ^22.20 || ^24.12 || >=25}
@@ -709,12 +746,42 @@ packages:
'@nestjs/websockets': '@nestjs/websockets':
optional: true optional: true
'@nestjs/mapped-types@2.0.6':
resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==}
peerDependencies:
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
class-transformer: ^0.4.0 || ^0.5.0
class-validator: ^0.13.0 || ^0.14.0
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/platform-express@10.4.22': '@nestjs/platform-express@10.4.22':
resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==} resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==}
peerDependencies: peerDependencies:
'@nestjs/common': ^10.0.0 '@nestjs/common': ^10.0.0
'@nestjs/core': ^10.0.0 '@nestjs/core': ^10.0.0
'@nestjs/swagger@8.1.1':
resolution: {integrity: sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==}
peerDependencies:
'@fastify/static': ^6.0.0 || ^7.0.0
'@nestjs/common': ^9.0.0 || ^10.0.0
'@nestjs/core': ^9.0.0 || ^10.0.0
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
'@fastify/static':
optional: true
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/terminus@10.3.0': '@nestjs/terminus@10.3.0':
resolution: {integrity: sha512-vOJGCwt1OgrFuuxWQwPoaHqy9m9CfIk2qMUX2mosZLK5dFVJSEjHXrklkh3/Fw9PiUnfzvYFfiAdJRzUaxx+5Q==} resolution: {integrity: sha512-vOJGCwt1OgrFuuxWQwPoaHqy9m9CfIk2qMUX2mosZLK5dFVJSEjHXrklkh3/Fw9PiUnfzvYFfiAdJRzUaxx+5Q==}
peerDependencies: peerDependencies:
@@ -780,6 +847,10 @@ packages:
engines: {node: '>=8.0.0', npm: '>=5.0.0'} engines: {node: '>=8.0.0', npm: '>=5.0.0'}
hasBin: true hasBin: true
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@rolldown/pluginutils@1.0.0-beta.27': '@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -921,6 +992,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@tokenizer/inflate@0.2.7': '@tokenizer/inflate@0.2.7':
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -988,6 +1062,12 @@ packages:
'@types/http-errors@2.0.5': '@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@22.20.1': '@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
@@ -1017,6 +1097,9 @@ packages:
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
'@vitejs/plugin-react@4.7.0': '@vitejs/plugin-react@4.7.0':
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@@ -1051,6 +1134,13 @@ packages:
arg@5.0.2: arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argon2@0.41.1:
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
engines: {node: '>=16.17.0'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-flatten@1.1.1: array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
@@ -1099,6 +1189,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1141,6 +1234,12 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
class-transformer@0.5.1:
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
class-validator@0.14.4:
resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==}
cli-boxes@2.2.1: cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1236,6 +1335,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1: ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -1453,6 +1555,10 @@ packages:
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsesc@3.1.0: jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1463,6 +1569,19 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
libphonenumber-js@1.13.12:
resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==}
lilconfig@3.1.3: lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1470,6 +1589,30 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
loose-envify@1.4.0: loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
@@ -1554,6 +1697,10 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
node-addon-api@8.9.2:
resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==}
engines: {node: ^18 || ^20 || >= 21}
node-fetch@2.7.0: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@@ -1563,6 +1710,10 @@ packages:
encoding: encoding:
optional: true optional: true
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-pg-migrate@7.9.1: node-pg-migrate@7.9.1:
resolution: {integrity: sha512-6z4OSN27ye8aYdX9ZU7NN2PTI5pOp34hTr+22Ej12djIYECq++gT7LPLZVOQXEeVCBOZQLqf87kC3Y36G434OQ==} resolution: {integrity: sha512-6z4OSN27ye8aYdX9ZU7NN2PTI5pOp34hTr+22Ej12djIYECq++gT7LPLZVOQXEeVCBOZQLqf87kC3Y36G434OQ==}
engines: {node: '>=18.19.0'} engines: {node: '>=18.19.0'}
@@ -1821,6 +1972,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
send@0.19.2: send@0.19.2:
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1904,6 +2060,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
swagger-ui-dist@5.18.2:
resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==}
tailwindcss@3.4.19: tailwindcss@3.4.19:
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -1994,6 +2153,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
validator@13.15.35:
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
engines: {node: '>= 0.10'}
vary@1.1.2: vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -2076,6 +2239,9 @@ packages:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'} engines: {node: '>=12'}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots: snapshots:
'@alloc/quick-lru@5.3.0': {} '@alloc/quick-lru@5.3.0': {}
@@ -2373,10 +2539,12 @@ snapshots:
'@lukeed/csprng@1.1.0': {} '@lukeed/csprng@1.1.0': {}
'@microsoft/tsdoc@0.15.1': {}
'@napi-rs/lzma-linux-x64-gnu@1.5.1': '@napi-rs/lzma-linux-x64-gnu@1.5.1':
optional: true optional: true
'@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': '@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)':
dependencies: dependencies:
file-type: 20.4.1(supports-color@7.2.0) file-type: 20.4.1(supports-color@7.2.0)
iterare: 1.2.1 iterare: 1.2.1
@@ -2384,12 +2552,15 @@ snapshots:
rxjs: 7.8.2 rxjs: 7.8.2
tslib: 2.8.1 tslib: 2.8.1
uid: 2.0.2 uid: 2.0.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.4
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': '@nestjs/core@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies: dependencies:
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nuxtjs/opencollective': 0.3.2 '@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1 fast-safe-stringify: 2.1.1
iterare: 1.2.1 iterare: 1.2.1
@@ -2399,14 +2570,22 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
uid: 2.0.2 uid: 2.0.2
optionalDependencies: optionalDependencies:
'@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
'@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)': '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)':
dependencies: dependencies:
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.4
'@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)':
dependencies:
'@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
body-parser: 1.20.4(supports-color@7.2.0) body-parser: 1.20.4(supports-color@7.2.0)
cors: 2.8.5 cors: 2.8.5
express: 4.22.1(supports-color@7.2.0) express: 4.22.1(supports-color@7.2.0)
@@ -2415,10 +2594,25 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@nestjs/terminus@10.3.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': '@nestjs/swagger@8.1.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)':
dependencies: dependencies:
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@microsoft/tsdoc': 0.15.1
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
js-yaml: 4.1.0
lodash: 4.17.21
path-to-regexp: 3.3.0
reflect-metadata: 0.2.2
swagger-ui-dist: 5.18.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.4
'@nestjs/terminus@10.3.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
boxen: 5.1.2 boxen: 5.1.2
check-disk-space: 3.4.0 check-disk-space: 3.4.0
reflect-metadata: 0.2.2 reflect-metadata: 0.2.2
@@ -2444,6 +2638,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
'@phc/format@1.0.0': {}
'@rolldown/pluginutils@1.0.0-beta.27': {} '@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.63.1': '@rollup/rollup-android-arm-eabi@4.63.1':
@@ -2521,6 +2717,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.63.1': '@rollup/rollup-win32-x64-msvc@4.63.1':
optional: true optional: true
'@scarf/scarf@1.4.0': {}
'@tokenizer/inflate@0.2.7(supports-color@7.2.0)': '@tokenizer/inflate@0.2.7(supports-color@7.2.0)':
dependencies: dependencies:
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -2596,6 +2794,13 @@ snapshots:
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 22.20.1
'@types/ms@2.1.0': {}
'@types/node@22.20.1': '@types/node@22.20.1':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@@ -2630,6 +2835,8 @@ snapshots:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 22.20.1 '@types/node': 22.20.1
'@types/validator@13.15.10': {}
'@vitejs/plugin-react@4.7.0(supports-color@7.2.0)(vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13))': '@vitejs/plugin-react@4.7.0(supports-color@7.2.0)(vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13))':
dependencies: dependencies:
'@babel/core': 7.29.7(supports-color@7.2.0) '@babel/core': 7.29.7(supports-color@7.2.0)
@@ -2668,6 +2875,14 @@ snapshots:
arg@5.0.2: {} arg@5.0.2: {}
argon2@0.41.1:
dependencies:
'@phc/format': 1.0.0
node-addon-api: 8.9.2
node-gyp-build: 4.8.4
argparse@2.0.1: {}
array-flatten@1.1.1: {} array-flatten@1.1.1: {}
autoprefixer@10.5.4(postcss@8.5.26): autoprefixer@10.5.4(postcss@8.5.26):
@@ -2746,6 +2961,8 @@ snapshots:
node-releases: 2.0.54 node-releases: 2.0.54
update-browserslist-db: 1.3.2(browserslist@4.28.8) update-browserslist-db: 1.3.2(browserslist@4.28.8)
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
busboy@1.6.0: busboy@1.6.0:
@@ -2789,6 +3006,14 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
class-transformer@0.5.1: {}
class-validator@0.14.4:
dependencies:
'@types/validator': 13.15.10
libphonenumber-js: 1.13.12
validator: 13.15.35
cli-boxes@2.2.1: {} cli-boxes@2.2.1: {}
cliui@8.0.1: cliui@8.0.1:
@@ -2867,6 +3092,10 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
ee-first@1.1.1: {} ee-first@1.1.1: {}
electron-to-chromium@1.5.420: {} electron-to-chromium@1.5.420: {}
@@ -3177,14 +3406,60 @@ snapshots:
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
jsesc@3.1.0: {} jsesc@3.1.0: {}
json5@2.2.3: {} json5@2.2.3: {}
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.8.5
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.1:
dependencies:
jwa: 2.0.1
safe-buffer: 5.2.1
libphonenumber-js@1.13.12: {}
lilconfig@3.1.3: {} lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.once@4.1.1: {}
lodash@4.17.21: {}
loose-envify@1.4.0: loose-envify@1.4.0:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
@@ -3254,10 +3529,14 @@ snapshots:
negotiator@0.6.3: {} negotiator@0.6.3: {}
node-addon-api@8.9.2: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
node-gyp-build@4.8.4: {}
node-pg-migrate@7.9.1(@types/pg@8.23.1)(pg@8.23.0): node-pg-migrate@7.9.1(@types/pg@8.23.1)(pg@8.23.0):
dependencies: dependencies:
glob: 11.0.3 glob: 11.0.3
@@ -3500,6 +3779,8 @@ snapshots:
semver@6.3.1: {} semver@6.3.1: {}
semver@7.8.5: {}
send@0.19.2(supports-color@7.2.0): send@0.19.2(supports-color@7.2.0):
dependencies: dependencies:
debug: 2.6.9(supports-color@7.2.0) debug: 2.6.9(supports-color@7.2.0)
@@ -3607,6 +3888,10 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
swagger-ui-dist@5.18.2:
dependencies:
'@scarf/scarf': 1.4.0
tailwindcss@3.4.19(tsx@4.23.13): tailwindcss@3.4.19(tsx@4.23.13):
dependencies: dependencies:
'@alloc/quick-lru': 5.3.0 '@alloc/quick-lru': 5.3.0
@@ -3712,6 +3997,8 @@ snapshots:
utils-merge@1.0.1: {} utils-merge@1.0.1: {}
validator@13.15.35: {}
vary@1.1.2: {} vary@1.1.2: {}
vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13): vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13):
@@ -3766,3 +4053,5 @@ snapshots:
string-width: 4.2.3 string-width: 4.2.3
y18n: 5.0.8 y18n: 5.0.8
yargs-parser: 21.1.1 yargs-parser: 21.1.1
zod@3.25.76: {}

View File

@@ -3,4 +3,6 @@ packages:
- "packages/*" - "packages/*"
allowBuilds: allowBuilds:
'@nestjs/core': true '@nestjs/core': true
'@scarf/scarf': false
argon2: true
esbuild: true esbuild: true