40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
import { createHmac, timingSafeEqual } from 'crypto';
|
|
|
|
const SECRET =
|
|
process.env.UNSUBSCRIBE_SECRET ||
|
|
process.env.COOKIE_SECRET ||
|
|
process.env.SECRET ||
|
|
'default-secret-change-in-production';
|
|
|
|
export function generateUnsubscribeToken(email: string, category: string): string {
|
|
const data = `${email}:${category}`;
|
|
return createHmac('sha256', SECRET).update(data).digest('hex');
|
|
}
|
|
|
|
export function verifyUnsubscribeToken(
|
|
email: string,
|
|
category: string,
|
|
token: string,
|
|
): boolean {
|
|
const expectedToken = generateUnsubscribeToken(email, category);
|
|
const tokenBuffer = Buffer.from(token, 'hex');
|
|
const expectedBuffer = Buffer.from(expectedToken, 'hex');
|
|
|
|
// Handle length mismatch safely to avoid timing leaks
|
|
if (tokenBuffer.length !== expectedBuffer.length) {
|
|
// Compare against zero-filled buffer of same length as token to maintain constant time
|
|
const zeroBuffer = Buffer.alloc(tokenBuffer.length);
|
|
timingSafeEqual(tokenBuffer, zeroBuffer);
|
|
return false;
|
|
}
|
|
|
|
return timingSafeEqual(tokenBuffer, expectedBuffer);
|
|
}
|
|
|
|
export function getUnsubscribeUrl(email: string, category: string): string {
|
|
const token = generateUnsubscribeToken(email, category);
|
|
const params = new URLSearchParams({ email, category, token });
|
|
const dashboardUrl = process.env.DASHBOARD_URL || 'http://localhost:3000';
|
|
return `${dashboardUrl}/unsubscribe?${params.toString()}`;
|
|
}
|