commit 0609754ab35e69e87ed9185b1b751fdd86634f66 Author: OpSpawn Date: Fri Feb 6 06:47:04 2026 +0000 SnapAPI v2.0: Screenshot and document conversion API diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed235bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +api-keys.json +analytics.json +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0114f2 --- /dev/null +++ b/README.md @@ -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 diff --git a/landing.html b/landing.html new file mode 100644 index 0000000..367c4a5 --- /dev/null +++ b/landing.html @@ -0,0 +1,568 @@ + + + + + +SnapAPI - Screenshots, PDFs & Markdown Conversion + + + + + + + + + + + + + +
+
+
+

SnapAPI

+

Screenshots, PDFs, and Markdown conversion. One API for all your document generation needs.

+ Online + v2.0 + PNG / JPEG / PDF + Markdown +
+ +
+ + +
+
+

Document Generation Suite

+
+
+

URL Screenshots

+

Capture any webpage as PNG, JPEG, or PDF. Custom viewports, full-page support.

+
+
+

Markdown to PDF

+

Convert Markdown to beautifully styled PDFs. Light and dark themes, custom margins.

+
+
+

Markdown to Image

+

Render Markdown as PNG or JPEG images. Perfect for social cards and previews.

+
+
+

Markdown to HTML

+

Convert Markdown to styled HTML. Free endpoint, no auth required.

+
+
+ +

Get started in 30 seconds

+

1. Get your API key

+

Contact us or use the demo key below for testing (100 captures/month).

+ +

2. Capture a screenshot

+
curl "https://YOUR_HOST/screenshot-api/api/capture?url=https://example.com" \
+  -H "X-API-Key: YOUR_API_KEY" \
+  -o screenshot.png
+ +

3. Convert Markdown to PDF

+
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
+ +

Node.js Example

+
// 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()));
+ +

Python Example

+
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)
+
+
+ + +
+
+

Endpoints

+ +

GET /api/capture

+

Capture a screenshot or PDF from a URL.

+ + + + + + + + + + + + + + +
ParameterTypeDescription
urlstringRequired. The URL to capture.
formatstringpng (default), jpeg, or pdf
widthintViewport width, 320-3840. Default: 1280
heightintViewport height, 200-2160. Default: 800
fullPageboolCapture the full scrollable page
delayintWait ms after page load (max 10000)
qualityintJPEG quality, 1-100. Default: 80
paperSizestringPDF paper size: A4, Letter, Legal, etc.
landscapeboolPDF landscape orientation
userAgentstringCustom User-Agent header
timeoutintNavigation timeout ms (5000-60000)
+ +

POST /api/md2pdf

+

Convert Markdown to a styled PDF document. Requires authentication.

+ + + + + + + + + +
Body FieldTypeDescription
markdownstringRequired. Markdown content to convert.
themestringlight (default) or dark
paperSizestringA4 (default), Letter, Legal, Tabloid
landscapeboolLandscape orientation. Default: false
fontSizestringBase font size. Default: "16px"
marginsobject{ top, bottom, left, right } in CSS units
+ +

POST /api/md2png

+

Convert Markdown to a PNG or JPEG image. Requires authentication.

+ + + + + + + + + +
Body FieldTypeDescription
markdownstringRequired. Markdown content to convert.
themestringlight (default) or dark
formatstringpng (default) or jpeg
widthintImage width, 320-3840. Default: 1280
fontSizestringBase font size. Default: "16px"
qualityintJPEG quality, 1-100. Default: 85
+ +

POST /api/md2html

+

Convert Markdown to styled HTML. No authentication required.

+ + + + + + + +
Body FieldTypeDescription
markdownstringRequired. Markdown content to convert.
themestringlight (default) or dark
fontSizestringBase font size. Default: "16px"
widthstringMax content width. Default: "800px"
+ +

Authentication

+

Pass your API key via the X-API-Key header or the api_key query parameter. The /api/md2html endpoint does not require authentication.

+ +

Error Responses

+ + + + + + + +
StatusMeaning
400Missing/invalid parameters, body too large (max 1MB)
401Missing or invalid API key
429Rate limit or monthly limit exceeded
500Conversion failed
503Server busy, max concurrent tasks reached
+ +

GET /api/status

+

Service health and statistics. No authentication required.

+
+
+ + +
+
+

Try Screenshot Capture

+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ + +
+
+

Try Markdown Conversion

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ + +
+
+

Pricing

+
+
+

Free

+
$0/mo
+
    +
  • 100 captures / month
  • +
  • Screenshots: PNG, JPEG, PDF
  • +
  • Markdown: PDF, PNG, HTML
  • +
  • 10 requests / minute
  • +
  • Light & dark themes
  • +
  • md2html: unlimited, no auth
  • +
+
+
+

Pro

+
$19/mo
+
    +
  • 5,000 captures / month
  • +
  • Screenshots: PNG, JPEG, PDF
  • +
  • Markdown: PDF, PNG, HTML
  • +
  • 60 requests / minute
  • +
  • Custom viewport up to 4K
  • +
  • Custom margins & paper sizes
  • +
  • Priority support
  • +
+
+
+

Contact us to get started with a Pro plan or discuss enterprise needs.

+
+
+ +
+

Built by an AI Agent (Claude) — transparent about AI authorship.

+

SnapAPI v2.0 · JSON API Docs · Status

+
+
+ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d335089 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a3cf53f --- /dev/null +++ b/package.json @@ -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" +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..74c1abe --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/screenshot-api.service b/screenshot-api.service new file mode 100644 index 0000000..1bf6f1a --- /dev/null +++ b/screenshot-api.service @@ -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 diff --git a/server.js b/server.js new file mode 100644 index 0000000..1ed4513 --- /dev/null +++ b/server.js @@ -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 ` + + + + + +${htmlContent} +`; +} + +// 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`); +});