Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 357x 357x 16x 1x 15x 15x 15x 15x 1x 14x 14x 2x 12x 1x 11x 16x 16x 2x 9x 9x 1x 8x 1x 7x 7x 7x 357x 11x 9x 9x 135x 9x | import { Hono } from "hono";
import type { ContainerContext } from "../../crypto/lifecycle.js";
import { storeInboundEmail } from "../../storage/email-storage.js";
interface InboundConnectorRow {
id: number;
cf_email_webhook_secret: string | null;
}
/** Expected payload from a Cloudflare Email Worker */
interface CloudflareEmailPayload {
from: string;
to: string;
raw: string;
rawSize: number;
}
/**
* Webhook routes for push-based inbound connectors.
*
* These routes are mounted BEFORE the lock middleware so they can reject
* requests with a proper 503 when the container is locked rather than a
* generic 423. Messages are only stored when the container is unlocked.
*/
export function webhookRoutes(context: ContainerContext): Hono {
const api = new Hono();
/**
* POST /api/webhook/cloudflare-email/:connectorId
*
* Receives an email from a Cloudflare Email Worker and stores it for all
* identities linked to the given inbound connector.
*
* Authentication: Bearer token in Authorization header, matched against
* cf_email_webhook_secret stored in inbound_connectors.
*
* Expected body (JSON):
* { from: string, to: string, raw: string (base64), rawSize: number }
*/
api.post("/cloudflare-email/:connectorId", async (c) => {
if (context.state !== "unlocked" || !context.db) {
return c.json({ error: "Service unavailable: container is locked" }, 503);
}
const db = context.db;
// Parse connector ID
const connectorIdStr = c.req.param("connectorId");
const connectorId = Number(connectorIdStr);
if (!Number.isInteger(connectorId) || connectorId <= 0) {
return c.json({ error: "Invalid connector ID" }, 400);
}
// Look up the connector
const connector = db
.prepare(
"SELECT id, cf_email_webhook_secret FROM inbound_connectors WHERE id = ? AND type = 'cloudflare-email'",
)
.get(connectorId) as InboundConnectorRow | undefined;
if (!connector) {
return c.json({ error: "Connector not found" }, 404);
}
if (!connector.cf_email_webhook_secret) {
return c.json({ error: "Connector is not fully configured" }, 400);
}
// Validate Authorization header
const authHeader = c.req.header("Authorization") ?? "";
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
if (!timingSafeEqual(token, connector.cf_email_webhook_secret)) {
return c.json({ error: "Unauthorized" }, 401);
}
// Parse body
let payload: CloudflareEmailPayload;
try {
payload = await c.req.json();
} catch {
return c.json({ error: "Invalid JSON body" }, 400);
}
if (!payload.raw || typeof payload.raw !== "string") {
return c.json({ error: "Missing required field: raw" }, 400);
}
// Parse and store the email
let result: { stored: number };
try {
result = await storeInboundEmail(db, connectorId, payload);
} catch (err) {
return c.json(
{
error: `Failed to parse email: ${err instanceof Error ? err.message : String(err)}`,
},
400,
);
}
return c.json({ ok: true, stored: result.stored });
});
return api;
}
/** Constant-time string comparison to prevent timing attacks */
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
|