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); +}