update shared utils

This commit is contained in:
JCEEE
2026-08-16 10:12:43 +01:00
parent 15535a7b0e
commit e2b99c45b6
2 changed files with 85 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
// Minimal signed-token helper (HMAC-SHA256, JWT-ish but not JWT) shared by the
// Hono proxy (issues + verifies) and the SvelteKit frontend (verifies locally
// so hooks.server.ts doesn't need a round-trip to the proxy on every request).
//
// The proxy independently re-verifies the same token on every write, so the
// frontend's local verification is a performance optimization, not the
// security boundary — the boundary is the proxy.
import crypto from "node:crypto";
// Dev-only fallback secrets. Both sides must derive the same default when the
// real env var isn't set, so local dev works without extra setup. Production
// MUST set SESSION_TOKEN_SECRET / PLATFORM_TOKEN_SECRET to a strong value.
export const DEV_SESSION_TOKEN_SECRET = "famchamp-dev-session-secret-change-me";
export const DEV_PLATFORM_TOKEN_SECRET = "famchamp-dev-platform-secret-change-me";
export type TokenPayload = Record<string, unknown> & { exp: number };
function encode(json: string): string {
return Buffer.from(json).toString("base64url");
}
function sign(secret: string, encodedPayload: string): string {
return crypto.createHmac("sha256", secret).update(encodedPayload).digest("base64url");
}
/** Signs `payload` (plus an `exp` computed from `ttlMs`) into an opaque token string. */
export function issueToken<T extends Record<string, unknown>>(
secret: string,
payload: T,
ttlMs: number,
): string {
const encoded = encode(JSON.stringify({ ...payload, exp: Date.now() + ttlMs }));
return `${encoded}.${sign(secret, encoded)}`;
}
/** Verifies signature + expiry. Returns the decoded payload, or null if invalid/expired/tampered. */
export function verifyToken<T extends TokenPayload = TokenPayload>(
secret: string,
token: string | undefined | null,
): T | null {
if (!token) return null;
const [encoded, sig] = token.split(".");
if (!encoded || !sig) return null;
const expected = sign(secret, encoded);
const a = Buffer.from(sig);
const b = Buffer.from(expected);
// Constant-time comparison — avoids leaking signature bytes via timing.
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
try {
const payload = JSON.parse(Buffer.from(encoded, "base64url").toString()) as T;
if (typeof payload.exp !== "number" || payload.exp <= Date.now()) return null;
return payload;
} catch {
return null;
}
}