From ae91cecbeaab9dc690266562433bfc819c128beb Mon Sep 17 00:00:00 2001 From: vally111 Date: Sun, 26 Jul 2026 03:16:20 +0000 Subject: [PATCH] feat: add Swagger UI documentation at /api-docs - Install swagger-ui-express@5.0.1 and swagger-jsdoc@6.2.8 - Add src/swagger.js with OpenAPI 3.0 spec (schemas, tags, servers) - Add @swagger JSDoc comments to GET /federation, POST /register, GET /lookup, GET /users, and GET /health - Mount Swagger UI at /api-docs - Fix pre-existing ESLint error: req was shadowed as _req in global error handler --- stellar-payment-platform/package-lock.json | 288 +++++++++++++++- stellar-payment-platform/package.json | 2 + stellar-payment-platform/server.js | 366 ++++++++++++++++++++- stellar-payment-platform/src/swagger.js | 235 +++++++++++++ 4 files changed, 873 insertions(+), 18 deletions(-) create mode 100644 stellar-payment-platform/src/swagger.js diff --git a/stellar-payment-platform/package-lock.json b/stellar-payment-platform/package-lock.json index b2d4279e..a8e63fac 100644 --- a/stellar-payment-platform/package-lock.json +++ b/stellar-payment-platform/package-lock.json @@ -25,6 +25,8 @@ "rate-limit-redis": "^4.2.0", "redis": "^4.7.0", "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "uuid": "^9.0.1", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", @@ -37,6 +39,78 @@ "tsx": "4.23.1" } }, + "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/json-schema-ref-parser/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1437,6 +1511,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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/@noble/ed25519": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.1.0.tgz", @@ -1670,6 +1750,13 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -1851,6 +1938,12 @@ "@types/istanbul-lib-report": "*" } }, + "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": "26.0.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", @@ -2267,7 +2360,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, "license": "MIT" }, "node_modules/base32.js": { @@ -2390,7 +2482,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2651,6 +2742,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", @@ -2997,7 +3094,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, "license": "MIT" }, "node_modules/confbox": { @@ -3441,6 +3537,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "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/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -3742,6 +3850,15 @@ "node": ">=4" } }, + "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/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4153,7 +4270,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { @@ -4658,7 +4774,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.", - "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -5874,6 +5989,26 @@ "node": ">=8" } }, + "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/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -6110,7 +6245,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6609,6 +6743,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6728,7 +6869,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7975,6 +8115,92 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-jsdoc/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/swagger-jsdoc/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/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/swagger-ui-dist": { + "version": "5.32.11", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.11.tgz", + "integrity": "sha512-NEZzRuxHHQkbG3GCjNbzz+XRDoM7AztnXyzc2VCW5RXUvZBDW7bb3W29/SPfvav3yOzqnDTOLP2Xzbjxo0bldQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -8339,6 +8565,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8573,6 +8808,15 @@ "dev": true, "license": "ISC" }, + "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.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", @@ -8614,6 +8858,36 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "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" + } } } } diff --git a/stellar-payment-platform/package.json b/stellar-payment-platform/package.json index 08c1cd72..91b6d7d9 100644 --- a/stellar-payment-platform/package.json +++ b/stellar-payment-platform/package.json @@ -36,6 +36,8 @@ "rate-limit-redis": "^4.2.0", "redis": "^4.7.0", "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "uuid": "^9.0.1", "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", diff --git a/stellar-payment-platform/server.js b/stellar-payment-platform/server.js index e6d2c05b..2058f9b1 100644 --- a/stellar-payment-platform/server.js +++ b/stellar-payment-platform/server.js @@ -17,6 +17,8 @@ const {verifyMultiSignerThreshold,} = require('./src/multisigner-verifier'); const { poolGet, poolRun, poolAll } = require('./src/db'); const xss = require('xss'); const { Keypair, StrKey } = require('@stellar/stellar-sdk'); +const swaggerUi = require('swagger-ui-express'); +const { swaggerSpec } = require('./src/swagger'); dotenv.config(); @@ -108,6 +110,14 @@ app.use(rejectNestedObjects); // Enable HTTP response compression for responses exceeding 1KB (1024 bytes) app.use(compression({ threshold: 1024 })); +// --------------------------------------------------------------------------- +// Swagger UI — interactive API documentation available at /api-docs +// --------------------------------------------------------------------------- +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { + customSiteTitle: 'Stellar Tags API Docs', + swaggerOptions: { persistAuthorization: true }, +})); + scheduleCleanupJob(prisma); const USER_DATABASE = { @@ -227,6 +237,84 @@ const registerLocalUser = async ({ username, address }) => { ); }; +/** + * @swagger + * /federation: + * get: + * summary: Resolve a username tag or Stellar address via the federation protocol + * description: > + * Implements the Stellar federation protocol. Use `type=name` (default) to + * resolve a username tag (e.g. `alice*localhost`) to a Stellar address, or + * `type=id` to do the reverse lookup by Stellar public key. + * tags: + * - Federation + * parameters: + * - in: query + * name: q + * required: true + * schema: + * type: string + * description: > + * The value to look up. For `type=name` provide a username tag + * (e.g. `alice*localhost`). For `type=id` provide a Stellar public key. + * example: alice*localhost + * - in: query + * name: type + * required: false + * schema: + * type: string + * enum: [name, id] + * default: name + * description: > + * Federation query type. `name` resolves a tag to an address; + * `id` resolves an address to a tag. + * responses: + * 200: + * description: Record found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/FederationResponse' + * examples: + * nameResolution: + * summary: Resolve username tag → address + * value: + * stellar_address: alice*localhost + * account_id: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * withMemo: + * summary: Response with optional memo fields + * value: + * stellar_address: alice*localhost + * account_id: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * memo_type: text + * memo: payment-ref-001 + * 400: + * description: Missing `q` parameter + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: "Missing 'q' parameter" + * statusCode: 400 + * 404: + * description: Name tag or address not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: Name tag not found + * statusCode: 404 + * 500: + * description: Database lookup failed + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ app.get('/federation', etagCache, async (req, res, next) => { const { q, type } = req.query; const queryValue = typeof q === 'string' ? q.trim() : ''; @@ -384,16 +472,105 @@ const verifyFreighterRegistrationSignature = ({ }; /** - * Registration endpoint with multi-signer threshold verification - * - * For single-signer accounts: - * - Signature must be the account's public key or a registered signer - * - Basic validation of address format - * - * For multi-signer accounts (enterprise): - * - Fetches account signers and thresholds from Horizon - * - Validates that provided signature(s) meet minimum threshold - * - Ensures authorization requirements are satisfied + * @swagger + * /register: + * post: + * summary: Register a new username–address mapping + * description: > + * Associates a human-readable username tag with a Stellar public key. + * Optionally accepts a SEP-0053 signature for multi-signer threshold + * verification. The endpoint is idempotent for the same + * (username, address) pair when an idempotency key header is supplied. + * tags: + * - Registration + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterRequest' + * examples: + * minimal: + * summary: Minimal registration (no signature) + * value: + * username: alice + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * withMemo: + * summary: Registration with a text memo + * value: + * username: alice + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * memo_type: text + * memo: payment-ref-001 + * withSignature: + * summary: Registration with Freighter signature (SEP-0053) + * value: + * username: alice + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * signature: BASE64_ENCODED_SIGNATURE + * signerAddress: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * responses: + * 201: + * description: Registration successful + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterResponse' + * example: + * ok: true + * username: alice*localhost + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * federation_address: alice*localhost + * 400: + * description: > + * Missing or invalid fields (username, address, memo validation, + * reserved username, or secret key detected). + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: "Missing required fields: username and address are both required." + * statusCode: 400 + * 401: + * description: Signature verification failed + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 403: + * description: Reserved username + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: This username is reserved and cannot be registered. + * statusCode: 403 + * 409: + * description: Address or username already registered + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: Address already registered + * statusCode: 409 + * 415: + * description: Content-Type must be application/json + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * description: Database insertion or verification failed + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' */ app.post('/register', idempotencyMiddleware(redisClient), async (req, res, next) => { if (!req.is('application/json')) { @@ -575,6 +752,101 @@ app.post('/register', idempotencyMiddleware(redisClient), async (req, res, next) app.all('/register', (req, res) => res.status(405).json({ error: "Method Not Allowed" })); +/** + * @swagger + * /lookup: + * get: + * summary: Look up a username by Stellar address, or search users + * description: > + * Two modes of operation: + * 1. **Exact lookup** — supply `address` to retrieve the username registered + * to that Stellar public key. + * 2. **Paginated search** — supply `search` to find users whose username or + * address contains the given string (case-insensitive). + * + * Exactly one of `address` or `search` must be provided. + * tags: + * - Lookup + * parameters: + * - in: query + * name: address + * schema: + * type: string + * description: Stellar public key for exact reverse-lookup. + * example: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * - in: query + * name: search + * schema: + * type: string + * description: Partial username or address string for paginated search. + * example: alice + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * minimum: 1 + * description: Page number (search mode only). + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * minimum: 1 + * maximum: 100 + * description: Results per page (search mode only). + * responses: + * 200: + * description: Record(s) found + * content: + * application/json: + * schema: + * oneOf: + * - $ref: '#/components/schemas/LookupResponse' + * - $ref: '#/components/schemas/LookupPagedResponse' + * examples: + * exactLookup: + * summary: Exact address lookup + * value: + * username: alice*localhost + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * pagedSearch: + * summary: Paginated search results + * value: + * data: + * - username: alice*localhost + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * created_at: '2024-01-15T10:30:00.000Z' + * totalCount: 1 + * totalPages: 1 + * currentPage: 1 + * 400: + * description: Neither `address` nor `search` was provided + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: "Missing required parameter: provide 'address' for exact lookup or 'search' for paginated search" + * statusCode: 400 + * 404: + * description: No username registered for the given address + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * example: + * success: false + * error: Username not found for this address + * statusCode: 404 + * 500: + * description: Database lookup failed + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ app.get('/lookup', async (req, res, next) => { const address = typeof req.query.address === 'string' ? req.query.address.trim() : ''; const search = typeof req.query.search === 'string' ? req.query.search.trim() : ''; @@ -668,6 +940,60 @@ app.get('/lookup', async (req, res, next) => { } }); +/** + * @swagger + * /users: + * get: + * summary: List all registered users with optional search and pagination + * description: > + * Returns a paginated list of registered username–address pairs. + * Optionally filter by a partial username or address string. + * tags: + * - Lookup + * parameters: + * - in: query + * name: search + * schema: + * type: string + * description: Filter by partial username or address (case-insensitive). + * example: alice + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * minimum: 1 + * description: Page number. + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * minimum: 1 + * maximum: 100 + * description: Results per page. + * responses: + * 200: + * description: Paginated list of users + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LookupPagedResponse' + * example: + * data: + * - username: alice*localhost + * address: GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ + * created_at: '2024-01-15T10:30:00.000Z' + * totalCount: 1 + * totalPages: 1 + * currentPage: 1 + * 500: + * description: Database error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ app.get('/users', async (req, res, next) => { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10)); @@ -722,6 +1048,24 @@ app.get('/api/v1/time', (_req, res) => { res.status(200).json({ time: new Date().toISOString() }); }); +/** + * @swagger + * /health: + * get: + * summary: Server health check + * description: Returns status "ok" when the server is running. Useful for uptime monitoring and load-balancer health probes. + * tags: + * - Health + * responses: + * 200: + * description: Server is healthy + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthResponse' + * example: + * status: ok + */ app.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); @@ -737,7 +1081,7 @@ app.use((err, _req, _res, next) => { // Global error handling middleware // eslint-disable-next-line no-unused-vars -app.use((err, _req, res, _next) => { +app.use((err, req, res, _next) => { const statusCode = err.statusCode || 500; const errorMessage = err.message || 'Internal server error'; diff --git a/stellar-payment-platform/src/swagger.js b/stellar-payment-platform/src/swagger.js new file mode 100644 index 00000000..5c50d216 --- /dev/null +++ b/stellar-payment-platform/src/swagger.js @@ -0,0 +1,235 @@ +'use strict'; + +const swaggerJsdoc = require('swagger-jsdoc'); + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Stellar Tags API', + version: '1.0.0', + description: + 'REST API for the Stellar Tags payment platform. Provides username registration and federation lookups that map human-readable tags to Stellar addresses.', + contact: { + name: 'Stellar Tags', + url: 'https://stellar-tags.vercel.app', + }, + }, + servers: [ + { + url: 'http://localhost:{port}', + description: 'Local development server', + variables: { + port: { + default: '5000', + description: 'Port the server listens on (defaults to 5000)', + }, + }, + }, + { + url: 'https://stellar-tags-production.up.railway.app', + description: 'Production server', + }, + ], + tags: [ + { + name: 'Federation', + description: 'Stellar federation protocol — resolve username tags to Stellar addresses', + }, + { + name: 'Registration', + description: 'Register new username–address mappings', + }, + { + name: 'Lookup', + description: 'Reverse-lookup and search users by address or keyword', + }, + { + name: 'Health', + description: 'Server liveness check', + }, + ], + components: { + schemas: { + FederationResponse: { + type: 'object', + required: ['stellar_address', 'account_id'], + properties: { + stellar_address: { + type: 'string', + example: 'alice*stellar-tags-production.up.railway.app', + description: 'Fully-qualified Stellar federation address (username*domain)', + }, + account_id: { + type: 'string', + example: 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ', + description: 'Stellar public key (G…) associated with this tag', + }, + memo_type: { + type: 'string', + enum: ['text', 'id', 'hash'], + description: 'Type of Stellar memo attached to this account (omitted when absent)', + }, + memo: { + type: 'string', + description: 'Memo value matching memo_type (omitted when absent)', + }, + }, + }, + RegisterRequest: { + type: 'object', + required: ['username', 'address'], + properties: { + username: { + type: 'string', + minLength: 3, + example: 'alice', + description: + 'Desired username. Must be ≥ 3 characters. A domain suffix (*domain) is appended automatically if omitted.', + }, + address: { + type: 'string', + example: 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ', + description: 'Stellar public key (must start with G)', + }, + signature: { + type: 'string', + description: + 'Optional. Base64-encoded SEP-0053 signature or raw ed25519 public key for multi-signer verification.', + }, + signerAddress: { + type: 'string', + description: + 'Optional. Stellar public key of the signer when using Freighter signature flow.', + }, + memo_type: { + type: 'string', + enum: ['text', 'id', 'hash'], + description: 'Optional. Stellar memo type to associate with this registration.', + }, + memo: { + type: 'string', + description: 'Optional. Memo value (required when memo_type is provided).', + }, + }, + }, + RegisterResponse: { + type: 'object', + required: ['ok', 'username', 'address', 'federation_address'], + properties: { + ok: { + type: 'boolean', + example: true, + }, + username: { + type: 'string', + example: 'alice*localhost', + }, + address: { + type: 'string', + example: 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ', + }, + federation_address: { + type: 'string', + example: 'alice*localhost', + }, + memo_type: { + type: 'string', + description: 'Included only when memo was supplied at registration', + }, + memo: { + type: 'string', + description: 'Included only when memo was supplied at registration', + }, + verification: { + type: 'object', + description: 'Included only when a signature was provided', + properties: { + accountId: { type: 'string' }, + signerCount: { type: 'integer' }, + thresholdMet: { type: 'boolean' }, + requiredThreshold: { type: 'integer' }, + providedWeight: { type: 'integer' }, + }, + }, + }, + }, + LookupResponse: { + type: 'object', + required: ['username', 'address'], + properties: { + username: { + type: 'string', + example: 'alice*localhost', + }, + address: { + type: 'string', + example: 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ', + }, + }, + }, + LookupPagedResponse: { + type: 'object', + required: ['data', 'totalCount', 'totalPages', 'currentPage'], + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + username: { type: 'string', example: 'alice*localhost' }, + address: { + type: 'string', + example: 'GAPUQZH3WZUXHEMUGZN5ZYU4D4GHCFEMOGUINU6MF345GBD2QXNYYIEQ', + }, + created_at: { + type: 'string', + format: 'date-time', + example: '2024-01-15T10:30:00.000Z', + }, + }, + }, + }, + totalCount: { type: 'integer', example: 42 }, + totalPages: { type: 'integer', example: 5 }, + currentPage: { type: 'integer', example: 1 }, + }, + }, + HealthResponse: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + example: 'ok', + }, + }, + }, + ErrorResponse: { + type: 'object', + required: ['success', 'error'], + properties: { + success: { + type: 'boolean', + example: false, + }, + error: { + type: 'string', + example: 'Missing required parameter', + }, + statusCode: { + type: 'integer', + example: 400, + }, + }, + }, + }, + }, + }, + // Scan server.js for JSDoc @swagger annotations + apis: ['./server.js'], +}; + +const swaggerSpec = swaggerJsdoc(options); + +module.exports = { swaggerSpec };