Migrate SRS to Cloudflare Worker: D1 state, email digest, review issues, live charts

This commit is contained in:
Prad Nukala
2026-08-24 19:11:07 -04:00
parent 6be5d55366
commit 7213cc1c8a
40 changed files with 2933 additions and 2132 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* One-tap email links: `GET /log?p=704&r=pass&d=2026-08-31&sig=<hmac>`.
* The sig is HMAC-SHA256 over `p|r|d` with LINK_KEY (WebCrypto, hex).
* Verification is constant-time; links older than 3 days are rejected.
*/
async function hmacKey(secret: string): Promise<CryptoKey> {
return crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
}
export async function signLink(
key: string,
p: number,
r: string,
d: string,
): Promise<string> {
const mac = await crypto.subtle.sign(
"HMAC",
await hmacKey(key),
new TextEncoder().encode(`${p}|${r}|${d}`),
);
return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
export async function verifyLink(
key: string,
p: number,
r: string,
d: string,
sig: string,
): Promise<boolean> {
if (!/^[0-9a-f]{64}$/.test(sig)) return false;
const bytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) bytes[i] = Number.parseInt(sig.slice(i * 2, i * 2 + 2), 16);
// crypto.subtle.verify is constant-time; never compare hex strings directly.
return crypto.subtle.verify(
"HMAC",
await hmacKey(key),
bytes,
new TextEncoder().encode(`${p}|${r}|${d}`),
);
}
/** Constant-time equality for webhook signatures and admin bearer keys. */
export async function timingSafeEqual(a: string, b: string): Promise<boolean> {
// HMAC both sides with a random key: unequal-length inputs and content
// differences are equally invisible to timing.
const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, false, [
"sign",
]);
const enc = new TextEncoder();
const [ma, mb] = await Promise.all([
crypto.subtle.sign("HMAC", key, enc.encode(a)),
crypto.subtle.sign("HMAC", key, enc.encode(b)),
]);
const va = new Uint8Array(ma);
const vb = new Uint8Array(mb);
let diff = 0;
for (let i = 0; i < va.length; i++) diff |= va[i]! ^ vb[i]!;
return diff === 0;
}
/** GitHub webhook `X-Hub-Signature-256: sha256=<hex>` verification. */
export async function verifyWebhook(
secret: string,
body: ArrayBuffer,
header: string | null,
): Promise<boolean> {
if (!header?.startsWith("sha256=")) return false;
const mac = await crypto.subtle.sign("HMAC", await hmacKey(secret), body);
const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
return timingSafeEqual(header.slice(7), expected);
}