Desafio 03: DICT — Diretório de Identificadores de Contas Transacionais
🇧🇷 Diretório de Identificadores de Contas Transacionais
🇬🇧 Directory of Transactional Account Identifiers
O DICT é o diretório central do Banco Central do Brasil que gerencia todas as chaves PIX. É a "agenda telefônica" do PIX — sem ele, ninguém saberia para qual conta enviar um PIX quando você informa apenas um CPF ou e-mail.
Switch: TypeScript vs Go
O que é o DICT?
| Tipo de Chave | Descrição |
|---|---|
| CPF | 11 dígitos, validação por dígito verificador |
| CNPJ | 14 dígitos, validação por dígito verificador |
| Formato RFC 5322, até 77 caracteres | |
| Telefone | Formato +55 XX XXXXX-XXXX |
| Aleatória | UUID v4, máxima privacidade |
| Característica | Descrição |
|---|---|
| Ownership | Uma chave = uma conta (única) |
| Portabilidade | Cliente pode mover chave entre bancos |
| Anti-enumeration | Proteção contra varreduras |
| Rate limiting | Limites por instituição |
Arquitetura do DICT Simulator
Fluxos Principais
1. Registro de Chave
2. Consulta de Chave
3. Portabilidade (Claim)
Domain Layer
typescript
export enum PixKeyType {
CPF = 'CPF',
CNPJ = 'CNPJ',
EMAIL = 'EMAIL',
PHONE = 'PHONE',
RANDOM = 'RANDOM'
}
export class PixKey extends Entity<string> {
public static create(props: PixKeyProps): Result<PixKey, Error> {
const validation = PixKey.validate(props);
if (validation.isErr()) return Err(validation.error);
return Ok(new PixKey({
...props,
id: props.id || uuidv4(),
createdAt: props.createdAt || new Date(),
active: props.active ?? true,
}));
}
public static createRandom(account: AccountInfo, owner: OwnerInfo): Result<PixKey, Error> {
return PixKey.create({
type: PixKeyType.RANDOM,
value: uuidv4(),
account, owner,
ispb: account.ispb,
createdAt: new Date(),
active: true,
});
}
public transferTo(newAccount: AccountInfo): Result<PixKey, Error> {
if (!this.props.active) return Err(new Error('Chave inativa'));
return Ok(new PixKey({
...this.props,
account: newAccount,
ispb: newAccount.ispb,
updatedAt: new Date(),
}));
}
}Validators — Validação de Cada Tipo
typescript
export class CPFValidator implements KeyValidator {
validate(value: string): ValidationResult {
const cpf = value.replace(/\D/g, '');
if (cpf.length !== 11) return { isValid: false, errorMessage: 'CPF deve ter 11 dígitos' };
if (/^(\d)\1+$/.test(cpf)) return { isValid: false, errorMessage: 'CPF inválido' };
if (!this.validateDigits(cpf)) return { isValid: false, errorMessage: 'CPF inválido' };
return { isValid: true, normalizedValue: cpf };
}
private validateDigits(cpf: string): boolean {
let sum = 0;
for (let i = 0; i < 9; i++) sum += parseInt(cpf[i]) * (10 - i);
let remainder = (sum * 10) % 11;
if (remainder === 10) remainder = 0;
if (remainder !== parseInt(cpf[9])) return false;
sum = 0;
for (let i = 0; i < 10; i++) sum += parseInt(cpf[i]) * (11 - i);
remainder = (sum * 10) % 11;
if (remainder === 10) remainder = 0;
return remainder === parseInt(cpf[10]);
}
}
export class CNPJValidator implements KeyValidator {
validate(value: string): ValidationResult {
const cnpj = value.replace(/\D/g, '');
if (cnpj.length !== 14) return { isValid: false, errorMessage: 'CNPJ deve ter 14 dígitos' };
if (/^(\d)\1+$/.test(cnpj)) return { isValid: false, errorMessage: 'CNPJ inválido' };
if (!this.validateDigits(cnpj)) return { isValid: false, errorMessage: 'CNPJ inválido' };
return { isValid: true, normalizedValue: cnpj };
}
private validateDigits(cnpj: string): boolean {
const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < 12; i++) sum += parseInt(cnpj[i]) * weights1[i];
let remainder = sum % 11;
let digit1 = remainder < 2 ? 0 : 11 - remainder;
if (digit1 !== parseInt(cnpj[12])) return false;
sum = 0;
for (let i = 0; i < 13; i++) sum += parseInt(cnpj[i]) * weights2[i];
remainder = sum % 11;
let digit2 = remainder < 2 ? 0 : 11 - remainder;
return digit2 === parseInt(cnpj[13]);
}
}
export class KeyValidatorFactory {
static create(type: PixKeyType): KeyValidator {
switch (type) {
case PixKeyType.CPF: return new CPFValidator();
case PixKeyType.CNPJ: return new CNPJValidator();
case PixKeyType.EMAIL: return new EmailValidator();
case PixKeyType.PHONE: return new PhoneValidator();
case PixKeyType.RANDOM: return new RandomKeyValidator();
}
}
}Use Cases — Registro de Chave
typescript
export class RegisterPixKeyUseCase {
private static readonly MAX_KEYS_PER_PERSON = 5;
constructor(
private readonly pixKeyRepo: PixKeyRepository,
private readonly lock: DistributedLock,
private readonly eventPublisher: EventPublisher
) {}
public async execute(input: RegisterPixKeyInput): Promise<Either<Error, PixKey>> {
// 1. Para chave aleatória, gera o valor
let value = input.value;
if (input.type === PixKeyType.RANDOM) {
value = crypto.randomUUID();
} else if (!value) {
return left(new Error('Valor é obrigatório'));
}
// 2. Valida formato
const validator = KeyValidatorFactory.create(input.type);
const validation = validator.validate(value);
if (!validation.isValid) return left(new Error(validation.errorMessage!));
// 3. Lock distribuído
const unlock = await this.lock.acquire(`pix_key:${value}`, { ttl: 10000 });
try {
// 4. Verifica se já existe
const existing = await this.pixKeyRepo.findByValue(value);
if (existing) return left(new KeyAlreadyRegisteredError(value));
// 5. Valida limite por pessoa
const existingByOwner = await this.pixKeyRepo.findByOwnerDocument(owner.document);
if (existingByOwner.length >= RegisterPixKeyUseCase.MAX_KEYS_PER_PERSON) {
return left(new MaxKeysReachedError(owner.document));
}
// 6. Cria e persiste
const pixKey = PixKey.create({ type: input.type, value, account, owner, ispb: account.ispb });
await this.pixKeyRepo.save(pixKey);
await this.eventPublisher.publish('pix.key.registered', { keyId: pixKey.id, type: pixKey.type });
return right(pixKey);
} finally {
await unlock();
}
}
}Use Cases — Claim (Portabilidade)
typescript
export class ClaimPixKeyUseCase {
private static readonly CLAIM_EXPIRY_HOURS = 7 * 24;
constructor(
private readonly pixKeyRepo: PixKeyRepository,
private readonly claimRepo: ClaimRepository,
private readonly lock: DistributedLock,
private readonly notificationService: NotificationService
) {}
public async execute(input: ClaimPixKeyInput): Promise<Either<Error, ClaimPixKeyOutput>> {
const unlock = await this.lock.acquire(`pix_key:${input.key}`, { ttl: 15000 });
try {
const currentKey = await this.pixKeyRepo.findByValue(input.key);
if (!currentKey) return left(new KeyNotFoundError(input.key));
if (currentKey.ispb === input.claimerIspb) {
return left(new SamePSPError('Não é possível claim no mesmo PSP'));
}
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + ClaimPixKeyUseCase.CLAIM_EXPIRY_HOURS);
const claim = { id: crypto.randomUUID(), key: input.key, currentIspb: currentKey.ispb, claimerIspb: input.claimerIspb, status: 'PENDING', expiresAt };
await this.claimRepo.save(claim);
await this.notificationService.notifyPSP(currentKey.ispb, { type: 'CLAIM_RECEIVED', claimId: claim.id });
return right({ claimId: claim.id, status: 'PENDING', expiresAt });
} finally {
await unlock();
}
}
}Anti-Enumeration — Segurança
typescript
export class AntiEnumerationService {
public async analyzeQuery(ispb: string, ip: string, key: string, result: 'found' | 'not_found'): Promise<SecurityAction> {
const ispbCount = await this.redis.incr(`query:${ispb}:${hourKey}`);
const ispbNotFound = result === 'not_found' ? await this.redis.incr(`notfound:${ispb}:${hourKey}`) : 0;
const notFoundRate = ispbCount > 0 ? ispbNotFound / ispbCount : 0;
// Alta taxa de "não encontrado" = possível enumeração
if (notFoundRate > 0.9 && ispbCount > 100) {
return SecurityAction.THROTTLE;
}
// Volume muito alto
if (ispbCount > 5000) {
return SecurityAction.BLOCK;
}
// Detecta CPFs sequenciais
if (this.isSequentialCPF(key)) {
return SecurityAction.BLOCK;
}
return SecurityAction.ALLOW;
}
}Proteções obrigatórias:
- Rate limiting por ISPB e IP
- Respostas homogêneas (404 genérico, sem detalhes)
- Masking de dados (nunca retorna CPF completo)
- Detecção de padrões anômalos
- Auditoria de todas as consultas
Comparação: TypeScript vs Go
| Aspecto | TypeScript | Go |
|---|---|---|
| Desenvolvimento | Rápido (Zod, Express) | Mais verboso |
| CPU-bound | Validações travam event loop | Não bloqueia |
| Memory | Mais RAM por conexão | 2-3x menos |
| Throughput | ~10K req/s | ~50K req/s |
| Regex | V8 otimizado | Nativo, pré-compilado |
| JSON | Nativo | Struct tags |
Quando usar TypeScript?
- PSP pequeno/médio (até 1M de chaves)
- Time-to-market rápido
- Volume moderado (até 5K consultas/s)
- Equipe sem experiência em Go
Caso Real: Nubank e Itaú
- Nubank — Clojure (core DICT) + TypeScript (BFFs), 80M+ clientes, Redis Cluster
- Itaú — Go (DICT alta performance) + Java (core banking legado), 60M+ clientes
Boas Práticas
Faça:
- Locks distribuídos em operações de escrita
- Rate limiting por ISPB, IP e usuário
- Masking de dados (nunca retorne CPF completo)
- Auditoria de todas as operações
- TTL em cache balanceando freshness vs performance
Evite:
- Respostas detalhadas em 404 (anti-enumeration)
- Cache sem invalidação (claims mudam ownership)
- Validação só no backend (sempre backend)
- Logs com dados sensíveis
- Concorrência sem locks
Como testar
bash
# TypeScript
make infra-up
pnpm --filter @banking/dict dev
# Registrar chave
curl -X POST http://localhost:3003/api/v1/dict/keys \
-H "Content-Type: application/json" \
-d '{"type":"CPF","value":"12345678901","account":{"ispb":"12345678","branch":"0001","number":"12345","type":"CACC"},"owner":{"name":"João","document":"12345678901"}}'
# Consultar chave
curl http://localhost:3003/api/v1/dict/keys/12345678901 \
-H "X-ISPB: 12345678"
# Go
cd packages/backend/dict-simulator-go
go run .Lições aprendidas
- Uma chave = uma conta — Invariante sagrado do DICT
- Anti-enumeration não é opcional — Proteção contra varreduras de CPFs
- Locks distribuídos são obrigatórios — Race conditions em claims corrompem ownership
- Masking de dados — Nunca retorne CPF/CNPJ completo
- Cache com TTL inteligente — Claims mudam ownership, cache precisa invalidar
- Auditoria de tudo — Cada consulta deve ser logada
- Limite de 5 chaves por pessoa — Regra do BCB
- Claim expira em 7 dias — Implemente limpeza automática
- Go escala linearmente — CPUs extras viram throughput direto
- TypeScript é suficiente — Para volumes moderados (< 5K req/s)