From 36572d99f3dd2e07ca88ce4189baa255820be270 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 01:57:38 +0200 Subject: [PATCH] feat(intake): token generator and shape check Phase 8a Task 2. 24 bytes of CSPRNG entropy, base64url-encoded to exactly 32 URL-safe characters. isIntakeToken matches that exact shape so the public route can fail fast on bogus input without hitting the database or charging the rate limiter. --- src/lib/intake/token.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/lib/intake/token.ts diff --git a/src/lib/intake/token.ts b/src/lib/intake/token.ts new file mode 100644 index 0000000..f5fcda9 --- /dev/null +++ b/src/lib/intake/token.ts @@ -0,0 +1,24 @@ +import { randomBytes } from "node:crypto"; + +/** + * 24 bytes of entropy, base64url encoded → 32 URL-safe characters. + * + * The token is the link's whole authentication. It must be unguessable and + * must not be derived from anything the public can see (no project id, no + * timestamp, no hash of either). `crypto.randomBytes` satisfies both: it is + * CSPRNG output, and the rest of the system treats the result as opaque. + * + * 24 bytes is 192 bits of entropy. Brute-forcing one token against the + * `(token)` unique index is wasted effort at any realistic rate limit. + */ +export function generateIntakeToken(): string { + return randomBytes(24).toString("base64url"); +} + +const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32}$/; + +/** True when `value` is the shape the generator produces. Used by the + * public route to reject obviously-bogus input early. */ +export function isIntakeToken(value: unknown): value is string { + return typeof value === "string" && TOKEN_PATTERN.test(value); +}