TS40 linhas1.431 bytes
apps/worker/src/services/assinatura.ts
SHA-256
35b41f68ea3f9b90c01c9099021b37288314ad31431f9d2f6b12d2fa1a65d996
Somente leiturafonte-eb8a54281800
/**
* HMAC-SHA256 com o ADMIN_SECRET — links de ação para o admin (veto da
* newsletter) e pseudonimização de IP/e-mail no rate limit.
*/
const encoder = new TextEncoder()
async function hmacHex(secret: string, msg: string): Promise<string> {
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
)
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(msg))
return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, '0')).join('')
}
/** Comparação em tempo constante (para strings de mesmo alfabeto). */
export function iguaisTempoConstante(a: string, b: string): boolean {
if (a.length !== b.length) return false
let diff = 0
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
return diff === 0
}
export function assinarVeto(secret: string, edicao: string): Promise<string> {
return hmacHex(secret, `newsletter-veto:${edicao}`)
}
export async function verificarVeto(secret: string, edicao: string, sig: string): Promise<boolean> {
return iguaisTempoConstante(await assinarVeto(secret, edicao), sig)
}
/** Identificador estável e não reversível (sem o segredo) para buckets de rate limit. */
export async function pseudonimo(secret: string, valor: string): Promise<string> {
return (await hmacHex(secret, `rate-limit:${valor}`)).slice(0, 32)
}