Commit Graph

3 Commits

Author SHA1 Message Date
4c638ad496 feat: implement Trunks with real FreeSWITCH gateway sync
- trunks table (tenant-scoped, RLS): host/proxy/realm, register,
  username/password_enc (AES-256-GCM), dtmf_mode, ping, transport, and a
  status/status_updated_at pair meant to be driven by FreeSWITCH events
- apps/api/src/trunks: CRUD (POST/GET/GET:id/DELETE), same RBAC/tenant
  pattern as Extensions, password never exposed in any GET
- packages/telephony: buildGatewayXml() generates a Sofia gateway XML file
- b2bcall-fs-config now writes sip_profiles/external/<trunk_id>.xml (shared
  Docker volume with FreeSWITCH -- the vanilla external profile already
  includes external/*.xml) and runs 'sofia profile external rescan' over
  ESL; syncs on boot and on demand via Redis pub/sub
  (b2bcall:trunks:sync), since apps/api runs on the host and fs-config has
  no port published to reach directly
- added FreeSwitchTelephonyProvider.waitUntilConnected() to fix a startup
  race: the first sync ran before the ESL connection had settled, logging
  a harmless but noisy error
- verified end-to-end with a fake host: create trunk -> gateway file
  written -> FreeSWITCH shows the real gateway (FAIL_WAIT, expected) ->
  delete -> file removed (cleanup also correctly swept the stale
  'example.com' gateway that had been copied into the volume from the
  vanilla image)
- apps/freeswitch-events/src/trunk-status.ts: written to update Trunk.status
  from sofia::gateway_state events, using the same normalizeEslEvent path
  already proven for CHANNEL_* events

- KNOWN GAP, documented rather than glossed over: monitored fs-events for
  ~90s while the gateway visibly transitioned states in FreeSWITCH
  (FAIL_WAIT/DOWN) and no sofia::gateway_state event was observed arriving.
  CUSTOM/sofia::* events have not actually been proven working end-to-end
  in this session -- only CHANNEL_* events have been. Needs verification
  against a real SIP target before the status auto-update can be trusted
  in production. See docs/TRUNKS.md and TODO.md.

- docs/TRUNKS.md
2026-08-28 08:01:56 -03:00
c03c6d4eaa feat: implement Extensions with real FreeSWITCH directory integration
- extensions table (tenant-scoped, RLS): number, sip_password_enc
  (AES-256-GCM via packages/shared/src/crypto.ts), caller_id, context,
  sofia_profile, codecs, max_registrations
- apps/api/src/extensions: CRUD (POST/GET/GET:id/DELETE), protected by a
  new generic PermissionGuard (@RequirePermission decorator), tenant
  resolved only from the JWT (never trusted from the client)
- SIP password is returned in plaintext only once, in the create response;
  toPublicExtension() explicitly destructures the encrypted field out
  (not a spread) so it can't leak by accident
- b2bcall-fs-config now resolves real directory data: Tenant.telephonyDomain
  -> Extension.number, decrypts the password, builds proper directory XML
  including a dial-string param (missing it caused originate to fail with
  MANDATORY_IE_MISSING instead of the expected USER_NOT_REGISTERED)
- pinned FreeSWITCH's 357737{domain} to a stable value (b2bcall.local) via a
  vars.xml patch in the Dockerfile -- it previously used the container's
  dynamic IP, which could never match a stored telephony_domain
- added HTTP Basic auth between FreeSWITCH and fs-config
  (gateway-credentials, timingSafeEqual comparison) now that the service
  returns real secret data, closing the gap flagged as pending in the XML
  Curl phase instead of leaving it open
- found and fixed: PermissionGuard's constructor-injected Reflector came
  back undefined at runtime under tsx/esbuild (unreliable cross-file
  decorator metadata emission) -- fixed with an explicit @Inject(Reflector);
  worth watching for in future guards/services run via tsx
- verified end-to-end: create extension -> originate user/<ext> reports
  USER_NOT_REGISTERED (found, not registered) -> delete -> back to
  SUBSCRIBER_ABSENT (not found); password never reappears in any GET;
  unauthenticated fs-config requests get 401
- docs/EXTENSIONS.md
2026-08-28 07:39:19 -03:00
60e9f6838e feat: add Event Socket integration (b2bcall-fs-events)
- packages/telephony: TelephonyProvider interface (agente.md secao 25) and
  FreeSwitchTelephonyProvider implementation over the 'esl' library
  (actively maintained, TypeScript-native, built-in reconnect-with-backoff
  satisfying secao 195); normalizeEslEvent() translates raw ESL events into
  the internal vocabulary (secao 24)
- apps/freeswitch-events (b2bcall-fs-events): permanent ESL connection,
  resubscribes on every reconnect, publishes normalized events to the
  'b2bcall:events' Redis pub/sub channel; containerized (Dockerfile +
  docker-compose service) since its whole job is reaching the freeswitch
  container by internal hostname
- packages/shared: reusable createLogger() (structured JSON per secao 189),
  fixed a BigInt serialization crash surfaced by the esl library's error
  stats
- found and fixed a real FreeSWITCH 1.11 default: without an explicit
  apply-inbound-acl, mod_event_socket silently rejects any non-loopback
  connection ('Access Denied, go away.') even with the correct password —
  added a dedicated ACL (loopback + the Docker Compose network range, never
  0.0.0.0/0) in infrastructure/freeswitch/overrides/autoload_configs/
- verified end-to-end with a local loopback test call: CALL_CREATED ->
  CALL_ANSWERED -> CALL_ENDED observed on the Redis channel with the
  correct callUuid and hangup cause
- docs/EVENT_SOCKET.md
2026-08-28 06:47:28 -03:00