integrations: webhooks and API tokens
ci / check (push) Successful in 54s
ci / images (push) Successful in 21s

API tokens (Settings -> API tokens): Authorization: Bearer sp_<40 base62>,
stored as sha256, shown once. A token acts as its admin with their current
role and stops when they're disabled; Bearer requests skip the CSRF header
and never fall back to the cookie. A token can't manage tokens.

Webhooks (Settings -> Webhooks): call.started/answered/ended,
voicemail.new, emergency.call, trunk/extension registration changes, from
the live state; queued per webhook, sent every 10 s with
X-SelfPhoneNG-Signature (HMAC of timestamp.body), retried after 1 m, 5 m,
30 m, 2 h, 12 h, then failed. Private/loopback targets are refused in the
connection's own DNS lookup (no rebinding), unless allowed. Send test and
a delivery log per webhook.

The e2e found voicemail.new missing for mailboxes without a user: the
voicemail scan now queues it for every mailbox. vmpin.test.ts gets 180 s:
Git Bash forks make it ~85 s on Windows (the earlier flaky gate run).

Task: M11-T5
This commit is contained in:
iamdoubz committed 2026-10-19 19:40:47 -05:00
1 parent d229ed2c45
commit 60846afddd
17 files changed
+1319 -8

No files matched your search

+25
View File
@@ -41,6 +41,13 @@
- Header `Authorization: Bearer sp_<40 random base62 chars>`. Stored as sha256.
Tokens act as the admin who created them. Shown once on creation.
As built (M11-T5): `auth/tokens.ts`; `requireAuth` takes a Bearer header before the cookie (a
Bearer request never falls back to the cookie, and an unknown or revoked token is 401). The token
acts as its creator with the creator's current role, so it stops working when they're disabled.
Bearer requests don't need `X-Requested-With` (no cookie is used, and a cross-site page can't set
Authorization). A token can't list, make or revoke tokens (403): a leaked one can't make itself a
spare. `lastUsedAt` is written on every request. Settings → API tokens.
### 2.4 Guards
Fastify `preHandler` hooks in `auth/guards.ts`:
- `requireAuth`: sets `req.user`.
@@ -501,6 +508,24 @@ Events: `call.started`, `call.answered`, `call.ended`, `voicemail.new`, `sms.rec
- Delivery: POST with 10 s timeout. A 2xx response means done. Otherwise retry after 1 m, 5 m, 30 m, 2 h, 12 h, then mark `failed`.
- Block private/loopback target IPs unless setting `webhooks.allowPrivate = true` (SSRF protection; homelab users can turn it on).
As built (M11-T5): `services/webhooks.ts`, `routes/integrations.ts`, Settings → Webhooks.
- Events come from the live state's messages (`watchWebhooks`): `call.started` (first time a
call is seen, also for calls in progress when the api starts), `call.answered`, `call.ended`
(the last ActiveCall + `endedAt`, not the CDR), `emergency.call`,
`trunk.registration_changed` and `extension.registration_changed` (only on a change, with
`previous` / `online`). `voicemail.new` (VoicemailDto + `extension`) is queued by the voicemail
scan itself, for every mailbox: the live message only goes to the mailbox's user. `sms.*` can
be picked but never fire until SMS is built (M9-T2).
- Each event becomes one `webhook_deliveries` row per enabled webhook that wants it; the
`webhookDelivery` job sends up to 20 due rows every 10 s, in parallel. 6 tries in all, then
`failed`. A webhook turned off keeps its pending rows until it's on again (or 30 days pass).
- The secret is 32 random bytes as hex, stored encrypted, shown to admins on the webhook.
- The private-address check is in the DNS lookup of the connection itself (node:http with a
`lookup`), so DNS rebinding can't get past it; IP literals are checked before. Blocked: 0/8,
10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16, 224/3, ::, ::1, fc00::/7, fe80::/10,
ff00::/8 and IPv4-mapped forms of those. Redirects aren't followed.
- `POST /:id/test` sends `webhook.test` right away (not queued) and returns `{ok, status, error}`.
## 10. Email (`services/mail.ts`)
nodemailer transport from `settings.smtp`. Templates are plain TypeScript functions returning `{subject, text, html}`: