mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +00:00
feat(api): add SRS Cloudflare Worker with email digest, charts, and D1 integration
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user