CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
security.ts262 linesDownload Raw Back to analytics
1/**2 * Analytics Security Utilities3 *4 * Token generation and validation for secure analytics tracking.5 * Prevents unauthorized data injection and replay attacks.6 */7 8import crypto from 'crypto';9 10const TOKEN_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days (for static sites)11 12/**13 * Generate a signed analytics tracking token14 * Token format (base64-encoded): deploymentId:timestamp:nonce:signature15 *16 * @param deploymentId - Deployment identifier17 * @returns Base64-encoded signed token18 */19export function generateAnalyticsToken(deploymentId: string): string {20  const secret = getAnalyticsSecret();21  const timestamp = Date.now().toString();22  const nonce = crypto.randomBytes(8).toString('hex');23  const payload = `${deploymentId}:${timestamp}:${nonce}`;24 25  const signature = crypto26    .createHmac('sha256', secret)27    .update(payload)28    .digest('hex');29 30  const token = `${payload}:${signature}`;31  return Buffer.from(token).toString('base64');32}33 34/**35 * Verify an analytics tracking token36 *37 * @param token - Base64-encoded token from client38 * @param expectedDeploymentId - Expected deployment ID39 * @returns true if valid, false otherwise40 */41export function verifyAnalyticsToken(42  token: string,43  expectedDeploymentId: string44): boolean {45  try {46    const secret = getAnalyticsSecret();47 48    // Decode token49    const decoded = Buffer.from(token, 'base64').toString('utf-8');50    const parts = decoded.split(':');51 52    if (parts.length !== 4) {53      return false; // Invalid format54    }55 56    const [deploymentId, timestamp, nonce, signature] = parts;57 58    // Verify deployment ID matches59    if (deploymentId !== expectedDeploymentId) {60      return false;61    }62 63    // Verify timestamp is recent (prevent replay attacks)64    const tokenAge = Date.now() - parseInt(timestamp, 10);65    if (tokenAge > TOKEN_EXPIRY_MS || tokenAge < 0) {66      return false; // Token expired or from future67    }68 69    // Verify signature70    const payload = `${deploymentId}:${timestamp}:${nonce}`;71    const expectedSignature = crypto72      .createHmac('sha256', secret)73      .update(payload)74      .digest('hex');75 76    // Constant-time comparison to prevent timing attacks77    return crypto.timingSafeEqual(78      Buffer.from(signature),79      Buffer.from(expectedSignature)80    );81  } catch (error) {82    // Invalid token format or other error83    return false;84  }85}86 87/**88 * Get analytics secret from environment89 * Generates a random secret if not configured (dev only)90 */91function getAnalyticsSecret(): string {92  const secret = process.env.ANALYTICS_SECRET;93 94  if (!secret) {95    // In development, use a stable secret to persist across restarts96    if (process.env.NODE_ENV === 'development') {97      console.warn(98        '[Analytics Security] ANALYTICS_SECRET not set, using development secret (not for production)'99      );100      return 'dev-analytics-secret-do-not-use-in-production-change-this-value';101    }102 103    throw new Error(104      'ANALYTICS_SECRET environment variable must be set in production'105    );106  }107 108  return secret;109}110 111/**112 * Validate request origin against allowed domains113 *114 * @param request - Incoming request115 * @param allowedOrigins - Array of allowed origin URLs116 * @returns true if origin is allowed, false otherwise117 */118export function validateOrigin(119  request: Request,120  allowedOrigins: string[]121): boolean {122  const origin = request.headers.get('origin') || '';123  const referer = request.headers.get('referer') || '';124 125  return allowedOrigins.some((allowed) => {126    if (allowed.includes('*')) {127      const suffix = allowed.replace(/^https?:\/\/\*/, '');128      const matchesOrigin = origin.endsWith(suffix) && /^https?:\/\//.test(origin);129      const matchesReferer = referer.endsWith(suffix) || referer.includes(suffix + '/');130      return matchesOrigin || matchesReferer;131    }132    return origin.startsWith(allowed) || referer.startsWith(allowed);133  });134}135 136/**137 * Get allowed origins for a deployment138 *139 * @param deploymentId - Deployment identifier140 * @param customDomain - Optional custom domain141 * @returns Array of allowed origin URLs142 */143export function getAllowedOrigins(144  deploymentId: string,145  customDomain?: string | null146): string[] {147  const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';148 149  const origins: string[] = [150    `${appUrl}/deployments/${deploymentId}`, // Published deployment path151    appUrl // Base app URL (for development/testing)152  ];153 154  // Add localhost variations for development155  if (appUrl.includes('localhost')) {156    origins.push('http://localhost:3000');157    origins.push('http://127.0.0.1:3000');158  }159 160  // Add custom domain if configured161  if (customDomain) {162    origins.push(`https://${customDomain}`);163    origins.push(`http://${customDomain}`);164  }165 166  // Allow subdomain-routed deployments (e.g., my-site.oswstudio.com)167  const appHost = appUrl.replace(/^https?:\/\//, '').split(':')[0];168  if (appHost && !appHost.includes('localhost')) {169    origins.push(`https://*.${appHost}`);170    origins.push(`http://*.${appHost}`);171  }172 173  return origins;174}175 176/**177 * Generate token hash for storage (to verify tokens without storing plaintext)178 *179 * @param token - Token to hash180 * @returns SHA-256 hash of token181 */182export function hashToken(token: string): string {183  return crypto184    .createHash('sha256')185    .update(token)186    .digest('hex');187}188 189/**190 * Check if user agent appears to be a bot191 *192 * @param userAgent - User agent string193 * @returns true if likely a bot, false otherwise194 */195export function isLikelyBot(userAgent: string): boolean {196  if (!userAgent) return true; // No user agent = suspicious197 198  const lowerUA = userAgent.toLowerCase();199 200  // Common bot indicators201  const botPatterns = [202    'bot',203    'crawl',204    'spider',205    'scrape',206    'curl',207    'wget',208    'python',209    'java',210    'http',211    'go-http-client',212    'axios',213    'fetch',214    'node-fetch',215    'requests', // Python216    'urllib',217    'headless',218    'phantom',219    'selenium',220    'puppeteer',221    'playwright'222  ];223 224  return botPatterns.some((pattern) => lowerUA.includes(pattern));225}226 227/**228 * Detect suspicious request patterns229 *230 * @param data - Analytics data to validate231 * @returns true if suspicious, false otherwise232 */233export function isSuspiciousRequest(data: {234  pagePath?: string;235  referrer?: string;236  userAgent?: string;237}): boolean {238  // Check for obviously fake/malicious data239  if (data.pagePath && data.pagePath.length > 500) {240    return true; // Unreasonably long path241  }242 243  if (data.referrer && data.referrer.length > 500) {244    return true; // Unreasonably long referrer245  }246 247  if (data.userAgent && data.userAgent.length > 500) {248    return true; // Unreasonably long user agent249  }250 251  // Check for SQL injection attempts252  const sqlPatterns = /(union|select|insert|update|delete|drop|create|alter)/i;253  if (254    (data.pagePath && sqlPatterns.test(data.pagePath)) ||255    (data.referrer && sqlPatterns.test(data.referrer))256  ) {257    return true;258  }259 260  return false;261}262