fix: code-driven OpenAPI docs — replace static JSON with swagger-jsdoc
Some checks failed
Deploy to Staging / build-and-deploy (push) Failing after 10m13s
Some checks failed
Deploy to Staging / build-and-deploy (push) Failing after 10m13s
BREAKING: OpenAPI spec is now generated from JSDoc annotations on route handlers at startup, eliminating drift between code and documentation. What was wrong: - Static public/openapi.json was manually maintained and could drift - Missing endpoints: signup, billing (checkout/success/webhook) - Signup route was imported but never mounted (dead code) What was fixed: - Added swagger-jsdoc to generate OpenAPI spec from JSDoc on route files - Every route handler now has @openapi JSDoc annotation as source of truth - Spec served dynamically at GET /openapi.json (no static file) - Deleted public/openapi.json - Documented all missing endpoints (signup, billing x3) - Mounted /v1/signup route - All 9 screenshot params documented with types, ranges, defaults
This commit is contained in:
parent
a70157d0ae
commit
713cc30ac7
10 changed files with 700 additions and 124 deletions
282
package-lock.json
generated
282
package-lock.json
generated
|
|
@ -16,17 +16,63 @@
|
|||
"pg": "^8.13.0",
|
||||
"pino": "^10.3.1",
|
||||
"puppeteer": "^24.0.0",
|
||||
"stripe": "^17.0.0"
|
||||
"stripe": "^17.0.0",
|
||||
"swagger-jsdoc": "^6.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pg": "^8.0.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/openapi-schemas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-methods": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||
"@apidevtools/swagger-methods": "^3.0.2",
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"z-schema": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openapi-types": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
|
|
@ -492,6 +538,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
|
|
@ -612,6 +664,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||
|
|
@ -668,6 +726,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/swagger-jsdoc": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
|
||||
"integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
|
|
@ -780,6 +845,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
|
|
@ -905,6 +976,16 @@
|
|||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
|
|
@ -952,6 +1033,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -1006,6 +1093,15 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
|
|
@ -1036,6 +1132,12 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
|
@ -1155,6 +1257,18 @@
|
|||
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
@ -1529,6 +1643,12 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -1664,6 +1784,27 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -1829,6 +1970,17 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
|
|
@ -1898,6 +2050,26 @@
|
|||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
|
|
@ -1985,6 +2157,18 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
|
|
@ -2084,6 +2268,13 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||
|
|
@ -2178,6 +2369,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
|
|
@ -2919,6 +3119,38 @@
|
|||
"node": ">=12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc": {
|
||||
"version": "6.2.8",
|
||||
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "6.2.0",
|
||||
"doctrine": "3.0.0",
|
||||
"glob": "7.1.6",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
"swagger-parser": "^10.0.3",
|
||||
"yaml": "2.0.0-1"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
|
|
@ -3067,6 +3299,15 @@
|
|||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
|
@ -3144,6 +3385,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.0.0-1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
|
|
@ -3181,6 +3431,36 @@
|
|||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/z-schema": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"validator": "^13.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"z-schema": "bin/z-schema"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"commander": "^9.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/z-schema/node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@
|
|||
"pg": "^8.13.0",
|
||||
"pino": "^10.3.1",
|
||||
"puppeteer": "^24.0.0",
|
||||
"stripe": "^17.0.0"
|
||||
"stripe": "^17.0.0",
|
||||
"swagger-jsdoc": "^6.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pg": "^8.0.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "SnapAPI — Screenshot API",
|
||||
"description": "Convert any URL to a pixel-perfect screenshot. EU-hosted, GDPR compliant.\n\n## Authentication\nAPI screenshot requests require an API key:\n- `Authorization: Bearer YOUR_API_KEY` header, or\n- `X-API-Key: YOUR_API_KEY` header\n\n## Playground\nThe `/v1/playground` endpoint requires no authentication but returns watermarked screenshots (5 requests/hour per IP).\n\n## Rate Limits\n- 120 requests per minute per IP (global)\n- 5 requests per hour per IP (playground)\n- Monthly screenshot limits based on your plan tier (API)",
|
||||
"version": "0.3.0",
|
||||
"contact": {
|
||||
"name": "SnapAPI Support",
|
||||
"url": "https://snapapi.eu",
|
||||
"email": "support@snapapi.eu"
|
||||
},
|
||||
"license": {"name": "Proprietary"}
|
||||
},
|
||||
"servers": [{"url": "https://snapapi.eu", "description": "Production (EU — Germany)"}],
|
||||
"tags": [
|
||||
{"name": "Screenshots", "description": "Screenshot capture endpoints"},
|
||||
{"name": "Playground", "description": "Free demo (no auth, watermarked)"},
|
||||
{"name": "System", "description": "Health and status endpoints"}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/screenshot": {
|
||||
"post": {
|
||||
"tags": ["Screenshots"],
|
||||
"summary": "Take a screenshot (authenticated)",
|
||||
"description": "Capture a pixel-perfect, unwatermarked screenshot. Requires an API key.",
|
||||
"operationId": "takeScreenshot",
|
||||
"security": [{"BearerAuth": []}, {"ApiKeyAuth": []}],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/ScreenshotRequest"},
|
||||
"examples": {
|
||||
"simple": {"summary": "Simple screenshot", "value": {"url": "https://example.com"}},
|
||||
"hd_jpeg": {"summary": "HD JPEG", "value": {"url": "https://github.com", "format": "jpeg", "width": 1920, "height": 1080, "quality": 90}},
|
||||
"mobile": {"summary": "Mobile", "value": {"url": "https://example.com", "width": 375, "height": 812, "deviceScale": 2}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"description": "Screenshot captured", "content": {"image/png": {"schema": {"type": "string", "format": "binary"}}, "image/jpeg": {"schema": {"type": "string", "format": "binary"}}, "image/webp": {"schema": {"type": "string", "format": "binary"}}}},
|
||||
"400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"401": {"description": "Missing API key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"403": {"description": "Invalid API key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"429": {"description": "Rate/usage limit exceeded", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"503": {"description": "Service busy", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"504": {"description": "Timeout", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/playground": {
|
||||
"post": {
|
||||
"tags": ["Playground"],
|
||||
"summary": "Free demo screenshot (watermarked)",
|
||||
"description": "Take a watermarked screenshot without authentication. Limited to 5 requests per hour per IP, max 1920x1080 resolution. Perfect for evaluating the API before purchasing a plan.",
|
||||
"operationId": "playgroundScreenshot",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": {"type": "string", "format": "uri", "description": "URL to capture", "example": "https://example.com"},
|
||||
"format": {"type": "string", "enum": ["png", "jpeg", "webp"], "default": "png"},
|
||||
"width": {"type": "integer", "minimum": 320, "maximum": 1920, "default": 1280},
|
||||
"height": {"type": "integer", "minimum": 200, "maximum": 1080, "default": 800}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {"description": "Watermarked screenshot", "content": {"image/png": {"schema": {"type": "string", "format": "binary"}}}},
|
||||
"400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"429": {"description": "Rate limit (5/hr)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
|
||||
"503": {"description": "Service busy", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "Health check",
|
||||
"operationId": "healthCheck",
|
||||
"responses": {
|
||||
"200": {"description": "Healthy", "content": {"application/json": {"schema": {"type": "object", "properties": {"status": {"type": "string"}, "version": {"type": "string"}, "uptime": {"type": "number"}, "browser": {"type": "object"}}}}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"BearerAuth": {"type": "http", "scheme": "bearer"},
|
||||
"ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-API-Key"}
|
||||
},
|
||||
"schemas": {
|
||||
"ScreenshotRequest": {
|
||||
"type": "object",
|
||||
"required": ["url"],
|
||||
"properties": {
|
||||
"url": {"type": "string", "format": "uri", "example": "https://example.com"},
|
||||
"format": {"type": "string", "enum": ["png", "jpeg", "webp"], "default": "png"},
|
||||
"width": {"type": "integer", "minimum": 320, "maximum": 3840, "default": 1280},
|
||||
"height": {"type": "integer", "minimum": 200, "maximum": 2160, "default": 800},
|
||||
"fullPage": {"type": "boolean", "default": false},
|
||||
"quality": {"type": "integer", "minimum": 1, "maximum": 100, "default": 80},
|
||||
"waitForSelector": {"type": "string"},
|
||||
"deviceScale": {"type": "number", "minimum": 1, "maximum": 3, "default": 1},
|
||||
"delay": {"type": "integer", "minimum": 0, "maximum": 5000, "default": 0}
|
||||
}
|
||||
},
|
||||
"Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "string"}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/docs/openapi.ts
Normal file
53
src/docs/openapi.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import swaggerJsdoc from "swagger-jsdoc";
|
||||
|
||||
const options: swaggerJsdoc.Options = {
|
||||
definition: {
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "SnapAPI — Screenshot API",
|
||||
description:
|
||||
"Convert any URL to a pixel-perfect screenshot. EU-hosted, GDPR compliant.\n\n" +
|
||||
"## Authentication\n" +
|
||||
"API screenshot requests require an API key:\n" +
|
||||
"- `Authorization: Bearer YOUR_API_KEY` header, or\n" +
|
||||
"- `X-API-Key: YOUR_API_KEY` header\n\n" +
|
||||
"## Playground\n" +
|
||||
"The `/v1/playground` endpoint requires no authentication but returns watermarked screenshots (5 requests/hour per IP).\n\n" +
|
||||
"## Rate Limits\n" +
|
||||
"- 120 requests per minute per IP (global)\n" +
|
||||
"- 5 requests per hour per IP (playground)\n" +
|
||||
"- Monthly screenshot limits based on your plan tier (API)",
|
||||
version: "0.3.0",
|
||||
contact: {
|
||||
name: "SnapAPI Support",
|
||||
url: "https://snapapi.eu",
|
||||
email: "support@snapapi.eu",
|
||||
},
|
||||
license: { name: "Proprietary" },
|
||||
},
|
||||
servers: [{ url: "https://snapapi.eu", description: "Production (EU — Germany)" }],
|
||||
tags: [
|
||||
{ name: "Screenshots", description: "Screenshot capture endpoints" },
|
||||
{ name: "Playground", description: "Free demo (no auth, watermarked)" },
|
||||
{ name: "Signup", description: "Account creation" },
|
||||
{ name: "Billing", description: "Subscription and payment management" },
|
||||
{ name: "System", description: "Health and status endpoints" },
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: { type: "http", scheme: "bearer" },
|
||||
ApiKeyAuth: { type: "apiKey", in: "header", name: "X-API-Key" },
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: "object",
|
||||
required: ["error"],
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: ["./src/routes/*.ts"],
|
||||
};
|
||||
|
||||
export const openapiSpec = swaggerJsdoc(options);
|
||||
10
src/index.ts
10
src/index.ts
|
|
@ -16,6 +16,8 @@ import { loadKeys, getAllKeys } from "./services/keys.js";
|
|||
import { initDatabase, pool } from "./services/db.js";
|
||||
import { billingRouter } from "./routes/billing.js";
|
||||
import { statusRouter } from "./routes/status.js";
|
||||
import { signupRouter } from "./routes/signup.js";
|
||||
import { openapiSpec } from "./docs/openapi.js";
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || "3100", 10);
|
||||
|
|
@ -93,6 +95,7 @@ app.use("/health", healthRouter);
|
|||
app.use("/v1/billing", billingRouter);
|
||||
app.use("/status", statusRouter);
|
||||
app.use("/v1/playground", playgroundRouter);
|
||||
app.use("/v1/signup", signupRouter);
|
||||
|
||||
// Authenticated routes
|
||||
app.use("/v1/screenshot", authMiddleware, usageMiddleware, screenshotRouter);
|
||||
|
|
@ -110,7 +113,12 @@ app.get("/api", (_req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Swagger docs
|
||||
// OpenAPI spec (code-generated)
|
||||
app.get("/openapi.json", (_req, res) => {
|
||||
res.json(openapiSpec);
|
||||
});
|
||||
|
||||
// Swagger docs UI
|
||||
app.get("/docs", (_req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/docs.html"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,7 +122,49 @@ async function isSnapAPIEvent(event: Stripe.Event): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
// POST /v1/billing/checkout
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/billing/checkout:
|
||||
* post:
|
||||
* tags: [Billing]
|
||||
* summary: Create a Stripe checkout session
|
||||
* description: Start a subscription checkout flow. Returns a Stripe URL to redirect the user to.
|
||||
* operationId: billingCheckout
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [plan]
|
||||
* properties:
|
||||
* plan:
|
||||
* type: string
|
||||
* enum: [starter, pro, business]
|
||||
* description: Subscription plan to purchase
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Checkout session created
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* url:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: Stripe Checkout URL — redirect the user here
|
||||
* 400:
|
||||
* description: Invalid plan
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 500:
|
||||
* description: Checkout creation failed
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
*/
|
||||
router.post("/checkout", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { plan } = req.body;
|
||||
|
|
@ -149,7 +191,32 @@ router.post("/checkout", async (req: Request, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// GET /v1/billing/success
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/billing/success:
|
||||
* get:
|
||||
* tags: [Billing]
|
||||
* summary: Post-checkout success page
|
||||
* description: Stripe redirects here after successful payment. Provisions the API key and shows it to the user.
|
||||
* operationId: billingSuccess
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: session_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Stripe checkout session ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: HTML page with API key
|
||||
* content:
|
||||
* text/html:
|
||||
* schema: { type: string }
|
||||
* 400:
|
||||
* description: Missing session_id
|
||||
* 500:
|
||||
* description: Error retrieving session
|
||||
*/
|
||||
router.get("/success", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const sessionId = req.query.session_id as string;
|
||||
|
|
@ -204,7 +271,36 @@ Use it with <code>X-API-Key</code> header or <code>?key=</code> param.<br><br>
|
|||
}
|
||||
});
|
||||
|
||||
// POST /v1/billing/webhook
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/billing/webhook:
|
||||
* post:
|
||||
* tags: [Billing]
|
||||
* summary: Stripe webhook receiver
|
||||
* description: >
|
||||
* Receives Stripe webhook events for subscription lifecycle management.
|
||||
* Handles checkout.session.completed, customer.subscription.updated,
|
||||
* customer.subscription.deleted, and customer.updated events.
|
||||
* operationId: billingWebhook
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* description: Raw Stripe event payload (verified via signature)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Webhook processed
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* received: { type: boolean }
|
||||
* 400:
|
||||
* description: Invalid signature or payload
|
||||
*/
|
||||
router.post("/webhook", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const sig = req.headers["stripe-signature"] as string;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,26 @@ import { getPoolStats } from "../services/browser.js";
|
|||
|
||||
export const healthRouter = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /health:
|
||||
* get:
|
||||
* tags: [System]
|
||||
* summary: Health check
|
||||
* operationId: healthCheck
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Service is healthy
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status: { type: string, example: ok }
|
||||
* version: { type: string }
|
||||
* uptime: { type: number, description: Uptime in seconds }
|
||||
* browser: { type: object, description: Browser pool stats }
|
||||
*/
|
||||
healthRouter.get("/", (_req, res) => {
|
||||
const pool = getPoolStats();
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -16,6 +16,73 @@ const playgroundLimiter = rateLimit({
|
|||
keyGenerator: (req) => req.ip || req.socket.remoteAddress || "unknown",
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/playground:
|
||||
* post:
|
||||
* tags: [Playground]
|
||||
* summary: Free demo screenshot (watermarked)
|
||||
* description: >
|
||||
* Take a watermarked screenshot without authentication.
|
||||
* Limited to 5 requests per hour per IP, max 1920×1080 resolution.
|
||||
* Perfect for evaluating the API before purchasing a plan.
|
||||
* operationId: playgroundScreenshot
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [url]
|
||||
* properties:
|
||||
* url:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: URL to capture
|
||||
* example: "https://example.com"
|
||||
* format:
|
||||
* type: string
|
||||
* enum: [png, jpeg, webp]
|
||||
* default: png
|
||||
* description: Output image format
|
||||
* width:
|
||||
* type: integer
|
||||
* minimum: 320
|
||||
* maximum: 1920
|
||||
* default: 1280
|
||||
* description: Viewport width (clamped to 1920 max)
|
||||
* height:
|
||||
* type: integer
|
||||
* minimum: 200
|
||||
* maximum: 1080
|
||||
* default: 800
|
||||
* description: Viewport height (clamped to 1080 max)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Watermarked screenshot image
|
||||
* content:
|
||||
* image/png:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/jpeg:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/webp:
|
||||
* schema: { type: string, format: binary }
|
||||
* 400:
|
||||
* description: Invalid request
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 429:
|
||||
* description: Rate limit exceeded (5/hr)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 503:
|
||||
* description: Service busy
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
*/
|
||||
playgroundRouter.post("/", playgroundLimiter, async (req, res) => {
|
||||
const { url, format, width, height } = req.body;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,123 @@ import logger from "../services/logger.js";
|
|||
|
||||
export const screenshotRouter = Router();
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/screenshot:
|
||||
* post:
|
||||
* tags: [Screenshots]
|
||||
* summary: Take a screenshot (authenticated)
|
||||
* description: Capture a pixel-perfect, unwatermarked screenshot. Requires an API key.
|
||||
* operationId: takeScreenshot
|
||||
* security:
|
||||
* - BearerAuth: []
|
||||
* - ApiKeyAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [url]
|
||||
* properties:
|
||||
* url:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: URL to capture
|
||||
* example: "https://example.com"
|
||||
* format:
|
||||
* type: string
|
||||
* enum: [png, jpeg, webp]
|
||||
* default: png
|
||||
* description: Output image format
|
||||
* width:
|
||||
* type: integer
|
||||
* minimum: 320
|
||||
* maximum: 3840
|
||||
* default: 1280
|
||||
* description: Viewport width in pixels
|
||||
* height:
|
||||
* type: integer
|
||||
* minimum: 200
|
||||
* maximum: 2160
|
||||
* default: 800
|
||||
* description: Viewport height in pixels
|
||||
* fullPage:
|
||||
* type: boolean
|
||||
* default: false
|
||||
* description: Capture full scrollable page instead of viewport only
|
||||
* quality:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 80
|
||||
* description: JPEG/WebP quality (ignored for PNG)
|
||||
* waitForSelector:
|
||||
* type: string
|
||||
* description: CSS selector to wait for before capturing (e.g. "#main-content")
|
||||
* deviceScale:
|
||||
* type: number
|
||||
* minimum: 1
|
||||
* maximum: 3
|
||||
* default: 1
|
||||
* description: Device scale factor (2 = Retina)
|
||||
* delay:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* maximum: 5000
|
||||
* default: 0
|
||||
* description: Extra delay in ms after page load before capturing
|
||||
* examples:
|
||||
* simple:
|
||||
* summary: Simple screenshot
|
||||
* value: { "url": "https://example.com" }
|
||||
* hd_jpeg:
|
||||
* summary: HD JPEG
|
||||
* value: { "url": "https://github.com", "format": "jpeg", "width": 1920, "height": 1080, "quality": 90 }
|
||||
* mobile:
|
||||
* summary: Mobile viewport
|
||||
* value: { "url": "https://example.com", "width": 375, "height": 812, "deviceScale": 2 }
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Screenshot image binary
|
||||
* content:
|
||||
* image/png:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/jpeg:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/webp:
|
||||
* schema: { type: string, format: binary }
|
||||
* 400:
|
||||
* description: Invalid request (bad URL, blocked domain, etc.)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 401:
|
||||
* description: Missing API key
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 403:
|
||||
* description: Invalid API key
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 429:
|
||||
* description: Rate or usage limit exceeded
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 503:
|
||||
* description: Service busy (queue full)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 504:
|
||||
* description: Screenshot timed out
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
*/
|
||||
screenshotRouter.post("/", async (req: any, res) => {
|
||||
const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, delay } = req.body;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,57 @@ import logger from "../services/logger.js";
|
|||
export const signupRouter = Router();
|
||||
|
||||
// Simple signup: email → instant API key (no verification for now)
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/signup/free:
|
||||
* post:
|
||||
* tags: [Signup]
|
||||
* summary: Create a free account
|
||||
* description: Sign up with an email to get a free API key (100 screenshots/month).
|
||||
* operationId: signupFree
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [email]
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: Your email address
|
||||
* example: "user@example.com"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: API key created
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* apiKey:
|
||||
* type: string
|
||||
* description: Your new API key
|
||||
* tier:
|
||||
* type: string
|
||||
* example: free
|
||||
* limit:
|
||||
* type: integer
|
||||
* example: 100
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Invalid email
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 500:
|
||||
* description: Signup failed
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
*/
|
||||
signupRouter.post("/free", async (req, res) => {
|
||||
const { email } = req.body;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue