Skip to content

feat: call ordinary phones from qrypt.chat, 10 free minutes a day (0.14.0) - #335

Merged
ralyodio merged 1 commit into
masterfrom
feat/phone-calls
Oct 26, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/phone-calls

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Anthony: "rebuild so it works like skype used to" (after looking at Suparelay), then "let people make ten minutes of phone calls a day for free".

What: /phone (and a "Phone" link in the navbar) is a dialer for US and Canadian numbers: a keypad, the minutes left today, mute, keypad tones during the call, hang up, and recent calls. Signed-in accounts get 10 free minutes a day. Calls to phones aren't end-to-end encrypted, and the page says so.

How (Telnyx, all on the existing account)

  • Browser line: each user gets a Telnyx telephony credential on the qrypt.chat calls credential connection, which has no outbound voice profile. A WebRTC token can only receive calls, so a stolen or abused token can't dial anything.
  • Placing a call: POST /api/phone/call checks the number and the allowance, saves a phone_calls row, and rings the user's line through the qrypt.chat call control app (carrying client_state {c: callId}). On call.answered it transfers that leg to the number with time_limit_secs = the allowance, so Telnyx ends the call itself when the minutes run out.
  • Billing: call.answered / call.hangup on the phone leg give answer → hang-up seconds. /api/phone/webhook verifies Ed25519 signatures with a 5-minute replay window, and accepts the bare 32-byte key we have as well as PEM.
  • Rules (src/lib/phone/rules.js): 911/N11, 900/976 and fictional 555-01XX are refused. Anonymous accounts get no free minutes. Operators (PHONE_OPERATOR_IDS: Anthony and the agents) get an hour per call. One live call per person.
  • Telnyx-side safety: the outbound profile allows US/CA only with a $10/day spend cap, and caller ID is forced to +1 949-284-7328 (ANI override).

Verified against the real phone network before writing the app:

  • Headless Chromium logged in with a minted token, the server rang it, the browser answered, and the server transferred it to NIST's time line.
  • The recorded audio transcribes as "At the tone, 12 hours, 43 minutes, coordinated universal time".
  • Telnyx ended the call itself at the 30 s limit (hangup_cause: time_limit).
  • phone.test.js replays that exact event sequence (including the extra internal leg Telnyx reports) and checks 33 billed seconds.

Server setup already done on dev2: migration 20261026120000_phone_calls.sql applied. TELNYX_CALL_APP_ID, TELNYX_CREDENTIAL_CONNECTION_ID, PHONE_CALLER_ID and PHONE_OPERATOR_IDS added to app.env (backup bak-009) and to vault qryptchat-web--prod.

Tests: bun run test:ci passes, apart from the known-flaky exit test. The local next build fails on dev2 even on plain master, because the box's Bun is 1.4.4 and the repo pins 1.4.0. CI builds with the pinned version.

Found along the way, not fixed here: src/app/api/webhooks/telnyx/sms/route.js verifies with format: 'pem', but the stored TELNYX_PUBLIC_KEY is the bare 32-byte key, so inbound SMS webhooks there probably always fail verification.

🤖 Generated with Claude Code

…14.0)

Anthony: "rebuild so it works like skype used to" (after Suparelay), and "let
people make ten minutes of phone calls a day for free".

/phone (Navbar "Phone"): a dialer for US and Canadian numbers on Telnyx.
- The browser's line is a per-user Telnyx telephony credential on a credential
  connection with NO outbound voice profile: a WebRTC token cannot dial the phone
  network. POST /api/phone/call checks the number and the allowance, rings that
  line through a Call Control app, and on call.answered transfers it to the number
  with time_limit_secs = the allowance, so Telnyx itself ends the call.
- call.answered / call.hangup on the phone leg (signed Ed25519 webhooks at
  /api/phone/webhook, raw 32-byte or PEM key) bill answer -> hang-up seconds.
- 10 free minutes a day (UTC) for signed-in accounts, none for anonymous ones,
  operators (PHONE_OPERATOR_IDS) an hour a call. 911/N11, 900/976 and fictional
  555-01XX numbers are refused; one live call per person.
- Caller ID is forced to PHONE_CALLER_ID by the connection's ANI override. The
  outbound profile allows US/CA only with a $10/day spend cap.
- The page says plainly that calls to phones are not end-to-end encrypted.

Tables phone_lines and phone_calls (service role only), migration applied on dev2.
Tested against the real phone network before building: browser rang, server
transferred to NIST's time line (+1 303-499-7111), audio transcribed as "At the
tone, 12 hours, 43 minutes, coordinated universal time", and Telnyx ended the
call at the time limit (hangup_cause time_limit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​telnyx/​webrtc@​2.28.0991009999100

View full report

@socket-security

Copy link
Copy Markdown
Contributor

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Priority Alert  (click "▶" to expand/collapse) Action
Low priority
Deprecated by its maintainer: npm uuid

Reason: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).

From: package.json → npm/@telnyx/webrtc@2.28.0 → npm/uuid@7.0.3

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/uuid@7.0.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
Low priority
Deprecated by its maintainer: npm uuid

Reason: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).

From: package.json → npm/@telnyx/webrtc@2.28.0 → npm/uuid@8.3.2

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/uuid@8.3.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn

View full report

@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

16 finding(s)

MEDIUM: 12 | LOW: 4

Severity Rule Location
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:142
MEDIUM js-unescaped-html-sink src/app/layout.jsx:146
MEDIUM js-unescaped-html-sink src/app/page.jsx:50
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:116
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:121
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:85
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:130
MEDIUM js-unescaped-html-sink src/lib/components/PitFooter.jsx:15
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

const result = await handleEvent({ ...phoneContext(), event });
return NextResponse.json({ ok: true, ...result });
} catch (error) {
console.error('[phone/webhook]', event?.event_type, error?.message);
@ralyodio
ralyodio merged commit d25749a into master Oct 26, 2026
11 checks passed
@ralyodio
ralyodio deleted the feat/phone-calls branch October 26, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants