File size: 1,279 Bytes
ef4c36f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { createHmac, timingSafeEqual } from "crypto";

/**
 * Shared-secret check for the public automation endpoint. Returns null when the
 * caller is allowed, otherwise the reason to reject with.
 *
 * ponytail: an API key is the whole security model here — one render burns ~10
 * minutes of CPU, so an open endpoint is free DoS for anyone who finds it.
 */
export function checkApiKey(req: Request): string | null {
  const expected = process.env.REEL_API_KEY;

  // fail closed: no key configured means the endpoint stays shut, not wide open
  if (!expected) return "REEL_API_KEY is not configured on the server";

  const provided =
    req.headers.get("x-api-key") ||
    req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ||
    "";

  if (!provided) return "Missing x-api-key header";
  if (!safeEqual(provided, expected)) return "Invalid API key";

  return null;
}

// ponytail: HMAC both sides first so timingSafeEqual never sees mismatched
// lengths (it throws) and length itself doesn't leak through timing
function safeEqual(a: string, b: string): boolean {
  const key = "reel-api-key-compare";
  const ha = createHmac("sha256", key).update(a).digest();
  const hb = createHmac("sha256", key).update(b).digest();
  return timingSafeEqual(ha, hb);
}