Spaces:
Running
Running
| 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; | |
| } | |
| } | |