# TODO — B2BCall ## PHASE 01 — Infrastructure - [x] Diagnóstico do servidor (Debian 13, 2 vCPU, ~1.9GB RAM, 26GB disco livre) - [x] Docker + Docker Compose instalados - [x] Estrutura de monorepo criada (apps/, packages/, infrastructure/, scripts/, docs/) - [x] PostgreSQL 18 (docker-compose, porta 127.0.0.1:5432) - [x] Redis 7 (docker-compose, porta 127.0.0.1:6379) - [x] Secrets gerados em `.env` (POSTGRES_PASSWORD, REDIS_PASSWORD, JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY, ESL_PASSWORD) - [x] `FREESWITCH_PAT` configurado em `.env` (não commitado) - [x] FreeSWITCH (imagem própria via pacotes SignalWire, não compilada da fonte — ver docs/FREESWITCH.md; rodando, saudável, ~44MB RAM, senha ESL customizada, nenhuma porta exposta ao host) - [ ] nginx (reverse proxy) ## PHASE 05 — FreeSWITCH (agente.md secao 232) - [x] Imagem própria (`infrastructure/freeswitch/`), pacotes SignalWire (PAT via BuildKit secret, nunca na imagem final — verificado com `docker history`) - [x] Módulos mínimos carregados: sofia, event_socket, commands, dptools, callcenter, avmd, curl, local_stream, etc. (mod_xml_curl instalado mas desativado até existir b2bcall-fs-config) - [x] Senha do Event Socket trocada da padrão via entrypoint runtime (nunca fica na imagem); porta 8021 não publicada no host - [x] docs/FREESWITCH.md, docs/NETWORK_ARCHITECTURE.md (decisão de network_mode adiada pra quando existir tronco SIP real) - [ ] Diretório/dialplan ainda são os estáticos da config vanilla (ramais de teste 1000-1019, senhas fracas) — substituir por mod_xml_curl na fase Extensions/Trunks/Dialplan ## PHASE 06 — Event Socket (agente.md secao 21-25, 195) - [x] `packages/telephony`: interface `TelephonyProvider` + `FreeSwitchTelephonyProvider` (sobre a lib `esl`, reconexão com backoff já embutida na lib) - [x] `normalizeEslEvent()`: eventos ESL crus → vocabulário interno (secao 24) - [x] `apps/freeswitch-events` (b2bcall-fs-events): conexão ESL permanente, resubscreve a cada reconexão, publica eventos normalizados no canal Redis `b2bcall:events` - [x] Achado: FreeSWITCH 1.11 aplica ACL implícita (só loopback) sem `apply-inbound-acl` — bloqueava conexão de outro container mesmo com senha certa. Corrigido com ACL própria cobrindo loopback + rede Docker. - [x] Testado ponta a ponta com chamada loopback local: CALL_CREATED → CALL_ANSWERED → CALL_ENDED corretos no Redis - [ ] Reconciliação pós-reconexão (calls/agents/queues/registrations/gateways) — não é possível ainda, sem essas tabelas persistidas ## PHASE 07 — XML Curl (agente.md secao 26) - [x] `apps/freeswitch-config` (b2bcall-fs-config): responde ao protocolo XML Curl do FreeSWITCH (POST form-encoded → XML), containerizado - [x] `mod_xml_curl` reativado, binding restrito a `directory|dialplan` (não `configuration` — evita chamadas HTTP desnecessárias no boot) - [x] Por enquanto sempre "not found" (sem tabela extensions/dialplan ainda); verificado que a config estática vanilla continua funcionando como fallback (`user/8888` → SUBSCRIBER_ABSENT via fs-config, `user/1000` → USER_NOT_REGISTERED via config estática — achou o usuário) - [ ] Sem autenticação HTTP ainda — ok enquanto só responde "not found"; adicionar `gateway-credentials` antes de servir directory/dialplan reais ## PHASE 02 — SaaS Core - [x] Monorepo Node.js/TypeScript (pnpm workspaces, tsconfig base) - [x] Node 22 LTS + pnpm instalados no host - [x] `packages/database` (Prisma 7 + driver adapter `pg`, migration inicial) - [x] `packages/types` (TenantStatus, AgentState), `packages/shared` - [x] Tabela `tenants` criada via migration (seção 29 do agente.md) ## PHASE 03 — Tenant Isolation - [x] Tabelas `users` + `tenant_memberships` (tenant-scoped) - [x] RLS (`ENABLE`/`FORCE ROW LEVEL SECURITY` + policy) em `tenant_memberships` - [x] Tenant context via `set_config('app.current_tenant_id', ..., true)` (transaction-local) - [x] Helper `withTenantContext()` em `packages/database` - [x] Role de banco separado para runtime (`b2bcall_app`, sem SUPERUSER/BYPASSRLS) — achado crítico: o role padrão do Docker Postgres é SUPERUSER e SEMPRE ignora RLS, até com FORCE. Ver `docs/TENANT_ISOLATION.md`. - [x] Teste automatizado de isolamento (`pnpm --filter @b2bcall/database run test:isolation`) ## PHASE 04 — Authentication / RBAC - [x] `packages/auth`: hash Argon2id (`@node-rs/argon2`), JWT access token (`jose`), refresh token opaco com rotation - [x] Tabelas `roles`, `permissions`, `role_permissions`, `user_roles`, `sessions`, `audit_logs` - [x] `login()` / `refreshSession()` / `logout()` / `listUserTenants()` / `setActiveTenant()` - [x] `userHasPermission()` (RBAC com scope PLATFORM/TENANT) - [x] Seed: catálogo de permissions + roles de sistema + Platform Super Admin inicial (senha em `FIRST_LOGIN.txt`, fora do Git, `mustChangePassword=true`) - [x] Teste automatizado (`pnpm --filter @b2bcall/auth run test:auth`) - [x] `apps/api` (NestJS + Fastify): endpoints de auth, JwtAuthGuard, DomainExceptionFilter, rate limit de login via Redis (5/min por IP e por e-mail), helmet/cors, health checks — testado ponta a ponta com curl (login, refresh rotation, logout, RBAC, 401/403/429) - [ ] Password reset por e-mail — depende de SMTP configurado ## PHASE 08 — Extensions (agente.md secao 39-40, 178) - [x] Tabela `extensions` (tenant-scoped, RLS) — number, sip_password_enc, caller_id, context, sofia_profile, codecs, max_registrations - [x] `packages/shared/src/crypto.ts`: AES-256-GCM (senha SIP cifrada em repouso), `generateStrongPassword()`, `maskSecret()` - [x] `apps/api/src/extensions`: CRUD (POST/GET/GET:id/DELETE), RBAC via novo `PermissionGuard` genérico (`@RequirePermission`), tenant só do JWT - [x] Senha SIP só aparece em texto puro na resposta do POST, nunca depois (destructuring explícito, não spread — evita vazamento por acidente) - [x] `b2bcall-fs-config` resolve directory real: Tenant.telephonyDomain → Extension.number, decifra a senha, monta XML com dial-string - [x] `Tenant.telephonyDomain` fixo (`b2bcall.local`) via patch no `vars.xml` do FreeSWITCH — antes usava o IP dinâmico do container, instável - [x] HTTP Basic auth entre FreeSWITCH e fs-config (`gateway-credentials`, timingSafeEqual) — adicionada nesta mesma fase, não deixada pendente - [x] Testado ponta a ponta: criar ramal → `user/1500` dá USER_NOT_REGISTERED (achou, sem telefone) → deletar → volta a SUBSCRIBER_ABSENT - [x] Achado: `PermissionGuard` injetando `Reflector` via construtor dava `undefined` em runtime rodando via `tsx`/esbuild (emissão de metadata de tipo não é 100% confiável cross-file) — corrigido com `@Inject()` explícito; atenção pra isso em guards/services futuros - [ ] Quota de ramais — depende de Plans/Entitlements (não existe ainda) - [ ] Multi-domínio real por tenant — hoje só um domínio fixo pra todos ## PHASE 09+ — ver `agente.md` seções 41 em diante (Trunks, Dialplan, Call Center, Predictive Dialer, Recordings, AI, Billing, Frontend, Reports, Security, Tests) --- ## Riscos conhecidos - **RAM da VM (1.9GB total)**: medido com Postgres+Redis+FreeSWITCH rodando juntos — ~91MB no total (Postgres 37MB, Redis 10MB, FreeSWITCH 44MB), bem tranquilo. O risco real ainda não testado é o build/runtime do Next.js (frontend) e vários workers Node simultâneos — reavaliar quando chegarmos lá. - **Disco (26GB livre)**: build do FreeSWITCH + imagens Docker + gravações vão consumir espaço rápido. Monitorar com `df -h`.