1import type { Database } from "bun:sqlite";2import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";3import type { Context } from "hono";4import { getCookie, setCookie } from "hono/cookie";5import { clientIp } from "../server/client.ts";6import { setting } from "../server/db.ts";789export const MIN_SECONDS_ON_PAGE = 3;10const STAMP_MAX_AGE = 86400;11export const FILINGS_PER_HOUR = 5;12export const REPLIES_PER_HOUR = 20;13export const MAX_CHARS = 20_000;14const FORM_COOKIE = "kepr_form";15const HEX = /^[0-9a-f]{32,64}$/;161718function secret(db: Database): string {19 db.query("INSERT OR IGNORE INTO settings (key, value) VALUES ('issue_secret', ?)").run(20 randomBytes(32).toString("hex"),21 );22 return setting(db, "issue_secret") as string;23}2425function mac(db: Database, message: string): string {26 return createHmac("sha256", secret(db)).update(message).digest("hex");27}282930export function ipHash(db: Database, c: Context): string {31 return mac(db, `ip:${clientIp(c)}`);32}333435export function followToken(db: Database, followerId: number): string {36 return mac(db, `follow:${followerId}`);37}3839export function followTokenMatches(db: Database, followerId: number, token: string): boolean {40 const expected = Buffer.from(followToken(db, followerId));41 const given = Buffer.from(token);42 return given.length === expected.length && timingSafeEqual(given, expected);43}444546474849export function formStamp(db: Database, c: Context, now: number): string {50 let cookie = getCookie(c, FORM_COOKIE);51 if (!cookie || !HEX.test(cookie)) {52 cookie = randomBytes(16).toString("hex");53 setCookie(c, FORM_COOKIE, cookie, {54 httpOnly: true,55 secure: true,56 sameSite: "Strict",57 path: "/",58 });59 }60 return `${now}.${mac(db, `form:${now}:${cookie}`)}`;61}6263export type StampCheck = "ok" | "invalid" | "too-fast";6465export function checkStamp(db: Database, c: Context, stamp: string, now: number): StampCheck {66 const cookie = getCookie(c, FORM_COOKIE);67 const [time, signature] = stamp.split(".");68 if (!cookie || !time || !signature || !/^\d{1,12}$/.test(time)) return "invalid";69 const expected = Buffer.from(mac(db, `form:${time}:${cookie}`));70 const given = Buffer.from(signature);71 if (given.length !== expected.length || !timingSafeEqual(given, expected)) return "invalid";72 const age = now - Number(time);73 if (age > STAMP_MAX_AGE || age < 0) return "invalid";74 return age < MIN_SECONDS_ON_PAGE ? "too-fast" : "ok";75}