import { log } from './logger.js'; const RELAY_API = 'https://api.bloby.bot/api'; // ─── Register a new handle ────────────────────────────────────────────────── export async function registerHandle( username: string, tier: string, walletAddress?: string, ): Promise<{ token: string; relayUrl: string }> { const payload: Record = { username, tier }; if (walletAddress) payload.walletAddress = walletAddress; const res = await fetch(`${RELAY_API}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `Registration failed (${res.status})`); } return { token: data.token, relayUrl: data.relayUrl }; } // ─── Check username availability ───────────────────────────────────────────── export async function checkAvailability( username: string, ): Promise<{ valid: boolean; error?: string; handles: { tier: string; url: string; paid: boolean; price: number; available: boolean }[]; }> { const res = await fetch(`${RELAY_API}/availability/${encodeURIComponent(username)}`); return res.json(); } // ─── Claim a reserved (purchased) handle ──────────────────────────────────── export async function claimReservedHandle( handle: string, hash: string, walletAddress?: string, ): Promise<{ token: string; relayUrl: string }> { const payload: Record = { handle, hash }; if (walletAddress) payload.walletAddress = walletAddress; const res = await fetch(`${RELAY_API}/handle/claim-reserved`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || `Claim failed (${res.status})`); } return { token: data.token, relayUrl: data.relayUrl }; } // ─── Release handle ───────────────────────────────────────────────────────── export async function releaseHandle(token: string): Promise { const res = await fetch(`${RELAY_API}/handle`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `Release failed (${res.status})`); } } // ─── Update tunnel URL ────────────────────────────────────────────────────── export async function updateTunnelUrl(token: string, tunnelUrl: string): Promise { const res = await fetch(`${RELAY_API}/tunnel`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ tunnelUrl }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `Tunnel update failed (${res.status})`); } log.ok('Relay updated with tunnel URL'); } // ─── Heartbeat ─────────────────────────────────────────────────────────────── let heartbeatTimer: ReturnType | null = null; export function startHeartbeat(token: string, tunnelUrl?: string): void { stopHeartbeat(); const beat = async () => { try { await fetch(`${RELAY_API}/heartbeat`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify(tunnelUrl ? { tunnelUrl } : {}), }); } catch { // Silent — next heartbeat will retry } }; beat(); // immediate first beat // Every 120s. The relay's HEARTBEAT_TIMEOUT_MS must stay well above this (currently // 360s = 3 missed beats of grace) so a single dropped beat never flaps a healthy bot. heartbeatTimer = setInterval(beat, 120_000); } export function stopHeartbeat(): void { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } // ─── Disconnect (graceful shutdown) ────────────────────────────────────────── export async function disconnect(token: string): Promise { stopHeartbeat(); try { await fetch(`${RELAY_API}/disconnect`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }); } catch { // Best-effort — if it fails, heartbeat timeout will mark offline } }