pingql/apps/shared/auth.ts

60 lines
2.1 KiB
TypeScript

import { createHmac, randomBytes, timingSafeEqual } from "crypto";
const EMAIL_HMAC_KEY = process.env.EMAIL_HMAC_KEY || "pingql-default-hmac-key";
export function generateKey(): string {
return randomBytes(32).toString("base64url");
}
export function hashEmail(email: string): string {
return createHmac("sha256", EMAIL_HMAC_KEY).update(email.toLowerCase().trim()).digest("hex");
}
export function extractAuthKey(headers: Record<string, string | undefined>, cookie: any): string | null {
const authHeader = headers["authorization"] ?? "";
const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim();
return bearer ?? cookie?.pingql_key?.value ?? null;
}
export async function resolveKey(
sql: any, key: string, opts?: { trackUsage?: boolean }
): Promise<{ accountId: string; keyId: string | null; plan: string } | null> {
const [account] = await sql`SELECT id, plan FROM accounts WHERE key = ${key}`;
if (account) return { accountId: account.id, keyId: null, plan: account.plan };
const [apiKey] = await sql`SELECT k.id, k.account_id, a.plan FROM api_keys k JOIN accounts a ON a.id = k.account_id WHERE k.key = ${key}`;
if (apiKey) {
if (opts?.trackUsage !== false) {
sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${apiKey.id}`.catch(() => {});
}
return { accountId: apiKey.account_id, keyId: apiKey.id, plan: apiKey.plan };
}
return null;
}
export const COOKIE_OPTS = {
httpOnly: true,
secure: process.env.COOKIE_SECURE !== "false",
sameSite: "none" as const,
path: "/",
domain: process.env.COOKIE_DOMAIN ?? ".pingql.com",
maxAge: 60 * 60 * 24 * 30,
};
export function safeTokenCompare(a: string | undefined, b: string | undefined): boolean {
if (!a || !b) return false;
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return timingSafeEqual(bufA, bufB);
}
export const SECURITY_HEADERS = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
"X-XSS-Protection": "0",
"Referrer-Policy": "strict-origin-when-cross-origin",
};