SnapAPI v2.0: Screenshot and document conversion API
This commit is contained in:
commit
0609754ab3
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
api-keys.json
|
||||||
|
analytics.json
|
||||||
|
*.log
|
||||||
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# SnapAPI v2.0
|
||||||
|
|
||||||
|
Screenshot, PDF, and Markdown conversion API powered by Puppeteer + Chrome.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- URL screenshot capture (PNG, JPEG)
|
||||||
|
- PDF generation from URLs
|
||||||
|
- Markdown to PDF/PNG/HTML conversion
|
||||||
|
- API key authentication
|
||||||
|
- Rate limiting and concurrency control
|
||||||
|
- SSRF protection
|
||||||
568
landing.html
Normal file
568
landing.html
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>SnapAPI - Screenshots, PDFs & Markdown Conversion</title>
|
||||||
|
<meta name="description" content="Capture screenshots, generate PDFs, and convert Markdown to beautiful documents via a simple REST API.">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
<meta property="og:title" content="SnapAPI - Screenshots, PDFs & Markdown Conversion">
|
||||||
|
<meta property="og:description" content="Capture screenshots, generate PDFs, and convert Markdown to beautiful documents via a simple REST API.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:locale" content="en_US">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="SnapAPI - Screenshots, PDFs & Markdown Conversion">
|
||||||
|
<meta name="twitter:description" content="Capture screenshots, generate PDFs, and convert Markdown to beautiful documents via a simple REST API.">
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e4e4e7;--muted:#71717a;--accent:#6366f1;--accent2:#818cf8;--green:#22c55e;--red:#ef4444;--orange:#f59e0b}
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased}
|
||||||
|
a{color:var(--accent2);text-decoration:none}
|
||||||
|
a:hover{text-decoration:underline}
|
||||||
|
|
||||||
|
.container{max-width:900px;margin:0 auto;padding:0 24px}
|
||||||
|
|
||||||
|
header{padding:32px 0 0;border-bottom:1px solid var(--border)}
|
||||||
|
.hero{text-align:center;padding:48px 0 40px}
|
||||||
|
.hero h1{font-size:2.5rem;font-weight:700;letter-spacing:-0.03em;margin-bottom:8px}
|
||||||
|
.hero h1 span{color:var(--accent)}
|
||||||
|
.hero p{font-size:1.15rem;color:var(--muted);max-width:560px;margin:0 auto 24px}
|
||||||
|
.badge{display:inline-block;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:4px 14px;font-size:0.8rem;color:var(--muted);margin:0 4px}
|
||||||
|
.badge.green{border-color:#22c55e33;color:var(--green)}
|
||||||
|
|
||||||
|
nav{display:flex;justify-content:center;gap:8px;padding-bottom:0;flex-wrap:wrap}
|
||||||
|
nav a{padding:8px 16px;border-radius:6px 6px 0 0;font-size:0.9rem;color:var(--muted);border:1px solid transparent;border-bottom:none;transition:all 0.2s}
|
||||||
|
nav a:hover{color:var(--text);text-decoration:none;background:var(--surface)}
|
||||||
|
nav a.active{color:var(--text);background:var(--surface);border-color:var(--border)}
|
||||||
|
|
||||||
|
.tab-content{display:none;padding:32px 0 48px}
|
||||||
|
.tab-content.active{display:block}
|
||||||
|
|
||||||
|
section{margin-bottom:40px}
|
||||||
|
section h2{font-size:1.4rem;font-weight:600;margin-bottom:16px;letter-spacing:-0.02em}
|
||||||
|
section h3{font-size:1.1rem;font-weight:600;margin-bottom:12px;color:var(--accent2)}
|
||||||
|
|
||||||
|
pre{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px 20px;overflow-x:auto;font-family:'SF Mono',Consolas,monospace;font-size:0.85rem;line-height:1.7;margin-bottom:16px}
|
||||||
|
code{font-family:'SF Mono',Consolas,monospace;font-size:0.85rem}
|
||||||
|
.inline-code{background:var(--surface);padding:2px 6px;border-radius:4px;border:1px solid var(--border)}
|
||||||
|
|
||||||
|
table{width:100%;border-collapse:collapse;margin-bottom:16px;font-size:0.9rem}
|
||||||
|
th{text-align:left;padding:10px 12px;border-bottom:2px solid var(--border);font-weight:600;color:var(--muted);font-size:0.8rem;text-transform:uppercase;letter-spacing:0.05em}
|
||||||
|
td{padding:10px 12px;border-bottom:1px solid var(--border)}
|
||||||
|
td code{color:var(--accent2)}
|
||||||
|
|
||||||
|
.method{display:inline-block;background:#6366f122;color:var(--accent2);padding:2px 8px;border-radius:4px;font-weight:600;font-size:0.8rem;font-family:'SF Mono',Consolas,monospace}
|
||||||
|
.method.post{background:#22c55e22;color:var(--green)}
|
||||||
|
|
||||||
|
.try-it{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;margin-bottom:16px}
|
||||||
|
.try-it label{display:block;font-size:0.85rem;color:var(--muted);margin-bottom:4px}
|
||||||
|
.try-it input,.try-it select,.try-it textarea{width:100%;padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.9rem;margin-bottom:12px;font-family:inherit}
|
||||||
|
.try-it textarea{font-family:'SF Mono',Consolas,monospace;font-size:0.85rem;resize:vertical;line-height:1.5}
|
||||||
|
.try-it input:focus,.try-it select:focus,.try-it textarea:focus{outline:none;border-color:var(--accent)}
|
||||||
|
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
|
||||||
|
button{padding:10px 20px;background:var(--accent);color:#fff;border:none;border-radius:6px;font-size:0.9rem;font-weight:600;cursor:pointer;font-family:inherit;transition:background 0.2s}
|
||||||
|
button:hover{background:var(--accent2)}
|
||||||
|
button:disabled{opacity:0.5;cursor:not-allowed}
|
||||||
|
|
||||||
|
.result{margin-top:16px;display:none}
|
||||||
|
.result img{max-width:100%;border:1px solid var(--border);border-radius:8px}
|
||||||
|
.result .meta{font-size:0.8rem;color:var(--muted);margin-top:8px}
|
||||||
|
|
||||||
|
.pricing{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
|
.plan{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px}
|
||||||
|
.plan h3{margin-bottom:4px}
|
||||||
|
.plan .price{font-size:1.8rem;font-weight:700;margin-bottom:12px}
|
||||||
|
.plan .price span{font-size:0.9rem;color:var(--muted);font-weight:400}
|
||||||
|
.plan ul{list-style:none;font-size:0.9rem}
|
||||||
|
.plan ul li{padding:4px 0;color:var(--muted)}
|
||||||
|
.plan ul li::before{content:"+ ";color:var(--green)}
|
||||||
|
.plan.pro{border-color:var(--accent)}
|
||||||
|
|
||||||
|
.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--green);margin-right:6px;animation:pulse 2s infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
||||||
|
|
||||||
|
.feature-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:32px}
|
||||||
|
.feature{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px}
|
||||||
|
.feature h4{font-size:1rem;margin-bottom:6px}
|
||||||
|
.feature p{font-size:0.85rem;color:var(--muted)}
|
||||||
|
|
||||||
|
footer{text-align:center;padding:32px 0;border-top:1px solid var(--border);color:var(--muted);font-size:0.85rem}
|
||||||
|
|
||||||
|
@media(max-width:640px){.hero h1{font-size:1.8rem}.pricing,.row,.row3,.feature-grid{grid-template-columns:1fr}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="hero">
|
||||||
|
<h1>Snap<span>API</span></h1>
|
||||||
|
<p>Screenshots, PDFs, and Markdown conversion. One API for all your document generation needs.</p>
|
||||||
|
<span class="badge green"><span class="status-dot"></span>Online</span>
|
||||||
|
<span class="badge">v2.0</span>
|
||||||
|
<span class="badge">PNG / JPEG / PDF</span>
|
||||||
|
<span class="badge">Markdown</span>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<a href="#" class="active" data-tab="quickstart">Quick Start</a>
|
||||||
|
<a href="#" data-tab="docs">API Docs</a>
|
||||||
|
<a href="#" data-tab="try">Try It</a>
|
||||||
|
<a href="#" data-tab="markdown">Markdown</a>
|
||||||
|
<a href="#" data-tab="pricing">Pricing</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Quick Start -->
|
||||||
|
<div id="quickstart" class="tab-content active">
|
||||||
|
<section>
|
||||||
|
<h2>Document Generation Suite</h2>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<div class="feature">
|
||||||
|
<h4>URL Screenshots</h4>
|
||||||
|
<p>Capture any webpage as PNG, JPEG, or PDF. Custom viewports, full-page support.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h4>Markdown to PDF</h4>
|
||||||
|
<p>Convert Markdown to beautifully styled PDFs. Light and dark themes, custom margins.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h4>Markdown to Image</h4>
|
||||||
|
<p>Render Markdown as PNG or JPEG images. Perfect for social cards and previews.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h4>Markdown to HTML</h4>
|
||||||
|
<p>Convert Markdown to styled HTML. Free endpoint, no auth required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Get started in 30 seconds</h2>
|
||||||
|
<h3>1. Get your API key</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Contact us or use the demo key below for testing (100 captures/month).</p>
|
||||||
|
|
||||||
|
<h3>2. Capture a screenshot</h3>
|
||||||
|
<pre>curl "https://YOUR_HOST/screenshot-api/api/capture?url=https://example.com" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-o screenshot.png</pre>
|
||||||
|
|
||||||
|
<h3>3. Convert Markdown to PDF</h3>
|
||||||
|
<pre>curl -X POST "https://YOUR_HOST/screenshot-api/api/md2pdf" \
|
||||||
|
-H "X-API-Key: YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"markdown": "# My Document\n\nHello **world**!"}' \
|
||||||
|
-o document.pdf</pre>
|
||||||
|
|
||||||
|
<h3>Node.js Example</h3>
|
||||||
|
<pre>// Screenshot
|
||||||
|
const resp = await fetch(
|
||||||
|
'https://YOUR_HOST/screenshot-api/api/capture?url=https://example.com',
|
||||||
|
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
|
||||||
|
);
|
||||||
|
fs.writeFileSync('screenshot.png', Buffer.from(await resp.arrayBuffer()));
|
||||||
|
|
||||||
|
// Markdown to PDF
|
||||||
|
const pdf = await fetch('https://YOUR_HOST/screenshot-api/api/md2pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ markdown: '# Hello\n\nWorld!', theme: 'light' })
|
||||||
|
});
|
||||||
|
fs.writeFileSync('doc.pdf', Buffer.from(await pdf.arrayBuffer()));</pre>
|
||||||
|
|
||||||
|
<h3>Python Example</h3>
|
||||||
|
<pre>import requests
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|
resp = requests.get(
|
||||||
|
'https://YOUR_HOST/screenshot-api/api/capture',
|
||||||
|
params={'url': 'https://example.com'},
|
||||||
|
headers={'X-API-Key': 'YOUR_API_KEY'}
|
||||||
|
)
|
||||||
|
with open('screenshot.png', 'wb') as f:
|
||||||
|
f.write(resp.content)
|
||||||
|
|
||||||
|
# Markdown to PDF
|
||||||
|
resp = requests.post(
|
||||||
|
'https://YOUR_HOST/screenshot-api/api/md2pdf',
|
||||||
|
headers={'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},
|
||||||
|
json={'markdown': '# Hello\n\nWorld!', 'theme': 'light'}
|
||||||
|
)
|
||||||
|
with open('doc.pdf', 'wb') as f:
|
||||||
|
f.write(resp.content)</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Docs -->
|
||||||
|
<div id="docs" class="tab-content">
|
||||||
|
<section>
|
||||||
|
<h2>Endpoints</h2>
|
||||||
|
|
||||||
|
<h3><span class="method">GET</span> /api/capture</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Capture a screenshot or PDF from a URL.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Parameter</th><th>Type</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>url</code></td><td>string</td><td><strong>Required.</strong> The URL to capture.</td></tr>
|
||||||
|
<tr><td><code>format</code></td><td>string</td><td><code>png</code> (default), <code>jpeg</code>, or <code>pdf</code></td></tr>
|
||||||
|
<tr><td><code>width</code></td><td>int</td><td>Viewport width, 320-3840. Default: 1280</td></tr>
|
||||||
|
<tr><td><code>height</code></td><td>int</td><td>Viewport height, 200-2160. Default: 800</td></tr>
|
||||||
|
<tr><td><code>fullPage</code></td><td>bool</td><td>Capture the full scrollable page</td></tr>
|
||||||
|
<tr><td><code>delay</code></td><td>int</td><td>Wait ms after page load (max 10000)</td></tr>
|
||||||
|
<tr><td><code>quality</code></td><td>int</td><td>JPEG quality, 1-100. Default: 80</td></tr>
|
||||||
|
<tr><td><code>paperSize</code></td><td>string</td><td>PDF paper size: A4, Letter, Legal, etc.</td></tr>
|
||||||
|
<tr><td><code>landscape</code></td><td>bool</td><td>PDF landscape orientation</td></tr>
|
||||||
|
<tr><td><code>userAgent</code></td><td>string</td><td>Custom User-Agent header</td></tr>
|
||||||
|
<tr><td><code>timeout</code></td><td>int</td><td>Navigation timeout ms (5000-60000)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><span class="method post">POST</span> /api/md2pdf</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Convert Markdown to a styled PDF document. Requires authentication.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Body Field</th><th>Type</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>markdown</code></td><td>string</td><td><strong>Required.</strong> Markdown content to convert.</td></tr>
|
||||||
|
<tr><td><code>theme</code></td><td>string</td><td><code>light</code> (default) or <code>dark</code></td></tr>
|
||||||
|
<tr><td><code>paperSize</code></td><td>string</td><td>A4 (default), Letter, Legal, Tabloid</td></tr>
|
||||||
|
<tr><td><code>landscape</code></td><td>bool</td><td>Landscape orientation. Default: false</td></tr>
|
||||||
|
<tr><td><code>fontSize</code></td><td>string</td><td>Base font size. Default: "16px"</td></tr>
|
||||||
|
<tr><td><code>margins</code></td><td>object</td><td><code>{ top, bottom, left, right }</code> in CSS units</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><span class="method post">POST</span> /api/md2png</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Convert Markdown to a PNG or JPEG image. Requires authentication.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Body Field</th><th>Type</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>markdown</code></td><td>string</td><td><strong>Required.</strong> Markdown content to convert.</td></tr>
|
||||||
|
<tr><td><code>theme</code></td><td>string</td><td><code>light</code> (default) or <code>dark</code></td></tr>
|
||||||
|
<tr><td><code>format</code></td><td>string</td><td><code>png</code> (default) or <code>jpeg</code></td></tr>
|
||||||
|
<tr><td><code>width</code></td><td>int</td><td>Image width, 320-3840. Default: 1280</td></tr>
|
||||||
|
<tr><td><code>fontSize</code></td><td>string</td><td>Base font size. Default: "16px"</td></tr>
|
||||||
|
<tr><td><code>quality</code></td><td>int</td><td>JPEG quality, 1-100. Default: 85</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><span class="method post">POST</span> /api/md2html</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Convert Markdown to styled HTML. <strong>No authentication required.</strong></p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Body Field</th><th>Type</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>markdown</code></td><td>string</td><td><strong>Required.</strong> Markdown content to convert.</td></tr>
|
||||||
|
<tr><td><code>theme</code></td><td>string</td><td><code>light</code> (default) or <code>dark</code></td></tr>
|
||||||
|
<tr><td><code>fontSize</code></td><td>string</td><td>Base font size. Default: "16px"</td></tr>
|
||||||
|
<tr><td><code>width</code></td><td>string</td><td>Max content width. Default: "800px"</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Authentication</h3>
|
||||||
|
<p style="color:var(--muted);margin-bottom:16px">Pass your API key via the <code class="inline-code">X-API-Key</code> header or the <code class="inline-code">api_key</code> query parameter. The <code class="inline-code">/api/md2html</code> endpoint does not require authentication.</p>
|
||||||
|
|
||||||
|
<h3>Error Responses</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Status</th><th>Meaning</th></tr>
|
||||||
|
<tr><td>400</td><td>Missing/invalid parameters, body too large (max 1MB)</td></tr>
|
||||||
|
<tr><td>401</td><td>Missing or invalid API key</td></tr>
|
||||||
|
<tr><td>429</td><td>Rate limit or monthly limit exceeded</td></tr>
|
||||||
|
<tr><td>500</td><td>Conversion failed</td></tr>
|
||||||
|
<tr><td>503</td><td>Server busy, max concurrent tasks reached</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3><span class="method">GET</span> /api/status</h3>
|
||||||
|
<p style="color:var(--muted)">Service health and statistics. No authentication required.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Try It - Screenshot -->
|
||||||
|
<div id="try" class="tab-content">
|
||||||
|
<section>
|
||||||
|
<h2>Try Screenshot Capture</h2>
|
||||||
|
<div class="try-it">
|
||||||
|
<label for="try-url">URL to capture</label>
|
||||||
|
<input type="url" id="try-url" placeholder="https://example.com" value="https://example.com">
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label for="try-format">Format</label>
|
||||||
|
<select id="try-format">
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="jpeg">JPEG</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="try-width">Width</label>
|
||||||
|
<input type="number" id="try-width" value="1280" min="320" max="3840">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label for="try-height">Height</label>
|
||||||
|
<input type="number" id="try-height" value="800" min="200" max="2160">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="try-fullpage">Full Page</label>
|
||||||
|
<select id="try-fullpage">
|
||||||
|
<option value="false">No</option>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label for="try-key">API Key</label>
|
||||||
|
<input type="text" id="try-key" placeholder="Enter your API key">
|
||||||
|
<button id="try-btn" onclick="tryCapture()">Capture</button>
|
||||||
|
<div class="result" id="try-result"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown -->
|
||||||
|
<div id="markdown" class="tab-content">
|
||||||
|
<section>
|
||||||
|
<h2>Try Markdown Conversion</h2>
|
||||||
|
<div class="try-it">
|
||||||
|
<label for="md-input">Markdown</label>
|
||||||
|
<textarea id="md-input" rows="12"># Project Report
|
||||||
|
|
||||||
|
**Author**: Engineering Team
|
||||||
|
**Date**: February 2026
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This is a sample document demonstrating the **Markdown to PDF** conversion API.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Full [GitHub Flavored Markdown](https://github.github.com/gfm/) support
|
||||||
|
- Light and dark themes
|
||||||
|
- Tables, code blocks, and more
|
||||||
|
|
||||||
|
| Feature | Status |
|
||||||
|
|---------|--------|
|
||||||
|
| Headers | Done |
|
||||||
|
| Tables | Done |
|
||||||
|
| Code | Done |
|
||||||
|
| Lists | Done |
|
||||||
|
|
||||||
|
### Code Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('/api/md2pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ markdown: '# Hello' })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> This PDF was generated by SnapAPI's Markdown conversion endpoint.
|
||||||
|
</textarea>
|
||||||
|
<div class="row3">
|
||||||
|
<div>
|
||||||
|
<label for="md-format">Output Format</label>
|
||||||
|
<select id="md-format">
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="png">PNG Image</option>
|
||||||
|
<option value="html">HTML (free)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="md-theme">Theme</label>
|
||||||
|
<select id="md-theme">
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="md-paper">Paper Size</label>
|
||||||
|
<select id="md-paper">
|
||||||
|
<option value="A4">A4</option>
|
||||||
|
<option value="Letter">Letter</option>
|
||||||
|
<option value="Legal">Legal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label for="md-key">API Key <span style="color:var(--muted);font-weight:400">(not needed for HTML)</span></label>
|
||||||
|
<input type="text" id="md-key" placeholder="Enter your API key">
|
||||||
|
<button id="md-btn" onclick="tryMarkdown()">Convert</button>
|
||||||
|
<div class="result" id="md-result"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pricing -->
|
||||||
|
<div id="pricing" class="tab-content">
|
||||||
|
<section>
|
||||||
|
<h2>Pricing</h2>
|
||||||
|
<div class="pricing">
|
||||||
|
<div class="plan">
|
||||||
|
<h3>Free</h3>
|
||||||
|
<div class="price">$0<span>/mo</span></div>
|
||||||
|
<ul>
|
||||||
|
<li>100 captures / month</li>
|
||||||
|
<li>Screenshots: PNG, JPEG, PDF</li>
|
||||||
|
<li>Markdown: PDF, PNG, HTML</li>
|
||||||
|
<li>10 requests / minute</li>
|
||||||
|
<li>Light & dark themes</li>
|
||||||
|
<li>md2html: unlimited, no auth</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="plan pro">
|
||||||
|
<h3>Pro</h3>
|
||||||
|
<div class="price">$19<span>/mo</span></div>
|
||||||
|
<ul>
|
||||||
|
<li>5,000 captures / month</li>
|
||||||
|
<li>Screenshots: PNG, JPEG, PDF</li>
|
||||||
|
<li>Markdown: PDF, PNG, HTML</li>
|
||||||
|
<li>60 requests / minute</li>
|
||||||
|
<li>Custom viewport up to 4K</li>
|
||||||
|
<li>Custom margins & paper sizes</li>
|
||||||
|
<li>Priority support</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="color:var(--muted);margin-top:16px;font-size:0.9rem">Contact us to get started with a Pro plan or discuss enterprise needs.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Built by an AI Agent (Claude) — transparent about AI authorship.</p>
|
||||||
|
<p style="margin-top:4px">SnapAPI v2.0 · <a href="/api">JSON API Docs</a> · <a href="/api/status">Status</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tab navigation
|
||||||
|
document.querySelectorAll('nav a').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelectorAll('nav a').forEach(l => l.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||||
|
link.classList.add('active');
|
||||||
|
document.getElementById(link.dataset.tab).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Screenshot try-it
|
||||||
|
async function tryCapture() {
|
||||||
|
const btn = document.getElementById('try-btn');
|
||||||
|
const result = document.getElementById('try-result');
|
||||||
|
const url = document.getElementById('try-url').value;
|
||||||
|
const format = document.getElementById('try-format').value;
|
||||||
|
const width = document.getElementById('try-width').value;
|
||||||
|
const height = document.getElementById('try-height').value;
|
||||||
|
const fullPage = document.getElementById('try-fullpage').value;
|
||||||
|
const apiKey = document.getElementById('try-key').value;
|
||||||
|
|
||||||
|
if (!url) { alert('Please enter a URL'); return; }
|
||||||
|
if (!apiKey) { alert('Please enter an API key'); return; }
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Capturing...';
|
||||||
|
result.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ url, format, width, height, fullPage });
|
||||||
|
const resp = await fetch(`/api/capture?${params}`, {
|
||||||
|
headers: { 'X-API-Key': apiKey }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json();
|
||||||
|
result.innerHTML = `<pre style="color:var(--red)">${JSON.stringify(err, null, 2)}</pre>`;
|
||||||
|
result.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const used = resp.headers.get('X-Captures-Used');
|
||||||
|
const limit = resp.headers.get('X-Captures-Limit');
|
||||||
|
|
||||||
|
if (format === 'pdf') {
|
||||||
|
const pdfUrl = URL.createObjectURL(blob);
|
||||||
|
result.innerHTML = `<a href="${pdfUrl}" target="_blank">Download PDF (${(blob.size/1024).toFixed(1)} KB)</a>
|
||||||
|
<div class="meta">Captures used: ${used}/${limit}</div>`;
|
||||||
|
} else {
|
||||||
|
const imgUrl = URL.createObjectURL(blob);
|
||||||
|
result.innerHTML = `<img src="${imgUrl}" alt="Screenshot">
|
||||||
|
<div class="meta">${(blob.size/1024).toFixed(1)} KB · Captures: ${used}/${limit}</div>`;
|
||||||
|
}
|
||||||
|
result.style.display = 'block';
|
||||||
|
} catch (e) {
|
||||||
|
result.innerHTML = `<pre style="color:var(--red)">Error: ${e.message}</pre>`;
|
||||||
|
result.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Capture';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown try-it
|
||||||
|
async function tryMarkdown() {
|
||||||
|
const btn = document.getElementById('md-btn');
|
||||||
|
const result = document.getElementById('md-result');
|
||||||
|
const markdown = document.getElementById('md-input').value;
|
||||||
|
const format = document.getElementById('md-format').value;
|
||||||
|
const theme = document.getElementById('md-theme').value;
|
||||||
|
const paperSize = document.getElementById('md-paper').value;
|
||||||
|
const apiKey = document.getElementById('md-key').value;
|
||||||
|
|
||||||
|
if (!markdown.trim()) { alert('Please enter some Markdown'); return; }
|
||||||
|
if (format !== 'html' && !apiKey) { alert('API key required for PDF/PNG output'); return; }
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Converting...';
|
||||||
|
result.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = format === 'html' ? '/api/md2html'
|
||||||
|
: format === 'png' ? '/api/md2png' : '/api/md2pdf';
|
||||||
|
|
||||||
|
const body = { markdown, theme };
|
||||||
|
if (format === 'pdf') body.paperSize = paperSize;
|
||||||
|
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (format !== 'html' && apiKey) headers['X-API-Key'] = apiKey;
|
||||||
|
|
||||||
|
const resp = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json();
|
||||||
|
result.innerHTML = `<pre style="color:var(--red)">${JSON.stringify(err, null, 2)}</pre>`;
|
||||||
|
result.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'html') {
|
||||||
|
const html = await resp.text();
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.cssText = 'width:100%;height:500px;border:1px solid var(--border);border-radius:8px;background:#fff';
|
||||||
|
result.innerHTML = '';
|
||||||
|
result.appendChild(iframe);
|
||||||
|
iframe.contentDocument.open();
|
||||||
|
iframe.contentDocument.write(html);
|
||||||
|
iframe.contentDocument.close();
|
||||||
|
result.innerHTML += `<div class="meta">${(html.length/1024).toFixed(1)} KB HTML</div>`;
|
||||||
|
} else if (format === 'pdf') {
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const pdfUrl = URL.createObjectURL(blob);
|
||||||
|
result.innerHTML = `<iframe src="${pdfUrl}" style="width:100%;height:500px;border:1px solid var(--border);border-radius:8px"></iframe>
|
||||||
|
<div class="meta"><a href="${pdfUrl}" target="_blank">Download PDF</a> (${(blob.size/1024).toFixed(1)} KB) · Captures: ${resp.headers.get('X-Captures-Used')}/${resp.headers.get('X-Captures-Limit')}</div>`;
|
||||||
|
} else {
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const imgUrl = URL.createObjectURL(blob);
|
||||||
|
result.innerHTML = `<img src="${imgUrl}" alt="Markdown render">
|
||||||
|
<div class="meta">${(blob.size/1024).toFixed(1)} KB · Captures: ${resp.headers.get('X-Captures-Used')}/${resp.headers.get('X-Captures-Limit')}</div>`;
|
||||||
|
}
|
||||||
|
result.style.display = 'block';
|
||||||
|
} catch (e) {
|
||||||
|
result.innerHTML = `<pre style="color:var(--red)">Error: ${e.message}</pre>`;
|
||||||
|
result.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Convert';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
940
package-lock.json
generated
Normal file
940
package-lock.json
generated
Normal file
@ -0,0 +1,940 @@
|
|||||||
|
{
|
||||||
|
"name": "screenshot-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "screenshot-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"puppeteer-core": "^24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@puppeteer/browsers": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"extract-zip": "^2.0.1",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"proxy-agent": "^6.5.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
|
"tar-fs": "^3.1.1",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"browsers": "lib/cjs/main-cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||||
|
"version": "0.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||||
|
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
|
||||||
|
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ast-types": {
|
||||||
|
"version": "0.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||||
|
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-fs": {
|
||||||
|
"version": "4.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz",
|
||||||
|
"integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.5.4",
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"bare-stream": "^2.6.4",
|
||||||
|
"bare-url": "^2.2.2",
|
||||||
|
"fast-fifo": "^1.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-os": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-os": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-stream": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"streamx": "^2.21.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*",
|
||||||
|
"bare-events": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-events": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-url": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/basic-ftp": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chromium-bidi": {
|
||||||
|
"version": "13.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz",
|
||||||
|
"integrity": "sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"devtools-protocol": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/degenerator": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ast-types": "^0.13.4",
|
||||||
|
"escodegen": "^2.1.0",
|
||||||
|
"esprima": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/devtools-protocol": {
|
||||||
|
"version": "0.0.1566079",
|
||||||
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz",
|
||||||
|
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escodegen": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"esprima": "^4.0.1",
|
||||||
|
"estraverse": "^5.2.0",
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"escodegen": "bin/escodegen.js",
|
||||||
|
"esgenerate": "bin/esgenerate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"source-map": "~0.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"bin": {
|
||||||
|
"esparse": "bin/esparse.js",
|
||||||
|
"esvalidate": "bin/esvalidate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estraverse": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events-universal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-zip": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"extract-zip": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/yauzl": "^2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-uri": {
|
||||||
|
"version": "6.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
|
||||||
|
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-ftp": "^5.0.2",
|
||||||
|
"data-uri-to-buffer": "^6.0.2",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "17.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||||
|
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/netmask": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pac-proxy-agent": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tootallnate/quickjs-emscripten": "^0.23.0",
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"get-uri": "^6.0.1",
|
||||||
|
"http-proxy-agent": "^7.0.0",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"pac-resolver": "^7.0.1",
|
||||||
|
"socks-proxy-agent": "^8.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pac-resolver": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"degenerator": "^5.0.0",
|
||||||
|
"netmask": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-agent": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"http-proxy-agent": "^7.0.1",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"lru-cache": "^7.14.1",
|
||||||
|
"pac-proxy-agent": "^7.1.0",
|
||||||
|
"proxy-from-env": "^1.1.0",
|
||||||
|
"socks-proxy-agent": "^8.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/puppeteer-core": {
|
||||||
|
"version": "24.37.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.1.tgz",
|
||||||
|
"integrity": "sha512-ylRJReaA6kd/CrahdrxxnSZf5S2hf1QR0S39QeoS55fuBoOl4UggGPW94zheu9lmCokpRQpa7q8r98xYyiQl0Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@puppeteer/browsers": "2.12.0",
|
||||||
|
"chromium-bidi": "13.1.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"devtools-protocol": "0.0.1566079",
|
||||||
|
"typed-query-selector": "^2.12.0",
|
||||||
|
"webdriver-bidi-protocol": "0.4.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "8.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||||
|
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||||
|
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^3.1.5"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bare-fs": "^4.0.1",
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/typed-query-selector": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/webdriver-bidi-protocol": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.76",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "screenshot-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "REST API for capturing screenshots and generating PDFs from URLs",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"puppeteer-core": "^24.0.0"
|
||||||
|
},
|
||||||
|
"author": "AI Agent (built by Claude)",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
6
public/favicon.svg
Normal file
6
public/favicon.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#10b981"/>
|
||||||
|
<rect x="6" y="8" width="20" height="16" rx="2" fill="none" stroke="white" stroke-width="2"/>
|
||||||
|
<circle cx="16" cy="16" r="4" fill="none" stroke="white" stroke-width="2"/>
|
||||||
|
<rect x="13" y="5" width="6" height="4" rx="1" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 361 B |
16
screenshot-api.service
Normal file
16
screenshot-api.service
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Screenshot & PDF API
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=agent
|
||||||
|
WorkingDirectory=/home/agent/projects/screenshot-api
|
||||||
|
ExecStart=/usr/bin/node server.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PORT=3001
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
605
server.js
Normal file
605
server.js
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const url = require('url');
|
||||||
|
const puppeteer = require('puppeteer-core');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { marked } = require('marked');
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
const CHROME_PATH = process.env.CHROME_PATH || '/usr/bin/google-chrome';
|
||||||
|
const API_KEYS_FILE = path.join(__dirname, 'api-keys.json');
|
||||||
|
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||||
|
const MAX_CONCURRENT = 3;
|
||||||
|
|
||||||
|
// Load or create API keys
|
||||||
|
let apiKeys = {};
|
||||||
|
try { apiKeys = JSON.parse(fs.readFileSync(API_KEYS_FILE, 'utf8')); } catch {}
|
||||||
|
|
||||||
|
// Generate a demo key if none exist
|
||||||
|
if (Object.keys(apiKeys).length === 0) {
|
||||||
|
const demoKey = 'demo_' + crypto.randomBytes(16).toString('hex');
|
||||||
|
apiKeys[demoKey] = {
|
||||||
|
name: 'demo',
|
||||||
|
tier: 'free',
|
||||||
|
limit: 100, // per month
|
||||||
|
used: 0,
|
||||||
|
resetMonth: new Date().toISOString().slice(0, 7),
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(API_KEYS_FILE, JSON.stringify(apiKeys, null, 2));
|
||||||
|
console.log(`Demo API key created: ${demoKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const rateLimits = new Map();
|
||||||
|
let activeTasks = 0;
|
||||||
|
|
||||||
|
function checkRateLimit(apiKey) {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = rateLimits.get(apiKey) || { count: 0, windowStart: now };
|
||||||
|
if (now - entry.windowStart > RATE_LIMIT_WINDOW) {
|
||||||
|
entry.count = 0;
|
||||||
|
entry.windowStart = now;
|
||||||
|
}
|
||||||
|
entry.count++;
|
||||||
|
rateLimits.set(apiKey, entry);
|
||||||
|
return entry.count <= 10; // 10 requests per minute
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMonthlyUsage(keyData) {
|
||||||
|
const currentMonth = new Date().toISOString().slice(0, 7);
|
||||||
|
if (keyData.resetMonth !== currentMonth) {
|
||||||
|
keyData.used = 0;
|
||||||
|
keyData.resetMonth = currentMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJson(res, status, data) {
|
||||||
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||||
|
res.end(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(res, status, message) {
|
||||||
|
sendJson(res, status, { error: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
let stats = { total_captures: 0, screenshots: 0, pdfs: 0, md_conversions: 0, errors: 0 };
|
||||||
|
const ANALYTICS_FILE = path.join(__dirname, 'analytics.json');
|
||||||
|
let analytics = { daily: {}, total_views: 0 };
|
||||||
|
try { analytics = JSON.parse(fs.readFileSync(ANALYTICS_FILE, 'utf8')); } catch {}
|
||||||
|
function trackPageView() {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
analytics.daily[today] = (analytics.daily[today] || 0) + 1;
|
||||||
|
analytics.total_views++;
|
||||||
|
if (analytics.total_views % 10 === 0) {
|
||||||
|
fs.writeFile(ANALYTICS_FILE, JSON.stringify(analytics, null, 2), () => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read POST body
|
||||||
|
function readBody(req, maxSize = 1024 * 1024) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let body = '';
|
||||||
|
let size = 0;
|
||||||
|
req.on('data', chunk => {
|
||||||
|
size += chunk.length;
|
||||||
|
if (size > maxSize) {
|
||||||
|
reject(new Error('Body too large (max 1MB)'));
|
||||||
|
req.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
req.on('end', () => resolve(body));
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown-to-HTML with styling
|
||||||
|
function renderMarkdownHTML(markdown, options = {}) {
|
||||||
|
const htmlContent = marked(markdown);
|
||||||
|
const theme = options.theme === 'dark' ? `
|
||||||
|
body { background: #1a1a2e; color: #e0e0e0; }
|
||||||
|
a { color: #64b5f6; }
|
||||||
|
code { background: #2d2d44; }
|
||||||
|
pre { background: #2d2d44; }
|
||||||
|
blockquote { border-left-color: #64b5f6; color: #b0b0b0; }
|
||||||
|
table th { background: #2d2d44; }
|
||||||
|
table td, table th { border-color: #3d3d54; }
|
||||||
|
hr { border-color: #3d3d54; }
|
||||||
|
` : `
|
||||||
|
body { background: #ffffff; color: #24292f; }
|
||||||
|
a { color: #0969da; }
|
||||||
|
code { background: #f6f8fa; }
|
||||||
|
pre { background: #f6f8fa; }
|
||||||
|
blockquote { border-left-color: #d0d7de; color: #57606a; }
|
||||||
|
table th { background: #f6f8fa; }
|
||||||
|
table td, table th { border-color: #d0d7de; }
|
||||||
|
hr { border-color: #d0d7de; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: ${options.width || '800px'};
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
font-size: ${options.fontSize || '16px'};
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; }
|
||||||
|
h1 { font-size: 2em; border-bottom: 1px solid #d0d7de; padding-bottom: 0.3em; }
|
||||||
|
h2 { font-size: 1.5em; border-bottom: 1px solid #d0d7de; padding-bottom: 0.3em; }
|
||||||
|
code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 85%;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
pre code { padding: 0; background: none; font-size: 85%; }
|
||||||
|
blockquote {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
border-left: 0.25em solid;
|
||||||
|
}
|
||||||
|
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||||
|
table td, table th { padding: 6px 13px; border: 1px solid; }
|
||||||
|
img { max-width: 100%; }
|
||||||
|
hr { border: none; border-top: 1px solid; margin: 2em 0; }
|
||||||
|
ul, ol { padding-left: 2em; }
|
||||||
|
li + li { margin-top: 0.25em; }
|
||||||
|
${theme}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${htmlContent}</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert markdown to PDF or PNG using Puppeteer
|
||||||
|
async function convertMarkdown(markdown, options = {}) {
|
||||||
|
const html = renderMarkdownHTML(markdown, options);
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
executablePath: CHROME_PATH,
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--single-process'],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setContent(html, { waitUntil: 'load' });
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (options.format === 'png' || options.format === 'jpeg') {
|
||||||
|
const width = Math.min(Math.max(parseInt(options.viewportWidth) || 1280, 320), 3840);
|
||||||
|
await page.setViewport({ width, height: 800 });
|
||||||
|
await page.setContent(html, { waitUntil: 'load' });
|
||||||
|
result = await page.screenshot({
|
||||||
|
type: options.format,
|
||||||
|
fullPage: true,
|
||||||
|
quality: options.format === 'jpeg' ? Math.min(parseInt(options.quality) || 85, 100) : undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = await page.pdf({
|
||||||
|
format: options.paperSize || 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
margin: {
|
||||||
|
top: options.marginTop || '20mm',
|
||||||
|
bottom: options.marginBottom || '20mm',
|
||||||
|
left: options.marginLeft || '15mm',
|
||||||
|
right: options.marginRight || '15mm',
|
||||||
|
},
|
||||||
|
landscape: options.landscape === true || options.landscape === 'true',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.md_conversions++;
|
||||||
|
stats.total_captures++;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureScreenshot(targetUrl, options = {}) {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
executablePath: CHROME_PATH,
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--single-process',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
const width = Math.min(Math.max(parseInt(options.width) || 1280, 320), 3840);
|
||||||
|
const height = Math.min(Math.max(parseInt(options.height) || 800, 200), 2160);
|
||||||
|
await page.setViewport({ width, height });
|
||||||
|
|
||||||
|
if (options.userAgent) {
|
||||||
|
await page.setUserAgent(options.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = Math.min(Math.max(parseInt(options.timeout) || 30000, 5000), 60000);
|
||||||
|
await page.goto(targetUrl, {
|
||||||
|
waitUntil: options.waitUntil || 'networkidle2',
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.delay) {
|
||||||
|
await new Promise(r => setTimeout(r, Math.min(parseInt(options.delay), 10000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (options.format === 'pdf') {
|
||||||
|
result = await page.pdf({
|
||||||
|
format: options.paperSize || 'A4',
|
||||||
|
printBackground: options.printBackground !== false,
|
||||||
|
landscape: options.landscape === true || options.landscape === 'true',
|
||||||
|
});
|
||||||
|
stats.pdfs++;
|
||||||
|
} else {
|
||||||
|
result = await page.screenshot({
|
||||||
|
type: options.imageType || 'png',
|
||||||
|
fullPage: options.fullPage === true || options.fullPage === 'true',
|
||||||
|
quality: options.imageType === 'jpeg' ? Math.min(parseInt(options.quality) || 80, 100) : undefined,
|
||||||
|
});
|
||||||
|
stats.screenshots++;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.total_captures++;
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
// CORS preflight
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(204, {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, X-API-Key',
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = url.parse(req.url, true);
|
||||||
|
|
||||||
|
// Health/status endpoint (no auth required)
|
||||||
|
if (parsed.pathname === '/api/status') {
|
||||||
|
sendJson(res, 200, {
|
||||||
|
service: 'screenshot-api',
|
||||||
|
status: 'ok',
|
||||||
|
stats,
|
||||||
|
page_views: analytics.total_views,
|
||||||
|
active_tasks: activeTasks,
|
||||||
|
max_concurrent: MAX_CONCURRENT,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API docs - JSON
|
||||||
|
if (parsed.pathname === '/api') {
|
||||||
|
sendJson(res, 200, {
|
||||||
|
service: 'SnapAPI - Document Generation Suite',
|
||||||
|
version: '2.0.0',
|
||||||
|
description: 'Screenshots, PDFs, and Markdown conversion API',
|
||||||
|
built_by: 'AI Agent (Claude) - transparent about AI authorship',
|
||||||
|
endpoints: {
|
||||||
|
'GET /api/capture': {
|
||||||
|
description: 'Capture a screenshot or PDF from a URL',
|
||||||
|
auth: 'X-API-Key header or ?api_key= query param',
|
||||||
|
params: {
|
||||||
|
url: 'Target URL (required)',
|
||||||
|
format: 'png (default), jpeg, or pdf',
|
||||||
|
width: 'Viewport width (320-3840, default 1280)',
|
||||||
|
height: 'Viewport height (200-2160, default 800)',
|
||||||
|
fullPage: 'Capture full page (true/false)',
|
||||||
|
delay: 'Wait ms after load (max 10000)',
|
||||||
|
quality: 'JPEG quality (1-100, default 80)',
|
||||||
|
paperSize: 'PDF paper size (A4, Letter, etc)',
|
||||||
|
landscape: 'PDF landscape mode (true/false)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'POST /api/md2pdf': {
|
||||||
|
description: 'Convert Markdown to PDF',
|
||||||
|
auth: 'X-API-Key header',
|
||||||
|
body: 'JSON: { markdown, theme?, paperSize?, landscape?, fontSize?, margins? }',
|
||||||
|
},
|
||||||
|
'POST /api/md2png': {
|
||||||
|
description: 'Convert Markdown to PNG image',
|
||||||
|
auth: 'X-API-Key header',
|
||||||
|
body: 'JSON: { markdown, theme?, width?, fontSize? }',
|
||||||
|
},
|
||||||
|
'POST /api/md2html': {
|
||||||
|
description: 'Convert Markdown to styled HTML (no auth required)',
|
||||||
|
body: 'JSON: { markdown, theme?, fontSize? }',
|
||||||
|
},
|
||||||
|
'GET /api/status': { description: 'Service health and stats' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favicon
|
||||||
|
if (parsed.pathname === '/favicon.svg') {
|
||||||
|
const faviconPath = path.join(__dirname, 'public', 'favicon.svg');
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(faviconPath);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
|
||||||
|
res.end(data);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('Not found');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landing page
|
||||||
|
if (parsed.pathname === '/') {
|
||||||
|
trackPageView();
|
||||||
|
const landingPage = fs.readFileSync(path.join(__dirname, 'landing.html'), 'utf8');
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html', 'Access-Control-Allow-Origin': '*' });
|
||||||
|
res.end(landingPage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture endpoint
|
||||||
|
if (parsed.pathname === '/api/capture') {
|
||||||
|
// Auth
|
||||||
|
const apiKey = req.headers['x-api-key'] || parsed.query.api_key;
|
||||||
|
if (!apiKey || !apiKeys[apiKey]) {
|
||||||
|
return sendError(res, 401, 'Invalid or missing API key. Include X-API-Key header.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyData = apiKeys[apiKey];
|
||||||
|
resetMonthlyUsage(keyData);
|
||||||
|
|
||||||
|
// Check monthly limit
|
||||||
|
if (keyData.used >= keyData.limit) {
|
||||||
|
return sendError(res, 429, `Monthly limit reached (${keyData.limit} captures). Upgrade your plan.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
if (!checkRateLimit(apiKey)) {
|
||||||
|
return sendError(res, 429, 'Rate limit exceeded. Max 10 requests per minute.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrency limit
|
||||||
|
if (activeTasks >= MAX_CONCURRENT) {
|
||||||
|
return sendError(res, 503, 'Server busy. Try again in a few seconds.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
const targetUrl = parsed.query.url;
|
||||||
|
if (!targetUrl) {
|
||||||
|
return sendError(res, 400, 'Missing required parameter: url');
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedTarget;
|
||||||
|
try {
|
||||||
|
parsedTarget = new URL(targetUrl);
|
||||||
|
} catch {
|
||||||
|
return sendError(res, 400, 'Invalid URL format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block private/internal URLs
|
||||||
|
const hostname = parsedTarget.hostname.toLowerCase();
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0' ||
|
||||||
|
hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.') ||
|
||||||
|
hostname === '::1' || hostname === 'metadata.google.internal') {
|
||||||
|
return sendError(res, 400, 'Cannot capture internal/private URLs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['http:', 'https:'].includes(parsedTarget.protocol)) {
|
||||||
|
return sendError(res, 400, 'Only http and https URLs are supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTasks++;
|
||||||
|
try {
|
||||||
|
const buffer = await captureScreenshot(targetUrl, {
|
||||||
|
format: parsed.query.format,
|
||||||
|
width: parsed.query.width,
|
||||||
|
height: parsed.query.height,
|
||||||
|
fullPage: parsed.query.fullPage,
|
||||||
|
delay: parsed.query.delay,
|
||||||
|
imageType: parsed.query.format === 'jpeg' ? 'jpeg' : 'png',
|
||||||
|
quality: parsed.query.quality,
|
||||||
|
paperSize: parsed.query.paperSize,
|
||||||
|
landscape: parsed.query.landscape,
|
||||||
|
printBackground: parsed.query.printBackground,
|
||||||
|
waitUntil: parsed.query.waitUntil,
|
||||||
|
timeout: parsed.query.timeout,
|
||||||
|
userAgent: parsed.query.userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
keyData.used++;
|
||||||
|
fs.writeFile(API_KEYS_FILE, JSON.stringify(apiKeys, null, 2), () => {});
|
||||||
|
|
||||||
|
const contentType = parsed.query.format === 'pdf'
|
||||||
|
? 'application/pdf'
|
||||||
|
: parsed.query.format === 'jpeg' ? 'image/jpeg' : 'image/png';
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Content-Length': buffer.length,
|
||||||
|
'X-Captures-Used': keyData.used,
|
||||||
|
'X-Captures-Limit': keyData.limit,
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
res.end(buffer);
|
||||||
|
} catch (err) {
|
||||||
|
stats.errors++;
|
||||||
|
sendError(res, 500, 'Capture failed: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
activeTasks--;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown to HTML (no auth required - lightweight)
|
||||||
|
if (parsed.pathname === '/api/md2html' && req.method === 'POST') {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(await readBody(req));
|
||||||
|
if (!body.markdown) {
|
||||||
|
return sendError(res, 400, 'Missing required field: markdown');
|
||||||
|
}
|
||||||
|
const html = renderMarkdownHTML(body.markdown, {
|
||||||
|
theme: body.theme,
|
||||||
|
fontSize: body.fontSize,
|
||||||
|
width: body.width,
|
||||||
|
});
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html', 'Access-Control-Allow-Origin': '*' });
|
||||||
|
res.end(html);
|
||||||
|
} catch (err) {
|
||||||
|
sendError(res, 400, 'Invalid JSON body: ' + err.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown to PDF (auth required - uses Puppeteer)
|
||||||
|
if (parsed.pathname === '/api/md2pdf' && req.method === 'POST') {
|
||||||
|
const apiKey = req.headers['x-api-key'] || parsed.query.api_key;
|
||||||
|
if (!apiKey || !apiKeys[apiKey]) {
|
||||||
|
return sendError(res, 401, 'Invalid or missing API key. Include X-API-Key header.');
|
||||||
|
}
|
||||||
|
const keyData = apiKeys[apiKey];
|
||||||
|
resetMonthlyUsage(keyData);
|
||||||
|
if (keyData.used >= keyData.limit) {
|
||||||
|
return sendError(res, 429, `Monthly limit reached (${keyData.limit}). Upgrade your plan.`);
|
||||||
|
}
|
||||||
|
if (!checkRateLimit(apiKey)) {
|
||||||
|
return sendError(res, 429, 'Rate limit exceeded. Max 10 requests per minute.');
|
||||||
|
}
|
||||||
|
if (activeTasks >= MAX_CONCURRENT) {
|
||||||
|
return sendError(res, 503, 'Server busy. Try again in a few seconds.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(await readBody(req));
|
||||||
|
if (!body.markdown) {
|
||||||
|
return sendError(res, 400, 'Missing required field: markdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTasks++;
|
||||||
|
try {
|
||||||
|
const buffer = await convertMarkdown(body.markdown, {
|
||||||
|
format: 'pdf',
|
||||||
|
theme: body.theme,
|
||||||
|
paperSize: body.paperSize,
|
||||||
|
landscape: body.landscape,
|
||||||
|
fontSize: body.fontSize,
|
||||||
|
marginTop: body.margins?.top,
|
||||||
|
marginBottom: body.margins?.bottom,
|
||||||
|
marginLeft: body.margins?.left,
|
||||||
|
marginRight: body.margins?.right,
|
||||||
|
});
|
||||||
|
|
||||||
|
keyData.used++;
|
||||||
|
fs.writeFile(API_KEYS_FILE, JSON.stringify(apiKeys, null, 2), () => {});
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Length': buffer.length,
|
||||||
|
'Content-Disposition': 'inline; filename="document.pdf"',
|
||||||
|
'X-Captures-Used': keyData.used,
|
||||||
|
'X-Captures-Limit': keyData.limit,
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
res.end(buffer);
|
||||||
|
} finally {
|
||||||
|
activeTasks--;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
stats.errors++;
|
||||||
|
sendError(res, err.message.includes('JSON') ? 400 : 500, err.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown to PNG (auth required - uses Puppeteer)
|
||||||
|
if (parsed.pathname === '/api/md2png' && req.method === 'POST') {
|
||||||
|
const apiKey = req.headers['x-api-key'] || parsed.query.api_key;
|
||||||
|
if (!apiKey || !apiKeys[apiKey]) {
|
||||||
|
return sendError(res, 401, 'Invalid or missing API key. Include X-API-Key header.');
|
||||||
|
}
|
||||||
|
const keyData = apiKeys[apiKey];
|
||||||
|
resetMonthlyUsage(keyData);
|
||||||
|
if (keyData.used >= keyData.limit) {
|
||||||
|
return sendError(res, 429, `Monthly limit reached (${keyData.limit}). Upgrade your plan.`);
|
||||||
|
}
|
||||||
|
if (!checkRateLimit(apiKey)) {
|
||||||
|
return sendError(res, 429, 'Rate limit exceeded. Max 10 requests per minute.');
|
||||||
|
}
|
||||||
|
if (activeTasks >= MAX_CONCURRENT) {
|
||||||
|
return sendError(res, 503, 'Server busy. Try again in a few seconds.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(await readBody(req));
|
||||||
|
if (!body.markdown) {
|
||||||
|
return sendError(res, 400, 'Missing required field: markdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTasks++;
|
||||||
|
try {
|
||||||
|
const buffer = await convertMarkdown(body.markdown, {
|
||||||
|
format: body.format === 'jpeg' ? 'jpeg' : 'png',
|
||||||
|
theme: body.theme,
|
||||||
|
viewportWidth: body.width,
|
||||||
|
fontSize: body.fontSize,
|
||||||
|
quality: body.quality,
|
||||||
|
});
|
||||||
|
|
||||||
|
keyData.used++;
|
||||||
|
fs.writeFile(API_KEYS_FILE, JSON.stringify(apiKeys, null, 2), () => {});
|
||||||
|
|
||||||
|
const imgType = body.format === 'jpeg' ? 'jpeg' : 'png';
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': `image/${imgType}`,
|
||||||
|
'Content-Length': buffer.length,
|
||||||
|
'Content-Disposition': `inline; filename="document.${imgType}"`,
|
||||||
|
'X-Captures-Used': keyData.used,
|
||||||
|
'X-Captures-Limit': keyData.limit,
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
});
|
||||||
|
res.end(buffer);
|
||||||
|
} finally {
|
||||||
|
activeTasks--;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
stats.errors++;
|
||||||
|
sendError(res, err.message.includes('JSON') ? 400 : 500, err.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendError(res, 404, 'Not found. Visit / for API documentation.');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Screenshot API running at http://localhost:${PORT}`);
|
||||||
|
console.log(`API keys: ${Object.keys(apiKeys).length} configured`);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user