feat: implement Dialplan with structured editor and versioning
- dialplan_extensions table (tenant-scoped, RLS): structured editor per agente.md secao 43 -- context, condition field/expr, actions/anti-actions (JSON), continue, order, enabled. One condition per extension (deliberate simplification vs raw FreeSWITCH's multi-condition extensions). - dialplan_versions table (tenant-scoped, RLS): generate/validate/version/ activate flow (secao 44). Reactivating an older version IS the rollback mechanism -- no separate endpoint needed. - apps/api/src/dialplan: extensions CRUD + versions/generate (builds XML, validates well-formedness with fast-xml-parser, saves as DRAFT) + versions/:id/activate (atomically flips ACTIVE, supersedes the previous one). Reused freeswitch.view/.configure permissions rather than inventing new ones not in the agente.md permission list. - packages/telephony: buildDialplanXml() plus ALLOWED_DIALPLAN_APPLICATIONS, an explicit allowlist (answer/bridge/playback/hangup/set/export/... -- deliberately no system/exec/socket) guarding against a tenant configuring a dialplan action that runs arbitrary commands on the FreeSWITCH host (agente.md secao 180) - b2bcall-fs-config resolves dialplan dynamically per call (unlike Trunks' file+rescan approach -- dialplan is fetched fresh via mod_xml_curl on every call anyway) by tenant id from the variable_b2bcall_tenant_id channel variable already injected at directory resolution, then serving whichever DialplanVersion is ACTIVE for that context - verified end-to-end: created a rule for destination_number 7000, generated and activated v1, originated a call that actually routed through the dialplan (not bypassing it via &app()) -- CALL_CREATED -> CALL_ANSWERED -> CALL_ENDED with the correct tenantId throughout. Created and activated a v2, then rolled back to v1 by reactivating it; status transitions (ACTIVE/SUPERSEDED) all confirmed via the API. CRITICAL FINDING, fixed in this same phase: deliberately testing that the application allowlist rejects 'system' got back 201 instead of 400 -- NestJS's ValidationPipe had been silently inert across all of apps/api's @Body() DTOs since the API was first created. Root cause: running via (esbuild) instead of a real build -- esbuild doesn't always resolve cross-file parameter types for design:paramtypes metadata, and Nest skips validation without any error when it can't determine the DTO class. Fixed by always building with tsc before running (tsc && tsx dist/main.js -- still via tsx because internal workspace packages aren't built to JS yet). Re-verified with two deliberate bad-input tests post-fix, both correctly rejected with 400. A stray malicious test row (dialplan action 'system') created while the bug was live was deleted; it was never baked into an activated version, so nothing could have executed it. See docs/VALIDATION_PIPE_BUG.md for the full writeup. docs/DIALPLAN.md, docs/VALIDATION_PIPE_BUG.md, docs/EXTENSIONS.md updated
This commit is contained in:
35
TODO.md
35
TODO.md
@@ -140,7 +140,40 @@
|
||||
produção. Ver docs/TRUNKS.md.
|
||||
- [ ] Quota de troncos — depende de Plans/Entitlements (não existe ainda)
|
||||
|
||||
## PHASE 10+ — ver `agente.md` seções 43 em diante (Dialplan, Call Center,
|
||||
## PHASE 10 — Dialplan (agente.md secao 43-44)
|
||||
- [x] `dialplan_extensions` (tenant-scoped, RLS) — editor estruturado:
|
||||
context, condition field/expr, actions/anti-actions (JSON), continue,
|
||||
order, enabled
|
||||
- [x] `dialplan_versions` (tenant-scoped, RLS) — gerar/validar/versionar/
|
||||
ativar; reativar versão antiga = rollback (sem endpoint separado)
|
||||
- [x] `apps/api/src/dialplan`: extensions CRUD + `versions/generate` +
|
||||
`versions/:id/activate`, permissions `freeswitch.view`/`.configure`
|
||||
- [x] Allowlist de applications seguras (`ALLOWED_DIALPLAN_APPLICATIONS`,
|
||||
sem `system`/`exec`/etc — agente.md secao 180)
|
||||
- [x] `b2bcall-fs-config` serve a versão ACTIVE dinamicamente por chamada
|
||||
(resolve tenant via `variable_b2bcall_tenant_id`, não domain — não
|
||||
sofre da limitação de multi-domínio do directory)
|
||||
- [x] Testado ponta a ponta: criar extension → gerar v1 → ativar → originate
|
||||
passando pelo dialplan de verdade → CALL_CREATED/ANSWERED/ENDED com
|
||||
tenantId correto. Criar v2 → ativar (v1 vira SUPERSEDED) → reativar v1
|
||||
(rollback, v2 vira SUPERSEDED). Tudo confirmado via API.
|
||||
|
||||
- [x] **ACHADO CRÍTICO, corrigido nesta fase**: testando a allowlist com
|
||||
`application: "system"`, a API aceitou (201) — `ValidationPipe` do
|
||||
Nest estava **completamente inoperante** em toda `apps/api` desde que
|
||||
ela foi criada (todo `@Body()`, todos os controllers) porque `tsx`
|
||||
(esbuild) não emite `design:paramtypes` corretamente pra tipos
|
||||
importados de outro arquivo, e o Nest pula validação silenciosamente
|
||||
quando não reconhece o tipo. Corrigido: `apps/api` agora builda com
|
||||
`tsc` de verdade antes de rodar (`tsc && tsx dist/main.js`) — nunca
|
||||
mais `tsx src/main.ts` direto. Ver docs/VALIDATION_PIPE_BUG.md.
|
||||
Reverificado com 2 testes deliberados pós-correção, ambos
|
||||
corretamente rejeitados com 400.
|
||||
- [ ] Só 1 condition por extension (simplificação) — FreeSWITCH suporta
|
||||
múltiplas em sequência, não implementado
|
||||
- [ ] `dialplan.view`/`.manage` não existem — reusei `freeswitch.*`
|
||||
|
||||
## PHASE 11+ — ver `agente.md` seções 37 em diante (Call Center/mod_callcenter,
|
||||
Predictive Dialer, Recordings, AI, Billing, Frontend, Reports, Security, Tests)
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user