OWASP Top 10 · Secrets Detection · Dependency Risk

AI Security Code Review

Paste any code diff — get an instant security audit: OWASP vulnerabilities, hardcoded secrets, insecure dependencies. Free, browser-only, BYO Anthropic API key.

Stored in your browser only. Never sent to our servers. Get a free key →

Security Review

How to run a security code review in 3 steps

1
Get your diff

Run git diff HEAD~1 or paste code directly. Works with any git diff or plain code snippet.

2
Choose review type

Pick OWASP Top 10 for web vulns, Secrets for credentials, Deps for library risk, or Full Audit for everything.

3
Act on findings

Copy the security report to share with your team, paste into a PR comment, or add to your security backlog.

AI Security Review vs SonarQube / Semgrep

FeatureAI Security Code ReviewSonarQube / Semgrep
Price~$0.003/review (Anthropic API)$150+/year (SQ Dev) or Semgrep free tier with limits
Understands intent & contextYes — reasons about logic, not just patternsRule-based pattern matching only
Setup requiredNone — paste and goCI pipeline config, rules, baseline setup
Catches logical auth bypassYesOnly if a rule exists for the pattern
OWASP Top 10All 10 categories, contextuallyVaries by rule set and language
Secrets detectionYes — understands context, fewer false positivesLimited (Semgrep Secrets is paid)
Exhaustive / systematicAI may miss some edge cases100% of code paths covered by rules

Security code review — FAQ

What security vulnerabilities does this tool detect?

The OWASP Top 10 mode checks for: injection flaws (SQL, LDAP, OS command), broken authentication, sensitive data exposure, XML external entities (XXE), broken access control, security misconfiguration, XSS (cross-site scripting), insecure deserialization, known vulnerable components, and insufficient logging. The Secrets mode catches hardcoded API keys, passwords, tokens, private keys, and connection strings.

Is my code safe to paste in?

Nothing is stored by us. The diff goes directly from your browser to Anthropic's API using your own key — no backend, no server, no logging. Your code never touches our infrastructure. Anthropic's API does not use inputs for model training by default. For highly sensitive codebases, review Anthropic's data processing agreement before use.

How does this compare to SonarQube or Semgrep?

Static analysis tools (SonarQube, Semgrep) use pattern matching — exhaustive but context-blind. This AI tool reasons about intent: it catches logical vulnerabilities, insecure defaults, and subtle auth bypass patterns that rules miss. Best practice: use both. Static analysis for systematic rule coverage, AI review for reasoning about what the code is actually trying to do.

What should I paste — the whole file or just the diff?

Paste the git diff when possible. Diffs give the AI the crucial context of what changed (the highest-risk code), they're smaller, and they cost less to analyze. For a new file or code without a git history, paste the file contents directly — the tool handles both.

How much does a security review cost?

A typical diff (200–500 lines) costs about $0.001–$0.005 with Claude Haiku, or $0.005–$0.02 with Claude Sonnet. You pay Anthropic directly at pay-per-token rates; there's no subscription or markup. Compare to SonarQube Developer Edition ($150+/year) or Snyk Team ($57+/month/developer).

Can this replace a manual security code review?

No — but it dramatically shortens one. Use this to get a first-pass list of potential issues before human reviewers look at the code. Security-critical systems should always have expert human review; AI is best at surfacing issues for humans to evaluate, not replacing their judgment.

