# Trunks (Troncos SIP) Agente.md seções 41-42. Primeiro recurso que grava configuração real no filesystem do FreeSWITCH (gateways Sofia) em vez de só responder via `mod_xml_curl`. ## Modelo `trunks` (tenant-scoped, RLS): `host`, `proxy`, `realm`, `register`, `username`/`password_enc` (AES-256-GCM, mesmo padrão de `sip_password_enc`), `from_user`/`from_domain`, `register_proxy`/`outbound_proxy`, `expire_seconds`, `retry_seconds`, `dtmf_mode`, `ping`/`ping_frequency`, `transport`, `max_cps`/`max_channels`, e `status`/`status_updated_at` (refletidos pelos eventos do FreeSWITCH, nunca escritos manualmente pela API). ## Mecanismo: por que arquivo + rescan, e não XML Curl "configuration" Ao contrário de Extensions (resolvido 100% via `mod_xml_curl` na hora do lookup), gateways Sofia não têm um binding XML Curl limpo e específico sem reativar a seção `configuration` inteira — e isso já causou problemas reais na fase XML Curl (chamadas HTTP desnecessárias no boot pra configs de outros módulos). Em vez disso, `b2bcall-fs-config`: 1. Gera um arquivo `.xml` por trunk habilitado em `sip_profiles/external/` (volume Docker compartilhado com o FreeSWITCH — o profile `external` da config vanilla já tem ``, então não precisou editar a config do profile). 2. Roda `sofia profile external rescan` via ESL — relê os gateways sem derrubar chamadas em andamento (diferente de `restart`). ## Gatilho de sincronização `apps/api` não roda no Docker (ainda está no host) e `fs-config` não expõe porta pro host — então a notificação "algo mudou, resincroniza" vai por Redis pub/sub (`b2bcall:trunks:sync`), o mesmo mecanismo já usado pra eventos normalizados. `apps/api` publica depois de criar/apagar um trunk; `fs-config` também roda uma sincronização completa ao subir (cobre trunks criados enquanto ele estava fora do ar). **Achado real**: a primeira sincronização no boot corria antes da conexão ESL do `fs-config` terminar de se estabelecer, gerando um erro cosmético ("FreeSWITCH ESL nao conectado") — a escrita dos arquivos funcionava, só o rescan falhava. Corrigido com `FreeSwitchTelephonyProvider.waitUntilConnected()` (timeout de 5s) antes de tentar o rescan. ## Status do trunk (secao 42) `b2bcall-fs-events` já escutava `sofia::gateway_state` desde a fase Event Socket (normalizado pra `GATEWAY_UP`/`GATEWAY_DOWN`); nesta fase, ele passou a também escrever esse estado de volta em `Trunk.status`. Como o nome do gateway no FreeSWITCH é o `Trunk.id` (UUID) e o evento não diz de qual tenant é, a busca percorre os tenants ativos (`packages/database`'s `withTenantContext`) até achar o trunk dono daquele id — aceitável dado que mudança de estado de trunk é rara, não é um evento de alto volume por chamada. Mapeamento de estados brutos do Sofia pro enum interno (`apps/freeswitch-events/src/trunk-status.ts`): ``` UP, REGED → REGISTERED/UP TRYING, REGISTER → TRYING FAILED, FAIL_WAIT → FAILED DOWN → DOWN NOREG, UNREGED → UNREGISTERED qualquer outro → UNKNOWN ``` ## Verificado Criei um trunk de teste apontando pra um host inexistente (`sip.trunk-inexistente.invalid`, nunca resolve — RFC 2606) via API: - `fs-config` sincronizou (`count: 1`), escreveu o arquivo `.xml`, rodou o rescan com sucesso. - `sofia status gateway` no FreeSWITCH mostrou o gateway real, estado `FAIL_WAIT` (esperado — host não existe). - Confirma o pipeline `API → Postgres → fs-config → arquivo XML → rescan → FreeSWITCH` funcionando ponta a ponta sem precisar de nenhum tronco/ credencial real. ## O que falta - Propagação de `GATEWAY_UP`/`DOWN` → `Trunk.status` não confirmada com evento real disparado por uma mudança de estado ao vivo nesta sessão de testes (o gateway ficou em `FAIL_WAIT` por falha de DNS, que pode não disparar o mesmo ciclo de eventos que uma rejeição SIP normal) — vale reverificar com um destino que responda de verdade (outro FreeSWITCH, por exemplo) antes de confiar nisso em produção. - Quota de troncos (`max_trunks`, secao 59) — depende de Plans/Entitlements. - `GET /trunks` não mostra `sofia status gateway` ao vivo, só o último status conhecido no banco — suficiente por enquanto, sem WebSocket ainda.