update shared utils
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Name → handle / slug helpers shared by frontend + proxy.
|
||||
|
||||
// URL-safe family slug (hyphenated, lowercase): "The Smiths" → "the-smiths".
|
||||
export function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
// Whitespace-free lowercase handle used for a member's URL segment and as the
|
||||
// per-family username suffix: "Jakey Boy" → "jakeyboy", "Joe Edhook" → "joeedhook".
|
||||
export function handle(name: string): string {
|
||||
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
// A member's PB username is namespaced by the family slug so it stays globally
|
||||
// unique (PB requires auth-identity usernames to be unique) even though the URL
|
||||
// segment is only unique within a family: "miss-fits:jakeyboy".
|
||||
export function famUsername(famSlug: string, handleName: string): string {
|
||||
return `${slugify(famSlug)}:${handleName}`;
|
||||
}
|
||||
|
||||
// Reverse of famUsername — the URL segment is the part after the last ":".
|
||||
export function handleOf(fullUsername: string): string {
|
||||
const i = fullUsername.lastIndexOf(':');
|
||||
return i >= 0 ? fullUsername.slice(i + 1) : fullUsername;
|
||||
}
|
||||
Reference in New Issue
Block a user