This executes in every viewer's browser, enabling session hijacking, credential theft, and further attacks. RISK: High. This is a stored XSS — the payload persists in your database and executes for every user who views the comments. REMEDIATION (pick one): 1. Render as text instead:
{comment.body}
— safest if markdown/HTML not needed 2. Sanitize before rendering: use DOMPurify.sanitize(comment.body) before passing to dangerouslySetInnerHTML 3. Use a safe markdown renderer (react-markdown with no HTML passthrough) --- ✅ No other XSS or injection issues found in this diff.`, deps: `🟡 MEDIUM — Dependency Version Downgrade File: package.json FINDING: - "jsonwebtoken": "^9.0.0" + "jsonwebtoken": "^8.5.1" Downgrading from v9 to v8. jsonwebtoken v8.x has known CVEs: - CVE-2022-23529 (CVSS 7.6): Remote code execution via malicious public key - CVE-2022-23540 (CVSS 6.4): Insecure default algorithm allows alg:none bypass RECOMMENDATION: Stay on v9.x. If v8 is required for compatibility, explicitly set the 'algorithms' option to reject 'none' and pin to the latest patch of v8. --- 🟢 LOW — New dependency added without lockfile update File: package.json (+) + "axios": "^1.6.0" Axios 1.6.x is generally safe. No critical CVEs. Ensure package-lock.json is committed and up to date so all environments resolve the same version. --- ✅ No other dependency risk issues in this diff.` }, secrets: { sqli: `🔴 CRITICAL — Live Stripe Secret Key Detected File: config/services.js, line 12 (+) KEY: sk_live_4xN8mKpQ2rHjLvWsT9uYcAeD TYPE: Stripe Live Secret Key (pattern: sk_live_*) RISK: Anyone with repo access can make charges, refunds, and access customer data using this key. If the repository is public or becomes public, this is permanently exposed in git history. IMMEDIATE ACTION: 1. Revoke at: https://dashboard.stripe.com/apikeys 2. Generate a new key 3. Set as environment variable: STRIPE_SECRET_KEY=sk_live_... 4. Reference in code: process.env.STRIPE_SECRET_KEY --- 🟠 HIGH — Database Password in Config File: config/database.yml, line 7 (+) + password: "P@ssw0rd!Prod2024" Plaintext password committed to source control. Even if removed in a follow-up commit, it persists in git history forever. REMEDIATION: - Rotate the password immediately - Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) - Reference via environment variable: password: <%= ENV['DB_PASSWORD'] %> --- ✅ No other credentials or secrets found in this diff.`, secrets: `🔴 CRITICAL — Multiple Secrets Detected File: src/config.js FINDING 1: AWS Access Key + AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE' + AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' FINDING 2: GitHub Personal Access Token + GITHUB_TOKEN = 'ghp_16C7e42F292c6912E7710c838347Ae178B4a' FINDING 3: Slack Webhook URL + SLACK_WEBHOOK = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' ACTIONS REQUIRED: 1. Revoke all three credentials immediately 2. Check AWS CloudTrail for unauthorized API calls in the last 24h 3. Check GitHub audit log for unexpected actions with that token 4. Move all secrets to environment variables or a secrets manager 5. Add .env to .gitignore and use .env.example with placeholder values --- ✅ No other secrets detected.`, xss: `✅ No hardcoded secrets or credentials found in this diff. Scan summary: - API keys: none detected - Passwords: none detected - Connection strings: none detected - Private keys (RSA/EC/SSH): none detected - Tokens (JWT, OAuth, webhook): none detected Note: This diff modifies UI rendering logic. No secrets are expected here. For XSS issues in this diff, use the "OWASP Top 10 Scan" mode.`, deps: `🟡 MEDIUM — NPM Auth Token in .npmrc File: .npmrc (+) + //registry.npmjs.org/:_authToken=npm_Ab3Cd4EfGhIjKlMnOpQr... npm auth tokens committed to .npmrc grant publish access to your packages. If this is a personal token, it can be used to publish malicious versions of your packages. ACTIONS: 1. Revoke the token at npmjs.com/settings/tokens 2. Generate a new token 3. Add .npmrc to .gitignore 4. Use CI environment variables for the token: NPM_TOKEN=${{ secrets.NPM_TOKEN }} --- ✅ No other secrets found in package.json or lock files.` }, deps: { sqli: `✅ No dependency changes detected in this diff. This diff appears to modify application logic (SQL query handling), not package dependencies. For SQL injection issues in this diff, use "OWASP Top 10 Scan" mode.`, secrets: `✅ No dependency changes in this diff. This diff modifies configuration files. No package.json, go.mod, requirements.txt, Gemfile, or Cargo.toml changes detected.`, xss: `🟢 INFO — React version in range, no CVEs package.json shows no change to react or react-dom versions. The XSS risk in this diff comes from application code (dangerouslySetInnerHTML), not the React library itself. Recommendation: Use "OWASP Top 10 Scan" mode to analyze the application-level XSS in this diff.`, deps: `🟠 HIGH — Vulnerable Dependency Introduced package.json changes: ADDED: + "lodash": "4.17.15" — CVE-2021-23337 (CVSS 7.2) Command injection via template + "minimist": "0.0.8" — CVE-2020-7598 (CVSS 5.6) Prototype pollution DOWNGRADED: - "jsonwebtoken": "9.0.2" + "jsonwebtoken": "8.5.1" — CVE-2022-23529 (CVSS 7.6) RCE via malicious public key SAFE ADDITIONS: + "zod": "3.22.4" — No known CVEs, actively maintained ✅ + "date-fns": "3.6.0" — No known CVEs ✅ RECOMMENDATIONS: 1. lodash: upgrade to 4.17.21 (patched) 2. minimist: upgrade to 1.2.8 (patched) 3. jsonwebtoken: revert to 9.x or pin explicit algorithm list in v8` }, full: { sqli: `## Security Audit Report ### Executive Summary This diff introduces a high-severity SQL injection vulnerability and lacks rate limiting on the login endpoint. Immediate remediation required before deploy. --- ### 🔴 CRITICAL — SQL Injection (OWASP A03:2021) **File:** src/auth/login.js, line 47 **Severity:** Critical (CVSS ~9.8) Raw string interpolation of user input into a SQL query allows authentication bypass and potential data exfiltration. An attacker can craft email/password inputs to return arbitrary rows or execute arbitrary SQL. **Fix:** Use parameterized queries or an ORM. --- ### 🟡 MEDIUM — No Rate Limiting (OWASP A07:2021) **Severity:** Medium The login endpoint accepts unlimited attempts. Enables brute-force attacks against any account. **Fix:** Add express-rate-limit (15 req/15min on login), or account lockout after 5 failures. --- ### 🟢 PASSED CHECKS - ✅ No hardcoded credentials in diff - ✅ No XSS sinks introduced - ✅ No new dependencies with known CVEs - ✅ No sensitive data logged (no console.log of req.body) - ✅ Password comparison uses constant-time function (bcrypt.compare) --- ### Recommended Actions (Priority Order) 1. 🔴 Fix SQL injection before next deploy 2. 🟡 Add rate limiting before this endpoint goes to production 3. 🟢 Consider adding integration test for SQLi (e.g. sqlmap or manual ' OR 1=1 --)`, secrets: `## Security Audit Report ### Executive Summary Two critical secrets exposed in this diff — live Stripe key and database password. Immediate revocation required. --- ### 🔴 CRITICAL — Live API Key Hardcoded **Severity:** Critical Stripe live secret key committed to source control. Revoke immediately. ### 🔴 CRITICAL — Database Password Exposed **Severity:** Critical Production database password in plaintext. Rotate immediately. --- ### 🟢 PASSED CHECKS - ✅ No XSS vulnerabilities in this diff - ✅ No SQL injection patterns - ✅ No insecure dependency changes - ✅ No private keys or certificates --- ### Recommended Actions 1. Revoke Stripe key + rotate DB password NOW 2. Audit git history for other committed secrets (git-secrets, truffleHog) 3. Set up pre-commit hooks to block secrets: git-secrets, detect-secrets, or gitleaks 4. Move all credentials to environment variables + a secrets manager`, xss: `## Security Audit Report ### Executive Summary One high-severity stored XSS vulnerability introduced. No other critical issues found. --- ### 🟠 HIGH — Stored XSS via dangerouslySetInnerHTML **File:** src/components/CommentList.jsx **Severity:** High (CVSS ~8.2) User-controlled content rendered as raw HTML without sanitization. Attacker-submitted malicious comments execute as JavaScript in all viewers' browsers. **Fix:** Use DOMPurify.sanitize() or render as plain text. --- ### 🟢 PASSED CHECKS - ✅ No SQL injection in this diff - ✅ No hardcoded secrets - ✅ No dependency CVEs - ✅ No insecure direct object references - ✅ No CSRF issues introduced --- ### Recommended Actions 1. Fix XSS before deploy — stored XSS is exploitable by any user who can post a comment 2. Add Content-Security-Policy header to limit impact of future XSS: script-src 'self' 3. Consider a sanitization middleware layer that strips HTML from all user text inputs at the API level`, deps: `## Security Audit Report ### Executive Summary Three vulnerable dependencies introduced, including one high-severity RCE. Safe additions are fine. --- ### 🟠 HIGH — Vulnerable Dependency: jsonwebtoken 8.x **CVE-2022-23529:** CVSS 7.6 — Remote code execution via malicious public key **CVE-2022-23540:** CVSS 6.4 — Algorithm confusion (alg:none bypass) Stay on v9.x or add explicit algorithm whitelisting if v8 is required. ### 🟡 MEDIUM — Vulnerable Dependencies: lodash 4.17.15, minimist 0.0.8 Both have known prototype pollution / injection CVEs. Upgrade to patched versions. --- ### 🟢 PASSED CHECKS - ✅ No secrets or credentials in package.json - ✅ zod 3.22.4 — safe ✅ - ✅ date-fns 3.6.0 — safe ✅ - ✅ No deprecated or abandoned packages added --- ### Recommended Actions 1. Upgrade jsonwebtoken to ^9.0.0 2. Upgrade lodash to ^4.17.21 3. Upgrade minimist to ^1.2.8 4. Add npm audit to your CI pipeline to catch future CVEs at PR time` } }; const EXAMPLE_DIFFS = { sqli: `diff --git a/src/auth/login.js b/src/auth/login.js index 3a2b1c4..9f8e2d1 100644 --- a/src/auth/login.js +++ b/src/auth/login.js @@ -44,8 +44,10 @@ router.post('/login', async (req, res) => { const { email, password } = req.body; - const user = await db.query( - 'SELECT * FROM users WHERE email = ? AND password = ?', - [email, bcrypt.hash(password, 10)] - ); + // simplified for debugging + const q = \`SELECT * FROM users WHERE email = '\${email}' AND password = '\${password}'\`; + const user = await db.query(q); if (!user.length) { return res.status(401).json({ error: 'Invalid credentials' }); }`, secrets: `diff --git a/config/services.js b/config/services.js index 1b2a3c4..5d6e7f8 100644 --- a/config/services.js +++ b/config/services.js @@ -8,6 +8,10 @@ module.exports = { database: { host: process.env.DB_HOST, + password: "P@ssw0rd!Prod2024", }, + stripe: { + secretKey: 'sk_live_4xN8mKpQ2rHjLvWsT9uYcAeD', + webhookSecret: 'whsec_abcdef123456', + }, };`, xss: `diff --git a/src/components/CommentList.jsx b/src/components/CommentList.jsx index 2c3d4e5..6f7a8b9 100644 --- a/src/components/CommentList.jsx +++ b/src/components/CommentList.jsx @@ -19,8 +19,9 @@ function CommentCard({ comment }) { return (
{comment.author} -

