From c2b8b2a33567e853662a4e4b9a3d3aac8f8427f9 Mon Sep 17 00:00:00 2001 From: stewroux Date: Fri, 5 Jun 2026 07:18:41 +0900 Subject: [PATCH] fix: security hardening and code quality improvements (daily review 2026-06-04) - Add .env and .env.* to .gitignore to prevent accidental API key commits (closes #2) - Add .env.example with documented required env vars - Add 200MB file size limit in handleImport to prevent memory exhaustion (closes #2) - Add 5-minute duration limit for Gemini AI analysis to prevent oversized payloads - Fix empty catch block in handleStop with explanatory comment (closes #3) - Remove console.warn/console.error debug logs from production code (closes #3) - Add GitHub Actions CI workflow (build on push/PR) - Add Claude Code SessionStart hook for consistent dev environment setup" --- .claude/hooks/session-start.sh | 18 ++++++++++++++++++ .claude/settings.json | 15 +++++++++++++++ .env.example | 4 ++++ .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ .gitignore | 5 +++++ App.tsx | 23 ++++++++++++++++++----- 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100644 index 0000000..12d3928 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +echo "=== AudioSpec_Analyzer Session Start ===" +echo "Node: $(node --version)" +echo "npm: $(npm --version)" + +if [ -f "package.json" ]; then + echo "Installing dependencies..." + npm install --silent + echo "Dependencies ready." +fi + +if [ ! -f ".env" ]; then + echo "WARNING: .env not found. Copy .env.example to .env and set GEMINI_API_KEY." +fi + +echo "=== Ready ===" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..2515ac7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e9e2c63 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Gemini API Key (required for AI Transcribe / Summarize features) +# Get your key at: https://aistudio.google.com/app/apikey +# SECURITY: Never commit the real key. This file is safe to commit. +GEMINI_API_KEY=your_gemini_api_key_here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c352112 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: + - main + - 'claude/**' + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build + env: + GEMINI_API_KEY: placeholder_for_ci_build diff --git a/.gitignore b/.gitignore index a547bf3..a6e6d88 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Environment variables — never commit secrets +.env +.env.* +!.env.example + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/App.tsx b/App.tsx index bec41f2..d02a3d7 100644 --- a/App.tsx +++ b/App.tsx @@ -8,6 +8,9 @@ import { TrackItem } from './components/TrackItem'; import { SpectrumAnalyzer } from './components/SpectrumAnalyzer'; import { Play, Pause, Square, Mic, Upload, Download, Sparkles, AlertCircle, Globe, Plus, Cpu, Ruler, ZoomIn, ZoomOut, MoveHorizontal, ChevronDown } from 'lucide-react'; +const MAX_FILE_SIZE_BYTES = 200 * 1024 * 1024; // 200 MB +const MAX_AI_DURATION_SEC = 300; // 5 minutes + export default function App() { const [lang, setLang] = useState('ja'); const t = translations[lang]; @@ -101,6 +104,12 @@ export default function App() { const file = e.target.files?.[0]; if (!file) return; + if (file.size > MAX_FILE_SIZE_BYTES) { + setErrorMessage('File size exceeds 200MB limit. Please use a smaller file.'); + e.target.value = ''; + return; + } + setIsProcessing(true); try { initAudioContext(); @@ -116,7 +125,7 @@ export default function App() { if (lowerName.endsWith('.wav')) { detectedWavInfo = parseWavHeader(arrayBuffer); } else if (lowerName.match(/\.(m4a|mp4|aac)$/)) { - try { detectedM4aInfo = parseM4aHeader(arrayBuffer); } catch (e) { console.warn(e); } + try { detectedM4aInfo = parseM4aHeader(arrayBuffer); } catch { /* ignore optional metadata parse errors */ } m4aCodecInfo = detectM4aCodec(arrayBuffer); } @@ -170,7 +179,6 @@ export default function App() { }); } catch (err) { - console.error(err); setErrorMessage(t.decodeError); } finally { setIsProcessing(false); @@ -306,7 +314,7 @@ export default function App() { const handleStop = () => { sourceNodesRef.current.forEach(node => { - try { node.stop(); } catch(e) {} + try { node.stop(); } catch { /* already stopped or not yet started */ } }); sourceNodesRef.current = []; if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); @@ -333,6 +341,12 @@ export default function App() { const handleAiAction = async (task: 'transcribe' | 'summarize') => { if (!audioContextRef.current || tracks.length === 0) return; + + if (editorState.duration > MAX_AI_DURATION_SEC) { + setErrorMessage('Audio too long for AI analysis. Maximum is 5 minutes (300s).'); + return; + } + setIsProcessing(true); setAiResult(null); @@ -345,7 +359,6 @@ export default function App() { summary: task === 'summarize' ? text : undefined }); } catch (err) { - console.error(err); setErrorMessage("AI Analysis Failed. Check API Key or Audio length."); } finally { setIsProcessing(false); @@ -605,4 +618,4 @@ export default function App() { ); -} \ No newline at end of file +}