// Message relay. The Worker never inspects ciphertext — it just routes an // encrypted envelope to the recipient's Mailbox Durable Object, which delivers // live over WebSocket or queues until the recipient reconnects. import { Hono } from "hono"; import { requireAuth } from "../lib/auth.js"; import { now, uuid } from "../lib/util.js"; const app = new Hono(); function mailboxStub(env, userId) { const id = env.MAILBOX.idFromName(userId); return env.MAILBOX.get(id); } // POST /api/messages/send // body: { toUserId, messages: [{ toDeviceId, type, body }] } // one entry per recipient device // `body` is Signal ciphertext (base64); `type` is the Signal message type (1=whisper,3=prekey). app.post("/messages/send", requireAuth(), async (c) => { const fromUserId = c.get("userId"); const fromDeviceId = c.get("deviceId"); const payload = await c.req.json().catch(() => null); if (!payload?.toUserId || !Array.isArray(payload.messages)) return c.json({ error: "bad request" }, 400); const ts = now(); const results = []; for (const m of payload.messages) { const envelope = { id: uuid(), fromUserId, fromDeviceId, toDeviceId: m.toDeviceId, type: m.type, body: m.body, // ciphertext — opaque to the server sentAt: ts, }; const stub = mailboxStub(c.env, payload.toUserId); const res = await stub.fetch("https://mailbox/deliver", { method: "POST", body: JSON.stringify(envelope), }); results.push({ toDeviceId: m.toDeviceId, delivered: res.ok }); } // Best-effort push wake-up (payloadless — client fetches the real ciphertext over WS). c.executionCtx.waitUntil(wakeRecipient(c.env, payload.toUserId)); return c.json({ ok: true, id: uuid(), sentAt: ts, results }); }); async function wakeRecipient(env, toUserId) { // Placeholder for Web Push fan-out; wired up once VAPID keys are configured. // Left intentionally minimal so message send never blocks on push. return; } // GET /api/ws — upgrade to WebSocket, routed to the caller's own Mailbox DO. app.get("/ws", async (c) => { // Auth via query token (browsers can't set headers on WebSocket handshakes). const token = c.req.query("token"); if (!token) return c.json({ error: "unauthorized" }, 401); const sess = await c.env.DB.prepare( "SELECT user_id, device_id, expires_at FROM sessions WHERE token = ?" ) .bind(token) .first(); if (!sess || sess.expires_at < now()) return c.json({ error: "unauthorized" }, 401); const stub = mailboxStub(c.env, sess.user_id); const url = new URL("https://mailbox/connect"); url.searchParams.set("deviceId", sess.device_id); return stub.fetch(url.toString(), { headers: { Upgrade: "websocket", Connection: "Upgrade" }, }); }); export default app;