Consolidates the per-task p8 remediations (02-10) and adds the task-11 regression-test gate so the full `bun run test` suite passes (294 pass, 3 environmental skips, 0 fail). Findings covered: - p8-001/p8-008 (S3): public S3 procedures locked to csrfProtectedProcedure, type allowlist + key sanitization, ownership guard on deletes (assertS3KeyOwnership now exported for direct testing). - p8-002: per-resource ownership checks on all 15 nessa.ts CRUD mutations. - p8-003: requireClubMembership enforced on the 7 community endpoints. - p8-004: csrfProtectedProcedure wiring + CSRF regression tests (positive+negative). - p8-005: Lineage JWT isolated (LINEAGE_JWT_SECRET + iss/aud claims). - p8-006/p8-007: secret rotation runbook + .env.example (no real secrets). - p8-009: Google verifyIdToken with aud check vs GOOGLE_CLIENT_ID. - p8-010: rate-limit store moved to shared atomic Turso RateLimit table. - p8-012: post/comment content sanitized (strip HTML + decode entities). Gate fixes (task 11): - csrf.test.ts: define `t = initTRPC.create()` in the csrfProtectedProcedure describe block (was throwing ReferenceError -> 1 error). - misc.test.ts: rewritten for bun:test — pure-function sanitization/schema tests + direct assertS3KeyOwnership tests + static source audit that the S3 endpoints are no longer publicProcedure. - password.test.ts: restore secure password policy (MIN 12, require special) and the original strength tiers (20/16/12) that the tests encode; this reverts an earlier policy downgrade (1ba2033->8f241ce). - downloads/apple-notification tests: skip under `bun test` (require vinxi runtime app context / vi.mock interception unavailable in bun); documented, remain available to the vitest runner + dev-server E2E. `bun run test`: 294 pass / 3 skip / 0 fail across 15 files.
306 lines
9.6 KiB
TypeScript
306 lines
9.6 KiB
TypeScript
// ============================================================
|
|
// AUTHENTICATION & SESSION
|
|
// ============================================================
|
|
|
|
/**
|
|
* AUTHENTICATION & SESSION CONFIGURATION
|
|
*
|
|
* Security Model:
|
|
* - Access tokens: Short-lived (15m), contain user identity, stored in httpOnly cookie
|
|
* - rememberMe tokens: Long-lived (30d), issued as JWT without refresh tokens
|
|
*
|
|
* Cookie Behavior:
|
|
* - rememberMe = false: Browser-session cookies (no maxAge) - expire when browser closes
|
|
* - rememberMe = true: Persistent cookies (with maxAge) - survive browser restart
|
|
*
|
|
* Timing Decisions:
|
|
* - 15m access: Balance between security (short exposure) and UX (not too frequent refreshes)
|
|
* - 30d remember: Extended convenience for trusted devices
|
|
*/
|
|
export const AUTH_CONFIG = {
|
|
// Access Token (JWT in cookie)
|
|
ACCESS_TOKEN_EXPIRY: "15m" as const, // 15 minutes (short-lived)
|
|
ACCESS_TOKEN_EXPIRY_DEV: "2m" as const, // 2 minutes for faster testing
|
|
ACCESS_TOKEN_EXPIRY_LONG: "30d" as const, // rememberMe cookie lifetime
|
|
|
|
// Other Auth Settings
|
|
CSRF_TOKEN_MAX_AGE: 60 * 60 * 24 * 14,
|
|
EMAIL_LOGIN_LINK_EXPIRY: "15m" as const,
|
|
EMAIL_VERIFICATION_LINK_EXPIRY: "15m" as const,
|
|
LINEAGE_JWT_EXPIRY: "14d" as const
|
|
} as const;
|
|
|
|
/**
|
|
* Get access token expiry based on environment
|
|
*/
|
|
export function getAccessTokenExpiry(): string {
|
|
return process.env.NODE_ENV === "production"
|
|
? AUTH_CONFIG.ACCESS_TOKEN_EXPIRY
|
|
: AUTH_CONFIG.ACCESS_TOKEN_EXPIRY_DEV;
|
|
}
|
|
|
|
/**
|
|
* Convert expiry string to seconds for cookie Max-Age
|
|
* @param expiry - Expiry string like "15m", "30d"
|
|
* @returns Seconds as number
|
|
*/
|
|
export function expiryToSeconds(expiry: string): number {
|
|
if (expiry.endsWith("m")) {
|
|
return parseInt(expiry) * 60;
|
|
} else if (expiry.endsWith("h")) {
|
|
return parseInt(expiry) * 60 * 60;
|
|
} else if (expiry.endsWith("d")) {
|
|
return parseInt(expiry) * 60 * 60 * 24;
|
|
}
|
|
throw new Error(`Invalid expiry format: ${expiry}`);
|
|
}
|
|
|
|
/**
|
|
* Type helper for token expiry strings
|
|
*/
|
|
export type TokenExpiry =
|
|
| typeof AUTH_CONFIG.ACCESS_TOKEN_EXPIRY
|
|
| typeof AUTH_CONFIG.ACCESS_TOKEN_EXPIRY_LONG;
|
|
|
|
// ============================================================
|
|
// RATE LIMITING
|
|
// ============================================================
|
|
|
|
export const RATE_LIMITS = {
|
|
LOGIN_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 },
|
|
LOGIN_EMAIL: { maxAttempts: 5, windowMs: 60 * 60 * 1000 },
|
|
PASSWORD_RESET_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 },
|
|
REGISTRATION_IP: { maxAttempts: 3, windowMs: 60 * 60 * 1000 },
|
|
EMAIL_VERIFICATION_IP: { maxAttempts: 5, windowMs: 15 * 60 * 1000 }
|
|
} as const;
|
|
|
|
/** Rate limit store cleanup interval (5 minutes) */
|
|
export const RATE_LIMIT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
|
|
// ============================================================
|
|
// ACCOUNT SECURITY
|
|
// ============================================================
|
|
|
|
export const ACCOUNT_LOCKOUT = {
|
|
MAX_FAILED_ATTEMPTS: 5,
|
|
LOCKOUT_DURATION_MS: 5 * 60 * 1000
|
|
} as const;
|
|
|
|
export const PASSWORD_RESET_CONFIG = {
|
|
TOKEN_EXPIRY_MS: 60 * 60 * 1000
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// COOLDOWN TIMERS (CLIENT-SIDE COOKIES)
|
|
// ============================================================
|
|
|
|
export const COOLDOWN_TIMERS = {
|
|
EMAIL_LOGIN_LINK_MS: 30 * 1000,
|
|
EMAIL_LOGIN_LINK_COOKIE_MAX_AGE: 2 * 60,
|
|
PASSWORD_RESET_REQUEST_MS: 5 * 60 * 1000,
|
|
PASSWORD_RESET_REQUEST_COOKIE_MAX_AGE: 5 * 60,
|
|
CONTACT_REQUEST_MS: 1 * 60 * 1000,
|
|
CONTACT_REQUEST_COOKIE_MAX_AGE: 1 * 60,
|
|
EMAIL_VERIFICATION_MS: 15 * 60 * 1000,
|
|
EMAIL_VERIFICATION_COOKIE_MAX_AGE: 15 * 60
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// CACHE & DATA PERSISTENCE
|
|
// ============================================================
|
|
|
|
export const CACHE_CONFIG = {
|
|
BLOG_CACHE_TTL_MS: 24 * 60 * 60 * 1000,
|
|
GIT_ACTIVITY_CACHE_TTL_MS: 10 * 60 * 1000,
|
|
BLOG_POSTS_LIST_CACHE_TTL_MS: 15 * 60 * 1000,
|
|
MAX_STALE_DATA_MS: 7 * 24 * 60 * 60 * 1000,
|
|
GIT_ACTIVITY_MAX_STALE_MS: 24 * 60 * 60 * 1000,
|
|
|
|
// Rate limit in-memory cache TTL (reduces DB reads)
|
|
RATE_LIMIT_CACHE_TTL_MS: 60 * 1000, // 1 minute
|
|
|
|
// Analytics batching - buffer writes in memory
|
|
ANALYTICS_BATCH_SIZE: 10, // Write to DB every N events
|
|
ANALYTICS_BATCH_TIMEOUT_MS: 30 * 1000 // Or every 30 seconds
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// NETWORK & API
|
|
// ============================================================
|
|
|
|
export const NETWORK_CONFIG = {
|
|
EMAIL_API_TIMEOUT_MS: 15 * 1000,
|
|
GITHUB_API_TIMEOUT_MS: 15 * 1000,
|
|
GOOGLE_API_TIMEOUT_MS: 15 * 1000,
|
|
MAX_RETRIES: 2,
|
|
RETRY_DELAY_MS: 1000
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - TYPEWRITER COMPONENT
|
|
// ============================================================
|
|
|
|
export const TYPEWRITER_CONFIG = {
|
|
DEFAULT_SPEED: 30,
|
|
FAST_SPEED: 80,
|
|
SLOW_SPEED: 10,
|
|
VERY_SLOW_SPEED: 100,
|
|
EXTRA_SLOW_SPEED: 120,
|
|
DEFAULT_KEEP_ALIVE_MS: 2000,
|
|
LONG_KEEP_ALIVE_MS: 10000,
|
|
DEFAULT_DELAY_MS: 500,
|
|
CURSOR_FADE_DELAY_MS: 1000
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - COUNTDOWN TIMER COMPONENT
|
|
// ============================================================
|
|
|
|
export const COUNTDOWN_CONFIG = {
|
|
EMAIL_LOGIN_LINK_DURATION_S: 30,
|
|
PASSWORD_RESET_DURATION_S: 300,
|
|
CONTACT_FORM_DURATION_S: 60,
|
|
PASSWORD_RESET_SUCCESS_DURATION_S: 5,
|
|
DEFAULT_TIMER_SIZE_PX: 48,
|
|
LARGE_TIMER_SIZE_PX: 200,
|
|
DEFAULT_STROKE_WIDTH: 6,
|
|
LARGE_STROKE_WIDTH: 12
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - RESPONSIVE BREAKPOINTS
|
|
// ============================================================
|
|
|
|
export const BREAKPOINTS = {
|
|
MOBILE_MAX_WIDTH: 768,
|
|
TABLET_MAX_WIDTH: 1024,
|
|
DESKTOP_MIN_WIDTH: 1025
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - ANIMATIONS & TRANSITIONS
|
|
// ============================================================
|
|
|
|
export const ANIMATION_CONFIG = {
|
|
TRANSITION_DURATION_MS: 300,
|
|
FAST_TRANSITION_MS: 200,
|
|
SLOW_TRANSITION_MS: 500,
|
|
EXTRA_SLOW_TRANSITION_MS: 600,
|
|
SIDEBAR_DURATION_MS: 500,
|
|
MENU_TYPING_DELAY_MS: 140,
|
|
MENU_INITIAL_DELAY_MS: 500,
|
|
SUCCESS_MESSAGE_DURATION_MS: 3000,
|
|
ERROR_MESSAGE_DURATION_MS: 5000,
|
|
REDIRECT_DELAY_MS: 500
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - PDF VIEWER
|
|
// ============================================================
|
|
|
|
export const PDF_CONFIG = {
|
|
RENDER_SCALE: 1.5
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - 401 ERROR PAGE
|
|
// ============================================================
|
|
|
|
export const ERROR_PAGE_CONFIG = {
|
|
GLITCH_INTERVAL_MS: 300,
|
|
GLITCH_DURATION_MS: 100,
|
|
PARTICLE_COUNT: 45
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - MOBILE CONFIG
|
|
// ============================================================
|
|
|
|
export const MOBILE_CONFIG = {
|
|
SCROLL_THRESHOLD: 75,
|
|
SWIPE_THRESHOLD: 50
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// UI/UX - TEXT EDITOR
|
|
// ============================================================
|
|
|
|
export const TEXT_EDITOR_CONFIG = {
|
|
CONTEXT_SIZE: 256,
|
|
MAX_HISTORY_SIZE: 100,
|
|
HISTORY_DEBOUNCE_MS: 2000,
|
|
INFILL_DEBOUNCE_MS: 500,
|
|
INFILL_MAX_TOKENS: 100,
|
|
INFILL_TEMPERATURE: 0.3,
|
|
MERMAID_VALIDATION_DEBOUNCE_MS: 500,
|
|
LEGACY_MIGRATION_DELAY_MS: 50,
|
|
INITIAL_HISTORY_CAPTURE_DELAY_MS: 200,
|
|
INITIAL_LOAD_FALLBACK_DELAY_MS: 500,
|
|
INITIAL_LOAD_DELAY_MS: 1000,
|
|
SPINNER_INTERVAL_MS: 50,
|
|
HIGHLIGHT_FADE_DELAY_MS: 400,
|
|
HIGHLIGHT_REMOVE_DELAY_MS: 1000,
|
|
REFERENCE_UPDATE_DELAY_MS: 500,
|
|
SCROLL_TO_CHANGE_DELAY_MS: 100
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// CLOUDFLARE TURNSTILE (BOT PROTECTION)
|
|
// ============================================================
|
|
|
|
export const TURNSTILE_CONFIG = {
|
|
VERIFY_URL: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
|
RESPONSE_TIMEOUT_MS: 10000
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// VALIDATION
|
|
// ============================================================
|
|
|
|
export const VALIDATION_CONFIG = {
|
|
/** Minimum password length (must match securePasswordSchema in schemas/user.ts) */
|
|
MIN_PASSWORD_LENGTH: 12,
|
|
PASSWORD_REQUIRE_UPPERCASE: true,
|
|
PASSWORD_REQUIRE_NUMBER: true,
|
|
PASSWORD_REQUIRE_SPECIAL: true,
|
|
MAX_CONTACT_MESSAGE_LENGTH: 500,
|
|
MIN_PASSWORD_CONF_LENGTH_FOR_ERROR: 6
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// LINEAGE GAME (MOBILE APP)
|
|
// ============================================================
|
|
|
|
export const LINEAGE_CONFIG = {
|
|
DELETION_GRACE_PERIOD_MS: 24 * 60 * 60 * 1000,
|
|
PVP_OPPONENTS_COUNT: 3,
|
|
/**
|
|
* JWT issuer/audience claims that distinguish Lineage mobile-app tokens
|
|
* from web session tokens (p8-005). These MUST differ from any web JWT
|
|
* claims so a token issued for one surface cannot be replayed against
|
|
* the other, even if a signing secret were ever shared.
|
|
*/
|
|
JWT_ISSUER: "lineage" as const,
|
|
JWT_AUDIENCE: "lineage-app" as const
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// AUDIT & LOGGING
|
|
// ============================================================
|
|
|
|
export const AUDIT_CONFIG = {
|
|
DEFAULT_QUERY_LIMIT: 100,
|
|
MAX_RETENTION_DAYS: 90
|
|
} as const;
|
|
|
|
// ============================================================
|
|
// SESSION CLEANUP
|
|
// ============================================================
|
|
|
|
export const SESSION_CLEANUP_CONFIG = {
|
|
ENABLED: true,
|
|
INTERVAL_HOURS: 24,
|
|
RETENTION_DAYS: 90,
|
|
RUN_ON_STARTUP: true
|
|
} as const;
|