rebreak-monorepo/backend/server/api/calls/ice-servers.get.ts
chahinebrini 0cac3c9d1a feat(calls): Phase 1a — TURN ice-servers endpoint + coturn ops + DM call-button header
Backend:
- GET /api/calls/ice-servers: ephemeral HMAC TURN credentials (10-min TTL),
  iceTransportPolicy:"relay" (no IP leak), 503 until coturn configured
- nitro runtimeConfig: turnHost/turnSecret/turnRealm (Infisical staging set)

Ops:
- ops/calls/ runbook + turnserver.conf (self-hosted coturn, force-relay,
  use-auth-secret, hardening). coturn provisioned + verified on rebreak-server.

Frontend (DM header redesign):
- removed standalone "i" button; header center (avatar+name+chevron) opens info sheet
- call icon top-right, only when canCall (mutual-follow + callsEnabled);
  shows "coming soon" until the WebRTC client lands

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:06:33 +02:00

53 lines
1.7 KiB
TypeScript

/**
* GET /api/calls/ice-servers
*
* Liefert ephemere TURN-Credentials für eine WebRTC-Audio-Verbindung.
* coturn läuft mit `use-auth-secret` (TURN-REST-API-Schema):
* username = "<unix_expiry>:<userId>"
* credential = base64( HMAC-SHA1(TURN_SECRET, username) )
* coturn akzeptiert das ohne DB-Lookup, die Credentials laufen nach TTL ab.
*
* Privacy: iceTransportPolicy = "relay" → der Client tauscht NIE direkte
* Host-IPs aus, alles läuft über coturn. Ist TURN nicht konfiguriert, wird
* bewusst 503 geworfen (kein IP-leakender STUN-only-Fallback).
*
* Response: { iceServers: RTCIceServer[], iceTransportPolicy: "relay", ttl: number }
*/
import { createHmac } from "node:crypto";
import { requireUser } from "../../utils/auth";
const TTL_SECONDS = 600; // 10 min — länger als ein typischer Call-Aufbau
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const config = useRuntimeConfig(event);
const host = config.turnHost as string;
const secret = config.turnSecret as string;
if (!host || !secret) {
// coturn/Secrets noch nicht eingerichtet → Calls sind nicht verfügbar.
throw createError({ statusCode: 503, statusMessage: "calls_not_configured" });
}
const expiry = Math.floor(Date.now() / 1000) + TTL_SECONDS;
const username = `${expiry}:${user.id}`;
const credential = createHmac("sha1", secret).update(username).digest("base64");
return {
iceServers: [
{
urls: [
`turn:${host}:3478?transport=udp`,
`turn:${host}:3478?transport=tcp`,
`turns:${host}:5349?transport=tcp`,
],
username,
credential,
},
],
iceTransportPolicy: "relay" as const,
ttl: TTL_SECONDS,
};
});