59 lines
1.5 KiB
TypeScript
59 lines
1.5 KiB
TypeScript
import { createHmac } from 'crypto'
|
|
|
|
interface ApprovePayload {
|
|
channels: { id: string; name: string }[]
|
|
email: string
|
|
exp: number
|
|
}
|
|
|
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
|
|
|
|
function getSecret(): string {
|
|
const secret = process.env.TOKEN_SECRET
|
|
if (!secret) throw new Error('TOKEN_SECRET env var is not set')
|
|
return secret
|
|
}
|
|
|
|
export function createApproveToken(
|
|
channels: { id: string; name: string }[],
|
|
email: string,
|
|
): string {
|
|
const payload: ApprovePayload = {
|
|
channels,
|
|
email,
|
|
exp: Date.now() + SEVEN_DAYS_MS,
|
|
}
|
|
const data = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
|
const sig = createHmac('sha256', getSecret()).update(data).digest('hex')
|
|
return `${data}.${sig}`
|
|
}
|
|
|
|
export function verifyApproveToken(token: string): ApprovePayload | null {
|
|
const parts = token.split('.')
|
|
if (parts.length !== 2) return null
|
|
|
|
const [data, sig] = parts
|
|
const expected = createHmac('sha256', getSecret()).update(data).digest('hex')
|
|
|
|
// Constant-time comparison
|
|
if (sig.length !== expected.length) return null
|
|
let mismatch = 0
|
|
for (let i = 0; i < sig.length; i++) {
|
|
mismatch |= sig.charCodeAt(i) ^ expected.charCodeAt(i)
|
|
}
|
|
if (mismatch !== 0) return null
|
|
|
|
try {
|
|
const payload: ApprovePayload = JSON.parse(
|
|
Buffer.from(data, 'base64url').toString('utf-8'),
|
|
)
|
|
|
|
if (!payload.exp || !payload.channels || !payload.email) return null
|
|
if (Date.now() > payload.exp) return null
|
|
|
|
return payload
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|