courtmitra / packages /domain /src /policies /reply-token.ts
Hetansh Waghela
module 4-8 done, bugs fixed
7aa8d9d
Raw
History Blame
2.06 kB
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<string, unknown>;
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;
}
}