From d077b0ea8a858b203d6cc62dd755a37cf1b37b71 Mon Sep 17 00:00:00 2001 From: Devanarayanan Date: Wed, 25 Feb 2026 02:32:21 +0530 Subject: [PATCH 01/14] feat: TypeScript migration, perf overhaul, floating icons, v1.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING / MIGRATION - Rename all .js/.jsx source files to .ts/.tsx with full type annotations - Remove jsconfig.json; add tsconfig.json (strict mode, Next.js plugin) - Rename proxy.js → proxy.ts (CSP middleware) FEATURES - Add floating background icons (hardware: microchip/server/memory/HDD/ database/network; trading: chart-line/bar/area/bitcoin; physics: atom) as fixed, GPU-composited, pointer-events-none layer (opacity 0.10–0.15) - Add physics easter egg in footer (responsive font via clamp()) - Add RUNTIME_ENV: MEC Kochi line to footer terminal - Add Galaxy Watch 4 to THE_ARSENAL panel CI/CD - Add Lighthouse CI job (guard → lighthouse → audit → build-push-sign) with lighthouserc.json: accessibility ≥0.9 error, perf/BP/SEO ≥0.9 warn - Update dependabot: unified groups (all-dependencies / all-actions), ignore eslint ≥ 10 to keep v9 pinned for eslint-config-next compat PERFORMANCE - Reduce SCROLL_LOCK_DURATION 1100ms → 800ms (less input lag) - Add will-change:transform + contain:layout style paint to FloatingBgIcons (promotes to own GPU compositor tile, prevents scroll paint invalidation) - Add contain:layout style to .scroll-snap-section (isolates reflow) - Restore production caching: remove force-dynamic/revalidate=0/fetchCache exports from layout.tsx (was disabling all caching in production) SECURITY / DEPENDENCIES - Add minimatch override ^10.2.2 (fixes 5 high CVEs in eslint-config-next dependency tree); npm audit now reports 0 vulnerabilities - Pin eslint@9.39.1 (eslint@10 breaks eslint-config-next@16 plugin stack) BUG FIXES - Fix React hydration mismatch: move md:auto-rows-[minmax(18rem,1fr)] from JSX className to .explorer-grid CSS class (arbitrary Tailwind values with parens/commas were dropped inconsistently between SSR and client) - Revert explorer card inner scrollbars (panels now expand naturally) - Fix footer UTC+05:30 / deployed UTC suffix to stay single-line on mobile - Fix min-h-[44px]/min-w-[44px] → min-h-11/min-w-11; h-[28rem]/w-[28rem] → h-112/w-md (eliminate Tailwind arbitrary-value lint warnings) - Fix bare email links in README.md and SECURITY.md (MD034) - Fix project structure code fence missing language tag (MD040) - Fix bold-as-heading in README.md contact section (MD036) DOCS - Update README: version badge (v1.1.1), TypeScript 5.9 badge, React Icons badge; add floating icons feature; update file tree (.ts/.tsx, proxy.ts, tsconfig.json, lighthouserc.json, ErrorHandler.tsx); update tech stack; bump Last Updated to February 25 2026 --- .github/dependabot.yml | 34 +- .github/workflows/deploy.yml | 36 + README.md | 46 +- SECURITY.md | 6 +- app/api/analytics/{route.js => route.ts} | 212 +-- app/globals.css | 21 +- app/layout.js | 220 --- app/layout.tsx | 235 ++++ app/legal/{page.js => page.tsx} | 22 +- app/{not-found.js => not-found.tsx} | 0 app/{page.js => page.tsx} | 1210 +++++++++++------ app/{sitemap.js => sitemap.ts} | 4 +- components/{Analytics.js => Analytics.tsx} | 14 +- .../{ErrorHandler.js => ErrorHandler.tsx} | 2 +- eslint.config.mjs | 4 + jsconfig.json | 7 - ...nalytics-config.js => analytics-config.ts} | 2 +- lib/{analytics.js => analytics.ts} | 37 +- lib/{legal.js => legal.ts} | 0 lighthouserc.json | 24 + next.config.mjs | 37 +- package-lock.json | 1004 +++++++------- package.json | 21 +- proxy.js => proxy.ts | 6 +- tsconfig.json | 33 + 25 files changed, 1987 insertions(+), 1250 deletions(-) rename app/api/analytics/{route.js => route.ts} (74%) delete mode 100644 app/layout.js create mode 100644 app/layout.tsx rename app/legal/{page.js => page.tsx} (93%) rename app/{not-found.js => not-found.tsx} (100%) rename app/{page.js => page.tsx} (59%) rename app/{sitemap.js => sitemap.ts} (74%) rename components/{Analytics.js => Analytics.tsx} (90%) rename components/{ErrorHandler.js => ErrorHandler.tsx} (93%) delete mode 100644 jsconfig.json rename lib/{analytics-config.js => analytics-config.ts} (94%) rename lib/{analytics.js => analytics.ts} (83%) rename lib/{legal.js => legal.ts} (100%) create mode 100644 lighthouserc.json rename proxy.js => proxy.ts (95%) create mode 100644 tsconfig.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5e21113..35b00da 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,31 @@ version: 2 updates: - # Enable version updates for npm - - package-ecosystem: "npm" - # Look for `package.json` and `lock` files in the `root` directory - directory: "/" - # Check the npm registry for updates every day (weekdays) + # npm dependency updates + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "daily" + interval: 'weekly' + day: 'monday' + groups: + all-dependencies: + update-types: + - 'minor' + - 'patch' + ignore: + # Keep eslint pinned at v9 — v10 breaks eslint-config-next + - dependency-name: 'eslint' + versions: ['>= 10'] + open-pull-requests-limit: 10 + + # GitHub Actions version updates + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + day: 'monday' + groups: + all-actions: + update-types: + - 'minor' + - 'patch' + open-pull-requests-limit: 5 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 88a0ff1..f2be32e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,6 +45,42 @@ jobs: - name: Build check run: npm run build + # ------------------------------------------------------------------ + # JOB 1b: LIGHTHOUSE CI (Performance + Accessibility) + # ------------------------------------------------------------------ + lighthouse: + needs: guard + name: 🔦 Lighthouse CI + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Harden Runner + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@2f8dda6cf4de7d73b29853c3f29e73a01e297bd8 # v12.1.0 + with: + configPath: ./lighthouserc.json + uploadArtifacts: true + temporaryPublicStorage: true + # ------------------------------------------------------------------ # JOB 2: SECURITY AUDIT (SAST + SCA) # ------------------------------------------------------------------ diff --git a/README.md b/README.md index 600947d..98d2d09 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ **Where code meets conscience.** A brutalist × cyberpunk portfolio built with Next.js 16. +[![Version](https://img.shields.io/badge/version-1.1.1-cyan?logo=github)](package.json) [![Security: SLSA Level 3](https://img.shields.io/badge/SLSA-Level%203-brightgreen)](https://slsa.dev) [![Security Scan: Trivy](https://img.shields.io/badge/Security-Trivy%20Scanned-blue)](.github/workflows/deploy.yml) [![Attestations](https://img.shields.io/badge/Attestations-Enabled-success)](https://github.com/devakesu/devakesu-web/attestations) @@ -11,13 +12,15 @@ [![React](https://img.shields.io/badge/React-19.2-61DAFB?logo=react&logoColor=white)](https://react.dev) [![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.1.18-38B2AC?logo=tailwind-css&logoColor=white)](https://tailwindcss.com) [![Node.js](https://img.shields.io/badge/Node.js-20.x-339933?logo=node.js&logoColor=white)](https://nodejs.org) -[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![React Icons](https://img.shields.io/badge/React%20Icons-5.5-e91e63?logo=react&logoColor=white)](https://react-icons.github.io/react-icons/) ## ✨ Features ### 🎨 Design & UX - **Cyberpunk Aesthetic** - Glitch effects, VHS flicker, scanlines, neon glow +- **Floating Background Icons** - Hardware, trading & physics icons layered at low opacity across the full page - **Interactive Elements** - Laser cursor, parallax scrolling, click burst animations - **Smooth Section Scrolling** - Ultra-smooth one-section-per-scroll navigation on desktop - **Haptic Feedback** - Mobile vibration on interactions @@ -68,12 +71,13 @@ ## 🛠️ Tech Stack - **Framework**: Next.js 16.1.6 (App Router) +- **Language**: TypeScript 5.9 (strict mode, full coverage) - **Styling**: Tailwind CSS 4.1.18 (CSS-based configuration) - **Fonts**: Space Grotesk, JetBrains Mono -- **Icons**: React Icons 5.5.0 +- **Icons**: React Icons 5.5.0 (hardware, trading, physics, social) - **Analytics**: Server-side Google Analytics (optional) - **Deployment**: Coolify (Self-hosted) -- **CI/CD**: GitHub Actions with SLSA provenance +- **CI/CD**: GitHub Actions with SLSA provenance & Lighthouse CI --- @@ -114,24 +118,26 @@ npm start ## 📁 Project Structure -``` +```text devakesu-web/ ├── app/ │ ├── api/ │ │ └── analytics/ -│ │ └── route.js # Server-side analytics API +│ │ └── route.ts # Server-side analytics API │ ├── legal/ -│ │ └── page.js # Privacy & legal policies -│ ├── page.js # Main portfolio page -│ ├── layout.js # Root layout with fonts & scripts +│ │ └── page.tsx # Privacy & legal policies +│ ├── page.tsx # Main portfolio page (Client Component) +│ ├── layout.tsx # Root layout with fonts & scripts │ ├── globals.css # Global styles, animations & scroll config -│ ├── not-found.js # Custom 404 page -│ └── favicon.svg # Site icon (Next.js App Router) +│ ├── not-found.tsx # Custom 404 page +│ └── sitemap.ts # Dynamic XML sitemap ├── components/ -│ └── Analytics.js # Client analytics component +│ ├── Analytics.tsx # Client analytics component +│ └── ErrorHandler.tsx # Global error boundary ├── lib/ -│ ├── analytics.js # Analytics helper functions -│ └── legal.js # Legal content (privacy, terms, cookies) +│ ├── analytics.ts # Analytics helper functions +│ ├── analytics-config.ts # Analytics configuration +│ └── legal.ts # Legal content (privacy, terms, cookies) ├── public/ │ ├── js/ │ │ ├── cursor.js # Laser cursor effect @@ -146,8 +152,10 @@ devakesu-web/ │ └── deploy.yml # CI/CD pipeline with SLSA provenance ├── .vscode/ # VSCode workspace settings ├── next.config.mjs # Next.js configuration -├── middleware.js # CSP middleware with nonce support +├── proxy.ts # CSP middleware with per-request nonces +├── tsconfig.json # TypeScript configuration ├── tailwind.config.js # Tailwind CSS configuration +├── lighthouserc.json # Lighthouse CI thresholds ├── package.json # Dependencies & scripts ├── SECURITY.md # Security policy & reporting ├── .env.example # Environment variable template @@ -198,7 +206,7 @@ devakesu-web/ If you discover a security vulnerability in this project, please report it privately: -- **Email**: fusion@devakesu.com +- **Email**: [fusion@devakesu.com](mailto:fusion@devakesu.com) - **Subject**: Security Vulnerability Report - **See**: [SECURITY.md](SECURITY.md) for detailed reporting guidelines @@ -373,10 +381,10 @@ This is a personal portfolio, but suggestions and bug reports are welcome! ## 📡 Contact -**Devanarayanan (Kesu)** +### Devanarayanan (Kesu) - 🌐 Website: [devakesu.com](https://devakesu.com) -- 📧 Email: fusion@devakesu.com +- 📧 Email: [fusion@devakesu.com](mailto:fusion@devakesu.com) - 💼 LinkedIn: [@devakesu](https://linkedin.com/in/devakesu) - 🐙 GitHub: [@devakesu](https://github.com/devakesu) - 📸 Instagram: [@deva.kesu](https://instagram.com/deva.kesu) @@ -404,5 +412,5 @@ _Love is the only way to rescue humanity from all evils._ --- -**Last Updated**: February 12, 2026 -**Version**: 1.1.0 +**Last Updated**: February 25, 2026 +**Version**: 1.1.1 diff --git a/SECURITY.md b/SECURITY.md index 256872c..1bd6dc2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -55,7 +55,7 @@ If you discover a security vulnerability, please report it responsibly: ### How to Report -1. **Email**: fusion@devakesu.com +1. **Email**: [fusion@devakesu.com](mailto:fusion@devakesu.com) 2. **Subject**: Security Vulnerability Report 3. **Include**: - Description of the vulnerability @@ -148,5 +148,5 @@ We appreciate security researchers who responsibly disclose vulnerabilities. --- -**Last Updated**: February 12, 2026 -**Version**: 1.1.0 +**Last Updated**: February 25, 2026 +**Version**: 1.1.1 diff --git a/app/api/analytics/route.js b/app/api/analytics/route.ts similarity index 74% rename from app/api/analytics/route.js rename to app/api/analytics/route.ts index 104e37a..3a84605 100644 --- a/app/api/analytics/route.js +++ b/app/api/analytics/route.ts @@ -1,5 +1,10 @@ -import { NextResponse } from 'next/server'; -import { sendAnalyticsEvent, getClientId, getSessionId, isAnalyticsConfigured } from '@/lib/analytics'; +import { NextRequest, NextResponse } from 'next/server'; +import { + sendAnalyticsEvent, + getClientId, + getSessionId, + isAnalyticsConfigured, +} from '@/lib/analytics'; import { isIP } from 'node:net'; // Module-level flag to prevent log flooding during misconfiguration @@ -26,7 +31,12 @@ let hasLoggedProdIpWarning = false; // // For single-instance deployments (e.g., traditional VPS, single Docker container), // this in-memory approach is sufficient and performant. -const rateLimitMap = new Map(); +interface RateLimitRecord { + count: number; + resetTime: number; +} + +const rateLimitMap = new Map(); const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute const RATE_LIMIT_MAX_REQUESTS = 60; // 60 requests per minute per IP const RATE_LIMIT_MAX_REQUESTS_UNKNOWN = 10; // Stricter limit for unknown IPs @@ -40,7 +50,7 @@ const UNKNOWN_IP_KEY = '__unknown__'; // Special key for requests without valid * @param {Map} map - The rate limit map * @param {number} targetSize - Target size to reduce the map to */ -function evictOldestEntries(map, targetSize) { +function evictOldestEntries(map: Map, targetSize: number): void { const excess = map.size - targetSize; if (excess <= 0) return; @@ -55,7 +65,7 @@ function evictOldestEntries(map, targetSize) { } } -function checkRateLimit(ip, isUnknown = false) { +function checkRateLimit(ip: string, isUnknown = false): boolean { const now = Date.now(); const record = rateLimitMap.get(ip); const maxRequests = isUnknown ? RATE_LIMIT_MAX_REQUESTS_UNKNOWN : RATE_LIMIT_MAX_REQUESTS; @@ -66,7 +76,7 @@ function checkRateLimit(ip, isUnknown = false) { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS, }); - + // Incremental cleanup: remove a limited number of expired entries per request if (rateLimitMap.size > MAX_MAP_SIZE) { let cleaned = 0; @@ -79,13 +89,13 @@ function checkRateLimit(ip, isUnknown = false) { } } } - + // Hard cap: if still over limit after cleanup, evict oldest entries if (rateLimitMap.size > MAX_MAP_SIZE) { evictOldestEntries(rateLimitMap, MAX_MAP_SIZE); } } - + return true; } @@ -99,71 +109,71 @@ function checkRateLimit(ip, isUnknown = false) { /** * Extracts the client IP address from request headers. - * + * * DEPLOYMENT ARCHITECTURE ASSUMPTIONS: * This function prioritizes headers in the following order, which assumes a specific deployment setup: * 1. cf-connecting-ip (Cloudflare CDN) - Most trusted when behind Cloudflare * 2. x-real-ip (nginx/Apache reverse proxy) - Common for traditional reverse proxies * 3. x-forwarded-for (various proxies/load balancers) - Takes first IP in chain - * + * * CONFIGURATION NOTES: * - If NOT behind Cloudflare: Consider prioritizing x-real-ip or x-forwarded-for * - Behind AWS ALB/ELB: x-forwarded-for is the standard header * - Behind Google Cloud Load Balancer: x-forwarded-for is used * - Behind Azure Front Door: x-azure-clientip or x-forwarded-for - * + * * The current order assumes Cloudflare as the primary CDN. If your deployment differs, * you can modify the priority order in this function, or consider making it configurable * via an environment variable (e.g., PRIMARY_IP_HEADER=x-real-ip) for better flexibility * across different deployment architectures without code changes. - * + * * SECURITY WARNING: * These headers can be spoofed if not properly configured at the reverse proxy level. * Ensure your reverse proxy strips/overwrites these headers from client requests. - * + * * DEVELOPMENT TESTING: * In development mode, set TEST_CLIENT_IP environment variable to test IP-based logic * with a specific IP address (e.g., TEST_CLIENT_IP=203.0.113.45). - * + * * @param {Headers} headerList - The Headers object from the request * @returns {string|null} The client IP address or null if it cannot be determined */ -function getClientIp(headerList) { - const cf = headerList.get("cf-connecting-ip")?.trim(); +function getClientIp(headerList: Headers): string | null { + const cf = headerList.get('cf-connecting-ip')?.trim(); if (cf && isIP(cf)) return cf; - const realIp = headerList.get("x-real-ip")?.trim(); + const realIp = headerList.get('x-real-ip')?.trim(); if (realIp && isIP(realIp)) return realIp; - const forwarded = headerList.get("x-forwarded-for"); - const forwardedIp = forwarded?.split(",")[0]?.trim(); + const forwarded = headerList.get('x-forwarded-for'); + const forwardedIp = forwarded?.split(',')[0]?.trim(); if (forwardedIp && isIP(forwardedIp)) return forwardedIp; // In development, allow testing with a specific IP via environment variable // SECURITY NOTE: NODE_ENV should be set securely in deployment configuration // to prevent accidental exposure of development-only behavior in production - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === 'development') { const testIp = process.env.TEST_CLIENT_IP; - + // Log warning once per server start to make it prominent but avoid spam if (!hasLoggedDevIpWarning) { hasLoggedDevIpWarning = true; console.warn( - "\n" + - "═══════════════════════════════════════════════════════════════════════\n" + - "⚠️ DEVELOPMENT MODE: Client IP Detection\n" + - "═══════════════════════════════════════════════════════════════════════\n" + - "No IP forwarding headers found. This affects IP-based security features\n" + - "such as rate limiting, geolocation, and audit logging.\n\n" + - "To test real IP logic in development:\n" + - " 1. Set TEST_CLIENT_IP environment variable (e.g., TEST_CLIENT_IP=203.0.113.45)\n" + - " 2. Or send x-real-ip or cf-connecting-ip headers in your requests\n" + - `\nCurrent fallback: ${testIp || "127.0.0.1"}\n` + - "═══════════════════════════════════════════════════════════════════════\n" + '\n' + + '═══════════════════════════════════════════════════════════════════════\n' + + '⚠️ DEVELOPMENT MODE: Client IP Detection\n' + + '═══════════════════════════════════════════════════════════════════════\n' + + 'No IP forwarding headers found. This affects IP-based security features\n' + + 'such as rate limiting, geolocation, and audit logging.\n\n' + + 'To test real IP logic in development:\n' + + ' 1. Set TEST_CLIENT_IP environment variable (e.g., TEST_CLIENT_IP=203.0.113.45)\n' + + ' 2. Or send x-real-ip or cf-connecting-ip headers in your requests\n' + + `\nCurrent fallback: ${testIp || '127.0.0.1'}\n` + + '═══════════════════════════════════════════════════════════════════════\n' ); } - - return testIp || "127.0.0.1"; + + return testIp || '127.0.0.1'; } // In production, return null to signal that IP extraction failed @@ -172,15 +182,15 @@ function getClientIp(headerList) { if (!hasLoggedProdIpWarning) { hasLoggedProdIpWarning = true; console.warn( - "[getClientIp] No IP forwarding headers found in production. " + - "Ensure reverse proxy is configured to set x-forwarded-for, x-real-ip, or cf-connecting-ip headers. " + - "Request will be rejected if IP is required for security checks." + '[getClientIp] No IP forwarding headers found in production. ' + + 'Ensure reverse proxy is configured to set x-forwarded-for, x-real-ip, or cf-connecting-ip headers. ' + + 'Request will be rejected if IP is required for security checks.' ); } return null; } -export async function POST(request) { +export async function POST(request: NextRequest) { try { // Extract client IP using the priority-ordered header check // This replaces the old TRUST_PROXY-based logic with a more robust approach @@ -208,7 +218,9 @@ export async function POST(request) { const referer = request.headers.get('referer'); if (!origin && !referer) { - console.warn('Analytics request missing both Origin and Referer headers - rejecting non-browser request'); + console.warn( + 'Analytics request missing both Origin and Referer headers - rejecting non-browser request' + ); return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } @@ -222,22 +234,25 @@ export async function POST(request) { // Origin/referer check to prevent cross-origin abuse // Validate against server-side allowlist instead of client-controlled Host header const allowedOrigin = process.env.NEXT_PUBLIC_SITE_URL; - + // Pre-validate and parse the allowed origin configuration - let allowedUrl = null; + let allowedUrl: URL | null = null; if (allowedOrigin) { try { allowedUrl = new URL(allowedOrigin); - } catch (configError) { + } catch (configError: unknown) { // Misconfigured NEXT_PUBLIC_SITE_URL - log and reject - console.error('Invalid NEXT_PUBLIC_SITE_URL configuration:', allowedOrigin, configError.message); + const message = configError instanceof Error ? configError.message : String(configError); + console.error('Invalid NEXT_PUBLIC_SITE_URL configuration:', allowedOrigin, message); return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); } } else if (process.env.NODE_ENV === 'production') { // Missing NEXT_PUBLIC_SITE_URL in production is a server misconfiguration // Guard log to emit only once per process to prevent log flooding if (!hasLoggedMissingUrl) { - console.error('NEXT_PUBLIC_SITE_URL is not configured. Analytics origin validation cannot be performed.'); + console.error( + 'NEXT_PUBLIC_SITE_URL is not configured. Analytics origin validation cannot be performed.' + ); hasLoggedMissingUrl = true; } return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); @@ -248,7 +263,7 @@ export async function POST(request) { // Note: Older browsers may not send these headers; validation is opt-in when present for defense-in-depth const secFetchSite = request.headers.get('sec-fetch-site'); const secFetchMode = request.headers.get('sec-fetch-mode'); - + // If Sec-Fetch headers are present (modern browser), validate them if (secFetchSite !== null) { // Allow same-origin, same-site, and none (direct navigation) @@ -258,7 +273,7 @@ export async function POST(request) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } } - + // If Sec-Fetch-Mode is present, validate it's an appropriate mode for analytics if (secFetchMode !== null) { // Only allow modes typically used for legitimate fetch/navigation requests: @@ -272,35 +287,39 @@ export async function POST(request) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } } - + // Enforce same-origin policy const source = origin || referer; try { - const url = new URL(source); - + const url = new URL(source!); + // In production, validate against NEXT_PUBLIC_SITE_URL // In development, allow localhost and 127.0.0.1 with common dev ports - const isDevelopmentLocal = process.env.NODE_ENV !== 'production' && + const isDevelopmentLocal = + process.env.NODE_ENV !== 'production' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && - (url.port === '3000' || url.port === '3001' || url.port === '' || url.port === '80' || url.port === '443'); - + (url.port === '3000' || + url.port === '3001' || + url.port === '' || + url.port === '80' || + url.port === '443'); + // Compare full origin (protocol + host) to prevent http/https scheme mismatches - const isAllowed = - (allowedUrl && url.origin === allowedUrl.origin) || - isDevelopmentLocal; - + const isAllowed = (allowedUrl && url.origin === allowedUrl.origin) || isDevelopmentLocal; + if (!isAllowed) { console.warn( - 'Analytics request from unexpected origin:', - source, - 'expected:', + 'Analytics request from unexpected origin:', + source, + 'expected:', allowedOrigin || 'localhost:3000/3001' ); return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } - } catch (e) { + } catch (e: unknown) { // Malformed origin/referer from client: treat as suspicious and block - console.warn('Analytics request with invalid origin/referer:', source, 'error:', e.message); + const message = e instanceof Error ? e.message : String(e); + console.warn('Analytics request with invalid origin/referer:', source, 'error:', message); return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } @@ -310,14 +329,20 @@ export async function POST(request) { return NextResponse.json({ error: 'Content-Type must be application/json' }, { status: 400 }); } - let body; + let body: Record; try { body = await request.json(); - } catch (parseError) { + } catch { return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); } - const { eventName, pageLocation, pageTitle, referrer, customParams } = body; + const { eventName, pageLocation, pageTitle, referrer, customParams } = body as { + eventName: unknown; + pageLocation: unknown; + pageTitle: unknown; + referrer: unknown; + customParams: unknown; + }; // Basic validation if (!eventName || typeof eventName !== 'string' || eventName.length > 100) { @@ -338,27 +363,48 @@ export async function POST(request) { if (customParams !== undefined && customParams !== null) { if (typeof customParams !== 'object' || Array.isArray(customParams)) { - return NextResponse.json({ error: 'Invalid customParams: must be a plain object' }, { status: 400 }); + return NextResponse.json( + { error: 'Invalid customParams: must be a plain object' }, + { status: 400 } + ); } - - const keys = Object.keys(customParams); + + const params = customParams as Record; + const keys = Object.keys(params); if (keys.length > 20) { - return NextResponse.json({ error: 'Invalid customParams: too many keys (max 20)' }, { status: 400 }); + return NextResponse.json( + { error: 'Invalid customParams: too many keys (max 20)' }, + { status: 400 } + ); } - + for (const key of keys) { if (typeof key !== 'string' || key.length > 100) { - return NextResponse.json({ error: 'Invalid customParams: key too long' }, { status: 400 }); + return NextResponse.json( + { error: 'Invalid customParams: key too long' }, + { status: 400 } + ); } - - const value = customParams[key]; + + const value = params[key]; const valueType = typeof value; - if (valueType !== 'string' && valueType !== 'number' && valueType !== 'boolean' && value !== null) { - return NextResponse.json({ error: 'Invalid customParams: values must be string/number/boolean/null' }, { status: 400 }); + if ( + valueType !== 'string' && + valueType !== 'number' && + valueType !== 'boolean' && + value !== null + ) { + return NextResponse.json( + { error: 'Invalid customParams: values must be string/number/boolean/null' }, + { status: 400 } + ); } - - if (valueType === 'string' && value.length > 500) { - return NextResponse.json({ error: 'Invalid customParams: string value too long' }, { status: 400 }); + + if (valueType === 'string' && (value as string).length > 500) { + return NextResponse.json( + { error: 'Invalid customParams: string value too long' }, + { status: 400 } + ); } } } @@ -370,7 +416,7 @@ export async function POST(request) { // Extract client context for forwarding to Google Analytics // This enables accurate device/browser/location attribution const userAgent = request.headers.get('user-agent') || undefined; - + // Forward client IP to Google Analytics if it was successfully extracted // The IP was already extracted using getClientIp() which validates headers // and ensures we only forward IPs from trusted proxy headers @@ -378,13 +424,13 @@ export async function POST(request) { // Send event to Google Analytics await sendAnalyticsEvent({ - eventName, - pageLocation, - pageTitle, - referrer, + eventName: eventName as string, + pageLocation: pageLocation as string | undefined, + pageTitle: pageTitle as string | undefined, + referrer: referrer as string | undefined, clientId, sessionId, - customParams, + customParams: customParams as Record | undefined, userAgent, clientIp, }); diff --git a/app/globals.css b/app/globals.css index 10c1045..74eed13 100644 --- a/app/globals.css +++ b/app/globals.css @@ -193,6 +193,7 @@ body { .scroll-snap-section { scroll-margin-top: 0; min-height: 90vh; + contain: layout style; padding-bottom: 2rem; scroll-snap-align: start; scroll-snap-stop: always; @@ -481,6 +482,13 @@ body { } } +/* ========================= */ +/* EXPLORER GRID */ +/* ========================= */ +.explorer-grid { + grid-auto-rows: auto; +} + /* ========================= */ /* TERMINAL PANEL STYLES */ /* ========================= */ @@ -535,9 +543,9 @@ body { } .panel-body { - padding: 32px 20px; + padding: 16px 20px; text-align: center; - min-height: 140px; + min-height: 80px; display: flex; flex-direction: column; align-items: center; @@ -566,6 +574,15 @@ body { /* Staggered reveal animations - handled by JavaScript */ +/* ========================= */ +/* FOOTER EQUATION */ +/* ========================= */ +.footer-eqn { + font-size: clamp(6.5px, 1.8vw, 12px); + white-space: nowrap; + overflow: hidden; +} + /* ========================= */ /* SIGNAL METER STYLES */ /* ========================= */ diff --git a/app/layout.js b/app/layout.js deleted file mode 100644 index 50395d3..0000000 --- a/app/layout.js +++ /dev/null @@ -1,220 +0,0 @@ -import './globals.css'; -import Script from 'next/script'; -import { JetBrains_Mono, Space_Grotesk } from 'next/font/google'; -import { headers } from 'next/headers'; -import { isAnalyticsEnabled } from '@/lib/analytics-config'; -import Analytics from '@/components/Analytics'; -import ErrorHandler from '@/components/ErrorHandler'; - -const jetbrainsMono = JetBrains_Mono({ - subsets: ['latin'], - weight: ['400', '500', '600'], - variable: '--font-jetbrains-mono', - display: 'swap', - preload: true, - fallback: ['monospace'], -}); - -const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], - weight: ['300', '400', '500', '600', '700'], - variable: '--font-space-grotesk', - display: 'swap', - preload: true, - fallback: ['system-ui', 'sans-serif'], -}); - -export const viewport = { - width: 'device-width', - initialScale: 1, - viewportFit: 'cover', - themeColor: '#0A0A0A', - colorScheme: 'dark', -}; - -// Helper function to safely construct URL with validation -function getMetadataBaseUrl() { - const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://devakesu.com'; - try { - return new URL(siteUrl); - } catch (error) { - console.warn( - `Invalid NEXT_PUBLIC_SITE_URL: ${siteUrl}, falling back to default. Reason: ${error instanceof Error ? error.message : String(error)}` - ); - return new URL('https://devakesu.com'); - } -} - -export const metadata = { - metadataBase: getMetadataBaseUrl(), - - // Basic Metadata - title: { - default: '@devakesu - Devanarayanan', - template: '%s | @devakesu', - }, - description: 'Disciplined chaos. Brutalist × cyberpunk portfolio. Where code meets conscience.', - - keywords: [ - // Technical Skills - 'developer', - 'full-stack developer', - 'software engineer', - 'python', - 'typescript', - 'javascript', - 'php', - 'java', - 'kotlin', - 'c', - 'next.js', - 'react', - 'node.js', - 'web development', - 'android development', - 'cloud infrastructure', - 'gcp', - 'aws', - - // Design & Aesthetics - 'brutalist design', - 'cyberpunk', - 'portfolio', - 'ui/ux', - 'human-centered design', - 'ethical design', - - // Core Values & Interests - 'science', - 'justice', - 'social good', - 'ethics-first', - 'open source', - 'systems thinking', - 'empathy', - 'sustainability', - 'environmental protection', - 'lgbtq+', - 'love peace justice', - 'united nations', - 'sdgs', - - // Personal Brand - 'devakesu', - 'devanarayanan', - 'kesu', - 'devakesu.com', - - // Interests - 'technology', - 'nature', - 'research', - 'art', - 'chemistry', - 'construction', - 'astronomy', - 'music', - 'cricket', - 'food', - 'flowers', - 'beauty', - 'love', - 'peace', - 'recycling', - 'inclusivity', - ], - authors: [{ name: 'Devanarayanan (Kesu)', url: 'https://devakesu.com' }], - creator: 'Devanarayanan', - publisher: 'Devanarayanan', - - // Robots & Indexing - robots: { - index: true, - follow: true, - googleBot: { - index: true, - follow: true, - 'max-video-preview': -1, - 'max-image-preview': 'large', - 'max-snippet': -1, - }, - }, - - // OpenGraph (Facebook, LinkedIn, etc.) - openGraph: { - type: 'website', - locale: 'en_US', - url: 'https://devakesu.com', - siteName: 'devakesu', - title: 'devakesu - Devanarayanan', - description: 'Where code meets conscience. Disciplined chaos meets brutalist design.', - images: [ - { - url: '/profile.jpg', - width: 1200, - height: 630, - alt: 'devakesu - Portfolio', - }, - ], - }, - - // Twitter Card - twitter: { - card: 'summary_large_image', - title: 'devakesu - Devanarayanan', - description: 'Where code meets conscience. Disciplined chaos meets brutalist design.', - creator: '@devakesu', - images: ['/profile.jpg'], - }, - - // Additional Meta - applicationName: 'devakesu Portfolio', - generator: 'Next.js', - category: 'technology', - classification: 'Portfolio', - - // Icons & Manifest - icons: { - icon: [{ url: '/favicon.svg', type: 'image/svg+xml' }], - // TODO: Create apple-touch-icon.png (180x180) - iOS requires PNG for home screen icons - // apple: [{ url: '/apple-touch-icon.png', type: 'image/png', sizes: '180x180' }], - }, - - // Verification (add when you set these up) - // verification: { - // google: "your-google-verification-code", - // yandex: "your-yandex-verification-code", - // }, - - // Other - formatDetection: { - telephone: false, - email: false, - address: false, - }, -}; - -export default async function RootLayout({ children }) { - // Note: Reading headers() makes this layout render dynamically (disables static optimization). - // This is an intentional tradeoff for security: per-request nonces in CSP provide strong - // XSS protection. The nonce is required for inline scripts loaded via Next.js Script component. - const headersList = await headers(); - const nonce = headersList.get('x-nonce') || undefined; - - return ( - - - - {isAnalyticsEnabled() && } - {children} -