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

@@ -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 { 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]
);
}