{comment.body}

+ {/* Allow formatted comments */} +
{comment.createdAt}
);`, deps: `diff --git a/package.json b/package.json index 4e5f6a7..8b9c0d1 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,13 @@ "dependencies": { - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^8.5.1", + "lodash": "4.17.15", + "minimist": "0.0.8", + "zod": "3.22.4", + "date-fns": "3.6.0", "express": "^4.18.2", "bcrypt": "^5.1.1" }` }; const SYSTEM_PROMPTS = { owasp: `You are a senior application security engineer performing a code security review focused on the OWASP Top 10 (2021 edition). Analyze the provided code diff for: A01 - Broken Access Control A02 - Cryptographic Failures A03 - Injection (SQL, LDAP, OS command, NoSQL, SSTI) A04 - Insecure Design A05 - Security Misconfiguration A06 - Vulnerable and Outdated Components A07 - Identification and Authentication Failures A08 - Software and Data Integrity Failures A09 - Security Logging and Monitoring Failures A10 - Server-Side Request Forgery (SSRF) For each finding: - Label severity: 🔴 CRITICAL / 🟠 HIGH / 🟡 MEDIUM / 🟢 LOW / ✅ PASSED - State the specific OWASP category - Identify the exact file and line number - Explain the risk concisely (what an attacker could do) - Give a concrete remediation code snippet or clear fix End with a brief summary of passed checks. Be direct and actionable — no fluff.`, secrets: `You are a secrets detection security engineer. Analyze the provided code diff for hardcoded credentials, secrets, and sensitive data including: - API keys (AWS, GCP, Azure, Stripe, Twilio, SendGrid, GitHub PATs, etc.) - Database passwords and connection strings - Private keys (RSA, EC, SSH, PEM) - JWT secrets and signing keys - OAuth client secrets - Webhook secrets and signing secrets - Internal service tokens and bearer tokens - SMTP credentials For each finding: - Label severity: 🔴 CRITICAL (live/prod keys) / 🟠 HIGH (service access) / 🟡 MEDIUM (test keys or low-privilege) - Identify the exact file and line number - State what type of credential it is and what it provides access to - Give immediate remediation steps (revoke, rotate, use env vars) If no secrets are found, confirm the clean scan with what you checked. Be precise — no false positives.`, deps: `You are a supply chain security engineer reviewing dependency changes in a code diff. Analyze any changes to package.json, go.mod, requirements.txt, Gemfile, Cargo.toml, pom.xml, or other dependency files for: - CVEs in newly added or updated packages (state CVE ID, CVSS score, description) - Version downgrades that reintroduce known vulnerabilities - Abandoned or deprecated packages (no commits in 2+ years, archived repos) - Packages with known malicious versions or supply chain incidents - Overly broad version ranges that could pull in future vulnerable versions - Development dependencies inadvertently included in production For each finding: severity label, package name, specific CVE or risk, and recommended version or alternative. For safe additions, briefly confirm they are clean. If no dependency changes are found, say so clearly.`, full: `You are a staff security engineer performing a comprehensive security audit of a code diff. Review for ALL of the following: 1. OWASP Top 10 vulnerabilities (injection, XSS, auth issues, access control, etc.) 2. Hardcoded secrets and credentials 3. Vulnerable or risky dependency changes 4. Insecure cryptography (weak algorithms, hardcoded keys, improper IV/salt usage) 5. Insecure data handling (logging sensitive data, insecure serialization) 6. Race conditions or TOCTOU issues 7. Input validation gaps at API boundaries 8. Missing security headers or CORS misconfigurations Structure your output as a security report with: - Executive Summary (1 short paragraph) - Findings (each with severity emoji, category, file:line, risk, fix) - Passed Checks (bullet list of things that look good) - Recommended Actions (prioritized numbered list) Use 🔴 CRITICAL / 🟠 HIGH / 🟡 MEDIUM / 🟢 LOW / ✅ PASSED labels. Be direct and specific.` }; const MODE_LABELS = { owasp: 'OWASP Top 10 Scan', secrets: 'Secrets & Credentials Scan', deps: 'Dependency Risk Review', full: 'Full Security Audit' }; let currentMode = 'owasp'; let currentExample = 'sqli'; // Load persisted API key const apiKeyInput = document.getElementById('apiKey'); const savedKey = localStorage.getItem('anthropic_api_key'); if (savedKey) apiKeyInput.value = savedKey; apiKeyInput.addEventListener('input', () => { localStorage.setItem('anthropic_api_key', apiKeyInput.value.trim()); }); // Mode tabs document.querySelectorAll('.mode-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.mode-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); }); tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); currentMode = tab.dataset.mode; document.getElementById('generateBtn').textContent = ''; document.getElementById('generateBtn').appendChild(Object.assign(document.createTextNode('Run Security Review '), Object.assign(document.createElement('span'), {className:'shortcut', textContent:'Ctrl+↵'}))); }); }); // Example buttons document.querySelectorAll('.example-btn').forEach(btn => { btn.addEventListener('click', () => { currentExample = btn.dataset.example; document.getElementById('diffInput').value = EXAMPLE_DIFFS[currentExample]; updateStats(EXAMPLE_DIFFS[currentExample]); }); }); // Diff stats const diffInput = document.getElementById('diffInput'); diffInput.addEventListener('input', () => updateStats(diffInput.value)); function updateStats(text) { const adds = (text.match(/^\+[^+]/gm) || []).length; const dels = (text.match(/^-[^-]/gm) || []).length; const files = (text.match(/^diff --git/gm) || []).length; const statsEl = document.getElementById('diffStats'); if (adds || dels || files) { statsEl.style.display = 'flex'; document.getElementById('statAdd').textContent = `+${adds} additions`; document.getElementById('statDel').textContent = `-${dels} deletions`; document.getElementById('statFiles').textContent = files ? `${files} file${files !== 1 ? 's' : ''}` : ''; } else { statsEl.style.display = 'none'; } } // Share permalink (base64url encode the diff) function getShareUrl() { const diff = diffInput.value; const encoded = btoa(unescape(encodeURIComponent(diff))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,''); return `${location.origin}${location.pathname}?d=${encoded}&m=${currentMode}`; } // Load from URL on page load (function loadFromUrl() { const params = new URLSearchParams(location.search); const d = params.get('d'); const m = params.get('m'); if (d) { try { const decoded = decodeURIComponent(escape(atob(d.replace(/-/g,'+').replace(/_/g,'/')))); diffInput.value = decoded; updateStats(decoded); } catch(e) {} } if (m && SYSTEM_PROMPTS[m]) { currentMode = m; document.querySelectorAll('.mode-tab').forEach(t => { const isActive = t.dataset.mode === m; t.classList.toggle('active', isActive); t.setAttribute('aria-selected', isActive ? 'true' : 'false'); }); } })(); async function generate(isDemo) { const diff = diffInput.value.trim(); if (!diff) { diffInput.focus(); return; } const btn = document.getElementById('generateBtn'); const outputCard = document.getElementById('outputCard'); const outputText = document.getElementById('outputText'); const outputLabel = document.getElementById('outputLabel'); const demoBadge = document.getElementById('demoBadge'); if (isDemo) { const demoData = DEMO_SUMMARIES[currentMode]; // pick example key from current diff content let key = currentExample; if (!demoData[key]) key = Object.keys(demoData)[0]; outputText.textContent = demoData[key]; outputLabel.childNodes[0].textContent = MODE_LABELS[currentMode] + ' '; demoBadge.style.display = 'inline-block'; outputCard.classList.add('visible'); outputCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); return; } const apiKey = apiKeyInput.value.trim(); if (!apiKey) { alert('Please enter your Anthropic API key, or click "Try Demo" to see an example output.'); return; } btn.disabled = true; btn.innerHTML = 'Scanning…'; demoBadge.style.display = 'none'; outputCard.classList.remove('visible'); try { const resp = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json', 'anthropic-dangerous-direct-browser-access': 'true' }, body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 1024, system: SYSTEM_PROMPTS[currentMode], messages: [{ role: 'user', content: `Please perform a security review of the following code diff:\n\n${diff}` }] }) }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.error?.message || `API error ${resp.status}`); } const data = await resp.json(); const text = data.content?.[0]?.text || ''; outputText.textContent = text; outputLabel.childNodes[0].textContent = MODE_LABELS[currentMode] + ' '; outputCard.classList.add('visible'); outputCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(e) { alert('Error: ' + e.message); } finally { btn.disabled = false; btn.innerHTML = 'Run Security Review Ctrl+↵'; } } document.getElementById('generateBtn').addEventListener('click', () => generate(false)); document.getElementById('demoBtn').addEventListener('click', () => generate(true)); document.getElementById('clearBtn').addEventListener('click', () => { diffInput.value = ''; document.getElementById('diffStats').style.display = 'none'; document.getElementById('outputCard').classList.remove('visible'); }); document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); generate(false); } }); // Copy buttons document.getElementById('copyBtn').addEventListener('click', async function() { const text = document.getElementById('outputText').textContent; await navigator.clipboard.writeText(text); this.textContent = 'Copied!'; this.classList.add('copied'); setTimeout(() => { this.textContent = 'Copy to clipboard'; this.classList.remove('copied'); }, 2000); }); document.getElementById('shareBtn').addEventListener('click', async function() { await navigator.clipboard.writeText(getShareUrl()); this.textContent = 'Link copied!'; this.classList.add('copied'); setTimeout(() => { this.textContent = 'Copy share link'; this.classList.remove('copied'); }, 2000); }); })();