import { createHmac, timingSafeEqual } from "node:crypto"; function getKey(): string { const key = process.env["TOKEN_ENCRYPTION_KEY"]; if (!key) throw new Error("Missing TOKEN_ENCRYPTION_KEY for reply token verification"); return key; } interface ReplyTokenData { actionId: string; missionId: string; nonce: string; } function base64UrlEncode(value: string): string { return Buffer.from(value, "utf8").toString("base64url"); } function base64UrlDecode(value: string): string { return Buffer.from(value, "base64url").toString("utf8"); } function sign(payload: string): string { return createHmac("sha256", getKey()).update(payload).digest("base64url"); } function isReplyTokenData(value: unknown): value is ReplyTokenData { if (!value || typeof value !== "object") return false; const candidate = value as Record; return ( typeof candidate["actionId"] === "string" && typeof candidate["missionId"] === "string" && typeof candidate["nonce"] === "string" ); } export function parseReplyAddress(address: string): { token: string } | null { const match = address.match(/^reply\+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)@/i); if (!match) return null; return { token: match[1]! }; } export function verifyReplyToken(token: string): ReplyTokenData | null { const [payload, signature] = token.split("."); if (!payload || !signature) return null; const expected = sign(payload); if (!tokensMatch(signature, expected)) return null; try { const decoded: unknown = JSON.parse(base64UrlDecode(payload)); return isReplyTokenData(decoded) ? decoded : null; } catch { return null; } } export function generateReplyToken( actionId: string, missionId: string, nonce: string, ): string { const payload = base64UrlEncode(JSON.stringify({ actionId, missionId, nonce })); return `${payload}.${sign(payload)}`; } export function tokensMatch(a: string, b: string): boolean { try { return timingSafeEqual(Buffer.from(a), Buffer.from(b)); } catch { return false; } }