From 739e6f7f17c879eed58f7b2d54f8e3cfea9150a5 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sun, 20 Sep 2026 20:19:37 +0200 Subject: [PATCH] Rewrite Next, ESLint, PostCSS, and tsconfig overlay. CSP, login/register redirects, pdfjs-dist externals, and canvas alias stay the same. package.json is unchanged. --- periscope/src/frontend/eslint.config.mjs | 9 +++ periscope/src/frontend/next.config.ts | 71 +++++++++++++++++++ periscope/src/frontend/postcss.config.mjs | 5 ++ periscope/src/frontend/tsconfig.json | 30 ++++++++ ..._periscope_frontend_next_config_rewrite.py | 40 +++++++++++ 5 files changed, 155 insertions(+) create mode 100644 periscope/src/frontend/eslint.config.mjs create mode 100644 periscope/src/frontend/next.config.ts create mode 100644 periscope/src/frontend/postcss.config.mjs create mode 100644 periscope/src/frontend/tsconfig.json create mode 100644 tests/test_periscope_frontend_next_config_rewrite.py diff --git a/periscope/src/frontend/eslint.config.mjs b/periscope/src/frontend/eslint.config.mjs new file mode 100644 index 0000000..b3a4cb9 --- /dev/null +++ b/periscope/src/frontend/eslint.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +export default defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), +]); diff --git a/periscope/src/frontend/next.config.ts b/periscope/src/frontend/next.config.ts new file mode 100644 index 0000000..7d03f63 --- /dev/null +++ b/periscope/src/frontend/next.config.ts @@ -0,0 +1,71 @@ +import type { NextConfig } from "next"; +import { + CSP_CONNECT_HOSTS, + CSP_FRAME_HOSTS, + CSP_SCRIPT_HOSTS, +} from "./src/lib/csp-hosts"; + +function spaceJoin(hosts: string[]): string { + return hosts.length > 0 ? ` ${hosts.join(" ")}` : ""; +} + +function publicApiOrigins(): string[] { + const raw = process.env.NEXT_PUBLIC_API_URL; + if (!raw) return []; + try { + return [new URL(raw).origin]; + } catch { + return []; + } +} + +function contentSecurityPolicy(): string { + const connect = [...CSP_CONNECT_HOSTS, ...publicApiOrigins()]; + return [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://vercel.live" + + spaceJoin(CSP_SCRIPT_HOSTS), + "style-src 'self' 'unsafe-inline' https://vercel.live https://fonts.googleapis.com", + "img-src 'self' data: https: blob:", + "font-src 'self' data: https://vercel.live https://assets.vercel.com https://fonts.gstatic.com", + "connect-src 'self' blob: https://storage.googleapis.com https://vercel.live wss://ws-us3.pusher.com" + + spaceJoin(connect), + "frame-src 'self' https://vercel.live" + spaceJoin(CSP_FRAME_HOSTS), + "worker-src 'self' blob:", + ].join("; "); +} + +const nextConfig: NextConfig = { + allowedDevOrigins: ["127.0.0.1", "localhost"], + serverExternalPackages: ["pdfjs-dist"], + turbopack: { + resolveAlias: { + canvas: { browser: "" }, + }, + }, + async redirects() { + return [ + { source: "/login", destination: "/sign-in", permanent: false }, + { source: "/register", destination: "/sign-up", permanent: false }, + ]; + }, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Strict-Transport-Security", + value: "max-age=31536000; includeSubDomains", + }, + { key: "Content-Security-Policy", value: contentSecurityPolicy() }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/periscope/src/frontend/postcss.config.mjs b/periscope/src/frontend/postcss.config.mjs new file mode 100644 index 0000000..c2ddf74 --- /dev/null +++ b/periscope/src/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/periscope/src/frontend/tsconfig.json b/periscope/src/frontend/tsconfig.json new file mode 100644 index 0000000..c43cccc --- /dev/null +++ b/periscope/src/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/tests/test_periscope_frontend_next_config_rewrite.py b/tests/test_periscope_frontend_next_config_rewrite.py new file mode 100644 index 0000000..7c1f3b7 --- /dev/null +++ b/tests/test_periscope_frontend_next_config_rewrite.py @@ -0,0 +1,40 @@ +"""Frontend Next/ESLint/PostCSS overlay lives under periscope/src.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "periscope" / "src" / "frontend" + + +def test_next_and_eslint_are_src(): + for rel in ("next.config.ts", "eslint.config.mjs", "postcss.config.mjs", "tsconfig.json"): + path = SRC / rel + assert path.is_file(), rel + assert "Native Periscope overlay" not in path.read_text(encoding="utf-8")[:400] + + +def test_next_config_keeps_runtime_contract(): + text = (SRC / "next.config.ts").read_text(encoding="utf-8") + assert 'from "./src/lib/csp-hosts"' in text + assert 'allowedDevOrigins: ["127.0.0.1", "localhost"]' in text + assert 'serverExternalPackages: ["pdfjs-dist"]' in text + assert 'canvas: { browser: "" }' in text + assert 'source: "/login"' in text + assert 'destination: "/sign-in"' in text + assert 'source: "/register"' in text + assert 'destination: "/sign-up"' in text + assert "X-Content-Type-Options" in text + assert "X-Frame-Options" in text + assert "Strict-Transport-Security" in text + assert "Content-Security-Policy" in text + assert "worker-src 'self' blob:" in text + assert "pdfjs-dist" in text + + +def test_eslint_uses_next_core_web_vitals(): + text = (SRC / "eslint.config.mjs").read_text(encoding="utf-8") + assert "eslint-config-next/core-web-vitals" in text + assert "eslint-config-next/typescript" in text + assert ".next/**" in text