Files
publisher-dashboard/apps/api-server/src/utils/cookies.ts
RevIQ 829d365e80 Implement auth procedures with code review fixes
Add complete auth backend (Workstream D):
- Auth middleware for session/API key authentication
- Signup with password or passkey (WebAuthn)
- Login flow with device trust and email confirmation
- Password reset and email verification
- Session management and logout

Utilities created:
- cookies.ts: Cookie helpers and configuration
- crypto.ts: Token generation and hashing
- password.ts: zxcvbn validation, argon2id hashing
- geo.ts: IP/location extraction from headers
- email.ts: Stubbed email sending
- session.ts: Session creation and device trust

Code review improvements applied:
- Use ORPCError instead of Error in procedures
- Add ast-grep rule to enforce ORPCError usage
- Remove error info leakage (generic messages)
- Optimize N+1 query with JOIN in login-password
- Extract signupWithPassword/signupWithPasskey for testability
- Add 15-minute WebAuthn challenge expiry check
- Strengthen CookieOptions type definitions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:19:15 +08:00

110 lines
2.6 KiB
TypeScript

/**
* Cookie configuration for authentication
* All cookies use 'rev.' prefix, HttpOnly, Secure, SameSite=Lax
*/
export const COOKIE_NAMES = {
SESSION_TOKEN: "rev.session_token",
DEVICE_FINGERPRINT: "rev.device_fingerprint",
LOGIN_REQUEST_TOKEN: "rev.login_request_token",
} as const;
export const COOKIE_DURATIONS = {
SESSION: 60 * 60 * 24 * 7, // 7 days in seconds
DEVICE_FINGERPRINT: 60 * 60 * 24 * 365, // 1 year in seconds
LOGIN_REQUEST: 60 * 15, // 15 minutes in seconds
} as const;
export const TOKEN_DURATIONS = {
EMAIL_VERIFICATION: 60 * 60 * 24, // 24 hours in seconds
PASSWORD_RESET: 60 * 60, // 1 hour in seconds
} as const;
// Base cookie options (all cookies share these)
const baseCookieOptions = {
httpOnly: true,
secure: true,
sameSite: "lax" as const,
path: "/",
};
export const COOKIE_OPTIONS = {
session: { ...baseCookieOptions, maxAge: COOKIE_DURATIONS.SESSION },
device: {
...baseCookieOptions,
maxAge: COOKIE_DURATIONS.DEVICE_FINGERPRINT,
},
loginRequest: {
...baseCookieOptions,
maxAge: COOKIE_DURATIONS.LOGIN_REQUEST,
},
} as const;
/**
* Cookie options type for setCookie function
*/
export interface CookieOptions {
httpOnly?: boolean;
secure?: boolean;
sameSite?: "strict" | "lax" | "none";
path?: string;
maxAge?: number;
}
/**
* Parse cookie string and get a specific cookie value
*/
export const getCookie = (
headers: Headers,
name: string,
): string | undefined => {
const cookieHeader = headers.get("Cookie");
if (!cookieHeader) {
return undefined;
}
const cookies = cookieHeader.split(";").map((c) => c.trim());
for (const cookie of cookies) {
const [cookieName, ...valueParts] = cookie.split("=");
if (cookieName === name) {
return valueParts.join("=");
}
}
return undefined;
};
/**
* Set a cookie in the response headers
*/
export const setCookie = (
headers: Headers,
name: string,
value: string,
options: CookieOptions,
): void => {
const parts = [`${name}=${value}`];
if (options.httpOnly) {
parts.push("HttpOnly");
}
if (options.secure) {
parts.push("Secure");
}
if (options.sameSite) {
parts.push(`SameSite=${options.sameSite}`);
}
if (options.path) {
parts.push(`Path=${options.path}`);
}
if (options.maxAge) {
parts.push(`Max-Age=${String(options.maxAge)}`);
}
headers.append("Set-Cookie", parts.join("; "));
};
/**
* Delete a cookie by setting it to expire immediately
*/
export const deleteCookie = (headers: Headers, name: string): void => {
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
};